mirror of
https://github.com/WiIIiam278/HuskSync.git
synced 2025-12-24 00:59:18 +00:00
Compare commits
12 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
39767c5cd0 | ||
|
|
48f7037898 | ||
|
|
67dddf0cfa | ||
|
|
eeb5e57c1e | ||
|
|
5a6ea2cffe | ||
|
|
07ddd34f8e | ||
|
|
a0b86c298f | ||
|
|
6fbef032bc | ||
|
|
318aacd432 | ||
|
|
ba1b2ff62e | ||
|
|
67ef4888da | ||
|
|
a5d3015c6e |
@@ -3,7 +3,7 @@ import org.apache.tools.ant.filters.ReplaceTokens
|
||||
plugins {
|
||||
id 'com.github.johnrengelman.shadow' version '8.1.1'
|
||||
id 'org.cadixdev.licenser' version '0.6.1' apply false
|
||||
id 'org.ajoberstar.grgit' version '5.2.1'
|
||||
id 'org.ajoberstar.grgit' version '5.2.2'
|
||||
id 'maven-publish'
|
||||
id 'java'
|
||||
}
|
||||
@@ -20,6 +20,7 @@ ext {
|
||||
set 'jedis_version', jedis_version.toString()
|
||||
set 'mysql_driver_version', mysql_driver_version.toString()
|
||||
set 'mariadb_driver_version', mariadb_driver_version.toString()
|
||||
set 'mongodb_driver_version', mongodb_driver_version.toString()
|
||||
set 'snappy_version', snappy_version.toString()
|
||||
}
|
||||
|
||||
|
||||
@@ -17,7 +17,7 @@ dependencies {
|
||||
compileOnly 'commons-io:commons-io:2.15.1'
|
||||
compileOnly 'org.json:json:20240205'
|
||||
compileOnly 'de.themoep:minedown-adventure:1.7.2-SNAPSHOT'
|
||||
compileOnly 'com.github.Exlll.ConfigLib:configlib-yaml:v4.4.0'
|
||||
compileOnly 'com.github.Exlll.ConfigLib:configlib-yaml:v4.5.0'
|
||||
compileOnly 'com.zaxxer:HikariCP:5.1.0'
|
||||
compileOnly 'net.william278:DesertWell:2.0.4'
|
||||
compileOnly 'net.william278:AdvancementAPI:97a9583413'
|
||||
@@ -39,7 +39,7 @@ shadowJar {
|
||||
relocate 'org.jetbrains', 'net.william278.husksync.libraries'
|
||||
relocate 'org.intellij', 'net.william278.husksync.libraries'
|
||||
relocate 'com.zaxxer', 'net.william278.husksync.libraries'
|
||||
relocate 'de.exlll', 'net.william278.huskclaims.libraries'
|
||||
relocate 'de.exlll', 'net.william278.husksync.libraries'
|
||||
relocate 'net.william278.desertwell', 'net.william278.husksync.libraries.desertwell'
|
||||
relocate 'net.william278.paginedown', 'net.william278.husksync.libraries.paginedown'
|
||||
relocate 'net.william278.mapdataapi', 'net.william278.husksync.libraries.mapdataapi'
|
||||
|
||||
@@ -43,6 +43,7 @@ import net.william278.husksync.data.Data;
|
||||
import net.william278.husksync.data.Identifier;
|
||||
import net.william278.husksync.data.Serializer;
|
||||
import net.william278.husksync.database.Database;
|
||||
import net.william278.husksync.database.MongoDbDatabase;
|
||||
import net.william278.husksync.database.MySqlDatabase;
|
||||
import net.william278.husksync.event.BukkitEventDispatcher;
|
||||
import net.william278.husksync.hook.PlanHook;
|
||||
@@ -162,7 +163,11 @@ public class BukkitHuskSync extends JavaPlugin implements HuskSync, BukkitTask.S
|
||||
|
||||
// Initialize the database
|
||||
initialize(getSettings().getDatabase().getType().getDisplayName() + " database connection", (plugin) -> {
|
||||
this.database = new MySqlDatabase(this);
|
||||
this.database = switch (settings.getDatabase().getType()) {
|
||||
case MYSQL, MARIADB -> new MySqlDatabase(this);
|
||||
case MONGO -> new MongoDbDatabase(this);
|
||||
default -> throw new IllegalStateException("Invalid database type");
|
||||
};
|
||||
this.database.initialize();
|
||||
});
|
||||
|
||||
|
||||
@@ -27,6 +27,7 @@ import net.william278.husksync.user.OnlineUser;
|
||||
import org.bukkit.Bukkit;
|
||||
import org.bukkit.entity.Player;
|
||||
import org.bukkit.entity.Projectile;
|
||||
import org.bukkit.event.Cancellable;
|
||||
import org.bukkit.event.EventHandler;
|
||||
import org.bukkit.event.EventPriority;
|
||||
import org.bukkit.event.Listener;
|
||||
@@ -49,6 +50,7 @@ import org.jetbrains.annotations.NotNull;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.UUID;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
public class BukkitEventListener extends EventListener implements BukkitJoinEventListener, BukkitQuitEventListener,
|
||||
@@ -132,52 +134,52 @@ public class BukkitEventListener extends EventListener implements BukkitJoinEven
|
||||
public void onProjectileLaunch(@NotNull ProjectileLaunchEvent event) {
|
||||
final Projectile projectile = event.getEntity();
|
||||
if (projectile.getShooter() instanceof Player player) {
|
||||
event.setCancelled(cancelPlayerEvent(player.getUniqueId()));
|
||||
cancelPlayerEvent(player.getUniqueId(), event);
|
||||
}
|
||||
}
|
||||
|
||||
@EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = true)
|
||||
public void onDropItem(@NotNull PlayerDropItemEvent event) {
|
||||
event.setCancelled(cancelPlayerEvent(event.getPlayer().getUniqueId()));
|
||||
cancelPlayerEvent(event.getPlayer().getUniqueId(), event);
|
||||
}
|
||||
|
||||
@EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = true)
|
||||
public void onPickupItem(@NotNull EntityPickupItemEvent event) {
|
||||
if (event.getEntity() instanceof Player player) {
|
||||
event.setCancelled(cancelPlayerEvent(player.getUniqueId()));
|
||||
cancelPlayerEvent(player.getUniqueId(), event);
|
||||
}
|
||||
}
|
||||
|
||||
@EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = true)
|
||||
public void onPlayerInteract(@NotNull PlayerInteractEvent event) {
|
||||
event.setCancelled(cancelPlayerEvent(event.getPlayer().getUniqueId()));
|
||||
cancelPlayerEvent(event.getPlayer().getUniqueId(), event);
|
||||
}
|
||||
|
||||
@EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = true)
|
||||
public void onPlayerInteractEntity(@NotNull PlayerInteractEntityEvent event) {
|
||||
event.setCancelled(cancelPlayerEvent(event.getPlayer().getUniqueId()));
|
||||
cancelPlayerEvent(event.getPlayer().getUniqueId(), event);
|
||||
}
|
||||
|
||||
@EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = true)
|
||||
public void onBlockPlace(@NotNull BlockPlaceEvent event) {
|
||||
event.setCancelled(cancelPlayerEvent(event.getPlayer().getUniqueId()));
|
||||
cancelPlayerEvent(event.getPlayer().getUniqueId(), event);
|
||||
}
|
||||
|
||||
@EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = true)
|
||||
public void onBlockBreak(@NotNull BlockBreakEvent event) {
|
||||
event.setCancelled(cancelPlayerEvent(event.getPlayer().getUniqueId()));
|
||||
cancelPlayerEvent(event.getPlayer().getUniqueId(), event);
|
||||
}
|
||||
|
||||
@EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = true)
|
||||
public void onInventoryOpen(@NotNull InventoryOpenEvent event) {
|
||||
if (event.getPlayer() instanceof Player player) {
|
||||
event.setCancelled(cancelPlayerEvent(player.getUniqueId()));
|
||||
cancelPlayerEvent(player.getUniqueId(), event);
|
||||
}
|
||||
}
|
||||
|
||||
@EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = true)
|
||||
public void onInventoryClick(@NotNull InventoryClickEvent event) {
|
||||
event.setCancelled(cancelPlayerEvent(event.getWhoClicked().getUniqueId()));
|
||||
cancelPlayerEvent(event.getWhoClicked().getUniqueId(), event);
|
||||
}
|
||||
|
||||
@EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = true)
|
||||
@@ -187,7 +189,7 @@ public class BukkitEventListener extends EventListener implements BukkitJoinEven
|
||||
@EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = true)
|
||||
public void onPlayerTakeDamage(@NotNull EntityDamageEvent event) {
|
||||
if (event.getEntity() instanceof Player player) {
|
||||
event.setCancelled(cancelPlayerEvent(player.getUniqueId()));
|
||||
cancelPlayerEvent(player.getUniqueId(), event);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -197,7 +199,13 @@ public class BukkitEventListener extends EventListener implements BukkitJoinEven
|
||||
final String commandLabel = commandArgs[0].toLowerCase(Locale.ENGLISH);
|
||||
|
||||
if (blacklistedCommands.contains("*") || blacklistedCommands.contains(commandLabel)) {
|
||||
event.setCancelled(cancelPlayerEvent(event.getPlayer().getUniqueId()));
|
||||
cancelPlayerEvent(event.getPlayer().getUniqueId(), event);
|
||||
}
|
||||
}
|
||||
|
||||
private void cancelPlayerEvent(@NotNull UUID uuid, @NotNull Cancellable event) {
|
||||
if (cancelPlayerEvent(uuid)) {
|
||||
event.setCancelled(true);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -12,4 +12,5 @@ libraries:
|
||||
- 'redis.clients:jedis:${jedis_version}'
|
||||
- 'com.mysql:mysql-connector-j:${mysql_driver_version}'
|
||||
- 'org.mariadb.jdbc:mariadb-java-client:${mariadb_driver_version}'
|
||||
- 'org.mongodb:mongodb-driver:${mongodb_driver_version}'
|
||||
- 'org.xerial.snappy:snappy-java:${snappy_version}'
|
||||
@@ -9,7 +9,7 @@ dependencies {
|
||||
api 'org.json:json:20240205'
|
||||
api 'com.google.code.gson:gson:2.10.1'
|
||||
api 'com.fatboyindustrial.gson-javatime-serialisers:gson-javatime-serialisers:1.1.2'
|
||||
api 'com.github.Exlll.ConfigLib:configlib-yaml:v4.4.0'
|
||||
api 'com.github.Exlll.ConfigLib:configlib-yaml:v4.5.0'
|
||||
api 'net.william278:DesertWell:2.0.4'
|
||||
api 'net.william278:PagineDown:1.1'
|
||||
api('com.zaxxer:HikariCP:5.1.0') {
|
||||
@@ -18,20 +18,21 @@ dependencies {
|
||||
|
||||
compileOnly 'org.projectlombok:lombok:1.18.30'
|
||||
compileOnly 'org.jetbrains:annotations:24.1.0'
|
||||
compileOnly 'net.kyori:adventure-api:4.15.0'
|
||||
compileOnly 'net.kyori:adventure-api:4.16.0'
|
||||
compileOnly 'net.kyori:adventure-platform-api:4.3.2'
|
||||
compileOnly 'com.google.guava:guava:33.0.0-jre'
|
||||
compileOnly 'com.github.plan-player-analytics:Plan:5.5.2272'
|
||||
compileOnly "redis.clients:jedis:$jedis_version"
|
||||
compileOnly "com.mysql:mysql-connector-j:$mysql_driver_version"
|
||||
compileOnly "org.mariadb.jdbc:mariadb-java-client:$mariadb_driver_version"
|
||||
compileOnly "org.mongodb:mongodb-driver:$mongodb_driver_version"
|
||||
compileOnly "org.xerial.snappy:snappy-java:$snappy_version"
|
||||
|
||||
testImplementation "redis.clients:jedis:$jedis_version"
|
||||
testImplementation "org.xerial.snappy:snappy-java:$snappy_version"
|
||||
testImplementation 'com.google.guava:guava:33.0.0-jre'
|
||||
testImplementation 'com.github.plan-player-analytics:Plan:5.5.2272'
|
||||
testCompileOnly 'com.github.Exlll.ConfigLib:configlib-yaml:v4.4.0'
|
||||
testCompileOnly 'com.github.Exlll.ConfigLib:configlib-yaml:v4.5.0'
|
||||
testCompileOnly 'org.jetbrains:annotations:24.1.0'
|
||||
|
||||
annotationProcessor 'org.projectlombok:lombok:1.18.30'
|
||||
|
||||
@@ -379,7 +379,7 @@ public interface HuskSync extends Task.Supplier, EventDispatcher, ConfigProvider
|
||||
HuskSync has failed to load! The plugin will not be enabled and no data will be synchronized.
|
||||
Please make sure the plugin has been setup correctly (https://william278.net/docs/husksync/setup):
|
||||
|
||||
1) Make sure you've entered your MySQL or MariaDB database details correctly in config.yml
|
||||
1) Make sure you've entered your MySQL, MariaDB or MongoDB database details correctly in config.yml
|
||||
2) Make sure your Redis server details are also correct in config.yml
|
||||
3) Make sure your config is up-to-date (https://william278.net/docs/husksync/config-file)
|
||||
4) Check the error below for more details
|
||||
|
||||
@@ -66,7 +66,8 @@ public class HuskSyncCommand extends Command implements TabProvider {
|
||||
AboutMenu.Credit.of("William278").description("Click to visit website").url("https://william278.net"))
|
||||
.credits("Contributors",
|
||||
AboutMenu.Credit.of("HarvelsX").description("Code"),
|
||||
AboutMenu.Credit.of("HookWoods").description("Code"))
|
||||
AboutMenu.Credit.of("HookWoods").description("Code"),
|
||||
AboutMenu.Credit.of("Preva1l").description("Code"))
|
||||
.credits("Translators",
|
||||
AboutMenu.Credit.of("Namiu").description("Japanese (ja-jp)"),
|
||||
AboutMenu.Credit.of("anchelthe").description("Spanish (es-es)"),
|
||||
|
||||
@@ -30,6 +30,7 @@ import net.william278.husksync.data.Identifier;
|
||||
import net.william278.husksync.database.Database;
|
||||
import net.william278.husksync.listener.EventListener;
|
||||
import net.william278.husksync.sync.DataSyncer;
|
||||
import org.checkerframework.checker.units.qual.C;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
import java.util.ArrayList;
|
||||
@@ -85,10 +86,10 @@ public class Settings {
|
||||
@NoArgsConstructor(access = AccessLevel.PRIVATE)
|
||||
public static class DatabaseSettings {
|
||||
|
||||
@Comment("Type of database to use (MYSQL, MARIADB)")
|
||||
@Comment("Type of database to use (MYSQL, MARIADB, MONGO)")
|
||||
private Database.Type type = Database.Type.MYSQL;
|
||||
|
||||
@Comment("Specify credentials here for your MYSQL or MARIADB database")
|
||||
@Comment("Specify credentials here for your MYSQL, MARIADB OR MONGO database")
|
||||
private DatabaseCredentials credentials = new DatabaseCredentials();
|
||||
|
||||
@Getter
|
||||
@@ -100,9 +101,12 @@ public class Settings {
|
||||
private String database = "HuskSync";
|
||||
private String username = "root";
|
||||
private String password = "pa55w0rd";
|
||||
@Comment("Only change this if you have select MYSQL or MARIADB")
|
||||
private String parameters = String.join("&",
|
||||
"?autoReconnect=true", "useSSL=false",
|
||||
"useUnicode=true", "characterEncoding=UTF-8");
|
||||
@Comment("Only change this if you have selected MONGO")
|
||||
private String mongoAuthDb = "admin";
|
||||
}
|
||||
|
||||
@Comment("MYSQL / MARIADB database Hikari connection pool properties. Don't modify this unless you know what you're doing!")
|
||||
|
||||
@@ -23,10 +23,16 @@ import com.google.common.collect.Maps;
|
||||
import com.google.gson.annotations.Expose;
|
||||
import com.google.gson.annotations.SerializedName;
|
||||
import de.themoep.minedown.adventure.MineDown;
|
||||
import lombok.AccessLevel;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Getter;
|
||||
import lombok.NoArgsConstructor;
|
||||
import lombok.experimental.Accessors;
|
||||
import net.william278.desertwell.util.Version;
|
||||
import net.william278.husksync.HuskSync;
|
||||
import net.william278.husksync.adapter.Adaptable;
|
||||
import net.william278.husksync.adapter.DataAdapter;
|
||||
import org.apache.commons.text.WordUtils;
|
||||
import org.jetbrains.annotations.ApiStatus;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
@@ -41,6 +47,7 @@ import java.util.stream.Collectors;
|
||||
*
|
||||
* @since 3.0
|
||||
*/
|
||||
@NoArgsConstructor(access = AccessLevel.PRIVATE)
|
||||
public class DataSnapshot {
|
||||
|
||||
/*
|
||||
@@ -59,7 +66,7 @@ public class DataSnapshot {
|
||||
protected OffsetDateTime timestamp;
|
||||
|
||||
@SerializedName("save_cause")
|
||||
protected SaveCause saveCause;
|
||||
protected String saveCause;
|
||||
|
||||
@SerializedName("server_name")
|
||||
protected String serverName;
|
||||
@@ -77,7 +84,7 @@ public class DataSnapshot {
|
||||
protected Map<String, String> data;
|
||||
|
||||
private DataSnapshot(@NotNull UUID id, boolean pinned, @NotNull OffsetDateTime timestamp,
|
||||
@NotNull SaveCause saveCause, @NotNull String serverName, @NotNull Map<String, String> data,
|
||||
@NotNull String saveCause, @NotNull String serverName, @NotNull Map<String, String> data,
|
||||
@NotNull Version minecraftVersion, @NotNull String platformType, int formatVersion) {
|
||||
this.id = id;
|
||||
this.pinned = pinned;
|
||||
@@ -90,10 +97,6 @@ public class DataSnapshot {
|
||||
this.formatVersion = formatVersion;
|
||||
}
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
private DataSnapshot() {
|
||||
}
|
||||
|
||||
@NotNull
|
||||
@ApiStatus.Internal
|
||||
public static DataSnapshot.Builder builder(@NotNull HuskSync plugin) {
|
||||
@@ -196,7 +199,7 @@ public class DataSnapshot {
|
||||
*/
|
||||
@NotNull
|
||||
public SaveCause getSaveCause() {
|
||||
return saveCause;
|
||||
return SaveCause.of(saveCause);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -219,7 +222,7 @@ public class DataSnapshot {
|
||||
* @since 3.0
|
||||
*/
|
||||
public void setSaveCause(@NotNull SaveCause saveCause) {
|
||||
this.saveCause = saveCause;
|
||||
this.saveCause = saveCause.name();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -270,24 +273,21 @@ public class DataSnapshot {
|
||||
*
|
||||
* @since 3.0
|
||||
*/
|
||||
@NoArgsConstructor(access = AccessLevel.PRIVATE)
|
||||
public static class Packed extends DataSnapshot implements Adaptable {
|
||||
|
||||
protected Packed(@NotNull UUID id, boolean pinned, @NotNull OffsetDateTime timestamp,
|
||||
@NotNull SaveCause saveCause, @NotNull String serverName, @NotNull Map<String, String> data,
|
||||
@NotNull String saveCause, @NotNull String serverName, @NotNull Map<String, String> data,
|
||||
@NotNull Version minecraftVersion, @NotNull String platformType, int formatVersion) {
|
||||
super(id, pinned, timestamp, saveCause, serverName, data, minecraftVersion, platformType, formatVersion);
|
||||
}
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
private Packed() {
|
||||
}
|
||||
|
||||
@ApiStatus.Internal
|
||||
public void edit(@NotNull HuskSync plugin, @NotNull Consumer<Unpacked> editor) {
|
||||
final Unpacked data = unpack(plugin);
|
||||
editor.accept(data);
|
||||
this.pinned = data.isPinned();
|
||||
this.saveCause = data.getSaveCause();
|
||||
this.saveCause = data.getSaveCause().name();
|
||||
this.data = data.serializeData(plugin);
|
||||
}
|
||||
|
||||
@@ -341,7 +341,7 @@ public class DataSnapshot {
|
||||
private final Map<Identifier, Data> deserialized;
|
||||
|
||||
private Unpacked(@NotNull UUID id, boolean pinned, @NotNull OffsetDateTime timestamp,
|
||||
@NotNull SaveCause saveCause, @NotNull String serverName, @NotNull Map<String, String> data,
|
||||
@NotNull String saveCause, @NotNull String serverName, @NotNull Map<String, String> data,
|
||||
@NotNull Version minecraftVersion, @NotNull String platformType, int formatVersion,
|
||||
@NotNull HuskSync plugin) {
|
||||
super(id, pinned, timestamp, saveCause, serverName, data, minecraftVersion, platformType, formatVersion);
|
||||
@@ -349,7 +349,7 @@ public class DataSnapshot {
|
||||
}
|
||||
|
||||
private Unpacked(@NotNull UUID id, boolean pinned, @NotNull OffsetDateTime timestamp,
|
||||
@NotNull SaveCause saveCause, @NotNull String serverName, @NotNull Map<Identifier, Data> data,
|
||||
@NotNull String saveCause, @NotNull String serverName, @NotNull Map<Identifier, Data> data,
|
||||
@NotNull Version minecraftVersion, @NotNull String platformType, int formatVersion) {
|
||||
super(id, pinned, timestamp, saveCause, serverName, Map.of(), minecraftVersion, platformType, formatVersion);
|
||||
this.deserialized = data;
|
||||
@@ -719,7 +719,7 @@ public class DataSnapshot {
|
||||
id,
|
||||
pinned || plugin.getSettings().getSynchronization().doAutoPin(saveCause),
|
||||
timestamp,
|
||||
saveCause,
|
||||
saveCause.name(),
|
||||
serverName,
|
||||
data,
|
||||
plugin.getMinecraftVersion(),
|
||||
@@ -742,138 +742,253 @@ public class DataSnapshot {
|
||||
|
||||
}
|
||||
|
||||
public interface Cause {
|
||||
|
||||
@NotNull
|
||||
String name();
|
||||
|
||||
/**
|
||||
* Returns the capitalized display name of the cause.
|
||||
*
|
||||
* @return the cause display name
|
||||
*/
|
||||
@NotNull
|
||||
default String getDisplayName() {
|
||||
return WordUtils.capitalizeFully(name().replaceAll("_", " "));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Identifies the cause of a player data save.
|
||||
*
|
||||
* @implNote This enum is saved in the database.
|
||||
* A string wrapper, for identifying the cause of a player data save.
|
||||
* </p>
|
||||
* Cause names have a max length of 32 characters.
|
||||
*/
|
||||
public enum SaveCause {
|
||||
@Accessors(fluent = true)
|
||||
@Getter
|
||||
@AllArgsConstructor(access = AccessLevel.PRIVATE)
|
||||
public static class SaveCause implements Cause {
|
||||
|
||||
/**
|
||||
* Indicates data saved when a player disconnected from the server (either to change servers, or to log off)
|
||||
*
|
||||
* @since 2.0
|
||||
*/
|
||||
DISCONNECT,
|
||||
public static final SaveCause DISCONNECT = of("DISCONNECT");
|
||||
|
||||
/**
|
||||
* Indicates data saved when the world saved
|
||||
*
|
||||
* @since 2.0
|
||||
*/
|
||||
WORLD_SAVE,
|
||||
public static final SaveCause WORLD_SAVE = of("WORLD_SAVE");
|
||||
|
||||
/**
|
||||
* Indicates data saved when the user died
|
||||
*
|
||||
* @since 2.1
|
||||
*/
|
||||
DEATH,
|
||||
public static final SaveCause DEATH = of("DEATH");
|
||||
|
||||
/**
|
||||
* Indicates data saved when the server shut down
|
||||
*
|
||||
* @since 2.0
|
||||
*/
|
||||
SERVER_SHUTDOWN,
|
||||
public static final SaveCause SERVER_SHUTDOWN = of("SERVER_SHUTDOWN");
|
||||
|
||||
/**
|
||||
* Indicates data was saved by editing inventory contents via the {@code /inventory} command
|
||||
*
|
||||
* @since 2.0
|
||||
*/
|
||||
INVENTORY_COMMAND,
|
||||
public static final SaveCause INVENTORY_COMMAND = of("INVENTORY_COMMAND");
|
||||
|
||||
/**
|
||||
* Indicates data was saved by editing Ender Chest contents via the {@code /enderchest} command
|
||||
*
|
||||
* @since 2.0
|
||||
*/
|
||||
ENDERCHEST_COMMAND,
|
||||
public static final SaveCause ENDERCHEST_COMMAND = of("ENDERCHEST_COMMAND");
|
||||
|
||||
/**
|
||||
* Indicates data was saved by restoring it from a previous version
|
||||
*
|
||||
* @since 2.0
|
||||
*/
|
||||
BACKUP_RESTORE,
|
||||
public static final SaveCause BACKUP_RESTORE = of("BACKUP_RESTORE");
|
||||
|
||||
/**
|
||||
* Indicates data was saved by an API call
|
||||
*
|
||||
* @since 2.0
|
||||
*/
|
||||
API,
|
||||
public static final SaveCause API = of("API");
|
||||
|
||||
/**
|
||||
* Indicates data was saved from being imported from MySQLPlayerDataBridge
|
||||
*
|
||||
* @since 2.0
|
||||
*/
|
||||
MPDB_MIGRATION,
|
||||
public static final SaveCause MPDB_MIGRATION = of("MPDB_MIGRATION");
|
||||
|
||||
/**
|
||||
* Indicates data was saved from being imported from a legacy version (v1.x -> v2.x)
|
||||
*
|
||||
* @since 2.0
|
||||
*/
|
||||
LEGACY_MIGRATION,
|
||||
public static final SaveCause LEGACY_MIGRATION = of("LEGACY_MIGRATION");
|
||||
|
||||
/**
|
||||
* Indicates data was saved from being imported from a legacy version (v2.x -> v3.x)
|
||||
*
|
||||
* @since 3.0
|
||||
*/
|
||||
CONVERTED_FROM_V2;
|
||||
public static final SaveCause CONVERTED_FROM_V2 = of("CONVERTED_FROM_V2");
|
||||
|
||||
@NotNull
|
||||
public String getDisplayName() {
|
||||
return name().toLowerCase(Locale.ENGLISH);
|
||||
private final String name;
|
||||
|
||||
/**
|
||||
* Get or create a {@link SaveCause} from a name
|
||||
*
|
||||
* @param name the name to be displayed
|
||||
* @return the cause
|
||||
*/
|
||||
@NotNull
|
||||
public static SaveCause of(@NotNull String name) {
|
||||
return new SaveCause(name.length() > 32 ? name.substring(0, 31) : name);
|
||||
}
|
||||
|
||||
@NotNull
|
||||
public String getLocale(@NotNull HuskSync plugin) {
|
||||
return plugin.getLocales().getRawLocale("save_cause_" + name().toLowerCase())
|
||||
return plugin.getLocales()
|
||||
.getRawLocale("save_cause_" + name().toLowerCase(Locale.ENGLISH))
|
||||
.orElse(getDisplayName());
|
||||
}
|
||||
|
||||
@NotNull
|
||||
public static SaveCause[] values() {
|
||||
return new SaveCause[]{
|
||||
DISCONNECT, WORLD_SAVE, DEATH, SERVER_SHUTDOWN, INVENTORY_COMMAND, ENDERCHEST_COMMAND,
|
||||
BACKUP_RESTORE, API, MPDB_MIGRATION, LEGACY_MIGRATION, CONVERTED_FROM_V2
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Represents the cause of a player having their data updated.
|
||||
* A string wrapper, for identifying the cause of a player data update.
|
||||
* </p>
|
||||
* Cause names have a max length of 32 characters.
|
||||
*/
|
||||
public enum UpdateCause {
|
||||
@Accessors(fluent = true)
|
||||
@Getter
|
||||
@AllArgsConstructor(access = AccessLevel.PRIVATE)
|
||||
public static class UpdateCause implements Cause {
|
||||
|
||||
/**
|
||||
* Indicates the data was updated by a synchronization process
|
||||
*
|
||||
* @since 3.0
|
||||
*/
|
||||
SYNCHRONIZED("synchronization_complete", "synchronization_failed"),
|
||||
public static final UpdateCause SYNCHRONIZED = of("SYNCHRONIZED",
|
||||
"synchronization_complete", "synchronization_failed"
|
||||
);
|
||||
|
||||
/**
|
||||
* Indicates the data was updated by a user joining the server
|
||||
*
|
||||
* @since 3.0
|
||||
*/
|
||||
NEW_USER("user_registration_complete", null),
|
||||
public static final UpdateCause NEW_USER = of("NEW_USER",
|
||||
"user_registration_complete", null
|
||||
);
|
||||
|
||||
/**
|
||||
* Indicates the data was updated by a data update process (management command, API, etc.)
|
||||
*
|
||||
* @since 3.0
|
||||
*/
|
||||
UPDATED("data_update_complete", "data_update_failed");
|
||||
public static final UpdateCause UPDATED = of("UPDATED",
|
||||
"data_update_complete", "data_update_failed"
|
||||
);
|
||||
|
||||
private final String completedLocale;
|
||||
private final String failureLocale;
|
||||
/**
|
||||
* Indicates data was saved by an API call
|
||||
*
|
||||
* @since 3.3.3
|
||||
*/
|
||||
public static final UpdateCause API = of("API");
|
||||
|
||||
UpdateCause(@Nullable String completedLocale, @Nullable String failureLocale) {
|
||||
this.completedLocale = completedLocale;
|
||||
this.failureLocale = failureLocale;
|
||||
@NotNull
|
||||
private final String name;
|
||||
@Nullable
|
||||
private String completedLocale;
|
||||
@Nullable
|
||||
private String failureLocale;
|
||||
|
||||
/**
|
||||
* Get or create a {@link UpdateCause} from a name and completed/failure locales
|
||||
*
|
||||
* @param name the name to be displayed
|
||||
* @param completedLocale the locale to be displayed on successful update,
|
||||
* or {@code null} if none is to be shown
|
||||
* @param failureLocale the locale to be displayed on a failed update,
|
||||
* or {@code null} if none is to be shown
|
||||
* @return the cause
|
||||
*/
|
||||
public static UpdateCause of(@NotNull String name, @Nullable String completedLocale,
|
||||
@Nullable String failureLocale) {
|
||||
return new UpdateCause(
|
||||
name.length() > 32 ? name.substring(0, 31) : name,
|
||||
completedLocale, failureLocale
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get or create a {@link UpdateCause} from a name
|
||||
*
|
||||
* @param name the name to be displayed
|
||||
* @return the cause
|
||||
*/
|
||||
@NotNull
|
||||
public static UpdateCause of(@NotNull String name) {
|
||||
return of(name, null, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the message to be displayed when a user's data is successfully updated.
|
||||
*
|
||||
* @param plugin plugin instance
|
||||
* @return the message
|
||||
*/
|
||||
public Optional<MineDown> getCompletedLocale(@NotNull HuskSync plugin) {
|
||||
if (completedLocale != null) {
|
||||
return plugin.getLocales().getLocale(completedLocale);
|
||||
if (completedLocale() != null) {
|
||||
return Optional.of(plugin.getLocales().getLocale(completedLocale())
|
||||
.orElse(plugin.getLocales().format(getDisplayName())));
|
||||
}
|
||||
return Optional.empty();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the message to be displayed when a user's data fails to update.
|
||||
*
|
||||
* @param plugin plugin instance
|
||||
* @return the message
|
||||
*/
|
||||
public Optional<MineDown> getFailedLocale(@NotNull HuskSync plugin) {
|
||||
if (failureLocale != null) {
|
||||
return plugin.getLocales().getLocale(failureLocale);
|
||||
if (failureLocale() != null) {
|
||||
return Optional.of(plugin.getLocales().getLocale(failureLocale())
|
||||
.orElse(plugin.getLocales().format(failureLocale())));
|
||||
}
|
||||
return Optional.empty();
|
||||
}
|
||||
|
||||
@NotNull
|
||||
public static UpdateCause[] values() {
|
||||
return new UpdateCause[]{
|
||||
SYNCHRONIZED, NEW_USER, UPDATED
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -19,6 +19,7 @@
|
||||
|
||||
package net.william278.husksync.database;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Getter;
|
||||
import net.william278.husksync.HuskSync;
|
||||
import net.william278.husksync.config.Settings;
|
||||
@@ -253,9 +254,11 @@ public abstract class Database {
|
||||
/**
|
||||
* Identifies types of databases
|
||||
*/
|
||||
@Getter
|
||||
public enum Type {
|
||||
MYSQL("MySQL", "mysql"),
|
||||
MARIADB("MariaDB", "mariadb");
|
||||
MARIADB("MariaDB", "mariadb"),
|
||||
MONGO("MongoDB", "mongo");
|
||||
|
||||
private final String displayName;
|
||||
private final String protocol;
|
||||
@@ -264,16 +267,6 @@ public abstract class Database {
|
||||
this.displayName = displayName;
|
||||
this.protocol = protocol;
|
||||
}
|
||||
|
||||
@NotNull
|
||||
public String getDisplayName() {
|
||||
return displayName;
|
||||
}
|
||||
|
||||
@NotNull
|
||||
public String getProtocol() {
|
||||
return protocol;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -0,0 +1,382 @@
|
||||
/*
|
||||
* This file is part of HuskSync, licensed under the Apache License 2.0.
|
||||
*
|
||||
* Copyright (c) William278 <will27528@gmail.com>
|
||||
* Copyright (c) contributors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package net.william278.husksync.database;
|
||||
|
||||
import com.google.common.collect.Lists;
|
||||
import com.mongodb.MongoException;
|
||||
import com.mongodb.client.FindIterable;
|
||||
import com.mongodb.client.model.Updates;
|
||||
import net.william278.husksync.HuskSync;
|
||||
import net.william278.husksync.config.Settings;
|
||||
import net.william278.husksync.data.DataSnapshot;
|
||||
import net.william278.husksync.database.mongo.MongoCollectionHelper;
|
||||
import net.william278.husksync.database.mongo.MongoConnectionHandler;
|
||||
import net.william278.husksync.user.User;
|
||||
import org.bson.Document;
|
||||
import org.bson.conversions.Bson;
|
||||
import org.bson.types.Binary;
|
||||
import org.jetbrains.annotations.Blocking;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
import java.time.Instant;
|
||||
import java.time.OffsetDateTime;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import java.util.TimeZone;
|
||||
import java.util.UUID;
|
||||
import java.util.logging.Level;
|
||||
|
||||
public class MongoDbDatabase extends Database {
|
||||
private MongoConnectionHandler mongoConnectionHandler;
|
||||
private MongoCollectionHelper mongoCollectionHelper;
|
||||
|
||||
private final String usersTable;
|
||||
private final String userDataTable;
|
||||
public MongoDbDatabase(@NotNull HuskSync plugin) {
|
||||
super(plugin);
|
||||
this.usersTable = plugin.getSettings().getDatabase().getTableName(TableName.USERS);
|
||||
this.userDataTable = plugin.getSettings().getDatabase().getTableName(TableName.USER_DATA);
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize the database and ensure tables are present; create tables if they do not exist.
|
||||
*
|
||||
* @throws IllegalStateException if the database could not be initialized
|
||||
*/
|
||||
@Override
|
||||
public void initialize() throws IllegalStateException {
|
||||
final Settings.DatabaseSettings.DatabaseCredentials credentials = plugin.getSettings().getDatabase().getCredentials();
|
||||
try {
|
||||
mongoConnectionHandler = new MongoConnectionHandler(
|
||||
credentials.getHost(),
|
||||
credentials.getPort(),
|
||||
credentials.getUsername(),
|
||||
credentials.getPassword(),
|
||||
credentials.getDatabase(),
|
||||
credentials.getMongoAuthDb()
|
||||
);
|
||||
mongoCollectionHelper = new MongoCollectionHelper(mongoConnectionHandler);
|
||||
if (mongoCollectionHelper.getCollection(usersTable) == null) {
|
||||
mongoCollectionHelper.createCollection(usersTable);
|
||||
}
|
||||
if (mongoCollectionHelper.getCollection(userDataTable) == null) {
|
||||
mongoCollectionHelper.createCollection(userDataTable);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
throw new IllegalStateException("Failed to establish a connection to the MongoDB database. " +
|
||||
"Please check the supplied database credentials in the config file", e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensure a {@link User} has an entry in the database and that their username is up-to-date
|
||||
*
|
||||
* @param user The {@link User} to ensure
|
||||
*/
|
||||
@Blocking
|
||||
@Override
|
||||
public void ensureUser(@NotNull User user) {
|
||||
getUser(user.getUuid()).ifPresentOrElse(
|
||||
existingUser -> {
|
||||
if (!existingUser.getUsername().equals(user.getUsername())) {
|
||||
// Update a user's name if it has changed in the database
|
||||
try {
|
||||
Document filter = new Document("uuid", existingUser.getUuid().toString());
|
||||
Document doc = mongoCollectionHelper.getCollection(usersTable).find(filter).first();
|
||||
|
||||
Bson updates = Updates.set("uuid", user.getUuid().toString());
|
||||
mongoCollectionHelper.updateDocument(usersTable, doc, updates);
|
||||
} catch (MongoException e) {
|
||||
plugin.log(Level.SEVERE, "Failed to insert a user into the database", e);
|
||||
}
|
||||
}
|
||||
},
|
||||
() -> {
|
||||
// Insert new player data into the database
|
||||
try {
|
||||
Document doc = new Document("uuid", user.getUuid().toString()).append("username", user.getUsername());
|
||||
mongoCollectionHelper.insertDocument(usersTable, doc);
|
||||
} catch (MongoException e) {
|
||||
plugin.log(Level.SEVERE, "Failed to insert a user into the database", e);
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a player by their Minecraft account {@link UUID}
|
||||
*
|
||||
* @param uuid Minecraft account {@link UUID} of the {@link User} to get
|
||||
* @return An optional with the {@link User} present if they exist
|
||||
*/
|
||||
@Blocking
|
||||
@Override
|
||||
public Optional<User> getUser(@NotNull UUID uuid) {
|
||||
Document filter = new Document("uuid", uuid);
|
||||
Document doc = mongoCollectionHelper.getCollection(usersTable).find(filter).first();
|
||||
if (doc != null) {
|
||||
return Optional.of(new User(UUID.fromString(doc.getString("uuid")),
|
||||
doc.getString("username")));
|
||||
}
|
||||
return Optional.empty();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a user by their username (<i>case-insensitive</i>)
|
||||
*
|
||||
* @param username Username of the {@link User} to get (<i>case-insensitive</i>)
|
||||
* @return An optional with the {@link User} present if they exist
|
||||
*/
|
||||
@Blocking
|
||||
@Override
|
||||
public Optional<User> getUserByName(@NotNull String username) {
|
||||
Document filter = new Document("username", username);
|
||||
Document doc = mongoCollectionHelper.getCollection(usersTable).find(filter).first();
|
||||
if (doc != null) {
|
||||
return Optional.of(new User(UUID.fromString(doc.getString("uuid")),
|
||||
doc.getString("username")));
|
||||
}
|
||||
return Optional.empty();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the latest data snapshot for a user.
|
||||
*
|
||||
* @param user The user to get data for
|
||||
* @return an optional containing the {@link DataSnapshot}, if it exists, or an empty optional if it does not
|
||||
*/
|
||||
@Blocking
|
||||
@Override
|
||||
public Optional<DataSnapshot.Packed> getLatestSnapshot(@NotNull User user) {
|
||||
Document filter = new Document("player_uuid", user.getUuid().toString());
|
||||
Document sort = new Document("timestamp", -1); // -1 = Descending
|
||||
FindIterable<Document> iterable = mongoCollectionHelper.getCollection(userDataTable).find(filter).sort(sort);
|
||||
Document doc = iterable.first();
|
||||
if (doc != null) {
|
||||
final UUID versionUuid = UUID.fromString(doc.getString("version_uuid"));
|
||||
final OffsetDateTime timestamp = OffsetDateTime.ofInstant(Instant.ofEpochMilli((long) doc.get("timestamp")), TimeZone.getDefault().toZoneId());
|
||||
final Binary bin = doc.get("data", Binary.class);
|
||||
final byte[] dataByteArray = bin.getData();
|
||||
return Optional.of(DataSnapshot.deserialize(plugin, dataByteArray, versionUuid, timestamp));
|
||||
}
|
||||
return Optional.empty();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all {@link DataSnapshot} entries for a user from the database.
|
||||
*
|
||||
* @param user The user to get data for
|
||||
* @return The list of a user's {@link DataSnapshot} entries
|
||||
*/
|
||||
@Blocking
|
||||
@Override
|
||||
@NotNull
|
||||
public List<DataSnapshot.Packed> getAllSnapshots(@NotNull User user) {
|
||||
final List<DataSnapshot.Packed> retrievedData = Lists.newArrayList();
|
||||
Document filter = new Document("player_uuid", user.getUuid().toString());
|
||||
Document sort = new Document("timestamp", -1); // -1 = Descending
|
||||
FindIterable<Document> iterable = mongoCollectionHelper.getCollection(userDataTable).find(filter).sort(sort);
|
||||
for (Document doc : iterable) {
|
||||
final UUID versionUuid = UUID.fromString(doc.getString("version_uuid"));
|
||||
final OffsetDateTime timestamp = OffsetDateTime.ofInstant(Instant.ofEpochMilli((long) doc.get("timestamp")), TimeZone.getDefault().toZoneId());
|
||||
final Binary bin = doc.get("data", Binary.class);
|
||||
final byte[] dataByteArray = bin.getData();
|
||||
retrievedData.add(DataSnapshot.deserialize(plugin, dataByteArray, versionUuid, timestamp));
|
||||
}
|
||||
return retrievedData;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets a specific {@link DataSnapshot} entry for a user from the database, by its UUID.
|
||||
*
|
||||
* @param user The user to get data for
|
||||
* @param versionUuid The UUID of the {@link DataSnapshot} entry to get
|
||||
* @return An optional containing the {@link DataSnapshot}, if it exists
|
||||
*/
|
||||
@Blocking
|
||||
@Override
|
||||
public Optional<DataSnapshot.Packed> getSnapshot(@NotNull User user, @NotNull UUID versionUuid) {
|
||||
Document filter = new Document("player_uuid", user.getUuid().toString()).append("version_uuid", versionUuid.toString());
|
||||
Document sort = new Document("timestamp", -1); // -1 = Descending
|
||||
FindIterable<Document> iterable = mongoCollectionHelper.getCollection(userDataTable).find(filter).sort(sort);
|
||||
Document doc = iterable.first();
|
||||
if (doc != null) {
|
||||
final OffsetDateTime timestamp = OffsetDateTime.ofInstant(Instant.ofEpochMilli((long) doc.get("timestamp")), TimeZone.getDefault().toZoneId());
|
||||
final Binary bin = doc.get("data", Binary.class);
|
||||
final byte[] dataByteArray = bin.getData();
|
||||
return Optional.of(DataSnapshot.deserialize(plugin, dataByteArray, versionUuid, timestamp));
|
||||
}
|
||||
return Optional.empty();
|
||||
}
|
||||
|
||||
/**
|
||||
* <b>(Internal)</b> Prune user data for a given user to the maximum value as configured.
|
||||
*
|
||||
* @param user The user to prune data for
|
||||
* @implNote Data snapshots marked as {@code pinned} are exempt from rotation
|
||||
*/
|
||||
@Blocking
|
||||
@Override
|
||||
protected void rotateSnapshots(@NotNull User user) {
|
||||
try {
|
||||
final List<DataSnapshot.Packed> unpinnedUserData = getAllSnapshots(user).stream()
|
||||
.filter(dataSnapshot -> !dataSnapshot.isPinned()).toList();
|
||||
final int maxSnapshots = plugin.getSettings().getSynchronization().getMaxUserDataSnapshots();
|
||||
if (unpinnedUserData.size() > maxSnapshots) {
|
||||
|
||||
Document filter = new Document("player_uuid", user.getUuid().toString()).append("pinned", false);
|
||||
Document sort = new Document("timestamp", 1); // 1 = Ascending
|
||||
FindIterable<Document> iterable = mongoCollectionHelper.getCollection(userDataTable)
|
||||
.find(filter)
|
||||
.sort(sort)
|
||||
.limit(unpinnedUserData.size() - maxSnapshots);
|
||||
|
||||
for (Document doc : iterable) {
|
||||
mongoCollectionHelper.deleteDocument(userDataTable, doc);
|
||||
}
|
||||
}
|
||||
} catch (MongoException e) {
|
||||
plugin.log(Level.SEVERE, "Failed to prune user data from the database", e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes a specific {@link DataSnapshot} entry for a user from the database, by its UUID.
|
||||
*
|
||||
* @param user The user to get data for
|
||||
* @param versionUuid The UUID of the {@link DataSnapshot} entry to delete
|
||||
*/
|
||||
@Blocking
|
||||
@Override
|
||||
public boolean deleteSnapshot(@NotNull User user, @NotNull UUID versionUuid) {
|
||||
try {
|
||||
Document filter = new Document("player_uuid", user.getUuid().toString()).append("version_uuid", versionUuid.toString());
|
||||
Document doc = mongoCollectionHelper.getCollection(userDataTable).find(filter).first();
|
||||
if (doc == null) {
|
||||
return false;
|
||||
}
|
||||
mongoCollectionHelper.deleteDocument(userDataTable, doc);
|
||||
return true;
|
||||
} catch (MongoException e) {
|
||||
plugin.log(Level.SEVERE, "Failed to delete specific user data from the database", e);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes the most recent data snapshot by the given {@link User user}
|
||||
* The snapshot must have been created after {@link OffsetDateTime time} and NOT be pinned
|
||||
* Facilities the backup frequency feature, reducing redundant snapshots from being saved longer than needed
|
||||
*
|
||||
* @param user The user to delete a snapshot for
|
||||
* @param within The time to delete a snapshot after
|
||||
*/
|
||||
@Blocking
|
||||
@Override
|
||||
protected void rotateLatestSnapshot(@NotNull User user, @NotNull OffsetDateTime within) {
|
||||
try {
|
||||
Document filter = new Document("player_uuid", user.getUuid().toString()).append("pinned", false);
|
||||
Document sort = new Document("timestamp", 1); // 1 = Ascending
|
||||
FindIterable<Document> iterable = mongoCollectionHelper.getCollection(userDataTable)
|
||||
.find(filter)
|
||||
.sort(sort);
|
||||
|
||||
for (Document doc : iterable) {
|
||||
final OffsetDateTime timestamp = OffsetDateTime.ofInstant(
|
||||
Instant.ofEpochMilli((long) doc.get("timestamp")), TimeZone.getDefault().toZoneId()
|
||||
);
|
||||
if (timestamp.isAfter(within)) {
|
||||
mongoCollectionHelper.deleteDocument(userDataTable, doc);
|
||||
return;
|
||||
}
|
||||
}
|
||||
} catch (MongoException e) {
|
||||
plugin.log(Level.SEVERE, "Failed to prune user data from the database", e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* <b>Internal</b> - Create user data in the database
|
||||
*
|
||||
* @param user The user to add data for
|
||||
* @param data The {@link DataSnapshot} to set.
|
||||
*/
|
||||
@Blocking
|
||||
@Override
|
||||
protected void createSnapshot(@NotNull User user, @NotNull DataSnapshot.Packed data) {
|
||||
try {
|
||||
Document doc = new Document("player_uuid", user.getUuid().toString())
|
||||
.append("version_uuid", data.getId().toString())
|
||||
.append("timestamp", data.getTimestamp().toInstant().toEpochMilli())
|
||||
.append("save_cause", data.getSaveCause().name())
|
||||
.append("pinned", data.isPinned())
|
||||
.append("data", new Binary(data.asBytes(plugin)));
|
||||
mongoCollectionHelper.insertDocument(userDataTable, doc);
|
||||
} catch (MongoException e) {
|
||||
plugin.log(Level.SEVERE, "Failed to set user data in the database", e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Update a saved {@link DataSnapshot} by given version UUID
|
||||
*
|
||||
* @param user The user whose data snapshot
|
||||
* @param data The {@link DataSnapshot} to update
|
||||
*/
|
||||
@Blocking
|
||||
@Override
|
||||
public void updateSnapshot(@NotNull User user, @NotNull DataSnapshot.Packed data) {
|
||||
try {
|
||||
Document doc = new Document("player_uuid", user.getUuid().toString()).append("version_uuid", data.getId().toString());
|
||||
Bson updates = Updates.combine(
|
||||
Updates.set("save_cause", data.getSaveCause().name()),
|
||||
Updates.set("pinned", data.isPinned()),
|
||||
Updates.set("data", new Binary(data.asBytes(plugin)))
|
||||
);
|
||||
mongoCollectionHelper.updateDocument(userDataTable, doc, updates);
|
||||
} catch (MongoException e) {
|
||||
plugin.log(Level.SEVERE, "Failed to pin user data in the database", e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Wipes <b>all</b> {@link User} entries from the database.
|
||||
* <b>This should only be used when preparing tables for a data migration.</b>
|
||||
*/
|
||||
@Blocking
|
||||
@Override
|
||||
public void wipeDatabase() {
|
||||
try {
|
||||
mongoCollectionHelper.deleteCollection(usersTable);
|
||||
} catch (MongoException e) {
|
||||
plugin.log(Level.SEVERE, "Failed to wipe the database", e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Close the database connection
|
||||
*/
|
||||
@Override
|
||||
public void terminate() {
|
||||
if (mongoConnectionHandler != null) {
|
||||
mongoConnectionHandler.closeConnection();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,93 @@
|
||||
/*
|
||||
* This file is part of HuskSync, licensed under the Apache License 2.0.
|
||||
*
|
||||
* Copyright (c) William278 <will27528@gmail.com>
|
||||
* Copyright (c) contributors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package net.william278.husksync.database.mongo;
|
||||
|
||||
import com.mongodb.client.MongoCollection;
|
||||
import org.bson.Document;
|
||||
import org.bson.conversions.Bson;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
public class MongoCollectionHelper {
|
||||
private final MongoConnectionHandler database;
|
||||
|
||||
/**
|
||||
* Initialize the collection helper
|
||||
* @param database Instance of {@link MongoConnectionHandler}
|
||||
*/
|
||||
public MongoCollectionHelper(@NotNull MongoConnectionHandler database) {
|
||||
this.database = database;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a collection
|
||||
* @param collectionName the collection name
|
||||
*/
|
||||
public void createCollection(@NotNull String collectionName) {
|
||||
database.getDatabase().createCollection(collectionName);
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete a collection
|
||||
* @param collectionName the collection name
|
||||
*/
|
||||
public void deleteCollection(@NotNull String collectionName) {
|
||||
database.getDatabase().getCollection(collectionName).drop();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a collection
|
||||
* @param collectionName the collection name
|
||||
* @return MongoCollection<Document>
|
||||
*/
|
||||
public MongoCollection<Document> getCollection(@NotNull String collectionName) {
|
||||
return database.getDatabase().getCollection(collectionName);
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a document to a collection
|
||||
* @param collectionName collection to add to
|
||||
* @param document Document to add
|
||||
*/
|
||||
public void insertDocument(@NotNull String collectionName, @NotNull Document document) {
|
||||
MongoCollection<Document> collection = database.getDatabase().getCollection(collectionName);
|
||||
collection.insertOne(document);
|
||||
}
|
||||
|
||||
/**
|
||||
* Update a document
|
||||
* @param collectionName collection the document is in
|
||||
* @param document filter of document
|
||||
* @param updates Bson of updates
|
||||
*/
|
||||
public void updateDocument(@NotNull String collectionName, @NotNull Document document, @NotNull Bson updates) {
|
||||
MongoCollection<Document> collection = database.getDatabase().getCollection(collectionName);
|
||||
collection.updateOne(document, updates);
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete a document
|
||||
* @param collectionName collection the document is in
|
||||
* @param document filter to remove
|
||||
*/
|
||||
public void deleteDocument(@NotNull String collectionName, @NotNull Document document) {
|
||||
MongoCollection<Document> collection = database.getDatabase().getCollection(collectionName);
|
||||
collection.deleteOne(document);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
/*
|
||||
* This file is part of HuskSync, licensed under the Apache License 2.0.
|
||||
*
|
||||
* Copyright (c) William278 <will27528@gmail.com>
|
||||
* Copyright (c) contributors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package net.william278.husksync.database.mongo;
|
||||
|
||||
import com.mongodb.MongoClientSettings;
|
||||
import com.mongodb.MongoCredential;
|
||||
import com.mongodb.ServerAddress;
|
||||
import com.mongodb.client.MongoClient;
|
||||
import com.mongodb.client.MongoClients;
|
||||
import com.mongodb.client.MongoDatabase;
|
||||
import lombok.Getter;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
import java.util.Collections;
|
||||
|
||||
@Getter
|
||||
public class MongoConnectionHandler {
|
||||
private final MongoClient mongoClient;
|
||||
private final MongoDatabase database;
|
||||
|
||||
/**
|
||||
* Initiate a connection to a Mongo Server
|
||||
* @param host The IP/Host Name of the Mongo Server
|
||||
* @param port The Port of the Mongo Server
|
||||
* @param username The Username of the user with the appropriate permissions
|
||||
* @param password The Password of the user with the appropriate permissions
|
||||
* @param databaseName The database to use.
|
||||
* @param authDb The database to authenticate with.
|
||||
*/
|
||||
public MongoConnectionHandler(@NotNull String host, @NotNull Integer port, @NotNull String username, @NotNull String password, @NotNull String databaseName, @NotNull String authDb) {
|
||||
final ServerAddress serverAddress = new ServerAddress(host, port);
|
||||
final MongoCredential credential = MongoCredential.createCredential(username, authDb, password.toCharArray());
|
||||
|
||||
final MongoClientSettings settings = MongoClientSettings.builder()
|
||||
.credential(credential)
|
||||
.applyToClusterSettings(builder -> builder.hosts(Collections.singletonList(serverAddress)))
|
||||
.build();
|
||||
|
||||
this.mongoClient = MongoClients.create(settings);
|
||||
this.database = mongoClient.getDatabase(databaseName);
|
||||
}
|
||||
|
||||
/**
|
||||
* Close the connection with the database
|
||||
*/
|
||||
public void closeConnection() {
|
||||
if (this.mongoClient != null) {
|
||||
this.mongoClient.close();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -82,7 +82,7 @@ public abstract class EventListener {
|
||||
usersInWorld.stream()
|
||||
.filter(user -> !plugin.isLocked(user.getUuid()) && !user.isNpc())
|
||||
.forEach(user -> plugin.getDataSyncer().saveData(
|
||||
user, user.createSnapshot(DataSnapshot.SaveCause.WORLD_SAVE), null
|
||||
user, user.createSnapshot(DataSnapshot.SaveCause.WORLD_SAVE)
|
||||
));
|
||||
}
|
||||
|
||||
@@ -101,7 +101,7 @@ public abstract class EventListener {
|
||||
|
||||
final DataSnapshot.Packed snapshot = user.createSnapshot(DataSnapshot.SaveCause.DEATH);
|
||||
snapshot.edit(plugin, (data -> data.getInventory().ifPresent(inventory -> inventory.setContents(items))));
|
||||
plugin.getDataSyncer().saveData(user, snapshot, (u, d) -> plugin.getRedisManager().sendUserDataUpdate(u, d));
|
||||
plugin.getDataSyncer().saveData(user, snapshot);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -124,7 +124,9 @@ public abstract class EventListener {
|
||||
.forEach(user -> {
|
||||
plugin.lockPlayer(user.getUuid());
|
||||
plugin.getDataSyncer().saveData(
|
||||
user, user.createSnapshot(DataSnapshot.SaveCause.SERVER_SHUTDOWN), null
|
||||
user,
|
||||
user.createSnapshot(DataSnapshot.SaveCause.SERVER_SHUTDOWN),
|
||||
(saved, data) -> plugin.getRedisManager().clearUserData(saved)
|
||||
);
|
||||
});
|
||||
|
||||
@@ -170,7 +172,6 @@ public abstract class EventListener {
|
||||
return Map.entry(name().toLowerCase(), defaultPriority.name());
|
||||
}
|
||||
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
@NotNull
|
||||
public static Map<String, String> getDefaults() {
|
||||
|
||||
@@ -119,6 +119,22 @@ public abstract class DataSyncer {
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Save a {@link DataSnapshot.Packed user's data snapshot} to the database,
|
||||
* first firing the {@link net.william278.husksync.event.DataSaveEvent}. This will not update data on Redis.
|
||||
*
|
||||
* @param user the user to save the data for
|
||||
* @param data the data to save
|
||||
* @apiNote Data will not be saved if the {@link net.william278.husksync.event.DataSaveEvent} is cancelled.
|
||||
* Note that this method can also edit the data before saving it.
|
||||
* @implNote Note that the {@link net.william278.husksync.event.DataSaveEvent} will <b>not</b> be fired if the
|
||||
* save cause is {@link DataSnapshot.SaveCause#SERVER_SHUTDOWN}.
|
||||
* @since 3.3.3
|
||||
*/
|
||||
public void saveData(@NotNull User user, @NotNull DataSnapshot.Packed data) {
|
||||
saveData(user, data, null);
|
||||
}
|
||||
|
||||
// Adds a snapshot to the database and runs the after consumer
|
||||
@Blocking
|
||||
private void addSnapshotToDatabase(@NotNull User user, @NotNull DataSnapshot.Packed data,
|
||||
|
||||
@@ -1,33 +1,33 @@
|
||||
locales:
|
||||
synchronization_complete: '[⏵資料已同步!](#00fb9a)'
|
||||
synchronization_complete: '[⏵ 資料已同步!](#00fb9a)'
|
||||
synchronization_failed: '[⏵ 無法同步您的資料! 請聯繫管理員](#ff7e5e)'
|
||||
inventory_viewer_menu_title: '&0%1% 的背包'
|
||||
ender_chest_viewer_menu_title: '&0%1% 的終界箱'
|
||||
inventory_viewer_opened: '[查看](#00fb9a) [%1%](#00fb9a bold) [於 ⌚ %2% 的背包快照資料](#00fb9a)'
|
||||
ender_chest_viewer_opened: '[查看](#00fb9a) [%1%](#00fb9a bold) [於 ⌚ %2% 的終界箱快照資料](#00fb9a)'
|
||||
data_update_complete: '[🔔 你的資料已更新!](#00fb9a)'
|
||||
data_update_failed: '[🔔 無法更新您的資料! 請聯繫管理員](#ff7e5e)'
|
||||
user_registration_complete: '[⭐ User registration complete!](#00fb9a)'
|
||||
data_manager_title: '[查看](#00fb9a) [%3%](#00fb9a bold show_text=&7玩家 UUID:\n&8%4%) [的快照:](#00fb9a) [%1%](#00fb9a show_text=&7Version UUID:\n&8%2%) [:](#00fb9a)'
|
||||
data_manager_timestamp: '[⌚ %1%](#ffc43b-#f5c962 show_text=&7快照時間:\n&8何時保存的資料)'
|
||||
data_manager_pinned: '[※ 被標記的快照](#d8ff2b show_text=&7標記:\n&8此快照資料不會自動輪換更新)'
|
||||
data_manager_cause: '[⚑ %1%](#23a825-#36f539 show_text=&7保存原因:\n&8保存此快照的原因)'
|
||||
data_manager_server: '[☁ %1%](#ff87b3-#f5538e show_text=&7Server:\n&8Name of the server the data was saved on)'
|
||||
data_manager_size: '[⏏ %1%](color=#62a9f5-#7ab8fa show_text=&7Snapshot size:\n&8Estimated file size of the snapshot (in KiB))\n'
|
||||
inventory_viewer_opened: '[查看備份](#00fb9a) [%1%](#00fb9a bold) [於 ⌚ %2% 的背包備份](#00fb9a)'
|
||||
ender_chest_viewer_opened: '[查看備份](#00fb9a) [%1%](#00fb9a bold) [於 ⌚ %2% 的終界箱備份](#00fb9a)'
|
||||
data_update_complete: '[🔔 你的數據資料已更新!](#00fb9a)'
|
||||
data_update_failed: '[🔔 無法更新你的數據資料! 請聯繫管理員](#ff7e5e)'
|
||||
user_registration_complete: '[⭐ 用戶註冊完成!](#00fb9a)'
|
||||
data_manager_title: '[正在查看玩家](#00fb9a) [%3%](#00fb9a bold show_text=&7玩家UUID:\n&8%4%) [的數據備份](#00fb9a) [%1%](#00fb9a show_text=&7備份版本UUID:\n&8%2%)[:](#00fb9a)'
|
||||
data_manager_timestamp: '[⌚ %1%](#ffc43b-#f5c962 show_text=&7備份時間:\n&8數據保存時間)'
|
||||
data_manager_pinned: '[※ 被標記的備份](#d8ff2b show_text=&7已標記:\n&8此玩家數據備份不會按照備份時間自動排序)'
|
||||
data_manager_cause: '[⚑ %1%](#23a825-#36f539 show_text=&7保存原因:\n&8導致數據保存的原因)'
|
||||
data_manager_server: '[☁ %1%](#ff87b3-#f5538e show_text=&7伺服器:\n&8數據保存所在伺服器的名稱)'
|
||||
data_manager_size: '[⏏ %1%](color=#62a9f5-#7ab8fa show_text=&7備份大小:\n&8備份的估計文件大小(以KiB為單位))\n'
|
||||
data_manger_status: '[%1%](red)[/](gray)[%2%](red)[×](gray)[❤](red show_text=&7血量) [%3%](yellow)[×](gray)[🍖](yellow show_text=&7飽食度) [ʟᴠ](green)[.](gray)[%4%](green show_text=&7經驗等級) [🏹 %5%](dark_aqua show_text=&7遊戲模式)'
|
||||
data_manager_advancements_statistics: '[⭐ 成就: %1%](color=#ffc43b-#f5c962 show_text=&7已獲得的成就:\n&8%2%) [⌛ 遊戲時間: %3%ʜʀs](color=#62a9f5-#7ab8fa show_text=&7遊戲內的遊玩時間\n&8⚠ 根據遊戲內統計)\n'
|
||||
data_manager_advancements_statistics: '[⭐ 成就: %1%](color=#ffc43b-#f5c962 show_text=&7已獲得的成就:\n&8%2%) [⌛ 遊玩時間: %3%小時](color=#62a9f5-#7ab8fa show_text=&7在遊戲内遊玩的時間\n&8⚠ 基於遊戲内的統計訊息)\n'
|
||||
data_manager_item_buttons: '[查看:](gray) [[🪣 背包…]](color=#a17b5f-#f5b98c show_text=&7點擊查看 run_command=/inventory %1% %2%) [[⌀ 終界箱…]](#b649c4-#d254ff show_text=&7點擊查看 run_command=/enderchest %1% %2%)'
|
||||
data_manager_management_buttons: '[管理:](gray) [[❌ 刪除…]](#ff3300 show_text=&7點擊刪除這個快照\n&8這不會影像目前玩家的資料\n&#ff3300&⚠ 此操作不能取消! suggest_command=/husksync:userdata delete %1% %2%) [[⏪ 恢復…]](#00fb9a show_text=&7點擊將玩家資料覆蓋為此快照\n&8這將導致玩家的資料會被此快照覆蓋\n&#ff3300&⚠ %1% 當前的資料將被覆蓋! suggest_command=/husksync:userdata restore %1% %2%) [[※ 標記…]](#d8ff2b show_text=&7點擊切換標記狀態\n&8被標記的快照將不會自動輪換更新 run_command=/userdata pin %1% %2%)'
|
||||
data_manager_system_buttons: '[系統:](gray) [[⏷ 本地轉存…]](dark_gray show_text=&7點擊將此玩家資料快照轉存到本地文件中\n&8轉存的資料可以在以下路徑找到 ~/plugins/HuskSync/dumps/ run_command=/husksync:userdata dump %1% %2% file) [[☂ 雲端轉存…]](dark_gray show_text=&7點擊將此玩家資料快照轉存到 mc-logs 服務\n&8您將獲得一個包含資料的 URL. run_command=/husksync:userdata dump %1% %2% web)'
|
||||
data_manager_advancements_preview_remaining: '還有 %1% …'
|
||||
data_list_title: '[%1% 的玩家資料快照:](#00fb9a) [(%2%-%3% 共](#00fb9a) [%4%](#00fb9a bold)[)](#00fb9a)\n'
|
||||
data_list_item: '[%1%](gray show_text=&7User Data Snapshot for %2%&8⚡ %4% run_command=/userdata view %2% %3%) [%5%](#d8ff2b show_text=&7Pinned:\n&8Pinned snapshots won''t be automatically rotated. run_command=/userdata view %2% %3%) [%6%](color=#ffc43b-#f5c962 show_text=&7Version timestamp:&7\n&8When the data was saved\n&8%7% run_command=/userdata view %2% %3%) [⚑ %8%](#23a825-#36f539 show_text=&7Save cause:\n&8What caused the data to be saved run_command=/userdata view %2% %3%) [⏏ %9%](color=#62a9f5-#7ab8fa show_text=&7Snapshot size:&7\n&8Estimated file size of the snapshot (in KiB) run_command=/userdata view %2% %3%)'
|
||||
data_deleted: '[❌ 成功刪除:](#00fb9a) [%3%](#00fb9a show_text=&7玩家 UUID:\n&8%4%) [的快照:](#00fb9a) [%1%](#00fb9a show_text=&7Version UUID:\n&8%2%)'
|
||||
data_restored: '[⏪ 成功將玩家](#00fb9a) [%1%](#00fb9a show_text=&7玩家 UUID:\n&8%2%)[的資料恢復為 快照:](#00fb9a) [%3%.](#00fb9a show_text=&7Version UUID:\n&8%4%)'
|
||||
data_pinned: '[※ 成功標記](#00fb9a) [%3%](#00fb9a show_text=&7玩家 UUID:\n&8%4%) [的快照:](#00fb9a) [%1%](#00fb9a show_text=&7Version UUID:\n&8%2%)'
|
||||
data_unpinned: '[※ 成功解除](#00fb9a) [%3%](#00fb9a show_text=&7玩家 UUID:\n&8%4%) [快照:](#00fb9a) [%1%](#00fb9a show_text=&7Version UUID:\n&8%2%) [的標記](#00fb9a)'
|
||||
data_dumped: '[☂ 成功將 %2% 資料快照 %1% 儲存至:](#00fb9a) &7%3%'
|
||||
list_footer: '\n%1%[頁面](#00fb9a) [%2%](#00fb9a)/[%3%](#00fb9a)%4% %5%'
|
||||
data_manager_management_buttons: '[管理:](gray) [[❌ 刪除…]](#ff3300 show_text=&7點擊刪除此玩家數據的備份.\n&8這不會影響玩家的當前數據.\n&#ff3300&⚠ 此操作無法撤銷! suggest_command=/husksync:userdata delete %1% %2%) [[⏪ 恢復…]](#00fb9a show_text=&7點擊還原此玩家的數據.\n&8這將會讓用户的數據恢復到此備份.\n&#ff3300&⚠ %1%當前的數據將被覆蓋! suggest_command=/husksync:userdata restore %1% %2%) [[※ 標記/取消標記…]](#d8ff2b show_text=&7點擊標記或取消標記此玩家數據備份\n&8已標記的備份不會按照備份時間自動排序 run_command=/userdata pin %1% %2%)'
|
||||
data_manager_system_buttons: '[系統:](gray) [[⏷ 本地轉存…]](dark_gray show_text=&7點擊將此玩家數據資料轉存到本地文件中.\n&8轉存的資料可以在以下路徑找到~/plugins/HuskSync/dumps/中找到 run_command=/husksync:userdata dump %1% %2% file) [[☂ 雲端轉存…]](dark_gray show_text=&7點擊將此玩家數據資料轉存到 mc-logs 服務中\n&8您將獲得一個包含資料的URL. run_command=/husksync:userdata dump %1% %2% web)'
|
||||
data_manager_advancements_preview_remaining: '以及其他 %1%…'
|
||||
data_list_title: '[%1%的玩家數據備份:](#00fb9a) [(%2%-%3% of](#00fb9a) [%4%](#00fb9a bold)[)](#00fb9a)\n'
|
||||
data_list_item: '[%1%](gray show_text=&7玩家數據備份 %2%&8⚡ %4% run_command=/userdata view %2% %3%) [%5%](#d8ff2b show_text=&7已標記:\n&8標記的備份不會自動加載 run_command=/userdata view %2% %3%) [%6%](color=#ffc43b-#f5c962 show_text=&7備份時間:&7\n&8數據保存時間\n&8%7% run_command=/userdata view %2% %3%) [⚑ %8%](#23a825-#36f539 show_text=&7保存原因:\n&8導致數據保存的原因 run_command=/userdata view %2% %3%) [⏏ %9%](color=#62a9f5-#7ab8fa show_text=&7備份大小:&7\n&8備份的估計文件大小(以KiB為單位) run_command=/userdata view %2% %3%)'
|
||||
data_deleted: '[❌ 成功刪除玩家](#00fb9a) [%3%](#00fb9a show_text=&7玩家 UUID:\n&7%4%) [的數據備份](#00fb9a) [%1%.](#00fb9a show_text=&7備份版本UUID:\n&7%2%)'
|
||||
data_restored: '[⏪ 成功恢復玩家](#00fb9a) [%1%](#00fb9a show_text=&7玩家 UUID:\n&7%2%)[的數據備份](#00fb9a) [%3%.](#00fb9a show_text=&7備份版本UUID:\n&7%4%)'
|
||||
data_pinned: '[※ 成功標記玩家](#00fb9a) [%3%](#00fb9a show_text=&7玩家 UUID:\n&8%4%) [的數據備份](#00fb9a) [%1%.](#00fb9a show_text=&7備份版本UUID:\n&8%2%)'
|
||||
data_unpinned: '[※ 成功取消標記玩家](#00fb9a) [%3%](#00fb9a show_text=&7玩家 UUID:\n&8%4%) [的數據備份](#00fb9a) [%1%.](#00fb9a show_text=&7備份版本UUID:\n&8%2%)'
|
||||
data_dumped: '[☂ 已成功將 %1% 的玩家數據快照 %2% 儲存至:](#00fb9a) &7%3%'
|
||||
list_footer: '\n%1%[頁數](#00fb9a) [%2%](#00fb9a)/[%3%](#00fb9a)%4% %5%'
|
||||
list_previous_page_button: '[◀](white show_text=&7查看上一頁 run_command=%2% %1%) '
|
||||
list_next_page_button: ' [▶](white show_text=&7查看下一頁 run_command=%2% %1%)'
|
||||
list_page_jumpers: '(%1%)'
|
||||
@@ -35,29 +35,29 @@ locales:
|
||||
list_page_jumper_current_page: '[%1%](#00fb9a)'
|
||||
list_page_jumper_separator: ' '
|
||||
list_page_jumper_group_separator: '…'
|
||||
save_cause_disconnect: 'disconnect'
|
||||
save_cause_world_save: 'world save'
|
||||
save_cause_death: 'death'
|
||||
save_cause_server_shutdown: 'server shutdown'
|
||||
save_cause_inventory_command: 'inventory command'
|
||||
save_cause_enderchest_command: 'enderchest command'
|
||||
save_cause_backup_restore: 'backup restore'
|
||||
save_cause_disconnect: '斷開連接'
|
||||
save_cause_world_save: '保存世界'
|
||||
save_cause_death: '死亡'
|
||||
save_cause_server_shutdown: '伺服器關閉'
|
||||
save_cause_inventory_command: '背包指令'
|
||||
save_cause_enderchest_command: '終界箱指令'
|
||||
save_cause_backup_restore: '備份還原'
|
||||
save_cause_api: 'API'
|
||||
save_cause_mpdb_migration: 'MPDB migration'
|
||||
save_cause_legacy_migration: 'legacy migration'
|
||||
save_cause_converted_from_v2: 'converted from v2'
|
||||
up_to_date: '[HuskSync](#00fb9a bold) [| 您運行的是最新版本的 HuskSync (v%1%).](#00fb9a)'
|
||||
update_available: '[HuskSync](#ff7e5e bold) [| 發現可用的新版本: v%1% (running: v%2%).](#ff7e5e)'
|
||||
reload_complete: '[HuskSync](#00fb9a bold) [| 已重新載入配置和訊息文件](#00fb9a)\n[⚠ Ensure config files are up-to-date on all servers!](#00fb9a)\n[A restart is needed for config changes to take effect.](#00fb9a italic)'
|
||||
system_status_header: '[HuskSync](#00fb9a bold) [| System status report:](#00fb9a)'
|
||||
error_invalid_syntax: '[錯誤:](#ff3300) [語法不正確,用法:](#ff7e5e) [%1%](#ff7e5e italic show_text=&#ff7e5e&Click to suggest suggest_command=%1%)'
|
||||
error_invalid_player: '[錯誤:](#ff3300) [找不到這位玩家](#ff7e5e)'
|
||||
save_cause_mpdb_migration: 'MPDB遷移'
|
||||
save_cause_legacy_migration: '舊版遷移'
|
||||
save_cause_converted_from_v2: '從v2轉換'
|
||||
up_to_date: '[HuskSync](#00fb9a bold) [| 您運行的是最新版本的HuskSync(v%1%).](#00fb9a)'
|
||||
update_available: '[HuskSync](#ff7e5e bold) [| 發現可用的新版本:v%1%(當前版本:v%2%).](#ff7e5e)'
|
||||
reload_complete: '[HuskSync](#00fb9a bold) [| 已重新載入配置和訊息文件.](#00fb9a)\n[⚠ 確保所有伺服器上的配置文件都是最新的!](#00fb9a)\n[需要重新啟動配置更改才能生效.](#00fb9a italic)'
|
||||
system_status_header: '[HuskSync](#00fb9a bold) [| 系統狀態報告:](#00fb9a)'
|
||||
error_invalid_syntax: '[錯誤:](#ff3300) [語法不正確,用法:](#ff7e5e) [%1%](#ff7e5e italic show_text=&#ff7e5e&點擊建議 suggest_command=%1%)'
|
||||
error_invalid_player: '[錯誤:](#ff3300) [找不到這位玩家.](#ff7e5e)'
|
||||
error_no_permission: '[錯誤:](#ff3300) [您沒有權限執行這個指令](#ff7e5e)'
|
||||
error_console_command_only: '[錯誤:](#ff3300) [該指令只能透過 控制台 執行](#ff7e5e)'
|
||||
error_in_game_command_only: '[錯誤:](#ff3300) [該指令只能在遊戲內執行](#ff7e5e)'
|
||||
error_no_data_to_display: '[錯誤:](#ff3300) [找不到任何可顯示的用戶資訊.](#ff7e5e)'
|
||||
error_invalid_version_uuid: '[錯誤:](#ff3300) [找不到正確的 Version UUID.](#ff7e5e)'
|
||||
husksync_command_description: 'Manage the HuskSync plugin'
|
||||
userdata_command_description: 'View, manage & restore player userdata'
|
||||
inventory_command_description: 'View & edit a player''s inventory'
|
||||
enderchest_command_description: 'View & edit a player''s Ender Chest'
|
||||
error_in_game_command_only: '錯誤: 該指令只能在遊戲內執行.'
|
||||
error_no_data_to_display: '[錯誤:](#ff3300) [找不到任何可顯示的玩家數據.](#ff7e5e)'
|
||||
error_invalid_version_uuid: '[錯誤:](#ff3300) [找不到該版本UUID的任何玩家數據.](#ff7e5e)'
|
||||
husksync_command_description: '管理HuskSync插件'
|
||||
userdata_command_description: '查看、管理和恢復玩家用户數據'
|
||||
inventory_command_description: '查看和編輯玩家的背包'
|
||||
enderchest_command_description: '查看和編輯玩家的終界箱'
|
||||
|
||||
@@ -3,7 +3,7 @@ This will walk you through installing HuskSync on your network of Spigot servers
|
||||
## Requirements
|
||||
> **Note:** If the plugin fails to load, please check that you are not running an [incompatible version combination](Unsupported-Versions)
|
||||
|
||||
* A MySQL Database (v8.0+)
|
||||
* A MySQL Database (v8.0+) (or MongoDB Database)
|
||||
* A Redis Database (v5.0+) — see [[FAQs]] for more details.
|
||||
* Any number of Spigot servers, connected by a BungeeCord or Velocity-based proxy (Minecraft v1.17.1+, running Java 17+)
|
||||
|
||||
@@ -15,11 +15,19 @@ This will walk you through installing HuskSync on your network of Spigot servers
|
||||
- Start, then stop every server to let HuskSync generate the [[config file]].
|
||||
- HuskSync will throw an error in the console and disable itself as it is unable to connect to the database. You haven't set the credentials yet, so this is expected.
|
||||
- Advanced users: If you'd prefer, you can just create one config.yml file and create symbolic links in each `/plugins/HuskSync/` folder to it to make updating it easier.
|
||||
### 3. Enter MySQL & Redis database credentials
|
||||
### 3. Enter Mysql & Redis database credentials
|
||||
- Navigate to the HuskSync config file on each server (`~/plugins/HuskSync/config.yml`)
|
||||
- Under `credentials` in the `database` section, enter the credentials of your MySQL Database. You shouldn't touch the `connection_pool` properties.
|
||||
- Under `credentials` in the `redis` section, enter the credentials of your Redis Database. If your Redis server doesn't have a password, leave the password blank as it is.
|
||||
- Unless you want to have multiple clusters of servers within your network, each with separate user data, you should not change the value of `cluster_id`.
|
||||
<details>
|
||||
<summary><b>For MongoDB Users</b></summary>
|
||||
|
||||
- Navigate to the HuskSync config file on each server (`~/plugins/HuskSync/config.yml`)
|
||||
- Under `credentials` in the `database` section, enter the credentials of your MongoDB Database. You shouldn't touch the `connection_pool` properties.
|
||||
- Be sure to fill in the `mongo_auth_db` field with the database that the username and password is authenticated in. (In most cases this will {and should be} be the same database as the database your trying to connect to.)
|
||||
</details>
|
||||
|
||||
### 4. Set server names in server.yml files
|
||||
- Navigate to the HuskSync server name file on each server (`~/plugins/HuskSync/server.yml`)
|
||||
- Set the `name:` of the server in this file to the ID of this server as defined in the config of your proxy (e.g., if this is the "hub" server you access with `/server hub`, put `'hub'` here)
|
||||
|
||||
@@ -3,11 +3,12 @@ org.gradle.jvmargs='-Dfile.encoding=UTF-8'
|
||||
org.gradle.daemon=true
|
||||
javaVersion=17
|
||||
|
||||
plugin_version=3.3.2
|
||||
plugin_version=3.4
|
||||
plugin_archive=husksync
|
||||
plugin_description=A modern, cross-server player data synchronization system
|
||||
|
||||
jedis_version=5.1.0
|
||||
mysql_driver_version=8.3.0
|
||||
mariadb_driver_version=3.3.2
|
||||
mongodb_driver_version=3.12.14
|
||||
snappy_version=1.1.10.5
|
||||
|
||||
@@ -23,7 +23,7 @@ shadowJar {
|
||||
relocate 'org.jetbrains', 'net.william278.husksync.libraries'
|
||||
relocate 'org.intellij', 'net.william278.husksync.libraries'
|
||||
relocate 'com.zaxxer', 'net.william278.husksync.libraries'
|
||||
relocate 'de.exlll', 'net.william278.huskclaims.libraries'
|
||||
relocate 'de.exlll', 'net.william278.husksync.libraries'
|
||||
relocate 'net.william278.desertwell', 'net.william278.husksync.libraries.desertwell'
|
||||
relocate 'net.william278.paginedown', 'net.william278.husksync.libraries.paginedown'
|
||||
relocate 'net.william278.mapdataapi', 'net.william278.husksync.libraries.mapdataapi'
|
||||
|
||||
@@ -3,4 +3,5 @@ libraries:
|
||||
- 'redis.clients:jedis:${jedis_version}'
|
||||
- 'com.mysql:mysql-connector-j:${mysql_driver_version}'
|
||||
- 'org.mariadb.jdbc:mariadb-java-client:${mariadb_driver_version}'
|
||||
- 'org.mongodb:mongodb-driver:${mongodb_driver_version}'
|
||||
- 'org.xerial.snappy:snappy-java:${snappy_version}'
|
||||
Reference in New Issue
Block a user