mirror of
https://github.com/WiIIiam278/HuskSync.git
synced 2025-12-25 17:49:20 +00:00
Compare commits
76 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
eaa2ed74a6 | ||
|
|
44c652c452 | ||
|
|
78cf6bff63 | ||
|
|
8ad4158ec0 | ||
|
|
405e6d7162 | ||
|
|
cff1c8f982 | ||
|
|
f43ca2f043 | ||
|
|
3114ab1a62 | ||
|
|
2da9749b0c | ||
|
|
d4d510e100 | ||
|
|
550ea26097 | ||
|
|
2b1e72a42e | ||
|
|
75f8bee706 | ||
|
|
a3b50a0bf5 | ||
|
|
e9ab0909ce | ||
|
|
1e91b4b4ce | ||
|
|
043b51d812 | ||
|
|
fa5cea2aa3 | ||
|
|
e35dcf3aad | ||
|
|
68ec79add6 | ||
|
|
70235963ba | ||
|
|
245fbec80c | ||
|
|
4d1a465c03 | ||
|
|
07dc0b8c12 | ||
|
|
525f15e65b | ||
|
|
017d26673a | ||
|
|
087c787ec2 | ||
|
|
7218390f65 | ||
|
|
bd312c48ea | ||
|
|
e4cc792f54 | ||
|
|
7941745ed0 | ||
|
|
21f125c48a | ||
|
|
18b8b958fe | ||
|
|
35c23c7970 | ||
|
|
4bb38f67d3 | ||
|
|
98cf42065b | ||
|
|
328d4476aa | ||
|
|
8293d767da | ||
|
|
7b8fb92737 | ||
|
|
0f1cc2d24f | ||
|
|
676ba7a10a | ||
|
|
82dc765f66 | ||
|
|
16cfbc9410 | ||
|
|
2b4c7e6c3d | ||
|
|
a03d540938 | ||
|
|
6bcb3e7908 | ||
|
|
facbda65a8 | ||
|
|
2f5ddf6164 | ||
|
|
4dfbc0e32b | ||
|
|
07d0376dd6 | ||
|
|
d23ea087c1 | ||
|
|
ea77f2d782 | ||
|
|
ef3dc7e602 | ||
|
|
3fe6245ddf | ||
|
|
a35e83a424 | ||
|
|
be5d1128de | ||
|
|
8463e1bb7a | ||
|
|
5456b232f0 | ||
|
|
b0e585841c | ||
|
|
cd298af5ae | ||
|
|
e19477aada | ||
|
|
7f75b9a917 | ||
|
|
819421492b | ||
|
|
8f13a3955c | ||
|
|
73de0ff392 | ||
|
|
93edb0de4c | ||
|
|
bb5ae0b741 | ||
|
|
ccd7601a0e | ||
|
|
50d15e9580 | ||
|
|
aa1e8b8e95 | ||
|
|
3ff01f7bb3 | ||
|
|
93ab25bf44 | ||
|
|
4c0addfd67 | ||
|
|
b77cf2524d | ||
|
|
501ea3f609 | ||
|
|
a93af95fd2 |
@@ -26,7 +26,7 @@
|
|||||||
</p>
|
</p>
|
||||||
<br/>
|
<br/>
|
||||||
|
|
||||||
**HuskSync** is a modern, cross-server player data synchronization system that enables the comprehensive synchronization of your user's data across multiple proxied servers. It does this by making use of Redis and MySQL to optimally cache data while players change servers.
|
**HuskSync** is a modern, cross-server player data synchronization system that enables the comprehensive synchronization of your user's data across multiple proxied servers. It does this by making use of Redis and a MySQL/Mongo/PostgreSQL to optimally cache data while players change servers.
|
||||||
|
|
||||||
## Features
|
## Features
|
||||||
**⭐ Seamless synchronization** — Utilises optimised Redis caching when players change server to sync player data super quickly for a seamless experience.
|
**⭐ Seamless synchronization** — Utilises optimised Redis caching when players change server to sync player data super quickly for a seamless experience.
|
||||||
@@ -44,11 +44,11 @@
|
|||||||
**Ready?** [It's syncing time!](https://william278.net/docs/husksync/setup)
|
**Ready?** [It's syncing time!](https://william278.net/docs/husksync/setup)
|
||||||
|
|
||||||
## Setup
|
## Setup
|
||||||
Requires a MySQL (v8.0+) database, a Redis (v5.0+) server and any number of Spigot-based 1.17.1+ Minecraft servers, running Java 17+.
|
Requires a MySQL/Mongo/PostgreSQL database, a Redis (v5.0+) server and any number of Spigot-based 1.17.1+ Minecraft servers, running Java 17+.
|
||||||
|
|
||||||
1. Place the plugin jar file in the /plugins/ directory of each Spigot server. You do not need to install HuskSync as a proxy plugin.
|
1. Place the plugin jar file in the /plugins/ directory of each Spigot server. You do not need to install HuskSync as a proxy plugin.
|
||||||
2. Start, then stop every server to let HuskSync generate the config file.
|
2. Start, then stop every server to let HuskSync generate the config file.
|
||||||
3. Navigate to the HuskSync config file on each server (~/plugins/HuskSync/config.yml) and fill in both the MySQL and Redis database credentials.
|
3. Navigate to the HuskSync config file on each server (~/plugins/HuskSync/config.yml) and fill in both your database and Redis server credentials.
|
||||||
4. Start every server again and synchronization will begin.
|
4. Start every server again and synchronization will begin.
|
||||||
|
|
||||||
## Development
|
## Development
|
||||||
|
|||||||
21
build.gradle
21
build.gradle
@@ -20,6 +20,7 @@ ext {
|
|||||||
set 'jedis_version', jedis_version.toString()
|
set 'jedis_version', jedis_version.toString()
|
||||||
set 'mysql_driver_version', mysql_driver_version.toString()
|
set 'mysql_driver_version', mysql_driver_version.toString()
|
||||||
set 'mariadb_driver_version', mariadb_driver_version.toString()
|
set 'mariadb_driver_version', mariadb_driver_version.toString()
|
||||||
|
set 'postgres_driver_version', postgres_driver_version.toString()
|
||||||
set 'mongodb_driver_version', mongodb_driver_version.toString()
|
set 'mongodb_driver_version', mongodb_driver_version.toString()
|
||||||
set 'snappy_version', snappy_version.toString()
|
set 'snappy_version', snappy_version.toString()
|
||||||
}
|
}
|
||||||
@@ -69,6 +70,7 @@ allprojects {
|
|||||||
mavenLocal()
|
mavenLocal()
|
||||||
mavenCentral()
|
mavenCentral()
|
||||||
maven { url 'https://hub.spigotmc.org/nexus/content/repositories/snapshots/' }
|
maven { url 'https://hub.spigotmc.org/nexus/content/repositories/snapshots/' }
|
||||||
|
maven { url "https://repo.dmulloy2.net/repository/public/" }
|
||||||
maven { url 'https://repo.codemc.io/repository/maven-public/' }
|
maven { url 'https://repo.codemc.io/repository/maven-public/' }
|
||||||
maven { url 'https://repo.minebench.de/' }
|
maven { url 'https://repo.minebench.de/' }
|
||||||
maven { url 'https://repo.alessiodp.com/releases/' }
|
maven { url 'https://repo.alessiodp.com/releases/' }
|
||||||
@@ -166,15 +168,20 @@ logger.lifecycle("Building HuskSync ${version} by William278")
|
|||||||
|
|
||||||
@SuppressWarnings('GrMethodMayBeStatic')
|
@SuppressWarnings('GrMethodMayBeStatic')
|
||||||
def versionMetadata() {
|
def versionMetadata() {
|
||||||
// Get if there is a tag for this commit
|
// Require grgit
|
||||||
|
if (grgit == null) {
|
||||||
|
return '-unknown'
|
||||||
|
}
|
||||||
|
|
||||||
|
// If unclean, return the last commit hash with -indev
|
||||||
|
if (!grgit.status().clean) {
|
||||||
|
return '-' + grgit.head().abbreviatedId + '-indev'
|
||||||
|
}
|
||||||
|
|
||||||
|
// Otherwise if this matches a tag, return nothing
|
||||||
def tag = grgit.tag.list().find { it.commit.id == grgit.head().id }
|
def tag = grgit.tag.list().find { it.commit.id == grgit.head().id }
|
||||||
if (tag != null) {
|
if (tag != null) {
|
||||||
return ''
|
return ''
|
||||||
}
|
}
|
||||||
|
return '-' + grgit.head().abbreviatedId
|
||||||
// Otherwise, get the last commit hash and if it's a clean head
|
|
||||||
if (grgit == null) {
|
|
||||||
return '-' + System.getenv("GITHUB_RUN_NUMBER") ? 'build.' + System.getenv("GITHUB_RUN_NUMBER") : 'unknown'
|
|
||||||
}
|
|
||||||
return '-' + grgit.head().abbreviatedId + (grgit.status().clean ? '' : '-indev')
|
|
||||||
}
|
}
|
||||||
@@ -9,21 +9,23 @@ dependencies {
|
|||||||
implementation 'me.lucko:commodore:2.2'
|
implementation 'me.lucko:commodore:2.2'
|
||||||
implementation 'net.kyori:adventure-platform-bukkit:4.3.2'
|
implementation 'net.kyori:adventure-platform-bukkit:4.3.2'
|
||||||
implementation 'dev.triumphteam:triumph-gui:3.1.7'
|
implementation 'dev.triumphteam:triumph-gui:3.1.7'
|
||||||
implementation 'space.arim.morepaperlib:morepaperlib:0.4.3'
|
implementation 'space.arim.morepaperlib:morepaperlib:0.4.4'
|
||||||
implementation 'de.tr7zw:item-nbt-api:2.12.2'
|
implementation 'de.tr7zw:item-nbt-api:2.12.4'
|
||||||
|
|
||||||
compileOnly 'org.spigotmc:spigot-api:1.17.1-R0.1-SNAPSHOT'
|
compileOnly 'org.spigotmc:spigot-api:1.17.1-R0.1-SNAPSHOT'
|
||||||
compileOnly 'org.projectlombok:lombok:1.18.30'
|
compileOnly 'com.github.retrooper.packetevents:spigot:2.3.0'
|
||||||
compileOnly 'commons-io:commons-io:2.15.1'
|
compileOnly 'com.comphenix.protocol:ProtocolLib:5.1.0'
|
||||||
compileOnly 'org.json:json:20240205'
|
compileOnly 'org.projectlombok:lombok:1.18.32'
|
||||||
compileOnly 'de.themoep:minedown-adventure:1.7.2-SNAPSHOT'
|
compileOnly 'commons-io:commons-io:2.16.1'
|
||||||
compileOnly 'com.github.Exlll.ConfigLib:configlib-yaml:v4.5.0'
|
compileOnly 'org.json:json:20240303'
|
||||||
|
compileOnly 'net.william278:minedown:1.8.2'
|
||||||
|
compileOnly 'de.exlll:configlib-yaml:4.5.0'
|
||||||
compileOnly 'com.zaxxer:HikariCP:5.1.0'
|
compileOnly 'com.zaxxer:HikariCP:5.1.0'
|
||||||
compileOnly 'net.william278:DesertWell:2.0.4'
|
compileOnly 'net.william278:DesertWell:2.0.4'
|
||||||
compileOnly 'net.william278:AdvancementAPI:97a9583413'
|
compileOnly 'net.william278:AdvancementAPI:97a9583413'
|
||||||
compileOnly "redis.clients:jedis:$jedis_version"
|
compileOnly "redis.clients:jedis:$jedis_version"
|
||||||
|
|
||||||
annotationProcessor 'org.projectlombok:lombok:1.18.30'
|
annotationProcessor 'org.projectlombok:lombok:1.18.32'
|
||||||
}
|
}
|
||||||
|
|
||||||
shadowJar {
|
shadowJar {
|
||||||
|
|||||||
@@ -38,17 +38,14 @@ import net.william278.husksync.command.BukkitCommand;
|
|||||||
import net.william278.husksync.config.Locales;
|
import net.william278.husksync.config.Locales;
|
||||||
import net.william278.husksync.config.Server;
|
import net.william278.husksync.config.Server;
|
||||||
import net.william278.husksync.config.Settings;
|
import net.william278.husksync.config.Settings;
|
||||||
import net.william278.husksync.data.BukkitSerializer;
|
import net.william278.husksync.data.*;
|
||||||
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.Database;
|
||||||
import net.william278.husksync.database.MongoDbDatabase;
|
import net.william278.husksync.database.MongoDbDatabase;
|
||||||
import net.william278.husksync.database.MySqlDatabase;
|
import net.william278.husksync.database.MySqlDatabase;
|
||||||
|
import net.william278.husksync.database.PostgresDatabase;
|
||||||
import net.william278.husksync.event.BukkitEventDispatcher;
|
import net.william278.husksync.event.BukkitEventDispatcher;
|
||||||
import net.william278.husksync.hook.PlanHook;
|
import net.william278.husksync.hook.PlanHook;
|
||||||
import net.william278.husksync.listener.BukkitEventListener;
|
import net.william278.husksync.listener.BukkitEventListener;
|
||||||
import net.william278.husksync.listener.EventListener;
|
|
||||||
import net.william278.husksync.migrator.LegacyMigrator;
|
import net.william278.husksync.migrator.LegacyMigrator;
|
||||||
import net.william278.husksync.migrator.Migrator;
|
import net.william278.husksync.migrator.Migrator;
|
||||||
import net.william278.husksync.migrator.MpdbMigrator;
|
import net.william278.husksync.migrator.MpdbMigrator;
|
||||||
@@ -68,6 +65,7 @@ import org.jetbrains.annotations.NotNull;
|
|||||||
import space.arim.morepaperlib.MorePaperLib;
|
import space.arim.morepaperlib.MorePaperLib;
|
||||||
import space.arim.morepaperlib.commands.CommandRegistration;
|
import space.arim.morepaperlib.commands.CommandRegistration;
|
||||||
import space.arim.morepaperlib.scheduling.AsynchronousScheduler;
|
import space.arim.morepaperlib.scheduling.AsynchronousScheduler;
|
||||||
|
import space.arim.morepaperlib.scheduling.AttachedScheduler;
|
||||||
import space.arim.morepaperlib.scheduling.GracefulScheduling;
|
import space.arim.morepaperlib.scheduling.GracefulScheduling;
|
||||||
import space.arim.morepaperlib.scheduling.RegionalScheduler;
|
import space.arim.morepaperlib.scheduling.RegionalScheduler;
|
||||||
|
|
||||||
@@ -99,7 +97,7 @@ public class BukkitHuskSync extends JavaPlugin implements HuskSync, BukkitTask.S
|
|||||||
private MorePaperLib paperLib;
|
private MorePaperLib paperLib;
|
||||||
private Database database;
|
private Database database;
|
||||||
private RedisManager redisManager;
|
private RedisManager redisManager;
|
||||||
private EventListener eventListener;
|
private BukkitEventListener eventListener;
|
||||||
private DataAdapter dataAdapter;
|
private DataAdapter dataAdapter;
|
||||||
private DataSyncer dataSyncer;
|
private DataSyncer dataSyncer;
|
||||||
private LegacyConverter legacyConverter;
|
private LegacyConverter legacyConverter;
|
||||||
@@ -114,11 +112,10 @@ public class BukkitHuskSync extends JavaPlugin implements HuskSync, BukkitTask.S
|
|||||||
private Server serverName;
|
private Server serverName;
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onEnable() {
|
public void onLoad() {
|
||||||
// Initial plugin setup
|
// Initial plugin setup
|
||||||
this.disabling = false;
|
this.disabling = false;
|
||||||
this.gson = createGson();
|
this.gson = createGson();
|
||||||
this.audiences = BukkitAudiences.create(this);
|
|
||||||
this.paperLib = new MorePaperLib(this);
|
this.paperLib = new MorePaperLib(this);
|
||||||
|
|
||||||
// Load settings and locales
|
// Load settings and locales
|
||||||
@@ -128,6 +125,13 @@ public class BukkitHuskSync extends JavaPlugin implements HuskSync, BukkitTask.S
|
|||||||
loadServer();
|
loadServer();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
this.eventListener = createEventListener();
|
||||||
|
eventListener.onLoad();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onEnable() {
|
||||||
|
this.audiences = BukkitAudiences.create(this);
|
||||||
// Prepare data adapter
|
// Prepare data adapter
|
||||||
initialize("data adapter", (plugin) -> {
|
initialize("data adapter", (plugin) -> {
|
||||||
if (settings.getSynchronization().isCompressData()) {
|
if (settings.getSynchronization().isCompressData()) {
|
||||||
@@ -142,13 +146,15 @@ public class BukkitHuskSync extends JavaPlugin implements HuskSync, BukkitTask.S
|
|||||||
registerSerializer(Identifier.INVENTORY, new BukkitSerializer.Inventory(this));
|
registerSerializer(Identifier.INVENTORY, new BukkitSerializer.Inventory(this));
|
||||||
registerSerializer(Identifier.ENDER_CHEST, new BukkitSerializer.EnderChest(this));
|
registerSerializer(Identifier.ENDER_CHEST, new BukkitSerializer.EnderChest(this));
|
||||||
registerSerializer(Identifier.ADVANCEMENTS, new BukkitSerializer.Advancements(this));
|
registerSerializer(Identifier.ADVANCEMENTS, new BukkitSerializer.Advancements(this));
|
||||||
registerSerializer(Identifier.LOCATION, new BukkitSerializer.Location(this));
|
registerSerializer(Identifier.LOCATION, new BukkitSerializer.Json<>(this, BukkitData.Location.class));
|
||||||
registerSerializer(Identifier.HEALTH, new BukkitSerializer.Health(this));
|
registerSerializer(Identifier.HEALTH, new BukkitSerializer.Json<>(this, BukkitData.Health.class));
|
||||||
registerSerializer(Identifier.HUNGER, new BukkitSerializer.Hunger(this));
|
registerSerializer(Identifier.HUNGER, new BukkitSerializer.Json<>(this, BukkitData.Hunger.class));
|
||||||
registerSerializer(Identifier.GAME_MODE, new BukkitSerializer.GameMode(this));
|
registerSerializer(Identifier.ATTRIBUTES, new BukkitSerializer.Json<>(this, BukkitData.Attributes.class));
|
||||||
|
registerSerializer(Identifier.GAME_MODE, new BukkitSerializer.Json<>(this, BukkitData.GameMode.class));
|
||||||
|
registerSerializer(Identifier.FLIGHT_STATUS, new BukkitSerializer.Json<>(this, BukkitData.FlightStatus.class));
|
||||||
registerSerializer(Identifier.POTION_EFFECTS, new BukkitSerializer.PotionEffects(this));
|
registerSerializer(Identifier.POTION_EFFECTS, new BukkitSerializer.PotionEffects(this));
|
||||||
registerSerializer(Identifier.STATISTICS, new BukkitSerializer.Statistics(this));
|
registerSerializer(Identifier.STATISTICS, new BukkitSerializer.Json<>(this, BukkitData.Statistics.class));
|
||||||
registerSerializer(Identifier.EXPERIENCE, new BukkitSerializer.Experience(this));
|
registerSerializer(Identifier.EXPERIENCE, new BukkitSerializer.Json<>(this, BukkitData.Experience.class));
|
||||||
registerSerializer(Identifier.PERSISTENT_DATA, new BukkitSerializer.PersistentData(this));
|
registerSerializer(Identifier.PERSISTENT_DATA, new BukkitSerializer.PersistentData(this));
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -165,8 +171,8 @@ public class BukkitHuskSync extends JavaPlugin implements HuskSync, BukkitTask.S
|
|||||||
initialize(getSettings().getDatabase().getType().getDisplayName() + " database connection", (plugin) -> {
|
initialize(getSettings().getDatabase().getType().getDisplayName() + " database connection", (plugin) -> {
|
||||||
this.database = switch (settings.getDatabase().getType()) {
|
this.database = switch (settings.getDatabase().getType()) {
|
||||||
case MYSQL, MARIADB -> new MySqlDatabase(this);
|
case MYSQL, MARIADB -> new MySqlDatabase(this);
|
||||||
|
case POSTGRES -> new PostgresDatabase(this);
|
||||||
case MONGO -> new MongoDbDatabase(this);
|
case MONGO -> new MongoDbDatabase(this);
|
||||||
default -> throw new IllegalStateException("Invalid database type");
|
|
||||||
};
|
};
|
||||||
this.database.initialize();
|
this.database.initialize();
|
||||||
});
|
});
|
||||||
@@ -184,7 +190,7 @@ public class BukkitHuskSync extends JavaPlugin implements HuskSync, BukkitTask.S
|
|||||||
});
|
});
|
||||||
|
|
||||||
// Register events
|
// Register events
|
||||||
initialize("events", (plugin) -> this.eventListener = createEventListener());
|
initialize("events", (plugin) -> eventListener.onEnable());
|
||||||
|
|
||||||
// Register commands
|
// Register commands
|
||||||
initialize("commands", (plugin) -> BukkitCommand.Type.registerCommands(this));
|
initialize("commands", (plugin) -> BukkitCommand.Type.registerCommands(this));
|
||||||
@@ -331,11 +337,16 @@ public class BukkitHuskSync extends JavaPlugin implements HuskSync, BukkitTask.S
|
|||||||
}
|
}
|
||||||
|
|
||||||
@NotNull
|
@NotNull
|
||||||
public RegionalScheduler getRegionalScheduler() {
|
public RegionalScheduler getSyncScheduler() {
|
||||||
return regionalScheduler == null
|
return regionalScheduler == null
|
||||||
? regionalScheduler = getScheduler().globalRegionalScheduler() : regionalScheduler;
|
? regionalScheduler = getScheduler().globalRegionalScheduler() : regionalScheduler;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@NotNull
|
||||||
|
public AttachedScheduler getUserSyncScheduler(@NotNull UserDataHolder user) {
|
||||||
|
return getScheduler().entitySpecificScheduler(((BukkitUser) user).getPlayer());
|
||||||
|
}
|
||||||
|
|
||||||
@NotNull
|
@NotNull
|
||||||
public CommandRegistration getCommandRegistrar() {
|
public CommandRegistration getCommandRegistrar() {
|
||||||
return paperLib.commandRegistration();
|
return paperLib.commandRegistration();
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -21,27 +21,32 @@ package net.william278.husksync.data;
|
|||||||
|
|
||||||
import com.google.gson.reflect.TypeToken;
|
import com.google.gson.reflect.TypeToken;
|
||||||
import de.tr7zw.changeme.nbtapi.NBT;
|
import de.tr7zw.changeme.nbtapi.NBT;
|
||||||
|
import de.tr7zw.changeme.nbtapi.NBTCompound;
|
||||||
import de.tr7zw.changeme.nbtapi.NBTContainer;
|
import de.tr7zw.changeme.nbtapi.NBTContainer;
|
||||||
import de.tr7zw.changeme.nbtapi.iface.ReadWriteNBT;
|
import de.tr7zw.changeme.nbtapi.iface.ReadWriteNBT;
|
||||||
|
import de.tr7zw.changeme.nbtapi.iface.ReadWriteNBTCompoundList;
|
||||||
|
import de.tr7zw.changeme.nbtapi.utils.DataFixerUtil;
|
||||||
|
import lombok.AccessLevel;
|
||||||
|
import lombok.AllArgsConstructor;
|
||||||
|
import net.william278.desertwell.util.Version;
|
||||||
import net.william278.husksync.HuskSync;
|
import net.william278.husksync.HuskSync;
|
||||||
import net.william278.husksync.adapter.Adaptable;
|
import net.william278.husksync.adapter.Adaptable;
|
||||||
import net.william278.husksync.api.HuskSyncAPI;
|
import net.william278.husksync.api.HuskSyncAPI;
|
||||||
|
import org.bukkit.Material;
|
||||||
import org.bukkit.inventory.ItemStack;
|
import org.bukkit.inventory.ItemStack;
|
||||||
import org.jetbrains.annotations.ApiStatus;
|
import org.jetbrains.annotations.ApiStatus;
|
||||||
import org.jetbrains.annotations.NotNull;
|
import org.jetbrains.annotations.NotNull;
|
||||||
|
import org.jetbrains.annotations.Nullable;
|
||||||
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
import static net.william278.husksync.data.BukkitData.Items.Inventory.INVENTORY_SLOT_COUNT;
|
import static net.william278.husksync.data.BukkitData.Items.Inventory.INVENTORY_SLOT_COUNT;
|
||||||
|
|
||||||
|
@AllArgsConstructor(access = AccessLevel.PRIVATE)
|
||||||
public class BukkitSerializer {
|
public class BukkitSerializer {
|
||||||
|
|
||||||
protected final HuskSync plugin;
|
protected final HuskSync plugin;
|
||||||
|
|
||||||
private BukkitSerializer(@NotNull HuskSync plugin) {
|
|
||||||
this.plugin = plugin;
|
|
||||||
}
|
|
||||||
|
|
||||||
@SuppressWarnings("unused")
|
@SuppressWarnings("unused")
|
||||||
public BukkitSerializer(@NotNull HuskSyncAPI api) {
|
public BukkitSerializer(@NotNull HuskSyncAPI api) {
|
||||||
this.plugin = api.getPlugin();
|
this.plugin = api.getPlugin();
|
||||||
@@ -53,7 +58,8 @@ public class BukkitSerializer {
|
|||||||
return plugin;
|
return plugin;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static class Inventory extends BukkitSerializer implements Serializer<BukkitData.Items.Inventory> {
|
public static class Inventory extends BukkitSerializer implements Serializer<BukkitData.Items.Inventory>,
|
||||||
|
ItemDeserializer {
|
||||||
private static final String ITEMS_TAG = "items";
|
private static final String ITEMS_TAG = "items";
|
||||||
private static final String HELD_ITEM_SLOT_TAG = "held_item_slot";
|
private static final String HELD_ITEM_SLOT_TAG = "held_item_slot";
|
||||||
|
|
||||||
@@ -62,16 +68,21 @@ public class BukkitSerializer {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public BukkitData.Items.Inventory deserialize(@NotNull String serialized) throws DeserializationException {
|
public BukkitData.Items.Inventory deserialize(@NotNull String serialized, @NotNull Version dataMcVersion)
|
||||||
|
throws DeserializationException {
|
||||||
final ReadWriteNBT root = NBT.parseNBT(serialized);
|
final ReadWriteNBT root = NBT.parseNBT(serialized);
|
||||||
final ItemStack[] items = root.getItemStackArray(ITEMS_TAG);
|
final ReadWriteNBT items = root.hasTag(ITEMS_TAG) ? root.getCompound(ITEMS_TAG) : null;
|
||||||
final int heldItemSlot = root.getInteger(HELD_ITEM_SLOT_TAG);
|
|
||||||
return BukkitData.Items.Inventory.from(
|
return BukkitData.Items.Inventory.from(
|
||||||
items == null ? new ItemStack[INVENTORY_SLOT_COUNT] : items,
|
items != null ? getItems(items, dataMcVersion) : new ItemStack[INVENTORY_SLOT_COUNT],
|
||||||
heldItemSlot
|
root.getInteger(HELD_ITEM_SLOT_TAG)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public BukkitData.Items.Inventory deserialize(@NotNull String serialized) {
|
||||||
|
return deserialize(serialized, plugin.getMinecraftVersion());
|
||||||
|
}
|
||||||
|
|
||||||
@NotNull
|
@NotNull
|
||||||
@Override
|
@Override
|
||||||
public String serialize(@NotNull BukkitData.Items.Inventory data) throws SerializationException {
|
public String serialize(@NotNull BukkitData.Items.Inventory data) throws SerializationException {
|
||||||
@@ -83,18 +94,25 @@ public class BukkitSerializer {
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public static class EnderChest extends BukkitSerializer implements Serializer<BukkitData.Items.EnderChest> {
|
public static class EnderChest extends BukkitSerializer implements Serializer<BukkitData.Items.EnderChest>,
|
||||||
|
ItemDeserializer {
|
||||||
|
|
||||||
public EnderChest(@NotNull HuskSync plugin) {
|
public EnderChest(@NotNull HuskSync plugin) {
|
||||||
super(plugin);
|
super(plugin);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public BukkitData.Items.EnderChest deserialize(@NotNull String serialized) throws DeserializationException {
|
public BukkitData.Items.EnderChest deserialize(@NotNull String serialized, @NotNull Version dataMcVersion)
|
||||||
final ItemStack[] items = NBT.itemStackArrayFromNBT(NBT.parseNBT(serialized));
|
throws DeserializationException {
|
||||||
|
final ItemStack[] items = getItems(NBT.parseNBT(serialized), dataMcVersion);
|
||||||
return items == null ? BukkitData.Items.EnderChest.empty() : BukkitData.Items.EnderChest.adapt(items);
|
return items == null ? BukkitData.Items.EnderChest.empty() : BukkitData.Items.EnderChest.adapt(items);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public BukkitData.Items.EnderChest deserialize(@NotNull String serialized) {
|
||||||
|
return deserialize(serialized, plugin.getMinecraftVersion());
|
||||||
|
}
|
||||||
|
|
||||||
@NotNull
|
@NotNull
|
||||||
@Override
|
@Override
|
||||||
public String serialize(@NotNull BukkitData.Items.EnderChest data) throws SerializationException {
|
public String serialize(@NotNull BukkitData.Items.EnderChest data) throws SerializationException {
|
||||||
@@ -102,6 +120,57 @@ public class BukkitSerializer {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Utility interface for deserializing and upgrading item stacks from legacy versions
|
||||||
|
private interface ItemDeserializer {
|
||||||
|
|
||||||
|
@Nullable
|
||||||
|
default ItemStack[] getItems(@NotNull ReadWriteNBT tag, @NotNull Version mcVersion) {
|
||||||
|
if (mcVersion.compareTo(getPlugin().getMinecraftVersion()) < 0) {
|
||||||
|
return upgradeItemStack((NBTCompound) tag, mcVersion);
|
||||||
|
}
|
||||||
|
return NBT.itemStackArrayFromNBT(tag);
|
||||||
|
}
|
||||||
|
|
||||||
|
@NotNull
|
||||||
|
private ItemStack @NotNull [] upgradeItemStack(@NotNull NBTCompound compound, @NotNull Version mcVersion) {
|
||||||
|
final ReadWriteNBTCompoundList items = compound.getCompoundList("items");
|
||||||
|
final ItemStack[] itemStacks = new ItemStack[compound.getInteger("size")];
|
||||||
|
for (int i = 0; i < items.size(); i++) {
|
||||||
|
if (items.get(i) == null) {
|
||||||
|
itemStacks[i] = new ItemStack(Material.AIR);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
itemStacks[i] = NBT.itemStackFromNBT(upgradeItemData(items.get(i), mcVersion));
|
||||||
|
} catch (Throwable e) {
|
||||||
|
itemStacks[i] = new ItemStack(Material.AIR);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return itemStacks;
|
||||||
|
}
|
||||||
|
|
||||||
|
@NotNull
|
||||||
|
private ReadWriteNBT upgradeItemData(@NotNull ReadWriteNBT tag, @NotNull Version mcVersion)
|
||||||
|
throws NoSuchFieldException, IllegalAccessException {
|
||||||
|
return DataFixerUtil.fixUpItemData(tag, getDataVersion(mcVersion), DataFixerUtil.getCurrentVersion());
|
||||||
|
}
|
||||||
|
|
||||||
|
private int getDataVersion(@NotNull Version mcVersion) {
|
||||||
|
return switch (mcVersion.toStringWithoutMetadata()) {
|
||||||
|
case "1.16", "1.16.1", "1.16.2", "1.16.3", "1.16.4", "1.16.5" -> DataFixerUtil.VERSION1_16_5;
|
||||||
|
case "1.17", "1.17.1" -> DataFixerUtil.VERSION1_17_1;
|
||||||
|
case "1.18", "1.18.1", "1.18.2" -> DataFixerUtil.VERSION1_18_2;
|
||||||
|
case "1.19", "1.19.1", "1.19.2" -> DataFixerUtil.VERSION1_19_2;
|
||||||
|
case "1.20", "1.20.1", "1.20.2" -> DataFixerUtil.VERSION1_20_2;
|
||||||
|
case "1.20.3", "1.20.4" -> DataFixerUtil.VERSION1_20_4;
|
||||||
|
default -> DataFixerUtil.getCurrentVersion();
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
@NotNull
|
||||||
|
HuskSync getPlugin();
|
||||||
|
}
|
||||||
|
|
||||||
public static class PotionEffects extends BukkitSerializer implements Serializer<BukkitData.PotionEffects> {
|
public static class PotionEffects extends BukkitSerializer implements Serializer<BukkitData.PotionEffects> {
|
||||||
|
|
||||||
private static final TypeToken<List<Data.PotionEffects.Effect>> TYPE = new TypeToken<>() {
|
private static final TypeToken<List<Data.PotionEffects.Effect>> TYPE = new TypeToken<>() {
|
||||||
@@ -149,46 +218,6 @@ public class BukkitSerializer {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public static class Location extends BukkitSerializer implements Serializer<BukkitData.Location> {
|
|
||||||
|
|
||||||
public Location(@NotNull HuskSync plugin) {
|
|
||||||
super(plugin);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public BukkitData.Location deserialize(@NotNull String serialized) throws DeserializationException {
|
|
||||||
return plugin.getDataAdapter().fromJson(serialized, BukkitData.Location.class);
|
|
||||||
}
|
|
||||||
|
|
||||||
@NotNull
|
|
||||||
@Override
|
|
||||||
public String serialize(@NotNull BukkitData.Location element) throws SerializationException {
|
|
||||||
return plugin.getDataAdapter().toJson(element);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public static class Statistics extends BukkitSerializer implements Serializer<BukkitData.Statistics> {
|
|
||||||
|
|
||||||
public Statistics(@NotNull HuskSync plugin) {
|
|
||||||
super(plugin);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public BukkitData.Statistics deserialize(@NotNull String serialized) throws DeserializationException {
|
|
||||||
return BukkitData.Statistics.from(plugin.getGson().fromJson(
|
|
||||||
serialized,
|
|
||||||
BukkitData.Statistics.StatisticsMap.class
|
|
||||||
));
|
|
||||||
}
|
|
||||||
|
|
||||||
@NotNull
|
|
||||||
@Override
|
|
||||||
public String serialize(@NotNull BukkitData.Statistics element) throws SerializationException {
|
|
||||||
return plugin.getGson().toJson(element.getStatisticsSet());
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
public static class PersistentData extends BukkitSerializer implements Serializer<BukkitData.PersistentData> {
|
public static class PersistentData extends BukkitSerializer implements Serializer<BukkitData.PersistentData> {
|
||||||
|
|
||||||
public PersistentData(@NotNull HuskSync plugin) {
|
public PersistentData(@NotNull HuskSync plugin) {
|
||||||
@@ -208,43 +237,11 @@ public class BukkitSerializer {
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public static class Health extends Json<BukkitData.Health> implements Serializer<BukkitData.Health> {
|
public static class Json<T extends Data & Adaptable> extends BukkitSerializer implements Serializer<T> {
|
||||||
|
|
||||||
public Health(@NotNull HuskSync plugin) {
|
|
||||||
super(plugin, BukkitData.Health.class);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
public static class Hunger extends Json<BukkitData.Hunger> implements Serializer<BukkitData.Hunger> {
|
|
||||||
|
|
||||||
public Hunger(@NotNull HuskSync plugin) {
|
|
||||||
super(plugin, BukkitData.Hunger.class);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
public static class Experience extends Json<BukkitData.Experience> implements Serializer<BukkitData.Experience> {
|
|
||||||
|
|
||||||
public Experience(@NotNull HuskSync plugin) {
|
|
||||||
super(plugin, BukkitData.Experience.class);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
public static class GameMode extends Json<BukkitData.GameMode> implements Serializer<BukkitData.GameMode> {
|
|
||||||
|
|
||||||
public GameMode(@NotNull HuskSync plugin) {
|
|
||||||
super(plugin, BukkitData.GameMode.class);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
public static abstract class Json<T extends Data & Adaptable> extends BukkitSerializer implements Serializer<T> {
|
|
||||||
|
|
||||||
private final Class<T> type;
|
private final Class<T> type;
|
||||||
|
|
||||||
protected Json(@NotNull HuskSync plugin, Class<T> type) {
|
public Json(@NotNull HuskSync plugin, Class<T> type) {
|
||||||
super(plugin);
|
super(plugin);
|
||||||
this.type = type;
|
this.type = type;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -41,8 +41,10 @@ public interface BukkitUserDataHolder extends UserDataHolder {
|
|||||||
case "statistics" -> getStatistics();
|
case "statistics" -> getStatistics();
|
||||||
case "health" -> getHealth();
|
case "health" -> getHealth();
|
||||||
case "hunger" -> getHunger();
|
case "hunger" -> getHunger();
|
||||||
|
case "attributes" -> getAttributes();
|
||||||
case "experience" -> getExperience();
|
case "experience" -> getExperience();
|
||||||
case "game_mode" -> getGameMode();
|
case "game_mode" -> getGameMode();
|
||||||
|
case "flight_status" -> getFlightStatus();
|
||||||
case "persistent_data" -> getPersistentData();
|
case "persistent_data" -> getPersistentData();
|
||||||
default -> throw new IllegalStateException(String.format("Unexpected data type: %s", id));
|
default -> throw new IllegalStateException(String.format("Unexpected data type: %s", id));
|
||||||
};
|
};
|
||||||
@@ -116,6 +118,12 @@ public interface BukkitUserDataHolder extends UserDataHolder {
|
|||||||
return Optional.of(BukkitData.Hunger.adapt(getBukkitPlayer()));
|
return Optional.of(BukkitData.Hunger.adapt(getBukkitPlayer()));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@NotNull
|
||||||
|
@Override
|
||||||
|
default Optional<Data.Attributes> getAttributes() {
|
||||||
|
return Optional.of(BukkitData.Attributes.adapt(getBukkitPlayer(), getPlugin()));
|
||||||
|
}
|
||||||
|
|
||||||
@NotNull
|
@NotNull
|
||||||
@Override
|
@Override
|
||||||
default Optional<Data.Experience> getExperience() {
|
default Optional<Data.Experience> getExperience() {
|
||||||
@@ -128,6 +136,12 @@ public interface BukkitUserDataHolder extends UserDataHolder {
|
|||||||
return Optional.of(BukkitData.GameMode.adapt(getBukkitPlayer()));
|
return Optional.of(BukkitData.GameMode.adapt(getBukkitPlayer()));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@NotNull
|
||||||
|
@Override
|
||||||
|
default Optional<Data.FlightStatus> getFlightStatus() {
|
||||||
|
return Optional.of(BukkitData.FlightStatus.adapt(getBukkitPlayer()));
|
||||||
|
}
|
||||||
|
|
||||||
@NotNull
|
@NotNull
|
||||||
@Override
|
@Override
|
||||||
default Optional<Data.PersistentData> getPersistentData() {
|
default Optional<Data.PersistentData> getPersistentData() {
|
||||||
|
|||||||
@@ -20,47 +20,57 @@
|
|||||||
package net.william278.husksync.listener;
|
package net.william278.husksync.listener;
|
||||||
|
|
||||||
import net.william278.husksync.BukkitHuskSync;
|
import net.william278.husksync.BukkitHuskSync;
|
||||||
import net.william278.husksync.HuskSync;
|
|
||||||
import net.william278.husksync.data.BukkitData;
|
import net.william278.husksync.data.BukkitData;
|
||||||
import net.william278.husksync.user.BukkitUser;
|
import net.william278.husksync.user.BukkitUser;
|
||||||
import net.william278.husksync.user.OnlineUser;
|
import net.william278.husksync.user.OnlineUser;
|
||||||
import org.bukkit.Bukkit;
|
|
||||||
import org.bukkit.entity.Player;
|
import org.bukkit.entity.Player;
|
||||||
import org.bukkit.entity.Projectile;
|
|
||||||
import org.bukkit.event.Cancellable;
|
|
||||||
import org.bukkit.event.EventHandler;
|
import org.bukkit.event.EventHandler;
|
||||||
import org.bukkit.event.EventPriority;
|
import org.bukkit.event.EventPriority;
|
||||||
import org.bukkit.event.Listener;
|
import org.bukkit.event.Listener;
|
||||||
import org.bukkit.event.block.BlockBreakEvent;
|
|
||||||
import org.bukkit.event.block.BlockPlaceEvent;
|
|
||||||
import org.bukkit.event.entity.EntityDamageEvent;
|
|
||||||
import org.bukkit.event.entity.EntityPickupItemEvent;
|
|
||||||
import org.bukkit.event.entity.PlayerDeathEvent;
|
import org.bukkit.event.entity.PlayerDeathEvent;
|
||||||
import org.bukkit.event.entity.ProjectileLaunchEvent;
|
|
||||||
import org.bukkit.event.inventory.InventoryClickEvent;
|
|
||||||
import org.bukkit.event.inventory.InventoryOpenEvent;
|
|
||||||
import org.bukkit.event.inventory.PrepareItemCraftEvent;
|
|
||||||
import org.bukkit.event.player.PlayerCommandPreprocessEvent;
|
import org.bukkit.event.player.PlayerCommandPreprocessEvent;
|
||||||
import org.bukkit.event.player.PlayerDropItemEvent;
|
|
||||||
import org.bukkit.event.player.PlayerInteractEntityEvent;
|
|
||||||
import org.bukkit.event.player.PlayerInteractEvent;
|
|
||||||
import org.bukkit.event.server.MapInitializeEvent;
|
import org.bukkit.event.server.MapInitializeEvent;
|
||||||
import org.bukkit.event.world.WorldSaveEvent;
|
import org.bukkit.event.world.WorldSaveEvent;
|
||||||
|
import org.bukkit.inventory.ItemStack;
|
||||||
import org.jetbrains.annotations.NotNull;
|
import org.jetbrains.annotations.NotNull;
|
||||||
|
|
||||||
import java.util.List;
|
|
||||||
import java.util.Locale;
|
|
||||||
import java.util.UUID;
|
|
||||||
import java.util.stream.Collectors;
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
public class BukkitEventListener extends EventListener implements BukkitJoinEventListener, BukkitQuitEventListener,
|
public class BukkitEventListener extends EventListener implements BukkitJoinEventListener, BukkitQuitEventListener,
|
||||||
BukkitDeathEventListener, Listener {
|
BukkitDeathEventListener, Listener {
|
||||||
protected final List<String> blacklistedCommands;
|
|
||||||
|
|
||||||
public BukkitEventListener(@NotNull BukkitHuskSync huskSync) {
|
protected LockedHandler lockedHandler;
|
||||||
super(huskSync);
|
|
||||||
this.blacklistedCommands = huskSync.getSettings().getSynchronization().getBlacklistedCommandsWhileLocked();
|
public BukkitEventListener(@NotNull BukkitHuskSync plugin) {
|
||||||
Bukkit.getServer().getPluginManager().registerEvents(this, huskSync);
|
super(plugin);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void onLoad() {
|
||||||
|
this.lockedHandler = createLockedHandler((BukkitHuskSync) plugin);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void onEnable() {
|
||||||
|
getPlugin().getServer().getPluginManager().registerEvents(this, getPlugin());
|
||||||
|
lockedHandler.onEnable();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void handlePluginDisable() {
|
||||||
|
super.handlePluginDisable();
|
||||||
|
lockedHandler.onDisable();
|
||||||
|
}
|
||||||
|
|
||||||
|
@NotNull
|
||||||
|
private LockedHandler createLockedHandler(@NotNull BukkitHuskSync plugin) {
|
||||||
|
if (!getPlugin().getSettings().isCancelPackets()) {
|
||||||
|
return new BukkitLockedEventListener(plugin);
|
||||||
|
}
|
||||||
|
if (getPlugin().isDependencyLoaded("PacketEvents")) {
|
||||||
|
return new BukkitPacketEventsLockedPacketListener(plugin);
|
||||||
|
} else if (getPlugin().isDependencyLoaded("ProtocolLib")) {
|
||||||
|
return new BukkitProtocolLibLockedPacketListener(plugin);
|
||||||
|
}
|
||||||
|
|
||||||
|
return new BukkitLockedEventListener(plugin);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -71,9 +81,11 @@ public class BukkitEventListener extends EventListener implements BukkitJoinEven
|
|||||||
@Override
|
@Override
|
||||||
public void handlePlayerQuit(@NotNull BukkitUser bukkitUser) {
|
public void handlePlayerQuit(@NotNull BukkitUser bukkitUser) {
|
||||||
final Player player = bukkitUser.getPlayer();
|
final Player player = bukkitUser.getPlayer();
|
||||||
if (!bukkitUser.isLocked() && !player.getItemOnCursor().getType().isAir()) {
|
final ItemStack itemOnCursor = player.getItemOnCursor();
|
||||||
player.getWorld().dropItem(player.getLocation(), player.getItemOnCursor());
|
if (!bukkitUser.isLocked() && !itemOnCursor.getType().isAir()) {
|
||||||
player.setItemOnCursor(null);
|
player.setItemOnCursor(null);
|
||||||
|
player.getWorld().dropItem(player.getLocation(), itemOnCursor);
|
||||||
|
plugin.debug("Dropped " + itemOnCursor + " for " + player.getName() + " on quit");
|
||||||
}
|
}
|
||||||
super.handlePlayerQuit(bukkitUser);
|
super.handlePlayerQuit(bukkitUser);
|
||||||
}
|
}
|
||||||
@@ -88,7 +100,7 @@ public class BukkitEventListener extends EventListener implements BukkitJoinEven
|
|||||||
final OnlineUser user = BukkitUser.adapt(event.getEntity(), plugin);
|
final OnlineUser user = BukkitUser.adapt(event.getEntity(), plugin);
|
||||||
|
|
||||||
// If the player is locked or the plugin disabling, clear their drops
|
// If the player is locked or the plugin disabling, clear their drops
|
||||||
if (cancelPlayerEvent(user.getUuid())) {
|
if (lockedHandler.cancelPlayerEvent(user.getUuid())) {
|
||||||
event.getDrops().clear();
|
event.getDrops().clear();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -125,94 +137,21 @@ public class BukkitEventListener extends EventListener implements BukkitJoinEven
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// We handle commands here to allow specific command handling on ProtocolLib servers
|
||||||
/*
|
|
||||||
* Events to cancel if the player has not been set yet
|
|
||||||
*/
|
|
||||||
|
|
||||||
@EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = true)
|
|
||||||
public void onProjectileLaunch(@NotNull ProjectileLaunchEvent event) {
|
|
||||||
final Projectile projectile = event.getEntity();
|
|
||||||
if (projectile.getShooter() instanceof Player player) {
|
|
||||||
cancelPlayerEvent(player.getUniqueId(), event);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = true)
|
|
||||||
public void onDropItem(@NotNull PlayerDropItemEvent event) {
|
|
||||||
cancelPlayerEvent(event.getPlayer().getUniqueId(), event);
|
|
||||||
}
|
|
||||||
|
|
||||||
@EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = true)
|
|
||||||
public void onPickupItem(@NotNull EntityPickupItemEvent event) {
|
|
||||||
if (event.getEntity() instanceof Player player) {
|
|
||||||
cancelPlayerEvent(player.getUniqueId(), event);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = true)
|
|
||||||
public void onPlayerInteract(@NotNull PlayerInteractEvent event) {
|
|
||||||
cancelPlayerEvent(event.getPlayer().getUniqueId(), event);
|
|
||||||
}
|
|
||||||
|
|
||||||
@EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = true)
|
|
||||||
public void onPlayerInteractEntity(@NotNull PlayerInteractEntityEvent event) {
|
|
||||||
cancelPlayerEvent(event.getPlayer().getUniqueId(), event);
|
|
||||||
}
|
|
||||||
|
|
||||||
@EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = true)
|
|
||||||
public void onBlockPlace(@NotNull BlockPlaceEvent event) {
|
|
||||||
cancelPlayerEvent(event.getPlayer().getUniqueId(), event);
|
|
||||||
}
|
|
||||||
|
|
||||||
@EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = true)
|
|
||||||
public void onBlockBreak(@NotNull BlockBreakEvent event) {
|
|
||||||
cancelPlayerEvent(event.getPlayer().getUniqueId(), event);
|
|
||||||
}
|
|
||||||
|
|
||||||
@EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = true)
|
|
||||||
public void onInventoryOpen(@NotNull InventoryOpenEvent event) {
|
|
||||||
if (event.getPlayer() instanceof Player player) {
|
|
||||||
cancelPlayerEvent(player.getUniqueId(), event);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = true)
|
|
||||||
public void onInventoryClick(@NotNull InventoryClickEvent event) {
|
|
||||||
cancelPlayerEvent(event.getWhoClicked().getUniqueId(), event);
|
|
||||||
}
|
|
||||||
|
|
||||||
@EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = true)
|
|
||||||
public void onCraftItem(@NotNull PrepareItemCraftEvent event) {
|
|
||||||
}
|
|
||||||
|
|
||||||
@EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = true)
|
|
||||||
public void onPlayerTakeDamage(@NotNull EntityDamageEvent event) {
|
|
||||||
if (event.getEntity() instanceof Player player) {
|
|
||||||
cancelPlayerEvent(player.getUniqueId(), event);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@EventHandler(priority = EventPriority.LOW, ignoreCancelled = true)
|
@EventHandler(priority = EventPriority.LOW, ignoreCancelled = true)
|
||||||
public void onPermissionCommand(@NotNull PlayerCommandPreprocessEvent event) {
|
public void onCommandProcessed(@NotNull PlayerCommandPreprocessEvent event) {
|
||||||
final String[] commandArgs = event.getMessage().substring(1).split(" ");
|
if (!lockedHandler.isCommandDisabled(event.getMessage().substring(1).split(" ")[0])) {
|
||||||
final String commandLabel = commandArgs[0].toLowerCase(Locale.ENGLISH);
|
return;
|
||||||
|
|
||||||
if (blacklistedCommands.contains("*") || blacklistedCommands.contains(commandLabel)) {
|
|
||||||
cancelPlayerEvent(event.getPlayer().getUniqueId(), event);
|
|
||||||
}
|
}
|
||||||
}
|
if (lockedHandler.cancelPlayerEvent(event.getPlayer().getUniqueId())) {
|
||||||
|
|
||||||
private void cancelPlayerEvent(@NotNull UUID uuid, @NotNull Cancellable event) {
|
|
||||||
if (cancelPlayerEvent(uuid)) {
|
|
||||||
event.setCancelled(true);
|
event.setCancelled(true);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@NotNull
|
@NotNull
|
||||||
@Override
|
@Override
|
||||||
public HuskSync getPlugin() {
|
public BukkitHuskSync getPlugin() {
|
||||||
return plugin;
|
return (BukkitHuskSync) plugin;
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,131 @@
|
|||||||
|
/*
|
||||||
|
* 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.listener;
|
||||||
|
|
||||||
|
import lombok.Getter;
|
||||||
|
import net.william278.husksync.BukkitHuskSync;
|
||||||
|
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;
|
||||||
|
import org.bukkit.event.block.BlockBreakEvent;
|
||||||
|
import org.bukkit.event.block.BlockPlaceEvent;
|
||||||
|
import org.bukkit.event.entity.EntityDamageEvent;
|
||||||
|
import org.bukkit.event.entity.EntityPickupItemEvent;
|
||||||
|
import org.bukkit.event.entity.ProjectileLaunchEvent;
|
||||||
|
import org.bukkit.event.inventory.InventoryClickEvent;
|
||||||
|
import org.bukkit.event.inventory.InventoryOpenEvent;
|
||||||
|
import org.bukkit.event.player.PlayerArmorStandManipulateEvent;
|
||||||
|
import org.bukkit.event.player.PlayerDropItemEvent;
|
||||||
|
import org.bukkit.event.player.PlayerInteractEntityEvent;
|
||||||
|
import org.bukkit.event.player.PlayerInteractEvent;
|
||||||
|
import org.jetbrains.annotations.NotNull;
|
||||||
|
|
||||||
|
import java.util.Objects;
|
||||||
|
import java.util.UUID;
|
||||||
|
|
||||||
|
@Getter
|
||||||
|
public class BukkitLockedEventListener implements LockedHandler, Listener {
|
||||||
|
|
||||||
|
protected final BukkitHuskSync plugin;
|
||||||
|
|
||||||
|
protected BukkitLockedEventListener(@NotNull BukkitHuskSync plugin) {
|
||||||
|
this.plugin = plugin;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onEnable() {
|
||||||
|
plugin.getServer().getPluginManager().registerEvents(this, plugin);
|
||||||
|
}
|
||||||
|
|
||||||
|
@EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = true)
|
||||||
|
public void onProjectileLaunch(@NotNull ProjectileLaunchEvent event) {
|
||||||
|
final Projectile projectile = event.getEntity();
|
||||||
|
if (projectile.getShooter() instanceof Player player) {
|
||||||
|
cancelPlayerEvent(player.getUniqueId(), event);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = true)
|
||||||
|
public void onDropItem(@NotNull PlayerDropItemEvent event) {
|
||||||
|
cancelPlayerEvent(event.getPlayer().getUniqueId(), event);
|
||||||
|
}
|
||||||
|
|
||||||
|
@EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = true)
|
||||||
|
public void onPickupItem(@NotNull EntityPickupItemEvent event) {
|
||||||
|
if (event.getEntity() instanceof Player player) {
|
||||||
|
cancelPlayerEvent(player.getUniqueId(), event);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = true)
|
||||||
|
public void onPlayerInteract(@NotNull PlayerInteractEvent event) {
|
||||||
|
cancelPlayerEvent(event.getPlayer().getUniqueId(), event);
|
||||||
|
}
|
||||||
|
|
||||||
|
@EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = true)
|
||||||
|
public void onPlayerInteractEntity(@NotNull PlayerInteractEntityEvent event) {
|
||||||
|
cancelPlayerEvent(event.getPlayer().getUniqueId(), event);
|
||||||
|
}
|
||||||
|
|
||||||
|
@EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = true)
|
||||||
|
public void onPlayerInteractArmorStand(@NotNull PlayerArmorStandManipulateEvent event) {
|
||||||
|
cancelPlayerEvent(event.getPlayer().getUniqueId(), event);
|
||||||
|
}
|
||||||
|
|
||||||
|
@EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = true)
|
||||||
|
public void onBlockPlace(@NotNull BlockPlaceEvent event) {
|
||||||
|
cancelPlayerEvent(event.getPlayer().getUniqueId(), event);
|
||||||
|
}
|
||||||
|
|
||||||
|
@EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = true)
|
||||||
|
public void onBlockBreak(@NotNull BlockBreakEvent event) {
|
||||||
|
cancelPlayerEvent(event.getPlayer().getUniqueId(), event);
|
||||||
|
}
|
||||||
|
|
||||||
|
@EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = true)
|
||||||
|
public void onInventoryOpen(@NotNull InventoryOpenEvent event) {
|
||||||
|
if (event.getPlayer() instanceof Player player) {
|
||||||
|
cancelPlayerEvent(player.getUniqueId(), event);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = true)
|
||||||
|
public void onInventoryClick(@NotNull InventoryClickEvent event) {
|
||||||
|
cancelPlayerEvent(event.getWhoClicked().getUniqueId(), event);
|
||||||
|
}
|
||||||
|
|
||||||
|
@EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = true)
|
||||||
|
public void onPlayerTakeDamage(@NotNull EntityDamageEvent event) {
|
||||||
|
if (event.getEntity() instanceof Player player) {
|
||||||
|
cancelPlayerEvent(player.getUniqueId(), event);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void cancelPlayerEvent(@NotNull UUID uuid, @NotNull Cancellable event) {
|
||||||
|
if (cancelPlayerEvent(uuid)) {
|
||||||
|
event.setCancelled(true);
|
||||||
|
plugin.debug("Cancelled event " + event.getClass().getSimpleName() + " from " + Objects.requireNonNull(plugin.getServer().getPlayer(uuid)).getName());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,103 @@
|
|||||||
|
/*
|
||||||
|
* 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.listener;
|
||||||
|
|
||||||
|
import com.github.retrooper.packetevents.PacketEvents;
|
||||||
|
import com.github.retrooper.packetevents.event.PacketListenerAbstract;
|
||||||
|
import com.github.retrooper.packetevents.event.PacketListenerPriority;
|
||||||
|
import com.github.retrooper.packetevents.event.PacketReceiveEvent;
|
||||||
|
import com.github.retrooper.packetevents.protocol.packettype.PacketType;
|
||||||
|
import com.google.common.collect.Sets;
|
||||||
|
import io.github.retrooper.packetevents.factory.spigot.SpigotPacketEventsBuilder;
|
||||||
|
import net.william278.husksync.BukkitHuskSync;
|
||||||
|
import org.jetbrains.annotations.NotNull;
|
||||||
|
|
||||||
|
import java.util.Set;
|
||||||
|
import java.util.logging.Level;
|
||||||
|
|
||||||
|
public class BukkitPacketEventsLockedPacketListener extends BukkitLockedEventListener implements LockedHandler {
|
||||||
|
|
||||||
|
protected BukkitPacketEventsLockedPacketListener(@NotNull BukkitHuskSync plugin) {
|
||||||
|
super(plugin);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onLoad() {
|
||||||
|
super.onLoad();
|
||||||
|
PacketEvents.setAPI(SpigotPacketEventsBuilder.build(getPlugin()));
|
||||||
|
PacketEvents.getAPI().getSettings().reEncodeByDefault(false)
|
||||||
|
.checkForUpdates(false)
|
||||||
|
.bStats(true);
|
||||||
|
PacketEvents.getAPI().load();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onEnable() {
|
||||||
|
super.onEnable();
|
||||||
|
PacketEvents.getAPI().getEventManager().registerListener(new PlayerPacketAdapter(this));
|
||||||
|
PacketEvents.getAPI().init();
|
||||||
|
plugin.log(Level.INFO, "Using PacketEvents to cancel packets for locked players");
|
||||||
|
}
|
||||||
|
|
||||||
|
private static class PlayerPacketAdapter extends PacketListenerAbstract {
|
||||||
|
|
||||||
|
private static final Set<PacketType.Play.Client> ALLOWED_PACKETS = Set.of(
|
||||||
|
PacketType.Play.Client.KEEP_ALIVE, PacketType.Play.Client.PONG, PacketType.Play.Client.PLUGIN_MESSAGE, // Connection packets
|
||||||
|
PacketType.Play.Client.CHAT_MESSAGE, PacketType.Play.Client.CHAT_COMMAND, PacketType.Play.Client.CHAT_SESSION_UPDATE, // Chat / command packets
|
||||||
|
PacketType.Play.Client.PLAYER_POSITION, PacketType.Play.Client.PLAYER_POSITION_AND_ROTATION, PacketType.Play.Client.PLAYER_ROTATION, // Movement packets
|
||||||
|
PacketType.Play.Client.HELD_ITEM_CHANGE, PacketType.Play.Client.ANIMATION, PacketType.Play.Client.TELEPORT_CONFIRM, // Animation packets
|
||||||
|
PacketType.Play.Client.CLIENT_SETTINGS // Video setting packets
|
||||||
|
);
|
||||||
|
|
||||||
|
private static final Set<PacketType.Play.Client> CANCEL_PACKETS = getPacketsToListenFor();
|
||||||
|
|
||||||
|
|
||||||
|
private final BukkitPacketEventsLockedPacketListener listener;
|
||||||
|
|
||||||
|
public PlayerPacketAdapter(@NotNull BukkitPacketEventsLockedPacketListener listener) {
|
||||||
|
super(PacketListenerPriority.HIGH);
|
||||||
|
this.listener = listener;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onPacketReceive(PacketReceiveEvent event) {
|
||||||
|
if(!(event.getPacketType() instanceof PacketType.Play.Client client)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!CANCEL_PACKETS.contains(client)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (listener.cancelPlayerEvent(event.getUser().getUUID())) {
|
||||||
|
event.setCancelled(true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Returns the set of ALL Server packets, excluding the set of allowed packets
|
||||||
|
@NotNull
|
||||||
|
private static Set<PacketType.Play.Client> getPacketsToListenFor() {
|
||||||
|
return Sets.difference(
|
||||||
|
Sets.newHashSet(PacketType.Play.Client.values()),
|
||||||
|
ALLOWED_PACKETS
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -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.listener;
|
||||||
|
|
||||||
|
import com.comphenix.protocol.PacketType;
|
||||||
|
import com.comphenix.protocol.ProtocolLibrary;
|
||||||
|
import com.comphenix.protocol.events.ListenerPriority;
|
||||||
|
import com.comphenix.protocol.events.PacketAdapter;
|
||||||
|
import com.comphenix.protocol.events.PacketEvent;
|
||||||
|
import com.google.common.collect.Sets;
|
||||||
|
import net.william278.husksync.BukkitHuskSync;
|
||||||
|
import org.jetbrains.annotations.NotNull;
|
||||||
|
|
||||||
|
import java.util.Set;
|
||||||
|
import java.util.logging.Level;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
|
import static com.comphenix.protocol.PacketType.Play.Client;
|
||||||
|
|
||||||
|
public class BukkitProtocolLibLockedPacketListener extends BukkitLockedEventListener implements LockedHandler {
|
||||||
|
|
||||||
|
protected BukkitProtocolLibLockedPacketListener(@NotNull BukkitHuskSync plugin) {
|
||||||
|
super(plugin);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onEnable() {
|
||||||
|
super.onEnable();
|
||||||
|
ProtocolLibrary.getProtocolManager().addPacketListener(new PlayerPacketAdapter(this));
|
||||||
|
plugin.log(Level.INFO, "Using ProtocolLib to cancel packets for locked players");
|
||||||
|
}
|
||||||
|
|
||||||
|
private static class PlayerPacketAdapter extends PacketAdapter {
|
||||||
|
|
||||||
|
// Packets we want the player to still be able to send/receiver to/from the server
|
||||||
|
private static final Set<PacketType> ALLOWED_PACKETS = Set.of(
|
||||||
|
Client.KEEP_ALIVE, Client.PONG, Client.CUSTOM_PAYLOAD, // Connection packets
|
||||||
|
Client.CHAT_COMMAND, Client.CLIENT_COMMAND, Client.CHAT, Client.CHAT_SESSION_UPDATE, // Chat / command packets
|
||||||
|
Client.POSITION, Client.POSITION_LOOK, Client.LOOK, // Movement packets
|
||||||
|
Client.HELD_ITEM_SLOT, Client.ARM_ANIMATION, Client.TELEPORT_ACCEPT, // Animation packets
|
||||||
|
Client.SETTINGS // Video setting packets
|
||||||
|
);
|
||||||
|
|
||||||
|
private final BukkitProtocolLibLockedPacketListener listener;
|
||||||
|
|
||||||
|
public PlayerPacketAdapter(@NotNull BukkitProtocolLibLockedPacketListener listener) {
|
||||||
|
super(listener.getPlugin(), ListenerPriority.HIGHEST, getPacketsToListenFor());
|
||||||
|
this.listener = listener;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onPacketReceiving(@NotNull PacketEvent event) {
|
||||||
|
if (listener.cancelPlayerEvent(event.getPlayer().getUniqueId()) && !event.isReadOnly()) {
|
||||||
|
event.setCancelled(true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onPacketSending(@NotNull PacketEvent event) {
|
||||||
|
if (listener.cancelPlayerEvent(event.getPlayer().getUniqueId()) && !event.isReadOnly()) {
|
||||||
|
event.setCancelled(true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Returns the set of ALL Server packets, excluding the set of allowed packets
|
||||||
|
@NotNull
|
||||||
|
private static Set<PacketType> getPacketsToListenFor() {
|
||||||
|
return Sets.difference(
|
||||||
|
Client.getInstance().values().stream().filter(PacketType::isSupported).collect(Collectors.toSet()),
|
||||||
|
ALLOWED_PACKETS
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -323,18 +323,18 @@ public class LegacyMigrator extends Migrator {
|
|||||||
|
|
||||||
// Stats
|
// Stats
|
||||||
.statistics(BukkitData.Statistics.from(
|
.statistics(BukkitData.Statistics.from(
|
||||||
BukkitData.Statistics.createStatisticsMap(
|
convertStatisticMap(stats.untypedStatisticValues()),
|
||||||
convertStatisticMap(stats.untypedStatisticValues()),
|
convertMaterialStatisticMap(stats.blockStatisticValues()),
|
||||||
convertMaterialStatisticMap(stats.blockStatisticValues()),
|
convertMaterialStatisticMap(stats.itemStatisticValues()),
|
||||||
convertMaterialStatisticMap(stats.itemStatisticValues()),
|
convertEntityStatisticMap(stats.entityStatisticValues())
|
||||||
convertEntityStatisticMap(stats.entityStatisticValues())
|
))
|
||||||
)))
|
|
||||||
|
|
||||||
// Health, hunger, experience & game mode
|
// Health, hunger, experience & game mode
|
||||||
.health(BukkitData.Health.from(health, maxHealth, healthScale))
|
.health(BukkitData.Health.from(health, healthScale))
|
||||||
.hunger(BukkitData.Hunger.from(hunger, saturation, saturationExhaustion))
|
.hunger(BukkitData.Hunger.from(hunger, saturation, saturationExhaustion))
|
||||||
.experience(BukkitData.Experience.from(totalExp, expLevel, expProgress))
|
.experience(BukkitData.Experience.from(totalExp, expLevel, expProgress))
|
||||||
.gameMode(BukkitData.GameMode.from(gameMode, isFlying, isFlying))
|
.gameMode(BukkitData.GameMode.from(gameMode))
|
||||||
|
.flightStatus(BukkitData.FlightStatus.from(isFlying, isFlying))
|
||||||
|
|
||||||
// Build & pack into new format
|
// Build & pack into new format
|
||||||
.saveCause(DataSnapshot.SaveCause.LEGACY_MIGRATION).buildAndPack();
|
.saveCause(DataSnapshot.SaveCause.LEGACY_MIGRATION).buildAndPack();
|
||||||
|
|||||||
@@ -320,7 +320,7 @@ public class MpdbMigrator extends Migrator {
|
|||||||
.inventory(BukkitData.Items.Inventory.from(inventory.getContents(), 0))
|
.inventory(BukkitData.Items.Inventory.from(inventory.getContents(), 0))
|
||||||
.enderChest(BukkitData.Items.EnderChest.adapt(enderChest))
|
.enderChest(BukkitData.Items.EnderChest.adapt(enderChest))
|
||||||
.experience(BukkitData.Experience.from(totalExp, expLevel, expProgress))
|
.experience(BukkitData.Experience.from(totalExp, expLevel, expProgress))
|
||||||
.gameMode(BukkitData.GameMode.from("SURVIVAL", false, false))
|
.gameMode(BukkitData.GameMode.from("SURVIVAL"))
|
||||||
.saveCause(DataSnapshot.SaveCause.MPDB_MIGRATION)
|
.saveCause(DataSnapshot.SaveCause.MPDB_MIGRATION)
|
||||||
.buildAndPack();
|
.buildAndPack();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -109,7 +109,7 @@ public class BukkitUser extends OnlineUser implements BukkitUserDataHolder {
|
|||||||
gui.setCloseGuiAction((close) -> onClose.accept(BukkitData.Items.ItemArray.adapt(
|
gui.setCloseGuiAction((close) -> onClose.accept(BukkitData.Items.ItemArray.adapt(
|
||||||
Arrays.stream(close.getInventory().getContents()).limit(size).toArray(ItemStack[]::new)
|
Arrays.stream(close.getInventory().getContents()).limit(size).toArray(ItemStack[]::new)
|
||||||
)));
|
)));
|
||||||
plugin.runSync(() -> gui.open(player));
|
plugin.runSync(() -> gui.open(player), this);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|||||||
@@ -19,56 +19,38 @@
|
|||||||
|
|
||||||
package net.william278.husksync.util;
|
package net.william278.husksync.util;
|
||||||
|
|
||||||
import org.bukkit.Keyed;
|
import org.bukkit.*;
|
||||||
import org.bukkit.Material;
|
import org.bukkit.attribute.Attribute;
|
||||||
import org.bukkit.Statistic;
|
|
||||||
import org.bukkit.entity.EntityType;
|
import org.bukkit.entity.EntityType;
|
||||||
import org.jetbrains.annotations.NotNull;
|
import org.jetbrains.annotations.NotNull;
|
||||||
import org.jetbrains.annotations.Nullable;
|
import org.jetbrains.annotations.Nullable;
|
||||||
|
|
||||||
import java.util.Arrays;
|
|
||||||
import java.util.Optional;
|
|
||||||
|
|
||||||
// Utility class for adapting "Keyed" Bukkit objects
|
// Utility class for adapting "Keyed" Bukkit objects
|
||||||
public final class BukkitKeyedAdapter {
|
public final class BukkitKeyedAdapter {
|
||||||
|
|
||||||
@Nullable
|
@Nullable
|
||||||
public static Statistic matchStatistic(@NotNull String key) {
|
public static Statistic matchStatistic(@NotNull String key) {
|
||||||
try {
|
return getRegistryValue(Registry.STATISTIC, key);
|
||||||
return Arrays.stream(Statistic.values())
|
|
||||||
.filter(stat -> stat.getKey().toString().equals(key))
|
|
||||||
.findFirst().orElse(null);
|
|
||||||
} catch (Throwable e) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Nullable
|
@Nullable
|
||||||
public static EntityType matchEntityType(@NotNull String key) {
|
public static EntityType matchEntityType(@NotNull String key) {
|
||||||
try {
|
return getRegistryValue(Registry.ENTITY_TYPE, key);
|
||||||
return Arrays.stream(EntityType.values())
|
|
||||||
.filter(entityType -> entityType.getKey().toString().equals(key))
|
|
||||||
.findFirst().orElse(null);
|
|
||||||
} catch (Throwable e) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Nullable
|
@Nullable
|
||||||
public static Material matchMaterial(@NotNull String key) {
|
public static Material matchMaterial(@NotNull String key) {
|
||||||
try {
|
return getRegistryValue(Registry.MATERIAL, key);
|
||||||
return Material.matchMaterial(key);
|
|
||||||
} catch (Throwable e) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public static Optional<String> getKeyName(@NotNull Keyed keyed) {
|
@Nullable
|
||||||
try {
|
public static Attribute matchAttribute(@NotNull String key) {
|
||||||
return Optional.of(keyed.getKey().toString());
|
return getRegistryValue(Registry.ATTRIBUTE, key);
|
||||||
} catch (Throwable e) {
|
}
|
||||||
return Optional.empty();
|
|
||||||
}
|
private static <T extends Keyed> T getRegistryValue(@NotNull Registry<T> registry, @NotNull String keyString) {
|
||||||
|
final NamespacedKey key = NamespacedKey.fromString(keyString);
|
||||||
|
return key != null ? registry.get(key) : null;
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -27,9 +27,6 @@ import net.william278.husksync.data.BukkitData;
|
|||||||
import net.william278.husksync.data.Data;
|
import net.william278.husksync.data.Data;
|
||||||
import net.william278.husksync.data.DataSnapshot;
|
import net.william278.husksync.data.DataSnapshot;
|
||||||
import net.william278.husksync.data.Identifier;
|
import net.william278.husksync.data.Identifier;
|
||||||
import org.bukkit.Material;
|
|
||||||
import org.bukkit.Statistic;
|
|
||||||
import org.bukkit.entity.EntityType;
|
|
||||||
import org.bukkit.inventory.ItemStack;
|
import org.bukkit.inventory.ItemStack;
|
||||||
import org.bukkit.util.io.BukkitObjectInputStream;
|
import org.bukkit.util.io.BukkitObjectInputStream;
|
||||||
import org.jetbrains.annotations.NotNull;
|
import org.jetbrains.annotations.NotNull;
|
||||||
@@ -45,7 +42,8 @@ import java.time.OffsetDateTime;
|
|||||||
import java.util.*;
|
import java.util.*;
|
||||||
import java.util.logging.Level;
|
import java.util.logging.Level;
|
||||||
|
|
||||||
import static net.william278.husksync.util.BukkitKeyedAdapter.*;
|
import static net.william278.husksync.util.BukkitKeyedAdapter.matchEntityType;
|
||||||
|
import static net.william278.husksync.util.BukkitKeyedAdapter.matchMaterial;
|
||||||
|
|
||||||
public class BukkitLegacyConverter extends LegacyConverter {
|
public class BukkitLegacyConverter extends LegacyConverter {
|
||||||
|
|
||||||
@@ -53,9 +51,9 @@ public class BukkitLegacyConverter extends LegacyConverter {
|
|||||||
super(plugin);
|
super(plugin);
|
||||||
}
|
}
|
||||||
|
|
||||||
@NotNull
|
|
||||||
@Override
|
@Override
|
||||||
public DataSnapshot.Packed convert(byte[] data, @NotNull UUID id,
|
@NotNull
|
||||||
|
public DataSnapshot.Packed convert(byte @NotNull [] data, @NotNull UUID id,
|
||||||
@NotNull OffsetDateTime timestamp) throws DataAdapter.AdaptionException {
|
@NotNull OffsetDateTime timestamp) throws DataAdapter.AdaptionException {
|
||||||
final JSONObject object = new JSONObject(plugin.getDataAdapter().bytesToString(data));
|
final JSONObject object = new JSONObject(plugin.getDataAdapter().bytesToString(data));
|
||||||
final int version = object.getInt("format_version");
|
final int version = object.getInt("format_version");
|
||||||
@@ -87,7 +85,6 @@ public class BukkitLegacyConverter extends LegacyConverter {
|
|||||||
if (shouldImport(Identifier.HEALTH)) {
|
if (shouldImport(Identifier.HEALTH)) {
|
||||||
containers.put(Identifier.HEALTH, BukkitData.Health.from(
|
containers.put(Identifier.HEALTH, BukkitData.Health.from(
|
||||||
status.getDouble("health"),
|
status.getDouble("health"),
|
||||||
status.getDouble("max_health"),
|
|
||||||
status.getDouble("health_scale")
|
status.getDouble("health_scale")
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
@@ -107,7 +104,11 @@ public class BukkitLegacyConverter extends LegacyConverter {
|
|||||||
}
|
}
|
||||||
if (shouldImport(Identifier.GAME_MODE)) {
|
if (shouldImport(Identifier.GAME_MODE)) {
|
||||||
containers.put(Identifier.GAME_MODE, BukkitData.GameMode.from(
|
containers.put(Identifier.GAME_MODE, BukkitData.GameMode.from(
|
||||||
status.getString("game_mode"),
|
status.getString("game_mode")
|
||||||
|
));
|
||||||
|
}
|
||||||
|
if (shouldImport(Identifier.FLIGHT_STATUS)) {
|
||||||
|
containers.put(Identifier.FLIGHT_STATUS, BukkitData.FlightStatus.from(
|
||||||
status.getBoolean("is_flying"),
|
status.getBoolean("is_flying"),
|
||||||
status.getBoolean("is_flying")
|
status.getBoolean("is_flying")
|
||||||
));
|
));
|
||||||
@@ -185,7 +186,7 @@ public class BukkitLegacyConverter extends LegacyConverter {
|
|||||||
|
|
||||||
@NotNull
|
@NotNull
|
||||||
private Optional<Data.Statistics> readStatistics(@NotNull JSONObject object) {
|
private Optional<Data.Statistics> readStatistics(@NotNull JSONObject object) {
|
||||||
if (!object.has("statistics") || !shouldImport(Identifier.ADVANCEMENTS)) {
|
if (!object.has("statistics") || !shouldImport(Identifier.STATISTICS)) {
|
||||||
return Optional.empty();
|
return Optional.empty();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -202,39 +203,42 @@ public class BukkitLegacyConverter extends LegacyConverter {
|
|||||||
private BukkitData.Statistics readStatisticMaps(@NotNull JSONObject untyped, @NotNull JSONObject blocks,
|
private BukkitData.Statistics readStatisticMaps(@NotNull JSONObject untyped, @NotNull JSONObject blocks,
|
||||||
@NotNull JSONObject items, @NotNull JSONObject entities) {
|
@NotNull JSONObject items, @NotNull JSONObject entities) {
|
||||||
// Read generic stats
|
// Read generic stats
|
||||||
final Map<Statistic, Integer> genericStats = Maps.newHashMap();
|
final Map<String, Integer> genericStats = Maps.newHashMap();
|
||||||
untyped.keys().forEachRemaining(stat -> genericStats.put(matchStatistic(stat), untyped.getInt(stat)));
|
untyped.keys().forEachRemaining(stat -> genericStats.put(stat, untyped.getInt(stat)));
|
||||||
|
|
||||||
// Read block & item stats
|
// Read block & item stats
|
||||||
final Map<Statistic, Map<Material, Integer>> blockStats, itemStats;
|
final Map<String, Map<String, Integer>> blockStats, itemStats, entityStats;
|
||||||
blockStats = readMaterialStatistics(blocks);
|
blockStats = readMaterialStatistics(blocks);
|
||||||
itemStats = readMaterialStatistics(items);
|
itemStats = readMaterialStatistics(items);
|
||||||
|
|
||||||
// Read entity stats
|
// Read entity stats
|
||||||
final Map<Statistic, Map<EntityType, Integer>> entityStats = Maps.newHashMap();
|
entityStats = Maps.newHashMap();
|
||||||
entities.keys().forEachRemaining(stat -> {
|
entities.keys().forEachRemaining(stat -> {
|
||||||
final JSONObject entityStat = entities.getJSONObject(stat);
|
final JSONObject entityStat = entities.getJSONObject(stat);
|
||||||
final Map<EntityType, Integer> entityMap = Maps.newHashMap();
|
final Map<String, Integer> entityMap = Maps.newHashMap();
|
||||||
entityStat.keys().forEachRemaining(entity -> entityMap.put(matchEntityType(entity), entityStat.getInt(entity)));
|
entityStat.keys().forEachRemaining(entity -> {
|
||||||
entityStats.put(matchStatistic(stat), entityMap);
|
if (matchEntityType(entity) != null) {
|
||||||
|
entityMap.put(entity, entityStat.getInt(entity));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
entityStats.put(stat, entityMap);
|
||||||
});
|
});
|
||||||
|
|
||||||
return BukkitData.Statistics.from(genericStats, blockStats, itemStats, entityStats);
|
return BukkitData.Statistics.from(genericStats, blockStats, itemStats, entityStats);
|
||||||
}
|
}
|
||||||
|
|
||||||
@NotNull
|
@NotNull
|
||||||
private Map<Statistic, Map<Material, Integer>> readMaterialStatistics(@NotNull JSONObject items) {
|
private Map<String, Map<String, Integer>> readMaterialStatistics(@NotNull JSONObject items) {
|
||||||
final Map<Statistic, Map<Material, Integer>> itemStats = Maps.newHashMap();
|
final Map<String, Map<String, Integer>> itemStats = Maps.newHashMap();
|
||||||
items.keys().forEachRemaining(stat -> {
|
items.keys().forEachRemaining(stat -> {
|
||||||
final JSONObject itemStat = items.getJSONObject(stat);
|
final JSONObject itemStat = items.getJSONObject(stat);
|
||||||
final Map<Material, Integer> itemMap = Maps.newHashMap();
|
final Map<String, Integer> itemMap = Maps.newHashMap();
|
||||||
itemStat.keys().forEachRemaining(item -> {
|
itemStat.keys().forEachRemaining(item -> {
|
||||||
final Material material = matchMaterial(item);
|
if (matchMaterial(item) != null) {
|
||||||
if (material != null) {
|
itemMap.put(item, itemStat.getInt(item));
|
||||||
itemMap.put(material, itemStat.getInt(item));
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
itemStats.put(matchStatistic(stat), itemMap);
|
itemStats.put(stat, itemMap);
|
||||||
});
|
});
|
||||||
return itemStats;
|
return itemStats;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -37,6 +37,7 @@ import org.bukkit.inventory.meta.MapMeta;
|
|||||||
import org.bukkit.map.*;
|
import org.bukkit.map.*;
|
||||||
import org.jetbrains.annotations.ApiStatus;
|
import org.jetbrains.annotations.ApiStatus;
|
||||||
import org.jetbrains.annotations.NotNull;
|
import org.jetbrains.annotations.NotNull;
|
||||||
|
import org.jetbrains.annotations.Nullable;
|
||||||
|
|
||||||
import java.awt.*;
|
import java.awt.*;
|
||||||
import java.io.File;
|
import java.io.File;
|
||||||
@@ -75,8 +76,8 @@ public interface BukkitMapPersister {
|
|||||||
* @param items the array of {@link ItemStack}s to apply persisted locked maps to
|
* @param items the array of {@link ItemStack}s to apply persisted locked maps to
|
||||||
* @return the array of {@link ItemStack}s with persisted locked maps applied
|
* @return the array of {@link ItemStack}s with persisted locked maps applied
|
||||||
*/
|
*/
|
||||||
@NotNull
|
@Nullable
|
||||||
default ItemStack[] setMapViews(@NotNull ItemStack[] items) {
|
default ItemStack @NotNull [] setMapViews(@Nullable ItemStack @NotNull [] items) {
|
||||||
if (!getPlugin().getSettings().getSynchronization().isPersistLockedMaps()) {
|
if (!getPlugin().getSettings().getSynchronization().isPersistLockedMaps()) {
|
||||||
return items;
|
return items;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -21,8 +21,11 @@ package net.william278.husksync.util;
|
|||||||
|
|
||||||
import net.william278.husksync.BukkitHuskSync;
|
import net.william278.husksync.BukkitHuskSync;
|
||||||
import net.william278.husksync.HuskSync;
|
import net.william278.husksync.HuskSync;
|
||||||
|
import net.william278.husksync.data.UserDataHolder;
|
||||||
import org.jetbrains.annotations.NotNull;
|
import org.jetbrains.annotations.NotNull;
|
||||||
|
import org.jetbrains.annotations.Nullable;
|
||||||
import space.arim.morepaperlib.scheduling.AsynchronousScheduler;
|
import space.arim.morepaperlib.scheduling.AsynchronousScheduler;
|
||||||
|
import space.arim.morepaperlib.scheduling.AttachedScheduler;
|
||||||
import space.arim.morepaperlib.scheduling.RegionalScheduler;
|
import space.arim.morepaperlib.scheduling.RegionalScheduler;
|
||||||
import space.arim.morepaperlib.scheduling.ScheduledTask;
|
import space.arim.morepaperlib.scheduling.ScheduledTask;
|
||||||
|
|
||||||
@@ -34,9 +37,12 @@ public interface BukkitTask extends Task {
|
|||||||
class Sync extends Task.Sync implements BukkitTask {
|
class Sync extends Task.Sync implements BukkitTask {
|
||||||
|
|
||||||
private ScheduledTask task;
|
private ScheduledTask task;
|
||||||
|
private final @Nullable UserDataHolder user;
|
||||||
|
|
||||||
protected Sync(@NotNull HuskSync plugin, @NotNull Runnable runnable, long delayTicks) {
|
protected Sync(@NotNull HuskSync plugin, @NotNull Runnable runnable,
|
||||||
|
@Nullable UserDataHolder user, long delayTicks) {
|
||||||
super(plugin, runnable, delayTicks);
|
super(plugin, runnable, delayTicks);
|
||||||
|
this.user = user;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -57,7 +63,19 @@ public interface BukkitTask extends Task {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
final RegionalScheduler scheduler = ((BukkitHuskSync) getPlugin()).getRegionalScheduler();
|
// Use entity-specific scheduler if user is not null
|
||||||
|
if (user != null) {
|
||||||
|
final AttachedScheduler scheduler = ((BukkitHuskSync) getPlugin()).getUserSyncScheduler(user);
|
||||||
|
if (delayTicks > 0) {
|
||||||
|
this.task = scheduler.runDelayed(runnable, null, delayTicks);
|
||||||
|
} else {
|
||||||
|
this.task = scheduler.run(runnable, null);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Or default to the global scheduler
|
||||||
|
final RegionalScheduler scheduler = ((BukkitHuskSync) getPlugin()).getSyncScheduler();
|
||||||
if (delayTicks > 0) {
|
if (delayTicks > 0) {
|
||||||
this.task = scheduler.runDelayed(runnable, delayTicks);
|
this.task = scheduler.runDelayed(runnable, delayTicks);
|
||||||
} else {
|
} else {
|
||||||
@@ -146,8 +164,8 @@ public interface BukkitTask extends Task {
|
|||||||
|
|
||||||
@NotNull
|
@NotNull
|
||||||
@Override
|
@Override
|
||||||
default Task.Sync getSyncTask(@NotNull Runnable runnable, long delayTicks) {
|
default Task.Sync getSyncTask(@NotNull Runnable runnable, @Nullable UserDataHolder user, long delayTicks) {
|
||||||
return new Sync(getPlugin(), runnable, delayTicks);
|
return new Sync(getPlugin(), runnable, user, delayTicks);
|
||||||
}
|
}
|
||||||
|
|
||||||
@NotNull
|
@NotNull
|
||||||
|
|||||||
@@ -5,12 +5,16 @@ api-version: 1.17
|
|||||||
author: 'William278'
|
author: 'William278'
|
||||||
description: '${description}'
|
description: '${description}'
|
||||||
website: 'https://william278.net'
|
website: 'https://william278.net'
|
||||||
|
folia-supported: true
|
||||||
softdepend:
|
softdepend:
|
||||||
|
- 'packetevents'
|
||||||
|
- 'ProtocolLib'
|
||||||
- 'MysqlPlayerDataBridge'
|
- 'MysqlPlayerDataBridge'
|
||||||
- 'Plan'
|
- 'Plan'
|
||||||
libraries:
|
libraries:
|
||||||
- 'redis.clients:jedis:${jedis_version}'
|
- 'redis.clients:jedis:${jedis_version}'
|
||||||
- 'com.mysql:mysql-connector-j:${mysql_driver_version}'
|
- 'com.mysql:mysql-connector-j:${mysql_driver_version}'
|
||||||
- 'org.mariadb.jdbc:mariadb-java-client:${mariadb_driver_version}'
|
- 'org.mariadb.jdbc:mariadb-java-client:${mariadb_driver_version}'
|
||||||
- 'org.mongodb:mongodb-driver:${mongodb_driver_version}'
|
- 'org.postgresql:postgresql:${postgres_driver_version}'
|
||||||
|
- 'org.mongodb:mongodb-driver-sync:${mongodb_driver_version}'
|
||||||
- 'org.xerial.snappy:snappy-java:${snappy_version}'
|
- 'org.xerial.snappy:snappy-java:${snappy_version}'
|
||||||
@@ -3,37 +3,38 @@ plugins {
|
|||||||
}
|
}
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
api 'commons-io:commons-io:2.15.1'
|
api 'commons-io:commons-io:2.16.1'
|
||||||
api 'org.apache.commons:commons-text:1.11.0'
|
api 'org.apache.commons:commons-text:1.12.0'
|
||||||
api 'de.themoep:minedown-adventure:1.7.2-SNAPSHOT'
|
api 'net.william278:minedown:1.8.2'
|
||||||
api 'org.json:json:20240205'
|
api 'org.json:json:20240303'
|
||||||
api 'com.google.code.gson:gson:2.10.1'
|
api 'com.google.code.gson:gson:2.11.0'
|
||||||
api 'com.fatboyindustrial.gson-javatime-serialisers:gson-javatime-serialisers:1.1.2'
|
api 'com.fatboyindustrial.gson-javatime-serialisers:gson-javatime-serialisers:1.1.2'
|
||||||
api 'com.github.Exlll.ConfigLib:configlib-yaml:v4.5.0'
|
api 'de.exlll:configlib-yaml:4.5.0'
|
||||||
|
api 'net.william278:paginedown:1.1.2'
|
||||||
api 'net.william278:DesertWell:2.0.4'
|
api 'net.william278:DesertWell:2.0.4'
|
||||||
api 'net.william278:PagineDown:1.1'
|
|
||||||
api('com.zaxxer:HikariCP:5.1.0') {
|
api('com.zaxxer:HikariCP:5.1.0') {
|
||||||
exclude module: 'slf4j-api'
|
exclude module: 'slf4j-api'
|
||||||
}
|
}
|
||||||
|
|
||||||
compileOnly 'org.projectlombok:lombok:1.18.30'
|
compileOnly 'org.projectlombok:lombok:1.18.32'
|
||||||
compileOnly 'org.jetbrains:annotations:24.1.0'
|
compileOnly 'org.jetbrains:annotations:24.1.0'
|
||||||
compileOnly 'net.kyori:adventure-api:4.16.0'
|
compileOnly 'net.kyori:adventure-api:4.17.0'
|
||||||
compileOnly 'net.kyori:adventure-platform-api:4.3.2'
|
compileOnly 'net.kyori:adventure-platform-api:4.3.2'
|
||||||
compileOnly 'com.google.guava:guava:33.0.0-jre'
|
compileOnly 'com.google.guava:guava:33.2.0-jre'
|
||||||
compileOnly 'com.github.plan-player-analytics:Plan:5.5.2272'
|
compileOnly 'com.github.plan-player-analytics:Plan:5.5.2272'
|
||||||
compileOnly "redis.clients:jedis:$jedis_version"
|
compileOnly "redis.clients:jedis:$jedis_version"
|
||||||
compileOnly "com.mysql:mysql-connector-j:$mysql_driver_version"
|
compileOnly "com.mysql:mysql-connector-j:$mysql_driver_version"
|
||||||
compileOnly "org.mariadb.jdbc:mariadb-java-client:$mariadb_driver_version"
|
compileOnly "org.mariadb.jdbc:mariadb-java-client:$mariadb_driver_version"
|
||||||
compileOnly "org.mongodb:mongodb-driver:$mongodb_driver_version"
|
compileOnly "org.postgresql:postgresql:$postgres_driver_version"
|
||||||
|
compileOnly "org.mongodb:mongodb-driver-sync:$mongodb_driver_version"
|
||||||
compileOnly "org.xerial.snappy:snappy-java:$snappy_version"
|
compileOnly "org.xerial.snappy:snappy-java:$snappy_version"
|
||||||
|
|
||||||
testImplementation "redis.clients:jedis:$jedis_version"
|
testImplementation "redis.clients:jedis:$jedis_version"
|
||||||
testImplementation "org.xerial.snappy:snappy-java:$snappy_version"
|
testImplementation "org.xerial.snappy:snappy-java:$snappy_version"
|
||||||
testImplementation 'com.google.guava:guava:33.0.0-jre'
|
testImplementation 'com.google.guava:guava:33.2.0-jre'
|
||||||
testImplementation 'com.github.plan-player-analytics:Plan:5.5.2272'
|
testImplementation 'com.github.plan-player-analytics:Plan:5.5.2272'
|
||||||
testCompileOnly 'com.github.Exlll.ConfigLib:configlib-yaml:v4.5.0'
|
testCompileOnly 'de.exlll:configlib-yaml:4.5.0'
|
||||||
testCompileOnly 'org.jetbrains:annotations:24.1.0'
|
testCompileOnly 'org.jetbrains:annotations:24.1.0'
|
||||||
|
|
||||||
annotationProcessor 'org.projectlombok:lombok:1.18.30'
|
annotationProcessor 'org.projectlombok:lombok:1.18.32'
|
||||||
}
|
}
|
||||||
@@ -29,9 +29,6 @@ import net.william278.desertwell.util.UpdateChecker;
|
|||||||
import net.william278.desertwell.util.Version;
|
import net.william278.desertwell.util.Version;
|
||||||
import net.william278.husksync.adapter.DataAdapter;
|
import net.william278.husksync.adapter.DataAdapter;
|
||||||
import net.william278.husksync.config.ConfigProvider;
|
import net.william278.husksync.config.ConfigProvider;
|
||||||
import net.william278.husksync.config.Locales;
|
|
||||||
import net.william278.husksync.config.Server;
|
|
||||||
import net.william278.husksync.config.Settings;
|
|
||||||
import net.william278.husksync.data.Data;
|
import net.william278.husksync.data.Data;
|
||||||
import net.william278.husksync.data.Identifier;
|
import net.william278.husksync.data.Identifier;
|
||||||
import net.william278.husksync.data.Serializer;
|
import net.william278.husksync.data.Serializer;
|
||||||
@@ -180,31 +177,6 @@ public interface HuskSync extends Task.Supplier, EventDispatcher, ConfigProvider
|
|||||||
log(Level.INFO, "Successfully initialized " + name);
|
log(Level.INFO, "Successfully initialized " + name);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns the plugin {@link Settings}
|
|
||||||
*
|
|
||||||
* @return the {@link Settings}
|
|
||||||
*/
|
|
||||||
@NotNull
|
|
||||||
Settings getSettings();
|
|
||||||
|
|
||||||
void setSettings(@NotNull Settings settings);
|
|
||||||
|
|
||||||
@NotNull
|
|
||||||
String getServerName();
|
|
||||||
|
|
||||||
void setServerName(@NotNull Server serverName);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns the plugin {@link Locales}
|
|
||||||
*
|
|
||||||
* @return the {@link Locales}
|
|
||||||
*/
|
|
||||||
@NotNull
|
|
||||||
Locales getLocales();
|
|
||||||
|
|
||||||
void setLocales(@NotNull Locales locales);
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns if a dependency is loaded
|
* Returns if a dependency is loaded
|
||||||
*
|
*
|
||||||
|
|||||||
@@ -174,6 +174,7 @@ public class HuskSyncAPI {
|
|||||||
public void editCurrentData(@NotNull User user, @NotNull ThrowingConsumer<DataSnapshot.Unpacked> editor) {
|
public void editCurrentData(@NotNull User user, @NotNull ThrowingConsumer<DataSnapshot.Unpacked> editor) {
|
||||||
getCurrentData(user).thenAccept(optional -> optional.ifPresent(data -> {
|
getCurrentData(user).thenAccept(optional -> optional.ifPresent(data -> {
|
||||||
editor.accept(data);
|
editor.accept(data);
|
||||||
|
data.setId(UUID.randomUUID());
|
||||||
setCurrentData(user, data);
|
setCurrentData(user, data);
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
@@ -264,9 +265,9 @@ public class HuskSyncAPI {
|
|||||||
*
|
*
|
||||||
* @param user The user to save the data for
|
* @param user The user to save the data for
|
||||||
* @param snapshot The snapshot to save
|
* @param snapshot The snapshot to save
|
||||||
* @param callback A callback to run after the data has been saved (if the DataSaveEvent was not cancelled)
|
* @param callback A callback to run after the data has been saved (if the DataSaveEvent was not canceled)
|
||||||
* @apiNote This will fire the {@link net.william278.husksync.event.DataSaveEvent} event, unless
|
* @implNote Note that the {@link net.william278.husksync.event.DataSaveEvent} will be fired unless the
|
||||||
* the save cause is {@link DataSnapshot.SaveCause#SERVER_SHUTDOWN}
|
* {@link DataSnapshot.SaveCause#fireDataSaveEvent()} is {@code false}
|
||||||
* @since 3.3.2
|
* @since 3.3.2
|
||||||
*/
|
*/
|
||||||
public void addSnapshot(@NotNull User user, @NotNull DataSnapshot snapshot,
|
public void addSnapshot(@NotNull User user, @NotNull DataSnapshot snapshot,
|
||||||
@@ -284,8 +285,8 @@ public class HuskSyncAPI {
|
|||||||
*
|
*
|
||||||
* @param user The user to save the data for
|
* @param user The user to save the data for
|
||||||
* @param snapshot The snapshot to save
|
* @param snapshot The snapshot to save
|
||||||
* @apiNote This will fire the {@link net.william278.husksync.event.DataSaveEvent} event, unless
|
* @implNote Note that the {@link net.william278.husksync.event.DataSaveEvent} will be fired unless the
|
||||||
* * the save cause is {@link DataSnapshot.SaveCause#SERVER_SHUTDOWN}
|
* {@link DataSnapshot.SaveCause#fireDataSaveEvent()} is {@code false}
|
||||||
* @since 3.0
|
* @since 3.0
|
||||||
*/
|
*/
|
||||||
public void addSnapshot(@NotNull User user, @NotNull DataSnapshot snapshot) {
|
public void addSnapshot(@NotNull User user, @NotNull DataSnapshot snapshot) {
|
||||||
|
|||||||
@@ -30,6 +30,7 @@ import net.william278.husksync.user.User;
|
|||||||
import org.jetbrains.annotations.NotNull;
|
import org.jetbrains.annotations.NotNull;
|
||||||
|
|
||||||
import java.time.format.DateTimeFormatter;
|
import java.time.format.DateTimeFormatter;
|
||||||
|
import java.time.format.FormatStyle;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
|
|
||||||
@@ -51,7 +52,8 @@ public class EnderChestCommand extends ItemsCommand {
|
|||||||
|
|
||||||
// Display opening message
|
// Display opening message
|
||||||
plugin.getLocales().getLocale("ender_chest_viewer_opened", user.getUsername(),
|
plugin.getLocales().getLocale("ender_chest_viewer_opened", user.getUsername(),
|
||||||
snapshot.getTimestamp().format(DateTimeFormatter.ofPattern("dd/MM/yyyy, HH:mm")))
|
snapshot.getTimestamp().format(DateTimeFormatter
|
||||||
|
.ofLocalizedDateTime(FormatStyle.MEDIUM, FormatStyle.SHORT)))
|
||||||
.ifPresent(viewer::sendMessage);
|
.ifPresent(viewer::sendMessage);
|
||||||
|
|
||||||
// Show GUI
|
// Show GUI
|
||||||
|
|||||||
@@ -28,6 +28,7 @@ import net.kyori.adventure.text.format.TextColor;
|
|||||||
import net.william278.desertwell.about.AboutMenu;
|
import net.william278.desertwell.about.AboutMenu;
|
||||||
import net.william278.desertwell.util.UpdateChecker;
|
import net.william278.desertwell.util.UpdateChecker;
|
||||||
import net.william278.husksync.HuskSync;
|
import net.william278.husksync.HuskSync;
|
||||||
|
import net.william278.husksync.database.Database;
|
||||||
import net.william278.husksync.migrator.Migrator;
|
import net.william278.husksync.migrator.Migrator;
|
||||||
import net.william278.husksync.user.CommandUser;
|
import net.william278.husksync.user.CommandUser;
|
||||||
import net.william278.husksync.user.OnlineUser;
|
import net.william278.husksync.user.OnlineUser;
|
||||||
@@ -82,7 +83,8 @@ public class HuskSyncCommand extends Command implements TabProvider {
|
|||||||
AboutMenu.Credit.of("xF3d3").description("Italian (it-it)"),
|
AboutMenu.Credit.of("xF3d3").description("Italian (it-it)"),
|
||||||
AboutMenu.Credit.of("cada3141").description("Korean (ko-kr)"),
|
AboutMenu.Credit.of("cada3141").description("Korean (ko-kr)"),
|
||||||
AboutMenu.Credit.of("Wirayuda5620").description("Indonesian (id-id)"),
|
AboutMenu.Credit.of("Wirayuda5620").description("Indonesian (id-id)"),
|
||||||
AboutMenu.Credit.of("WinTone01").description("Turkish (tr-tr)"))
|
AboutMenu.Credit.of("WinTone01").description("Turkish (tr-tr)"),
|
||||||
|
AboutMenu.Credit.of("IbanEtchep").description("French (fr-fr)"))
|
||||||
.buttons(
|
.buttons(
|
||||||
AboutMenu.Link.of("https://william278.net/docs/husksync").text("Documentation").icon("⛏"),
|
AboutMenu.Link.of("https://william278.net/docs/husksync").text("Documentation").icon("⛏"),
|
||||||
AboutMenu.Link.of("https://github.com/WiIIiam278/HuskSync/issues").text("Issues").icon("❌").color(TextColor.color(0xff9f0f)),
|
AboutMenu.Link.of("https://github.com/WiIIiam278/HuskSync/issues").text("Issues").icon("❌").color(TextColor.color(0xff9f0f)),
|
||||||
@@ -216,7 +218,12 @@ public class HuskSyncCommand extends Command implements TabProvider {
|
|||||||
plugin.getSettings().getSynchronization().getNetworkLatencyMilliseconds() + "ms"
|
plugin.getSettings().getSynchronization().getNetworkLatencyMilliseconds() + "ms"
|
||||||
)),
|
)),
|
||||||
SERVER_NAME(plugin -> Component.text(plugin.getServerName())),
|
SERVER_NAME(plugin -> Component.text(plugin.getServerName())),
|
||||||
DATABASE_TYPE(plugin -> Component.text(plugin.getSettings().getDatabase().getType().getDisplayName())),
|
CLUSTER_ID(plugin -> Component.text(plugin.getSettings().getClusterId().isBlank() ? "None" : plugin.getSettings().getClusterId())),
|
||||||
|
DATABASE_TYPE(plugin ->
|
||||||
|
Component.text(plugin.getSettings().getDatabase().getType().getDisplayName() +
|
||||||
|
(plugin.getSettings().getDatabase().getType() == Database.Type.MONGO ?
|
||||||
|
(plugin.getSettings().getDatabase().getMongoSettings().isUsingAtlas() ? " Atlas" : "") : ""))
|
||||||
|
),
|
||||||
IS_DATABASE_LOCAL(plugin -> getLocalhostBoolean(plugin.getSettings().getDatabase().getCredentials().getHost())),
|
IS_DATABASE_LOCAL(plugin -> getLocalhostBoolean(plugin.getSettings().getDatabase().getCredentials().getHost())),
|
||||||
USING_REDIS_SENTINEL(plugin -> getBoolean(
|
USING_REDIS_SENTINEL(plugin -> getBoolean(
|
||||||
!plugin.getSettings().getRedis().getSentinel().getMaster().isBlank()
|
!plugin.getSettings().getRedis().getSentinel().getMaster().isBlank()
|
||||||
|
|||||||
@@ -30,6 +30,7 @@ import net.william278.husksync.user.User;
|
|||||||
import org.jetbrains.annotations.NotNull;
|
import org.jetbrains.annotations.NotNull;
|
||||||
|
|
||||||
import java.time.format.DateTimeFormatter;
|
import java.time.format.DateTimeFormatter;
|
||||||
|
import java.time.format.FormatStyle;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
|
|
||||||
@@ -51,7 +52,8 @@ public class InventoryCommand extends ItemsCommand {
|
|||||||
|
|
||||||
// Display opening message
|
// Display opening message
|
||||||
plugin.getLocales().getLocale("inventory_viewer_opened", user.getUsername(),
|
plugin.getLocales().getLocale("inventory_viewer_opened", user.getUsername(),
|
||||||
snapshot.getTimestamp().format(DateTimeFormatter.ofPattern("dd/MM/yyyy, HH:mm")))
|
snapshot.getTimestamp().format(DateTimeFormatter
|
||||||
|
.ofLocalizedDateTime(FormatStyle.MEDIUM, FormatStyle.SHORT)))
|
||||||
.ifPresent(viewer::sendMessage);
|
.ifPresent(viewer::sendMessage);
|
||||||
|
|
||||||
// Show GUI
|
// Show GUI
|
||||||
|
|||||||
@@ -71,26 +71,43 @@ public abstract class ItemsCommand extends Command implements TabProvider {
|
|||||||
private void showLatestItems(@NotNull OnlineUser viewer, @NotNull User user) {
|
private void showLatestItems(@NotNull OnlineUser viewer, @NotNull User user) {
|
||||||
plugin.getRedisManager().getUserData(user.getUuid(), user).thenAccept(data -> data
|
plugin.getRedisManager().getUserData(user.getUuid(), user).thenAccept(data -> data
|
||||||
.or(() -> plugin.getDatabase().getLatestSnapshot(user))
|
.or(() -> plugin.getDatabase().getLatestSnapshot(user))
|
||||||
.ifPresentOrElse(
|
.or(() -> {
|
||||||
snapshot -> this.showItems(
|
plugin.getLocales().getLocale("error_no_data_to_display")
|
||||||
viewer, snapshot.unpack(plugin), user,
|
.ifPresent(viewer::sendMessage);
|
||||||
viewer.hasPermission(getPermission("edit"))
|
return Optional.empty();
|
||||||
),
|
})
|
||||||
() -> plugin.getLocales().getLocale("error_no_data_to_display")
|
.flatMap(packed -> {
|
||||||
.ifPresent(viewer::sendMessage)
|
if (packed.isInvalid()) {
|
||||||
));
|
plugin.getLocales().getLocale("error_invalid_data", packed.getInvalidReason(plugin))
|
||||||
|
.ifPresent(viewer::sendMessage);
|
||||||
|
return Optional.empty();
|
||||||
|
}
|
||||||
|
return Optional.of(packed.unpack(plugin));
|
||||||
|
})
|
||||||
|
.ifPresent(snapshot -> this.showItems(
|
||||||
|
viewer, snapshot, user, viewer.hasPermission(getPermission("edit"))
|
||||||
|
)));
|
||||||
}
|
}
|
||||||
|
|
||||||
// View a specific version of the user data
|
// View a specific version of the user data
|
||||||
private void showSnapshotItems(@NotNull OnlineUser viewer, @NotNull User user, @NotNull UUID version) {
|
private void showSnapshotItems(@NotNull OnlineUser viewer, @NotNull User user, @NotNull UUID version) {
|
||||||
plugin.getDatabase().getSnapshot(user, version)
|
plugin.getDatabase().getSnapshot(user, version)
|
||||||
.ifPresentOrElse(
|
.or(() -> {
|
||||||
snapshot -> this.showItems(
|
plugin.getLocales().getLocale("error_invalid_version_uuid")
|
||||||
viewer, snapshot.unpack(plugin), user, false
|
.ifPresent(viewer::sendMessage);
|
||||||
),
|
return Optional.empty();
|
||||||
() -> plugin.getLocales().getLocale("error_invalid_version_uuid")
|
})
|
||||||
.ifPresent(viewer::sendMessage)
|
.flatMap(packed -> {
|
||||||
);
|
if (packed.isInvalid()) {
|
||||||
|
plugin.getLocales().getLocale("error_invalid_data", packed.getInvalidReason(plugin))
|
||||||
|
.ifPresent(viewer::sendMessage);
|
||||||
|
return Optional.empty();
|
||||||
|
}
|
||||||
|
return Optional.of(packed.unpack(plugin));
|
||||||
|
})
|
||||||
|
.ifPresent(snapshot -> this.showItems(
|
||||||
|
viewer, snapshot, user, false
|
||||||
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Show a GUI menu with the correct item data from the snapshot
|
// Show a GUI menu with the correct item data from the snapshot
|
||||||
|
|||||||
@@ -61,7 +61,7 @@ public class UserDataCommand extends Command implements TabProvider {
|
|||||||
.or(() -> parseStringArg(args, 0).flatMap(name -> plugin.getDatabase().getUserByName(name)))
|
.or(() -> parseStringArg(args, 0).flatMap(name -> plugin.getDatabase().getUserByName(name)))
|
||||||
.or(() -> args.length < 2 && executor instanceof User userExecutor
|
.or(() -> args.length < 2 && executor instanceof User userExecutor
|
||||||
? Optional.of(userExecutor) : Optional.empty());
|
? Optional.of(userExecutor) : Optional.empty());
|
||||||
final Optional<UUID> optionalUuid = parseUUIDArg(args, 2).or(() -> parseUUIDArg(args, 1));
|
final Optional<UUID> uuid = parseUUIDArg(args, 2).or(() -> parseUUIDArg(args, 1));
|
||||||
if (optionalUser.isEmpty()) {
|
if (optionalUser.isEmpty()) {
|
||||||
plugin.getLocales().getLocale("error_invalid_player")
|
plugin.getLocales().getLocale("error_invalid_player")
|
||||||
.ifPresent(executor::sendMessage);
|
.ifPresent(executor::sendMessage);
|
||||||
@@ -70,164 +70,179 @@ public class UserDataCommand extends Command implements TabProvider {
|
|||||||
|
|
||||||
final User user = optionalUser.get();
|
final User user = optionalUser.get();
|
||||||
switch (subCommand) {
|
switch (subCommand) {
|
||||||
case "view" -> optionalUuid.ifPresentOrElse(
|
case "view" -> uuid.ifPresentOrElse(
|
||||||
// Show the specified snapshot
|
version -> viewSnapshot(executor, user, version),
|
||||||
version -> plugin.getDatabase().getSnapshot(user, version).ifPresentOrElse(
|
() -> viewLatestSnapshot(executor, user)
|
||||||
data -> DataSnapshotOverview.of(
|
|
||||||
data.unpack(plugin), data.getFileSize(plugin), user, plugin
|
|
||||||
).show(executor),
|
|
||||||
() -> plugin.getLocales().getLocale("error_invalid_version_uuid")
|
|
||||||
.ifPresent(executor::sendMessage)),
|
|
||||||
|
|
||||||
// Show the latest snapshot
|
|
||||||
() -> plugin.getDatabase().getLatestSnapshot(user).ifPresentOrElse(
|
|
||||||
data -> DataSnapshotOverview.of(
|
|
||||||
data.unpack(plugin), data.getFileSize(plugin), user, plugin
|
|
||||||
).show(executor),
|
|
||||||
() -> plugin.getLocales().getLocale("error_no_data_to_display")
|
|
||||||
.ifPresent(executor::sendMessage))
|
|
||||||
);
|
);
|
||||||
|
case "list" -> listSnapshots(
|
||||||
case "list" -> {
|
executor, user, parseIntArg(args, 2).or(() -> parseIntArg(args, 1)).orElse(1)
|
||||||
// Check if there is data to display
|
);
|
||||||
final List<DataSnapshot.Packed> dataList = plugin.getDatabase().getAllSnapshots(user);
|
case "delete" -> uuid.ifPresentOrElse(
|
||||||
if (dataList.isEmpty()) {
|
version -> deleteSnapshot(executor, user, version),
|
||||||
plugin.getLocales().getLocale("error_no_data_to_display")
|
() -> plugin.getLocales().getLocale("error_invalid_syntax",
|
||||||
.ifPresent(executor::sendMessage);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Show the list to the player
|
|
||||||
DataSnapshotList.create(dataList, user, plugin).displayPage(
|
|
||||||
executor,
|
|
||||||
parseIntArg(args, 2).or(() -> parseIntArg(args, 1)).orElse(1)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
case "delete" -> {
|
|
||||||
if (optionalUuid.isEmpty()) {
|
|
||||||
plugin.getLocales().getLocale("error_invalid_syntax",
|
|
||||||
"/userdata delete <username> <version_uuid>")
|
"/userdata delete <username> <version_uuid>")
|
||||||
.ifPresent(executor::sendMessage);
|
.ifPresent(executor::sendMessage)
|
||||||
return;
|
);
|
||||||
}
|
case "restore" -> uuid.ifPresentOrElse(
|
||||||
|
version -> restoreSnapshot(executor, user, version),
|
||||||
// Delete user data by specified UUID and clear their data cache
|
() -> plugin.getLocales().getLocale("error_invalid_syntax",
|
||||||
final UUID version = optionalUuid.get();
|
|
||||||
if (!plugin.getDatabase().deleteSnapshot(user, version)) {
|
|
||||||
plugin.getLocales().getLocale("error_invalid_version_uuid")
|
|
||||||
.ifPresent(executor::sendMessage);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
plugin.getRedisManager().clearUserData(user);
|
|
||||||
|
|
||||||
plugin.getLocales().getLocale("data_deleted",
|
|
||||||
version.toString().split("-")[0],
|
|
||||||
version.toString(),
|
|
||||||
user.getUsername(),
|
|
||||||
user.getUuid().toString())
|
|
||||||
.ifPresent(executor::sendMessage);
|
|
||||||
}
|
|
||||||
|
|
||||||
case "restore" -> {
|
|
||||||
if (optionalUuid.isEmpty()) {
|
|
||||||
plugin.getLocales().getLocale("error_invalid_syntax",
|
|
||||||
"/userdata restore <username> <version_uuid>")
|
"/userdata restore <username> <version_uuid>")
|
||||||
.ifPresent(executor::sendMessage);
|
.ifPresent(executor::sendMessage)
|
||||||
return;
|
);
|
||||||
}
|
case "pin" -> uuid.ifPresentOrElse(
|
||||||
|
version -> pinSnapshot(executor, user, version),
|
||||||
// Restore user data by specified UUID
|
() -> plugin.getLocales().getLocale("error_invalid_syntax",
|
||||||
final Optional<DataSnapshot.Packed> optionalData = plugin.getDatabase().getSnapshot(user, optionalUuid.get());
|
|
||||||
if (optionalData.isEmpty()) {
|
|
||||||
plugin.getLocales().getLocale("error_invalid_version_uuid")
|
|
||||||
.ifPresent(executor::sendMessage);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Restore users with a minimum of one health (prevent restoring players with <=0 health)
|
|
||||||
final DataSnapshot.Packed data = optionalData.get().copy();
|
|
||||||
data.edit(plugin, (unpacked -> {
|
|
||||||
unpacked.getHealth().ifPresent(status -> status.setHealth(Math.max(1, status.getHealth())));
|
|
||||||
unpacked.setSaveCause(DataSnapshot.SaveCause.BACKUP_RESTORE);
|
|
||||||
unpacked.setPinned(
|
|
||||||
plugin.getSettings().getSynchronization().doAutoPin(DataSnapshot.SaveCause.BACKUP_RESTORE)
|
|
||||||
);
|
|
||||||
}));
|
|
||||||
|
|
||||||
// Save data
|
|
||||||
final RedisManager redis = plugin.getRedisManager();
|
|
||||||
plugin.getDataSyncer().saveData(user, data, (u, s) -> {
|
|
||||||
redis.getUserData(u).ifPresent(d -> redis.setUserData(u, s, RedisKeyType.TTL_1_YEAR));
|
|
||||||
redis.sendUserDataUpdate(u, s);
|
|
||||||
plugin.getLocales().getLocale("data_restored", u.getUsername(), u.getUuid().toString(),
|
|
||||||
s.getShortId(), s.getId().toString()).ifPresent(executor::sendMessage);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
case "pin" -> {
|
|
||||||
if (optionalUuid.isEmpty()) {
|
|
||||||
plugin.getLocales().getLocale("error_invalid_syntax",
|
|
||||||
"/userdata pin <username> <version_uuid>")
|
"/userdata pin <username> <version_uuid>")
|
||||||
.ifPresent(executor::sendMessage);
|
.ifPresent(executor::sendMessage)
|
||||||
return;
|
);
|
||||||
}
|
case "dump" -> uuid.ifPresentOrElse(
|
||||||
|
version -> dumpSnapshot(executor, user, version, parseStringArg(args, 3)
|
||||||
// Check that the data exists
|
.map(arg -> arg.equalsIgnoreCase("web")).orElse(false)),
|
||||||
final Optional<DataSnapshot.Packed> optionalData = plugin.getDatabase().getSnapshot(user, optionalUuid.get());
|
() -> plugin.getLocales().getLocale("error_invalid_syntax",
|
||||||
if (optionalData.isEmpty()) {
|
"/userdata dump <web/file> <username> <version_uuid>")
|
||||||
plugin.getLocales().getLocale("error_invalid_version_uuid")
|
.ifPresent(executor::sendMessage)
|
||||||
.ifPresent(executor::sendMessage);
|
);
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Pin or unpin the data
|
|
||||||
final DataSnapshot.Packed data = optionalData.get();
|
|
||||||
if (data.isPinned()) {
|
|
||||||
plugin.getDatabase().unpinSnapshot(user, data.getId());
|
|
||||||
} else {
|
|
||||||
plugin.getDatabase().pinSnapshot(user, data.getId());
|
|
||||||
}
|
|
||||||
plugin.getLocales().getLocale(data.isPinned() ? "data_unpinned" : "data_pinned", data.getShortId(),
|
|
||||||
data.getId().toString(), user.getUsername(), user.getUuid().toString())
|
|
||||||
.ifPresent(executor::sendMessage);
|
|
||||||
}
|
|
||||||
|
|
||||||
case "dump" -> {
|
|
||||||
if (optionalUuid.isEmpty()) {
|
|
||||||
plugin.getLocales().getLocale("error_invalid_syntax",
|
|
||||||
"/userdata dump <username> <version_uuid>")
|
|
||||||
.ifPresent(executor::sendMessage);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Determine dump type
|
|
||||||
final boolean webDump = parseStringArg(args, 3)
|
|
||||||
.map(arg -> arg.equalsIgnoreCase("web"))
|
|
||||||
.orElse(false);
|
|
||||||
final Optional<DataSnapshot.Packed> data = plugin.getDatabase().getSnapshot(user, optionalUuid.get());
|
|
||||||
if (data.isEmpty()) {
|
|
||||||
plugin.getLocales().getLocale("error_invalid_version_uuid")
|
|
||||||
.ifPresent(executor::sendMessage);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Dump the data
|
|
||||||
final DataSnapshot.Packed userData = data.get();
|
|
||||||
final DataDumper dumper = DataDumper.create(userData, user, plugin);
|
|
||||||
try {
|
|
||||||
plugin.getLocales().getLocale("data_dumped", userData.getShortId(), user.getUsername(),
|
|
||||||
(webDump ? dumper.toWeb() : dumper.toFile())).ifPresent(executor::sendMessage);
|
|
||||||
} catch (Throwable e) {
|
|
||||||
plugin.log(Level.SEVERE, "Failed to dump user data", e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
default -> plugin.getLocales().getLocale("error_invalid_syntax", getUsage())
|
default -> plugin.getLocales().getLocale("error_invalid_syntax", getUsage())
|
||||||
.ifPresent(executor::sendMessage);
|
.ifPresent(executor::sendMessage);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Show the latest snapshot
|
||||||
|
private void viewLatestSnapshot(@NotNull CommandUser executor, @NotNull User user) {
|
||||||
|
plugin.getDatabase().getLatestSnapshot(user).ifPresentOrElse(
|
||||||
|
data -> {
|
||||||
|
if (data.isInvalid()) {
|
||||||
|
plugin.getLocales().getLocale("error_invalid_data", data.getInvalidReason(plugin))
|
||||||
|
.ifPresent(executor::sendMessage);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
DataSnapshotOverview.of(data.unpack(plugin), data.getFileSize(plugin), user, plugin)
|
||||||
|
.show(executor);
|
||||||
|
},
|
||||||
|
() -> plugin.getLocales().getLocale("error_no_data_to_display")
|
||||||
|
.ifPresent(executor::sendMessage)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Show the specified snapshot
|
||||||
|
private void viewSnapshot(@NotNull CommandUser executor, @NotNull User user, @NotNull UUID version) {
|
||||||
|
plugin.getDatabase().getSnapshot(user, version).ifPresentOrElse(
|
||||||
|
data -> {
|
||||||
|
if (data.isInvalid()) {
|
||||||
|
plugin.getLocales().getLocale("error_invalid_data", data.getInvalidReason(plugin))
|
||||||
|
.ifPresent(executor::sendMessage);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
DataSnapshotOverview.of(data.unpack(plugin), data.getFileSize(plugin), user, plugin)
|
||||||
|
.show(executor);
|
||||||
|
},
|
||||||
|
() -> plugin.getLocales().getLocale("error_invalid_version_uuid")
|
||||||
|
.ifPresent(executor::sendMessage)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// View a list of snapshots
|
||||||
|
private void listSnapshots(@NotNull CommandUser executor, @NotNull User user, int page) {
|
||||||
|
final List<DataSnapshot.Packed> dataList = plugin.getDatabase().getAllSnapshots(user);
|
||||||
|
if (dataList.isEmpty()) {
|
||||||
|
plugin.getLocales().getLocale("error_no_data_to_display")
|
||||||
|
.ifPresent(executor::sendMessage);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
DataSnapshotList.create(dataList, user, plugin).displayPage(executor, page);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Delete a snapshot
|
||||||
|
private void deleteSnapshot(@NotNull CommandUser executor, @NotNull User user, @NotNull UUID version) {
|
||||||
|
if (!plugin.getDatabase().deleteSnapshot(user, version)) {
|
||||||
|
plugin.getLocales().getLocale("error_invalid_version_uuid")
|
||||||
|
.ifPresent(executor::sendMessage);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
plugin.getRedisManager().clearUserData(user);
|
||||||
|
plugin.getLocales().getLocale("data_deleted",
|
||||||
|
version.toString().split("-")[0],
|
||||||
|
version.toString(),
|
||||||
|
user.getUsername(),
|
||||||
|
user.getUuid().toString())
|
||||||
|
.ifPresent(executor::sendMessage);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Restore a snapshot
|
||||||
|
private void restoreSnapshot(@NotNull CommandUser executor, @NotNull User user, @NotNull UUID version) {
|
||||||
|
final Optional<DataSnapshot.Packed> optionalData = plugin.getDatabase().getSnapshot(user, version);
|
||||||
|
if (optionalData.isEmpty()) {
|
||||||
|
plugin.getLocales().getLocale("error_invalid_version_uuid")
|
||||||
|
.ifPresent(executor::sendMessage);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Restore users with a minimum of one health (prevent restoring players with <= 0 health)
|
||||||
|
final DataSnapshot.Packed data = optionalData.get().copy();
|
||||||
|
if (data.isInvalid()) {
|
||||||
|
plugin.getLocales().getLocale("error_invalid_data", data.getInvalidReason(plugin))
|
||||||
|
.ifPresent(executor::sendMessage);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
data.edit(plugin, (unpacked -> {
|
||||||
|
unpacked.getHealth().ifPresent(status -> status.setHealth(Math.max(1, status.getHealth())));
|
||||||
|
unpacked.setSaveCause(DataSnapshot.SaveCause.BACKUP_RESTORE);
|
||||||
|
unpacked.setPinned(
|
||||||
|
plugin.getSettings().getSynchronization().doAutoPin(DataSnapshot.SaveCause.BACKUP_RESTORE)
|
||||||
|
);
|
||||||
|
}));
|
||||||
|
|
||||||
|
// Save data
|
||||||
|
final RedisManager redis = plugin.getRedisManager();
|
||||||
|
plugin.getDataSyncer().saveData(user, data, (u, s) -> {
|
||||||
|
redis.getUserData(u).ifPresent(d -> redis.setUserData(u, s, RedisKeyType.TTL_1_YEAR));
|
||||||
|
redis.sendUserDataUpdate(u, s);
|
||||||
|
plugin.getLocales().getLocale("data_restored", u.getUsername(), u.getUuid().toString(),
|
||||||
|
s.getShortId(), s.getId().toString()).ifPresent(executor::sendMessage);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Pin a snapshot
|
||||||
|
private void pinSnapshot(@NotNull CommandUser executor, @NotNull User user, @NotNull UUID version) {
|
||||||
|
final Optional<DataSnapshot.Packed> optionalData = plugin.getDatabase().getSnapshot(user, version);
|
||||||
|
if (optionalData.isEmpty()) {
|
||||||
|
plugin.getLocales().getLocale("error_invalid_version_uuid")
|
||||||
|
.ifPresent(executor::sendMessage);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Pin or unpin the data
|
||||||
|
final DataSnapshot.Packed data = optionalData.get();
|
||||||
|
if (data.isPinned()) {
|
||||||
|
plugin.getDatabase().unpinSnapshot(user, data.getId());
|
||||||
|
} else {
|
||||||
|
plugin.getDatabase().pinSnapshot(user, data.getId());
|
||||||
|
}
|
||||||
|
plugin.getLocales().getLocale(data.isPinned() ? "data_unpinned" : "data_pinned", data.getShortId(),
|
||||||
|
data.getId().toString(), user.getUsername(), user.getUuid().toString())
|
||||||
|
.ifPresent(executor::sendMessage);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Dump a snapshot
|
||||||
|
private void dumpSnapshot(@NotNull CommandUser executor, @NotNull User user, @NotNull UUID version, boolean webDump) {
|
||||||
|
final Optional<DataSnapshot.Packed> data = plugin.getDatabase().getSnapshot(user, version);
|
||||||
|
if (data.isEmpty()) {
|
||||||
|
plugin.getLocales().getLocale("error_invalid_version_uuid")
|
||||||
|
.ifPresent(executor::sendMessage);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Dump the data
|
||||||
|
final DataSnapshot.Packed userData = data.get();
|
||||||
|
final DataDumper dumper = DataDumper.create(userData, user, plugin);
|
||||||
|
try {
|
||||||
|
plugin.getLocales().getLocale("data_dumped", userData.getShortId(), user.getUsername(),
|
||||||
|
(webDump ? dumper.toWeb() : dumper.toFile())).ifPresent(executor::sendMessage);
|
||||||
|
} catch (Throwable e) {
|
||||||
|
plugin.log(Level.SEVERE, "Failed to dump user data", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Nullable
|
@Nullable
|
||||||
@Override
|
@Override
|
||||||
public List<String> suggest(@NotNull CommandUser executor, @NotNull String[] args) {
|
public List<String> suggest(@NotNull CommandUser executor, @NotNull String[] args) {
|
||||||
|
|||||||
@@ -57,7 +57,7 @@ public class Locales {
|
|||||||
Map<String, String> locales = Maps.newTreeMap();
|
Map<String, String> locales = Maps.newTreeMap();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns a raw, un-formatted locale loaded from the locales file
|
* Returns a raw, unformatted locale loaded from the locale file
|
||||||
*
|
*
|
||||||
* @param localeId String identifier of the locale, corresponding to a key in the file
|
* @param localeId String identifier of the locale, corresponding to a key in the file
|
||||||
* @return An {@link Optional} containing the locale corresponding to the id, if it exists
|
* @return An {@link Optional} containing the locale corresponding to the id, if it exists
|
||||||
@@ -152,7 +152,7 @@ public class Locales {
|
|||||||
|
|
||||||
value.append(c);
|
value.append(c);
|
||||||
}
|
}
|
||||||
return value.toString().replace("__", "_\\_");
|
return value.toString();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -30,7 +30,6 @@ import net.william278.husksync.data.Identifier;
|
|||||||
import net.william278.husksync.database.Database;
|
import net.william278.husksync.database.Database;
|
||||||
import net.william278.husksync.listener.EventListener;
|
import net.william278.husksync.listener.EventListener;
|
||||||
import net.william278.husksync.sync.DataSyncer;
|
import net.william278.husksync.sync.DataSyncer;
|
||||||
import org.checkerframework.checker.units.qual.C;
|
|
||||||
import org.jetbrains.annotations.NotNull;
|
import org.jetbrains.annotations.NotNull;
|
||||||
|
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
@@ -76,6 +75,9 @@ public class Settings {
|
|||||||
@Comment({"Whether to enable the Player Analytics hook.", "Docs: https://william278.net/docs/husksync/plan-hook"})
|
@Comment({"Whether to enable the Player Analytics hook.", "Docs: https://william278.net/docs/husksync/plan-hook"})
|
||||||
private boolean enablePlanHook = true;
|
private boolean enablePlanHook = true;
|
||||||
|
|
||||||
|
@Comment("Whether to cancel game event packets directly when handling locked players if ProtocolLib or PacketEvents is installed")
|
||||||
|
private boolean cancelPackets = true;
|
||||||
|
|
||||||
|
|
||||||
// Database settings
|
// Database settings
|
||||||
@Comment("Database settings")
|
@Comment("Database settings")
|
||||||
@@ -86,10 +88,10 @@ public class Settings {
|
|||||||
@NoArgsConstructor(access = AccessLevel.PRIVATE)
|
@NoArgsConstructor(access = AccessLevel.PRIVATE)
|
||||||
public static class DatabaseSettings {
|
public static class DatabaseSettings {
|
||||||
|
|
||||||
@Comment("Type of database to use (MYSQL, MARIADB, MONGO)")
|
@Comment("Type of database to use (MYSQL, MARIADB, POSTGRES, MONGO)")
|
||||||
private Database.Type type = Database.Type.MYSQL;
|
private Database.Type type = Database.Type.MYSQL;
|
||||||
|
|
||||||
@Comment("Specify credentials here for your MYSQL, MARIADB OR MONGO database")
|
@Comment("Specify credentials here for your MYSQL, MARIADB, POSTGRES OR MONGO database")
|
||||||
private DatabaseCredentials credentials = new DatabaseCredentials();
|
private DatabaseCredentials credentials = new DatabaseCredentials();
|
||||||
|
|
||||||
@Getter
|
@Getter
|
||||||
@@ -101,15 +103,13 @@ public class Settings {
|
|||||||
private String database = "HuskSync";
|
private String database = "HuskSync";
|
||||||
private String username = "root";
|
private String username = "root";
|
||||||
private String password = "pa55w0rd";
|
private String password = "pa55w0rd";
|
||||||
@Comment("Only change this if you have select MYSQL or MARIADB")
|
@Comment("Only change this if you're using MARIADB or POSTGRES")
|
||||||
private String parameters = String.join("&",
|
private String parameters = String.join("&",
|
||||||
"?autoReconnect=true", "useSSL=false",
|
"?autoReconnect=true", "useSSL=false",
|
||||||
"useUnicode=true", "characterEncoding=UTF-8");
|
"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!")
|
@Comment("MYSQL, MARIADB, POSTGRES database Hikari connection pool properties. Don't modify this unless you know what you're doing!")
|
||||||
private PoolSettings connectionPool = new PoolSettings();
|
private PoolSettings connectionPool = new PoolSettings();
|
||||||
|
|
||||||
@Getter
|
@Getter
|
||||||
@@ -123,6 +123,19 @@ public class Settings {
|
|||||||
private long connectionTimeout = 5000;
|
private long connectionTimeout = 5000;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Comment("Advanced MongoDB settings. Don't modify unless you know what you're doing!")
|
||||||
|
private MongoSettings mongoSettings = new MongoSettings();
|
||||||
|
|
||||||
|
@Getter
|
||||||
|
@Configuration
|
||||||
|
@NoArgsConstructor(access = AccessLevel.PRIVATE)
|
||||||
|
public static class MongoSettings {
|
||||||
|
private boolean usingAtlas = false;
|
||||||
|
private String parameters = String.join("&",
|
||||||
|
"?retryWrites=true", "w=majority",
|
||||||
|
"authSource=HuskSync");
|
||||||
|
}
|
||||||
|
|
||||||
@Comment("Names of tables to use on your database. Don't modify this unless you know what you're doing!")
|
@Comment("Names of tables to use on your database. Don't modify this unless you know what you're doing!")
|
||||||
@Getter(AccessLevel.NONE)
|
@Getter(AccessLevel.NONE)
|
||||||
private Map<String, String> tableNames = Database.TableName.getDefaults();
|
private Map<String, String> tableNames = Database.TableName.getDefaults();
|
||||||
@@ -133,7 +146,7 @@ public class Settings {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Redis settings
|
// 𝓡𝓮𝓭𝓲𝓼 settings
|
||||||
@Comment("Redis settings")
|
@Comment("Redis settings")
|
||||||
private RedisSettings redis = new RedisSettings();
|
private RedisSettings redis = new RedisSettings();
|
||||||
|
|
||||||
@@ -242,9 +255,6 @@ public class Settings {
|
|||||||
@Comment("Persist maps locked in a Cartography Table to let them be viewed on any server")
|
@Comment("Persist maps locked in a Cartography Table to let them be viewed on any server")
|
||||||
private boolean persistLockedMaps = true;
|
private boolean persistLockedMaps = true;
|
||||||
|
|
||||||
@Comment("Whether to synchronize player max health (requires health syncing to be enabled)")
|
|
||||||
private boolean synchronizeMaxHealth = true;
|
|
||||||
|
|
||||||
@Comment("If using the DELAY sync method, how long should this server listen for Redis key data updates before "
|
@Comment("If using the DELAY sync method, how long should this server listen for Redis key data updates before "
|
||||||
+ "pulling data from the database instead (i.e., if the user did not change servers).")
|
+ "pulling data from the database instead (i.e., if the user did not change servers).")
|
||||||
private int networkLatencyMilliseconds = 500;
|
private int networkLatencyMilliseconds = 500;
|
||||||
@@ -256,6 +266,11 @@ public class Settings {
|
|||||||
@Comment("Commands which should be blocked before a player has finished syncing (Use * to block all commands)")
|
@Comment("Commands which should be blocked before a player has finished syncing (Use * to block all commands)")
|
||||||
private List<String> blacklistedCommandsWhileLocked = new ArrayList<>(List.of("*"));
|
private List<String> blacklistedCommandsWhileLocked = new ArrayList<>(List.of("*"));
|
||||||
|
|
||||||
|
@Comment({"For attribute syncing, which attributes should be ignored/skipped when syncing",
|
||||||
|
"(e.g. ['minecraft:generic.max_health', 'minecraft:generic.attack_damage'])"})
|
||||||
|
@Getter(AccessLevel.NONE)
|
||||||
|
private List<String> ignoredAttributes = new ArrayList<>(List.of(""));
|
||||||
|
|
||||||
@Comment("Event priorities for listeners (HIGHEST, NORMAL, LOWEST). Change if you encounter plugin conflicts")
|
@Comment("Event priorities for listeners (HIGHEST, NORMAL, LOWEST). Change if you encounter plugin conflicts")
|
||||||
@Getter(AccessLevel.NONE)
|
@Getter(AccessLevel.NONE)
|
||||||
private Map<String, String> eventPriorities = EventListener.ListenerType.getDefaults();
|
private Map<String, String> eventPriorities = EventListener.ListenerType.getDefaults();
|
||||||
@@ -268,6 +283,10 @@ public class Settings {
|
|||||||
return id.isCustom() || features.getOrDefault(id.getKeyValue(), id.isEnabledByDefault());
|
return id.isCustom() || features.getOrDefault(id.getKeyValue(), id.isEnabledByDefault());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public boolean isIgnoredAttribute(@NotNull String attribute) {
|
||||||
|
return ignoredAttributes.contains(attribute);
|
||||||
|
}
|
||||||
|
|
||||||
@NotNull
|
@NotNull
|
||||||
public EventListener.Priority getEventPriority(@NotNull EventListener.ListenerType type) {
|
public EventListener.Priority getEventPriority(@NotNull EventListener.ListenerType type) {
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -19,7 +19,9 @@
|
|||||||
|
|
||||||
package net.william278.husksync.data;
|
package net.william278.husksync.data;
|
||||||
|
|
||||||
|
import com.google.common.collect.Sets;
|
||||||
import com.google.gson.annotations.SerializedName;
|
import com.google.gson.annotations.SerializedName;
|
||||||
|
import net.kyori.adventure.key.Key;
|
||||||
import net.william278.husksync.HuskSync;
|
import net.william278.husksync.HuskSync;
|
||||||
import net.william278.husksync.user.OnlineUser;
|
import net.william278.husksync.user.OnlineUser;
|
||||||
import org.jetbrains.annotations.NotNull;
|
import org.jetbrains.annotations.NotNull;
|
||||||
@@ -51,8 +53,8 @@ public interface Data {
|
|||||||
*/
|
*/
|
||||||
interface Items extends Data {
|
interface Items extends Data {
|
||||||
|
|
||||||
@NotNull
|
@Nullable
|
||||||
Stack[] getStack();
|
Stack @NotNull [] getStack();
|
||||||
|
|
||||||
default int getSlotCount() {
|
default int getSlotCount() {
|
||||||
return getStack().length;
|
return getStack().length;
|
||||||
@@ -76,6 +78,9 @@ public interface Data {
|
|||||||
*/
|
*/
|
||||||
interface Inventory extends Items {
|
interface Inventory extends Items {
|
||||||
|
|
||||||
|
String ITEMS_TAG = "items";
|
||||||
|
String HELD_ITEM_SLOT_TAG = "held_item_slot";
|
||||||
|
|
||||||
int getHeldItemSlot();
|
int getHeldItemSlot();
|
||||||
|
|
||||||
void setHeldItemSlot(int heldItemSlot) throws IllegalArgumentException;
|
void setHeldItemSlot(int heldItemSlot) throws IllegalArgumentException;
|
||||||
@@ -283,15 +288,97 @@ public interface Data {
|
|||||||
|
|
||||||
void setHealth(double health);
|
void setHealth(double health);
|
||||||
|
|
||||||
double getMaxHealth();
|
/**
|
||||||
|
* @deprecated Use {@link Attributes#getMaxHealth()} instead
|
||||||
|
*/
|
||||||
|
@Deprecated(forRemoval = true, since = "3.5")
|
||||||
|
default double getMaxHealth() {
|
||||||
|
return getHealth();
|
||||||
|
}
|
||||||
|
|
||||||
void setMaxHealth(double maxHealth);
|
/**
|
||||||
|
* @deprecated Use {@link Attributes#setMaxHealth(double)} instead
|
||||||
|
*/
|
||||||
|
@Deprecated(forRemoval = true, since = "3.5")
|
||||||
|
default void setMaxHealth(double maxHealth) {
|
||||||
|
}
|
||||||
|
|
||||||
double getHealthScale();
|
double getHealthScale();
|
||||||
|
|
||||||
void setHealthScale(double healthScale);
|
void setHealthScale(double healthScale);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A data container holding player attribute data
|
||||||
|
*/
|
||||||
|
interface Attributes extends Data {
|
||||||
|
|
||||||
|
Key MAX_HEALTH_KEY = Key.key("generic.max_health");
|
||||||
|
|
||||||
|
List<Attribute> getAttributes();
|
||||||
|
|
||||||
|
record Attribute(
|
||||||
|
@NotNull String name,
|
||||||
|
double baseValue,
|
||||||
|
@NotNull Set<Modifier> modifiers
|
||||||
|
) {
|
||||||
|
|
||||||
|
public double getValue() {
|
||||||
|
double value = baseValue;
|
||||||
|
for (Modifier modifier : modifiers) {
|
||||||
|
value = modifier.modify(value);
|
||||||
|
}
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
record Modifier(
|
||||||
|
@NotNull UUID uuid,
|
||||||
|
@NotNull String name,
|
||||||
|
double amount,
|
||||||
|
@SerializedName("operation") int operationType,
|
||||||
|
@SerializedName("equipment_slot") int equipmentSlot
|
||||||
|
) {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean equals(Object obj) {
|
||||||
|
return obj instanceof Modifier modifier && modifier.uuid.equals(uuid);
|
||||||
|
}
|
||||||
|
|
||||||
|
public double modify(double value) {
|
||||||
|
return switch (operationType) {
|
||||||
|
case 0 -> value + amount;
|
||||||
|
case 1 -> value * amount;
|
||||||
|
case 2 -> value * (1 + amount);
|
||||||
|
default -> value;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
default Optional<Attribute> getAttribute(@NotNull Key key) {
|
||||||
|
return getAttributes().stream()
|
||||||
|
.filter(attribute -> attribute.name().equals(key.asString()))
|
||||||
|
.findFirst();
|
||||||
|
}
|
||||||
|
|
||||||
|
default void removeAttribute(@NotNull Key key) {
|
||||||
|
getAttributes().removeIf(attribute -> attribute.name().equals(key.asString()));
|
||||||
|
}
|
||||||
|
|
||||||
|
default double getMaxHealth() {
|
||||||
|
return getAttribute(MAX_HEALTH_KEY)
|
||||||
|
.map(Attribute::getValue)
|
||||||
|
.orElse(20.0);
|
||||||
|
}
|
||||||
|
|
||||||
|
default void setMaxHealth(double maxHealth) {
|
||||||
|
removeAttribute(MAX_HEALTH_KEY);
|
||||||
|
getAttributes().add(new Attribute(MAX_HEALTH_KEY.asString(), maxHealth, Sets.newHashSet()));
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A data container holding data for:
|
* A data container holding data for:
|
||||||
* <ul>
|
* <ul>
|
||||||
@@ -341,12 +428,7 @@ public interface Data {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A data container holding data for:
|
* Data container holding data for the player's current game mode
|
||||||
* <ul>
|
|
||||||
* <li>Game mode</li>
|
|
||||||
* <li>Allow flight</li>
|
|
||||||
* <li>Is flying</li>
|
|
||||||
* </ul>
|
|
||||||
*/
|
*/
|
||||||
interface GameMode extends Data {
|
interface GameMode extends Data {
|
||||||
|
|
||||||
@@ -355,13 +437,65 @@ public interface Data {
|
|||||||
|
|
||||||
void setGameMode(@NotNull String gameMode);
|
void setGameMode(@NotNull String gameMode);
|
||||||
|
|
||||||
boolean getAllowFlight();
|
/**
|
||||||
|
* Get if the player can fly.
|
||||||
|
*
|
||||||
|
* @return {@code false} since v3.5
|
||||||
|
* @deprecated Moved to its own data type. This will always return {@code false}.
|
||||||
|
* Use {@link FlightStatus#isAllowFlight()} instead
|
||||||
|
*/
|
||||||
|
@Deprecated(forRemoval = true, since = "3.5")
|
||||||
|
default boolean getAllowFlight() {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set if the player can fly.
|
||||||
|
*
|
||||||
|
* @deprecated Moved to its own data type.
|
||||||
|
* Use {@link FlightStatus#setAllowFlight(boolean)} instead
|
||||||
|
*/
|
||||||
|
@Deprecated(forRemoval = true, since = "3.5")
|
||||||
|
default void setAllowFlight(boolean allowFlight) {
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get if the player is flying.
|
||||||
|
*
|
||||||
|
* @return {@code false} since v3.5
|
||||||
|
* @deprecated Moved to its own data type. This will always return {@code false}.
|
||||||
|
* Use {@link FlightStatus#isFlying()} instead
|
||||||
|
*/
|
||||||
|
@Deprecated(forRemoval = true, since = "3.5")
|
||||||
|
default boolean getIsFlying() {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set if the player is flying.
|
||||||
|
*
|
||||||
|
* @deprecated Moved to its own data type.
|
||||||
|
* Use {@link FlightStatus#setFlying(boolean)} instead
|
||||||
|
*/
|
||||||
|
@Deprecated(forRemoval = true, since = "3.5")
|
||||||
|
default void setIsFlying(boolean isFlying) {
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Data container holding data for the player's flight status
|
||||||
|
*
|
||||||
|
* @since 3.5
|
||||||
|
*/
|
||||||
|
interface FlightStatus extends Data {
|
||||||
|
boolean isAllowFlight();
|
||||||
|
|
||||||
void setAllowFlight(boolean allowFlight);
|
void setAllowFlight(boolean allowFlight);
|
||||||
|
|
||||||
boolean getIsFlying();
|
boolean isFlying();
|
||||||
|
|
||||||
void setIsFlying(boolean isFlying);
|
void setFlying(boolean isFlying);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,72 @@
|
|||||||
|
/*
|
||||||
|
* 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.data;
|
||||||
|
|
||||||
|
import lombok.AllArgsConstructor;
|
||||||
|
import lombok.Getter;
|
||||||
|
import net.william278.husksync.HuskSync;
|
||||||
|
import org.jetbrains.annotations.NotNull;
|
||||||
|
|
||||||
|
import java.util.function.BiFunction;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* An exception related to {@link DataSnapshot} formatting, thrown if an exception occurs when unpacking a snapshot
|
||||||
|
*/
|
||||||
|
@Getter
|
||||||
|
public class DataException extends IllegalStateException {
|
||||||
|
|
||||||
|
private final Reason reason;
|
||||||
|
|
||||||
|
private DataException(@NotNull DataException.Reason reason, @NotNull DataSnapshot data, @NotNull HuskSync plugin) {
|
||||||
|
super(reason.getMessage(plugin, data));
|
||||||
|
this.reason = reason;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reasons why {@link DataException}s were thrown
|
||||||
|
*/
|
||||||
|
@AllArgsConstructor
|
||||||
|
public enum Reason {
|
||||||
|
INVALID_MINECRAFT_VERSION((plugin, snapshot) -> String.format("The Minecraft version of the snapshot (%s) is " +
|
||||||
|
"newer than the server's version (%s). Ensure each server is on the same version of Minecraft.",
|
||||||
|
snapshot.getMinecraftVersion(), plugin.getMinecraftVersion())),
|
||||||
|
INVALID_FORMAT_VERSION((plugin, snapshot) -> String.format("The format version of the snapshot (%s) is newer " +
|
||||||
|
"than the server's version (%s). Ensure each server is running the same version of HuskSync.",
|
||||||
|
snapshot.getFormatVersion(), DataSnapshot.CURRENT_FORMAT_VERSION)),
|
||||||
|
INVALID_PLATFORM_TYPE((plugin, snapshot) -> String.format("The platform type of the snapshot (%s) does " +
|
||||||
|
"not match the server's platform type (%s). Ensure each server has the same platform type.",
|
||||||
|
snapshot.getPlatformType(), plugin.getPlatformType())),
|
||||||
|
NO_LEGACY_CONVERTER((plugin, snapshot) -> String.format("No legacy converter to convert format version: %s",
|
||||||
|
snapshot.getFormatVersion()));
|
||||||
|
|
||||||
|
private final BiFunction<HuskSync, DataSnapshot, String> exception;
|
||||||
|
|
||||||
|
@NotNull
|
||||||
|
String getMessage(@NotNull HuskSync plugin, @NotNull DataSnapshot data) {
|
||||||
|
return exception.apply(plugin, data);
|
||||||
|
}
|
||||||
|
|
||||||
|
@NotNull
|
||||||
|
DataException toException(@NotNull DataSnapshot data, @NotNull HuskSync plugin) {
|
||||||
|
return new DataException(this, data, plugin);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -110,6 +110,15 @@ public interface DataHolder {
|
|||||||
getData().put(Identifier.HUNGER, hunger);
|
getData().put(Identifier.HUNGER, hunger);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@NotNull
|
||||||
|
default Optional<Data.Attributes> getAttributes() {
|
||||||
|
return Optional.ofNullable((Data.Attributes) getData().get(Identifier.ATTRIBUTES));
|
||||||
|
}
|
||||||
|
|
||||||
|
default void setAttributes(@NotNull Data.Attributes attributes) {
|
||||||
|
getData().put(Identifier.ATTRIBUTES, attributes);
|
||||||
|
}
|
||||||
|
|
||||||
@NotNull
|
@NotNull
|
||||||
default Optional<Data.Experience> getExperience() {
|
default Optional<Data.Experience> getExperience() {
|
||||||
return Optional.ofNullable((Data.Experience) getData().get(Identifier.EXPERIENCE));
|
return Optional.ofNullable((Data.Experience) getData().get(Identifier.EXPERIENCE));
|
||||||
@@ -128,6 +137,15 @@ public interface DataHolder {
|
|||||||
getData().put(Identifier.GAME_MODE, gameMode);
|
getData().put(Identifier.GAME_MODE, gameMode);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@NotNull
|
||||||
|
default Optional<Data.FlightStatus> getFlightStatus() {
|
||||||
|
return Optional.ofNullable((Data.FlightStatus) getData().get(Identifier.FLIGHT_STATUS));
|
||||||
|
}
|
||||||
|
|
||||||
|
default void setFlightStatus(@NotNull Data.FlightStatus flightStatus) {
|
||||||
|
getData().put(Identifier.FLIGHT_STATUS, flightStatus);
|
||||||
|
}
|
||||||
|
|
||||||
@NotNull
|
@NotNull
|
||||||
default Optional<Data.PersistentData> getPersistentData() {
|
default Optional<Data.PersistentData> getPersistentData() {
|
||||||
return Optional.ofNullable((Data.PersistentData) getData().get(Identifier.PERSISTENT_DATA));
|
return Optional.ofNullable((Data.PersistentData) getData().get(Identifier.PERSISTENT_DATA));
|
||||||
|
|||||||
@@ -47,6 +47,7 @@ import java.util.stream.Collectors;
|
|||||||
*
|
*
|
||||||
* @since 3.0
|
* @since 3.0
|
||||||
*/
|
*/
|
||||||
|
@SuppressWarnings({"LombokSetterMayBeUsed", "LombokGetterMayBeUsed"})
|
||||||
@NoArgsConstructor(access = AccessLevel.PRIVATE)
|
@NoArgsConstructor(access = AccessLevel.PRIVATE)
|
||||||
public class DataSnapshot {
|
public class DataSnapshot {
|
||||||
|
|
||||||
@@ -83,6 +84,11 @@ public class DataSnapshot {
|
|||||||
@SerializedName("data")
|
@SerializedName("data")
|
||||||
protected Map<String, String> data;
|
protected Map<String, String> data;
|
||||||
|
|
||||||
|
// If the snapshot is invalid, this will be set to the validation exception
|
||||||
|
@Nullable
|
||||||
|
@Expose(serialize = false, deserialize = false)
|
||||||
|
transient DataException.Reason exception = null;
|
||||||
|
|
||||||
private DataSnapshot(@NotNull UUID id, boolean pinned, @NotNull OffsetDateTime timestamp,
|
private DataSnapshot(@NotNull UUID id, boolean pinned, @NotNull OffsetDateTime timestamp,
|
||||||
@NotNull String 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 Version minecraftVersion, @NotNull String platformType, int formatVersion) {
|
||||||
@@ -107,37 +113,25 @@ public class DataSnapshot {
|
|||||||
@NotNull
|
@NotNull
|
||||||
@ApiStatus.Internal
|
@ApiStatus.Internal
|
||||||
public static DataSnapshot.Packed deserialize(@NotNull HuskSync plugin, byte[] data, @Nullable UUID id,
|
public static DataSnapshot.Packed deserialize(@NotNull HuskSync plugin, byte[] data, @Nullable UUID id,
|
||||||
@Nullable OffsetDateTime timestamp) throws IllegalStateException {
|
@Nullable OffsetDateTime timestamp) {
|
||||||
final DataSnapshot.Packed snapshot = plugin.getDataAdapter().fromBytes(data, DataSnapshot.Packed.class);
|
final DataSnapshot.Packed snapshot = plugin.getDataAdapter().fromBytes(data, DataSnapshot.Packed.class);
|
||||||
if (snapshot.getMinecraftVersion().compareTo(plugin.getMinecraftVersion()) > 0) {
|
if (snapshot.getMinecraftVersion().compareTo(plugin.getMinecraftVersion()) > 0) {
|
||||||
throw new IllegalStateException(String.format("Cannot set data for user because the Minecraft version of " +
|
return snapshot.invalid(DataException.Reason.INVALID_MINECRAFT_VERSION);
|
||||||
"their user data (%s) is newer than the server's Minecraft version (%s)." +
|
|
||||||
"Please ensure each server is running the same version of Minecraft.",
|
|
||||||
snapshot.getMinecraftVersion(), plugin.getMinecraftVersion()));
|
|
||||||
}
|
}
|
||||||
if (snapshot.getFormatVersion() > CURRENT_FORMAT_VERSION) {
|
if (snapshot.getFormatVersion() > CURRENT_FORMAT_VERSION) {
|
||||||
throw new IllegalStateException(String.format("Cannot set data for user because the format version of " +
|
return snapshot.invalid(DataException.Reason.INVALID_FORMAT_VERSION);
|
||||||
"their user data (%s) is newer than the current format version (%s). " +
|
|
||||||
"Please ensure each server is running the latest version of HuskSync.",
|
|
||||||
snapshot.getFormatVersion(), CURRENT_FORMAT_VERSION));
|
|
||||||
}
|
}
|
||||||
if (snapshot.getFormatVersion() < 4) {
|
if (snapshot.getFormatVersion() < 4) {
|
||||||
if (plugin.getLegacyConverter().isPresent()) {
|
if (plugin.getLegacyConverter().isPresent()) {
|
||||||
return plugin.getLegacyConverter().get().convert(
|
return plugin.getLegacyConverter().get().convert(
|
||||||
data,
|
data, Objects.requireNonNull(id, "Attempted legacy conversion with null UUID!"),
|
||||||
Objects.requireNonNull(id, "Attempted legacy conversion with null UUID!"),
|
|
||||||
Objects.requireNonNull(timestamp, "Attempted legacy conversion with null timestamp!")
|
Objects.requireNonNull(timestamp, "Attempted legacy conversion with null timestamp!")
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
throw new IllegalStateException(String.format(
|
return snapshot.invalid(DataException.Reason.NO_LEGACY_CONVERTER);
|
||||||
"No legacy converter to convert format version: %s", snapshot.getFormatVersion()
|
|
||||||
));
|
|
||||||
}
|
}
|
||||||
if (!snapshot.getPlatformType().equalsIgnoreCase(plugin.getPlatformType())) {
|
if (!snapshot.getPlatformType().equalsIgnoreCase(plugin.getPlatformType())) {
|
||||||
throw new IllegalStateException(String.format("Cannot set data for user because the platform type of " +
|
return snapshot.invalid(DataException.Reason.INVALID_PLATFORM_TYPE);
|
||||||
"their user data (%s) is different to the server platform type (%s). " +
|
|
||||||
"Please ensure each server is running the same platform type.",
|
|
||||||
snapshot.getPlatformType(), plugin.getPlatformType()));
|
|
||||||
}
|
}
|
||||||
return snapshot;
|
return snapshot;
|
||||||
}
|
}
|
||||||
@@ -160,6 +154,17 @@ public class DataSnapshot {
|
|||||||
return id;
|
return id;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* <b>Internal use only</b> Set the ID of the snapshot
|
||||||
|
*
|
||||||
|
* @param id The snapshot ID
|
||||||
|
* @since 3.0
|
||||||
|
*/
|
||||||
|
@ApiStatus.Internal
|
||||||
|
public void setId(@NotNull UUID id) {
|
||||||
|
this.id = id;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the short display ID of the snapshot
|
* Get the short display ID of the snapshot
|
||||||
*
|
*
|
||||||
@@ -282,6 +287,32 @@ public class DataSnapshot {
|
|||||||
super(id, pinned, timestamp, saveCause, serverName, data, minecraftVersion, platformType, formatVersion);
|
super(id, pinned, timestamp, saveCause, serverName, data, minecraftVersion, platformType, formatVersion);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@NotNull
|
||||||
|
@ApiStatus.Internal
|
||||||
|
DataSnapshot.Packed invalid(@NotNull DataException.Reason reason) {
|
||||||
|
this.exception = reason;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isInvalid() {
|
||||||
|
return exception != null;
|
||||||
|
}
|
||||||
|
|
||||||
|
@NotNull
|
||||||
|
public String getInvalidReason(@NotNull HuskSync plugin) {
|
||||||
|
if (exception == null) {
|
||||||
|
throw new IllegalStateException("Attempted to get an invalid reason for a valid snapshot!");
|
||||||
|
}
|
||||||
|
return exception.getMessage(plugin, this);
|
||||||
|
}
|
||||||
|
|
||||||
|
@ApiStatus.Internal
|
||||||
|
void validate(@NotNull HuskSync plugin) throws DataException {
|
||||||
|
if (exception != null) {
|
||||||
|
throw exception.toException(this, plugin);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@ApiStatus.Internal
|
@ApiStatus.Internal
|
||||||
public void edit(@NotNull HuskSync plugin, @NotNull Consumer<Unpacked> editor) {
|
public void edit(@NotNull HuskSync plugin, @NotNull Consumer<Unpacked> editor) {
|
||||||
final Unpacked data = unpack(plugin);
|
final Unpacked data = unpack(plugin);
|
||||||
@@ -321,7 +352,8 @@ public class DataSnapshot {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@NotNull
|
@NotNull
|
||||||
public DataSnapshot.Unpacked unpack(@NotNull HuskSync plugin) {
|
public DataSnapshot.Unpacked unpack(@NotNull HuskSync plugin) throws DataException {
|
||||||
|
this.validate(plugin);
|
||||||
return new Unpacked(
|
return new Unpacked(
|
||||||
id, pinned, timestamp, saveCause, serverName, data,
|
id, pinned, timestamp, saveCause, serverName, data,
|
||||||
getMinecraftVersion(), platformType, formatVersion, plugin
|
getMinecraftVersion(), platformType, formatVersion, plugin
|
||||||
@@ -360,7 +392,7 @@ public class DataSnapshot {
|
|||||||
private Map<Identifier, Data> deserializeData(@NotNull HuskSync plugin) {
|
private Map<Identifier, Data> deserializeData(@NotNull HuskSync plugin) {
|
||||||
return data.entrySet().stream()
|
return data.entrySet().stream()
|
||||||
.map((entry) -> plugin.getIdentifier(entry.getKey()).map(id -> Map.entry(
|
.map((entry) -> plugin.getIdentifier(entry.getKey()).map(id -> Map.entry(
|
||||||
id, plugin.getSerializers().get(id).deserialize(entry.getValue())
|
id, plugin.getSerializers().get(id).deserialize(entry.getValue(), getMinecraftVersion())
|
||||||
)).orElse(null))
|
)).orElse(null))
|
||||||
.filter(Objects::nonNull)
|
.filter(Objects::nonNull)
|
||||||
.collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue));
|
.collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue));
|
||||||
@@ -658,6 +690,21 @@ public class DataSnapshot {
|
|||||||
return data(Identifier.HUNGER, hunger);
|
return data(Identifier.HUNGER, hunger);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set the attributes of the snapshot
|
||||||
|
* <p>
|
||||||
|
* Equivalent to {@code data(Identifier.ATTRIBUTES, attributes)}
|
||||||
|
* </p>
|
||||||
|
*
|
||||||
|
* @param attributes The user's attributes
|
||||||
|
* @return The builder
|
||||||
|
* @since 3.5
|
||||||
|
*/
|
||||||
|
@NotNull
|
||||||
|
public Builder attributes(@NotNull Data.Attributes attributes) {
|
||||||
|
return data(Identifier.ATTRIBUTES, attributes);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Set the experience of the snapshot
|
* Set the experience of the snapshot
|
||||||
* <p>
|
* <p>
|
||||||
@@ -688,6 +735,21 @@ public class DataSnapshot {
|
|||||||
return data(Identifier.GAME_MODE, gameMode);
|
return data(Identifier.GAME_MODE, gameMode);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set the flight status of the snapshot
|
||||||
|
* <p>
|
||||||
|
* Equivalent to {@code data(Identifier.FLIGHT_STATUS, flightStatus)}
|
||||||
|
* </p>
|
||||||
|
*
|
||||||
|
* @param flightStatus The flight status
|
||||||
|
* @return The builder
|
||||||
|
* @since 3.5
|
||||||
|
*/
|
||||||
|
@NotNull
|
||||||
|
public Builder flightStatus(@NotNull Data.FlightStatus flightStatus) {
|
||||||
|
return data(Identifier.FLIGHT_STATUS, flightStatus);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Set the persistent data container of the snapshot
|
* Set the persistent data container of the snapshot
|
||||||
* <p>
|
* <p>
|
||||||
@@ -795,7 +857,7 @@ public class DataSnapshot {
|
|||||||
*
|
*
|
||||||
* @since 2.0
|
* @since 2.0
|
||||||
*/
|
*/
|
||||||
public static final SaveCause SERVER_SHUTDOWN = of("SERVER_SHUTDOWN");
|
public static final SaveCause SERVER_SHUTDOWN = of("SERVER_SHUTDOWN", false);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Indicates data was saved by editing inventory contents via the {@code /inventory} command
|
* Indicates data was saved by editing inventory contents via the {@code /inventory} command
|
||||||
@@ -830,25 +892,27 @@ public class DataSnapshot {
|
|||||||
*
|
*
|
||||||
* @since 2.0
|
* @since 2.0
|
||||||
*/
|
*/
|
||||||
public static final SaveCause MPDB_MIGRATION = of("MPDB_MIGRATION");
|
public static final SaveCause MPDB_MIGRATION = of("MPDB_MIGRATION", false);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Indicates data was saved from being imported from a legacy version (v1.x -> v2.x)
|
* Indicates data was saved from being imported from a legacy version (v1.x -> v2.x)
|
||||||
*
|
*
|
||||||
* @since 2.0
|
* @since 2.0
|
||||||
*/
|
*/
|
||||||
public static final SaveCause LEGACY_MIGRATION = of("LEGACY_MIGRATION");
|
public static final SaveCause LEGACY_MIGRATION = of("LEGACY_MIGRATION", false);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Indicates data was saved from being imported from a legacy version (v2.x -> v3.x)
|
* Indicates data was saved from being imported from a legacy version (v2.x -> v3.x)
|
||||||
*
|
*
|
||||||
* @since 3.0
|
* @since 3.0
|
||||||
*/
|
*/
|
||||||
public static final SaveCause CONVERTED_FROM_V2 = of("CONVERTED_FROM_V2");
|
public static final SaveCause CONVERTED_FROM_V2 = of("CONVERTED_FROM_V2", false);
|
||||||
|
|
||||||
@NotNull
|
@NotNull
|
||||||
private final String name;
|
private final String name;
|
||||||
|
|
||||||
|
private final boolean fireDataSaveEvent;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get or create a {@link SaveCause} from a name
|
* Get or create a {@link SaveCause} from a name
|
||||||
*
|
*
|
||||||
@@ -857,13 +921,25 @@ public class DataSnapshot {
|
|||||||
*/
|
*/
|
||||||
@NotNull
|
@NotNull
|
||||||
public static SaveCause of(@NotNull String name) {
|
public static SaveCause of(@NotNull String name) {
|
||||||
return new SaveCause(name.length() > 32 ? name.substring(0, 31) : name);
|
return new SaveCause(name.length() > 32 ? name.substring(0, 31) : name, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get or create a {@link SaveCause} from a name and whether it should fire a save event
|
||||||
|
*
|
||||||
|
* @param name the name to be displayed
|
||||||
|
* @param firesSaveEvent whether the cause should fire a save event
|
||||||
|
* @return the cause
|
||||||
|
*/
|
||||||
|
@NotNull
|
||||||
|
public static SaveCause of(@NotNull String name, boolean firesSaveEvent) {
|
||||||
|
return new SaveCause(name.length() > 32 ? name.substring(0, 31) : name, firesSaveEvent);
|
||||||
}
|
}
|
||||||
|
|
||||||
@NotNull
|
@NotNull
|
||||||
public String getLocale(@NotNull HuskSync plugin) {
|
public String getLocale(@NotNull HuskSync plugin) {
|
||||||
return plugin.getLocales()
|
return plugin.getLocales()
|
||||||
.getRawLocale("save_cause_" + name().toLowerCase(Locale.ENGLISH))
|
.getRawLocale("save_cause_%s".formatted(name().toLowerCase(Locale.ENGLISH)))
|
||||||
.orElse(getDisplayName());
|
.orElse(getDisplayName());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -41,8 +41,10 @@ public class Identifier {
|
|||||||
public static Identifier STATISTICS = huskSync("statistics", true);
|
public static Identifier STATISTICS = huskSync("statistics", true);
|
||||||
public static Identifier HEALTH = huskSync("health", true);
|
public static Identifier HEALTH = huskSync("health", true);
|
||||||
public static Identifier HUNGER = huskSync("hunger", true);
|
public static Identifier HUNGER = huskSync("hunger", true);
|
||||||
|
public static Identifier ATTRIBUTES = huskSync("attributes", true);
|
||||||
public static Identifier EXPERIENCE = huskSync("experience", true);
|
public static Identifier EXPERIENCE = huskSync("experience", true);
|
||||||
public static Identifier GAME_MODE = huskSync("game_mode", true);
|
public static Identifier GAME_MODE = huskSync("game_mode", true);
|
||||||
|
public static Identifier FLIGHT_STATUS = huskSync("flight_status", true);
|
||||||
public static Identifier PERSISTENT_DATA = huskSync("persistent_data", true);
|
public static Identifier PERSISTENT_DATA = huskSync("persistent_data", true);
|
||||||
|
|
||||||
private final Key key;
|
private final Key key;
|
||||||
@@ -113,8 +115,8 @@ public class Identifier {
|
|||||||
@SuppressWarnings("unchecked")
|
@SuppressWarnings("unchecked")
|
||||||
public static Map<String, Boolean> getConfigMap() {
|
public static Map<String, Boolean> getConfigMap() {
|
||||||
return Map.ofEntries(Stream.of(
|
return Map.ofEntries(Stream.of(
|
||||||
INVENTORY, ENDER_CHEST, POTION_EFFECTS, ADVANCEMENTS, LOCATION,
|
INVENTORY, ENDER_CHEST, POTION_EFFECTS, ADVANCEMENTS, LOCATION, STATISTICS,
|
||||||
STATISTICS, HEALTH, HUNGER, EXPERIENCE, GAME_MODE, PERSISTENT_DATA
|
HEALTH, HUNGER, ATTRIBUTES, EXPERIENCE, GAME_MODE, FLIGHT_STATUS, PERSISTENT_DATA
|
||||||
)
|
)
|
||||||
.map(Identifier::getConfigEntry)
|
.map(Identifier::getConfigEntry)
|
||||||
.toArray(Map.Entry[]::new));
|
.toArray(Map.Entry[]::new));
|
||||||
|
|||||||
@@ -19,22 +19,27 @@
|
|||||||
|
|
||||||
package net.william278.husksync.data;
|
package net.william278.husksync.data;
|
||||||
|
|
||||||
|
import net.william278.desertwell.util.Version;
|
||||||
import org.jetbrains.annotations.NotNull;
|
import org.jetbrains.annotations.NotNull;
|
||||||
|
|
||||||
public interface Serializer<T extends Data> {
|
public interface Serializer<T extends Data> {
|
||||||
|
|
||||||
T deserialize(@NotNull String serialized) throws DeserializationException;
|
T deserialize(@NotNull String serialized);
|
||||||
|
|
||||||
|
default T deserialize(@NotNull String serialized, @NotNull Version dataMcVersion) throws DeserializationException {
|
||||||
|
return deserialize(serialized);
|
||||||
|
}
|
||||||
|
|
||||||
@NotNull
|
@NotNull
|
||||||
String serialize(@NotNull T element) throws SerializationException;
|
String serialize(@NotNull T element) throws SerializationException;
|
||||||
|
|
||||||
static final class DeserializationException extends IllegalStateException {
|
final class DeserializationException extends IllegalStateException {
|
||||||
DeserializationException(@NotNull String message, @NotNull Throwable cause) {
|
DeserializationException(@NotNull String message, @NotNull Throwable cause) {
|
||||||
super(message, cause);
|
super(message, cause);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
static final class SerializationException extends IllegalStateException {
|
final class SerializationException extends IllegalStateException {
|
||||||
SerializationException(@NotNull String message, @NotNull Throwable cause) {
|
SerializationException(@NotNull String message, @NotNull Throwable cause) {
|
||||||
super(message, cause);
|
super(message, cause);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -60,7 +60,7 @@ public interface UserDataHolder extends DataHolder {
|
|||||||
*/
|
*/
|
||||||
@Override
|
@Override
|
||||||
default void setData(@NotNull Identifier identifier, @NotNull Data data) {
|
default void setData(@NotNull Identifier identifier, @NotNull Data data) {
|
||||||
getPlugin().runSync(() -> data.apply(this, getPlugin()));
|
getPlugin().runSync(() -> data.apply(this, getPlugin()), this);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -97,6 +97,7 @@ public interface UserDataHolder extends DataHolder {
|
|||||||
unpacked = snapshot.unpack(plugin);
|
unpacked = snapshot.unpack(plugin);
|
||||||
} catch (Throwable e) {
|
} catch (Throwable e) {
|
||||||
plugin.log(Level.SEVERE, String.format("Failed to unpack data snapshot for %s", getUsername()), e);
|
plugin.log(Level.SEVERE, String.format("Failed to unpack data snapshot for %s", getUsername()), e);
|
||||||
|
runAfter.accept(false);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -118,7 +119,7 @@ public interface UserDataHolder extends DataHolder {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
plugin.runAsync(() -> runAfter.accept(true));
|
plugin.runAsync(() -> runAfter.accept(true));
|
||||||
});
|
}, this);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -171,6 +172,11 @@ public interface UserDataHolder extends DataHolder {
|
|||||||
this.setData(Identifier.GAME_MODE, gameMode);
|
this.setData(Identifier.GAME_MODE, gameMode);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
default void setFlightStatus(@NotNull Data.FlightStatus flightStatus) {
|
||||||
|
this.setData(Identifier.FLIGHT_STATUS, flightStatus);
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
default void setPersistentData(@NotNull Data.PersistentData persistentData) {
|
default void setPersistentData(@NotNull Data.PersistentData persistentData) {
|
||||||
this.setData(Identifier.PERSISTENT_DATA, persistentData);
|
this.setData(Identifier.PERSISTENT_DATA, persistentData);
|
||||||
|
|||||||
@@ -19,7 +19,6 @@
|
|||||||
|
|
||||||
package net.william278.husksync.database;
|
package net.william278.husksync.database;
|
||||||
|
|
||||||
import lombok.AllArgsConstructor;
|
|
||||||
import lombok.Getter;
|
import lombok.Getter;
|
||||||
import net.william278.husksync.HuskSync;
|
import net.william278.husksync.HuskSync;
|
||||||
import net.william278.husksync.config.Settings;
|
import net.william278.husksync.config.Settings;
|
||||||
@@ -258,6 +257,7 @@ public abstract class Database {
|
|||||||
public enum Type {
|
public enum Type {
|
||||||
MYSQL("MySQL", "mysql"),
|
MYSQL("MySQL", "mysql"),
|
||||||
MARIADB("MariaDB", "mariadb"),
|
MARIADB("MariaDB", "mariadb"),
|
||||||
|
POSTGRES("PostgreSQL", "postgresql"),
|
||||||
MONGO("MongoDB", "mongo");
|
MONGO("MongoDB", "mongo");
|
||||||
|
|
||||||
private final String displayName;
|
private final String displayName;
|
||||||
|
|||||||
@@ -20,6 +20,7 @@
|
|||||||
package net.william278.husksync.database;
|
package net.william278.husksync.database;
|
||||||
|
|
||||||
import com.google.common.collect.Lists;
|
import com.google.common.collect.Lists;
|
||||||
|
import com.mongodb.ConnectionString;
|
||||||
import com.mongodb.MongoException;
|
import com.mongodb.MongoException;
|
||||||
import com.mongodb.client.FindIterable;
|
import com.mongodb.client.FindIterable;
|
||||||
import com.mongodb.client.model.Updates;
|
import com.mongodb.client.model.Updates;
|
||||||
@@ -64,14 +65,8 @@ public class MongoDbDatabase extends Database {
|
|||||||
public void initialize() throws IllegalStateException {
|
public void initialize() throws IllegalStateException {
|
||||||
final Settings.DatabaseSettings.DatabaseCredentials credentials = plugin.getSettings().getDatabase().getCredentials();
|
final Settings.DatabaseSettings.DatabaseCredentials credentials = plugin.getSettings().getDatabase().getCredentials();
|
||||||
try {
|
try {
|
||||||
mongoConnectionHandler = new MongoConnectionHandler(
|
ConnectionString URI = createConnectionURI(credentials);
|
||||||
credentials.getHost(),
|
mongoConnectionHandler = new MongoConnectionHandler(URI, credentials.getDatabase());
|
||||||
credentials.getPort(),
|
|
||||||
credentials.getUsername(),
|
|
||||||
credentials.getPassword(),
|
|
||||||
credentials.getDatabase(),
|
|
||||||
credentials.getMongoAuthDb()
|
|
||||||
);
|
|
||||||
mongoCollectionHelper = new MongoCollectionHelper(mongoConnectionHandler);
|
mongoCollectionHelper = new MongoCollectionHelper(mongoConnectionHandler);
|
||||||
if (mongoCollectionHelper.getCollection(usersTable) == null) {
|
if (mongoCollectionHelper.getCollection(usersTable) == null) {
|
||||||
mongoCollectionHelper.createCollection(usersTable);
|
mongoCollectionHelper.createCollection(usersTable);
|
||||||
@@ -85,6 +80,19 @@ public class MongoDbDatabase extends Database {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@NotNull
|
||||||
|
private ConnectionString createConnectionURI(Settings.DatabaseSettings.DatabaseCredentials credentials) {
|
||||||
|
String baseURI = plugin.getSettings().getDatabase().getMongoSettings().isUsingAtlas() ?
|
||||||
|
"mongodb+srv://{0}:{1}@{2}/{4}{5}" : "mongodb://{0}:{1}@{2}:{3}/{4}{5}";
|
||||||
|
baseURI = baseURI.replace("{0}", credentials.getUsername());
|
||||||
|
baseURI = baseURI.replace("{1}", credentials.getPassword());
|
||||||
|
baseURI = baseURI.replace("{2}", credentials.getHost());
|
||||||
|
baseURI = baseURI.replace("{3}", String.valueOf(credentials.getPort()));
|
||||||
|
baseURI = baseURI.replace("{4}", credentials.getDatabase());
|
||||||
|
baseURI = baseURI.replace("{5}", plugin.getSettings().getDatabase().getMongoSettings().getParameters());
|
||||||
|
return new ConnectionString(baseURI);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Ensure a {@link User} has an entry in the database and that their username is up-to-date
|
* Ensure a {@link User} has an entry in the database and that their username is up-to-date
|
||||||
*
|
*
|
||||||
@@ -93,31 +101,38 @@ public class MongoDbDatabase extends Database {
|
|||||||
@Blocking
|
@Blocking
|
||||||
@Override
|
@Override
|
||||||
public void ensureUser(@NotNull User user) {
|
public void ensureUser(@NotNull User user) {
|
||||||
getUser(user.getUuid()).ifPresentOrElse(
|
try {
|
||||||
existingUser -> {
|
getUser(user.getUuid()).ifPresentOrElse(
|
||||||
if (!existingUser.getUsername().equals(user.getUsername())) {
|
existingUser -> {
|
||||||
// Update a user's name if it has changed in the database
|
if (!existingUser.getUsername().equals(user.getUsername())) {
|
||||||
try {
|
// Update a user's name if it has changed in the database
|
||||||
Document filter = new Document("uuid", existingUser.getUuid().toString());
|
try {
|
||||||
Document doc = mongoCollectionHelper.getCollection(usersTable).find(filter).first();
|
Document filter = new Document("uuid", existingUser.getUuid().toString());
|
||||||
|
Document doc = mongoCollectionHelper.getCollection(usersTable).find(filter).first();
|
||||||
|
if (doc == null) {
|
||||||
|
throw new MongoException("User document returned null!");
|
||||||
|
}
|
||||||
|
|
||||||
Bson updates = Updates.set("uuid", user.getUuid().toString());
|
Bson updates = Updates.set("username", user.getUsername());
|
||||||
mongoCollectionHelper.updateDocument(usersTable, doc, updates);
|
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) {
|
} catch (MongoException e) {
|
||||||
plugin.log(Level.SEVERE, "Failed to insert a user into the database", e);
|
plugin.log(Level.SEVERE, "Failed to insert a user into the database", e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
);
|
||||||
() -> {
|
} catch (MongoException e) {
|
||||||
// Insert new player data into the database
|
plugin.log(Level.SEVERE, "Failed to ensure user data is in the database", e);
|
||||||
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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -129,13 +144,18 @@ public class MongoDbDatabase extends Database {
|
|||||||
@Blocking
|
@Blocking
|
||||||
@Override
|
@Override
|
||||||
public Optional<User> getUser(@NotNull UUID uuid) {
|
public Optional<User> getUser(@NotNull UUID uuid) {
|
||||||
Document filter = new Document("uuid", uuid);
|
try {
|
||||||
Document doc = mongoCollectionHelper.getCollection(usersTable).find(filter).first();
|
Document filter = new Document("uuid", uuid);
|
||||||
if (doc != null) {
|
Document doc = mongoCollectionHelper.getCollection(usersTable).find(filter).first();
|
||||||
return Optional.of(new User(UUID.fromString(doc.getString("uuid")),
|
if (doc != null) {
|
||||||
doc.getString("username")));
|
return Optional.of(new User(UUID.fromString(doc.getString("uuid")),
|
||||||
|
doc.getString("username")));
|
||||||
|
}
|
||||||
|
return Optional.empty();
|
||||||
|
} catch (MongoException e) {
|
||||||
|
plugin.log(Level.SEVERE, "Failed to get user data from the database", e);
|
||||||
|
return Optional.empty();
|
||||||
}
|
}
|
||||||
return Optional.empty();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -147,13 +167,18 @@ public class MongoDbDatabase extends Database {
|
|||||||
@Blocking
|
@Blocking
|
||||||
@Override
|
@Override
|
||||||
public Optional<User> getUserByName(@NotNull String username) {
|
public Optional<User> getUserByName(@NotNull String username) {
|
||||||
Document filter = new Document("username", username);
|
try {
|
||||||
Document doc = mongoCollectionHelper.getCollection(usersTable).find(filter).first();
|
Document filter = new Document("username", username);
|
||||||
if (doc != null) {
|
Document doc = mongoCollectionHelper.getCollection(usersTable).find(filter).first();
|
||||||
return Optional.of(new User(UUID.fromString(doc.getString("uuid")),
|
if (doc != null) {
|
||||||
doc.getString("username")));
|
return Optional.of(new User(UUID.fromString(doc.getString("uuid")),
|
||||||
|
doc.getString("username")));
|
||||||
|
}
|
||||||
|
return Optional.empty();
|
||||||
|
} catch (MongoException e) {
|
||||||
|
plugin.log(Level.SEVERE, "Failed to get user data from the database", e);
|
||||||
|
return Optional.empty();
|
||||||
}
|
}
|
||||||
return Optional.empty();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -165,18 +190,23 @@ public class MongoDbDatabase extends Database {
|
|||||||
@Blocking
|
@Blocking
|
||||||
@Override
|
@Override
|
||||||
public Optional<DataSnapshot.Packed> getLatestSnapshot(@NotNull User user) {
|
public Optional<DataSnapshot.Packed> getLatestSnapshot(@NotNull User user) {
|
||||||
Document filter = new Document("player_uuid", user.getUuid().toString());
|
try {
|
||||||
Document sort = new Document("timestamp", -1); // -1 = Descending
|
Document filter = new Document("player_uuid", user.getUuid().toString());
|
||||||
FindIterable<Document> iterable = mongoCollectionHelper.getCollection(userDataTable).find(filter).sort(sort);
|
Document sort = new Document("timestamp", -1); // -1 = Descending
|
||||||
Document doc = iterable.first();
|
FindIterable<Document> iterable = mongoCollectionHelper.getCollection(userDataTable).find(filter).sort(sort);
|
||||||
if (doc != null) {
|
Document doc = iterable.first();
|
||||||
final UUID versionUuid = UUID.fromString(doc.getString("version_uuid"));
|
if (doc != null) {
|
||||||
final OffsetDateTime timestamp = OffsetDateTime.ofInstant(Instant.ofEpochMilli((long) doc.get("timestamp")), TimeZone.getDefault().toZoneId());
|
final UUID versionUuid = UUID.fromString(doc.getString("version_uuid"));
|
||||||
final Binary bin = doc.get("data", Binary.class);
|
final OffsetDateTime timestamp = OffsetDateTime.ofInstant(Instant.ofEpochMilli((long) doc.get("timestamp")), TimeZone.getDefault().toZoneId());
|
||||||
final byte[] dataByteArray = bin.getData();
|
final Binary bin = doc.get("data", Binary.class);
|
||||||
return Optional.of(DataSnapshot.deserialize(plugin, dataByteArray, versionUuid, timestamp));
|
final byte[] dataByteArray = bin.getData();
|
||||||
|
return Optional.of(DataSnapshot.deserialize(plugin, dataByteArray, versionUuid, timestamp));
|
||||||
|
}
|
||||||
|
return Optional.empty();
|
||||||
|
} catch (MongoException e) {
|
||||||
|
plugin.log(Level.SEVERE, "Failed to get latest snapshot from the database", e);
|
||||||
|
return Optional.empty();
|
||||||
}
|
}
|
||||||
return Optional.empty();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -189,18 +219,23 @@ public class MongoDbDatabase extends Database {
|
|||||||
@Override
|
@Override
|
||||||
@NotNull
|
@NotNull
|
||||||
public List<DataSnapshot.Packed> getAllSnapshots(@NotNull User user) {
|
public List<DataSnapshot.Packed> getAllSnapshots(@NotNull User user) {
|
||||||
final List<DataSnapshot.Packed> retrievedData = Lists.newArrayList();
|
try {
|
||||||
Document filter = new Document("player_uuid", user.getUuid().toString());
|
final List<DataSnapshot.Packed> retrievedData = Lists.newArrayList();
|
||||||
Document sort = new Document("timestamp", -1); // -1 = Descending
|
Document filter = new Document("player_uuid", user.getUuid().toString());
|
||||||
FindIterable<Document> iterable = mongoCollectionHelper.getCollection(userDataTable).find(filter).sort(sort);
|
Document sort = new Document("timestamp", -1); // -1 = Descending
|
||||||
for (Document doc : iterable) {
|
FindIterable<Document> iterable = mongoCollectionHelper.getCollection(userDataTable).find(filter).sort(sort);
|
||||||
final UUID versionUuid = UUID.fromString(doc.getString("version_uuid"));
|
for (Document doc : iterable) {
|
||||||
final OffsetDateTime timestamp = OffsetDateTime.ofInstant(Instant.ofEpochMilli((long) doc.get("timestamp")), TimeZone.getDefault().toZoneId());
|
final UUID versionUuid = UUID.fromString(doc.getString("version_uuid"));
|
||||||
final Binary bin = doc.get("data", Binary.class);
|
final OffsetDateTime timestamp = OffsetDateTime.ofInstant(Instant.ofEpochMilli((long) doc.get("timestamp")), TimeZone.getDefault().toZoneId());
|
||||||
final byte[] dataByteArray = bin.getData();
|
final Binary bin = doc.get("data", Binary.class);
|
||||||
retrievedData.add(DataSnapshot.deserialize(plugin, dataByteArray, versionUuid, timestamp));
|
final byte[] dataByteArray = bin.getData();
|
||||||
|
retrievedData.add(DataSnapshot.deserialize(plugin, dataByteArray, versionUuid, timestamp));
|
||||||
|
}
|
||||||
|
return retrievedData;
|
||||||
|
} catch (MongoException e) {
|
||||||
|
plugin.log(Level.SEVERE, "Failed to get all snapshots from the database", e);
|
||||||
|
return Lists.newArrayList();
|
||||||
}
|
}
|
||||||
return retrievedData;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -213,17 +248,22 @@ public class MongoDbDatabase extends Database {
|
|||||||
@Blocking
|
@Blocking
|
||||||
@Override
|
@Override
|
||||||
public Optional<DataSnapshot.Packed> getSnapshot(@NotNull User user, @NotNull UUID versionUuid) {
|
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());
|
try {
|
||||||
Document sort = new Document("timestamp", -1); // -1 = Descending
|
Document filter = new Document("player_uuid", user.getUuid().toString()).append("version_uuid", versionUuid.toString());
|
||||||
FindIterable<Document> iterable = mongoCollectionHelper.getCollection(userDataTable).find(filter).sort(sort);
|
Document sort = new Document("timestamp", -1); // -1 = Descending
|
||||||
Document doc = iterable.first();
|
FindIterable<Document> iterable = mongoCollectionHelper.getCollection(userDataTable).find(filter).sort(sort);
|
||||||
if (doc != null) {
|
Document doc = iterable.first();
|
||||||
final OffsetDateTime timestamp = OffsetDateTime.ofInstant(Instant.ofEpochMilli((long) doc.get("timestamp")), TimeZone.getDefault().toZoneId());
|
if (doc != null) {
|
||||||
final Binary bin = doc.get("data", Binary.class);
|
final OffsetDateTime timestamp = OffsetDateTime.ofInstant(Instant.ofEpochMilli((long) doc.get("timestamp")), TimeZone.getDefault().toZoneId());
|
||||||
final byte[] dataByteArray = bin.getData();
|
final Binary bin = doc.get("data", Binary.class);
|
||||||
return Optional.of(DataSnapshot.deserialize(plugin, dataByteArray, versionUuid, timestamp));
|
final byte[] dataByteArray = bin.getData();
|
||||||
|
return Optional.of(DataSnapshot.deserialize(plugin, dataByteArray, versionUuid, timestamp));
|
||||||
|
}
|
||||||
|
return Optional.empty();
|
||||||
|
} catch (MongoException e) {
|
||||||
|
plugin.log(Level.SEVERE, "Failed to get snapshot from the database", e);
|
||||||
|
return Optional.empty();
|
||||||
}
|
}
|
||||||
return Optional.empty();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -253,7 +293,7 @@ public class MongoDbDatabase extends Database {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (MongoException e) {
|
} catch (MongoException e) {
|
||||||
plugin.log(Level.SEVERE, "Failed to prune user data from the database", e);
|
plugin.log(Level.SEVERE, "Failed to rotate snapshots", e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -308,7 +348,7 @@ public class MongoDbDatabase extends Database {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (MongoException e) {
|
} catch (MongoException e) {
|
||||||
plugin.log(Level.SEVERE, "Failed to prune user data from the database", e);
|
plugin.log(Level.SEVERE, "Failed to rotate latest snapshot from the database", e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -352,7 +392,7 @@ public class MongoDbDatabase extends Database {
|
|||||||
);
|
);
|
||||||
mongoCollectionHelper.updateDocument(userDataTable, doc, updates);
|
mongoCollectionHelper.updateDocument(userDataTable, doc, updates);
|
||||||
} catch (MongoException e) {
|
} catch (MongoException e) {
|
||||||
plugin.log(Level.SEVERE, "Failed to pin user data in the database", e);
|
plugin.log(Level.SEVERE, "Failed to update snapshot in the database", e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,430 @@
|
|||||||
|
/*
|
||||||
|
* 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.zaxxer.hikari.HikariDataSource;
|
||||||
|
import net.william278.husksync.HuskSync;
|
||||||
|
import net.william278.husksync.adapter.DataAdapter;
|
||||||
|
import net.william278.husksync.data.DataSnapshot;
|
||||||
|
import net.william278.husksync.user.User;
|
||||||
|
import org.jetbrains.annotations.Blocking;
|
||||||
|
import org.jetbrains.annotations.NotNull;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.sql.*;
|
||||||
|
import java.time.OffsetDateTime;
|
||||||
|
import java.util.*;
|
||||||
|
import java.util.logging.Level;
|
||||||
|
|
||||||
|
import static net.william278.husksync.config.Settings.DatabaseSettings;
|
||||||
|
|
||||||
|
public class PostgresDatabase extends Database {
|
||||||
|
|
||||||
|
private static final String DATA_POOL_NAME = "HuskSyncHikariPool";
|
||||||
|
private final String flavor;
|
||||||
|
private final String driverClass;
|
||||||
|
private HikariDataSource dataSource;
|
||||||
|
|
||||||
|
public PostgresDatabase(@NotNull HuskSync plugin) {
|
||||||
|
super(plugin);
|
||||||
|
|
||||||
|
final Type type = plugin.getSettings().getDatabase().getType();
|
||||||
|
this.flavor = type.getProtocol();
|
||||||
|
this.driverClass = "org.postgresql.Driver";
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fetch the auto-closeable connection from the hikariDataSource
|
||||||
|
*
|
||||||
|
* @return The {@link Connection} to the MySQL database
|
||||||
|
* @throws SQLException if the connection fails for some reason
|
||||||
|
*/
|
||||||
|
@Blocking
|
||||||
|
@NotNull
|
||||||
|
private Connection getConnection() throws SQLException {
|
||||||
|
if (dataSource == null) {
|
||||||
|
throw new IllegalStateException("The database has not been initialized");
|
||||||
|
}
|
||||||
|
return dataSource.getConnection();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Blocking
|
||||||
|
@Override
|
||||||
|
public void initialize() throws IllegalStateException {
|
||||||
|
// Initialize the Hikari pooled connection
|
||||||
|
final DatabaseSettings.DatabaseCredentials credentials = plugin.getSettings().getDatabase().getCredentials();
|
||||||
|
dataSource = new HikariDataSource();
|
||||||
|
dataSource.setDriverClassName(driverClass);
|
||||||
|
dataSource.setJdbcUrl(String.format("jdbc:%s://%s:%s/%s%s",
|
||||||
|
flavor,
|
||||||
|
credentials.getHost(),
|
||||||
|
credentials.getPort(),
|
||||||
|
credentials.getDatabase(),
|
||||||
|
credentials.getParameters()
|
||||||
|
));
|
||||||
|
|
||||||
|
// Authenticate with the database
|
||||||
|
dataSource.setUsername(credentials.getUsername());
|
||||||
|
dataSource.setPassword(credentials.getPassword());
|
||||||
|
|
||||||
|
// Set connection pool options
|
||||||
|
final DatabaseSettings.PoolSettings pool = plugin.getSettings().getDatabase().getConnectionPool();
|
||||||
|
dataSource.setMaximumPoolSize(pool.getMaximumPoolSize());
|
||||||
|
dataSource.setMinimumIdle(pool.getMinimumIdle());
|
||||||
|
dataSource.setMaxLifetime(pool.getMaximumLifetime());
|
||||||
|
dataSource.setKeepaliveTime(pool.getKeepaliveTime());
|
||||||
|
dataSource.setConnectionTimeout(pool.getConnectionTimeout());
|
||||||
|
dataSource.setPoolName(DATA_POOL_NAME);
|
||||||
|
|
||||||
|
// Set additional connection pool properties
|
||||||
|
final Properties properties = new Properties();
|
||||||
|
properties.putAll(
|
||||||
|
Map.of("cachePrepStmts", "true",
|
||||||
|
"prepStmtCacheSize", "250",
|
||||||
|
"prepStmtCacheSqlLimit", "2048",
|
||||||
|
"useServerPrepStmts", "true",
|
||||||
|
"useLocalSessionState", "true",
|
||||||
|
"useLocalTransactionState", "true"
|
||||||
|
));
|
||||||
|
properties.putAll(
|
||||||
|
Map.of(
|
||||||
|
"rewriteBatchedStatements", "true",
|
||||||
|
"cacheResultSetMetadata", "true",
|
||||||
|
"cacheServerConfiguration", "true",
|
||||||
|
"elideSetAutoCommits", "true",
|
||||||
|
"maintainTimeStats", "false")
|
||||||
|
);
|
||||||
|
dataSource.setDataSourceProperties(properties);
|
||||||
|
|
||||||
|
// Prepare database schema; make tables if they don't exist
|
||||||
|
try (Connection connection = dataSource.getConnection()) {
|
||||||
|
final String[] databaseSchema = getSchemaStatements(String.format("database/%s_schema.sql", flavor));
|
||||||
|
try (Statement statement = connection.createStatement()) {
|
||||||
|
for (String tableCreationStatement : databaseSchema) {
|
||||||
|
statement.execute(tableCreationStatement);
|
||||||
|
}
|
||||||
|
} catch (SQLException e) {
|
||||||
|
throw new IllegalStateException("Failed to create database tables. Please ensure you are running PostgreSQL " +
|
||||||
|
"and that your connecting user account has privileges to create tables.", e);
|
||||||
|
}
|
||||||
|
} catch (SQLException | IOException e) {
|
||||||
|
throw new IllegalStateException("Failed to establish a connection to the PostgreSQL database. " +
|
||||||
|
"Please check the supplied database credentials in the config file", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@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 (Connection connection = getConnection()) {
|
||||||
|
try (PreparedStatement statement = connection.prepareStatement(formatStatementTables("""
|
||||||
|
UPDATE "%users_table%"
|
||||||
|
SET "username"=?
|
||||||
|
WHERE "uuid"=?"""))) {
|
||||||
|
|
||||||
|
statement.setString(1, user.getUsername());
|
||||||
|
statement.setObject(2, existingUser.getUuid());
|
||||||
|
statement.executeUpdate();
|
||||||
|
}
|
||||||
|
plugin.log(Level.INFO, "Updated " + user.getUsername() + "'s name in the database (" + existingUser.getUsername() + " -> " + user.getUsername() + ")");
|
||||||
|
} catch (SQLException e) {
|
||||||
|
plugin.log(Level.SEVERE, "Failed to update a user's name on the database", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
() -> {
|
||||||
|
// Insert new player data into the database
|
||||||
|
try (Connection connection = getConnection()) {
|
||||||
|
try (PreparedStatement statement = connection.prepareStatement(formatStatementTables("""
|
||||||
|
INSERT INTO "%users_table%" ("uuid","username")
|
||||||
|
VALUES (?,?);"""))) {
|
||||||
|
|
||||||
|
statement.setObject(1, user.getUuid());
|
||||||
|
statement.setString(2, user.getUsername());
|
||||||
|
statement.executeUpdate();
|
||||||
|
}
|
||||||
|
} catch (SQLException e) {
|
||||||
|
plugin.log(Level.SEVERE, "Failed to insert a user into the database", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Blocking
|
||||||
|
@Override
|
||||||
|
public Optional<User> getUser(@NotNull UUID uuid) {
|
||||||
|
try (Connection connection = getConnection()) {
|
||||||
|
try (PreparedStatement statement = connection.prepareStatement(formatStatementTables("""
|
||||||
|
SELECT "uuid", "username"
|
||||||
|
FROM "%users_table%"
|
||||||
|
WHERE "uuid"=?"""))) {
|
||||||
|
|
||||||
|
statement.setObject(1, uuid);
|
||||||
|
|
||||||
|
final ResultSet resultSet = statement.executeQuery();
|
||||||
|
if (resultSet.next()) {
|
||||||
|
return Optional.of(new User((UUID) resultSet.getObject("uuid"),
|
||||||
|
resultSet.getString("username")));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (SQLException e) {
|
||||||
|
plugin.log(Level.SEVERE, "Failed to fetch a user from uuid from the database", e);
|
||||||
|
}
|
||||||
|
return Optional.empty();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Blocking
|
||||||
|
@Override
|
||||||
|
public Optional<User> getUserByName(@NotNull String username) {
|
||||||
|
try (Connection connection = getConnection()) {
|
||||||
|
try (PreparedStatement statement = connection.prepareStatement(formatStatementTables("""
|
||||||
|
SELECT "uuid", "username"
|
||||||
|
FROM "%users_table%"
|
||||||
|
WHERE "username"=?"""))) {
|
||||||
|
statement.setString(1, username);
|
||||||
|
|
||||||
|
final ResultSet resultSet = statement.executeQuery();
|
||||||
|
if (resultSet.next()) {
|
||||||
|
return Optional.of(new User((UUID) resultSet.getObject("uuid"),
|
||||||
|
resultSet.getString("username")));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (SQLException e) {
|
||||||
|
plugin.log(Level.SEVERE, "Failed to fetch a user by name from the database", e);
|
||||||
|
}
|
||||||
|
return Optional.empty();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Blocking
|
||||||
|
@Override
|
||||||
|
public Optional<DataSnapshot.Packed> getLatestSnapshot(@NotNull User user) {
|
||||||
|
try (Connection connection = getConnection()) {
|
||||||
|
try (PreparedStatement statement = connection.prepareStatement(formatStatementTables("""
|
||||||
|
SELECT "version_uuid", "timestamp", "data"
|
||||||
|
FROM "%user_data_table%"
|
||||||
|
WHERE "player_uuid"=?
|
||||||
|
ORDER BY "timestamp" DESC
|
||||||
|
LIMIT 1;"""))) {
|
||||||
|
statement.setObject(1, user.getUuid());
|
||||||
|
final ResultSet resultSet = statement.executeQuery();
|
||||||
|
if (resultSet.next()) {
|
||||||
|
final UUID versionUuid = (UUID) resultSet.getObject("version_uuid");
|
||||||
|
final OffsetDateTime timestamp = OffsetDateTime.ofInstant(
|
||||||
|
resultSet.getTimestamp("timestamp").toInstant(), TimeZone.getDefault().toZoneId()
|
||||||
|
);
|
||||||
|
final byte[] dataByteArray = resultSet.getBytes("data");
|
||||||
|
return Optional.of(DataSnapshot.deserialize(plugin, dataByteArray, versionUuid, timestamp));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (SQLException | DataAdapter.AdaptionException e) {
|
||||||
|
plugin.log(Level.SEVERE, "Failed to fetch a user's current user data from the database", e);
|
||||||
|
}
|
||||||
|
return Optional.empty();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Blocking
|
||||||
|
@Override
|
||||||
|
@NotNull
|
||||||
|
public List<DataSnapshot.Packed> getAllSnapshots(@NotNull User user) {
|
||||||
|
final List<DataSnapshot.Packed> retrievedData = Lists.newArrayList();
|
||||||
|
try (Connection connection = getConnection()) {
|
||||||
|
try (PreparedStatement statement = connection.prepareStatement(formatStatementTables("""
|
||||||
|
SELECT "version_uuid", "timestamp", "data"
|
||||||
|
FROM "%user_data_table%"
|
||||||
|
WHERE "player_uuid"=?
|
||||||
|
ORDER BY "timestamp" DESC;"""))) {
|
||||||
|
statement.setObject(1, user.getUuid());
|
||||||
|
final ResultSet resultSet = statement.executeQuery();
|
||||||
|
while (resultSet.next()) {
|
||||||
|
final UUID versionUuid = (UUID) resultSet.getObject("version_uuid");
|
||||||
|
final OffsetDateTime timestamp = OffsetDateTime.ofInstant(
|
||||||
|
resultSet.getTimestamp("timestamp").toInstant(), TimeZone.getDefault().toZoneId()
|
||||||
|
);
|
||||||
|
final byte[] dataByteArray = resultSet.getBytes("data");
|
||||||
|
retrievedData.add(DataSnapshot.deserialize(plugin, dataByteArray, versionUuid, timestamp));
|
||||||
|
}
|
||||||
|
return retrievedData;
|
||||||
|
}
|
||||||
|
} catch (SQLException | DataAdapter.AdaptionException e) {
|
||||||
|
plugin.log(Level.SEVERE, "Failed to fetch a user's current user data from the database", e);
|
||||||
|
}
|
||||||
|
return retrievedData;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Blocking
|
||||||
|
@Override
|
||||||
|
public Optional<DataSnapshot.Packed> getSnapshot(@NotNull User user, @NotNull UUID versionUuid) {
|
||||||
|
try (Connection connection = getConnection()) {
|
||||||
|
try (PreparedStatement statement = connection.prepareStatement(formatStatementTables("""
|
||||||
|
SELECT "version_uuid", "timestamp", "data"
|
||||||
|
FROM "%user_data_table%"
|
||||||
|
WHERE "player_uuid"=? AND "version_uuid"=?
|
||||||
|
ORDER BY "timestamp" DESC
|
||||||
|
LIMIT 1;"""))) {
|
||||||
|
statement.setObject(1, user.getUuid());
|
||||||
|
statement.setObject(2, versionUuid);
|
||||||
|
final ResultSet resultSet = statement.executeQuery();
|
||||||
|
if (resultSet.next()) {
|
||||||
|
final OffsetDateTime timestamp = OffsetDateTime.ofInstant(
|
||||||
|
resultSet.getTimestamp("timestamp").toInstant(), TimeZone.getDefault().toZoneId()
|
||||||
|
);
|
||||||
|
final byte[] dataByteArray = resultSet.getBytes("data");
|
||||||
|
return Optional.of(DataSnapshot.deserialize(plugin, dataByteArray, versionUuid, timestamp));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (SQLException | DataAdapter.AdaptionException e) {
|
||||||
|
plugin.log(Level.SEVERE, "Failed to fetch specific user data by UUID from the database", e);
|
||||||
|
}
|
||||||
|
return Optional.empty();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Blocking
|
||||||
|
@Override
|
||||||
|
protected void rotateSnapshots(@NotNull User user) {
|
||||||
|
final List<DataSnapshot.Packed> unpinnedUserData = getAllSnapshots(user).stream()
|
||||||
|
.filter(dataSnapshot -> !dataSnapshot.isPinned()).toList();
|
||||||
|
final int maxSnapshots = plugin.getSettings().getSynchronization().getMaxUserDataSnapshots();
|
||||||
|
if (unpinnedUserData.size() > maxSnapshots) {
|
||||||
|
try (Connection connection = getConnection()) {
|
||||||
|
try (PreparedStatement statement = connection.prepareStatement(formatStatementTables("""
|
||||||
|
DELETE FROM "%user_data_table%"
|
||||||
|
WHERE "player_uuid"=?
|
||||||
|
AND "pinned" = FALSE
|
||||||
|
ORDER BY "timestamp" ASC
|
||||||
|
LIMIT %entry_count%;""".replace("%entry_count%",
|
||||||
|
Integer.toString(unpinnedUserData.size() - maxSnapshots))))) {
|
||||||
|
statement.setObject(1, user.getUuid());
|
||||||
|
statement.executeUpdate();
|
||||||
|
}
|
||||||
|
} catch (SQLException e) {
|
||||||
|
plugin.log(Level.SEVERE, "Failed to prune user data from the database", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Blocking
|
||||||
|
@Override
|
||||||
|
public boolean deleteSnapshot(@NotNull User user, @NotNull UUID versionUuid) {
|
||||||
|
try (Connection connection = getConnection()) {
|
||||||
|
try (PreparedStatement statement = connection.prepareStatement(formatStatementTables("""
|
||||||
|
DELETE FROM "%user_data_table%"
|
||||||
|
WHERE "player_uuid"=? AND "version_uuid"=?
|
||||||
|
LIMIT 1;"""))) {
|
||||||
|
statement.setObject(1, user.getUuid());
|
||||||
|
statement.setString(2, versionUuid.toString());
|
||||||
|
return statement.executeUpdate() > 0;
|
||||||
|
}
|
||||||
|
} catch (SQLException e) {
|
||||||
|
plugin.log(Level.SEVERE, "Failed to delete specific user data from the database", e);
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Blocking
|
||||||
|
@Override
|
||||||
|
protected void rotateLatestSnapshot(@NotNull User user, @NotNull OffsetDateTime within) {
|
||||||
|
try (Connection connection = getConnection()) {
|
||||||
|
try (PreparedStatement statement = connection.prepareStatement(formatStatementTables("""
|
||||||
|
DELETE FROM "%user_data_table%"
|
||||||
|
WHERE "player_uuid"=? AND "timestamp" = (
|
||||||
|
SELECT "timestamp"
|
||||||
|
FROM "%user_data_table%"
|
||||||
|
WHERE "player_uuid"=? AND "timestamp" > ? AND "pinned" = FALSE
|
||||||
|
ORDER BY "timestamp" ASC
|
||||||
|
LIMIT 1
|
||||||
|
);"""))) {
|
||||||
|
statement.setObject(1, user.getUuid());
|
||||||
|
statement.setObject(2, user.getUuid());
|
||||||
|
statement.setTimestamp(3, Timestamp.from(within.toInstant()));
|
||||||
|
statement.executeUpdate();
|
||||||
|
}
|
||||||
|
} catch (SQLException e) {
|
||||||
|
plugin.log(Level.SEVERE, "Failed to delete a user's data from the database", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Blocking
|
||||||
|
@Override
|
||||||
|
protected void createSnapshot(@NotNull User user, @NotNull DataSnapshot.Packed data) {
|
||||||
|
try (Connection connection = getConnection()) {
|
||||||
|
try (PreparedStatement statement = connection.prepareStatement(formatStatementTables("""
|
||||||
|
INSERT INTO "%user_data_table%"
|
||||||
|
("player_uuid","version_uuid","timestamp","save_cause","pinned","data")
|
||||||
|
VALUES (?,?,?,?,?,?);"""))) {
|
||||||
|
statement.setObject(1, user.getUuid());
|
||||||
|
statement.setObject(2, data.getId());
|
||||||
|
statement.setTimestamp(3, Timestamp.from(data.getTimestamp().toInstant()));
|
||||||
|
statement.setString(4, data.getSaveCause().name());
|
||||||
|
statement.setBoolean(5, data.isPinned());
|
||||||
|
statement.setBytes(6, data.asBytes(plugin));
|
||||||
|
statement.executeUpdate();
|
||||||
|
}
|
||||||
|
} catch (SQLException | DataAdapter.AdaptionException e) {
|
||||||
|
plugin.log(Level.SEVERE, "Failed to set user data in the database", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Blocking
|
||||||
|
@Override
|
||||||
|
public void updateSnapshot(@NotNull User user, @NotNull DataSnapshot.Packed data) {
|
||||||
|
try (Connection connection = getConnection()) {
|
||||||
|
try (PreparedStatement statement = connection.prepareStatement(formatStatementTables("""
|
||||||
|
UPDATE "%user_data_table%"
|
||||||
|
SET "save_cause"=?,"pinned"=?,"data"=?
|
||||||
|
WHERE "player_uuid"=? AND "version_uuid"=?
|
||||||
|
LIMIT 1;"""))) {
|
||||||
|
statement.setString(1, data.getSaveCause().name());
|
||||||
|
statement.setBoolean(2, data.isPinned());
|
||||||
|
statement.setBytes(3, data.asBytes(plugin));
|
||||||
|
statement.setObject(4, user.getUuid());
|
||||||
|
statement.setObject(5, data.getId());
|
||||||
|
statement.executeUpdate();
|
||||||
|
}
|
||||||
|
} catch (SQLException e) {
|
||||||
|
plugin.log(Level.SEVERE, "Failed to pin user data in the database", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void wipeDatabase() {
|
||||||
|
try (Connection connection = getConnection()) {
|
||||||
|
try (Statement statement = connection.createStatement()) {
|
||||||
|
statement.executeUpdate(formatStatementTables("DELETE FROM \"%user_data_table%\";"));
|
||||||
|
}
|
||||||
|
} catch (SQLException e) {
|
||||||
|
plugin.log(Level.SEVERE, "Failed to wipe the database", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void terminate() {
|
||||||
|
if (dataSource != null) {
|
||||||
|
if (!dataSource.isClosed()) {
|
||||||
|
dataSource.close();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -19,17 +19,15 @@
|
|||||||
|
|
||||||
package net.william278.husksync.database.mongo;
|
package net.william278.husksync.database.mongo;
|
||||||
|
|
||||||
|
import com.mongodb.ConnectionString;
|
||||||
import com.mongodb.MongoClientSettings;
|
import com.mongodb.MongoClientSettings;
|
||||||
import com.mongodb.MongoCredential;
|
|
||||||
import com.mongodb.ServerAddress;
|
|
||||||
import com.mongodb.client.MongoClient;
|
import com.mongodb.client.MongoClient;
|
||||||
import com.mongodb.client.MongoClients;
|
import com.mongodb.client.MongoClients;
|
||||||
import com.mongodb.client.MongoDatabase;
|
import com.mongodb.client.MongoDatabase;
|
||||||
import lombok.Getter;
|
import lombok.Getter;
|
||||||
|
import org.bson.UuidRepresentation;
|
||||||
import org.jetbrains.annotations.NotNull;
|
import org.jetbrains.annotations.NotNull;
|
||||||
|
|
||||||
import java.util.Collections;
|
|
||||||
|
|
||||||
@Getter
|
@Getter
|
||||||
public class MongoConnectionHandler {
|
public class MongoConnectionHandler {
|
||||||
private final MongoClient mongoClient;
|
private final MongoClient mongoClient;
|
||||||
@@ -37,24 +35,21 @@ public class MongoConnectionHandler {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Initiate a connection to a Mongo Server
|
* Initiate a connection to a Mongo Server
|
||||||
* @param host The IP/Host Name of the Mongo Server
|
* @param uri The connection string
|
||||||
* @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) {
|
public MongoConnectionHandler(@NotNull ConnectionString uri, @NotNull String databaseName) {
|
||||||
final ServerAddress serverAddress = new ServerAddress(host, port);
|
try {
|
||||||
final MongoCredential credential = MongoCredential.createCredential(username, authDb, password.toCharArray());
|
final MongoClientSettings settings = MongoClientSettings.builder()
|
||||||
|
.applyConnectionString(uri)
|
||||||
|
.uuidRepresentation(UuidRepresentation.STANDARD)
|
||||||
|
.build();
|
||||||
|
|
||||||
final MongoClientSettings settings = MongoClientSettings.builder()
|
this.mongoClient = MongoClients.create(settings);
|
||||||
.credential(credential)
|
this.database = mongoClient.getDatabase(databaseName);
|
||||||
.applyToClusterSettings(builder -> builder.hosts(Collections.singletonList(serverAddress)))
|
} catch (Exception e) {
|
||||||
.build();
|
throw new IllegalStateException("Failed to establish a connection to the MongoDB database. " +
|
||||||
|
"Please check the supplied database credentials in the config file", e);
|
||||||
this.mongoClient = MongoClients.create(settings);
|
}
|
||||||
this.database = mongoClient.getDatabase(databaseName);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -184,7 +184,7 @@ public class PlanHook {
|
|||||||
public String getHealth(@NotNull UUID uuid) {
|
public String getHealth(@NotNull UUID uuid) {
|
||||||
return getLatestSnapshot(uuid)
|
return getLatestSnapshot(uuid)
|
||||||
.flatMap(DataHolder::getHealth)
|
.flatMap(DataHolder::getHealth)
|
||||||
.map(health -> String.format("%s / %s", health.getHealth(), health.getMaxHealth()))
|
.map(health -> String.format("%s", health.getHealth()))
|
||||||
.orElse(UNKNOWN_STRING);
|
.orElse(UNKNOWN_STRING);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -28,7 +28,6 @@ import org.jetbrains.annotations.NotNull;
|
|||||||
import java.util.Arrays;
|
import java.util.Arrays;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.UUID;
|
|
||||||
|
|
||||||
import static net.william278.husksync.config.Settings.SynchronizationSettings.SaveOnDeathSettings;
|
import static net.william278.husksync.config.Settings.SynchronizationSettings.SaveOnDeathSettings;
|
||||||
|
|
||||||
@@ -104,20 +103,11 @@ public abstract class EventListener {
|
|||||||
plugin.getDataSyncer().saveData(user, snapshot);
|
plugin.getDataSyncer().saveData(user, snapshot);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Determine whether a player event should be canceled
|
|
||||||
*
|
|
||||||
* @param userUuid The UUID of the user to check
|
|
||||||
* @return Whether the event should be canceled
|
|
||||||
*/
|
|
||||||
protected final boolean cancelPlayerEvent(@NotNull UUID userUuid) {
|
|
||||||
return plugin.isDisabling() || plugin.isLocked(userUuid);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Handle the plugin disabling
|
* Handle the plugin disabling
|
||||||
*/
|
*/
|
||||||
public final void handlePluginDisable() {
|
public void handlePluginDisable() {
|
||||||
// Save for all online players
|
// Save for all online players
|
||||||
plugin.getOnlineUsers().stream()
|
plugin.getOnlineUsers().stream()
|
||||||
.filter(user -> !plugin.isLocked(user.getUuid()) && !user.isNpc())
|
.filter(user -> !plugin.isLocked(user.getUuid()) && !user.isNpc())
|
||||||
|
|||||||
@@ -0,0 +1,69 @@
|
|||||||
|
/*
|
||||||
|
* 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.listener;
|
||||||
|
|
||||||
|
import net.william278.husksync.HuskSync;
|
||||||
|
import org.jetbrains.annotations.ApiStatus;
|
||||||
|
import org.jetbrains.annotations.NotNull;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Locale;
|
||||||
|
import java.util.UUID;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Interface for doing stuff with locked users or when the plugin is disabled
|
||||||
|
*/
|
||||||
|
public interface LockedHandler {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get if a command should be disabled while the user is locked
|
||||||
|
*/
|
||||||
|
default boolean isCommandDisabled(@NotNull String label) {
|
||||||
|
final List<String> blocked = getPlugin().getSettings().getSynchronization().getBlacklistedCommandsWhileLocked();
|
||||||
|
return blocked.contains("*") || blocked.contains(label.toLowerCase(Locale.ENGLISH));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Determine whether a player event should be canceled
|
||||||
|
*
|
||||||
|
* @param userUuid The UUID of the user to check
|
||||||
|
* @return Whether the event should be canceled
|
||||||
|
*/
|
||||||
|
default boolean cancelPlayerEvent(@NotNull UUID userUuid) {
|
||||||
|
return getPlugin().isDisabling() || getPlugin().isLocked(userUuid);
|
||||||
|
}
|
||||||
|
|
||||||
|
@NotNull
|
||||||
|
@ApiStatus.Internal
|
||||||
|
HuskSync getPlugin();
|
||||||
|
|
||||||
|
default void onLoad() {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
default void onEnable() {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
default void onDisable() {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -158,10 +158,15 @@ public class RedisManager extends JedisPubSub {
|
|||||||
final RedisMessage redisMessage = RedisMessage.fromJson(plugin, message);
|
final RedisMessage redisMessage = RedisMessage.fromJson(plugin, message);
|
||||||
switch (messageType) {
|
switch (messageType) {
|
||||||
case UPDATE_USER_DATA -> plugin.getOnlineUser(redisMessage.getTargetUuid()).ifPresent(
|
case UPDATE_USER_DATA -> plugin.getOnlineUser(redisMessage.getTargetUuid()).ifPresent(
|
||||||
user -> user.applySnapshot(
|
user -> {
|
||||||
DataSnapshot.deserialize(plugin, redisMessage.getPayload()),
|
try {
|
||||||
DataSnapshot.UpdateCause.UPDATED
|
final DataSnapshot.Packed data = DataSnapshot.deserialize(plugin, redisMessage.getPayload());
|
||||||
)
|
user.applySnapshot(data, DataSnapshot.UpdateCause.UPDATED);
|
||||||
|
} catch (Throwable e) {
|
||||||
|
plugin.log(Level.SEVERE, "An exception occurred updating user data from Redis", e);
|
||||||
|
user.completeSync(false, DataSnapshot.UpdateCause.UPDATED, plugin);
|
||||||
|
}
|
||||||
|
}
|
||||||
);
|
);
|
||||||
case REQUEST_USER_DATA -> plugin.getOnlineUser(redisMessage.getTargetUuid()).ifPresent(
|
case REQUEST_USER_DATA -> plugin.getOnlineUser(redisMessage.getTargetUuid()).ifPresent(
|
||||||
user -> RedisMessage.create(
|
user -> RedisMessage.create(
|
||||||
@@ -174,7 +179,13 @@ public class RedisManager extends JedisPubSub {
|
|||||||
redisMessage.getTargetUuid()
|
redisMessage.getTargetUuid()
|
||||||
);
|
);
|
||||||
if (future != null) {
|
if (future != null) {
|
||||||
future.complete(Optional.of(DataSnapshot.deserialize(plugin, redisMessage.getPayload())));
|
try {
|
||||||
|
final DataSnapshot.Packed data = DataSnapshot.deserialize(plugin, redisMessage.getPayload());
|
||||||
|
future.complete(Optional.of(data));
|
||||||
|
} catch (Throwable e) {
|
||||||
|
plugin.log(Level.SEVERE, "An exception occurred returning user data from Redis", e);
|
||||||
|
future.complete(Optional.empty());
|
||||||
|
}
|
||||||
pendingRequests.remove(redisMessage.getTargetUuid());
|
pendingRequests.remove(redisMessage.getTargetUuid());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -38,6 +38,7 @@ import java.util.concurrent.atomic.AtomicReference;
|
|||||||
import java.util.function.BiConsumer;
|
import java.util.function.BiConsumer;
|
||||||
import java.util.function.Function;
|
import java.util.function.Function;
|
||||||
import java.util.function.Supplier;
|
import java.util.function.Supplier;
|
||||||
|
import java.util.logging.Level;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Handles the synchronization of data when a player changes servers or logs in
|
* Handles the synchronization of data when a player changes servers or logs in
|
||||||
@@ -100,16 +101,16 @@ public abstract class DataSyncer {
|
|||||||
* @param user the user to save the data for
|
* @param user the user to save the data for
|
||||||
* @param data the data to save
|
* @param data the data to save
|
||||||
* @param after a consumer to run after data has been saved. Will be run async (off the main thread).
|
* @param after a consumer to run after data has been saved. Will be run async (off the main thread).
|
||||||
* @apiNote Data will not be saved if the {@link net.william278.husksync.event.DataSaveEvent} is cancelled.
|
* @apiNote Data will not be saved if the {@link net.william278.husksync.event.DataSaveEvent} is canceled.
|
||||||
* Note that this method can also edit the data before saving it.
|
* 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
|
* @implNote Note that the {@link net.william278.husksync.event.DataSaveEvent} will <b>not</b> be fired if
|
||||||
* save cause is {@link DataSnapshot.SaveCause#SERVER_SHUTDOWN}.
|
* {@link DataSnapshot.SaveCause#fireDataSaveEvent()} is {@code false} (e.g., with the SERVER_SHUTDOWN cause).
|
||||||
* @since 3.3.2
|
* @since 3.3.2
|
||||||
*/
|
*/
|
||||||
@Blocking
|
@Blocking
|
||||||
public void saveData(@NotNull User user, @NotNull DataSnapshot.Packed data,
|
public void saveData(@NotNull User user, @NotNull DataSnapshot.Packed data,
|
||||||
@Nullable BiConsumer<User, DataSnapshot.Packed> after) {
|
@Nullable BiConsumer<User, DataSnapshot.Packed> after) {
|
||||||
if (data.getSaveCause() == DataSnapshot.SaveCause.SERVER_SHUTDOWN) {
|
if (!data.getSaveCause().fireDataSaveEvent()) {
|
||||||
addSnapshotToDatabase(user, data, after);
|
addSnapshotToDatabase(user, data, after);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -125,10 +126,10 @@ public abstract class DataSyncer {
|
|||||||
*
|
*
|
||||||
* @param user the user to save the data for
|
* @param user the user to save the data for
|
||||||
* @param data the data to save
|
* @param data the data to save
|
||||||
* @apiNote Data will not be saved if the {@link net.william278.husksync.event.DataSaveEvent} is cancelled.
|
* @apiNote Data will not be saved if the {@link net.william278.husksync.event.DataSaveEvent} is canceled.
|
||||||
* Note that this method can also edit the data before saving it.
|
* 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
|
* @implNote Note that the {@link net.william278.husksync.event.DataSaveEvent} will <b>not</b> be fired if
|
||||||
* save cause is {@link DataSnapshot.SaveCause#SERVER_SHUTDOWN}.
|
* {@link DataSnapshot.SaveCause#fireDataSaveEvent()} is {@code false} (e.g., with the SERVER_SHUTDOWN cause).
|
||||||
* @since 3.3.3
|
* @since 3.3.3
|
||||||
*/
|
*/
|
||||||
public void saveData(@NotNull User user, @NotNull DataSnapshot.Packed data) {
|
public void saveData(@NotNull User user, @NotNull DataSnapshot.Packed data) {
|
||||||
@@ -156,10 +157,15 @@ public abstract class DataSyncer {
|
|||||||
// Set a user's data from the database, or set them as a new user
|
// Set a user's data from the database, or set them as a new user
|
||||||
@ApiStatus.Internal
|
@ApiStatus.Internal
|
||||||
protected void setUserFromDatabase(@NotNull OnlineUser user) {
|
protected void setUserFromDatabase(@NotNull OnlineUser user) {
|
||||||
getDatabase().getLatestSnapshot(user).ifPresentOrElse(
|
try {
|
||||||
snapshot -> user.applySnapshot(snapshot, DataSnapshot.UpdateCause.SYNCHRONIZED),
|
getDatabase().getLatestSnapshot(user).ifPresentOrElse(
|
||||||
() -> user.completeSync(true, DataSnapshot.UpdateCause.NEW_USER, plugin)
|
snapshot -> user.applySnapshot(snapshot, DataSnapshot.UpdateCause.SYNCHRONIZED),
|
||||||
);
|
() -> user.completeSync(true, DataSnapshot.UpdateCause.NEW_USER, plugin)
|
||||||
|
);
|
||||||
|
} catch (Throwable e) {
|
||||||
|
plugin.log(Level.WARNING, "Failed to set %s's data from the database".formatted(user.getUsername()), e);
|
||||||
|
user.completeSync(false, DataSnapshot.UpdateCause.SYNCHRONIZED, plugin);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Continuously listen for data from Redis
|
// Continuously listen for data from Redis
|
||||||
|
|||||||
@@ -45,15 +45,15 @@ public class LockstepDataSyncer extends DataSyncer {
|
|||||||
@Override
|
@Override
|
||||||
public void setUserData(@NotNull OnlineUser user) {
|
public void setUserData(@NotNull OnlineUser user) {
|
||||||
this.listenForRedisData(user, () -> {
|
this.listenForRedisData(user, () -> {
|
||||||
if (getRedis().getUserCheckedOut(user).isEmpty()) {
|
if (getRedis().getUserCheckedOut(user).isPresent()) {
|
||||||
getRedis().setUserCheckedOut(user, true);
|
return false;
|
||||||
getRedis().getUserData(user).ifPresentOrElse(
|
|
||||||
data -> user.applySnapshot(data, DataSnapshot.UpdateCause.SYNCHRONIZED),
|
|
||||||
() -> this.setUserFromDatabase(user)
|
|
||||||
);
|
|
||||||
return true;
|
|
||||||
}
|
}
|
||||||
return false;
|
getRedis().setUserCheckedOut(user, true);
|
||||||
|
getRedis().getUserData(user).ifPresentOrElse(
|
||||||
|
data -> user.applySnapshot(data, DataSnapshot.UpdateCause.SYNCHRONIZED),
|
||||||
|
() -> this.setUserFromDatabase(user)
|
||||||
|
);
|
||||||
|
return true;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -20,7 +20,6 @@
|
|||||||
package net.william278.husksync.user;
|
package net.william278.husksync.user;
|
||||||
|
|
||||||
import de.themoep.minedown.adventure.MineDown;
|
import de.themoep.minedown.adventure.MineDown;
|
||||||
import de.themoep.minedown.adventure.MineDownParser;
|
|
||||||
import net.kyori.adventure.audience.Audience;
|
import net.kyori.adventure.audience.Audience;
|
||||||
import net.kyori.adventure.text.Component;
|
import net.kyori.adventure.text.Component;
|
||||||
import net.william278.husksync.HuskSync;
|
import net.william278.husksync.HuskSync;
|
||||||
@@ -71,9 +70,7 @@ public abstract class OnlineUser extends User implements CommandUser, UserDataHo
|
|||||||
* @param mineDown the parsed {@link MineDown} to send
|
* @param mineDown the parsed {@link MineDown} to send
|
||||||
*/
|
*/
|
||||||
public void sendMessage(@NotNull MineDown mineDown) {
|
public void sendMessage(@NotNull MineDown mineDown) {
|
||||||
sendMessage(mineDown
|
sendMessage(mineDown.toComponent());
|
||||||
.disable(MineDownParser.Option.SIMPLE_FORMATTING)
|
|
||||||
.replace().toComponent());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -82,9 +79,7 @@ public abstract class OnlineUser extends User implements CommandUser, UserDataHo
|
|||||||
* @param mineDown the parsed {@link MineDown} to send
|
* @param mineDown the parsed {@link MineDown} to send
|
||||||
*/
|
*/
|
||||||
public void sendActionBar(@NotNull MineDown mineDown) {
|
public void sendActionBar(@NotNull MineDown mineDown) {
|
||||||
getAudience().sendActionBar(mineDown
|
getAudience().sendActionBar(mineDown.toComponent());
|
||||||
.disable(MineDownParser.Option.SIMPLE_FORMATTING)
|
|
||||||
.replace().toComponent());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -130,7 +125,7 @@ public abstract class OnlineUser extends User implements CommandUser, UserDataHo
|
|||||||
getPlugin().fireEvent(getPlugin().getPreSyncEvent(this, snapshot), (event) -> {
|
getPlugin().fireEvent(getPlugin().getPreSyncEvent(this, snapshot), (event) -> {
|
||||||
if (!isOffline()) {
|
if (!isOffline()) {
|
||||||
getPlugin().debug(String.format("Applying snapshot (%s) to %s (cause: %s)",
|
getPlugin().debug(String.format("Applying snapshot (%s) to %s (cause: %s)",
|
||||||
snapshot.getShortId(), getUsername(), cause
|
snapshot.getShortId(), getUsername(), cause.getDisplayName()
|
||||||
));
|
));
|
||||||
UserDataHolder.super.applySnapshot(
|
UserDataHolder.super.applySnapshot(
|
||||||
event.getData(), (succeeded) -> completeSync(succeeded, cause, getPlugin())
|
event.getData(), (succeeded) -> completeSync(succeeded, cause, getPlugin())
|
||||||
|
|||||||
@@ -27,6 +27,7 @@ import net.william278.paginedown.PaginatedList;
|
|||||||
import org.jetbrains.annotations.NotNull;
|
import org.jetbrains.annotations.NotNull;
|
||||||
|
|
||||||
import java.time.format.DateTimeFormatter;
|
import java.time.format.DateTimeFormatter;
|
||||||
|
import java.time.format.FormatStyle;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.concurrent.atomic.AtomicInteger;
|
import java.util.concurrent.atomic.AtomicInteger;
|
||||||
|
|
||||||
@@ -46,18 +47,19 @@ public class DataSnapshotList {
|
|||||||
final AtomicInteger snapshotNumber = new AtomicInteger(1);
|
final AtomicInteger snapshotNumber = new AtomicInteger(1);
|
||||||
this.paginatedList = PaginatedList.of(snapshots.stream()
|
this.paginatedList = PaginatedList.of(snapshots.stream()
|
||||||
.map(snapshot -> plugin.getLocales()
|
.map(snapshot -> plugin.getLocales()
|
||||||
.getRawLocale("data_list_item",
|
.getRawLocale(!snapshot.isInvalid() ? "data_list_item" : "data_list_item_invalid",
|
||||||
getNumberIcon(snapshotNumber.getAndIncrement()),
|
getNumberIcon(snapshotNumber.getAndIncrement()),
|
||||||
dataOwner.getUsername(),
|
dataOwner.getUsername(),
|
||||||
snapshot.getId().toString(),
|
snapshot.getId().toString(),
|
||||||
snapshot.getShortId(),
|
snapshot.getShortId(),
|
||||||
snapshot.isPinned() ? "※" : " ",
|
snapshot.isPinned() ? "※" : " ",
|
||||||
snapshot.getTimestamp().format(DateTimeFormatter
|
snapshot.getTimestamp().format(DateTimeFormatter
|
||||||
.ofPattern("dd/MM/yyyy, HH:mm")),
|
.ofLocalizedDateTime(FormatStyle.MEDIUM, FormatStyle.SHORT)),
|
||||||
snapshot.getTimestamp().format(DateTimeFormatter
|
snapshot.getTimestamp().format(DateTimeFormatter
|
||||||
.ofPattern("MMM dd yyyy, HH:mm:ss.SSS")),
|
.ofLocalizedDateTime(FormatStyle.LONG, FormatStyle.MEDIUM)),
|
||||||
snapshot.getSaveCause().getLocale(plugin),
|
snapshot.getSaveCause().getLocale(plugin),
|
||||||
String.format("%.2fKiB", snapshot.getFileSize(plugin) / 1024f))
|
String.format("%.2fKiB", snapshot.getFileSize(plugin) / 1024f),
|
||||||
|
snapshot.isInvalid() ? snapshot.getInvalidReason(plugin) : "")
|
||||||
.orElse("• " + snapshot.getId())).toList(),
|
.orElse("• " + snapshot.getId())).toList(),
|
||||||
plugin.getLocales().getBaseChatList(6)
|
plugin.getLocales().getBaseChatList(6)
|
||||||
.setHeaderFormat(plugin.getLocales()
|
.setHeaderFormat(plugin.getLocales()
|
||||||
|
|||||||
@@ -28,6 +28,7 @@ import net.william278.husksync.user.User;
|
|||||||
import org.jetbrains.annotations.NotNull;
|
import org.jetbrains.annotations.NotNull;
|
||||||
|
|
||||||
import java.time.format.DateTimeFormatter;
|
import java.time.format.DateTimeFormatter;
|
||||||
|
import java.time.format.FormatStyle;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Locale;
|
import java.util.Locale;
|
||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
@@ -61,7 +62,8 @@ public class DataSnapshotOverview {
|
|||||||
dataOwner.getUsername(), dataOwner.getUuid().toString())
|
dataOwner.getUsername(), dataOwner.getUuid().toString())
|
||||||
.ifPresent(user::sendMessage);
|
.ifPresent(user::sendMessage);
|
||||||
locales.getLocale("data_manager_timestamp",
|
locales.getLocale("data_manager_timestamp",
|
||||||
snapshot.getTimestamp().format(DateTimeFormatter.ofPattern("MMM dd yyyy, HH:mm:ss.SSS")),
|
snapshot.getTimestamp().format(DateTimeFormatter
|
||||||
|
.ofLocalizedDateTime(FormatStyle.MEDIUM, FormatStyle.SHORT)),
|
||||||
snapshot.getTimestamp().toString())
|
snapshot.getTimestamp().toString())
|
||||||
.ifPresent(user::sendMessage);
|
.ifPresent(user::sendMessage);
|
||||||
if (snapshot.isPinned()) {
|
if (snapshot.isPinned()) {
|
||||||
@@ -75,16 +77,17 @@ public class DataSnapshotOverview {
|
|||||||
|
|
||||||
// User status data, if present in the snapshot
|
// User status data, if present in the snapshot
|
||||||
final Optional<Data.Health> health = snapshot.getHealth();
|
final Optional<Data.Health> health = snapshot.getHealth();
|
||||||
|
final Optional<Data.Attributes> attributes = snapshot.getAttributes();
|
||||||
final Optional<Data.Hunger> food = snapshot.getHunger();
|
final Optional<Data.Hunger> food = snapshot.getHunger();
|
||||||
final Optional<Data.Experience> experience = snapshot.getExperience();
|
final Optional<Data.Experience> exp = snapshot.getExperience();
|
||||||
final Optional<Data.GameMode> gameMode = snapshot.getGameMode();
|
final Optional<Data.GameMode> mode = snapshot.getGameMode();
|
||||||
if (health.isPresent() && food.isPresent() && experience.isPresent() && gameMode.isPresent()) {
|
if (health.isPresent() && attributes.isPresent() && food.isPresent() && exp.isPresent() && mode.isPresent()) {
|
||||||
locales.getLocale("data_manager_status",
|
locales.getLocale("data_manager_status",
|
||||||
Integer.toString((int) health.get().getHealth()),
|
Integer.toString((int) health.get().getHealth()),
|
||||||
Integer.toString((int) health.get().getMaxHealth()),
|
Integer.toString((int) attributes.get().getMaxHealth()),
|
||||||
Integer.toString(food.get().getFoodLevel()),
|
Integer.toString(food.get().getFoodLevel()),
|
||||||
Integer.toString(experience.get().getExpLevel()),
|
Integer.toString(exp.get().getExpLevel()),
|
||||||
gameMode.get().getGameMode().toLowerCase(Locale.ENGLISH))
|
mode.get().getGameMode().toLowerCase(Locale.ENGLISH))
|
||||||
.ifPresent(user::sendMessage);
|
.ifPresent(user::sendMessage);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -36,7 +36,7 @@ public abstract class LegacyConverter {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@NotNull
|
@NotNull
|
||||||
public abstract DataSnapshot.Packed convert(@NotNull byte[] data, @NotNull UUID id,
|
public abstract DataSnapshot.Packed convert(byte @NotNull [] data, @NotNull UUID id,
|
||||||
@NotNull OffsetDateTime timestamp) throws DataAdapter.AdaptionException;
|
@NotNull OffsetDateTime timestamp) throws DataAdapter.AdaptionException;
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -20,7 +20,9 @@
|
|||||||
package net.william278.husksync.util;
|
package net.william278.husksync.util;
|
||||||
|
|
||||||
import net.william278.husksync.HuskSync;
|
import net.william278.husksync.HuskSync;
|
||||||
|
import net.william278.husksync.data.UserDataHolder;
|
||||||
import org.jetbrains.annotations.NotNull;
|
import org.jetbrains.annotations.NotNull;
|
||||||
|
import org.jetbrains.annotations.Nullable;
|
||||||
|
|
||||||
import java.util.concurrent.CompletableFuture;
|
import java.util.concurrent.CompletableFuture;
|
||||||
|
|
||||||
@@ -86,7 +88,7 @@ public interface Task extends Runnable {
|
|||||||
interface Supplier {
|
interface Supplier {
|
||||||
|
|
||||||
@NotNull
|
@NotNull
|
||||||
Task.Sync getSyncTask(@NotNull Runnable runnable, long delayTicks);
|
Task.Sync getSyncTask(@NotNull Runnable runnable, @Nullable UserDataHolder user, long delayTicks);
|
||||||
|
|
||||||
@NotNull
|
@NotNull
|
||||||
Task.Async getAsyncTask(@NotNull Runnable runnable, long delayTicks);
|
Task.Async getAsyncTask(@NotNull Runnable runnable, long delayTicks);
|
||||||
@@ -95,8 +97,8 @@ public interface Task extends Runnable {
|
|||||||
Task.Repeating getRepeatingTask(@NotNull Runnable runnable, long repeatingTicks);
|
Task.Repeating getRepeatingTask(@NotNull Runnable runnable, long repeatingTicks);
|
||||||
|
|
||||||
@NotNull
|
@NotNull
|
||||||
default Task.Sync runSyncDelayed(@NotNull Runnable runnable, long delayTicks) {
|
default Task.Sync runSyncDelayed(@NotNull Runnable runnable, @Nullable UserDataHolder user, long delayTicks) {
|
||||||
final Task.Sync task = getSyncTask(runnable, delayTicks);
|
final Task.Sync task = getSyncTask(runnable, user, delayTicks);
|
||||||
task.run();
|
task.run();
|
||||||
return task;
|
return task;
|
||||||
}
|
}
|
||||||
@@ -109,7 +111,12 @@ public interface Task extends Runnable {
|
|||||||
|
|
||||||
@NotNull
|
@NotNull
|
||||||
default Task.Sync runSync(@NotNull Runnable runnable) {
|
default Task.Sync runSync(@NotNull Runnable runnable) {
|
||||||
return runSyncDelayed(runnable, 0);
|
return runSyncDelayed(runnable, null, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
@NotNull
|
||||||
|
default Task.Sync runSync(@NotNull Runnable runnable, @NotNull UserDataHolder user) {
|
||||||
|
return runSyncDelayed(runnable, user, 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
@NotNull
|
@NotNull
|
||||||
|
|||||||
22
common/src/main/resources/database/postgresql_schema.sql
Normal file
22
common/src/main/resources/database/postgresql_schema.sql
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
-- Create the users table if it does not exist
|
||||||
|
CREATE TABLE IF NOT EXISTS "%users_table%"
|
||||||
|
(
|
||||||
|
uuid uuid NOT NULL UNIQUE,
|
||||||
|
username varchar(16) NOT NULL,
|
||||||
|
|
||||||
|
PRIMARY KEY (uuid)
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Create the user data table if it does not exist
|
||||||
|
CREATE TABLE IF NOT EXISTS "%user_data_table%"
|
||||||
|
(
|
||||||
|
version_uuid uuid NOT NULL UNIQUE,
|
||||||
|
player_uuid uuid NOT NULL,
|
||||||
|
timestamp timestamp NOT NULL,
|
||||||
|
save_cause varchar(32) NOT NULL,
|
||||||
|
pinned boolean NOT NULL DEFAULT FALSE,
|
||||||
|
data bytea NOT NULL,
|
||||||
|
|
||||||
|
PRIMARY KEY (version_uuid, player_uuid),
|
||||||
|
FOREIGN KEY (player_uuid) REFERENCES "%users_table%" (uuid) ON DELETE CASCADE
|
||||||
|
);
|
||||||
@@ -21,7 +21,8 @@ locales:
|
|||||||
data_manager_system_buttons: '[System:](gray) [[⏷ File Dump…]](dark_gray show_text=&7Click to dump this raw user data snapshot to a file.\n&8Data dumps can be found in ~/plugins/HuskSync/dumps/ run_command=/husksync:userdata dump %1% %2% file) [[☂ Web Dump…]](dark_gray show_text=&7Click to dump this raw user data snapshot to the mc-logs service\n&8You will be provided with a URL containing the data. run_command=/husksync:userdata dump %1% %2% web)'
|
data_manager_system_buttons: '[System:](gray) [[⏷ File Dump…]](dark_gray show_text=&7Click to dump this raw user data snapshot to a file.\n&8Data dumps can be found in ~/plugins/HuskSync/dumps/ run_command=/husksync:userdata dump %1% %2% file) [[☂ Web Dump…]](dark_gray show_text=&7Click to dump this raw user data snapshot to the mc-logs service\n&8You will be provided with a URL containing the data. run_command=/husksync:userdata dump %1% %2% web)'
|
||||||
data_manager_advancements_preview_remaining: 'и още %1%…'
|
data_manager_advancements_preview_remaining: 'и още %1%…'
|
||||||
data_list_title: '[Лист от](#00fb9a) [снапшоти на данните на потребителя](#00fb9a) [%1%](#00fb9a bold show_text=&7UUID: %2%)\n'
|
data_list_title: '[Лист от](#00fb9a) [снапшоти на данните на потребителя](#00fb9a) [%1%](#00fb9a bold show_text=&7UUID: %2%)\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_list_item: '[%1%](gray show_text=&7User Data Snapshot for %2%\n&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_list_item_invalid: '[%1%](dark_gray show_text=&7User Data Snapshot for %2%\n&8⚡ %4% suggest_command=/userdata delete %2% %3%) [%5%](dark_gray show_text=&7Pinned:\n&8Pinned snapshots won''t be automatically rotated. suggest_command=/userdata delete %2% %3%) [%6% ⚑ %8% ⏏ %9%](gray strikethrough show_text=&#ff3300&Invalid Data Snapshot\n&#ff7e5e&Click to delete\n\n&7⚠ %10% suggest_command=/userdata delete %2% %3%)'
|
||||||
data_deleted: '[❌ Успешно изтрихме снапшота с потребителски данни](#00fb9a) [%1%](#00fb9a show_text=&7Версия на UUID:\n&8%2%) [за](#00fb9a) [%3%.](#00fb9a show_text=&7UUID на Играча:\n&8%4%)'
|
data_deleted: '[❌ Успешно изтрихме снапшота с потребителски данни](#00fb9a) [%1%](#00fb9a show_text=&7Версия на UUID:\n&8%2%) [за](#00fb9a) [%3%.](#00fb9a show_text=&7UUID на Играча:\n&8%4%)'
|
||||||
data_restored: '[⏪ Успешно възстановихме](#00fb9a) [текущите потребителски данни за](#00fb9a) [%1%](#00fb9a show_text=&7UUID на Играча:\n&8%2%) [от снапшот](#00fb9a) [%3%.](#00fb9a show_text=&7Версия на UUID:\n&8%4%)'
|
data_restored: '[⏪ Успешно възстановихме](#00fb9a) [текущите потребителски данни за](#00fb9a) [%1%](#00fb9a show_text=&7UUID на Играча:\n&8%2%) [от снапшот](#00fb9a) [%3%.](#00fb9a show_text=&7Версия на UUID:\n&8%4%)'
|
||||||
data_pinned: '[※ Успешно закачихме снапшота с потребителски данни](#00fb9a) [%1%](#00fb9a show_text=&7Версия на UUID:\n&8%2%) [за](#00fb9a) [%3%.](#00fb9a show_text=&7UUID на Играча:\n&8%4%)'
|
data_pinned: '[※ Успешно закачихме снапшота с потребителски данни](#00fb9a) [%1%](#00fb9a show_text=&7Версия на UUID:\n&8%2%) [за](#00fb9a) [%3%.](#00fb9a show_text=&7UUID на Играча:\n&8%4%)'
|
||||||
@@ -52,6 +53,7 @@ locales:
|
|||||||
system_status_header: '[HuskSync](#00fb9a bold) [| System status report:](#00fb9a)'
|
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_syntax: '[Грешка:](#ff3300) [Неправилен синтаксис. Използвайте:](#ff7e5e) [%1%](#ff7e5e italic show_text=&#ff7e5e&Click to suggest suggest_command=%1%)'
|
||||||
error_invalid_player: '[Грешка:](#ff3300) [Не можахме да открием играч с това име.](#ff7e5e)'
|
error_invalid_player: '[Грешка:](#ff3300) [Не можахме да открием играч с това име.](#ff7e5e)'
|
||||||
|
error_invalid_data: '[Error:](#ff3300) [Failed to unpack user data as the snapshot is invalid or corrupt.](#ff7e5e) [(Details…)](gray show_text=&7⚠ %1%)'
|
||||||
error_no_permission: '[Грешка:](#ff3300) [Нямате право да използвате тази команда](#ff7e5e)'
|
error_no_permission: '[Грешка:](#ff3300) [Нямате право да използвате тази команда](#ff7e5e)'
|
||||||
error_console_command_only: '[Грешка:](#ff3300) [Тази команда може да бъде използвана единствено през конзолата](#ff7e5e)'
|
error_console_command_only: '[Грешка:](#ff3300) [Тази команда може да бъде използвана единствено през конзолата](#ff7e5e)'
|
||||||
error_in_game_command_only: 'Грешка: Тази команда може да бъде използвана само от играта.'
|
error_in_game_command_only: 'Грешка: Тази команда може да бъде използвана само от играта.'
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ locales:
|
|||||||
data_manager_advancements_preview_remaining: 'und %1% weitere…'
|
data_manager_advancements_preview_remaining: 'und %1% weitere…'
|
||||||
data_list_title: '[Nutzerdaten-Schnappschüsse von %1%:](#00fb9a) [(%2%-%3% von](#00fb9a) [%4%](#00fb9a bold)[)](#00fb9a)\n'
|
data_list_title: '[Nutzerdaten-Schnappschüsse von %1%:](#00fb9a) [(%2%-%3% von](#00fb9a) [%4%](#00fb9a bold)[)](#00fb9a)\n'
|
||||||
data_list_item: '[%1%](gray show_text=&7Nutzerdaten-Schnappschuss für %2%&8⚡ %4% run_command=/userdata view %2% %3%) [%5%](#d8ff2b show_text=&7Angeheftet:\n&8Angeheftete Schnappschüsse werden nicht automatisch rotiert. run_command=/userdata view %2% %3%) [%6%](color=#ffc43b-#f5c962 show_text=&7Versions-Zeitstempel:&7\n&8Zeitpunkt der Speicherung der Daten\n&8%7% run_command=/userdata view %2% %3%) [⚑ %8%](#23a825-#36f539 show_text=&7Speicherungsgrund:\n&8Grund für das Speichern der Daten run_command=/userdata view %2% %3%) [⏏ %9%](color=#62a9f5-#7ab8fa show_text=&7Schnappschuss-Größe:&7\n&8Geschätzte Dateigröße des Schnappschusses (in KiB) run_command=/userdata view %2% %3%)'
|
data_list_item: '[%1%](gray show_text=&7Nutzerdaten-Schnappschuss für %2%&8⚡ %4% run_command=/userdata view %2% %3%) [%5%](#d8ff2b show_text=&7Angeheftet:\n&8Angeheftete Schnappschüsse werden nicht automatisch rotiert. run_command=/userdata view %2% %3%) [%6%](color=#ffc43b-#f5c962 show_text=&7Versions-Zeitstempel:&7\n&8Zeitpunkt der Speicherung der Daten\n&8%7% run_command=/userdata view %2% %3%) [⚑ %8%](#23a825-#36f539 show_text=&7Speicherungsgrund:\n&8Grund für das Speichern der Daten run_command=/userdata view %2% %3%) [⏏ %9%](color=#62a9f5-#7ab8fa show_text=&7Schnappschuss-Größe:&7\n&8Geschätzte Dateigröße des Schnappschusses (in KiB) run_command=/userdata view %2% %3%)'
|
||||||
|
data_list_item_invalid: '[%1%](dark_gray show_text=&7User Data Snapshot for %2%\n&8⚡ %4% suggest_command=/userdata delete %2% %3%) [%5%](dark_gray show_text=&7Pinned:\n&8Pinned snapshots won''t be automatically rotated. suggest_command=/userdata delete %2% %3%) [%6% ⚑ %8% ⏏ %9%](gray strikethrough show_text=&#ff3300&Invalid Data Snapshot\n&#ff7e5e&Click to delete\n\n&7⚠ %10% suggest_command=/userdata delete %2% %3%)'
|
||||||
data_deleted: '[❌ Nutzerdaten-Schnappschuss erfolgreich gelöscht](#00fb9a) [%1%](#00fb9a show_text=&7Version UUID:\n&8%2%) [für](#00fb9a) [%3%.](#00fb9a show_text=&7Player UUID:\n&8%4%)'
|
data_deleted: '[❌ Nutzerdaten-Schnappschuss erfolgreich gelöscht](#00fb9a) [%1%](#00fb9a show_text=&7Version UUID:\n&8%2%) [für](#00fb9a) [%3%.](#00fb9a show_text=&7Player UUID:\n&8%4%)'
|
||||||
data_restored: '[⏪ Erfgreich wiederhergestellt](#00fb9a) [Aktuelle Nutzerdaten des Schnappschusses von %1%](#00fb9a show_text=&7Spieler-UUID:\n&8%2%) [%3%.](#00fb9a show_text=&7Versions-UUID:\n&8%4%)'
|
data_restored: '[⏪ Erfgreich wiederhergestellt](#00fb9a) [Aktuelle Nutzerdaten des Schnappschusses von %1%](#00fb9a show_text=&7Spieler-UUID:\n&8%2%) [%3%.](#00fb9a show_text=&7Versions-UUID:\n&8%4%)'
|
||||||
data_pinned: '[※ Nutzerdaten-Schnappschuss erfolgreich angepinnt](#00fb9a) [%1%](#00fb9a show_text=&7Versions-UUID:\n&8%2%) [für](#00fb9a) [%3%.](#00fb9a show_text=&7Spieler-UUID:\n&8%4%)'
|
data_pinned: '[※ Nutzerdaten-Schnappschuss erfolgreich angepinnt](#00fb9a) [%1%](#00fb9a show_text=&7Versions-UUID:\n&8%2%) [für](#00fb9a) [%3%.](#00fb9a show_text=&7Spieler-UUID:\n&8%4%)'
|
||||||
@@ -52,6 +53,7 @@ locales:
|
|||||||
system_status_header: '[HuskSync](#00fb9a bold) [| System status report:](#00fb9a)'
|
system_status_header: '[HuskSync](#00fb9a bold) [| System status report:](#00fb9a)'
|
||||||
error_invalid_syntax: '[Fehler:](#ff3300) [Falsche Syntax. Nutze:](#ff7e5e) [%1%](#ff7e5e italic show_text=&#ff7e5e&Click to suggest suggest_command=%1%)'
|
error_invalid_syntax: '[Fehler:](#ff3300) [Falsche Syntax. Nutze:](#ff7e5e) [%1%](#ff7e5e italic show_text=&#ff7e5e&Click to suggest suggest_command=%1%)'
|
||||||
error_invalid_player: '[Fehler:](#ff3300) [Es konnte kein Spieler mit diesem Namen gefunden werden.](#ff7e5e)'
|
error_invalid_player: '[Fehler:](#ff3300) [Es konnte kein Spieler mit diesem Namen gefunden werden.](#ff7e5e)'
|
||||||
|
error_invalid_data: '[Error:](#ff3300) [Failed to unpack user data as the snapshot is invalid or corrupt.](#ff7e5e) [(Details…)](gray show_text=&7⚠ %1%)'
|
||||||
error_no_permission: '[Fehler:](#ff3300) [Du hast nicht die benötigten Berechtigungen um diesen Befehl auszuführen](#ff7e5e)'
|
error_no_permission: '[Fehler:](#ff3300) [Du hast nicht die benötigten Berechtigungen um diesen Befehl auszuführen](#ff7e5e)'
|
||||||
error_console_command_only: '[Fehler:](#ff3300) [Dieser Befehl kann nur über die Konsole ausgeführt werden.](#ff7e5e)'
|
error_console_command_only: '[Fehler:](#ff3300) [Dieser Befehl kann nur über die Konsole ausgeführt werden.](#ff7e5e)'
|
||||||
error_in_game_command_only: 'Fehler: Dieser Befehl kann nur im Spiel genutzt werden.'
|
error_in_game_command_only: 'Fehler: Dieser Befehl kann nur im Spiel genutzt werden.'
|
||||||
|
|||||||
@@ -21,7 +21,8 @@ locales:
|
|||||||
data_manager_system_buttons: '[System:](gray) [[⏷ File Dump…]](dark_gray show_text=&7Click to dump this raw user data snapshot to a file.\n&8Data dumps can be found in ~/plugins/HuskSync/dumps/ run_command=/husksync:userdata dump %1% %2% file) [[☂ Web Dump…]](dark_gray show_text=&7Click to dump this raw user data snapshot to the mc-logs service\n&8You will be provided with a URL containing the data. run_command=/husksync:userdata dump %1% %2% web)'
|
data_manager_system_buttons: '[System:](gray) [[⏷ File Dump…]](dark_gray show_text=&7Click to dump this raw user data snapshot to a file.\n&8Data dumps can be found in ~/plugins/HuskSync/dumps/ run_command=/husksync:userdata dump %1% %2% file) [[☂ Web Dump…]](dark_gray show_text=&7Click to dump this raw user data snapshot to the mc-logs service\n&8You will be provided with a URL containing the data. run_command=/husksync:userdata dump %1% %2% web)'
|
||||||
data_manager_advancements_preview_remaining: 'and %1% more…'
|
data_manager_advancements_preview_remaining: 'and %1% more…'
|
||||||
data_list_title: '[%1%''s user data snapshots:](#00fb9a) [(%2%-%3% of](#00fb9a) [%4%](#00fb9a bold)[)](#00fb9a)\n'
|
data_list_title: '[%1%''s user data snapshots:](#00fb9a) [(%2%-%3% of](#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_list_item: '[%1%](gray show_text=&7User Data Snapshot for %2%\n&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_list_item_invalid: '[%1%](dark_gray show_text=&7User Data Snapshot for %2%\n&8⚡ %4% suggest_command=/userdata delete %2% %3%) [%5%](dark_gray show_text=&7Pinned:\n&8Pinned snapshots won''t be automatically rotated. suggest_command=/userdata delete %2% %3%) [%6% ⚑ %8% ⏏ %9%](gray strikethrough show_text=&#ff3300&Invalid Data Snapshot\n&#ff7e5e&Click to delete\n\n&7⚠ %10% suggest_command=/userdata delete %2% %3%)'
|
||||||
data_deleted: '[❌ Successfully deleted user data snapshot](#00fb9a) [%1%](#00fb9a show_text=&7Version UUID:\n&8%2%) [for](#00fb9a) [%3%.](#00fb9a show_text=&7Player UUID:\n&8%4%)'
|
data_deleted: '[❌ Successfully deleted user data snapshot](#00fb9a) [%1%](#00fb9a show_text=&7Version UUID:\n&8%2%) [for](#00fb9a) [%3%.](#00fb9a show_text=&7Player UUID:\n&8%4%)'
|
||||||
data_restored: '[⏪ Successfully restored](#00fb9a) [%1%](#00fb9a show_text=&7Player UUID:\n&8%2%)[''s current user data from snapshot](#00fb9a) [%3%.](#00fb9a show_text=&7Version UUID:\n&8%4%)'
|
data_restored: '[⏪ Successfully restored](#00fb9a) [%1%](#00fb9a show_text=&7Player UUID:\n&8%2%)[''s current user data from snapshot](#00fb9a) [%3%.](#00fb9a show_text=&7Version UUID:\n&8%4%)'
|
||||||
data_pinned: '[※ Successfully pinned user data snapshot](#00fb9a) [%1%](#00fb9a show_text=&7Version UUID:\n&8%2%) [for](#00fb9a) [%3%.](#00fb9a show_text=&7Player UUID:\n&8%4%)'
|
data_pinned: '[※ Successfully pinned user data snapshot](#00fb9a) [%1%](#00fb9a show_text=&7Version UUID:\n&8%2%) [for](#00fb9a) [%3%.](#00fb9a show_text=&7Player UUID:\n&8%4%)'
|
||||||
@@ -52,6 +53,7 @@ locales:
|
|||||||
system_status_header: '[HuskSync](#00fb9a bold) [| System status report:](#00fb9a)'
|
system_status_header: '[HuskSync](#00fb9a bold) [| System status report:](#00fb9a)'
|
||||||
error_invalid_syntax: '[Error:](#ff3300) [Incorrect syntax. Usage:](#ff7e5e) [%1%](#ff7e5e italic show_text=&#ff7e5e&Click to suggest suggest_command=%1%)'
|
error_invalid_syntax: '[Error:](#ff3300) [Incorrect syntax. Usage:](#ff7e5e) [%1%](#ff7e5e italic show_text=&#ff7e5e&Click to suggest suggest_command=%1%)'
|
||||||
error_invalid_player: '[Error:](#ff3300) [Could not find a player by that name.](#ff7e5e)'
|
error_invalid_player: '[Error:](#ff3300) [Could not find a player by that name.](#ff7e5e)'
|
||||||
|
error_invalid_data: '[Error:](#ff3300) [Failed to unpack user data as the snapshot is invalid or corrupt.](#ff7e5e) [(Details…)](gray show_text=&7⚠ %1%)'
|
||||||
error_no_permission: '[Error:](#ff3300) [You do not have permission to execute this command](#ff7e5e)'
|
error_no_permission: '[Error:](#ff3300) [You do not have permission to execute this command](#ff7e5e)'
|
||||||
error_console_command_only: '[Error:](#ff3300) [That command can only be run through console](#ff7e5e)'
|
error_console_command_only: '[Error:](#ff3300) [That command can only be run through console](#ff7e5e)'
|
||||||
error_in_game_command_only: 'Error: That command can only be used in-game.'
|
error_in_game_command_only: 'Error: That command can only be used in-game.'
|
||||||
|
|||||||
@@ -21,7 +21,8 @@ locales:
|
|||||||
data_manager_system_buttons: '[System:](gray) [[⏷ File Dump…]](dark_gray show_text=&7Click to dump this raw user data snapshot to a file.\n&8Data dumps can be found in ~/plugins/HuskSync/dumps/ run_command=/husksync:userdata dump %1% %2% file) [[☂ Web Dump…]](dark_gray show_text=&7Click to dump this raw user data snapshot to the mc-logs service\n&8You will be provided with a URL containing the data. run_command=/husksync:userdata dump %1% %2% web)'
|
data_manager_system_buttons: '[System:](gray) [[⏷ File Dump…]](dark_gray show_text=&7Click to dump this raw user data snapshot to a file.\n&8Data dumps can be found in ~/plugins/HuskSync/dumps/ run_command=/husksync:userdata dump %1% %2% file) [[☂ Web Dump…]](dark_gray show_text=&7Click to dump this raw user data snapshot to the mc-logs service\n&8You will be provided with a URL containing the data. run_command=/husksync:userdata dump %1% %2% web)'
|
||||||
data_manager_advancements_preview_remaining: 'y %1% más…'
|
data_manager_advancements_preview_remaining: 'y %1% más…'
|
||||||
data_list_title: '[%1%''s user data snapshots:](#00fb9a) [(%2%-%3% of](#00fb9a) [%4%](#00fb9a bold)[)](#00fb9a)\n'
|
data_list_title: '[%1%''s user data snapshots:](#00fb9a) [(%2%-%3% of](#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_list_item: '[%1%](gray show_text=&7User Data Snapshot for %2%\n&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_list_item_invalid: '[%1%](dark_gray show_text=&7User Data Snapshot for %2%\n&8⚡ %4% suggest_command=/userdata delete %2% %3%) [%5%](dark_gray show_text=&7Pinned:\n&8Pinned snapshots won''t be automatically rotated. suggest_command=/userdata delete %2% %3%) [%6% ⚑ %8% ⏏ %9%](gray strikethrough show_text=&#ff3300&Invalid Data Snapshot\n&#ff7e5e&Click to delete\n\n&7⚠ %10% suggest_command=/userdata delete %2% %3%)'
|
||||||
data_deleted: '[❌ Se ha eliminado correctamente la snapshot del usuario](#00fb9a) [%1%](#00fb9a show_text=&7Version UUID:\n&8%2%) [for](#00fb9a) [%3%.](#00fb9a show_text=&7Player UUID:\n&8%4%)'
|
data_deleted: '[❌ Se ha eliminado correctamente la snapshot del usuario](#00fb9a) [%1%](#00fb9a show_text=&7Version UUID:\n&8%2%) [for](#00fb9a) [%3%.](#00fb9a show_text=&7Player UUID:\n&8%4%)'
|
||||||
data_restored: '[⏪ Restaurado correctamente](#00fb9a) [%1%](#00fb9a show_text=&7UUID del jugador:\n&8%2%)[Informacion actual de la snapshot del jugador](#00fb9a) [%3%.](#00fb9a show_text=&7Version UUID:\n&8%4%)'
|
data_restored: '[⏪ Restaurado correctamente](#00fb9a) [%1%](#00fb9a show_text=&7UUID del jugador:\n&8%2%)[Informacion actual de la snapshot del jugador](#00fb9a) [%3%.](#00fb9a show_text=&7Version UUID:\n&8%4%)'
|
||||||
data_pinned: '[※ Se ha anclado perfectamente la snapshot del jugador](#00fb9a) [%1%](#00fb9a show_text=&7Version UUID:\n&8%2%) [for](#00fb9a) [%3%.](#00fb9a show_text=&7UUID del usuario:\n&8%4%)'
|
data_pinned: '[※ Se ha anclado perfectamente la snapshot del jugador](#00fb9a) [%1%](#00fb9a show_text=&7Version UUID:\n&8%2%) [for](#00fb9a) [%3%.](#00fb9a show_text=&7UUID del usuario:\n&8%4%)'
|
||||||
@@ -52,6 +53,7 @@ locales:
|
|||||||
system_status_header: '[HuskSync](#00fb9a bold) [| System status report:](#00fb9a)'
|
system_status_header: '[HuskSync](#00fb9a bold) [| System status report:](#00fb9a)'
|
||||||
error_invalid_syntax: '[Error:](#ff3300) [Sintanxis incorrecta. Usa:](#ff7e5e) [%1%](#ff7e5e italic show_text=&#ff7e5e&Click to suggest suggest_command=%1%)'
|
error_invalid_syntax: '[Error:](#ff3300) [Sintanxis incorrecta. Usa:](#ff7e5e) [%1%](#ff7e5e italic show_text=&#ff7e5e&Click to suggest suggest_command=%1%)'
|
||||||
error_invalid_player: '[Error:](#ff3300) [No se ha podido encontrar un jugador con ese nombre.](#ff7e5e)'
|
error_invalid_player: '[Error:](#ff3300) [No se ha podido encontrar un jugador con ese nombre.](#ff7e5e)'
|
||||||
|
error_invalid_data: '[Error:](#ff3300) [Failed to unpack user data as the snapshot is invalid or corrupt.](#ff7e5e) [(Details…)](gray show_text=&7⚠ %1%)'
|
||||||
error_no_permission: '[Error:](#ff3300) [No tienes permisos para ejecutar este comando.](#ff7e5e)'
|
error_no_permission: '[Error:](#ff3300) [No tienes permisos para ejecutar este comando.](#ff7e5e)'
|
||||||
error_console_command_only: '[Error:](#ff3300) [Este comando solo se puede ejecutar desde la consola.](#ff7e5e)'
|
error_console_command_only: '[Error:](#ff3300) [Este comando solo se puede ejecutar desde la consola.](#ff7e5e)'
|
||||||
error_in_game_command_only: 'Error: Ese comando solo se puede utilizar desde el juego.'
|
error_in_game_command_only: 'Error: Ese comando solo se puede utilizar desde el juego.'
|
||||||
|
|||||||
65
common/src/main/resources/locales/fr-fr.yml
Normal file
65
common/src/main/resources/locales/fr-fr.yml
Normal file
@@ -0,0 +1,65 @@
|
|||||||
|
locales:
|
||||||
|
synchronization_complete: '[⏵ Données synchronisées!](#00fb9a)'
|
||||||
|
synchronization_failed: '[⏵ Impossible de synchroniser vos données! Veuillez contacter un administrateur.](#ff7e5e)'
|
||||||
|
inventory_viewer_menu_title: '&0Inventaire de %1%'
|
||||||
|
ender_chest_viewer_menu_title: '&0Coffre de l''Ender de %1%'
|
||||||
|
inventory_viewer_opened: '[Visualisation de l''instantané de](#00fb9a) [%1%](#00fb9a bold)[''s inventaire à partir de ⌚ %2%](#00fb9a)'
|
||||||
|
ender_chest_viewer_opened: '[Visualisation de l''instantané du](#00fb9a) [%1%](#00fb9a bold)[''s coffre de l''Ender à partir de ⌚ %2%](#00fb9a)'
|
||||||
|
data_update_complete: '[🔔 Vos données ont été mises à jour!](#00fb9a)'
|
||||||
|
data_update_failed: '[🔔 Échec de la mise à jour de vos données! Veuillez contacter un administrateur.](#ff7e5e)'
|
||||||
|
user_registration_complete: '[⭐ Inscription de l''utilisateur complète!](#00fb9a)'
|
||||||
|
data_manager_title: '[Visualisation de l''instantané des données utilisateur](#00fb9a) [%1%](#00fb9a show_text=&7UUID de la version:\n&8%2%) [pour](#00fb9a) [%3%](#00fb9a bold show_text=&7UUID du joueur:\n&8%4%)[:](#00fb9a)'
|
||||||
|
data_manager_timestamp: '[⌚ %1%](#ffc43b-#f5c962 show_text=&7Horodatage de la version:\n&8Quand les données ont été enregistrées)'
|
||||||
|
data_manager_pinned: '[※ Instantané épinglé](#d8ff2b show_text=&7Épinglé:\n&8Cet instantané des données utilisateur ne sera pas automatiquement supprimé.)'
|
||||||
|
data_manager_cause: '[⚑ %1%](#23a825-#36f539 show_text=&7Cause de la sauvegarde:\n&8Ce qui a causé l''enregistrement des données)'
|
||||||
|
data_manager_server: '[☁ %1%](#ff87b3-#f5538e show_text=&7Serveur:\n&8Nom du serveur sur lequel les données ont été enregistrées)'
|
||||||
|
data_manager_size: '[⏏ %1%](color=#62a9f5-#7ab8fa show_text=&7Taille de l''instantané:\n&8Taille du fichier estimée de l''instantané (en KiB))\n'
|
||||||
|
data_manger_status: '[%1%](red)[/](gray)[%2%](red)[×](gray)[❤](red show_text=&7Points de vie) [%3%](yellow)[×](gray)[🍖](yellow show_text=&7Points de faim) [ʟᴠ](green)[.](gray)[%4%](greenshow_text=&7Niveau XP) [🏹 %5%](dark_aqua show_text=&7Mode de jeu)'
|
||||||
|
data_manager_advancements_statistics: '[⭐ Avancements: %1%](color=#ffc43b-#f5c962show_text=&7Avancements dans lesquels vous avez progressé:\n&8%2%) [⌛ Temps de jeu: %3%ʜʀs](color=#62a9f5-#7ab8fashow_text=&7Temps de jeu en jeu\n&8⚠ Basé sur les statistiques en jeu)\n'
|
||||||
|
data_manager_item_buttons: '[Voir:](gray) [[🪣 Inventaire…]](color=#a17b5f-#f5b98cshow_text=&7Cliquez pour voir run_command=/inventory %1% %2%) [[⌀ Coffre de l''Ender…]](#b649c4-#d254ffshow_text=&7Cliquez pour voir run_command=/enderchest %1% %2%)'
|
||||||
|
data_manager_management_buttons: '[Gérer:](gray) [[❌ Supprimer…]](#ff3300 show_text=&7Cliquezpour supprimer cet instantané des données utilisateur.\n&8Cela n''affectera pas les données actuelles de l''utilisateur.\n&#ff3300&⚠ Cette action est irréversible! suggest_command=/husksync:userdata delete%1% %2%) [[⏪ Restaurer…]](#00fb9a show_text=&7Cliquez pour restaurer ces données utilisateur.\n&8Cela définira les données de l''utilisateur sur cet instantané.\n&#ff3300&⚠ Les données actuelles de %1% serontremplacées! suggest_command=/husksync:userdata restore %1% %2%) [[※ Épingler/Détacher…]](#d8ff2bshow_text=&7Cliquez pour épingler ou détacher cet instantané des données utilisateur\n&8Les instantanés épinglés ne seront pas automatiquement supprimés run_command=/userdata pin %1% %2%)'
|
||||||
|
data_manager_system_buttons: '[Système:](gray) [[⏷ Export fichier…]](dark_gray show_text=&7Cliquezpour exporter cet instantané des données utilisateur à un fichier.\n&8Les exports de données peuvent être trouvés dans ~/plugins/HuskSync/dumps/run_command=/husksync:userdata dump %1% %2% file) [[☂ Export web…]](dark_grayshow_text=&7Cliquez pour exporter cet instantané des données utilisateur au service mc-logs\n&8Vousobtiendrez une URL contenant les données. run_command=/husksync:userdatadump %1% %2% web)'
|
||||||
|
data_manager_advancements_preview_remaining: 'et %1% autres…'
|
||||||
|
data_list_title: '[Les instantanés des données utilisateur de %1%:](#00fb9a) [(%2%-%3% sur](#00fb9a)[%4%](#00fb9a bold)[)](#00fb9a)\n'
|
||||||
|
data_list_item: '[%1%](gray show_text=&7Instantané des données utilisateur pour %2%\n&8⚡ %4% run_command=/userdataview %2% %3%) [%5%](#d8ff2b show_text=&7Épinglé:\n&8Les instantanés épinglés ne serontpas automatiquement supprimés. run_command=/userdata view %2% %3%) [%6%](color=#ffc43b-#f5c962show_text=&7Horodatage de la version:&7\n&8Quand les données ont été enregistrées\n&8%7% run_command=/userdataview %2% %3%) [⚑ %8%](#23a825-#36f539 show_text=&7Cause de la sauvegarde:\n&8Ce qui a causél''enregistrement des données run_command=/userdata view %2% %3%) [⏏ %9%](color=#62a9f5-#7ab8fashow_text=&7Taille de l''instantané:&7\n&8Taille du fichier estimée de l''instantané (en KiB) run_command=/userdataview %2% %3%)'
|
||||||
|
data_list_item_invalid: '[%1%](dark_gray show_text=&7Instantané des données utilisateur pour %2%\n&8⚡%4% suggest_command=/userdata delete %2% %3%) [%5%](dark_gray show_text=&7Épinglé:\n&8Lesinstantanés épinglés ne seront pas automatiquement supprimés. suggest_command=/userdata delete %2%%3%) [%6% ⚑ %8% ⏏ %9%](gray strikethrough show_text=&#ff3300&Instantané des donnéesinvalide\n&#ff7e5e&Cliquez pour supprimer\n\n&7⚠ %10% suggest_command=/userdata delete%2% %3%)'
|
||||||
|
data_deleted: '[❌ Instantané des données utilisateur supprimé avec succès](#00fb9a) [%1%](#00fb9ashow_text=&7UUID de la version:\n&8%2%) [pour](#00fb9a) [%3%.](#00fb9a show_text=&7UUID du joueur:\n&8%4%)'
|
||||||
|
data_restored: '[⏪ Données utilisateur actuelles de %1% restaurées avec succès à partir de l''instantané](#00fb9a) [%3%.](#00fb9a show_text=&7UUID de la version:\n&8%4%)'
|
||||||
|
data_pinned: '[※ Instantané des données utilisateur épinglé avec succès](#00fb9a) [%1%](#00fb9ashow_text=&7UUID de la version:\n&8%2%) [pour](#00fb9a) [%3%.](#00fb9a show_text=&7UUID du joueur:\n&8%4%)'
|
||||||
|
data_unpinned: '[※ Instantané des données utilisateur détaché avec succès](#00fb9a) [%1%](#00fb9ashow_text=&7UUID de la version:\n&8%2%) [pour](#00fb9a) [%3%.](#00fb9a show_text=&7UUID du joueur:\n&8%4%)'
|
||||||
|
data_dumped: '[☂ Dump de l''instantané des données utilisateur %1% pour %2% à:](#00fb9a)&7%3%'
|
||||||
|
list_footer: '\n%1%[Page](#00fb9a) [%2%](#00fb9a)/[%3%](#00fb9a)%4% %5%'
|
||||||
|
list_previous_page_button: '[◀](white show_text=&7Voir la page précédente run_command=%2%%1%) '
|
||||||
|
list_next_page_button: ' [▶](white show_text=&7Voir la page suivante run_command=%2% %1%)'
|
||||||
|
list_page_jumpers: '(%1%)'
|
||||||
|
list_page_jumper_button: '[%1%](show_text=&7Aller à la page %1% run_command=%2% %1%)'
|
||||||
|
list_page_jumper_current_page: '[%1%](#00fb9a)'
|
||||||
|
list_page_jumper_separator: ' '
|
||||||
|
list_page_jumper_group_separator: '…'
|
||||||
|
save_cause_disconnect: 'déconnexion'
|
||||||
|
save_cause_world_save: 'sauvegarde du monde'
|
||||||
|
save_cause_death: 'mort'
|
||||||
|
save_cause_server_shutdown: 'arrêt du serveur'
|
||||||
|
save_cause_inventory_command: 'commande d''inventaire'
|
||||||
|
save_cause_enderchest_command: 'commande du coffre de l''Ender'
|
||||||
|
save_cause_backup_restore: 'restauration de sauvegarde'
|
||||||
|
save_cause_api: 'API'
|
||||||
|
save_cause_mpdb_migration: 'migration MPDB'
|
||||||
|
save_cause_legacy_migration: 'migration legacy'
|
||||||
|
save_cause_converted_from_v2: 'converti de v2'
|
||||||
|
up_to_date: '[HuskSync](#00fb9a bold) [| Vous utilisez la dernière version de HuskSync(v%1%).](#00fb9a)'
|
||||||
|
update_available: '[HuskSync](#ff7e5e bold) [| Une nouvelle version de HuskSync est disponible:v%1% (version actuelle: v%2%).](#ff7e5e)'
|
||||||
|
reload_complete: '[HuskSync](#00fb9a bold) [| Config et messages rechargés.](#00fb9a)\n[⚠Assurez-vous que les fichiers de configuration sont à jour sur tous les serveurs!](#00fb9a)\n[Un redémarrage est nécessairepour que les modifications de configuration prennent effet.](#00fb9a italic)'
|
||||||
|
system_status_header: '[HuskSync](#00fb9a bold) [| Rapport d''état du système:](#00fb9a)'
|
||||||
|
error_invalid_syntax: '[Erreur:](#ff3300) [Syntaxe incorrecte. Utilisation:](#ff7e5e) [%1%](#ff7e5eitalic show_text=&#ff7e5e&Cliquez pour suggérer suggest_command=%1%)'
|
||||||
|
error_invalid_player: '[Erreur:](#ff3300) [Impossible de trouver un joueur avec ce nom.](#ff7e5e)'
|
||||||
|
error_invalid_data: '[Erreur:](#ff3300) [Impossible de déballer les données de l''instantané car elles sont invalides ou corrompues.](#ff7e5e) [(Détails…)](gray show_text=&7⚠ %1%)'
|
||||||
|
error_no_permission: '[Erreur:](#ff3300) [Vous n''avez pas la permission d''exécuter cettecommande](#ff7e5e)'
|
||||||
|
error_console_command_only: '[Erreur:](#ff3300) [Cette commande peut seulement être exécutée via la console](#ff7e5e)'
|
||||||
|
error_in_game_command_only: 'Erreur: Cette commande peut uniquement être utilisée en jeu.'
|
||||||
|
error_no_data_to_display: '[Erreur:](#ff3300) [Impossible de trouver des données utilisateur à afficher.](#ff7e5e)'
|
||||||
|
error_invalid_version_uuid: '[Erreur:](#ff3300) [Impossible de trouver des données utilisateur pour cet UUID de version.](#ff7e5e)'
|
||||||
|
husksync_command_description: 'Gérer le plugin HuskSync'
|
||||||
|
userdata_command_description: 'Voir, gérer & restaurer les données utilisateur des joueurs'
|
||||||
|
inventory_command_description: 'Voir & modifier l''inventaire d''un joueur'
|
||||||
|
enderchest_command_description: 'Voir & modifier le Coffre de l''Ender d''un joueur'
|
||||||
@@ -22,6 +22,7 @@ locales:
|
|||||||
data_manager_advancements_preview_remaining: 'dan %1% lagi…'
|
data_manager_advancements_preview_remaining: 'dan %1% lagi…'
|
||||||
data_list_title: '[Cuplikan data %1%:](#00fb9a) [(%2%-%3% dari](#00fb9a) [%4%](#00fb9a bold)[)](#00fb9a)\n'
|
data_list_title: '[Cuplikan data %1%:](#00fb9a) [(%2%-%3% dari](#00fb9a) [%4%](#00fb9a bold)[)](#00fb9a)\n'
|
||||||
data_list_item: '[%1%](gray show_text=&7Cuplikan data pengguna untuk %2%&8⚡ %4% run_command=/userdata view %2% %3%) [%5%](#d8ff2b show_text=&7Disematkan:\n&8Cuplikan yang disematkan tidak akan dirotasi otomatis. run_command=/userdata view %2% %3%) [%6%](color=#ffc43b-#f5c962 show_text=&7Versi stampel waktu:&7\n&8Saat data disimpan\n&8%7% run_command=/userdata view %2% %3%) [⚑ %8%](#23a825-#36f539 show_text=&7Disimpan karena:\n&8Apa yang menyebabkan data disimpan run_command=/userdata view %2% %3%) [⏏ %9%](color=#62a9f5-#7ab8fa show_text=&7Ukuran cuplikan:&7\n&8Perkiraan ukuran file cuplikan (dalam KiB) run_command=/userdata view %2% %3%)'
|
data_list_item: '[%1%](gray show_text=&7Cuplikan data pengguna untuk %2%&8⚡ %4% run_command=/userdata view %2% %3%) [%5%](#d8ff2b show_text=&7Disematkan:\n&8Cuplikan yang disematkan tidak akan dirotasi otomatis. run_command=/userdata view %2% %3%) [%6%](color=#ffc43b-#f5c962 show_text=&7Versi stampel waktu:&7\n&8Saat data disimpan\n&8%7% run_command=/userdata view %2% %3%) [⚑ %8%](#23a825-#36f539 show_text=&7Disimpan karena:\n&8Apa yang menyebabkan data disimpan run_command=/userdata view %2% %3%) [⏏ %9%](color=#62a9f5-#7ab8fa show_text=&7Ukuran cuplikan:&7\n&8Perkiraan ukuran file cuplikan (dalam KiB) run_command=/userdata view %2% %3%)'
|
||||||
|
data_list_item_invalid: '[%1%](dark_gray show_text=&7User Data Snapshot for %2%\n&8⚡ %4% suggest_command=/userdata delete %2% %3%) [%5%](dark_gray show_text=&7Pinned:\n&8Pinned snapshots won''t be automatically rotated. suggest_command=/userdata delete %2% %3%) [%6% ⚑ %8% ⏏ %9%](gray strikethrough show_text=&#ff3300&Invalid Data Snapshot\n&#ff7e5e&Click to delete\n\n&7⚠ %10% suggest_command=/userdata delete %2% %3%)'
|
||||||
data_deleted: '[❌ Berhasil menghapus cuplikan data pengguna](#00fb9a) [%1%](#00fb9a show_text=&7Versi UUID:\n&8%2%) [untuk](#00fb9a) [%3%.](#00fb9a show_text=&7UUID Pemain:\n&8%4%)'
|
data_deleted: '[❌ Berhasil menghapus cuplikan data pengguna](#00fb9a) [%1%](#00fb9a show_text=&7Versi UUID:\n&8%2%) [untuk](#00fb9a) [%3%.](#00fb9a show_text=&7UUID Pemain:\n&8%4%)'
|
||||||
data_restored: '[⏪ Berhasil dipulihkan](#00fb9a) [%1%](#00fb9a show_text=&7UUID Pemain:\n&8%2%)[data pengguna saat ini dari cuplikan](#00fb9a) [%3%.](#00fb9a show_text=&7Versi UUID:\n&8%4%)'
|
data_restored: '[⏪ Berhasil dipulihkan](#00fb9a) [%1%](#00fb9a show_text=&7UUID Pemain:\n&8%2%)[data pengguna saat ini dari cuplikan](#00fb9a) [%3%.](#00fb9a show_text=&7Versi UUID:\n&8%4%)'
|
||||||
data_pinned: '[※ Berhasil menyematkan cuplikan data pengguna](#00fb9a) [%1%](#00fb9a show_text=&7Versi UUID:\n&8%2%) [untuk](#00fb9a) [%3%.](#00fb9a show_text=&7UUID Pemain:\n&8%4%)'
|
data_pinned: '[※ Berhasil menyematkan cuplikan data pengguna](#00fb9a) [%1%](#00fb9a show_text=&7Versi UUID:\n&8%2%) [untuk](#00fb9a) [%3%.](#00fb9a show_text=&7UUID Pemain:\n&8%4%)'
|
||||||
@@ -52,6 +53,7 @@ locales:
|
|||||||
system_status_header: '[HuskSync](#00fb9a bold) [| Laporan status sistem:](#00fb9a)'
|
system_status_header: '[HuskSync](#00fb9a bold) [| Laporan status sistem:](#00fb9a)'
|
||||||
error_invalid_syntax: '[Kesalahan:](#ff3300) [Sintaks salah. Penggunaan:](#ff7e5e) [%1%](#ff7e5e italic show_text=&#ff7e5e&Klik untuk menyarankan suggest_command=%1%)'
|
error_invalid_syntax: '[Kesalahan:](#ff3300) [Sintaks salah. Penggunaan:](#ff7e5e) [%1%](#ff7e5e italic show_text=&#ff7e5e&Klik untuk menyarankan suggest_command=%1%)'
|
||||||
error_invalid_player: '[Kesalahan:](#ff3300) [Tidak dapat menemukan pemain dengan nama tersebut.](#ff7e5e)'
|
error_invalid_player: '[Kesalahan:](#ff3300) [Tidak dapat menemukan pemain dengan nama tersebut.](#ff7e5e)'
|
||||||
|
error_invalid_data: '[Error:](#ff3300) [Failed to unpack user data as the snapshot is invalid or corrupt.](#ff7e5e) [(Details…)](gray show_text=&7⚠ %1%)'
|
||||||
error_no_permission: '[Kesalahan:](#ff3300) [Kamu tidak memiliki izin untuk menjalankan perintah ini](#ff7e5e)'
|
error_no_permission: '[Kesalahan:](#ff3300) [Kamu tidak memiliki izin untuk menjalankan perintah ini](#ff7e5e)'
|
||||||
error_console_command_only: '[Kesalahan:](#ff3300) [Perintah itu hanya dapat dijalankan melalui konsol](#ff7e5e)'
|
error_console_command_only: '[Kesalahan:](#ff3300) [Perintah itu hanya dapat dijalankan melalui konsol](#ff7e5e)'
|
||||||
error_in_game_command_only: 'Kesalahan: Perintah itu hanya dapat dijalankan dalam game.'
|
error_in_game_command_only: 'Kesalahan: Perintah itu hanya dapat dijalankan dalam game.'
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ locales:
|
|||||||
data_manager_advancements_preview_remaining: 'e %1% altro…'
|
data_manager_advancements_preview_remaining: 'e %1% altro…'
|
||||||
data_list_title: '[Lista delle istantanee di %1%:](#00fb9a) [(%2%-%3% of](#00fb9a) [%4%](#00fb9a bold)[)](#00fb9a)\n'
|
data_list_title: '[Lista delle istantanee di %1%:](#00fb9a) [(%2%-%3% of](#00fb9a) [%4%](#00fb9a bold)[)](#00fb9a)\n'
|
||||||
data_list_item: '[%1%](gray show_text=&7Instantanea di %2%&8⚡ id: %4% run_command=/userdata view %2% %3%) [%5%](#d8ff2b show_text=&7Fissata:\n&8Se fissata, l''istantanea non viene mai modificata. run_command=/userdata view %2% %3%) [%6%](color=#ffc43b-#f5c962 show_text=&7Data di salvataggio:&7\n&8Momento preciso in cui è stato salvato il dato\n&8%7% run_command=/userdata view %2% %3%) [⚑ %8%](#23a825-#36f539 show_text=&7Causa di salvataggio:\n&8Che cosa ha causato il salvataggio run_command=/userdata view %2% %3%) [⏏ %9%](color=#62a9f5-#7ab8fa show_text=&7Peso dell''istantanea:&7\n&8Peso stimato del file (in KiB) run_command=/userdata view %2% %3%)'
|
data_list_item: '[%1%](gray show_text=&7Instantanea di %2%&8⚡ id: %4% run_command=/userdata view %2% %3%) [%5%](#d8ff2b show_text=&7Fissata:\n&8Se fissata, l''istantanea non viene mai modificata. run_command=/userdata view %2% %3%) [%6%](color=#ffc43b-#f5c962 show_text=&7Data di salvataggio:&7\n&8Momento preciso in cui è stato salvato il dato\n&8%7% run_command=/userdata view %2% %3%) [⚑ %8%](#23a825-#36f539 show_text=&7Causa di salvataggio:\n&8Che cosa ha causato il salvataggio run_command=/userdata view %2% %3%) [⏏ %9%](color=#62a9f5-#7ab8fa show_text=&7Peso dell''istantanea:&7\n&8Peso stimato del file (in KiB) run_command=/userdata view %2% %3%)'
|
||||||
|
data_list_item_invalid: '[%1%](dark_gray show_text=&7User Data Snapshot for %2%\n&8⚡ %4% suggest_command=/userdata delete %2% %3%) [%5%](dark_gray show_text=&7Pinned:\n&8Pinned snapshots won''t be automatically rotated. suggest_command=/userdata delete %2% %3%) [%6% ⚑ %8% ⏏ %9%](gray strikethrough show_text=&#ff3300&Invalid Data Snapshot\n&#ff7e5e&Click to delete\n\n&7⚠ %10% suggest_command=/userdata delete %2% %3%)'
|
||||||
data_deleted: '[❌ Istantanea eliminata con successo](#00fb9a) [%1%](#00fb9a show_text=&7Versione di UUID:\n&8%2%) [per](#00fb9a) [%3%.](#00fb9a show_text=&7Player UUID:\n&8%4%)'
|
data_deleted: '[❌ Istantanea eliminata con successo](#00fb9a) [%1%](#00fb9a show_text=&7Versione di UUID:\n&8%2%) [per](#00fb9a) [%3%.](#00fb9a show_text=&7Player UUID:\n&8%4%)'
|
||||||
data_restored: '[⏪ Ripristato con successo](#00fb9a) [Dati dall''istantanea di](#00fb9a)[%1%](#00fb9a show_text=&7Player UUID:\n&8%2%) [%3%.](#00fb9a show_text=&7Versione di UUID:\n&8%4%)'
|
data_restored: '[⏪ Ripristato con successo](#00fb9a) [Dati dall''istantanea di](#00fb9a)[%1%](#00fb9a show_text=&7Player UUID:\n&8%2%) [%3%.](#00fb9a show_text=&7Versione di UUID:\n&8%4%)'
|
||||||
data_pinned: '[※ Instantanea fissata](#00fb9a) [%1%](#00fb9a show_text=&7UUID della versione:\n&8%2%) [per](#00fb9a) [%3%.](#00fb9a show_text=&7Player UUID:\n&8%4%)'
|
data_pinned: '[※ Instantanea fissata](#00fb9a) [%1%](#00fb9a show_text=&7UUID della versione:\n&8%2%) [per](#00fb9a) [%3%.](#00fb9a show_text=&7Player UUID:\n&8%4%)'
|
||||||
@@ -52,6 +53,7 @@ locales:
|
|||||||
system_status_header: '[HuskSync](#00fb9a bold) [| System status report:](#00fb9a)'
|
system_status_header: '[HuskSync](#00fb9a bold) [| System status report:](#00fb9a)'
|
||||||
error_invalid_syntax: '[Errore:](#ff3300) [Sintassi errata. Usa:](#ff7e5e) [%1%](#ff7e5e italic show_text=&#ff7e5e&Click to suggest suggest_command=%1%)'
|
error_invalid_syntax: '[Errore:](#ff3300) [Sintassi errata. Usa:](#ff7e5e) [%1%](#ff7e5e italic show_text=&#ff7e5e&Click to suggest suggest_command=%1%)'
|
||||||
error_invalid_player: '[Errore:](#ff3300) [Impossibile trovare un giocatore con questo nome.](#ff7e5e)'
|
error_invalid_player: '[Errore:](#ff3300) [Impossibile trovare un giocatore con questo nome.](#ff7e5e)'
|
||||||
|
error_invalid_data: '[Error:](#ff3300) [Failed to unpack user data as the snapshot is invalid or corrupt.](#ff7e5e) [(Details…)](gray show_text=&7⚠ %1%)'
|
||||||
error_no_permission: '[Errore:](#ff3300) [Non hai il permesso di usare questo comando](#ff7e5e)'
|
error_no_permission: '[Errore:](#ff3300) [Non hai il permesso di usare questo comando](#ff7e5e)'
|
||||||
error_console_command_only: '[Errore:](#ff3300) [Questo comando può essere eseguito solo dalla](#ff7e5e)'
|
error_console_command_only: '[Errore:](#ff3300) [Questo comando può essere eseguito solo dalla](#ff7e5e)'
|
||||||
error_in_game_command_only: 'Errore: Questo comando può essere utilizzato solo in gioco.'
|
error_in_game_command_only: 'Errore: Questo comando può essere utilizzato solo in gioco.'
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ locales:
|
|||||||
data_manager_advancements_preview_remaining: 'さらに %1% 件…'
|
data_manager_advancements_preview_remaining: 'さらに %1% 件…'
|
||||||
data_list_title: '[%1% のユーザーデータスナップショット:](#00fb9a) [(%4%件中](#00fb9a bold) [%2%-%3%件](#00fb9a)[)](#00fb9a)\n'
|
data_list_title: '[%1% のユーザーデータスナップショット:](#00fb9a) [(%4%件中](#00fb9a bold) [%2%-%3%件](#00fb9a)[)](#00fb9a)\n'
|
||||||
data_list_item: '[%1%](gray show_text=&7%2% のユーザーデータスナップショット&8⚡ %4% run_command=/husksync:userdata view %2% %3%) [%5%](#d8ff2b show_text=&7ピン留め:\n&8ピン留めされたスナップショットは自動的にローテーションしません。 run_command=/husksync: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_list_item: '[%1%](gray show_text=&7%2% のユーザーデータスナップショット&8⚡ %4% run_command=/husksync:userdata view %2% %3%) [%5%](#d8ff2b show_text=&7ピン留め:\n&8ピン留めされたスナップショットは自動的にローテーションしません。 run_command=/husksync: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_list_item_invalid: '[%1%](dark_gray show_text=&7User Data Snapshot for %2%\n&8⚡ %4% suggest_command=/userdata delete %2% %3%) [%5%](dark_gray show_text=&7Pinned:\n&8Pinned snapshots won''t be automatically rotated. suggest_command=/userdata delete %2% %3%) [%6% ⚑ %8% ⏏ %9%](gray strikethrough show_text=&#ff3300&Invalid Data Snapshot\n&#ff7e5e&Click to delete\n\n&7⚠ %10% suggest_command=/userdata delete %2% %3%)'
|
||||||
data_deleted: '[❌](#00fb9a) [%3%](#00fb9a show_text=&7Player UUID:\n&8%4%) [のユーザーデータスナップショット](#00fb9a) [%1%](#00fb9a show_text=&7Version UUID:\n&8%2%) [の消去に成功しました。](#00fb9a)'
|
data_deleted: '[❌](#00fb9a) [%3%](#00fb9a show_text=&7Player UUID:\n&8%4%) [のユーザーデータスナップショット](#00fb9a) [%1%](#00fb9a show_text=&7Version UUID:\n&8%2%) [の消去に成功しました。](#00fb9a)'
|
||||||
data_restored: '[⏪](#00fb9a) [スナップショット](#00fb9a) [%3%](#00fb9a show_text=&7Version UUID:\n&8%4%) [から](#00fb9a) [%1%](#00fb9a show_text=&7Player UUID:\n&8%2%) [の現在のユーザーデータの復元に成功しました。](#00fb9a)'
|
data_restored: '[⏪](#00fb9a) [スナップショット](#00fb9a) [%3%](#00fb9a show_text=&7Version UUID:\n&8%4%) [から](#00fb9a) [%1%](#00fb9a show_text=&7Player UUID:\n&8%2%) [の現在のユーザーデータの復元に成功しました。](#00fb9a)'
|
||||||
data_pinned: '[※](#00fb9a) [%3%](#00fb9a show_text=&7Player UUID:\n&8%4%) [のユーザーデータスナップショット](#00fb9a) [%1%](#00fb9a show_text=&7Version UUID:\n&8%2%) [のピン留めに成功しました。](#00fb9a)'
|
data_pinned: '[※](#00fb9a) [%3%](#00fb9a show_text=&7Player UUID:\n&8%4%) [のユーザーデータスナップショット](#00fb9a) [%1%](#00fb9a show_text=&7Version UUID:\n&8%2%) [のピン留めに成功しました。](#00fb9a)'
|
||||||
@@ -52,6 +53,7 @@ locales:
|
|||||||
system_status_header: '[HuskSync](#00fb9a bold) [| System status report:](#00fb9a)'
|
system_status_header: '[HuskSync](#00fb9a bold) [| System status report:](#00fb9a)'
|
||||||
error_invalid_syntax: '[Error:](#ff3300) [構文が正しくありません。使用法:](#ff7e5e) [%1%](#ff7e5e italic show_text=&#ff7e5e&クリックでサジェスト suggest_command=%1%)'
|
error_invalid_syntax: '[Error:](#ff3300) [構文が正しくありません。使用法:](#ff7e5e) [%1%](#ff7e5e italic show_text=&#ff7e5e&クリックでサジェスト suggest_command=%1%)'
|
||||||
error_invalid_player: '[Error:](#ff3300) [そのプレイヤーは見つかりませんでした](#ff7e5e)'
|
error_invalid_player: '[Error:](#ff3300) [そのプレイヤーは見つかりませんでした](#ff7e5e)'
|
||||||
|
error_invalid_data: '[Error:](#ff3300) [Failed to unpack user data as the snapshot is invalid or corrupt.](#ff7e5e) [(Details…)](gray show_text=&7⚠ %1%)'
|
||||||
error_no_permission: '[Error:](#ff3300) [このコマンドを実行する権限がありません](#ff7e5e)'
|
error_no_permission: '[Error:](#ff3300) [このコマンドを実行する権限がありません](#ff7e5e)'
|
||||||
error_console_command_only: '[Error:](#ff3300) [そのコマンドは%1%コンソールからのみ実行できます](#ff7e5e)'
|
error_console_command_only: '[Error:](#ff3300) [そのコマンドは%1%コンソールからのみ実行できます](#ff7e5e)'
|
||||||
error_in_game_command_only: 'Error: そのコマンドはゲーム内でしか使えません。'
|
error_in_game_command_only: 'Error: そのコマンドはゲーム内でしか使えません。'
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ locales:
|
|||||||
data_manager_advancements_preview_remaining: '외 %1%개...'
|
data_manager_advancements_preview_remaining: '외 %1%개...'
|
||||||
data_list_title: '[%1%님의 유저 데이터 스냅샷 목록:](#00fb9a) [(%2%-%3% 중](#00fb9a) [%4%](#00fb9a bold)[)](#00fb9a)\n'
|
data_list_title: '[%1%님의 유저 데이터 스냅샷 목록:](#00fb9a) [(%2%-%3% 중](#00fb9a) [%4%](#00fb9a bold)[)](#00fb9a)\n'
|
||||||
data_list_item: '[%1%](gray show_text=&7%2%&7님의 유저 데이터 스냅샷&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_list_item: '[%1%](gray show_text=&7%2%&7님의 유저 데이터 스냅샷&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_list_item_invalid: '[%1%](dark_gray show_text=&7User Data Snapshot for %2%\n&8⚡ %4% suggest_command=/userdata delete %2% %3%) [%5%](dark_gray show_text=&7Pinned:\n&8Pinned snapshots won''t be automatically rotated. suggest_command=/userdata delete %2% %3%) [%6% ⚑ %8% ⏏ %9%](gray strikethrough show_text=&#ff3300&Invalid Data Snapshot\n&#ff7e5e&Click to delete\n\n&7⚠ %10% suggest_command=/userdata delete %2% %3%)'
|
||||||
data_deleted: '[❌ 성공적으로](#00fb9a) [%3%](#00fb9a show_text=&7플레이어 UUID:\n&8%4%) [님의 유저 데이터 스냅샷](#00fb9a) [%1%](#00fb9a show_text=&7버전 UUID:\n&8%2%)[을 삭제하였습니다.](#00fb9a)'
|
data_deleted: '[❌ 성공적으로](#00fb9a) [%3%](#00fb9a show_text=&7플레이어 UUID:\n&8%4%) [님의 유저 데이터 스냅샷](#00fb9a) [%1%](#00fb9a show_text=&7버전 UUID:\n&8%2%)[을 삭제하였습니다.](#00fb9a)'
|
||||||
data_restored: '[⏪ 성공적으로 복구되었습니다.](#00fb9a) [%1%](#00fb9a show_text=&7플레이어 UUID:\n&8%2%)[님의 현재 유저 데이터 스냅샷이](#00fb9a) [%3%](#00fb9a show_text=&7버전 UUID:\n&8%4%)[으로 변경되었습니다.](#00fb9a)'
|
data_restored: '[⏪ 성공적으로 복구되었습니다.](#00fb9a) [%1%](#00fb9a show_text=&7플레이어 UUID:\n&8%2%)[님의 현재 유저 데이터 스냅샷이](#00fb9a) [%3%](#00fb9a show_text=&7버전 UUID:\n&8%4%)[으로 변경되었습니다.](#00fb9a)'
|
||||||
data_pinned: '[※ 성공적으로](#00fb9a) [%3%](#00fb9a show_text=&7플레이어 UUID:\n&8%4%)[님의 유저 데이터 스냅샷](#00fb9a) [%1%](#00fb9a show_text=&7버전 UUID:\n&8%2%)[을 고정하였습니다.](#00fb9a)'
|
data_pinned: '[※ 성공적으로](#00fb9a) [%3%](#00fb9a show_text=&7플레이어 UUID:\n&8%4%)[님의 유저 데이터 스냅샷](#00fb9a) [%1%](#00fb9a show_text=&7버전 UUID:\n&8%2%)[을 고정하였습니다.](#00fb9a)'
|
||||||
@@ -52,6 +53,7 @@ locales:
|
|||||||
system_status_header: '[HuskSync](#00fb9a bold) [| System status report:](#00fb9a)'
|
system_status_header: '[HuskSync](#00fb9a bold) [| System status report:](#00fb9a)'
|
||||||
error_invalid_syntax: '[오류:](#ff3300) [잘못된 사용법. 사용법:](#ff7e5e) [%1%](#ff7e5e italic show_text=&#ff7e5e&클릭하여 입력할 수 있습니다. suggest_command=%1%)'
|
error_invalid_syntax: '[오류:](#ff3300) [잘못된 사용법. 사용법:](#ff7e5e) [%1%](#ff7e5e italic show_text=&#ff7e5e&클릭하여 입력할 수 있습니다. suggest_command=%1%)'
|
||||||
error_invalid_player: '[오류:](#ff3300) [해당 이름의 사용자를 찾을 수 없습니다.](#ff7e5e)'
|
error_invalid_player: '[오류:](#ff3300) [해당 이름의 사용자를 찾을 수 없습니다.](#ff7e5e)'
|
||||||
|
error_invalid_data: '[Error:](#ff3300) [Failed to unpack user data as the snapshot is invalid or corrupt.](#ff7e5e) [(Details…)](gray show_text=&7⚠ %1%)'
|
||||||
error_no_permission: '[오류:](#ff3300) [해당 명령어를 사용할 권한이 없습니다.](#ff7e5e)'
|
error_no_permission: '[오류:](#ff3300) [해당 명령어를 사용할 권한이 없습니다.](#ff7e5e)'
|
||||||
error_console_command_only: '[오류:](#ff3300) [해당 명령어는 콘솔을 통해서만 사용할 수 있습니다.](#ff7e5e)'
|
error_console_command_only: '[오류:](#ff3300) [해당 명령어는 콘솔을 통해서만 사용할 수 있습니다.](#ff7e5e)'
|
||||||
error_in_game_command_only: '오류: 해당 명령어는 게임 내부에서만 사용할 수 있습니다.'
|
error_in_game_command_only: '오류: 해당 명령어는 게임 내부에서만 사용할 수 있습니다.'
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ locales:
|
|||||||
data_manager_advancements_preview_remaining: 'en %1% meer…'
|
data_manager_advancements_preview_remaining: 'en %1% meer…'
|
||||||
data_list_title: '[%1%''s momentopnamen van gebruikersgegevens:](#00fb9a) [(%2%-%3% van](#00fb9a) [%4%](#00fb9a bold)[)](#00fb9a)\n'
|
data_list_title: '[%1%''s momentopnamen van gebruikersgegevens:](#00fb9a) [(%2%-%3% van](#00fb9a) [%4%](#00fb9a bold)[)](#00fb9a)\n'
|
||||||
data_list_item: '[%1%](gray show_text=&7Gebruikersgegevens momentopname voor %2%&8⚡ %4% run_command=/userdata view %2% %3%) [%5%](#d8ff2b show_text=&7Vastgezet:\n&8Vastgezette momentopnamen worden niet automatisch gerouleerd. run_command=/userdata view %2% %3%) [%6%](color=#ffc43b-#f5c962 show_text=&7Versie tijdmarkering:&7\n&8Wanneer de data was opgeslagen\n&8%7% run_command=/userdata view %2% %3%) [⚑ %8%](#23a825-#36f539 show_text=&7Reden opslaan:\n&8Waarom de data is opgeslagen run_command=/userdata view %2% %3%) [⏏ %9%](color=#62a9f5-#7ab8fa show_text=&7Grootte van momentopname:&7\n&8Geschatte bestandsgrootte van de momentopname (in KiB) run_command=/userdata view %2% %3%)'
|
data_list_item: '[%1%](gray show_text=&7Gebruikersgegevens momentopname voor %2%&8⚡ %4% run_command=/userdata view %2% %3%) [%5%](#d8ff2b show_text=&7Vastgezet:\n&8Vastgezette momentopnamen worden niet automatisch gerouleerd. run_command=/userdata view %2% %3%) [%6%](color=#ffc43b-#f5c962 show_text=&7Versie tijdmarkering:&7\n&8Wanneer de data was opgeslagen\n&8%7% run_command=/userdata view %2% %3%) [⚑ %8%](#23a825-#36f539 show_text=&7Reden opslaan:\n&8Waarom de data is opgeslagen run_command=/userdata view %2% %3%) [⏏ %9%](color=#62a9f5-#7ab8fa show_text=&7Grootte van momentopname:&7\n&8Geschatte bestandsgrootte van de momentopname (in KiB) run_command=/userdata view %2% %3%)'
|
||||||
|
data_list_item_invalid: '[%1%](dark_gray show_text=&7User Data Snapshot for %2%\n&8⚡ %4% suggest_command=/userdata delete %2% %3%) [%5%](dark_gray show_text=&7Pinned:\n&8Pinned snapshots won''t be automatically rotated. suggest_command=/userdata delete %2% %3%) [%6% ⚑ %8% ⏏ %9%](gray strikethrough show_text=&#ff3300&Invalid Data Snapshot\n&#ff7e5e&Click to delete\n\n&7⚠ %10% suggest_command=/userdata delete %2% %3%)'
|
||||||
data_deleted: '[❌ Momentopname van gebruikersgegevens is verwijderd](#00fb9a) [%1%](#00fb9a show_text=&7Versie UUID:\n&8%2%) [voor](#00fb9a) [%3%.](#00fb9a show_text=&7Speler UUID:\n&8%4%)'
|
data_deleted: '[❌ Momentopname van gebruikersgegevens is verwijderd](#00fb9a) [%1%](#00fb9a show_text=&7Versie UUID:\n&8%2%) [voor](#00fb9a) [%3%.](#00fb9a show_text=&7Speler UUID:\n&8%4%)'
|
||||||
data_restored: '[⏪ Succesvol hersteld](#00fb9a) [%1%](#00fb9a show_text=&7Speler UUID:\n&8%2%)[''s huidige gebruikersgegevens uit momentopname](#00fb9a) [%3%.](#00fb9a show_text=&7Versie UUID:\n&8%4%)'
|
data_restored: '[⏪ Succesvol hersteld](#00fb9a) [%1%](#00fb9a show_text=&7Speler UUID:\n&8%2%)[''s huidige gebruikersgegevens uit momentopname](#00fb9a) [%3%.](#00fb9a show_text=&7Versie UUID:\n&8%4%)'
|
||||||
data_pinned: '[※ Momentopname van gebruikersgegevens is vastgezet](#00fb9a) [%1%](#00fb9a show_text=&7Versie UUID:\n&8%2%) [voor](#00fb9a) [%3%.](#00fb9a show_text=&7Speler UUID:\n&8%4%)'
|
data_pinned: '[※ Momentopname van gebruikersgegevens is vastgezet](#00fb9a) [%1%](#00fb9a show_text=&7Versie UUID:\n&8%2%) [voor](#00fb9a) [%3%.](#00fb9a show_text=&7Speler UUID:\n&8%4%)'
|
||||||
@@ -52,6 +53,7 @@ locales:
|
|||||||
system_status_header: '[HuskSync](#00fb9a bold) [| System status report:](#00fb9a)'
|
system_status_header: '[HuskSync](#00fb9a bold) [| System status report:](#00fb9a)'
|
||||||
error_invalid_syntax: '[Error:](#ff3300) [Onjuiste syntaxis. Gebruik:](#ff7e5e) [%1%](#ff7e5e italic show_text=&#ff7e5e&Click to suggest suggest_command=%1%)'
|
error_invalid_syntax: '[Error:](#ff3300) [Onjuiste syntaxis. Gebruik:](#ff7e5e) [%1%](#ff7e5e italic show_text=&#ff7e5e&Click to suggest suggest_command=%1%)'
|
||||||
error_invalid_player: '[Error:](#ff3300) [Kan geen speler met die naam vinden.](#ff7e5e)'
|
error_invalid_player: '[Error:](#ff3300) [Kan geen speler met die naam vinden.](#ff7e5e)'
|
||||||
|
error_invalid_data: '[Error:](#ff3300) [Failed to unpack user data as the snapshot is invalid or corrupt.](#ff7e5e) [(Details…)](gray show_text=&7⚠ %1%)'
|
||||||
error_no_permission: '[Error:](#ff3300) [Je hebt geen toestemming om deze opdracht uit te voeren](#ff7e5e)'
|
error_no_permission: '[Error:](#ff3300) [Je hebt geen toestemming om deze opdracht uit te voeren](#ff7e5e)'
|
||||||
error_console_command_only: '[Error:](#ff3300) [Dat command kan alleen via de console worden uitgevoerd](#ff7e5e)'
|
error_console_command_only: '[Error:](#ff3300) [Dat command kan alleen via de console worden uitgevoerd](#ff7e5e)'
|
||||||
error_in_game_command_only: 'Error: Dat command kan alleen in-game worden gebruikt.'
|
error_in_game_command_only: 'Error: Dat command kan alleen in-game worden gebruikt.'
|
||||||
|
|||||||
@@ -21,7 +21,8 @@ locales:
|
|||||||
data_manager_system_buttons: '[System:](gray) [[⏷ File Dump…]](dark_gray show_text=&7Click to dump this raw user data snapshot to a file.\n&8Data dumps can be found in ~/plugins/HuskSync/dumps/ run_command=/husksync:userdata dump %1% %2% file) [[☂ Web Dump…]](dark_gray show_text=&7Click to dump this raw user data snapshot to the mc-logs service\n&8You will be provided with a URL containing the data. run_command=/husksync:userdata dump %1% %2% web)'
|
data_manager_system_buttons: '[System:](gray) [[⏷ File Dump…]](dark_gray show_text=&7Click to dump this raw user data snapshot to a file.\n&8Data dumps can be found in ~/plugins/HuskSync/dumps/ run_command=/husksync:userdata dump %1% %2% file) [[☂ Web Dump…]](dark_gray show_text=&7Click to dump this raw user data snapshot to the mc-logs service\n&8You will be provided with a URL containing the data. run_command=/husksync:userdata dump %1% %2% web)'
|
||||||
data_manager_advancements_preview_remaining: 'e %1% mais…'
|
data_manager_advancements_preview_remaining: 'e %1% mais…'
|
||||||
data_list_title: '[%1%''s user data snapshots:](#00fb9a) [(%2%-%3% of](#00fb9a) [%4%](#00fb9a bold)[)](#00fb9a)\n'
|
data_list_title: '[%1%''s user data snapshots:](#00fb9a) [(%2%-%3% of](#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_list_item: '[%1%](gray show_text=&7User Data Snapshot for %2%\n&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_list_item_invalid: '[%1%](dark_gray show_text=&7User Data Snapshot for %2%\n&8⚡ %4% suggest_command=/userdata delete %2% %3%) [%5%](dark_gray show_text=&7Pinned:\n&8Pinned snapshots won''t be automatically rotated. suggest_command=/userdata delete %2% %3%) [%6% ⚑ %8% ⏏ %9%](gray strikethrough show_text=&#ff3300&Invalid Data Snapshot\n&#ff7e5e&Click to delete\n\n&7⚠ %10% suggest_command=/userdata delete %2% %3%)'
|
||||||
data_deleted: '[❌ Snapshot de dados do usuário deletada com sucesso](#00fb9a) [%1%](#00fb9a show_text=&7Version UUID:\n&8%2%) [for](#00fb9a) [%3%.](#00fb9a show_text=&7Player UUID:\n&8%4%)'
|
data_deleted: '[❌ Snapshot de dados do usuário deletada com sucesso](#00fb9a) [%1%](#00fb9a show_text=&7Version UUID:\n&8%2%) [for](#00fb9a) [%3%.](#00fb9a show_text=&7Player UUID:\n&8%4%)'
|
||||||
data_restored: '[⏪ Restaurada com sucesso](#00fb9a) [%1%](#00fb9a show_text=&7Player UUID:\n&8%2%)[''s current user data from snapshot](#00fb9a) [%3%.](#00fb9a show_text=&7Version UUID:\n&8%4%)'
|
data_restored: '[⏪ Restaurada com sucesso](#00fb9a) [%1%](#00fb9a show_text=&7Player UUID:\n&8%2%)[''s current user data from snapshot](#00fb9a) [%3%.](#00fb9a show_text=&7Version UUID:\n&8%4%)'
|
||||||
data_pinned: '[※ Snapshot de dados do usuário marcada com sucesso](#00fb9a) [%1%](#00fb9a show_text=&7Version UUID:\n&8%2%) [for](#00fb9a) [%3%.](#00fb9a show_text=&7Player UUID:\n&8%4%)'
|
data_pinned: '[※ Snapshot de dados do usuário marcada com sucesso](#00fb9a) [%1%](#00fb9a show_text=&7Version UUID:\n&8%2%) [for](#00fb9a) [%3%.](#00fb9a show_text=&7Player UUID:\n&8%4%)'
|
||||||
@@ -52,6 +53,7 @@ locales:
|
|||||||
system_status_header: '[HuskSync](#00fb9a bold) [| System status report:](#00fb9a)'
|
system_status_header: '[HuskSync](#00fb9a bold) [| System status report:](#00fb9a)'
|
||||||
error_invalid_syntax: '[Error:](#ff3300) [Sintaxe incorreta. Utilize:](#ff7e5e) [%1%](#ff7e5e italic show_text=&#ff7e5e&Click to suggest suggest_command=%1%)'
|
error_invalid_syntax: '[Error:](#ff3300) [Sintaxe incorreta. Utilize:](#ff7e5e) [%1%](#ff7e5e italic show_text=&#ff7e5e&Click to suggest suggest_command=%1%)'
|
||||||
error_invalid_player: '[Error:](#ff3300) [Não foi possível encontrar um jogador com esse nome.](#ff7e5e)'
|
error_invalid_player: '[Error:](#ff3300) [Não foi possível encontrar um jogador com esse nome.](#ff7e5e)'
|
||||||
|
error_invalid_data: '[Error:](#ff3300) [Failed to unpack user data as the snapshot is invalid or corrupt.](#ff7e5e) [(Details…)](gray show_text=&7⚠ %1%)'
|
||||||
error_no_permission: '[Error:](#ff3300) [Você não tem permissão para executar este comando](#ff7e5e)'
|
error_no_permission: '[Error:](#ff3300) [Você não tem permissão para executar este comando](#ff7e5e)'
|
||||||
error_console_command_only: '[Error:](#ff3300) [Esse comando só pode ser executado através do console](#ff7e5e)'
|
error_console_command_only: '[Error:](#ff3300) [Esse comando só pode ser executado através do console](#ff7e5e)'
|
||||||
error_in_game_command_only: 'Error: Esse comando só pode ser usado dentro do jogo.'
|
error_in_game_command_only: 'Error: Esse comando só pode ser usado dentro do jogo.'
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ locales:
|
|||||||
data_manager_advancements_preview_remaining: 'и еще %1%…'
|
data_manager_advancements_preview_remaining: 'и еще %1%…'
|
||||||
data_list_title: '[Снимки данных %1%:](#00fb9a) [(%2%-%3% из](#00fb9a) [%4%](#00fb9a bold)[)](#00fb9a)\n'
|
data_list_title: '[Снимки данных %1%:](#00fb9a) [(%2%-%3% из](#00fb9a) [%4%](#00fb9a bold)[)](#00fb9a)\n'
|
||||||
data_list_item: '[%1%](gray show_text=&7Снимок данных %4% пользователя %2%&8⚡ 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Предполагаемый размер снимка (в килобайтах) run_command=/userdata view %2% %3%)'
|
data_list_item: '[%1%](gray show_text=&7Снимок данных %4% пользователя %2%&8⚡ 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Предполагаемый размер снимка (в килобайтах) run_command=/userdata view %2% %3%)'
|
||||||
|
data_list_item_invalid: '[%1%](dark_gray show_text=&7User Data Snapshot for %2%\n&8⚡ %4% suggest_command=/userdata delete %2% %3%) [%5%](dark_gray show_text=&7Pinned:\n&8Pinned snapshots won''t be automatically rotated. suggest_command=/userdata delete %2% %3%) [%6% ⚑ %8% ⏏ %9%](gray strikethrough show_text=&#ff3300&Invalid Data Snapshot\n&#ff7e5e&Click to delete\n\n&7⚠ %10% suggest_command=/userdata delete %2% %3%)'
|
||||||
data_deleted: '[❌ Снимок данных](#00fb9a) [%1%](#00fb9a show_text=&7UUID снимка:\n&8%2%) [пользователя](#00fb9a) [%3%](#00fb9a show_text=&7UUID игрока:\n&8%4%) [удален.](#00fb9a)'
|
data_deleted: '[❌ Снимок данных](#00fb9a) [%1%](#00fb9a show_text=&7UUID снимка:\n&8%2%) [пользователя](#00fb9a) [%3%](#00fb9a show_text=&7UUID игрока:\n&8%4%) [удален.](#00fb9a)'
|
||||||
data_restored: '[⏪ Данные пользователя](#00fb9a) [%1%](#00fb9a show_text=&7UUID игрока:\n&8%2%) [из снимка](#00fb9a) [%3%](#00fb9a show_text=&7UUID снимка:\n&8%4%) [успешно восстановлены.](#00fb9a)'
|
data_restored: '[⏪ Данные пользователя](#00fb9a) [%1%](#00fb9a show_text=&7UUID игрока:\n&8%2%) [из снимка](#00fb9a) [%3%](#00fb9a show_text=&7UUID снимка:\n&8%4%) [успешно восстановлены.](#00fb9a)'
|
||||||
data_pinned: '[※ Снимок данных](#00fb9a) [%1%](#00fb9a show_text=&7UUID снимка:\n&8%2%) [пользователя](#00fb9a) [%3%](#00fb9a show_text=&7UUID игрока:\n&8%4%) [успешно закреплен.](#00fb9a)'
|
data_pinned: '[※ Снимок данных](#00fb9a) [%1%](#00fb9a show_text=&7UUID снимка:\n&8%2%) [пользователя](#00fb9a) [%3%](#00fb9a show_text=&7UUID игрока:\n&8%4%) [успешно закреплен.](#00fb9a)'
|
||||||
@@ -52,6 +53,7 @@ locales:
|
|||||||
system_status_header: '[HuskSync](#00fb9a bold) [| System status report:](#00fb9a)'
|
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_syntax: '[Ошибка:](#ff3300) [Неправильный синтаксис. Используйте:](#ff7e5e) [%1%](#ff7e5e italic show_text=&#ff7e5e&Click to suggest suggest_command=%1%)'
|
||||||
error_invalid_player: '[Ошибка:](#ff3300) [Не удалось найти игрока с данным именем.](#ff7e5e)'
|
error_invalid_player: '[Ошибка:](#ff3300) [Не удалось найти игрока с данным именем.](#ff7e5e)'
|
||||||
|
error_invalid_data: '[Error:](#ff3300) [Failed to unpack user data as the snapshot is invalid or corrupt.](#ff7e5e) [(Details…)](gray show_text=&7⚠ %1%)'
|
||||||
error_no_permission: '[Ошибка:](#ff3300) [У вас недостаточно прав для выполнения данной команды.](#ff7e5e)'
|
error_no_permission: '[Ошибка:](#ff3300) [У вас недостаточно прав для выполнения данной команды.](#ff7e5e)'
|
||||||
error_console_command_only: '[Ошибка:](#ff3300) [Данная команда может быть выполнена только из консоли.](#ff7e5e)'
|
error_console_command_only: '[Ошибка:](#ff3300) [Данная команда может быть выполнена только из консоли.](#ff7e5e)'
|
||||||
error_in_game_command_only: 'Ошибка: Данная команда может быть выполнена только в игре.'
|
error_in_game_command_only: 'Ошибка: Данная команда может быть выполнена только в игре.'
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ locales:
|
|||||||
data_manager_advancements_preview_remaining: 've %1% daha fazla…'
|
data_manager_advancements_preview_remaining: 've %1% daha fazla…'
|
||||||
data_list_title: '[%1%''ın kullanıcı veri anlıkları:](#00fb9a) [(%2%-%3% /](#00fb9a) [%4%](#00fb9a bold)[)](#00fb9a)\n'
|
data_list_title: '[%1%''ın kullanıcı veri anlıkları:](#00fb9a) [(%2%-%3% /](#00fb9a) [%4%](#00fb9a bold)[)](#00fb9a)\n'
|
||||||
data_list_item: '[%1%](gray show_text=&7Oyuncu Veri Anlığı %2% için %3%&8⚡ %4% run_command=/userdata view %2% %3%) [%5%](#d8ff2b show_text=&7Sabitlendi:\n&8Sabitlenmiş anlıklar otomatik olarak döndürülmez. run_command=/userdata view %2% %3%) [%6%](color=#ffc43b-#f5c962 show_text=&7Versiyon zaman damgası:&7\n&8Verinin ne zaman kaydedildiği\n&8%7% run_command=/userdata view %2% %3%) [⚑ %8%](#23a825-#36f539 show_text=&7Kaydetme sebebi:\n&8Verinin kaydedilme nedeni run_command=/userdata view %2% %3%) [⏏ %9%](color=#62a9f5-#7ab8fa show_text=&7Anlık boyutu:&7\n&8Anlının tahmini dosya boyutu (KiB cinsinden) run_command=/userdata view %2% %3%)'
|
data_list_item: '[%1%](gray show_text=&7Oyuncu Veri Anlığı %2% için %3%&8⚡ %4% run_command=/userdata view %2% %3%) [%5%](#d8ff2b show_text=&7Sabitlendi:\n&8Sabitlenmiş anlıklar otomatik olarak döndürülmez. run_command=/userdata view %2% %3%) [%6%](color=#ffc43b-#f5c962 show_text=&7Versiyon zaman damgası:&7\n&8Verinin ne zaman kaydedildiği\n&8%7% run_command=/userdata view %2% %3%) [⚑ %8%](#23a825-#36f539 show_text=&7Kaydetme sebebi:\n&8Verinin kaydedilme nedeni run_command=/userdata view %2% %3%) [⏏ %9%](color=#62a9f5-#7ab8fa show_text=&7Anlık boyutu:&7\n&8Anlının tahmini dosya boyutu (KiB cinsinden) run_command=/userdata view %2% %3%)'
|
||||||
|
data_list_item_invalid: '[%1%](dark_gray show_text=&7User Data Snapshot for %2%\n&8⚡ %4% suggest_command=/userdata delete %2% %3%) [%5%](dark_gray show_text=&7Pinned:\n&8Pinned snapshots won''t be automatically rotated. suggest_command=/userdata delete %2% %3%) [%6% ⚑ %8% ⏏ %9%](gray strikethrough show_text=&#ff3300&Invalid Data Snapshot\n&#ff7e5e&Click to delete\n\n&7⚠ %10% suggest_command=/userdata delete %2% %3%)'
|
||||||
data_deleted: '[❌ Kullanıcı veri anlığı başarıyla silindi](#00fb9a) [%1%](#00fb9a show_text=&7Versiyon UUID:\n&8%2%) [için](#00fb9a) [%3%.](#00fb9a show_text=&7Oyuncu UUID:\n&8%4%)'
|
data_deleted: '[❌ Kullanıcı veri anlığı başarıyla silindi](#00fb9a) [%1%](#00fb9a show_text=&7Versiyon UUID:\n&8%2%) [için](#00fb9a) [%3%.](#00fb9a show_text=&7Oyuncu UUID:\n&8%4%)'
|
||||||
data_restored: '[⏪ Başarıyla geri yüklendi](#00fb9a) [%1%](#00fb9a show_text=&7Oyuncu UUID:\n&8%2%)[''ın mevcut kullanıcı verisi anlığından](#00fb9a) [%3%.](#00fb9a show_text=&7Versiyon UUID:\n&8%4%)'
|
data_restored: '[⏪ Başarıyla geri yüklendi](#00fb9a) [%1%](#00fb9a show_text=&7Oyuncu UUID:\n&8%2%)[''ın mevcut kullanıcı verisi anlığından](#00fb9a) [%3%.](#00fb9a show_text=&7Versiyon UUID:\n&8%4%)'
|
||||||
data_pinned: '[※ Kullanıcı veri anlığı başarıyla sabitlendi](#00fb9a) [%1%](#00fb9a show_text=&7Versiyon UUID:\n&8%2%) [için](#00fb9a) [%3%.](#00fb9a show_text=&7Oyuncu UUID:\n&8%4%)'
|
data_pinned: '[※ Kullanıcı veri anlığı başarıyla sabitlendi](#00fb9a) [%1%](#00fb9a show_text=&7Versiyon UUID:\n&8%2%) [için](#00fb9a) [%3%.](#00fb9a show_text=&7Oyuncu UUID:\n&8%4%)'
|
||||||
@@ -52,6 +53,7 @@ locales:
|
|||||||
system_status_header: '[HuskSync](#00fb9a bold) [| Sistem durumu raporu:](#00fb9a)'
|
system_status_header: '[HuskSync](#00fb9a bold) [| Sistem durumu raporu:](#00fb9a)'
|
||||||
error_invalid_syntax: '[Hata:](#ff3300) [Yanlış sözdizimi. Kullanım:](#ff7e5e) [%1%](#ff7e5e italic show_text=&#ff7e5e&Öneri için tıklayın Suggest_command=%1%)'
|
error_invalid_syntax: '[Hata:](#ff3300) [Yanlış sözdizimi. Kullanım:](#ff7e5e) [%1%](#ff7e5e italic show_text=&#ff7e5e&Öneri için tıklayın Suggest_command=%1%)'
|
||||||
error_invalid_player: '[Hata:](#ff3300) [Bu isimde bir oyuncu bulunamadı.](#ff7e5e)'
|
error_invalid_player: '[Hata:](#ff3300) [Bu isimde bir oyuncu bulunamadı.](#ff7e5e)'
|
||||||
|
error_invalid_data: '[Error:](#ff3300) [Failed to unpack user data as the snapshot is invalid or corrupt.](#ff7e5e) [(Details…)](gray show_text=&7⚠ %1%)'
|
||||||
error_no_permission: '[Hata:](#ff3300) [Bu komutu gerçekleştirmek için izniniz yok](#ff7e5e)'
|
error_no_permission: '[Hata:](#ff3300) [Bu komutu gerçekleştirmek için izniniz yok](#ff7e5e)'
|
||||||
error_console_command_only: '[Hata:](#ff3300) [Bu komut yalnızca konsoldan çalıştırılabilir](#ff7e5e)'
|
error_console_command_only: '[Hata:](#ff3300) [Bu komut yalnızca konsoldan çalıştırılabilir](#ff7e5e)'
|
||||||
error_in_game_command_only: 'Hata: Bu komut yalnızca oyun içinde kullanılabilir.'
|
error_in_game_command_only: 'Hata: Bu komut yalnızca oyun içinde kullanılabilir.'
|
||||||
|
|||||||
@@ -21,7 +21,8 @@ locales:
|
|||||||
data_manager_system_buttons: '[System:](gray) [[⏷ File Dump…]](dark_gray show_text=&7Click to dump this raw user data snapshot to a file.\n&8Data dumps can be found in ~/plugins/HuskSync/dumps/ run_command=/husksync:userdata dump %1% %2% file) [[☂ Web Dump…]](dark_gray show_text=&7Click to dump this raw user data snapshot to the mc-logs service\n&8You will be provided with a URL containing the data. run_command=/husksync:userdata dump %1% %2% web)'
|
data_manager_system_buttons: '[System:](gray) [[⏷ File Dump…]](dark_gray show_text=&7Click to dump this raw user data snapshot to a file.\n&8Data dumps can be found in ~/plugins/HuskSync/dumps/ run_command=/husksync:userdata dump %1% %2% file) [[☂ Web Dump…]](dark_gray show_text=&7Click to dump this raw user data snapshot to the mc-logs service\n&8You will be provided with a URL containing the data. run_command=/husksync:userdata dump %1% %2% web)'
|
||||||
data_manager_advancements_preview_remaining: 'and %1% more…'
|
data_manager_advancements_preview_remaining: 'and %1% more…'
|
||||||
data_list_title: '[%1%''s user data snapshots:](#00fb9a) [(%2%-%3% of](#00fb9a) [%4%](#00fb9a bold)[)](#00fb9a)\n'
|
data_list_title: '[%1%''s user data snapshots:](#00fb9a) [(%2%-%3% of](#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_list_item: '[%1%](gray show_text=&7User Data Snapshot for %2%\n&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_list_item_invalid: '[%1%](dark_gray show_text=&7User Data Snapshot for %2%\n&8⚡ %4% suggest_command=/userdata delete %2% %3%) [%5%](dark_gray show_text=&7Pinned:\n&8Pinned snapshots won''t be automatically rotated. suggest_command=/userdata delete %2% %3%) [%6% ⚑ %8% ⏏ %9%](gray strikethrough show_text=&#ff3300&Invalid Data Snapshot\n&#ff7e5e&Click to delete\n\n&7⚠ %10% suggest_command=/userdata delete %2% %3%)'
|
||||||
data_deleted: '[❌ Successfully deleted user data snapshot](#00fb9a) [%1%](#00fb9a show_text=&7Version UUID:\n&8%2%) [for](#00fb9a) [%3%.](#00fb9a show_text=&7Player UUID:\n&8%4%)'
|
data_deleted: '[❌ Successfully deleted user data snapshot](#00fb9a) [%1%](#00fb9a show_text=&7Version UUID:\n&8%2%) [for](#00fb9a) [%3%.](#00fb9a show_text=&7Player UUID:\n&8%4%)'
|
||||||
data_restored: '[⏪ Successfully restored](#00fb9a) [%1%](#00fb9a show_text=&7Player UUID:\n&8%2%)[''s current user data from snapshot](#00fb9a) [%3%.](#00fb9a show_text=&7Version UUID:\n&8%4%)'
|
data_restored: '[⏪ Successfully restored](#00fb9a) [%1%](#00fb9a show_text=&7Player UUID:\n&8%2%)[''s current user data from snapshot](#00fb9a) [%3%.](#00fb9a show_text=&7Version UUID:\n&8%4%)'
|
||||||
data_pinned: '[※ Successfully pinned user data snapshot](#00fb9a) [%1%](#00fb9a show_text=&7Version UUID:\n&8%2%) [for](#00fb9a) [%3%.](#00fb9a show_text=&7Player UUID:\n&8%4%)'
|
data_pinned: '[※ Successfully pinned user data snapshot](#00fb9a) [%1%](#00fb9a show_text=&7Version UUID:\n&8%2%) [for](#00fb9a) [%3%.](#00fb9a show_text=&7Player UUID:\n&8%4%)'
|
||||||
@@ -52,6 +53,7 @@ locales:
|
|||||||
system_status_header: '[HuskSync](#00fb9a bold) [| System status report:](#00fb9a)'
|
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_syntax: '[Помилка:](#ff3300) [Неправильний синтакс. Використання:](#ff7e5e) [%1%](#ff7e5e italic show_text=&#ff7e5e&Click to suggest suggest_command=%1%)'
|
||||||
error_invalid_player: '[Помилка:](#ff3300) [Гравця не знайдено](#ff7e5e)'
|
error_invalid_player: '[Помилка:](#ff3300) [Гравця не знайдено](#ff7e5e)'
|
||||||
|
error_invalid_data: '[Error:](#ff3300) [Failed to unpack user data as the snapshot is invalid or corrupt.](#ff7e5e) [(Details…)](gray show_text=&7⚠ %1%)'
|
||||||
error_no_permission: '[Помилка:](#ff3300) [Ввас немає дозволу на використання цієї команди](#ff7e5e)'
|
error_no_permission: '[Помилка:](#ff3300) [Ввас немає дозволу на використання цієї команди](#ff7e5e)'
|
||||||
error_console_command_only: '[Помилка:](#ff3300) [Ця команда може бути використана лише з допомогою %1% консолі](#ff7e5e)'
|
error_console_command_only: '[Помилка:](#ff3300) [Ця команда може бути використана лише з допомогою %1% консолі](#ff7e5e)'
|
||||||
error_in_game_command_only: 'Error: That command can only be used in-game.'
|
error_in_game_command_only: 'Error: That command can only be used in-game.'
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ locales:
|
|||||||
data_manager_advancements_preview_remaining: '以及其他 %1%…'
|
data_manager_advancements_preview_remaining: '以及其他 %1%…'
|
||||||
data_list_title: '[%1%的玩家数据备份:](#00fb9a) [(%2%-%3% of](#00fb9a) [%4%](#00fb9a bold)[)](#00fb9a)\n'
|
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_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_list_item_invalid: '[%1%](dark_gray show_text=&7%2%的用户数据快照\n&8⚡ %4% suggest_command=/userdata delete %2% %3%) [%5%](dark_gray show_text=&7置顶:\n&8已置顶的快照不会自动排序. suggest_command=/userdata delete %2% %3%) [%6% ⚑ %8% ⏏ %9%](gray strikethrough show_text=&#ff3300&无效的快照数据\n&#ff7e5e&点击删除\n\n&7⚠ %10% suggest_command=/userdata delete %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_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_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_pinned: '[※ 成功置顶玩家](#00fb9a) [%3%](#00fb9a show_text=&7玩家 UUID:\n&8%4%) [的数据备份](#00fb9a) [%1%.](#00fb9a show_text=&7备份版本UUID:\n&8%2%)'
|
||||||
@@ -52,6 +53,7 @@ locales:
|
|||||||
system_status_header: '[HuskSync](#00fb9a bold) [| 系统状态报告:](#00fb9a)'
|
system_status_header: '[HuskSync](#00fb9a bold) [| 系统状态报告:](#00fb9a)'
|
||||||
error_invalid_syntax: '[错误:](#ff3300) [语法错误.用法:](#ff7e5e) [%1%](#ff7e5e italic show_text=&#ff7e5e&点击建议 suggest_command=%1%)'
|
error_invalid_syntax: '[错误:](#ff3300) [语法错误.用法:](#ff7e5e) [%1%](#ff7e5e italic show_text=&#ff7e5e&点击建议 suggest_command=%1%)'
|
||||||
error_invalid_player: '[错误:](#ff3300) [找不到这个名称的玩家.](#ff7e5e)'
|
error_invalid_player: '[错误:](#ff3300) [找不到这个名称的玩家.](#ff7e5e)'
|
||||||
|
error_invalid_data: '[错误:](#ff3300) [无法解压缩快照数据, 因为它无效或已损坏.](#ff7e5e) [(详情…)](gray show_text=&7⚠ %1%)'
|
||||||
error_no_permission: '[错误:](#ff3300) [你没有执行此命令的权限](#ff7e5e)'
|
error_no_permission: '[错误:](#ff3300) [你没有执行此命令的权限](#ff7e5e)'
|
||||||
error_console_command_only: '[错误:](#ff3300) [该命令只能在控制台中运行](#ff7e5e)'
|
error_console_command_only: '[错误:](#ff3300) [该命令只能在控制台中运行](#ff7e5e)'
|
||||||
error_in_game_command_only: '错误: 该命令只能在游戏中使用.'
|
error_in_game_command_only: '错误: 该命令只能在游戏中使用.'
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ locales:
|
|||||||
data_manager_advancements_preview_remaining: '以及其他 %1%…'
|
data_manager_advancements_preview_remaining: '以及其他 %1%…'
|
||||||
data_list_title: '[%1%的玩家數據備份:](#00fb9a) [(%2%-%3% of](#00fb9a) [%4%](#00fb9a bold)[)](#00fb9a)\n'
|
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_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_list_item_invalid: '[%1%](dark_gray show_text=&7User Data Snapshot for %2%\n&8⚡ %4% suggest_command=/userdata delete %2% %3%) [%5%](dark_gray show_text=&7Pinned:\n&8Pinned snapshots won''t be automatically rotated. suggest_command=/userdata delete %2% %3%) [%6% ⚑ %8% ⏏ %9%](gray strikethrough show_text=&#ff3300&Invalid Data Snapshot\n&#ff7e5e&Click to delete\n\n&7⚠ %10% suggest_command=/userdata delete %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_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_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_pinned: '[※ 成功標記玩家](#00fb9a) [%3%](#00fb9a show_text=&7玩家 UUID:\n&8%4%) [的數據備份](#00fb9a) [%1%.](#00fb9a show_text=&7備份版本UUID:\n&8%2%)'
|
||||||
@@ -52,6 +53,7 @@ locales:
|
|||||||
system_status_header: '[HuskSync](#00fb9a bold) [| 系統狀態報告:](#00fb9a)'
|
system_status_header: '[HuskSync](#00fb9a bold) [| 系統狀態報告:](#00fb9a)'
|
||||||
error_invalid_syntax: '[錯誤:](#ff3300) [語法不正確,用法:](#ff7e5e) [%1%](#ff7e5e italic show_text=&#ff7e5e&點擊建議 suggest_command=%1%)'
|
error_invalid_syntax: '[錯誤:](#ff3300) [語法不正確,用法:](#ff7e5e) [%1%](#ff7e5e italic show_text=&#ff7e5e&點擊建議 suggest_command=%1%)'
|
||||||
error_invalid_player: '[錯誤:](#ff3300) [找不到這位玩家.](#ff7e5e)'
|
error_invalid_player: '[錯誤:](#ff3300) [找不到這位玩家.](#ff7e5e)'
|
||||||
|
error_invalid_data: '[Error:](#ff3300) [Failed to unpack user data as the snapshot is invalid or corrupt.](#ff7e5e) [(Details…)](gray show_text=&7⚠ %1%)'
|
||||||
error_no_permission: '[錯誤:](#ff3300) [您沒有權限執行這個指令](#ff7e5e)'
|
error_no_permission: '[錯誤:](#ff3300) [您沒有權限執行這個指令](#ff7e5e)'
|
||||||
error_console_command_only: '[錯誤:](#ff3300) [該指令只能透過 控制台 執行](#ff7e5e)'
|
error_console_command_only: '[錯誤:](#ff3300) [該指令只能透過 控制台 執行](#ff7e5e)'
|
||||||
error_in_game_command_only: '錯誤: 該指令只能在遊戲內執行.'
|
error_in_game_command_only: '錯誤: 該指令只能在遊戲內執行.'
|
||||||
|
|||||||
15
docs/API.md
15
docs/API.md
@@ -31,11 +31,12 @@ The HuskSync API is available for the following platforms:
|
|||||||
1. [API Introduction](#api-introduction)
|
1. [API Introduction](#api-introduction)
|
||||||
1. [Setup with Maven](#11-setup-with-maven)
|
1. [Setup with Maven](#11-setup-with-maven)
|
||||||
2. [Setup with Gradle](#12-setup-with-gradle)
|
2. [Setup with Gradle](#12-setup-with-gradle)
|
||||||
2. [Creating a class to interface with the API](#3-creating-a-class-to-interface-with-the-api)
|
2. [Adding HuskSync as a dependency](#2-adding-husksync-as-a-dependency)
|
||||||
3. [Checking if HuskSync is present and creating the hook](#4-checking-if-husksync-is-present-and-creating-the-hook)
|
3. [Creating a class to interface with the API](#3-creating-a-class-to-interface-with-the-api)
|
||||||
4. [Getting an instance of the API](#5-getting-an-instance-of-the-api)
|
4. [Checking if HuskSync is present and creating the hook](#4-checking-if-husksync-is-present-and-creating-the-hook)
|
||||||
5. [CompletableFuture and Optional basics](#6-completablefuture-and-optional-basics)
|
5. [Getting an instance of the API](#5-getting-an-instance-of-the-api)
|
||||||
6. [Next steps](#7-next-steps)
|
6. [CompletableFuture and Optional basics](#6-completablefuture-and-optional-basics)
|
||||||
|
7. [Next steps](#7-next-steps)
|
||||||
|
|
||||||
## API Introduction
|
## API Introduction
|
||||||
### 1.1 Setup with Maven
|
### 1.1 Setup with Maven
|
||||||
@@ -83,7 +84,7 @@ dependencies {
|
|||||||
```
|
```
|
||||||
</details>
|
</details>
|
||||||
|
|
||||||
### 2. Adding HuskSync as a dependency
|
## 2. Adding HuskSync as a dependency
|
||||||
- Add HuskSync to your `softdepend` (if you want to optionally use HuskSync) or `depend` (if your plugin relies on HuskSync) section in `plugin.yml` of your project.
|
- Add HuskSync to your `softdepend` (if you want to optionally use HuskSync) or `depend` (if your plugin relies on HuskSync) section in `plugin.yml` of your project.
|
||||||
|
|
||||||
```yaml
|
```yaml
|
||||||
@@ -146,7 +147,7 @@ public class HuskSyncAPIHook {
|
|||||||
## 6. CompletableFuture and Optional basics
|
## 6. CompletableFuture and Optional basics
|
||||||
- HuskSync's API methods often deal with `CompletableFuture`s and `Optional`s.
|
- HuskSync's API methods often deal with `CompletableFuture`s and `Optional`s.
|
||||||
- A `CompletableFuture` is an asynchronous callback mechanism. The method will be processed asynchronously and the data returned when it has been retrieved. Then, use `CompletableFuture#thenAccept(data -> {})` to do what you want to do with the `data` you requested after it has asynchronously been retrieved, to prevent lag.
|
- A `CompletableFuture` is an asynchronous callback mechanism. The method will be processed asynchronously and the data returned when it has been retrieved. Then, use `CompletableFuture#thenAccept(data -> {})` to do what you want to do with the `data` you requested after it has asynchronously been retrieved, to prevent lag.
|
||||||
- An `Optional` is a null-safe representation of data, or no data. You can check if the Optional is empty via `Optional#isEmpty()` (which will be returned by the API if no data could be found for the call you made). If the optional does contain data, you can get it via `Optional#get().
|
- An `Optional` is a null-safe representation of data, or no data. You can check if the Optional is empty via `Optional#isEmpty()` (which will be returned by the API if no data could be found for the call you made). If the optional does contain data, you can get it via `Optional#get()`.
|
||||||
|
|
||||||
> **Warning:** You should never call `#join()` on futures returned from the HuskSyncAPI as futures are processed on server asynchronous tasks, which could lead to thread deadlock and crash your server if you attempt to lock the main thread to process them.
|
> **Warning:** You should never call `#join()` on futures returned from the HuskSyncAPI as futures are processed on server asynchronous tasks, which could lead to thread deadlock and crash your server if you attempt to lock the main thread to process them.
|
||||||
|
|
||||||
|
|||||||
@@ -27,31 +27,38 @@ check_for_updates: true
|
|||||||
# Specify a common ID for grouping servers running HuskSync. Don't modify this unless you know what you're doing!
|
# Specify a common ID for grouping servers running HuskSync. Don't modify this unless you know what you're doing!
|
||||||
cluster_id: ''
|
cluster_id: ''
|
||||||
# Enable development debug logging
|
# Enable development debug logging
|
||||||
debug_logging: false
|
debug_logging: true
|
||||||
# Whether to provide modern, rich TAB suggestions for commands (if available)
|
# Whether to provide modern, rich TAB suggestions for commands (if available)
|
||||||
brigadier_tab_completion: false
|
brigadier_tab_completion: false
|
||||||
# Whether to enable the Player Analytics hook.
|
# Whether to enable the Player Analytics hook.
|
||||||
# Docs: https://william278.net/docs/husksync/plan-hook
|
# Docs: https://william278.net/docs/husksync/plan-hook
|
||||||
enable_plan_hook: true
|
enable_plan_hook: true
|
||||||
|
# Whether to cancel game event packets directly when handling locked players if ProtocolLib or PacketEvents is installed
|
||||||
|
cancel_packets: true
|
||||||
# Database settings
|
# Database settings
|
||||||
database:
|
database:
|
||||||
# Type of database to use (MYSQL, MARIADB)
|
# Type of database to use (MYSQL, MARIADB, POSTGRES, MONGO)
|
||||||
type: MYSQL
|
type: MYSQL
|
||||||
# Specify credentials here for your MYSQL or MARIADB database
|
# Specify credentials here for your MYSQL, MARIADB, POSTGRES OR MONGO database
|
||||||
credentials:
|
credentials:
|
||||||
host: localhost
|
host: localhost
|
||||||
port: 3306
|
port: 3306
|
||||||
database: HuskSync
|
database: minecraft
|
||||||
username: root
|
username: root
|
||||||
password: pa55w0rd
|
password: ''
|
||||||
|
# Only change this if you're using MARIADB or POSTGRES
|
||||||
parameters: ?autoReconnect=true&useSSL=false&useUnicode=true&characterEncoding=UTF-8
|
parameters: ?autoReconnect=true&useSSL=false&useUnicode=true&characterEncoding=UTF-8
|
||||||
# MYSQL / MARIADB database Hikari connection pool properties. Don't modify this unless you know what you're doing!
|
# MYSQL, MARIADB, POSTGRES database Hikari connection pool properties. Don't modify this unless you know what you're doing!
|
||||||
connection_pool:
|
connection_pool:
|
||||||
maximum_pool_size: 10
|
maximum_pool_size: 10
|
||||||
minimum_idle: 10
|
minimum_idle: 10
|
||||||
maximum_lifetime: 1800000
|
maximum_lifetime: 1800000
|
||||||
keepalive_time: 0
|
keepalive_time: 0
|
||||||
connection_timeout: 5000
|
connection_timeout: 5000
|
||||||
|
# Advanced MongoDB settings. Don't modify unless you know what you're doing!
|
||||||
|
mongo_settings:
|
||||||
|
using_atlas: false
|
||||||
|
parameters: ?retryWrites=true&w=majority&authSource=HuskSync
|
||||||
# Names of tables to use on your database. Don't modify this unless you know what you're doing!
|
# Names of tables to use on your database. Don't modify this unless you know what you're doing!
|
||||||
table_names:
|
table_names:
|
||||||
users: husksync_users
|
users: husksync_users
|
||||||
@@ -93,9 +100,9 @@ synchronization:
|
|||||||
# Configuration for how and when to sync player data when they die
|
# Configuration for how and when to sync player data when they die
|
||||||
save_on_death:
|
save_on_death:
|
||||||
# Whether to create a snapshot for users when they die (containing their death drops)
|
# Whether to create a snapshot for users when they die (containing their death drops)
|
||||||
enabled: false
|
enabled: true
|
||||||
# What items to save in death snapshots? (DROPS or ITEMS_TO_KEEP). Note that ITEMS_TO_KEEP (suggested for keepInventory servers) requires a Paper 1.19.4+ server.
|
# What items to save in death snapshots? (DROPS or ITEMS_TO_KEEP). Note that ITEMS_TO_KEEP (suggested for keepInventory servers) requires a Paper 1.19.4+ server.
|
||||||
items_to_save: DROPS
|
items_to_save: ITEMS_TO_KEEP
|
||||||
# Should a death snapshot still be created even if the items to save on the player's death are empty?
|
# Should a death snapshot still be created even if the items to save on the player's death are empty?
|
||||||
save_empty_items: true
|
save_empty_items: true
|
||||||
# Whether dead players who log out and log in to a different server should have their items saved.
|
# Whether dead players who log out and log in to a different server should have their items saved.
|
||||||
@@ -106,27 +113,30 @@ synchronization:
|
|||||||
notification_display_slot: ACTION_BAR
|
notification_display_slot: ACTION_BAR
|
||||||
# Persist maps locked in a Cartography Table to let them be viewed on any server
|
# Persist maps locked in a Cartography Table to let them be viewed on any server
|
||||||
persist_locked_maps: true
|
persist_locked_maps: true
|
||||||
# Whether to synchronize player max health (requires health syncing to be enabled)
|
|
||||||
synchronize_max_health: true
|
|
||||||
# If using the DELAY sync method, how long should this server listen for Redis key data updates before pulling data from the database instead (i.e., if the user did not change servers).
|
# If using the DELAY sync method, how long should this server listen for Redis key data updates before pulling data from the database instead (i.e., if the user did not change servers).
|
||||||
network_latency_milliseconds: 500
|
network_latency_milliseconds: 500
|
||||||
# Which data types to synchronize.
|
# Which data types to synchronize.
|
||||||
# Docs: https://william278.net/docs/husksync/sync-features
|
# Docs: https://william278.net/docs/husksync/sync-features
|
||||||
features:
|
features:
|
||||||
persistent_data: true
|
|
||||||
inventory: true
|
inventory: true
|
||||||
game_mode: true
|
|
||||||
advancements: true
|
|
||||||
experience: true
|
|
||||||
ender_chest: true
|
ender_chest: true
|
||||||
|
experience: true
|
||||||
|
advancements: true
|
||||||
|
game_mode: true
|
||||||
|
flight_status: true
|
||||||
potion_effects: true
|
potion_effects: true
|
||||||
location: false
|
|
||||||
statistics: true
|
statistics: true
|
||||||
health: true
|
health: true
|
||||||
hunger: true
|
hunger: true
|
||||||
|
attributes: true
|
||||||
|
persistent_data: true
|
||||||
|
location: false
|
||||||
# Commands which should be blocked before a player has finished syncing (Use * to block all commands)
|
# Commands which should be blocked before a player has finished syncing (Use * to block all commands)
|
||||||
blacklisted_commands_while_locked:
|
blacklisted_commands_while_locked:
|
||||||
- '*'
|
- '*'
|
||||||
|
# For attribute syncing, which attributes should be ignored/skipped when syncing
|
||||||
|
# (e.g. ['minecraft:generic.max_health', 'minecraft:generic.attack_damage'])
|
||||||
|
ignored_attributes: []
|
||||||
# Event priorities for listeners (HIGHEST, NORMAL, LOWEST). Change if you encounter plugin conflicts
|
# Event priorities for listeners (HIGHEST, NORMAL, LOWEST). Change if you encounter plugin conflicts
|
||||||
event_priorities:
|
event_priorities:
|
||||||
quit_listener: LOWEST
|
quit_listener: LOWEST
|
||||||
|
|||||||
@@ -164,8 +164,10 @@ huskSyncAPI.getCurrentData(user).thenAccept(optionalSnapshot -> {
|
|||||||
| `husksync:statistics` | User statistics | `#getStatistics` | `#setStatistics` |
|
| `husksync:statistics` | User statistics | `#getStatistics` | `#setStatistics` |
|
||||||
| `husksync:health` | User health | `#getHealth` | `#setHealth` |
|
| `husksync:health` | User health | `#getHealth` | `#setHealth` |
|
||||||
| `husksync:hunger` | User hunger, saturation & exhaustion | `#getHunger` | `#setHunger` |
|
| `husksync:hunger` | User hunger, saturation & exhaustion | `#getHunger` | `#setHunger` |
|
||||||
|
| `husksync:attributes` | User attributes | `#getAttributes` | `#setAttributes` |
|
||||||
| `husksync:experience` | User level, experience, and score | `#getExperience` | `#setExperience` |
|
| `husksync:experience` | User level, experience, and score | `#getExperience` | `#setExperience` |
|
||||||
| `husksync:game_mode` | User game mode and flight status | `#getGameMode` | `#setGameMode` |
|
| `husksync:game_mode` | User game mode | `#getGameMode` | `#setGameMode` |
|
||||||
|
| `husksync:flight_status` | User ability to fly/if flying now | `#getFlightStatus` | `#setFlightStatus` |
|
||||||
| `husksync:persistent_data` | User persistent data container | `#getPersistentData` | `#setPersistentData` |
|
| `husksync:persistent_data` | User persistent data container | `#getPersistentData` | `#setPersistentData` |
|
||||||
| Custom types; `plugin:foo` | Any custom data | `#getData(Identifer)` | `#setData(Identifier)` |
|
| Custom types; `plugin:foo` | Any custom data | `#getData(Identifer)` | `#setData(Identifier)` |
|
||||||
|
|
||||||
@@ -173,8 +175,8 @@ huskSyncAPI.getCurrentData(user).thenAccept(optionalSnapshot -> {
|
|||||||
* You can only get data from snapshots where a serializer has been registered for it on this server and, in the case of the built-in data types, where the sync feature has been enabled in the [[Config File]]. If you try to get data from a snapshot where the data type is not supported, you will get an empty `Optional`.
|
* You can only get data from snapshots where a serializer has been registered for it on this server and, in the case of the built-in data types, where the sync feature has been enabled in the [[Config File]]. If you try to get data from a snapshot where the data type is not supported, you will get an empty `Optional`.
|
||||||
|
|
||||||
### 4.2 Editing Health, Hunger, Experience, and GameMode data
|
### 4.2 Editing Health, Hunger, Experience, and GameMode data
|
||||||
* `DataSnapshot.Unpacked#getHealth()` returns an `Optional<Data.Health>`, which you can then use to get the player's current and max health.
|
* `DataSnapshot.Unpacked#getHealth()` returns an `Optional<Data.Health>`, which you can then use to get the player's current health.
|
||||||
* `DataSnapshot.Unpacked#setHealth(Data.Health)` sets the player's current and max health. You can create a `Health` instance to pass on the Bukkit platform through `BukkitData.Health.from(double, double, double)`.
|
* `DataSnapshot.Unpacked#setHealth(Data.Health)` sets the player's current health. You can create a `Health` instance to pass on the Bukkit platform through `BukkitData.Health.from(double, double)`.
|
||||||
* Similar methods exist for Hunger, Experience, and GameMode data types
|
* Similar methods exist for Hunger, Experience, and GameMode data types
|
||||||
* Once you've updated the data in the snapshot, you can save it to the database using `HuskSyncAPI#setCurrentData(user, userData)`.
|
* Once you've updated the data in the snapshot, you can save it to the database using `HuskSyncAPI#setCurrentData(user, userData)`.
|
||||||
|
|
||||||
@@ -201,21 +203,30 @@ huskSyncAPI.getCurrentData(user).thenAccept(optionalSnapshot -> {
|
|||||||
System.out.println("User has no game mode data!");
|
System.out.println("User has no game mode data!");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
Optional<Data.FlightStatus> flightStatusOptional = snapshot.getFlightStatus();
|
||||||
|
if (flightStatusOptional.isEmpty()) {
|
||||||
|
System.out.println("User has no flight status data!");
|
||||||
|
return;
|
||||||
|
}
|
||||||
// getExperience() and getHunger() work similarly
|
// getExperience() and getHunger() work similarly
|
||||||
|
|
||||||
// Get the health data
|
// Get the health data
|
||||||
Data.Health health = healthOptional.get();
|
Data.Health health = healthOptional.get();
|
||||||
double currentHealth = health.getCurrentHealth(); // Current health
|
double currentHealth = health.getCurrentHealth(); // Current health
|
||||||
double maxHealth = health.getMaxHealth(); // Max health
|
|
||||||
double healthScale = health.getHealthScale(); // Health scale (e.g., 20 for 20 hearts)
|
double healthScale = health.getHealthScale(); // Health scale (e.g., 20 for 20 hearts)
|
||||||
snapshot.setHealth(BukkitData.Health.from(20, 20, 20));
|
snapshot.setHealth(BukkitData.Health.from(20, 20));
|
||||||
|
// Need max health? Look at the Attributes data type.
|
||||||
|
|
||||||
// Get the game mode data
|
// Get the game mode data
|
||||||
Data.GameMode gameMode = gameModeOptional.get();
|
Data.GameMode gameMode = gameModeOptional.get();
|
||||||
String gameModeName = gameMode.getGameModeName(); // Game mode name (e.g., "SURVIVAL")
|
String gameModeName = gameMode.getGameModeName(); // Game mode name (e.g., "SURVIVAL")
|
||||||
boolean isFlying = gameMode.isFlying(); // Whether the player is *currently* flying
|
snapshot.setGameMode(BukkitData.GameMode.from("SURVIVAL"));
|
||||||
boolean canFly = gameMode.canFly(); // Whether the player *can* fly
|
|
||||||
snapshot.setGameMode(BukkitData.GameMode.from("SURVIVAL", false, false));
|
// Get flight data
|
||||||
|
Data.FlightStatus flightStatus = flightStatusOptional.get(); // Whether the player is flying
|
||||||
|
boolean isFlying = flightStatus.isFlying(); // Whether the player is *currently* flying
|
||||||
|
boolean canFly = flightStatus.isAllowFlight(); // Whether the player *can* fly
|
||||||
|
snapshot.setFlightStatus(BukkitData.FlightStatus.from(false, false));
|
||||||
|
|
||||||
// Save the snapshot - This will update the player if online and save the snapshot to the database
|
// Save the snapshot - This will update the player if online and save the snapshot to the database
|
||||||
huskSyncAPI.setCurrentData(user, snapshot);
|
huskSyncAPI.setCurrentData(user, snapshot);
|
||||||
@@ -245,11 +256,8 @@ huskSyncAPI.editCurrentData(user, snapshot -> {
|
|||||||
// Get the player's current health
|
// Get the player's current health
|
||||||
double currentHealth = health.getCurrentHealth();
|
double currentHealth = health.getCurrentHealth();
|
||||||
|
|
||||||
// Get the player's max health
|
// Set the player's health / health scale
|
||||||
double maxHealth = health.getMaxHealth();
|
snapshot.setHealth(BukkitData.Health.from(20, 20));
|
||||||
|
|
||||||
// Set the player's health
|
|
||||||
snapshot.setHealth(BukkitData.Health.from(20, 20, 20));
|
|
||||||
});
|
});
|
||||||
```
|
```
|
||||||
</details>
|
</details>
|
||||||
|
|||||||
@@ -12,14 +12,16 @@ HuskSync supports synchronising a wide range of different data elements, each of
|
|||||||
<details>
|
<details>
|
||||||
<summary> <b>Are modded items supported?</b></summary>
|
<summary> <b>Are modded items supported?</b></summary>
|
||||||
|
|
||||||
Modded items are not supported.
|
If you're running HuskSync on Arclight or similar, please note we will not be able to provide you with support, but have been reported to save & sync correctly with HuskSync v3.x+.
|
||||||
|
|
||||||
|
**TL;DR** — modded items may work, but since we can't guarantee compatibility, we do not officially mark them as supported. Be sure to test thoroughly before deploying on production!
|
||||||
|
|
||||||
</details>
|
</details>
|
||||||
|
|
||||||
<details>
|
<details>
|
||||||
<summary> <b>Are MMOItems / SlimeFun / ItemsAdder items supported?</b></summary>
|
<summary> <b>Are MMOItems / SlimeFun / ItemsAdder items supported?</b></summary>
|
||||||
|
|
||||||
These plugins, which provide custom items, should be supported as of HuskSync v3.x; but do note we cannot guarantee compatibility with all methods of injecting custom data to create custom items. Be sure to test thoroughly before deploying on production!
|
These plugins, which provide custom items, should be supported as of HuskSync v3.x+; but do note we cannot guarantee compatibility with all methods of injecting custom data to create custom items. Be sure to test thoroughly before deploying on production!
|
||||||
|
|
||||||
</details>
|
</details>
|
||||||
|
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ This will walk you through installing HuskSync on your network of Spigot servers
|
|||||||
## Requirements
|
## Requirements
|
||||||
> **Note:** If the plugin fails to load, please check that you are not running an [incompatible version combination](Unsupported-Versions)
|
> **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+) (or MongoDB Database)
|
* A MySQL Database (v8.0+) (MariaDB, PostrgreSQL or MongoDB are also supported)
|
||||||
* A Redis Database (v5.0+) — see [[FAQs]] for more details.
|
* 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+)
|
* Any number of Spigot servers, connected by a BungeeCord or Velocity-based proxy (Minecraft v1.17.1+, running Java 17+)
|
||||||
|
|
||||||
@@ -11,26 +11,42 @@ This will walk you through installing HuskSync on your network of Spigot servers
|
|||||||
### 1. Install the jar
|
### 1. Install the jar
|
||||||
- Place the plugin jar file in the `/plugins/` directory of each Spigot server.
|
- Place the plugin jar file in the `/plugins/` directory of each Spigot server.
|
||||||
- You do not need to install HuskSync as a proxy plugin.
|
- You do not need to install HuskSync as a proxy plugin.
|
||||||
|
- You can additionally install [ProtocolLib](https://www.spigotmc.org/resources/protocollib.1997/) or [PacketEvents](https://www.spigotmc.org/resources/packetevents-api.80279/) for better locked user handling, and [Plan](https://www.spigotmc.org/resources/plan-player-analytics.32536/) for analytics.
|
||||||
|
|
||||||
### 2. Restart servers
|
### 2. Restart servers
|
||||||
- Start, then stop every server to let HuskSync generate the [[config file]].
|
- 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.
|
- 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.
|
- Advanced users: If you'd prefer, you can 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`)
|
- 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 `database` section, enter the credentials of your (MySQL/MariaDB/MongoDB/PostgreSQL) 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.
|
- 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`.
|
- 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>
|
<details>
|
||||||
<summary><b>For MongoDB Users</b></summary>
|
<summary>Important — MongoDB Users</summary>
|
||||||
|
|
||||||
- Navigate to the HuskSync config file on each server (`~/plugins/HuskSync/config.yml`)
|
- Navigate to the HuskSync config file on each server (`~/plugins/HuskSync/config.yml`)
|
||||||
|
- Set `type` in the `database` section to `MONGO`
|
||||||
- Under `credentials` in the `database` section, enter the credentials of your MongoDB Database. You shouldn't touch the `connection_pool` properties.
|
- 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>
|
||||||
|
|
||||||
|
<summary>Additional configuration for MongoDB Atlas users</summary>
|
||||||
|
|
||||||
|
- Navigate to the HuskSync config file on each server (`~/plugins/HuskSync/config.yml`)
|
||||||
|
- Set `using_atlas` in the `mongo_settings` section to `true`.
|
||||||
|
- Remove `&authSource=HuskSync` from `parameters` in the `mongo_settings`.
|
||||||
|
|
||||||
|
(The `port` setting in `credentials` is disregarded when using Atlas.)
|
||||||
|
</details>
|
||||||
|
|
||||||
</details>
|
</details>
|
||||||
|
|
||||||
### 4. Set server names in server.yml files
|
### 4. Set server names in server.yml files
|
||||||
- Navigate to the HuskSync server name file on each server (`~/plugins/HuskSync/server.yml`)
|
- 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)
|
- 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)
|
||||||
|
|
||||||
### 5. Start every server again
|
### 5. Start every server again
|
||||||
- Provided your MySQL and Redis credentials were correct, synchronization should begin as soon as you start your servers again.
|
- Provided your MySQL and Redis credentials were correct, synchronization should begin as soon as you start your servers again.
|
||||||
- If you need to import data from HuskSync v1.x or MySQLPlayerDataBridge, please see the guides below:
|
- If you need to import data from HuskSync v1.x or MySQLPlayerDataBridge, please see the guides below:
|
||||||
|
|||||||
@@ -5,29 +5,29 @@ You can customise how much data HuskSync saves about a player by [turning each s
|
|||||||
## Feature table
|
## Feature table
|
||||||
✅—Supported ❌—Unsupported ⚠️—Experimental
|
✅—Supported ❌—Unsupported ⚠️—Experimental
|
||||||
|
|
||||||
| Name | Description | Availability |
|
| Name | Description | Availability |
|
||||||
|---------------------------|-------------------------------------------------------------|:------------:|
|
|---------------------------|---------------------------------------------------------------------------------------------|:------------:|
|
||||||
| Inventories | Items in player inventories & selected hotbar slot | ✅ |
|
| Inventories | Items in player inventories & selected hotbar slot | ✅ |
|
||||||
| Ender chests | Items in ender chests* | ✅ |
|
| Ender chests | Items in ender chests | ✅ |
|
||||||
| Health | Player health points | ✅ |
|
| Health | Player health points and scale | ✅ |
|
||||||
| Max health | Player max health points and health scale | ✅ |
|
| Hunger | Player hunger, saturation & exhaustion | ✅ |
|
||||||
| Hunger | Player hunger, saturation & exhaustion | ✅ |
|
| Attributes | Player max health, movement speed, reach, etc. ([wiki](https://minecraft.wiki/w/Attribute)) | ✅ |
|
||||||
| Experience | Player level, experience points & score | ✅ |
|
| Experience | Player level, experience points & score | ✅ |
|
||||||
| Potion effects | Active status effects on players | ✅ |
|
| Potion effects | Active status effects on players | ✅ |
|
||||||
| Advancements | Player advancements, recipes & progress | ✅ |
|
| Advancements | Player advancements, recipes & progress | ✅ |
|
||||||
| Game modes | Player's current game mode | ✅ |
|
| Game modes | Player's current game mode | ✅ |
|
||||||
| Statistics | Player's in-game stats (ESC -> Statistics) | ✅ |
|
| Flight status | If the player is currently flying / can fly | ✅ |
|
||||||
| Location | Player's current coordinate positon and world† | ✅ |
|
| Statistics | Player's in-game stats (ESC -> Statistics) | ✅ |
|
||||||
| Persistent Data Container | Custom plugin persistent data key map | ✅️ |
|
| Location | Player's current coordinate position and world (see below) | ✅ |
|
||||||
| Locked maps | Maps/treasure maps locked in a cartography table | ⚠️ |
|
| Persistent Data Container | Custom plugin persistent data key map | ✅️ |
|
||||||
| Unlocked maps | Regular, unlocked maps/treasure maps ([why?](#map-syncing)) | ❌ |
|
| Locked maps | Maps/treasure maps locked in a cartography table | ✅ |
|
||||||
| Economy balances | Vault economy balance. ([why?](#economy-syncing)) | ❌ |
|
| Unlocked maps | Regular, unlocked maps/treasure maps ([why?](#map-syncing)) | ❌ |
|
||||||
|
| Economy balances | Vault economy balance. ([why?](#economy-syncing)) | ❌ |
|
||||||
|
|
||||||
What about modded items? Or custom item plugins such as MMOItems or SlimeFun? These items are **not compatible**—check the [[FAQs]] for more information.
|
* What about modded items (Arclight, etc.)? – Though we can't provide support for these setups to work, they have been reported to save & sync correctly with HuskSync v3.x+.
|
||||||
|
* What about SlimeFun, MMOItems, etc.? – Yes, items created via these plugins should save & sync correctly, but be sure to test thoroughly first.
|
||||||
*Purpur's custom ender chest resizing feature is also supported.
|
* What about Purpur's custom ender chest resizing feature? – Yes, this is supported (but make sure it's enabled on _all_ servers!).
|
||||||
|
* What do you mean by location syncing? – This is intended for servers that have mirrored worlds across instances (such as RPG servers). With this enabled, players will be placed at the same coordinates when changing servers.
|
||||||
†This is intended for servers that have mirrored worlds across instances (such as RPG servers). With this option enabled, players will be placed at the same coordinates when changing servers.
|
|
||||||
|
|
||||||
### Map syncing
|
### Map syncing
|
||||||
Map items are a special case, as their data is not stored in the item itself, but rather in the game world files. In addition to this, their data is dynamic and changes based on the updating of the world, something that can't be tracked across multiple instances. As a result, it's not possible to sync unlocked map items. Locked maps, however, are supported. This works by saving the pixel canvas grid to the map NBT itself, and generating virtual maps on the other servers.
|
Map items are a special case, as their data is not stored in the item itself, but rather in the game world files. In addition to this, their data is dynamic and changes based on the updating of the world, something that can't be tracked across multiple instances. As a result, it's not possible to sync unlocked map items. Locked maps, however, are supported. This works by saving the pixel canvas grid to the map NBT itself, and generating virtual maps on the other servers.
|
||||||
|
|||||||
@@ -3,12 +3,13 @@ org.gradle.jvmargs='-Dfile.encoding=UTF-8'
|
|||||||
org.gradle.daemon=true
|
org.gradle.daemon=true
|
||||||
javaVersion=17
|
javaVersion=17
|
||||||
|
|
||||||
plugin_version=3.4
|
plugin_version=3.5.2
|
||||||
plugin_archive=husksync
|
plugin_archive=husksync
|
||||||
plugin_description=A modern, cross-server player data synchronization system
|
plugin_description=A modern, cross-server player data synchronization system
|
||||||
|
|
||||||
jedis_version=5.1.0
|
jedis_version=5.1.3
|
||||||
mysql_driver_version=8.3.0
|
mysql_driver_version=8.4.0
|
||||||
mariadb_driver_version=3.3.2
|
mariadb_driver_version=3.4.0
|
||||||
mongodb_driver_version=3.12.14
|
postgres_driver_version=42.7.3
|
||||||
|
mongodb_driver_version=5.1.0
|
||||||
snappy_version=1.1.10.5
|
snappy_version=1.1.10.5
|
||||||
|
|||||||
@@ -4,9 +4,9 @@ dependencies {
|
|||||||
|
|
||||||
compileOnly 'io.papermc.paper:paper-api:1.19.4-R0.1-SNAPSHOT'
|
compileOnly 'io.papermc.paper:paper-api:1.19.4-R0.1-SNAPSHOT'
|
||||||
compileOnly 'org.jetbrains:annotations:24.1.0'
|
compileOnly 'org.jetbrains:annotations:24.1.0'
|
||||||
compileOnly 'org.projectlombok:lombok:1.18.30'
|
compileOnly 'org.projectlombok:lombok:1.18.32'
|
||||||
|
|
||||||
annotationProcessor 'org.projectlombok:lombok:1.18.30'
|
annotationProcessor 'org.projectlombok:lombok:1.18.32'
|
||||||
}
|
}
|
||||||
|
|
||||||
shadowJar {
|
shadowJar {
|
||||||
|
|||||||
@@ -24,7 +24,10 @@ import net.william278.husksync.BukkitHuskSync;
|
|||||||
import net.william278.husksync.data.BukkitData;
|
import net.william278.husksync.data.BukkitData;
|
||||||
import net.william278.husksync.user.BukkitUser;
|
import net.william278.husksync.user.BukkitUser;
|
||||||
import net.william278.husksync.user.OnlineUser;
|
import net.william278.husksync.user.OnlineUser;
|
||||||
|
import org.bukkit.event.EventHandler;
|
||||||
|
import org.bukkit.event.EventPriority;
|
||||||
import org.bukkit.event.entity.PlayerDeathEvent;
|
import org.bukkit.event.entity.PlayerDeathEvent;
|
||||||
|
import org.bukkit.event.player.PlayerAdvancementDoneEvent;
|
||||||
import org.bukkit.inventory.ItemStack;
|
import org.bukkit.inventory.ItemStack;
|
||||||
import org.bukkit.inventory.PlayerInventory;
|
import org.bukkit.inventory.PlayerInventory;
|
||||||
import org.jetbrains.annotations.NotNull;
|
import org.jetbrains.annotations.NotNull;
|
||||||
@@ -44,7 +47,7 @@ public class PaperEventListener extends BukkitEventListener {
|
|||||||
public void handlePlayerDeath(@NotNull PlayerDeathEvent event) {
|
public void handlePlayerDeath(@NotNull PlayerDeathEvent event) {
|
||||||
// If the player is locked or the plugin disabling, clear their drops
|
// If the player is locked or the plugin disabling, clear their drops
|
||||||
final OnlineUser user = BukkitUser.adapt(event.getEntity(), plugin);
|
final OnlineUser user = BukkitUser.adapt(event.getEntity(), plugin);
|
||||||
if (cancelPlayerEvent(user.getUuid())) {
|
if (lockedHandler.cancelPlayerEvent(user.getUuid())) {
|
||||||
event.getDrops().clear();
|
event.getDrops().clear();
|
||||||
event.getItemsToKeep().clear();
|
event.getItemsToKeep().clear();
|
||||||
return;
|
return;
|
||||||
@@ -68,6 +71,13 @@ public class PaperEventListener extends BukkitEventListener {
|
|||||||
super.saveOnPlayerDeath(user, BukkitData.Items.ItemArray.adapt(itemsToSave));
|
super.saveOnPlayerDeath(user, BukkitData.Items.ItemArray.adapt(itemsToSave));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = true)
|
||||||
|
public void onPlayerAdvancementDone(@NotNull PlayerAdvancementDoneEvent event) {
|
||||||
|
if (lockedHandler.cancelPlayerEvent(event.getPlayer().getUniqueId())) {
|
||||||
|
event.message(null);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@NotNull
|
@NotNull
|
||||||
private List<ItemStack> preserveOrder(@NotNull PlayerInventory inventory, @NotNull List<ItemStack> toKeep) {
|
private List<ItemStack> preserveOrder(@NotNull PlayerInventory inventory, @NotNull List<ItemStack> toKeep) {
|
||||||
final List<ItemStack> preserved = Lists.newArrayList();
|
final List<ItemStack> preserved = Lists.newArrayList();
|
||||||
|
|||||||
@@ -3,5 +3,6 @@ libraries:
|
|||||||
- 'redis.clients:jedis:${jedis_version}'
|
- 'redis.clients:jedis:${jedis_version}'
|
||||||
- 'com.mysql:mysql-connector-j:${mysql_driver_version}'
|
- 'com.mysql:mysql-connector-j:${mysql_driver_version}'
|
||||||
- 'org.mariadb.jdbc:mariadb-java-client:${mariadb_driver_version}'
|
- 'org.mariadb.jdbc:mariadb-java-client:${mariadb_driver_version}'
|
||||||
- 'org.mongodb:mongodb-driver:${mongodb_driver_version}'
|
- 'org.postgresql:postgresql:${postgres_driver_version}'
|
||||||
|
- 'org.mongodb:mongodb-driver-sync:${mongodb_driver_version}'
|
||||||
- 'org.xerial.snappy:snappy-java:${snappy_version}'
|
- 'org.xerial.snappy:snappy-java:${snappy_version}'
|
||||||
@@ -6,8 +6,17 @@ main: 'net.william278.husksync.PaperHuskSync'
|
|||||||
loader: 'net.william278.husksync.PaperHuskSyncLoader'
|
loader: 'net.william278.husksync.PaperHuskSyncLoader'
|
||||||
version: '${version}'
|
version: '${version}'
|
||||||
api-version: '1.19'
|
api-version: '1.19'
|
||||||
|
folia-supported: true
|
||||||
dependencies:
|
dependencies:
|
||||||
server:
|
server:
|
||||||
|
packetevents:
|
||||||
|
required: false
|
||||||
|
load: BEFORE
|
||||||
|
join-classpath: true
|
||||||
|
ProtocolLib:
|
||||||
|
required: false
|
||||||
|
load: BEFORE
|
||||||
|
join-classpath: true
|
||||||
MysqlPlayerDataBridge:
|
MysqlPlayerDataBridge:
|
||||||
required: false
|
required: false
|
||||||
load: BEFORE
|
load: BEFORE
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
certifi==2023.7.22
|
certifi==2023.7.22
|
||||||
charset-normalizer==3.2.0
|
charset-normalizer==3.2.0
|
||||||
colorama==0.4.6
|
colorama==0.4.6
|
||||||
idna==3.4
|
idna==3.7
|
||||||
requests==2.31.0
|
requests==2.32.0
|
||||||
tqdm==4.66.1
|
tqdm==4.66.3
|
||||||
urllib3==2.0.7
|
urllib3==2.0.7
|
||||||
|
|||||||
@@ -13,14 +13,14 @@ from tqdm import tqdm
|
|||||||
class Parameters:
|
class Parameters:
|
||||||
root_dir = './servers/'
|
root_dir = './servers/'
|
||||||
proxy_version = "1.20"
|
proxy_version = "1.20"
|
||||||
minecraft_version = '1.20.4'
|
minecraft_version = '1.20.6'
|
||||||
eula_agreement = 'true'
|
eula_agreement = 'true'
|
||||||
|
|
||||||
backend_names = ['alpha', 'beta']
|
backend_names = ['alpha', 'beta']
|
||||||
backend_ports = [25567, 25568]
|
backend_ports = [25567, 25568]
|
||||||
backend_type = 'paper'
|
backend_type = 'paper'
|
||||||
backend_ram = 2048
|
backend_ram = 2048
|
||||||
backend_plugins = ['../target/HuskSync-Paper-*.jar']
|
backend_plugins = ['../target/HuskSync-Paper-*.jar', './ProtocolLib/ProtocolLib.jar']
|
||||||
backend_plugin_folders = ['./HuskSync']
|
backend_plugin_folders = ['./HuskSync']
|
||||||
operator_names = ['William278']
|
operator_names = ['William278']
|
||||||
operator_uuids = ['5dfb0558-e306-44f4-bb9a-f9218d4eb787']
|
operator_uuids = ['5dfb0558-e306-44f4-bb9a-f9218d4eb787']
|
||||||
@@ -102,8 +102,8 @@ def create_backend_server(name, port, parameters):
|
|||||||
# Download the latest paper for the version and place it in the server folder
|
# Download the latest paper for the version and place it in the server folder
|
||||||
server_jar = "paper.jar"
|
server_jar = "paper.jar"
|
||||||
download_paper_build("paper", parameters.minecraft_version,
|
download_paper_build("paper", parameters.minecraft_version,
|
||||||
get_latest_paper_build_number("paper", parameters.minecraft_version),
|
get_latest_paper_build_number("paper", parameters.minecraft_version),
|
||||||
f"{server_dir}/{server_jar}")
|
f"{server_dir}/{server_jar}")
|
||||||
|
|
||||||
# Create eula.text and set eula=true
|
# Create eula.text and set eula=true
|
||||||
with open(server_dir + "/eula.txt", "w") as file:
|
with open(server_dir + "/eula.txt", "w") as file:
|
||||||
@@ -176,8 +176,8 @@ def create_proxy_server(parameters):
|
|||||||
# Download the latest paper for the version and place it in the server folder
|
# Download the latest paper for the version and place it in the server folder
|
||||||
proxy_jar = "waterfall.jar"
|
proxy_jar = "waterfall.jar"
|
||||||
download_paper_build("waterfall", parameters.proxy_version,
|
download_paper_build("waterfall", parameters.proxy_version,
|
||||||
get_latest_paper_build_number("waterfall", parameters.proxy_version),
|
get_latest_paper_build_number("waterfall", parameters.proxy_version),
|
||||||
f"{server_dir}/{proxy_jar}")
|
f"{server_dir}/{proxy_jar}")
|
||||||
|
|
||||||
# Create the config.yml
|
# Create the config.yml
|
||||||
with open(server_dir + "/config.yml", "w") as file:
|
with open(server_dir + "/config.yml", "w") as file:
|
||||||
|
|||||||
Reference in New Issue
Block a user