Intial commit
This commit is contained in:
7
.gitignore
vendored
Normal file
7
.gitignore
vendored
Normal file
@@ -0,0 +1,7 @@
|
||||
target/
|
||||
.m2/
|
||||
.idea/
|
||||
*.iml
|
||||
*.db
|
||||
*.db-shm
|
||||
*.db-wal
|
||||
39
README.md
Normal file
39
README.md
Normal file
@@ -0,0 +1,39 @@
|
||||
# MultiSpawn-Core
|
||||
|
||||
Аддон для Paper 1.21.11 и Multiverse-Core 5.7.1. Для каждой пары
|
||||
«UUID игрока + мир» сохраняется отдельная последняя позиция. При возвращении
|
||||
в мир или перезаходе игрок появляется там, где остановился.
|
||||
|
||||
## Что делает плагин
|
||||
|
||||
- хранит координаты, поворот головы и наклон в SQLite;
|
||||
- пишет в базу асинхронно, не задерживая серверный поток;
|
||||
- работает только с мирами, импортированными в Multiverse-Core;
|
||||
- не вмешивается в Nether/End-порталы, End Gateway и portal-события
|
||||
Multiverse-Core;
|
||||
- по умолчанию игнорирует `world`, `world_nether` и `world_the_end`.
|
||||
|
||||
База создаётся по пути:
|
||||
`plugins/MultiSpawn-Core/positions.db`.
|
||||
|
||||
## Требования
|
||||
|
||||
- Paper 1.21.11;
|
||||
- Java 21 или новее;
|
||||
- Multiverse-Core 5.7.1.
|
||||
|
||||
## Сборка
|
||||
|
||||
```shell
|
||||
mvn clean package
|
||||
```
|
||||
|
||||
Готовый JAR: `target/MultiSpawn-Core-1.0.0.jar`.
|
||||
|
||||
## Настройка
|
||||
|
||||
В `config.yml` доступны белый список `enabled-worlds`, чёрный список
|
||||
`disabled-worlds` и переключатель `include-default-worlds`. Обычные миры
|
||||
начнут сохраняться только после включения `include-default-worlds: true`.
|
||||
|
||||
Перезагрузка настроек: `/multispawn reload`.
|
||||
101
pom.xml
Normal file
101
pom.xml
Normal file
@@ -0,0 +1,101 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project xmlns="http://maven.apache.org/POM/4.0.0"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
|
||||
<groupId>ru.multispawn</groupId>
|
||||
<artifactId>multispawn-core</artifactId>
|
||||
<version>1.0.0</version>
|
||||
<name>MultiSpawn-Core</name>
|
||||
<description>Per-world player return positions for Multiverse-Core.</description>
|
||||
|
||||
<properties>
|
||||
<maven.compiler.release>21</maven.compiler.release>
|
||||
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
|
||||
</properties>
|
||||
|
||||
<repositories>
|
||||
<repository>
|
||||
<id>papermc</id>
|
||||
<url>https://repo.papermc.io/repository/maven-public/</url>
|
||||
</repository>
|
||||
<repository>
|
||||
<id>onarandombox</id>
|
||||
<url>https://repo.onarandombox.com/content/groups/public/</url>
|
||||
</repository>
|
||||
</repositories>
|
||||
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>io.papermc.paper</groupId>
|
||||
<artifactId>paper-api</artifactId>
|
||||
<version>1.21.11-R0.1-SNAPSHOT</version>
|
||||
<scope>provided</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.mvplugins.multiverse.core</groupId>
|
||||
<artifactId>multiverse-core</artifactId>
|
||||
<version>5.7.1</version>
|
||||
<scope>provided</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.xerial</groupId>
|
||||
<artifactId>sqlite-jdbc</artifactId>
|
||||
<version>3.50.3.0</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.junit.jupiter</groupId>
|
||||
<artifactId>junit-jupiter</artifactId>
|
||||
<version>5.13.4</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
|
||||
<build>
|
||||
<finalName>MultiSpawn-Core-${project.version}</finalName>
|
||||
<plugins>
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-compiler-plugin</artifactId>
|
||||
<version>3.14.0</version>
|
||||
</plugin>
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-surefire-plugin</artifactId>
|
||||
<version>3.5.3</version>
|
||||
</plugin>
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-shade-plugin</artifactId>
|
||||
<version>3.6.0</version>
|
||||
<executions>
|
||||
<execution>
|
||||
<phase>package</phase>
|
||||
<goals>
|
||||
<goal>shade</goal>
|
||||
</goals>
|
||||
<configuration>
|
||||
<createDependencyReducedPom>false</createDependencyReducedPom>
|
||||
<!-- sqlite-jdbc loads platform natives dynamically; keep them all. -->
|
||||
<minimizeJar>false</minimizeJar>
|
||||
<filters>
|
||||
<filter>
|
||||
<artifact>*:*</artifact>
|
||||
<excludes>
|
||||
<exclude>META-INF/*.SF</exclude>
|
||||
<exclude>META-INF/*.DSA</exclude>
|
||||
<exclude>META-INF/*.RSA</exclude>
|
||||
</excludes>
|
||||
</filter>
|
||||
</filters>
|
||||
<transformers>
|
||||
<transformer implementation="org.apache.maven.plugins.shade.resource.ServicesResourceTransformer"/>
|
||||
</transformers>
|
||||
</configuration>
|
||||
</execution>
|
||||
</executions>
|
||||
</plugin>
|
||||
</plugins>
|
||||
</build>
|
||||
</project>
|
||||
82
src/main/java/ru/multispawn/core/MultiSpawnPlugin.java
Normal file
82
src/main/java/ru/multispawn/core/MultiSpawnPlugin.java
Normal file
@@ -0,0 +1,82 @@
|
||||
package ru.multispawn.core;
|
||||
|
||||
import org.bukkit.command.Command;
|
||||
import org.bukkit.command.CommandSender;
|
||||
import org.bukkit.plugin.java.JavaPlugin;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.mvplugins.multiverse.core.MultiverseCoreApi;
|
||||
import ru.multispawn.core.storage.PositionStore;
|
||||
|
||||
import java.io.File;
|
||||
import java.sql.SQLException;
|
||||
import java.util.logging.Level;
|
||||
|
||||
public final class MultiSpawnPlugin extends JavaPlugin {
|
||||
private PositionStore positionStore;
|
||||
private WorldPolicy worldPolicy;
|
||||
private PlayerPositionListener listener;
|
||||
|
||||
@Override
|
||||
public void onEnable() {
|
||||
saveDefaultConfig();
|
||||
|
||||
final MultiverseCoreApi multiverse;
|
||||
try {
|
||||
multiverse = MultiverseCoreApi.get();
|
||||
} catch (IllegalStateException exception) {
|
||||
getLogger().log(Level.SEVERE, "Multiverse-Core API is not available.", exception);
|
||||
getServer().getPluginManager().disablePlugin(this);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!getDataFolder().exists() && !getDataFolder().mkdirs()) {
|
||||
getLogger().severe("Could not create plugin data folder.");
|
||||
getServer().getPluginManager().disablePlugin(this);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
File database = new File(getDataFolder(), "positions.db");
|
||||
positionStore = new PositionStore(this, database.toPath());
|
||||
} catch (SQLException exception) {
|
||||
getLogger().log(Level.SEVERE, "Could not open SQLite database.", exception);
|
||||
getServer().getPluginManager().disablePlugin(this);
|
||||
return;
|
||||
}
|
||||
|
||||
worldPolicy = new WorldPolicy(multiverse, getConfig());
|
||||
listener = new PlayerPositionListener(this, positionStore, worldPolicy);
|
||||
getServer().getPluginManager().registerEvents(listener, this);
|
||||
|
||||
getLogger().info("Enabled with " + positionStore.cachedPositionCount()
|
||||
+ " cached player positions.");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDisable() {
|
||||
if (listener != null && positionStore != null) {
|
||||
getServer().getOnlinePlayers().forEach(listener::saveCurrent);
|
||||
}
|
||||
if (positionStore != null) {
|
||||
positionStore.close();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onCommand(
|
||||
@NotNull CommandSender sender,
|
||||
@NotNull Command command,
|
||||
@NotNull String label,
|
||||
@NotNull String[] args
|
||||
) {
|
||||
if (args.length == 1 && args[0].equalsIgnoreCase("reload")) {
|
||||
reloadConfig();
|
||||
worldPolicy.reload(getConfig());
|
||||
sender.sendMessage("§aMultiSpawn-Core: конфигурация перезагружена.");
|
||||
return true;
|
||||
}
|
||||
|
||||
sender.sendMessage("§eИспользование: /" + label + " reload");
|
||||
return true;
|
||||
}
|
||||
}
|
||||
131
src/main/java/ru/multispawn/core/PlayerPositionListener.java
Normal file
131
src/main/java/ru/multispawn/core/PlayerPositionListener.java
Normal file
@@ -0,0 +1,131 @@
|
||||
package ru.multispawn.core;
|
||||
|
||||
import org.bukkit.Location;
|
||||
import org.bukkit.entity.Player;
|
||||
import org.bukkit.event.EventHandler;
|
||||
import org.bukkit.event.EventPriority;
|
||||
import org.bukkit.event.Listener;
|
||||
import org.bukkit.event.player.PlayerJoinEvent;
|
||||
import org.bukkit.event.player.PlayerPortalEvent;
|
||||
import org.bukkit.event.player.PlayerQuitEvent;
|
||||
import org.bukkit.event.player.PlayerTeleportEvent;
|
||||
import org.mvplugins.multiverse.core.event.MVPlayerTouchedPortalEvent;
|
||||
import ru.multispawn.core.model.StoredPosition;
|
||||
import ru.multispawn.core.storage.PositionStore;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.UUID;
|
||||
|
||||
public final class PlayerPositionListener implements Listener {
|
||||
private static final long PORTAL_TOUCH_WINDOW_NANOS = 1_000_000_000L;
|
||||
|
||||
private final MultiSpawnPlugin plugin;
|
||||
private final PositionStore store;
|
||||
private final WorldPolicy worldPolicy;
|
||||
private final Map<UUID, Long> recentPortalTouches = new HashMap<>();
|
||||
|
||||
public PlayerPositionListener(MultiSpawnPlugin plugin, PositionStore store, WorldPolicy worldPolicy) {
|
||||
this.plugin = plugin;
|
||||
this.store = store;
|
||||
this.worldPolicy = worldPolicy;
|
||||
}
|
||||
|
||||
/**
|
||||
* Covers the portal path exposed by the Multiverse-Core API itself.
|
||||
*/
|
||||
@EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true)
|
||||
public void onMultiversePortalTouch(MVPlayerTouchedPortalEvent event) {
|
||||
recentPortalTouches.put(event.getPlayer().getUniqueId(), System.nanoTime());
|
||||
}
|
||||
|
||||
@EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = true)
|
||||
public void onTeleport(PlayerTeleportEvent event) {
|
||||
Location from = event.getFrom();
|
||||
Location to = event.getTo();
|
||||
if (to == null || from.getWorld() == null || to.getWorld() == null
|
||||
|| from.getWorld().equals(to.getWorld())) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (isPortalTransition(event)) {
|
||||
recentPortalTouches.remove(event.getPlayer().getUniqueId());
|
||||
return;
|
||||
}
|
||||
|
||||
save(event.getPlayer(), from);
|
||||
|
||||
if (!plugin.getConfig().getBoolean("restore.on-world-change", true)
|
||||
|| !worldPolicy.isHandled(to.getWorld())) {
|
||||
return;
|
||||
}
|
||||
|
||||
store.find(event.getPlayer().getUniqueId(), to.getWorld().getName())
|
||||
.map(StoredPosition::toLocation)
|
||||
.ifPresent(event::setTo);
|
||||
}
|
||||
|
||||
@EventHandler(priority = EventPriority.MONITOR)
|
||||
public void onJoin(PlayerJoinEvent event) {
|
||||
if (!plugin.getConfig().getBoolean("restore.on-join", true)) {
|
||||
return;
|
||||
}
|
||||
|
||||
Player player = event.getPlayer();
|
||||
plugin.getServer().getScheduler().runTask(plugin, () -> {
|
||||
if (!player.isOnline() || !worldPolicy.isHandled(player.getWorld())) {
|
||||
return;
|
||||
}
|
||||
store.find(player.getUniqueId(), player.getWorld().getName())
|
||||
.map(StoredPosition::toLocation)
|
||||
.ifPresent(location ->
|
||||
player.teleport(location, PlayerTeleportEvent.TeleportCause.PLUGIN));
|
||||
});
|
||||
}
|
||||
|
||||
@EventHandler(priority = EventPriority.MONITOR)
|
||||
public void onQuit(PlayerQuitEvent event) {
|
||||
recentPortalTouches.remove(event.getPlayer().getUniqueId());
|
||||
saveCurrent(event.getPlayer());
|
||||
}
|
||||
|
||||
public void saveCurrent(Player player) {
|
||||
save(player, player.getLocation());
|
||||
}
|
||||
|
||||
private void save(Player player, Location location) {
|
||||
if (location.getWorld() != null && worldPolicy.isHandled(location.getWorld())) {
|
||||
store.save(player.getUniqueId(), StoredPosition.from(location));
|
||||
}
|
||||
}
|
||||
|
||||
private boolean isPortalTransition(PlayerTeleportEvent event) {
|
||||
if (event instanceof PlayerPortalEvent) {
|
||||
return true;
|
||||
}
|
||||
|
||||
PlayerTeleportEvent.TeleportCause cause = event.getCause();
|
||||
if (cause == PlayerTeleportEvent.TeleportCause.NETHER_PORTAL
|
||||
&& plugin.getConfig().getBoolean("portals.ignore-nether-portals", true)) {
|
||||
return true;
|
||||
}
|
||||
if (cause == PlayerTeleportEvent.TeleportCause.END_PORTAL
|
||||
&& plugin.getConfig().getBoolean("portals.ignore-end-portals", true)) {
|
||||
return true;
|
||||
}
|
||||
if (cause == PlayerTeleportEvent.TeleportCause.END_GATEWAY
|
||||
&& plugin.getConfig().getBoolean("portals.ignore-end-gateways", true)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
Long touchedAt = recentPortalTouches.get(event.getPlayer().getUniqueId());
|
||||
if (touchedAt == null) {
|
||||
return false;
|
||||
}
|
||||
if (System.nanoTime() - touchedAt <= PORTAL_TOUCH_WINDOW_NANOS) {
|
||||
return true;
|
||||
}
|
||||
recentPortalTouches.remove(event.getPlayer().getUniqueId());
|
||||
return false;
|
||||
}
|
||||
}
|
||||
56
src/main/java/ru/multispawn/core/WorldPolicy.java
Normal file
56
src/main/java/ru/multispawn/core/WorldPolicy.java
Normal file
@@ -0,0 +1,56 @@
|
||||
package ru.multispawn.core;
|
||||
|
||||
import org.bukkit.World;
|
||||
import org.bukkit.configuration.file.FileConfiguration;
|
||||
import org.mvplugins.multiverse.core.MultiverseCoreApi;
|
||||
|
||||
import java.util.HashSet;
|
||||
import java.util.Locale;
|
||||
import java.util.Set;
|
||||
|
||||
/**
|
||||
* Decides which worlds may have per-player return positions.
|
||||
*/
|
||||
public final class WorldPolicy {
|
||||
private final MultiverseCoreApi multiverse;
|
||||
private boolean includeDefaultWorlds;
|
||||
private Set<String> defaultWorlds = Set.of();
|
||||
private Set<String> enabledWorlds = Set.of();
|
||||
private Set<String> disabledWorlds = Set.of();
|
||||
|
||||
public WorldPolicy(MultiverseCoreApi multiverse, FileConfiguration config) {
|
||||
this.multiverse = multiverse;
|
||||
reload(config);
|
||||
}
|
||||
|
||||
public void reload(FileConfiguration config) {
|
||||
includeDefaultWorlds = config.getBoolean("storage.include-default-worlds", false);
|
||||
defaultWorlds = normalized(config.getStringList("storage.default-worlds"));
|
||||
enabledWorlds = normalized(config.getStringList("storage.enabled-worlds"));
|
||||
disabledWorlds = normalized(config.getStringList("storage.disabled-worlds"));
|
||||
}
|
||||
|
||||
public boolean isHandled(World world) {
|
||||
String normalizedName = world.getName().toLowerCase(Locale.ROOT);
|
||||
|
||||
// The plugin is deliberately an addon: unmanaged Bukkit worlds are ignored.
|
||||
if (!multiverse.getWorldManager().getWorld(world.getName()).isDefined()) {
|
||||
return false;
|
||||
}
|
||||
if (disabledWorlds.contains(normalizedName)) {
|
||||
return false;
|
||||
}
|
||||
if (!enabledWorlds.isEmpty() && !enabledWorlds.contains(normalizedName)) {
|
||||
return false;
|
||||
}
|
||||
return includeDefaultWorlds || !defaultWorlds.contains(normalizedName);
|
||||
}
|
||||
|
||||
private static Set<String> normalized(Iterable<String> names) {
|
||||
Set<String> result = new HashSet<>();
|
||||
for (String name : names) {
|
||||
result.add(name.toLowerCase(Locale.ROOT));
|
||||
}
|
||||
return Set.copyOf(result);
|
||||
}
|
||||
}
|
||||
6
src/main/java/ru/multispawn/core/model/PositionKey.java
Normal file
6
src/main/java/ru/multispawn/core/model/PositionKey.java
Normal file
@@ -0,0 +1,6 @@
|
||||
package ru.multispawn.core.model;
|
||||
|
||||
import java.util.UUID;
|
||||
|
||||
public record PositionKey(UUID playerId, String worldName) {
|
||||
}
|
||||
33
src/main/java/ru/multispawn/core/model/StoredPosition.java
Normal file
33
src/main/java/ru/multispawn/core/model/StoredPosition.java
Normal file
@@ -0,0 +1,33 @@
|
||||
package ru.multispawn.core.model;
|
||||
|
||||
import org.bukkit.Bukkit;
|
||||
import org.bukkit.Location;
|
||||
import org.bukkit.World;
|
||||
|
||||
public record StoredPosition(
|
||||
String worldName,
|
||||
double x,
|
||||
double y,
|
||||
double z,
|
||||
float yaw,
|
||||
float pitch
|
||||
) {
|
||||
public static StoredPosition from(Location location) {
|
||||
if (location.getWorld() == null) {
|
||||
throw new IllegalArgumentException("Location must have a world");
|
||||
}
|
||||
return new StoredPosition(
|
||||
location.getWorld().getName(),
|
||||
location.getX(),
|
||||
location.getY(),
|
||||
location.getZ(),
|
||||
location.getYaw(),
|
||||
location.getPitch()
|
||||
);
|
||||
}
|
||||
|
||||
public Location toLocation() {
|
||||
World world = Bukkit.getWorld(worldName);
|
||||
return world == null ? null : new Location(world, x, y, z, yaw, pitch);
|
||||
}
|
||||
}
|
||||
107
src/main/java/ru/multispawn/core/storage/PositionRepository.java
Normal file
107
src/main/java/ru/multispawn/core/storage/PositionRepository.java
Normal file
@@ -0,0 +1,107 @@
|
||||
package ru.multispawn.core.storage;
|
||||
|
||||
import ru.multispawn.core.model.PositionKey;
|
||||
import ru.multispawn.core.model.StoredPosition;
|
||||
|
||||
import java.nio.file.Path;
|
||||
import java.sql.Connection;
|
||||
import java.sql.DriverManager;
|
||||
import java.sql.PreparedStatement;
|
||||
import java.sql.ResultSet;
|
||||
import java.sql.SQLException;
|
||||
import java.sql.Statement;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.UUID;
|
||||
|
||||
public final class PositionRepository implements AutoCloseable {
|
||||
private final Connection connection;
|
||||
|
||||
public PositionRepository(Path databasePath) throws SQLException {
|
||||
connection = DriverManager.getConnection("jdbc:sqlite:" + databasePath.toAbsolutePath());
|
||||
configure();
|
||||
createSchema();
|
||||
}
|
||||
|
||||
private void configure() throws SQLException {
|
||||
try (Statement statement = connection.createStatement()) {
|
||||
statement.execute("PRAGMA journal_mode=WAL");
|
||||
statement.execute("PRAGMA synchronous=NORMAL");
|
||||
statement.execute("PRAGMA busy_timeout=5000");
|
||||
}
|
||||
}
|
||||
|
||||
private void createSchema() throws SQLException {
|
||||
try (Statement statement = connection.createStatement()) {
|
||||
statement.execute("""
|
||||
CREATE TABLE IF NOT EXISTS player_positions (
|
||||
player_uuid TEXT NOT NULL,
|
||||
world_name TEXT NOT NULL,
|
||||
x REAL NOT NULL,
|
||||
y REAL NOT NULL,
|
||||
z REAL NOT NULL,
|
||||
yaw REAL NOT NULL,
|
||||
pitch REAL NOT NULL,
|
||||
updated_at INTEGER NOT NULL,
|
||||
PRIMARY KEY (player_uuid, world_name)
|
||||
)
|
||||
""");
|
||||
}
|
||||
}
|
||||
|
||||
public Map<PositionKey, StoredPosition> loadAll() throws SQLException {
|
||||
Map<PositionKey, StoredPosition> positions = new HashMap<>();
|
||||
try (Statement statement = connection.createStatement();
|
||||
ResultSet result = statement.executeQuery("""
|
||||
SELECT player_uuid, world_name, x, y, z, yaw, pitch
|
||||
FROM player_positions
|
||||
""")) {
|
||||
while (result.next()) {
|
||||
UUID playerId = UUID.fromString(result.getString("player_uuid"));
|
||||
String world = result.getString("world_name");
|
||||
positions.put(
|
||||
new PositionKey(playerId, world),
|
||||
new StoredPosition(
|
||||
world,
|
||||
result.getDouble("x"),
|
||||
result.getDouble("y"),
|
||||
result.getDouble("z"),
|
||||
result.getFloat("yaw"),
|
||||
result.getFloat("pitch")
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
return positions;
|
||||
}
|
||||
|
||||
public void upsert(PositionKey key, StoredPosition position) throws SQLException {
|
||||
try (PreparedStatement statement = connection.prepareStatement("""
|
||||
INSERT INTO player_positions
|
||||
(player_uuid, world_name, x, y, z, yaw, pitch, updated_at)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
|
||||
ON CONFLICT(player_uuid, world_name) DO UPDATE SET
|
||||
x = excluded.x,
|
||||
y = excluded.y,
|
||||
z = excluded.z,
|
||||
yaw = excluded.yaw,
|
||||
pitch = excluded.pitch,
|
||||
updated_at = excluded.updated_at
|
||||
""")) {
|
||||
statement.setString(1, key.playerId().toString());
|
||||
statement.setString(2, key.worldName());
|
||||
statement.setDouble(3, position.x());
|
||||
statement.setDouble(4, position.y());
|
||||
statement.setDouble(5, position.z());
|
||||
statement.setFloat(6, position.yaw());
|
||||
statement.setFloat(7, position.pitch());
|
||||
statement.setLong(8, System.currentTimeMillis());
|
||||
statement.executeUpdate();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() throws SQLException {
|
||||
connection.close();
|
||||
}
|
||||
}
|
||||
73
src/main/java/ru/multispawn/core/storage/PositionStore.java
Normal file
73
src/main/java/ru/multispawn/core/storage/PositionStore.java
Normal file
@@ -0,0 +1,73 @@
|
||||
package ru.multispawn.core.storage;
|
||||
|
||||
import org.bukkit.plugin.java.JavaPlugin;
|
||||
import ru.multispawn.core.model.PositionKey;
|
||||
import ru.multispawn.core.model.StoredPosition;
|
||||
|
||||
import java.nio.file.Path;
|
||||
import java.sql.SQLException;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import java.util.UUID;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.concurrent.ExecutorService;
|
||||
import java.util.concurrent.Executors;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.logging.Level;
|
||||
|
||||
public final class PositionStore implements AutoCloseable {
|
||||
private final JavaPlugin plugin;
|
||||
private final PositionRepository repository;
|
||||
private final Map<PositionKey, StoredPosition> cache = new ConcurrentHashMap<>();
|
||||
private final ExecutorService writer = Executors.newSingleThreadExecutor(runnable -> {
|
||||
Thread thread = new Thread(runnable, "MultiSpawn-SQLite");
|
||||
thread.setDaemon(true);
|
||||
return thread;
|
||||
});
|
||||
|
||||
public PositionStore(JavaPlugin plugin, Path databasePath) throws SQLException {
|
||||
this.plugin = plugin;
|
||||
this.repository = new PositionRepository(databasePath);
|
||||
cache.putAll(repository.loadAll());
|
||||
}
|
||||
|
||||
public Optional<StoredPosition> find(UUID playerId, String worldName) {
|
||||
return Optional.ofNullable(cache.get(new PositionKey(playerId, worldName)));
|
||||
}
|
||||
|
||||
public void save(UUID playerId, StoredPosition position) {
|
||||
PositionKey key = new PositionKey(playerId, position.worldName());
|
||||
cache.put(key, position);
|
||||
writer.execute(() -> {
|
||||
try {
|
||||
repository.upsert(key, position);
|
||||
} catch (SQLException exception) {
|
||||
plugin.getLogger().log(Level.SEVERE,
|
||||
"Could not save position for " + playerId + " in " + position.worldName(), exception);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public int cachedPositionCount() {
|
||||
return cache.size();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() {
|
||||
writer.shutdown();
|
||||
try {
|
||||
if (!writer.awaitTermination(10, TimeUnit.SECONDS)) {
|
||||
plugin.getLogger().warning("Timed out while flushing SQLite writes.");
|
||||
}
|
||||
} catch (InterruptedException exception) {
|
||||
Thread.currentThread().interrupt();
|
||||
plugin.getLogger().warning("Interrupted while flushing SQLite writes.");
|
||||
}
|
||||
|
||||
try {
|
||||
repository.close();
|
||||
} catch (SQLException exception) {
|
||||
plugin.getLogger().log(Level.SEVERE, "Could not close SQLite database.", exception);
|
||||
}
|
||||
}
|
||||
}
|
||||
29
src/main/resources/config.yml
Normal file
29
src/main/resources/config.yml
Normal file
@@ -0,0 +1,29 @@
|
||||
# Only worlds imported into Multiverse-Core are handled.
|
||||
storage:
|
||||
# Standard server worlds are ignored by default.
|
||||
# Enable this if their positions should also be stored.
|
||||
include-default-worlds: false
|
||||
|
||||
# Names considered standard server worlds.
|
||||
default-worlds:
|
||||
- world
|
||||
- world_nether
|
||||
- world_the_end
|
||||
|
||||
# Empty means every Multiverse world (subject to the settings above).
|
||||
# If populated, only these worlds are handled.
|
||||
enabled-worlds: []
|
||||
|
||||
# These worlds are always ignored.
|
||||
disabled-worlds: []
|
||||
|
||||
restore:
|
||||
on-world-change: true
|
||||
on-join: true
|
||||
|
||||
# Portal transitions never save or restore positions.
|
||||
# MVPlayerTouchedPortalEvent is also observed for Multiverse portal handling.
|
||||
portals:
|
||||
ignore-nether-portals: true
|
||||
ignore-end-portals: true
|
||||
ignore-end-gateways: true
|
||||
17
src/main/resources/plugin.yml
Normal file
17
src/main/resources/plugin.yml
Normal file
@@ -0,0 +1,17 @@
|
||||
name: MultiSpawn-Core
|
||||
version: '1.0.0'
|
||||
main: ru.multispawn.core.MultiSpawnPlugin
|
||||
api-version: '1.21.11'
|
||||
author: MultiSpawn
|
||||
description: Restores each player's last position in Multiverse worlds.
|
||||
depend:
|
||||
- Multiverse-Core
|
||||
commands:
|
||||
multispawn:
|
||||
description: MultiSpawn-Core administration
|
||||
usage: /multispawn reload
|
||||
permission: multispawn.admin
|
||||
permissions:
|
||||
multispawn.admin:
|
||||
description: Allows reloading MultiSpawn-Core
|
||||
default: op
|
||||
@@ -0,0 +1,35 @@
|
||||
package ru.multispawn.core.storage;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.io.TempDir;
|
||||
import ru.multispawn.core.model.PositionKey;
|
||||
import ru.multispawn.core.model.StoredPosition;
|
||||
|
||||
import java.nio.file.Path;
|
||||
import java.util.Map;
|
||||
import java.util.UUID;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
|
||||
class PositionRepositoryTest {
|
||||
@TempDir
|
||||
Path tempDir;
|
||||
|
||||
@Test
|
||||
void upsertPersistsAndUpdatesPosition() throws Exception {
|
||||
UUID playerId = UUID.randomUUID();
|
||||
PositionKey key = new PositionKey(playerId, "survival");
|
||||
|
||||
try (PositionRepository repository = new PositionRepository(tempDir.resolve("test.db"))) {
|
||||
repository.upsert(key, new StoredPosition("survival", 1, 2, 3, 4, 5));
|
||||
repository.upsert(key, new StoredPosition("survival", 10, 20, 30, 40, 50));
|
||||
|
||||
Map<PositionKey, StoredPosition> loaded = repository.loadAll();
|
||||
assertEquals(1, loaded.size());
|
||||
assertEquals(
|
||||
new StoredPosition("survival", 10, 20, 30, 40, 50),
|
||||
loaded.get(key)
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user