9
0
mirror of https://github.com/WiIIiam278/HuskSync.git synced 2025-12-23 16:49:19 +00:00

Compare commits

..

23 Commits
3.1.2 ... 3.2

Author SHA1 Message Date
William
2aa33b2f2c fix: Improve accuracy of max health syncing #148 2023-12-21 18:30:40 +00:00
William
972fee1bc7 fix: Fix flight syncing sometimes failing, close #206 2023-12-21 17:34:01 +00:00
William
efe34977b5 ci: Use wiki-action@v3 2023-12-21 17:31:25 +00:00
William
02ed9687ee deps: Bump runtime dependencies 2023-12-21 17:31:22 +00:00
William
08889a1739 docs: Bump to 3.2 (Redis key protocol changes) 2023-12-21 17:01:56 +00:00
William
9cf6d1eab6 refactor: change default sync mode to LOCKSTEP 2023-12-21 17:01:34 +00:00
William
33c2eb2237 refactor: Use cloud for server for HuskHomes consistency 2023-12-21 15:57:58 +00:00
William
299586aa86 refactor: Rename DATA_UPDATE -> LATEST_SNAPSHOT 2023-12-21 15:53:56 +00:00
William
05c988f2c7 refactor: Extend DATA_UPDATE Redis cache time on LOCKSTEP mode 2023-12-21 15:50:35 +00:00
William
8e0ad76968 refactor: Improve getUserCheckedOut debug log 2023-12-21 15:06:17 +00:00
William
4db162e78f refactor: Even more minor debug logging tweaks 2023-12-21 15:02:36 +00:00
William
272bc1278a refactor: More minor debug logging tweaks 2023-12-21 15:01:46 +00:00
William
35fdcf7106 refactor: Further improve debug log messages 2023-12-21 14:59:15 +00:00
William
48e087a3d7 refactor: Improve debug log wording for getUserCheckedOut 2023-12-21 14:55:49 +00:00
William
ca000197e4 refactor: Further improvements to debug messages 2023-12-21 14:50:22 +00:00
William
a6bab88cee refactor: Add debug log for listenForRedis timeout 2023-12-21 14:30:14 +00:00
William
f0c64df439 refactor: Improve debug logging messages 2023-12-21 14:25:38 +00:00
William
ac5ab56717 fix: Don't wrap saveUserData in runAsync twice 2023-12-21 13:24:52 +00:00
William
c2025350ba fix: Optimize imports 2023-12-19 22:06:29 +00:00
William
4c2bb5c6df fix: Get correct platform Audience for OnlineUsers 2023-12-19 22:06:13 +00:00
William
fb069296e1 refactor: Use native adventure implementation on Paper 2023-12-19 22:03:24 +00:00
Roman Alexander
22eedc8522 feat: Add support for Redis Sentinels (#216)
* Add support for Redis Sentinels

* Add some comments
2023-12-19 19:27:03 +00:00
William278
664c8c3352 Bump to 3.1.3 2023-12-12 13:30:00 +00:00
35 changed files with 277 additions and 223 deletions

View File

@@ -19,7 +19,7 @@ jobs:
- name: 'Checkout for CI 🛎️' - name: 'Checkout for CI 🛎️'
uses: actions/checkout@v4 uses: actions/checkout@v4
- name: 'Push Docs to Github Wiki 📄️' - name: 'Push Docs to Github Wiki 📄️'
uses: Andrew-Chen-Wang/github-wiki-action@v4 uses: Andrew-Chen-Wang/github-wiki-action@v3
env: env:
WIKI_DIR: 'docs/' WIKI_DIR: 'docs/'
GH_TOKEN: ${{ github.token }} GH_TOKEN: ${{ github.token }}

View File

@@ -36,7 +36,6 @@ shadowJar {
relocate 'org.json', 'net.william278.husksync.libraries.json' relocate 'org.json', 'net.william278.husksync.libraries.json'
relocate 'com.fatboyindustrial', 'net.william278.husksync.libraries' relocate 'com.fatboyindustrial', 'net.william278.husksync.libraries'
relocate 'de.themoep', 'net.william278.husksync.libraries' relocate 'de.themoep', 'net.william278.husksync.libraries'
relocate 'net.kyori', 'net.william278.husksync.libraries'
relocate 'org.jetbrains', 'net.william278.husksync.libraries' relocate 'org.jetbrains', 'net.william278.husksync.libraries'
relocate 'org.intellij', 'net.william278.husksync.libraries' relocate 'org.intellij', 'net.william278.husksync.libraries'
relocate 'com.zaxxer', 'net.william278.husksync.libraries' relocate 'com.zaxxer', 'net.william278.husksync.libraries'

View File

@@ -20,6 +20,7 @@
package net.william278.husksync; package net.william278.husksync;
import com.google.gson.Gson; import com.google.gson.Gson;
import net.kyori.adventure.platform.AudienceProvider;
import net.kyori.adventure.platform.bukkit.BukkitAudiences; import net.kyori.adventure.platform.bukkit.BukkitAudiences;
import net.william278.desertwell.util.Version; import net.william278.desertwell.util.Version;
import net.william278.husksync.adapter.DataAdapter; import net.william278.husksync.adapter.DataAdapter;
@@ -46,7 +47,6 @@ import net.william278.husksync.migrator.MpdbMigrator;
import net.william278.husksync.redis.RedisManager; import net.william278.husksync.redis.RedisManager;
import net.william278.husksync.sync.DataSyncer; import net.william278.husksync.sync.DataSyncer;
import net.william278.husksync.user.BukkitUser; import net.william278.husksync.user.BukkitUser;
import net.william278.husksync.user.ConsoleUser;
import net.william278.husksync.user.OnlineUser; import net.william278.husksync.user.OnlineUser;
import net.william278.husksync.util.BukkitLegacyConverter; import net.william278.husksync.util.BukkitLegacyConverter;
import net.william278.husksync.util.BukkitMapPersister; import net.william278.husksync.util.BukkitMapPersister;
@@ -346,12 +346,6 @@ public class BukkitHuskSync extends JavaPlugin implements HuskSync, BukkitTask.S
} }
} }
@NotNull
@Override
public ConsoleUser getConsole() {
return new ConsoleUser(audiences.console());
}
@NotNull @NotNull
@Override @Override
public Version getPluginVersion() { public Version getPluginVersion() {
@@ -415,7 +409,7 @@ public class BukkitHuskSync extends JavaPlugin implements HuskSync, BukkitTask.S
} }
@NotNull @NotNull
public BukkitAudiences getAudiences() { public AudienceProvider getAudiences() {
return audiences; return audiences;
} }

View File

@@ -593,54 +593,54 @@ public abstract class BukkitData implements Data {
@NotNull @NotNull
public static BukkitData.Statistics from(@NotNull StatisticsMap stats) { public static BukkitData.Statistics from(@NotNull StatisticsMap stats) {
return new BukkitData.Statistics( return new BukkitData.Statistics(
stats.genericStats().entrySet().stream() stats.genericStats().entrySet().stream()
.flatMap(entry -> { .flatMap(entry -> {
Statistic statistic = matchStatistic(entry.getKey()); Statistic statistic = matchStatistic(entry.getKey());
return statistic != null ? Stream.of(new AbstractMap.SimpleEntry<>(statistic, entry.getValue())) : Stream.empty(); return statistic != null ? Stream.of(new AbstractMap.SimpleEntry<>(statistic, entry.getValue())) : Stream.empty();
})
.collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue)),
stats.blockStats().entrySet().stream()
.flatMap(entry -> {
Statistic statistic = matchStatistic(entry.getKey());
return statistic != null ? Stream.of(new AbstractMap.SimpleEntry<>(statistic, entry.getValue())) : Stream.empty();
})
.collect(Collectors.toMap(
Map.Entry::getKey,
entry -> entry.getValue().entrySet().stream()
.flatMap(blockEntry -> {
Material material = Material.matchMaterial(blockEntry.getKey());
return material != null ? Stream.of(new AbstractMap.SimpleEntry<>(material, blockEntry.getValue())) : Stream.empty();
}) })
.collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue)) .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue)),
)), stats.blockStats().entrySet().stream()
stats.itemStats().entrySet().stream() .flatMap(entry -> {
.flatMap(entry -> { Statistic statistic = matchStatistic(entry.getKey());
Statistic statistic = matchStatistic(entry.getKey()); return statistic != null ? Stream.of(new AbstractMap.SimpleEntry<>(statistic, entry.getValue())) : Stream.empty();
return statistic != null ? Stream.of(new AbstractMap.SimpleEntry<>(statistic, entry.getValue())) : Stream.empty();
})
.collect(Collectors.toMap(
Map.Entry::getKey,
entry -> entry.getValue().entrySet().stream()
.flatMap(itemEntry -> {
Material material = Material.matchMaterial(itemEntry.getKey());
return material != null ? Stream.of(new AbstractMap.SimpleEntry<>(material, itemEntry.getValue())) : Stream.empty();
}) })
.collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue)) .collect(Collectors.toMap(
)), Map.Entry::getKey,
stats.entityStats().entrySet().stream() entry -> entry.getValue().entrySet().stream()
.flatMap(entry -> { .flatMap(blockEntry -> {
Statistic statistic = matchStatistic(entry.getKey()); Material material = Material.matchMaterial(blockEntry.getKey());
return statistic != null ? Stream.of(new AbstractMap.SimpleEntry<>(statistic, entry.getValue())) : Stream.empty(); return material != null ? Stream.of(new AbstractMap.SimpleEntry<>(material, blockEntry.getValue())) : Stream.empty();
}) })
.collect(Collectors.toMap( .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue))
Map.Entry::getKey, )),
entry -> entry.getValue().entrySet().stream() stats.itemStats().entrySet().stream()
.flatMap(itemEntry -> { .flatMap(entry -> {
EntityType entityType = matchEntityType(itemEntry.getKey()); Statistic statistic = matchStatistic(entry.getKey());
return entityType != null ? Stream.of(new AbstractMap.SimpleEntry<>(entityType, itemEntry.getValue())) : Stream.empty(); return statistic != null ? Stream.of(new AbstractMap.SimpleEntry<>(statistic, entry.getValue())) : Stream.empty();
}) })
.collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue)) .collect(Collectors.toMap(
)) Map.Entry::getKey,
entry -> entry.getValue().entrySet().stream()
.flatMap(itemEntry -> {
Material material = Material.matchMaterial(itemEntry.getKey());
return material != null ? Stream.of(new AbstractMap.SimpleEntry<>(material, itemEntry.getValue())) : Stream.empty();
})
.collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue))
)),
stats.entityStats().entrySet().stream()
.flatMap(entry -> {
Statistic statistic = matchStatistic(entry.getKey());
return statistic != null ? Stream.of(new AbstractMap.SimpleEntry<>(statistic, entry.getValue())) : Stream.empty();
})
.collect(Collectors.toMap(
Map.Entry::getKey,
entry -> entry.getValue().entrySet().stream()
.flatMap(itemEntry -> {
EntityType entityType = matchEntityType(itemEntry.getKey());
return entityType != null ? Stream.of(new AbstractMap.SimpleEntry<>(entityType, itemEntry.getValue())) : Stream.empty();
})
.collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue))
))
); );
} }
@@ -822,10 +822,9 @@ public abstract class BukkitData implements Data {
@NotNull @NotNull
public static BukkitData.Health adapt(@NotNull Player player) { public static BukkitData.Health adapt(@NotNull Player player) {
final double maxHealth = getMaxHealth(player);
return from( return from(
Math.min(player.getHealth(), maxHealth), player.getHealth(),
maxHealth, getMaxHealth(player),
player.isHealthScaled() ? player.getHealthScale() : 0d player.isHealthScaled() ? player.getHealthScale() : 0d
); );
} }
@@ -834,64 +833,65 @@ public abstract class BukkitData implements Data {
public void apply(@NotNull BukkitUser user, @NotNull BukkitHuskSync plugin) throws IllegalStateException { public void apply(@NotNull BukkitUser user, @NotNull BukkitHuskSync plugin) throws IllegalStateException {
final Player player = user.getPlayer(); final Player player = user.getPlayer();
// Set base max health // Set max health
final AttributeInstance maxHealthAttribute = Objects.requireNonNull( final AttributeInstance maxHealth = getMaxHealthAttribute(player);
player.getAttribute(Attribute.GENERIC_MAX_HEALTH), "Max health attribute was null" try {
); if (plugin.getSettings().doSynchronizeMaxHealth() && this.maxHealth != 0) {
double currentMaxHealth = maxHealthAttribute.getBaseValue(); maxHealth.setBaseValue(this.maxHealth);
if (plugin.getSettings().doSynchronizeMaxHealth() && maxHealth != 0d) { }
maxHealthAttribute.setBaseValue(maxHealth); } catch (Throwable e) {
currentMaxHealth = maxHealth; plugin.log(Level.WARNING, String.format("Failed setting the max health of %s to %s",
player.getName(), this.maxHealth), e);
} }
// Set health // Set health
final double currentHealth = player.getHealth(); try {
if (health != currentHealth) { final double health = player.getHealth();
final double healthToSet = currentHealth > currentMaxHealth ? currentMaxHealth : health; player.setHealth(Math.min(health, maxHealth.getBaseValue()));
try { } catch (Throwable e) {
player.setHealth(Math.min(healthToSet, currentMaxHealth)); plugin.log(Level.WARNING, String.format("Failed setting the health of %s to %s",
} catch (IllegalArgumentException e) { player.getName(), this.maxHealth), e);
plugin.log(Level.WARNING, "Failed to set player health", e);
}
} }
// Set health scale // Set health scale
try { try {
if (healthScale != 0d) { if (this.healthScale != 0d) {
player.setHealthScale(healthScale); player.setHealthScaled(true);
player.setHealthScale(this.healthScale);
} else { } else {
player.setHealthScale(maxHealth); player.setHealthScaled(false);
player.setHealthScale(this.maxHealth);
} }
player.setHealthScaled(healthScale != 0D); } catch (Throwable e) {
} catch (IllegalArgumentException e) { plugin.log(Level.WARNING, String.format("Failed setting the health scale of %s to %s",
plugin.log(Level.WARNING, "Failed to set player health scale", e); player.getName(), this.healthScale), e);
} }
} }
/** // Returns the max health of a player, accounting for health boost potion effects
* Returns a {@link Player}'s maximum health, minus any health boost effects
*
* @param player The {@link Player} to get the maximum health of
* @return The {@link Player}'s max health
*/
private static double getMaxHealth(@NotNull Player player) { private static double getMaxHealth(@NotNull Player player) {
double maxHealth = Objects.requireNonNull( // Get the base value of the attribute (ignore armor, items that give health boosts, etc.)
player.getAttribute(Attribute.GENERIC_MAX_HEALTH), "Max health attribute was null" double maxHealth = getMaxHealthAttribute(player).getBaseValue();
).getBaseValue();
// If the player has additional health bonuses from synchronized potion effects, // Subtract health boost potion effects from stored max health
// subtract these from this number as they are synchronized separately
if (player.hasPotionEffect(PotionEffectType.HEALTH_BOOST) && maxHealth > 20d) { if (player.hasPotionEffect(PotionEffectType.HEALTH_BOOST) && maxHealth > 20d) {
final PotionEffect healthBoost = Objects.requireNonNull( final PotionEffect healthBoost = Objects.requireNonNull(
player.getPotionEffect(PotionEffectType.HEALTH_BOOST), "Health boost effect was null" player.getPotionEffect(PotionEffectType.HEALTH_BOOST), "Health boost effect was null"
); );
final double boostEffect = 4 * (healthBoost.getAmplifier() + 1); maxHealth -= (4 * (healthBoost.getAmplifier() + 1));
maxHealth -= boostEffect;
} }
return maxHealth; return maxHealth;
} }
// Returns the max health attribute of a player
@NotNull
private static AttributeInstance getMaxHealthAttribute(@NotNull Player player) {
return Objects.requireNonNull(
player.getAttribute(Attribute.GENERIC_MAX_HEALTH), "Max health attribute was null"
);
}
@Override @Override
public double getHealth() { public double getHealth() {
return health; return health;
@@ -1093,7 +1093,7 @@ public abstract class BukkitData implements Data {
final Player player = user.getPlayer(); final Player player = user.getPlayer();
player.setGameMode(org.bukkit.GameMode.valueOf(gameMode)); player.setGameMode(org.bukkit.GameMode.valueOf(gameMode));
player.setAllowFlight(allowFlight); player.setAllowFlight(allowFlight);
player.setFlying(isFlying); player.setFlying(allowFlight && isFlying);
} }
@NotNull @NotNull

View File

@@ -23,7 +23,6 @@ import de.themoep.minedown.adventure.MineDown;
import dev.triumphteam.gui.builder.gui.StorageBuilder; import dev.triumphteam.gui.builder.gui.StorageBuilder;
import dev.triumphteam.gui.guis.Gui; import dev.triumphteam.gui.guis.Gui;
import dev.triumphteam.gui.guis.StorageGui; import dev.triumphteam.gui.guis.StorageGui;
import net.kyori.adventure.audience.Audience;
import net.roxeez.advancement.display.FrameType; import net.roxeez.advancement.display.FrameType;
import net.william278.andjam.Toast; import net.william278.andjam.Toast;
import net.william278.husksync.BukkitHuskSync; import net.william278.husksync.BukkitHuskSync;
@@ -77,12 +76,6 @@ public class BukkitUser extends OnlineUser implements BukkitUserDataHolder {
return player == null || !player.isOnline(); return player == null || !player.isOnline();
} }
@NotNull
@Override
public Audience getAudience() {
return ((BukkitHuskSync) plugin).getAudiences().player(player);
}
@Override @Override
public void sendToast(@NotNull MineDown title, @NotNull MineDown description, public void sendToast(@NotNull MineDown title, @NotNull MineDown description,
@NotNull String iconMaterial, @NotNull String backgroundType) { @NotNull String iconMaterial, @NotNull String backgroundType) {

View File

@@ -6,7 +6,6 @@ dependencies {
api 'commons-io:commons-io:2.15.1' api 'commons-io:commons-io:2.15.1'
api 'org.apache.commons:commons-text:1.11.0' api 'org.apache.commons:commons-text:1.11.0'
api 'de.themoep:minedown-adventure:1.7.2-SNAPSHOT' api 'de.themoep:minedown-adventure:1.7.2-SNAPSHOT'
api 'net.kyori:adventure-api:4.14.0'
api 'org.json:json:20231013' api 'org.json:json:20231013'
api 'com.google.code.gson:gson:2.10.1' api 'com.google.code.gson:gson:2.10.1'
api 'com.fatboyindustrial.gson-javatime-serialisers:gson-javatime-serialisers:1.1.2' api 'com.fatboyindustrial.gson-javatime-serialisers:gson-javatime-serialisers:1.1.2'
@@ -18,6 +17,8 @@ dependencies {
exclude module: 'slf4j-api' exclude module: 'slf4j-api'
} }
compileOnly 'net.kyori:adventure-api:4.15.0'
compileOnly 'net.kyori:adventure-platform-api:4.3.1'
compileOnly 'org.jetbrains:annotations:24.1.0' compileOnly 'org.jetbrains:annotations:24.1.0'
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"

View File

@@ -22,6 +22,8 @@ package net.william278.husksync;
import com.fatboyindustrial.gsonjavatime.Converters; import com.fatboyindustrial.gsonjavatime.Converters;
import com.google.gson.Gson; import com.google.gson.Gson;
import com.google.gson.GsonBuilder; import com.google.gson.GsonBuilder;
import net.kyori.adventure.audience.Audience;
import net.kyori.adventure.platform.AudienceProvider;
import net.william278.annotaml.Annotaml; import net.william278.annotaml.Annotaml;
import net.william278.desertwell.util.ThrowingConsumer; import net.william278.desertwell.util.ThrowingConsumer;
import net.william278.desertwell.util.UpdateChecker; import net.william278.desertwell.util.UpdateChecker;
@@ -48,6 +50,7 @@ import java.io.File;
import java.io.IOException; import java.io.IOException;
import java.io.InputStream; import java.io.InputStream;
import java.lang.reflect.InvocationTargetException; import java.lang.reflect.InvocationTargetException;
import java.text.SimpleDateFormat;
import java.util.*; import java.util.*;
import java.util.logging.Level; import java.util.logging.Level;
@@ -245,17 +248,46 @@ public interface HuskSync extends Task.Supplier, EventDispatcher {
*/ */
default void debug(@NotNull String message, @NotNull Throwable... throwable) { default void debug(@NotNull String message, @NotNull Throwable... throwable) {
if (getSettings().doDebugLogging()) { if (getSettings().doDebugLogging()) {
log(Level.INFO, String.format("[DEBUG] %s", message), throwable); log(Level.INFO, getDebugString(message), throwable);
} }
} }
// Get the debug log message format
@NotNull
private String getDebugString(@NotNull String message) {
return String.format("[DEBUG] [%s] %s", new SimpleDateFormat("mm:ss.SSS").format(new Date()), message);
}
/** /**
* Get the console user * Get the {@link AudienceProvider} instance
* *
* @return the {@link ConsoleUser} * @return the {@link AudienceProvider} instance
* @since 1.0
*/ */
@NotNull @NotNull
ConsoleUser getConsole(); AudienceProvider getAudiences();
/**
* Get the {@link Audience} instance for the given {@link OnlineUser}
*
* @param user the {@link OnlineUser} to get the {@link Audience} for
* @return the {@link Audience} instance
*/
@NotNull
default Audience getAudience(@NotNull UUID user) {
return getAudiences().player(user);
}
/**
* Get the {@link ConsoleUser} instance
*
* @return the {@link ConsoleUser} instance
* @since 1.0
*/
@NotNull
default ConsoleUser getConsole() {
return new ConsoleUser(getAudiences());
}
/** /**
* Returns the plugin version * Returns the plugin version

View File

@@ -134,12 +134,23 @@ public class Settings {
@YamlKey("redis.use_ssl") @YamlKey("redis.use_ssl")
private boolean redisUseSsl = false; private boolean redisUseSsl = false;
@YamlComment("If you're using Redis Sentinel, specify the master set name. If you don't know what this is, don't change anything here.")
@YamlKey("redis.sentinel.master")
private String redisSentinelMaster = "";
@YamlComment("List of host:port pairs")
@YamlKey("redis.sentinel.nodes")
private List<String> redisSentinelNodes = new ArrayList<>();
@YamlKey("redis.sentinel.password")
private String redisSentinelPassword = "";
// Synchronization settings // Synchronization settings
@YamlComment("The mode of data synchronization to use (DELAY or LOCKSTEP). DELAY should be fine for most networks." @YamlComment("The data synchronization mode to use (LOCKSTEP or DELAY). LOCKSTEP is recommended for most networks."
+ " Docs: https://william278.net/docs/husksync/sync-modes") + " Docs: https://william278.net/docs/husksync/sync-modes")
@YamlKey("synchronization.mode") @YamlKey("synchronization.mode")
private DataSyncer.Mode syncMode = DataSyncer.Mode.DELAY; private DataSyncer.Mode syncMode = DataSyncer.Mode.LOCKSTEP;
@YamlComment("The number of data snapshot backups that should be kept at once per user") @YamlComment("The number of data snapshot backups that should be kept at once per user")
@YamlKey("synchronization.max_user_data_snapshots") @YamlKey("synchronization.max_user_data_snapshots")
@@ -324,6 +335,21 @@ public class Settings {
return redisUseSsl; return redisUseSsl;
} }
@NotNull
public String getRedisSentinelMaster() {
return redisSentinelMaster;
}
@NotNull
public List<String> getRedisSentinelNodes() {
return redisSentinelNodes;
}
@NotNull
public String getRedisSentinelPassword() {
return redisSentinelPassword;
}
@NotNull @NotNull
public DataSyncer.Mode getSyncMode() { public DataSyncer.Mode getSyncMode() {
return syncMode; return syncMode;

View File

@@ -65,7 +65,7 @@ public abstract class EventListener {
return; return;
} }
plugin.lockPlayer(user.getUuid()); plugin.lockPlayer(user.getUuid());
plugin.runAsync(() -> plugin.getDataSyncer().saveUserData(user)); plugin.getDataSyncer().saveUserData(user);
} }
/** /**

View File

@@ -24,15 +24,13 @@ import org.jetbrains.annotations.NotNull;
import java.util.Locale; import java.util.Locale;
public enum RedisKeyType { public enum RedisKeyType {
DATA_UPDATE(10),
SERVER_SWITCH(10),
DATA_CHECKOUT(60 * 60 * 24 * 7 * 52);
private final int timeToLive; LATEST_SNAPSHOT,
SERVER_SWITCH,
DATA_CHECKOUT;
RedisKeyType(int timeToLive) { public static final int TTL_1_YEAR = 60 * 60 * 24 * 7 * 52; // 1 year
this.timeToLive = timeToLive; public static final int TTL_10_SECONDS = 10; // 10 seconds
}
@NotNull @NotNull
public String getKeyPrefix(@NotNull String clusterId) { public String getKeyPrefix(@NotNull String clusterId) {
@@ -44,8 +42,4 @@ public enum RedisKeyType {
); );
} }
public int getTimeToLive() {
return timeToLive;
}
} }

View File

@@ -24,14 +24,11 @@ import net.william278.husksync.data.DataSnapshot;
import net.william278.husksync.user.User; import net.william278.husksync.user.User;
import org.jetbrains.annotations.Blocking; import org.jetbrains.annotations.Blocking;
import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.NotNull;
import redis.clients.jedis.Jedis; import redis.clients.jedis.*;
import redis.clients.jedis.JedisPool;
import redis.clients.jedis.JedisPoolConfig;
import redis.clients.jedis.JedisPubSub;
import redis.clients.jedis.exceptions.JedisException; import redis.clients.jedis.exceptions.JedisException;
import redis.clients.jedis.util.Pool;
import java.nio.charset.StandardCharsets; import java.nio.charset.StandardCharsets;
import java.text.SimpleDateFormat;
import java.util.*; import java.util.*;
import java.util.concurrent.CompletableFuture; import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentHashMap;
@@ -39,7 +36,7 @@ import java.util.concurrent.TimeUnit;
import java.util.logging.Level; import java.util.logging.Level;
/** /**
* Manages the connection to the Redis server, handling the caching of user data * Manages the connection to Redis, handling the caching of user data
*/ */
public class RedisManager extends JedisPubSub { public class RedisManager extends JedisPubSub {
@@ -47,7 +44,7 @@ public class RedisManager extends JedisPubSub {
private final HuskSync plugin; private final HuskSync plugin;
private final String clusterId; private final String clusterId;
private JedisPool jedisPool; private Pool<Jedis> jedisPool;
private final Map<UUID, CompletableFuture<Optional<DataSnapshot.Packed>>> pendingRequests; private final Map<UUID, CompletableFuture<Optional<DataSnapshot.Packed>>> pendingRequests;
public RedisManager(@NotNull HuskSync plugin) { public RedisManager(@NotNull HuskSync plugin) {
@@ -57,7 +54,7 @@ public class RedisManager extends JedisPubSub {
} }
/** /**
* Initialize the redis connection pool * Initialize Redis connection pool
*/ */
@Blocking @Blocking
public void initialize() throws IllegalStateException { public void initialize() throws IllegalStateException {
@@ -71,15 +68,22 @@ public class RedisManager extends JedisPubSub {
config.setMaxIdle(0); config.setMaxIdle(0);
config.setTestOnBorrow(true); config.setTestOnBorrow(true);
config.setTestOnReturn(true); config.setTestOnReturn(true);
this.jedisPool = password.isEmpty() Set<String> redisSentinelNodes = new HashSet<>(plugin.getSettings().getRedisSentinelNodes());
? new JedisPool(config, host, port, 0, useSSL) if (redisSentinelNodes.isEmpty()) {
: new JedisPool(config, host, port, 0, password, useSSL); this.jedisPool = password.isEmpty()
? new JedisPool(config, host, port, 0, useSSL)
: new JedisPool(config, host, port, 0, password, useSSL);
} else {
String sentinelPassword = plugin.getSettings().getRedisSentinelPassword();
String redisSentinelMaster = plugin.getSettings().getRedisSentinelMaster();
this.jedisPool = new JedisSentinelPool(redisSentinelMaster, redisSentinelNodes, password.isEmpty() ? null : password, sentinelPassword.isEmpty() ? null : sentinelPassword);
}
// Ping the server to check the connection // Ping the server to check the connection
try { try {
jedisPool.getResource().ping(); jedisPool.getResource().ping();
} catch (JedisException e) { } catch (JedisException e) {
throw new IllegalStateException("Failed to establish connection with the Redis server. " throw new IllegalStateException("Failed to establish connection with Redis. "
+ "Please check the supplied credentials in the config file", e); + "Please check the supplied credentials in the config file", e);
} }
@@ -175,23 +179,23 @@ public class RedisManager extends JedisPubSub {
} }
/** /**
* Set a user's data to the Redis server * Set a user's data to Redis
* *
* @param user the user to set data for * @param user the user to set data for
* @param data the user's data to set * @param data the user's data to set
* @param timeToLive The time to cache the data for
*/ */
@Blocking @Blocking
public void setUserData(@NotNull User user, @NotNull DataSnapshot.Packed data) { public void setUserData(@NotNull User user, @NotNull DataSnapshot.Packed data, int timeToLive) {
try (Jedis jedis = jedisPool.getResource()) { try (Jedis jedis = jedisPool.getResource()) {
jedis.setex( jedis.setex(
getKey(RedisKeyType.DATA_UPDATE, user.getUuid(), clusterId), getKey(RedisKeyType.LATEST_SNAPSHOT, user.getUuid(), clusterId),
RedisKeyType.DATA_UPDATE.getTimeToLive(), timeToLive,
data.asBytes(plugin) data.asBytes(plugin)
); );
plugin.debug(String.format("[%s] Set %s key to redis at: %s", user.getUsername(), plugin.debug(String.format("[%s] Set %s key on Redis", user.getUsername(), RedisKeyType.LATEST_SNAPSHOT));
RedisKeyType.DATA_UPDATE.name(), new SimpleDateFormat("mm:ss.SSS").format(new Date())));
} catch (Throwable e) { } catch (Throwable e) {
plugin.log(Level.SEVERE, "An exception occurred setting a user's server switch", e); plugin.log(Level.SEVERE, "An exception occurred setting user data on Redis", e);
} }
} }
@@ -206,11 +210,10 @@ public class RedisManager extends JedisPubSub {
} else { } else {
jedis.del(getKey(RedisKeyType.DATA_CHECKOUT, user.getUuid(), clusterId)); jedis.del(getKey(RedisKeyType.DATA_CHECKOUT, user.getUuid(), clusterId));
} }
plugin.debug(String.format("[%s] %s %s key to redis at: %s", plugin.debug(String.format("[%s] %s %s key to/from Redis", user.getUsername(),
checkedOut ? "set" : "removed", user.getUsername(), RedisKeyType.DATA_CHECKOUT.name(), checkedOut ? "Set" : "Removed", RedisKeyType.DATA_CHECKOUT));
new SimpleDateFormat("mm:ss.SSS").format(new Date())));
} catch (Throwable e) { } catch (Throwable e) {
plugin.log(Level.SEVERE, "An exception occurred setting a user's server switch", e); plugin.log(Level.SEVERE, "An exception occurred setting checkout to", e);
} }
} }
@@ -220,17 +223,16 @@ public class RedisManager extends JedisPubSub {
final byte[] key = getKey(RedisKeyType.DATA_CHECKOUT, user.getUuid(), clusterId); final byte[] key = getKey(RedisKeyType.DATA_CHECKOUT, user.getUuid(), clusterId);
final byte[] readData = jedis.get(key); final byte[] readData = jedis.get(key);
if (readData != null) { if (readData != null) {
plugin.debug("[" + user.getUsername() + "] Successfully read " final String checkoutServer = new String(readData, StandardCharsets.UTF_8);
+ RedisKeyType.DATA_CHECKOUT.name() + " key from redis at: " + plugin.debug(String.format("[%s] Waiting for %s %s key to be unset on Redis",
new SimpleDateFormat("mm:ss.SSS").format(new Date())); user.getUsername(), checkoutServer, RedisKeyType.DATA_CHECKOUT));
return Optional.of(new String(readData, StandardCharsets.UTF_8)); return Optional.of(checkoutServer);
} }
} catch (Throwable e) { } catch (Throwable e) {
plugin.log(Level.SEVERE, "An exception occurred fetching a user's checkout key from redis", e); plugin.log(Level.SEVERE, "An exception occurred getting a user's checkout key from Redis", e);
} }
plugin.debug("[" + user.getUsername() + "] Could not read " + plugin.debug(String.format("[%s] %s key not set on Redis", user.getUsername(),
RedisKeyType.DATA_CHECKOUT.name() + " key from redis at: " + RedisKeyType.DATA_CHECKOUT));
new SimpleDateFormat("mm:ss.SSS").format(new Date()));
return Optional.empty(); return Optional.empty();
} }
@@ -240,7 +242,7 @@ public class RedisManager extends JedisPubSub {
try (Jedis jedis = jedisPool.getResource()) { try (Jedis jedis = jedisPool.getResource()) {
final Set<String> keys = jedis.keys(keyFormat); final Set<String> keys = jedis.keys(keyFormat);
if (keys == null) { if (keys == null) {
plugin.log(Level.WARNING, "Checkout key set returned null from jedis during clearing"); plugin.log(Level.WARNING, "Checkout key returned null from Redis during clearing");
return; return;
} }
for (String key : keys) { for (String key : keys) {
@@ -249,12 +251,12 @@ public class RedisManager extends JedisPubSub {
} }
} }
} catch (Throwable e) { } catch (Throwable e) {
plugin.log(Level.SEVERE, "An exception occurred clearing users checked out on this server", e); plugin.log(Level.SEVERE, "An exception occurred clearing this server's checkout keys on Redis", e);
} }
} }
/** /**
* Set a user's server switch to the Redis server * Set a user's server switch to Redis
* *
* @param user the user to set the server switch for * @param user the user to set the server switch for
*/ */
@@ -263,17 +265,18 @@ public class RedisManager extends JedisPubSub {
try (Jedis jedis = jedisPool.getResource()) { try (Jedis jedis = jedisPool.getResource()) {
jedis.setex( jedis.setex(
getKey(RedisKeyType.SERVER_SWITCH, user.getUuid(), clusterId), getKey(RedisKeyType.SERVER_SWITCH, user.getUuid(), clusterId),
RedisKeyType.SERVER_SWITCH.getTimeToLive(), new byte[0] RedisKeyType.TTL_10_SECONDS,
new byte[0]
); );
plugin.debug(String.format("[%s] Set %s key to redis at: %s", user.getUsername(), plugin.debug(String.format("[%s] Set %s key to Redis",
RedisKeyType.SERVER_SWITCH.name(), new SimpleDateFormat("mm:ss.SSS").format(new Date()))); user.getUsername(), RedisKeyType.SERVER_SWITCH));
} catch (Throwable e) { } catch (Throwable e) {
plugin.log(Level.SEVERE, "An exception occurred setting a user's server switch", e); plugin.log(Level.SEVERE, "An exception occurred setting a user's server switch key from Redis", e);
} }
} }
/** /**
* Fetch a user's data from the Redis server and consume the key if found * Fetch a user's data from Redis and consume the key if found
* *
* @param user The user to fetch data for * @param user The user to fetch data for
* @return The user's data, if it's present on the database. Otherwise, an empty optional. * @return The user's data, if it's present on the database. Otherwise, an empty optional.
@@ -281,17 +284,15 @@ public class RedisManager extends JedisPubSub {
@Blocking @Blocking
public Optional<DataSnapshot.Packed> getUserData(@NotNull User user) { public Optional<DataSnapshot.Packed> getUserData(@NotNull User user) {
try (Jedis jedis = jedisPool.getResource()) { try (Jedis jedis = jedisPool.getResource()) {
final byte[] key = getKey(RedisKeyType.DATA_UPDATE, user.getUuid(), clusterId); final byte[] key = getKey(RedisKeyType.LATEST_SNAPSHOT, user.getUuid(), clusterId);
final byte[] dataByteArray = jedis.get(key); final byte[] dataByteArray = jedis.get(key);
if (dataByteArray == null) { if (dataByteArray == null) {
plugin.debug("[" + user.getUsername() + "] Could not read " + plugin.debug(String.format("[%s] Waiting for %s key from Redis",
RedisKeyType.DATA_UPDATE.name() + " key from redis at: " + user.getUsername(), RedisKeyType.LATEST_SNAPSHOT));
new SimpleDateFormat("mm:ss.SSS").format(new Date()));
return Optional.empty(); return Optional.empty();
} }
plugin.debug("[" + user.getUsername() + "] Successfully read " plugin.debug(String.format("[%s] Read %s key from Redis",
+ RedisKeyType.DATA_UPDATE.name() + " key from redis at: " + user.getUsername(), RedisKeyType.LATEST_SNAPSHOT));
new SimpleDateFormat("mm:ss.SSS").format(new Date()));
// Consume the key (delete from redis) // Consume the key (delete from redis)
jedis.del(key); jedis.del(key);
@@ -299,7 +300,7 @@ public class RedisManager extends JedisPubSub {
// Use Snappy to decompress the json // Use Snappy to decompress the json
return Optional.of(DataSnapshot.deserialize(plugin, dataByteArray)); return Optional.of(DataSnapshot.deserialize(plugin, dataByteArray));
} catch (Throwable e) { } catch (Throwable e) {
plugin.log(Level.SEVERE, "An exception occurred fetching a user's data from redis", e); plugin.log(Level.SEVERE, "An exception occurred getting a user's data from Redis", e);
return Optional.empty(); return Optional.empty();
} }
} }
@@ -310,20 +311,18 @@ public class RedisManager extends JedisPubSub {
final byte[] key = getKey(RedisKeyType.SERVER_SWITCH, user.getUuid(), clusterId); final byte[] key = getKey(RedisKeyType.SERVER_SWITCH, user.getUuid(), clusterId);
final byte[] readData = jedis.get(key); final byte[] readData = jedis.get(key);
if (readData == null) { if (readData == null) {
plugin.debug("[" + user.getUsername() + "] Could not read " + plugin.debug(String.format("[%s] Waiting for %s key from Redis",
RedisKeyType.SERVER_SWITCH.name() + " key from redis at: " + user.getUsername(), RedisKeyType.SERVER_SWITCH));
new SimpleDateFormat("mm:ss.SSS").format(new Date()));
return false; return false;
} }
plugin.debug("[" + user.getUsername() + "] Successfully read " plugin.debug(String.format("[%s] Read %s key from Redis",
+ RedisKeyType.SERVER_SWITCH.name() + " key from redis at: " + user.getUsername(), RedisKeyType.SERVER_SWITCH));
new SimpleDateFormat("mm:ss.SSS").format(new Date()));
// Consume the key (delete from redis) // Consume the key (delete from redis)
jedis.del(key); jedis.del(key);
return true; return true;
} catch (Throwable e) { } catch (Throwable e) {
plugin.log(Level.SEVERE, "An exception occurred fetching a user's server switch from redis", e); plugin.log(Level.SEVERE, "An exception occurred getting a user's server switch from Redis", e);
return false; return false;
} }
} }

View File

@@ -114,6 +114,8 @@ public abstract class DataSyncer {
} }
if (plugin.isDisabling() || timesRun.getAndIncrement() > maxListenAttempts) { if (plugin.isDisabling() || timesRun.getAndIncrement() > maxListenAttempts) {
task.get().cancel(); task.get().cancel();
plugin.debug(String.format("[%s] Redis timed out after %s attempts; setting from database",
user.getUsername(), timesRun.get()));
setUserFromDatabase(user); setUserFromDatabase(user);
return; return;
} }
@@ -132,8 +134,8 @@ public abstract class DataSyncer {
* @since 3.1 * @since 3.1
*/ */
public enum Mode { public enum Mode {
DELAY(DelayDataSyncer::new), LOCKSTEP(LockstepDataSyncer::new),
LOCKSTEP(LockstepDataSyncer::new); DELAY(DelayDataSyncer::new);
private final Function<HuskSync, ? extends DataSyncer> supplier; private final Function<HuskSync, ? extends DataSyncer> supplier;

View File

@@ -21,6 +21,7 @@ package net.william278.husksync.sync;
import net.william278.husksync.HuskSync; import net.william278.husksync.HuskSync;
import net.william278.husksync.data.DataSnapshot; import net.william278.husksync.data.DataSnapshot;
import net.william278.husksync.redis.RedisKeyType;
import net.william278.husksync.user.OnlineUser; import net.william278.husksync.user.OnlineUser;
import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.NotNull;
@@ -61,7 +62,7 @@ public class DelayDataSyncer extends DataSyncer {
plugin.runAsync(() -> { plugin.runAsync(() -> {
plugin.getRedisManager().setUserServerSwitch(user); plugin.getRedisManager().setUserServerSwitch(user);
final DataSnapshot.Packed data = user.createSnapshot(DataSnapshot.SaveCause.DISCONNECT); final DataSnapshot.Packed data = user.createSnapshot(DataSnapshot.SaveCause.DISCONNECT);
plugin.getRedisManager().setUserData(user, data); plugin.getRedisManager().setUserData(user, data, RedisKeyType.TTL_10_SECONDS);
plugin.getDatabase().addSnapshot(user, data); plugin.getDatabase().addSnapshot(user, data);
}); });
} }

View File

@@ -21,6 +21,7 @@ package net.william278.husksync.sync;
import net.william278.husksync.HuskSync; import net.william278.husksync.HuskSync;
import net.william278.husksync.data.DataSnapshot; import net.william278.husksync.data.DataSnapshot;
import net.william278.husksync.redis.RedisKeyType;
import net.william278.husksync.user.OnlineUser; import net.william278.husksync.user.OnlineUser;
import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.NotNull;
@@ -60,7 +61,7 @@ public class LockstepDataSyncer extends DataSyncer {
public void saveUserData(@NotNull OnlineUser user) { public void saveUserData(@NotNull OnlineUser user) {
plugin.runAsync(() -> { plugin.runAsync(() -> {
final DataSnapshot.Packed data = user.createSnapshot(DataSnapshot.SaveCause.DISCONNECT); final DataSnapshot.Packed data = user.createSnapshot(DataSnapshot.SaveCause.DISCONNECT);
plugin.getRedisManager().setUserData(user, data); plugin.getRedisManager().setUserData(user, data, RedisKeyType.TTL_1_YEAR);
plugin.getRedisManager().setUserCheckedOut(user, false); plugin.getRedisManager().setUserCheckedOut(user, false);
plugin.getDatabase().addSnapshot(user, data); plugin.getDatabase().addSnapshot(user, data);
}); });

View File

@@ -20,6 +20,7 @@
package net.william278.husksync.user; package net.william278.husksync.user;
import net.kyori.adventure.audience.Audience; import net.kyori.adventure.audience.Audience;
import net.kyori.adventure.platform.AudienceProvider;
import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.NotNull;
public final class ConsoleUser implements CommandUser { public final class ConsoleUser implements CommandUser {
@@ -27,8 +28,8 @@ public final class ConsoleUser implements CommandUser {
@NotNull @NotNull
private final Audience audience; private final Audience audience;
public ConsoleUser(@NotNull Audience console) { public ConsoleUser(@NotNull AudienceProvider audiences) {
this.audience = console; this.audience = audiences.console();
} }
@Override @Override

View File

@@ -50,13 +50,11 @@ public abstract class OnlineUser extends User implements CommandUser, UserDataHo
*/ */
public abstract boolean isOffline(); public abstract boolean isOffline();
/**
* Get the player's adventure {@link Audience}
*
* @return the player's {@link Audience}
*/
@NotNull @NotNull
public abstract Audience getAudience(); @Override
public Audience getAudience() {
return getPlugin().getAudience(getUuid());
}
/** /**
* Send a message to this player * Send a message to this player
@@ -131,6 +129,9 @@ public abstract class OnlineUser extends User implements CommandUser, UserDataHo
public void applySnapshot(@NotNull DataSnapshot.Packed snapshot, @NotNull DataSnapshot.UpdateCause cause) { public void applySnapshot(@NotNull DataSnapshot.Packed snapshot, @NotNull DataSnapshot.UpdateCause cause) {
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)",
snapshot.getShortId(), getUsername(), cause
));
UserDataHolder.super.applySnapshot( UserDataHolder.super.applySnapshot(
event.getData(), (succeeded) -> completeSync(succeeded, cause, getPlugin()) event.getData(), (succeeded) -> completeSync(succeeded, cause, getPlugin())
); );

View File

@@ -11,7 +11,7 @@ data_manager_title: '[Преглеждане потребителският сн
data_manager_timestamp: '[⌚ %1%](#ffc43b-#f5c962 show_text=&7Клеймо на Версията:\n&8Когато данните са били запазени)' data_manager_timestamp: '[⌚ %1%](#ffc43b-#f5c962 show_text=&7Клеймо на Версията:\n&8Когато данните са били запазени)'
data_manager_pinned: '[※ Закачен снапшот](#d8ff2b show_text=&7Закачен:\n&8Снапшота на този потребител няма да бъде автоматично завъртан.)' data_manager_pinned: '[※ Закачен снапшот](#d8ff2b show_text=&7Закачен:\n&8Снапшота на този потребител няма да бъде автоматично завъртан.)'
data_manager_cause: '[⚑ %1%](#23a825-#36f539 show_text=&7Причина на Запазване:\n&8Какво е накарало данните да бъдат запазени)' data_manager_cause: '[⚑ %1%](#23a825-#36f539 show_text=&7Причина на Запазване:\n&8Какво е накарало данните да бъдат запазени)'
data_manager_server: '[ %1%](#ff87b3-#f5538e show_text=&7Server:\n&8Name of the server the data was saved on)' data_manager_server: '[ %1%](#ff87b3-#f5538e show_text=&7Server:\n&8Name of the server the data was saved on)'
data_manager_size: '[⏏ %1%](color=#62a9f5-#7ab8fa show_text=&7Snapshot size:\n&8Estimated file size of the snapshot (in KiB))\n' data_manager_size: '[⏏ %1%](color=#62a9f5-#7ab8fa show_text=&7Snapshot size:\n&8Estimated file size of the snapshot (in KiB))\n'
data_manger_status: '[%1%](red)[/](gray)[%2%](red)[×](gray)[❤](red show_text=&7Точки кръв) [%3%](yellow)[×](gray)[🍖](yellow show_text=&7Точки глад) [ʟᴠ](green)[.](gray)[%4%](green show_text=&7Ниво опит) [🏹 %5%](dark_aqua show_text=&7Режим на игра)' data_manger_status: '[%1%](red)[/](gray)[%2%](red)[×](gray)[❤](red show_text=&7Точки кръв) [%3%](yellow)[×](gray)[🍖](yellow show_text=&7Точки глад) [ʟᴠ](green)[.](gray)[%4%](green show_text=&7Ниво опит) [🏹 %5%](dark_aqua show_text=&7Режим на игра)'
data_manager_advancements_statistics: '[⭐ Напредъци: %1%](color=#ffc43b-#f5c962 show_text=&7Напредъци, в които имате прогрес:\n&8%2%) [⌛ Изиграно Време: %3%ʜʀs](color=#62a9f5-#7ab8fa show_text=&7Изиграно време в играта\n&8⚠ Базирано на статистики от играта)\n' data_manager_advancements_statistics: '[⭐ Напредъци: %1%](color=#ffc43b-#f5c962 show_text=&7Напредъци, в които имате прогрес:\n&8%2%) [⌛ Изиграно Време: %3%ʜʀs](color=#62a9f5-#7ab8fa show_text=&7Изиграно време в играта\n&8⚠ Базирано на статистики от играта)\n'

View File

@@ -11,7 +11,7 @@ data_manager_title: '[Du siehst den Nutzerdaten-Schnappschuss](#00fb9a) [%1%](#0
data_manager_timestamp: '[⌚ %1%](#ffc43b-#f5c962 show_text=&7Versions-Zeitstempel:\n&8Zeitpunkt der Speicherung der Daten)' data_manager_timestamp: '[⌚ %1%](#ffc43b-#f5c962 show_text=&7Versions-Zeitstempel:\n&8Zeitpunkt der Speicherung der Daten)'
data_manager_pinned: '[※ Schnappschuss angeheftet](#d8ff2b show_text=&7Angeheftet:\n&8Dieser Nutzerdaten-Schnappschuss wird nicht automatisch rotiert.)' data_manager_pinned: '[※ Schnappschuss angeheftet](#d8ff2b show_text=&7Angeheftet:\n&8Dieser Nutzerdaten-Schnappschuss wird nicht automatisch rotiert.)'
data_manager_cause: '[⚑ %1%](#23a825-#36f539 show_text=&7Speicherungsgrund:\n&8Der Grund für das Speichern der Daten)' data_manager_cause: '[⚑ %1%](#23a825-#36f539 show_text=&7Speicherungsgrund:\n&8Der Grund für das Speichern der Daten)'
data_manager_server: '[ %1%](#ff87b3-#f5538e show_text=&7Server:\n&8Name des Servers, auf dem die Daten gespeichert wurden)' data_manager_server: '[ %1%](#ff87b3-#f5538e show_text=&7Server:\n&8Name des Servers, auf dem die Daten gespeichert wurden)'
data_manager_size: '[⏏ %1%](color=#62a9f5-#7ab8fa show_text=&7Schnappschuss-Größe:\n&8Geschätzte Dateigröße des Schnappschusses (in KiB))\n' data_manager_size: '[⏏ %1%](color=#62a9f5-#7ab8fa show_text=&7Schnappschuss-Größe:\n&8Geschätzte Dateigröße des Schnappschusses (in KiB))\n'
data_manger_status: '[%1%](red)[/](gray)[%2%](red)[×](gray)[❤](red show_text=&7Lebenspunkte) [%3%](yellow)[×](gray)[🍖](yellow show_text=&7Hungerpunkte) [ʟᴠ](green)[.](gray)[%4%](green show_text=&7XP-Level) [🏹 %5%](dark_aqua show_text=&7Spielmodus)' data_manger_status: '[%1%](red)[/](gray)[%2%](red)[×](gray)[❤](red show_text=&7Lebenspunkte) [%3%](yellow)[×](gray)[🍖](yellow show_text=&7Hungerpunkte) [ʟᴠ](green)[.](gray)[%4%](green show_text=&7XP-Level) [🏹 %5%](dark_aqua show_text=&7Spielmodus)'
data_manager_advancements_statistics: '[⭐ Erfolge: %1%](color=#ffc43b-#f5c962 show_text=&7Erfolge in denen du Fortschritt gemacht hast:\n&8%2%) [⌛ Spielzeit: %3%ʜʀs](color=#62a9f5-#7ab8fa show_text=&7Deine verbrachte Zeit im Spiel\n&8⚠ Basierend auf Spielstatistiken)\n' data_manager_advancements_statistics: '[⭐ Erfolge: %1%](color=#ffc43b-#f5c962 show_text=&7Erfolge in denen du Fortschritt gemacht hast:\n&8%2%) [⌛ Spielzeit: %3%ʜʀs](color=#62a9f5-#7ab8fa show_text=&7Deine verbrachte Zeit im Spiel\n&8⚠ Basierend auf Spielstatistiken)\n'

View File

@@ -11,7 +11,7 @@ data_manager_title: '[Viewing user data snapshot](#00fb9a) [%1%](#00fb9a show_te
data_manager_timestamp: '[⌚ %1%](#ffc43b-#f5c962 show_text=&7Version timestamp:\n&8When the data was saved)' data_manager_timestamp: '[⌚ %1%](#ffc43b-#f5c962 show_text=&7Version timestamp:\n&8When the data was saved)'
data_manager_pinned: '[※ Snapshot pinned](#d8ff2b show_text=&7Pinned:\n&8This user data snapshot won''t be automatically rotated.)' data_manager_pinned: '[※ Snapshot pinned](#d8ff2b show_text=&7Pinned:\n&8This user data snapshot won''t be automatically rotated.)'
data_manager_cause: '[⚑ %1%](#23a825-#36f539 show_text=&7Save cause:\n&8What caused the data to be saved)' data_manager_cause: '[⚑ %1%](#23a825-#36f539 show_text=&7Save cause:\n&8What caused the data to be saved)'
data_manager_server: '[ %1%](#ff87b3-#f5538e show_text=&7Server:\n&8Name of the server the data was saved on)' data_manager_server: '[ %1%](#ff87b3-#f5538e show_text=&7Server:\n&8Name of the server the data was saved on)'
data_manager_size: '[⏏ %1%](color=#62a9f5-#7ab8fa show_text=&7Snapshot size:\n&8Estimated file size of the snapshot (in KiB))\n' data_manager_size: '[⏏ %1%](color=#62a9f5-#7ab8fa show_text=&7Snapshot size:\n&8Estimated file size of the snapshot (in KiB))\n'
data_manger_status: '[%1%](red)[/](gray)[%2%](red)[×](gray)[❤](red show_text=&7Health points) [%3%](yellow)[×](gray)[🍖](yellow show_text=&7Hunger points) [ʟᴠ](green)[.](gray)[%4%](green show_text=&7XP level) [🏹 %5%](dark_aqua show_text=&7Game mode)' data_manger_status: '[%1%](red)[/](gray)[%2%](red)[×](gray)[❤](red show_text=&7Health points) [%3%](yellow)[×](gray)[🍖](yellow show_text=&7Hunger points) [ʟᴠ](green)[.](gray)[%4%](green show_text=&7XP level) [🏹 %5%](dark_aqua show_text=&7Game mode)'
data_manager_advancements_statistics: '[⭐ Advancements: %1%](color=#ffc43b-#f5c962 show_text=&7Advancements you have progress in:\n&8%2%) [⌛ Play Time: %3%ʜʀs](color=#62a9f5-#7ab8fa show_text=&7In-game play time\n&8⚠ Based on in-game statistics)\n' data_manager_advancements_statistics: '[⭐ Advancements: %1%](color=#ffc43b-#f5c962 show_text=&7Advancements you have progress in:\n&8%2%) [⌛ Play Time: %3%ʜʀs](color=#62a9f5-#7ab8fa show_text=&7In-game play time\n&8⚠ Based on in-game statistics)\n'

View File

@@ -11,7 +11,7 @@ data_manager_title: '[Viendo una snapshot sobre la informacion del jugador](#00f
data_manager_timestamp: '[⌚ %1%](#ffc43b-#f5c962 show_text=&7Version del registro:\n&8Cuando los datos se han guardado)' data_manager_timestamp: '[⌚ %1%](#ffc43b-#f5c962 show_text=&7Version del registro:\n&8Cuando los datos se han guardado)'
data_manager_pinned: '[※ Snapshot anclada](#d8ff2b show_text=&Anclado:\n&8La informacion de este jugador no se rotará automaticamente.)' data_manager_pinned: '[※ Snapshot anclada](#d8ff2b show_text=&Anclado:\n&8La informacion de este jugador no se rotará automaticamente.)'
data_manager_cause: '[⚑ %1%](#23a825-#36f539 show_text=&7Motivo del guardado:\n&8Lo que ha causado que se guarde)' data_manager_cause: '[⚑ %1%](#23a825-#36f539 show_text=&7Motivo del guardado:\n&8Lo que ha causado que se guarde)'
data_manager_server: '[ %1%](#ff87b3-#f5538e show_text=&7Server:\n&8Name of the server the data was saved on)' data_manager_server: '[ %1%](#ff87b3-#f5538e show_text=&7Server:\n&8Name of the server the data was saved on)'
data_manager_size: '[⏏ %1%](color=#62a9f5-#7ab8fa show_text=&7Snapshot size:\n&8Estimated file size of the snapshot (in KiB))\n' data_manager_size: '[⏏ %1%](color=#62a9f5-#7ab8fa show_text=&7Snapshot size:\n&8Estimated file size of the snapshot (in KiB))\n'
data_manger_status: '[%1%](red)[/](gray)[%2%](red)[×](gray)[❤](red show_text=&7Puntos de vida) [%3%](yellow)[×](gray)[🍖](yellow show_text=&7Puntos de hambre) [ʟᴠ](green)[.](gray)[%4%](green show_text=&7Nivel de exp) [🏹 %5%](dark_aqua show_text=&7Gamemode)' data_manger_status: '[%1%](red)[/](gray)[%2%](red)[×](gray)[❤](red show_text=&7Puntos de vida) [%3%](yellow)[×](gray)[🍖](yellow show_text=&7Puntos de hambre) [ʟᴠ](green)[.](gray)[%4%](green show_text=&7Nivel de exp) [🏹 %5%](dark_aqua show_text=&7Gamemode)'
data_manager_advancements_statistics: '[⭐ Logros: %1%](color=#ffc43b-#f5c962 show_text=&7Logros que has conseguido:\n&8%2%) [⌛ Tiempo de juego: %3%ʜʀs](color=#62a9f5-#7ab8fa show_text=&7In-game play time\n&8⚠ Based on in-game statistics)\n' data_manager_advancements_statistics: '[⭐ Logros: %1%](color=#ffc43b-#f5c962 show_text=&7Logros que has conseguido:\n&8%2%) [⌛ Tiempo de juego: %3%ʜʀs](color=#62a9f5-#7ab8fa show_text=&7In-game play time\n&8⚠ Based on in-game statistics)\n'

View File

@@ -11,7 +11,7 @@ data_manager_title: '[Stai vedendo l''istantanea](#00fb9a) [%1%](#00fb9a show_te
data_manager_timestamp: '[⌚ %1%](#ffc43b-#f5c962 show_text=&7:\n&8Quando i dati sono stati salvati)' data_manager_timestamp: '[⌚ %1%](#ffc43b-#f5c962 show_text=&7:\n&8Quando i dati sono stati salvati)'
data_manager_pinned: '[※ Istantanea fissata](#d8ff2b show_text=&7Pinned:\n&8Quest''istantanea non sarà cancellata automaticamente.)' data_manager_pinned: '[※ Istantanea fissata](#d8ff2b show_text=&7Pinned:\n&8Quest''istantanea non sarà cancellata automaticamente.)'
data_manager_cause: '[⚑ %1%](#23a825-#36f539 show_text=&7Causa di salvataggio:\n&8Cosa ha causato il salvataggio dei dati)' data_manager_cause: '[⚑ %1%](#23a825-#36f539 show_text=&7Causa di salvataggio:\n&8Cosa ha causato il salvataggio dei dati)'
data_manager_server: '[ %1%](#ff87b3-#f5538e show_text=&7Server:\n&8Name of the server the data was saved on)' data_manager_server: '[ %1%](#ff87b3-#f5538e show_text=&7Server:\n&8Name of the server the data was saved on)'
data_manager_size: '[⏏ %1%](color=#62a9f5-#7ab8fa show_text=&7Peso dell''istantanea:\n&8Peso stimato del file (in KiB))\n' data_manager_size: '[⏏ %1%](color=#62a9f5-#7ab8fa show_text=&7Peso dell''istantanea:\n&8Peso stimato del file (in KiB))\n'
data_manger_status: '[%1%](red)[/](gray)[%2%](red)[×](gray)[❤](red show_text=&7Vita) [%3%](yellow)[×](gray)[🍖](yellow show_text=&7Fame) [ʟᴠ](green)[.](gray)[%4%](green show_text=&7Livello di XP) [🏹 %5%](dark_aqua show_text=&7Modalità di gioco)' data_manger_status: '[%1%](red)[/](gray)[%2%](red)[×](gray)[❤](red show_text=&7Vita) [%3%](yellow)[×](gray)[🍖](yellow show_text=&7Fame) [ʟᴠ](green)[.](gray)[%4%](green show_text=&7Livello di XP) [🏹 %5%](dark_aqua show_text=&7Modalità di gioco)'
data_manager_advancements_statistics: '[⭐ Progressi: %1%](color=#ffc43b-#f5c962 show_text=&7Progressi compiuti in:\n&8%2%) [⌛ Tempo di gioco: %3%ʜʀs](color=#62a9f5-#7ab8fa show_text=&7Tempo di gioco\n&8⚠ Basato sulle statistiche di gioco)\n' data_manager_advancements_statistics: '[⭐ Progressi: %1%](color=#ffc43b-#f5c962 show_text=&7Progressi compiuti in:\n&8%2%) [⌛ Tempo di gioco: %3%ʜʀs](color=#62a9f5-#7ab8fa show_text=&7Tempo di gioco\n&8⚠ Basato sulle statistiche di gioco)\n'

View File

@@ -11,7 +11,7 @@ data_manager_title: '[%3%](#00fb9a bold show_text=&7プレイヤーUUID:\n&8%4%)
data_manager_timestamp: '[⌚ %1%](#ffc43b-#f5c962 show_text=&7バージョンタイムスタンプ:\n&8データの保存時期)' data_manager_timestamp: '[⌚ %1%](#ffc43b-#f5c962 show_text=&7バージョンタイムスタンプ:\n&8データの保存時期)'
data_manager_pinned: '[※ ピン留めされたスナップショット](#d8ff2b show_text=&7ピン留め:\n&8このユーザーデータのスナップショットは自動的にローテーションされません。)' data_manager_pinned: '[※ ピン留めされたスナップショット](#d8ff2b show_text=&7ピン留め:\n&8このユーザーデータのスナップショットは自動的にローテーションされません。)'
data_manager_cause: '[⚑ %1%](#23a825-#36f539 show_text=&7保存理由:\n&8データが保存された理由)' data_manager_cause: '[⚑ %1%](#23a825-#36f539 show_text=&7保存理由:\n&8データが保存された理由)'
data_manager_server: '[ %1%](#ff87b3-#f5538e show_text=&7Server:\n&8Name of the server the data was saved on)' data_manager_server: '[ %1%](#ff87b3-#f5538e show_text=&7Server:\n&8Name of the server the data was saved on)'
data_manager_size: '[⏏ %1%](color=#62a9f5-#7ab8fa show_text=&7スナップショットサイズ:\n&8スナップショットの推定ファイルサイズ単位:KiB)\n' data_manager_size: '[⏏ %1%](color=#62a9f5-#7ab8fa show_text=&7スナップショットサイズ:\n&8スナップショットの推定ファイルサイズ単位:KiB)\n'
data_manger_status: '[%1%](red)[/](gray)[%2%](red)[×](gray)[❤](red show_text=&7体力) [%3%](yellow)[×](gray)[🍖](yellow show_text=&7空腹度) [ʟᴠ](green)[.](gray)[%4%](green show_text=&7経験値レベル) [🏹 %5%](dark_aqua show_text=&7ゲームモード)' data_manger_status: '[%1%](red)[/](gray)[%2%](red)[×](gray)[❤](red show_text=&7体力) [%3%](yellow)[×](gray)[🍖](yellow show_text=&7空腹度) [ʟᴠ](green)[.](gray)[%4%](green show_text=&7経験値レベル) [🏹 %5%](dark_aqua show_text=&7ゲームモード)'
data_manager_advancements_statistics: '[⭐ 進捗: %1%](color=#ffc43b-#f5c962 show_text=&7達成した進捗:\n&8%2%) [⌛ プレイ時間: %3%ʜʀs](color=#62a9f5-#7ab8fa show_text=&7ゲーム内のプレイ時間\n&8⚠ ゲーム内の統計に基づく)\n' data_manager_advancements_statistics: '[⭐ 進捗: %1%](color=#ffc43b-#f5c962 show_text=&7達成した進捗:\n&8%2%) [⌛ プレイ時間: %3%ʜʀs](color=#62a9f5-#7ab8fa show_text=&7ゲーム内のプレイ時間\n&8⚠ ゲーム内の統計に基づく)\n'

View File

@@ -11,7 +11,7 @@ data_manager_title: '[%3%](#00fb9a bold show_text=&7플레이어 UUID:\n&8%4%)[
data_manager_timestamp: '[⌚ %1%](#ffc43b-#f5c962 show_text=&7저장 시각:\n&8데이터가 저장된 시각)' data_manager_timestamp: '[⌚ %1%](#ffc43b-#f5c962 show_text=&7저장 시각:\n&8데이터가 저장된 시각)'
data_manager_pinned: '[※ 스냅샷 고정됨](#d8ff2b show_text=&7고정됨:\n&8이 유저의 데이터 스냅샷은 자동으로 갱신되지 않습니다.)' data_manager_pinned: '[※ 스냅샷 고정됨](#d8ff2b show_text=&7고정됨:\n&8이 유저의 데이터 스냅샷은 자동으로 갱신되지 않습니다.)'
data_manager_cause: '[⚑ %1%](#23a825-#36f539 show_text=&7저장 사유:\n&8데이터가 저장된 사유입니다.)' data_manager_cause: '[⚑ %1%](#23a825-#36f539 show_text=&7저장 사유:\n&8데이터가 저장된 사유입니다.)'
data_manager_server: '[ %1%](#ff87b3-#f5538e show_text=&7서버:\n&8데이터 저장이 이루어진 서버입니다.)' data_manager_server: '[ %1%](#ff87b3-#f5538e show_text=&7서버:\n&8데이터 저장이 이루어진 서버입니다.)'
data_manager_size: '[⏏ %1%](color=#62a9f5-#7ab8fa show_text=&7스냅샷 크기:\n&8스냅샷 파일의 대략적인 크기입니다. (단위 KiB))\n' data_manager_size: '[⏏ %1%](color=#62a9f5-#7ab8fa show_text=&7스냅샷 크기:\n&8스냅샷 파일의 대략적인 크기입니다. (단위 KiB))\n'
data_manger_status: '[%1%](red)[/](gray)[%2%](red)[×](gray)[❤](red show_text=&7체력) [%3%](yellow)[×](gray)[🍖](yellow show_text=&7허기) [ʟᴠ](green)[.](gray)[%4%](green show_text=&7경험치 레벨) [🏹 %5%](dark_aqua show_text=&7게임 모드)' data_manger_status: '[%1%](red)[/](gray)[%2%](red)[×](gray)[❤](red show_text=&7체력) [%3%](yellow)[×](gray)[🍖](yellow show_text=&7허기) [ʟᴠ](green)[.](gray)[%4%](green show_text=&7경험치 레벨) [🏹 %5%](dark_aqua show_text=&7게임 모드)'
data_manager_advancements_statistics: '[⭐ 도전 과제: %1%](color=#ffc43b-#f5c962 show_text=&7진행한 도전 과제:\n&8%2%) [⌛ 플레이 타임: %3%ʜʀs](color=#62a9f5-#7ab8fa show_text=&7인게임 플레이 시간\n&8⚠ 인게임 통계에 기반합니다.)\n' data_manager_advancements_statistics: '[⭐ 도전 과제: %1%](color=#ffc43b-#f5c962 show_text=&7진행한 도전 과제:\n&8%2%) [⌛ 플레이 타임: %3%ʜʀs](color=#62a9f5-#7ab8fa show_text=&7인게임 플레이 시간\n&8⚠ 인게임 통계에 기반합니다.)\n'

View File

@@ -11,7 +11,7 @@ data_manager_title: '[Momentopname van gebruikersgegevens bekijken](#00fb9a) [%1
data_manager_timestamp: '[⌚ %1%](#ffc43b-#f5c962 show_text=&7Versie tijdmarkering:\n&8Toen de gegevens werden opgeslagen)' data_manager_timestamp: '[⌚ %1%](#ffc43b-#f5c962 show_text=&7Versie tijdmarkering:\n&8Toen de gegevens werden opgeslagen)'
data_manager_pinned: '[※ Momentopname vastgezet](#d8ff2b show_text=&7Vastgezet:\n&8Deze momentopname van gebruikersgegevens wordt niet automatisch gerouleerd.)' data_manager_pinned: '[※ Momentopname vastgezet](#d8ff2b show_text=&7Vastgezet:\n&8Deze momentopname van gebruikersgegevens wordt niet automatisch gerouleerd.)'
data_manager_cause: '[⚑ %1%](#23a825-#36f539 show_text=&7Reden opslaan:\n&8Waarom de data is opgeslagen)' data_manager_cause: '[⚑ %1%](#23a825-#36f539 show_text=&7Reden opslaan:\n&8Waarom de data is opgeslagen)'
data_manager_server: '[ %1%](#ff87b3-#f5538e show_text=&7Server:\n&8Name of the server the data was saved on)' data_manager_server: '[ %1%](#ff87b3-#f5538e show_text=&7Server:\n&8Name of the server the data was saved on)'
data_manager_size: '[⏏ %1%](color=#62a9f5-#7ab8fa show_text=&7Grootte van momentopname:\n&8Geschatte bestandsgrootte van de momentopname (in KiB))\n' data_manager_size: '[⏏ %1%](color=#62a9f5-#7ab8fa show_text=&7Grootte van momentopname:\n&8Geschatte bestandsgrootte van de momentopname (in KiB))\n'
data_manger_status: '[%1%](red)[/](gray)[%2%](red)[×](gray)[❤](red show_text=&7Gezondheids punten) [%3%](yellow)[×](gray)[🍖](yellow show_text=&7Honger punten) [ʟᴠ](green)[.](gray)[%4%](green show_text=&7XP level) [🏹 %5%](dark_aqua show_text=&7Speltype)' data_manger_status: '[%1%](red)[/](gray)[%2%](red)[×](gray)[❤](red show_text=&7Gezondheids punten) [%3%](yellow)[×](gray)[🍖](yellow show_text=&7Honger punten) [ʟᴠ](green)[.](gray)[%4%](green show_text=&7XP level) [🏹 %5%](dark_aqua show_text=&7Speltype)'
data_manager_advancements_statistics: '[⭐ Advancements: %1%](color=#ffc43b-#f5c962 show_text=&7Advancements waarin je voortgang hebt:\n&8%2%) [⌛ Speeltijd: %3%uren](color=#62a9f5-#7ab8fa show_text=&7In-game speeltijd\n&8⚠ Gebaseerd op in-game statistieken)\n' data_manager_advancements_statistics: '[⭐ Advancements: %1%](color=#ffc43b-#f5c962 show_text=&7Advancements waarin je voortgang hebt:\n&8%2%) [⌛ Speeltijd: %3%uren](color=#62a9f5-#7ab8fa show_text=&7In-game speeltijd\n&8⚠ Gebaseerd op in-game statistieken)\n'

View File

@@ -11,7 +11,7 @@ data_manager_title: '[Visualizando snapshot dos dados do usuário](#00fb9a) [%1%
data_manager_timestamp: '[⌚ %1%](#ffc43b-#f5c962 show_text=&7Version timestamp:\n&8Quando os dados foram salvos)' data_manager_timestamp: '[⌚ %1%](#ffc43b-#f5c962 show_text=&7Version timestamp:\n&8Quando os dados foram salvos)'
data_manager_pinned: '[※ Snapshot marcada](#d8ff2b show_text=&7Marcada:\n&8Essa snapshot de dados do usuário não será girada automaticamente.)' data_manager_pinned: '[※ Snapshot marcada](#d8ff2b show_text=&7Marcada:\n&8Essa snapshot de dados do usuário não será girada automaticamente.)'
data_manager_cause: '[⚑ %1%](#23a825-#36f539 show_text=&7Causa do salvamento:\n&8O motivo para que os dados fossem salvos)' data_manager_cause: '[⚑ %1%](#23a825-#36f539 show_text=&7Causa do salvamento:\n&8O motivo para que os dados fossem salvos)'
data_manager_server: '[ %1%](#ff87b3-#f5538e show_text=&7Server:\n&8Name of the server the data was saved on)' data_manager_server: '[ %1%](#ff87b3-#f5538e show_text=&7Server:\n&8Name of the server the data was saved on)'
data_manager_size: '[⏏ %1%](color=#62a9f5-#7ab8fa show_text=&7Snapshot size:\n&8Estimated file size of the snapshot (in KiB))\n' data_manager_size: '[⏏ %1%](color=#62a9f5-#7ab8fa show_text=&7Snapshot size:\n&8Estimated file size of the snapshot (in KiB))\n'
data_manger_status: '[%1%](red)[/](gray)[%2%](red)[×](gray)[❤](red show_text=&7Pontos de Vida) [%3%](yellow)[×](gray)[🍖](yellow show_text=&7Pontos de vida) [ʟᴠ](green)[.](gray)[%4%](green show_text=&7XP level) [🏹 %5%](dark_aqua show_text=&7Game mode)' data_manger_status: '[%1%](red)[/](gray)[%2%](red)[×](gray)[❤](red show_text=&7Pontos de Vida) [%3%](yellow)[×](gray)[🍖](yellow show_text=&7Pontos de vida) [ʟᴠ](green)[.](gray)[%4%](green show_text=&7XP level) [🏹 %5%](dark_aqua show_text=&7Game mode)'
data_manager_advancements_statistics: '[⭐ Progressos: %1%](color=#ffc43b-#f5c962 show_text=&7Progressos que você tem realizado em:\n&8%2%) [⌛ Tempo de jogo: %3%ʜʀs](color=#62a9f5-#7ab8fa show_text=&7Tempo de jogo dentro do jogo\n&8⚠ Com base em estatísticas dentro do jogo)\n' data_manager_advancements_statistics: '[⭐ Progressos: %1%](color=#ffc43b-#f5c962 show_text=&7Progressos que você tem realizado em:\n&8%2%) [⌛ Tempo de jogo: %3%ʜʀs](color=#62a9f5-#7ab8fa show_text=&7Tempo de jogo dentro do jogo\n&8⚠ Com base em estatísticas dentro do jogo)\n'

View File

@@ -11,7 +11,7 @@ data_manager_title: '[Просмотр снимка данных](#00fb9a) [%1%]
data_manager_timestamp: '[⌚ %1%](#ffc43b-#f5c962 show_text=&7Время:\n&8Когда данные были сохранены)' data_manager_timestamp: '[⌚ %1%](#ffc43b-#f5c962 show_text=&7Время:\n&8Когда данные были сохранены)'
data_manager_pinned: '[※ Снимок закреплен](#d8ff2b show_text=&7Закреплен:\n&8Этот снимок данных пользователя не будет автоматически применено.)' data_manager_pinned: '[※ Снимок закреплен](#d8ff2b show_text=&7Закреплен:\n&8Этот снимок данных пользователя не будет автоматически применено.)'
data_manager_cause: '[⚑ %1%](#23a825-#36f539 show_text=&7Причина сохранения:\n&8Что привело к сохранению данных)' data_manager_cause: '[⚑ %1%](#23a825-#36f539 show_text=&7Причина сохранения:\n&8Что привело к сохранению данных)'
data_manager_server: '[ %1%](#ff87b3-#f5538e show_text=&7Сервер:\n&8Название сервера где данные были сохранены)' data_manager_server: '[ %1%](#ff87b3-#f5538e show_text=&7Сервер:\n&8Название сервера где данные были сохранены)'
data_manager_size: '[⏏ %1%](color=#62a9f5-#7ab8fa show_text=&7Размер:\n&8Предполагаемый размер снимка (в килобайтах))\n' data_manager_size: '[⏏ %1%](color=#62a9f5-#7ab8fa show_text=&7Размер:\n&8Предполагаемый размер снимка (в килобайтах))\n'
data_manger_status: '[%1%](red)[/](gray)[%2%](red)[×](gray)[❤](red show_text=&7Здоровье) [%3%](yellow)[×](gray)[🍖](yellow show_text=&7Голод) [ʟᴠ](green)[.](gray)[%4%](green show_text=&7Уровень опыта) [🏹 %5%](dark_aqua show_text=&7Игровой режим)' data_manger_status: '[%1%](red)[/](gray)[%2%](red)[×](gray)[❤](red show_text=&7Здоровье) [%3%](yellow)[×](gray)[🍖](yellow show_text=&7Голод) [ʟᴠ](green)[.](gray)[%4%](green show_text=&7Уровень опыта) [🏹 %5%](dark_aqua show_text=&7Игровой режим)'
data_manager_advancements_statistics: '[⭐ Достижения: %1%](color=#ffc43b-#f5c962 show_text=&7Полученные вами достижения:\n&8%2%) [⌛ Отыгранное время: %3%ʜʀs](color=#62a9f5-#7ab8fa show_text=&7Сколько времени вы отыграли\n&8⚠ Строится по внутреигровой статистике)\n' data_manager_advancements_statistics: '[⭐ Достижения: %1%](color=#ffc43b-#f5c962 show_text=&7Полученные вами достижения:\n&8%2%) [⌛ Отыгранное время: %3%ʜʀs](color=#62a9f5-#7ab8fa show_text=&7Сколько времени вы отыграли\n&8⚠ Строится по внутреигровой статистике)\n'

View File

@@ -11,7 +11,7 @@ data_manager_title: '[Kullanıcı veri anlık görünümü](#00fb9a) [%1%](#00fb
data_manager_timestamp: '[⌚ %1%](#ffc43b-#f5c962 show_text=&7Versiyon zaman damgası:\n&8Verinin ne zaman kaydedildiği)' data_manager_timestamp: '[⌚ %1%](#ffc43b-#f5c962 show_text=&7Versiyon zaman damgası:\n&8Verinin ne zaman kaydedildiği)'
data_manager_pinned: '[※ Anlık sabitlendi](#d8ff2b show_text=&7Sabitlendi:\n&8Bu kullanıcı veri anlığı otomatik olarak döndürülmeyecek.)' data_manager_pinned: '[※ Anlık sabitlendi](#d8ff2b show_text=&7Sabitlendi:\n&8Bu kullanıcı veri anlığı otomatik olarak döndürülmeyecek.)'
data_manager_cause: '[⚑ %1%](#23a825-#36f539 show_text=&7Kaydetme sebebi:\n&8Verinin kaydedilme nedeni)' data_manager_cause: '[⚑ %1%](#23a825-#36f539 show_text=&7Kaydetme sebebi:\n&8Verinin kaydedilme nedeni)'
data_manager_server: '[ %1%](#ff87b3-#f5538e show_text=&7Sunucu:\n&8Verinin kaydedildiği sunucu adı)' data_manager_server: '[ %1%](#ff87b3-#f5538e show_text=&7Sunucu:\n&8Verinin kaydedildiği sunucu adı)'
data_manager_size: '[⏏ %1%](color=#62a9f5-#7ab8fa show_text=&7Anlık boyutu:\n&8Anlının tahmini dosya boyutu (KiB cinsinden))\n' data_manager_size: '[⏏ %1%](color=#62a9f5-#7ab8fa show_text=&7Anlık boyutu:\n&8Anlının tahmini dosya boyutu (KiB cinsinden))\n'
data_manger_status: '[%1%](red)[/](gray)[%2%](red)[×](gray)[❤](red show_text=&7Can puanı) [%3%](yellow)[×](gray)[🍖](yellow show_text=&7Açlık puanı) [ʟᴠ](green)[.](gray)[%4%](green show_text=&7XP seviyesi) [🏹 %5%](dark_aqua show_text=&7Oyun modu)' data_manger_status: '[%1%](red)[/](gray)[%2%](red)[×](gray)[❤](red show_text=&7Can puanı) [%3%](yellow)[×](gray)[🍖](yellow show_text=&7Açlık puanı) [ʟᴠ](green)[.](gray)[%4%](green show_text=&7XP seviyesi) [🏹 %5%](dark_aqua show_text=&7Oyun modu)'
data_manager_advancements_statistics: '[⭐ Gelişmeler: %1%](color=#ffc43b-#f5c962 show_text=&7Gelişmelerdeki ilerlemeniz:\n&8%2%) [⌛ Oynama Süresi: %3%s](color=#62a9f5-#7ab8fa show_text=&7Oyunda geçen süre\n&8⚠ Oyun içi istatistiklere dayanır)\n' data_manager_advancements_statistics: '[⭐ Gelişmeler: %1%](color=#ffc43b-#f5c962 show_text=&7Gelişmelerdeki ilerlemeniz:\n&8%2%) [⌛ Oynama Süresi: %3%s](color=#62a9f5-#7ab8fa show_text=&7Oyunda geçen süre\n&8⚠ Oyun içi istatistiklere dayanır)\n'

View File

@@ -11,7 +11,7 @@ data_manager_title: '[Viewing user data snapshot](#00fb9a) [%1%](#00fb9a show_te
data_manager_timestamp: '[⌚ %1%](#ffc43b-#f5c962 show_text=&7Version timestamp:\n&8When the data was saved)' data_manager_timestamp: '[⌚ %1%](#ffc43b-#f5c962 show_text=&7Version timestamp:\n&8When the data was saved)'
data_manager_pinned: '[※ Snapshot pinned](#d8ff2b show_text=&7Pinned:\n&8This user data snapshot won''t be automatically rotated.)' data_manager_pinned: '[※ Snapshot pinned](#d8ff2b show_text=&7Pinned:\n&8This user data snapshot won''t be automatically rotated.)'
data_manager_cause: '[⚑ %1%](#23a825-#36f539 show_text=&7Save cause:\n&8What caused the data to be saved)' data_manager_cause: '[⚑ %1%](#23a825-#36f539 show_text=&7Save cause:\n&8What caused the data to be saved)'
data_manager_server: '[ %1%](#ff87b3-#f5538e show_text=&7Server:\n&8Name of the server the data was saved on)' data_manager_server: '[ %1%](#ff87b3-#f5538e show_text=&7Server:\n&8Name of the server the data was saved on)'
data_manager_size: '[⏏ %1%](color=#62a9f5-#7ab8fa show_text=&7Snapshot size:\n&8Estimated file size of the snapshot (in KiB))\n' data_manager_size: '[⏏ %1%](color=#62a9f5-#7ab8fa show_text=&7Snapshot size:\n&8Estimated file size of the snapshot (in KiB))\n'
data_manger_status: '[%1%](red)[/](gray)[%2%](red)[×](gray)[❤](red show_text=&7Health points) [%3%](yellow)[×](gray)[🍖](yellow show_text=&7Hunger points) [ʟᴠ](green)[.](gray)[%4%](green show_text=&7XP level) [🏹 %5%](dark_aqua show_text=&7Game mode)' data_manger_status: '[%1%](red)[/](gray)[%2%](red)[×](gray)[❤](red show_text=&7Health points) [%3%](yellow)[×](gray)[🍖](yellow show_text=&7Hunger points) [ʟᴠ](green)[.](gray)[%4%](green show_text=&7XP level) [🏹 %5%](dark_aqua show_text=&7Game mode)'
data_manager_advancements_statistics: '[⭐ Advancements: %1%](color=#ffc43b-#f5c962 show_text=&7Advancements you have progress in:\n&8%2%) [⌛ Play Time: %3%ʜʀs](color=#62a9f5-#7ab8fa show_text=&7In-game play time\n&8⚠ Based on in-game statistics)\n' data_manager_advancements_statistics: '[⭐ Advancements: %1%](color=#ffc43b-#f5c962 show_text=&7Advancements you have progress in:\n&8%2%) [⌛ Play Time: %3%ʜʀs](color=#62a9f5-#7ab8fa show_text=&7In-game play time\n&8⚠ Based on in-game statistics)\n'

View File

@@ -11,7 +11,7 @@ data_manager_title: '[正在查看玩家](#00fb9a) [%3%](#00fb9a bold show_text=
data_manager_timestamp: '[⌚ %1%](#ffc43b-#f5c962 show_text=&7备份时间:\n&7何时保存了此数据)' data_manager_timestamp: '[⌚ %1%](#ffc43b-#f5c962 show_text=&7备份时间:\n&7何时保存了此数据)'
data_manager_pinned: '[※ 置顶备份](#d8ff2b show_text=&7置顶:\n&8此数据备份不会按照备份时间自动排序.)' data_manager_pinned: '[※ 置顶备份](#d8ff2b show_text=&7置顶:\n&8此数据备份不会按照备份时间自动排序.)'
data_manager_cause: '[⚑ %1%](#23a825-#36f539 show_text=&7备份原因:\n&7为何保存了此数据)' data_manager_cause: '[⚑ %1%](#23a825-#36f539 show_text=&7备份原因:\n&7为何保存了此数据)'
data_manager_server: '[ %1%](#ff87b3-#f5538e show_text=&7服务器:\n&8保存数据的服务器的名称)' data_manager_server: '[ %1%](#ff87b3-#f5538e show_text=&7服务器:\n&8保存数据的服务器的名称)'
data_manager_size: '[⏏ %1%](color=#62a9f5-#7ab8fa show_text=&7快照大小:\n&8快照的估计文件大小以KiB为单位)\n' data_manager_size: '[⏏ %1%](color=#62a9f5-#7ab8fa show_text=&7快照大小:\n&8快照的估计文件大小以KiB为单位)\n'
data_manger_status: '[%1%](red)[/](gray)[%2%](red)[×](gray)[❤](red show_text=&7血量) [%3%](yellow)[×](gray)[🍖](yellow show_text=&7饱食度) [ʟᴠ](green)[.](gray)[%4%](green show_text=&7经验等级) [🏹 %5%](dark_aqua show_text=&7游戏模式)' data_manger_status: '[%1%](red)[/](gray)[%2%](red)[×](gray)[❤](red show_text=&7血量) [%3%](yellow)[×](gray)[🍖](yellow show_text=&7饱食度) [ʟᴠ](green)[.](gray)[%4%](green show_text=&7经验等级) [🏹 %5%](dark_aqua show_text=&7游戏模式)'
data_manager_advancements_statistics: '[⭐ 成就: %1%](color=#ffc43b-#f5c962 show_text=&7%2%) [⌛ 游玩时间: %3%ʜʀs](color=#62a9f5-#7ab8fa show_text=&7⚠ 基于游戏内的统计)\n' data_manager_advancements_statistics: '[⭐ 成就: %1%](color=#ffc43b-#f5c962 show_text=&7%2%) [⌛ 游玩时间: %3%ʜʀs](color=#62a9f5-#7ab8fa show_text=&7⚠ 基于游戏内的统计)\n'

View File

@@ -11,7 +11,7 @@ data_manager_title: '[查看](#00fb9a) [%3%](#00fb9a bold show_text=&7玩家 UUI
data_manager_timestamp: '[⌚ %1%](#ffc43b-#f5c962 show_text=&7快照時間:\n&8何時保存的資料)' data_manager_timestamp: '[⌚ %1%](#ffc43b-#f5c962 show_text=&7快照時間:\n&8何時保存的資料)'
data_manager_pinned: '[※ 被標記的快照](#d8ff2b show_text=&7標記:\n&8此快照資料不會自動輪換更新)' data_manager_pinned: '[※ 被標記的快照](#d8ff2b show_text=&7標記:\n&8此快照資料不會自動輪換更新)'
data_manager_cause: '[⚑ %1%](#23a825-#36f539 show_text=&7保存原因:\n&8保存此快照的原因)' data_manager_cause: '[⚑ %1%](#23a825-#36f539 show_text=&7保存原因:\n&8保存此快照的原因)'
data_manager_server: '[ %1%](#ff87b3-#f5538e show_text=&7Server:\n&8Name of the server the data was saved on)' data_manager_server: '[ %1%](#ff87b3-#f5538e show_text=&7Server:\n&8Name of the server the data was saved on)'
data_manager_size: '[⏏ %1%](color=#62a9f5-#7ab8fa show_text=&7Snapshot size:\n&8Estimated file size of the snapshot (in KiB))\n' data_manager_size: '[⏏ %1%](color=#62a9f5-#7ab8fa show_text=&7Snapshot size:\n&8Estimated file size of the snapshot (in KiB))\n'
data_manger_status: '[%1%](red)[/](gray)[%2%](red)[×](gray)[❤](red show_text=&7血量) [%3%](yellow)[×](gray)[🍖](yellow show_text=&7飽食度) [ʟᴠ](green)[.](gray)[%4%](green show_text=&7經驗等級) [🏹 %5%](dark_aqua show_text=&7遊戲模式)' data_manger_status: '[%1%](red)[/](gray)[%2%](red)[×](gray)[❤](red show_text=&7血量) [%3%](yellow)[×](gray)[🍖](yellow show_text=&7飽食度) [ʟᴠ](green)[.](gray)[%4%](green show_text=&7經驗等級) [🏹 %5%](dark_aqua show_text=&7遊戲模式)'
data_manager_advancements_statistics: '[⭐ 成就: %1%](color=#ffc43b-#f5c962 show_text=&7已獲得的成就:\n&8%2%) [⌛ 遊戲時間: %3%ʜʀs](color=#62a9f5-#7ab8fa show_text=&7遊戲內的遊玩時間\n&8⚠ 根據遊戲內統計)\n' data_manager_advancements_statistics: '[⭐ 成就: %1%](color=#ffc43b-#f5c962 show_text=&7已獲得的成就:\n&8%2%) [⌛ 遊戲時間: %3%ʜʀs](color=#62a9f5-#7ab8fa show_text=&7遊戲內的遊玩時間\n&8⚠ 根據遊戲內統計)\n'

View File

@@ -60,8 +60,8 @@ redis:
password: '' password: ''
use_ssl: false use_ssl: false
synchronization: synchronization:
# The mode of data synchronization to use (DELAY or LOCKSTEP). DELAY should be fine for most networks. Docs: https://william278.net/docs/husksync/sync-modes # The data synchronization mode to use (LOCKSTEP or DELAY). LOCKSTEP is recommended for most networks. Docs: https://william278.net/docs/husksync/sync-modes
mode: DELAY mode: LOCKSTEP
# The number of data snapshot backups that should be kept at once per user # The number of data snapshot backups that should be kept at once per user
max_user_data_snapshots: 16 max_user_data_snapshots: 16
# Number of hours between new snapshots being saved as backups (Use "0" to backup all snapshots) # Number of hours between new snapshots being saved as backups (Use "0" to backup all snapshots)

View File

@@ -3,8 +3,8 @@ HuskSync offers two built-in **synchronization modes** that utilise Redis and My
![Overall architecture of the synchronisation systems](https://raw.githubusercontent.com/WiIIiam278/HuskSync/master/images/system-diagram.png) ![Overall architecture of the synchronisation systems](https://raw.githubusercontent.com/WiIIiam278/HuskSync/master/images/system-diagram.png)
## Available Modes ## Available Modes
* The `DELAY` sync mode is the default sync mode, that use the `network_latency_miliseconds` value to apply a delay before listening to Redis data * The `LOCKSTEP` sync mode is the default sync mode. It uses a data checkout system to ensure that all servers are in sync regardless of network latency or tick rate fluctuations. This mode was introduced in HuskSync v3.1
* The `LOCKSTEP` sync mode uses a data checkout system to ensure that all servers are in sync regardless of network latency or tick rate fluctuations. This mode was introduced in HuskSync v3.1 * The `DELAY` sync mode uses the `network_latency_miliseconds` value to apply a delay before listening to Redis data
You can change which sync mode you are using by editing the `sync_mode` setting under `synchronization` in `config.yml`. You can change which sync mode you are using by editing the `sync_mode` setting under `synchronization` in `config.yml`.
@@ -15,11 +15,22 @@ You can change which sync mode you are using by editing the `sync_mode` setting
```yaml ```yaml
synchronization: synchronization:
# The mode of data synchronization to use (DELAY or LOCKSTEP). DELAY should be fine for most networks. Docs: https://william278.net/docs/husksync/sync-modes # The data synchronization mode to use (LOCKSTEP or DELAY). LOCKSTEP is recommended for most networks. Docs: https://william278.net/docs/husksync/sync-modes
mode: DELAY mode: LOCKSTEP
``` ```
</details> </details>
## Lockstep
The `LOCKSTEP` sync mode works as described below:
* When a user connects to a server, the server will continuously asynchronously check if a `DATA_CHECKOUT` key is present.
* If, or when, the key is not present, the plugin will set a new `DATA_CHECKOUT` key.
* After this, the plugin will check Redis for the presence of a `DATA_UPDATE` key.
* If a `DATA_UPDATE` key is present, the user's data will be set from the snapshot deserialized from Redis contained within that key.
* Otherwise, their data will be pulled from the database.
* When a user disconnects from a server, their data is serialized and set to Redis with a `DATA_UPDATE` key. After this key has been set, the user's current `DATA_CHECKOUT` key will be removed from Redis.
Additionally, note that `DATA_CHECKOUT` keys are set with the server ID of the server which "checked out" the data (taken from the `server.yml` config file). On both shutdown and startup, the plugin will clear all `DATA_CHECKOUT` keys for the current server ID (to prevent stale keys in the event of a server crash for instance)
## Delay ## Delay
The `DELAY` sync mode works as described below: The `DELAY` sync mode works as described below:
* When a user disconnects from a server, a `SERVER_SWITCH` key is immediately set on Redis, followed by a `DATA_UPDATE` key which contains the user's packed and serialized Data Snapshot. * When a user disconnects from a server, a `SERVER_SWITCH` key is immediately set on Redis, followed by a `DATA_UPDATE` key which contains the user's packed and serialized Data Snapshot.
@@ -31,15 +42,4 @@ The `DELAY` sync mode works as described below:
`DELAY` has been the default sync mode since HuskSync v2.0. In HuskSync v3.1, `LOCKSTEP` was introduced. Since the delay mode has been tested and deployed for the longest, it is still the default, though note this may change in the future. `DELAY` has been the default sync mode since HuskSync v2.0. In HuskSync v3.1, `LOCKSTEP` was introduced. Since the delay mode has been tested and deployed for the longest, it is still the default, though note this may change in the future.
However, if your network has a fluctuating tick rate or significant latency (especially if you have servers on different hardware/locations), you may wish to use `LOCKSTEP` instead for a more reliable sync system. However, if your network has a fluctuating tick rate or significant latency (especially if you have servers on different hardware/locations), you may wish to use `LOCKSTEP` instead for a more reliable sync system.
## Lockstep
The `LOCKSTEP` sync mode works as described below:
* When a user connects to a server, the server will continuously asynchronously check if a `DATA_CHECKOUT` key is present.
* If, or when, the key is not present, the plugin will set a new `DATA_CHECKOUT` key.
* After this, the plugin will check Redis for the presence of a `DATA_UPDATE` key.
* If a `DATA_UPDATE` key is present, the user's data will be set from the snapshot deserialized from Redis contained within that key.
* Otherwise, their data will be pulled from the database.
* When a user disconnects from a server, their data is serialized and set to Redis with a `DATA_UPDATE` key. After this key has been set, the user's current `DATA_CHECKOUT` key will be removed from Redis.
Additionally, note that `DATA_CHECKOUT` keys are set with the server ID of the server which "checked out" the data (taken from the `server.yml` config file). On both shutdown and startup, the plugin will clear all `DATA_CHECKOUT` keys for the current server ID (to prevent stale keys in the event of a server crash for instance)

View File

@@ -3,11 +3,11 @@ org.gradle.jvmargs='-Dfile.encoding=UTF-8'
org.gradle.daemon=true org.gradle.daemon=true
javaVersion=16 javaVersion=16
plugin_version=3.1.2 plugin_version=3.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.0
mysql_driver_version=8.2.0 mysql_driver_version=8.2.0
mariadb_driver_version=3.3.0 mariadb_driver_version=3.3.2
snappy_version=1.1.10.5 snappy_version=1.1.10.5

View File

@@ -17,7 +17,6 @@ shadowJar {
relocate 'org.json', 'net.william278.husksync.libraries.json' relocate 'org.json', 'net.william278.husksync.libraries.json'
relocate 'com.fatboyindustrial', 'net.william278.husksync.libraries' relocate 'com.fatboyindustrial', 'net.william278.husksync.libraries'
relocate 'de.themoep', 'net.william278.husksync.libraries' relocate 'de.themoep', 'net.william278.husksync.libraries'
relocate 'net.kyori', 'net.william278.husksync.libraries'
relocate 'org.jetbrains', 'net.william278.husksync.libraries' relocate 'org.jetbrains', 'net.william278.husksync.libraries'
relocate 'org.intellij', 'net.william278.husksync.libraries' relocate 'org.intellij', 'net.william278.husksync.libraries'
relocate 'com.zaxxer', 'net.william278.husksync.libraries' relocate 'com.zaxxer', 'net.william278.husksync.libraries'

View File

@@ -19,10 +19,14 @@
package net.william278.husksync; package net.william278.husksync;
import net.kyori.adventure.audience.Audience;
import net.william278.husksync.listener.BukkitEventListener; import net.william278.husksync.listener.BukkitEventListener;
import net.william278.husksync.listener.PaperEventListener; import net.william278.husksync.listener.PaperEventListener;
import org.bukkit.entity.Player;
import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.NotNull;
import java.util.UUID;
@SuppressWarnings("unused") @SuppressWarnings("unused")
public class PaperHuskSync extends BukkitHuskSync { public class PaperHuskSync extends BukkitHuskSync {
@@ -32,4 +36,11 @@ public class PaperHuskSync extends BukkitHuskSync {
return new PaperEventListener(this); return new PaperEventListener(this);
} }
@NotNull
@Override
public Audience getAudience(@NotNull UUID user) {
final Player player = getServer().getPlayer(user);
return player == null || !player.isOnline() ? Audience.empty() : player;
}
} }