mirror of
https://github.com/WiIIiam278/HuskSync.git
synced 2025-12-23 16:49:19 +00:00
Compare commits
23 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2aa33b2f2c | ||
|
|
972fee1bc7 | ||
|
|
efe34977b5 | ||
|
|
02ed9687ee | ||
|
|
08889a1739 | ||
|
|
9cf6d1eab6 | ||
|
|
33c2eb2237 | ||
|
|
299586aa86 | ||
|
|
05c988f2c7 | ||
|
|
8e0ad76968 | ||
|
|
4db162e78f | ||
|
|
272bc1278a | ||
|
|
35fdcf7106 | ||
|
|
48e087a3d7 | ||
|
|
ca000197e4 | ||
|
|
a6bab88cee | ||
|
|
f0c64df439 | ||
|
|
ac5ab56717 | ||
|
|
c2025350ba | ||
|
|
4c2bb5c6df | ||
|
|
fb069296e1 | ||
|
|
22eedc8522 | ||
|
|
664c8c3352 |
2
.github/workflows/update_docs.yml
vendored
2
.github/workflows/update_docs.yml
vendored
@@ -19,7 +19,7 @@ jobs:
|
||||
- name: 'Checkout for CI 🛎️'
|
||||
uses: actions/checkout@v4
|
||||
- name: 'Push Docs to Github Wiki 📄️'
|
||||
uses: Andrew-Chen-Wang/github-wiki-action@v4
|
||||
uses: Andrew-Chen-Wang/github-wiki-action@v3
|
||||
env:
|
||||
WIKI_DIR: 'docs/'
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
|
||||
@@ -36,7 +36,6 @@ shadowJar {
|
||||
relocate 'org.json', 'net.william278.husksync.libraries.json'
|
||||
relocate 'com.fatboyindustrial', '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.intellij', 'net.william278.husksync.libraries'
|
||||
relocate 'com.zaxxer', 'net.william278.husksync.libraries'
|
||||
|
||||
@@ -20,6 +20,7 @@
|
||||
package net.william278.husksync;
|
||||
|
||||
import com.google.gson.Gson;
|
||||
import net.kyori.adventure.platform.AudienceProvider;
|
||||
import net.kyori.adventure.platform.bukkit.BukkitAudiences;
|
||||
import net.william278.desertwell.util.Version;
|
||||
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.sync.DataSyncer;
|
||||
import net.william278.husksync.user.BukkitUser;
|
||||
import net.william278.husksync.user.ConsoleUser;
|
||||
import net.william278.husksync.user.OnlineUser;
|
||||
import net.william278.husksync.util.BukkitLegacyConverter;
|
||||
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
|
||||
@Override
|
||||
public Version getPluginVersion() {
|
||||
@@ -415,7 +409,7 @@ public class BukkitHuskSync extends JavaPlugin implements HuskSync, BukkitTask.S
|
||||
}
|
||||
|
||||
@NotNull
|
||||
public BukkitAudiences getAudiences() {
|
||||
public AudienceProvider getAudiences() {
|
||||
return audiences;
|
||||
}
|
||||
|
||||
|
||||
@@ -593,54 +593,54 @@ public abstract class BukkitData implements Data {
|
||||
@NotNull
|
||||
public static BukkitData.Statistics from(@NotNull StatisticsMap stats) {
|
||||
return new BukkitData.Statistics(
|
||||
stats.genericStats().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, 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();
|
||||
stats.genericStats().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, Map.Entry::getValue))
|
||||
)),
|
||||
stats.itemStats().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 -> {
|
||||
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.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, 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,
|
||||
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))
|
||||
)),
|
||||
stats.itemStats().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, 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
|
||||
public static BukkitData.Health adapt(@NotNull Player player) {
|
||||
final double maxHealth = getMaxHealth(player);
|
||||
return from(
|
||||
Math.min(player.getHealth(), maxHealth),
|
||||
maxHealth,
|
||||
player.getHealth(),
|
||||
getMaxHealth(player),
|
||||
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 {
|
||||
final Player player = user.getPlayer();
|
||||
|
||||
// Set base max health
|
||||
final AttributeInstance maxHealthAttribute = Objects.requireNonNull(
|
||||
player.getAttribute(Attribute.GENERIC_MAX_HEALTH), "Max health attribute was null"
|
||||
);
|
||||
double currentMaxHealth = maxHealthAttribute.getBaseValue();
|
||||
if (plugin.getSettings().doSynchronizeMaxHealth() && maxHealth != 0d) {
|
||||
maxHealthAttribute.setBaseValue(maxHealth);
|
||||
currentMaxHealth = maxHealth;
|
||||
// Set max health
|
||||
final AttributeInstance maxHealth = getMaxHealthAttribute(player);
|
||||
try {
|
||||
if (plugin.getSettings().doSynchronizeMaxHealth() && this.maxHealth != 0) {
|
||||
maxHealth.setBaseValue(this.maxHealth);
|
||||
}
|
||||
} catch (Throwable e) {
|
||||
plugin.log(Level.WARNING, String.format("Failed setting the max health of %s to %s",
|
||||
player.getName(), this.maxHealth), e);
|
||||
}
|
||||
|
||||
// Set health
|
||||
final double currentHealth = player.getHealth();
|
||||
if (health != currentHealth) {
|
||||
final double healthToSet = currentHealth > currentMaxHealth ? currentMaxHealth : health;
|
||||
try {
|
||||
player.setHealth(Math.min(healthToSet, currentMaxHealth));
|
||||
} catch (IllegalArgumentException e) {
|
||||
plugin.log(Level.WARNING, "Failed to set player health", e);
|
||||
}
|
||||
try {
|
||||
final double health = player.getHealth();
|
||||
player.setHealth(Math.min(health, maxHealth.getBaseValue()));
|
||||
} catch (Throwable e) {
|
||||
plugin.log(Level.WARNING, String.format("Failed setting the health of %s to %s",
|
||||
player.getName(), this.maxHealth), e);
|
||||
}
|
||||
|
||||
// Set health scale
|
||||
try {
|
||||
if (healthScale != 0d) {
|
||||
player.setHealthScale(healthScale);
|
||||
if (this.healthScale != 0d) {
|
||||
player.setHealthScaled(true);
|
||||
player.setHealthScale(this.healthScale);
|
||||
} else {
|
||||
player.setHealthScale(maxHealth);
|
||||
player.setHealthScaled(false);
|
||||
player.setHealthScale(this.maxHealth);
|
||||
}
|
||||
player.setHealthScaled(healthScale != 0D);
|
||||
} catch (IllegalArgumentException e) {
|
||||
plugin.log(Level.WARNING, "Failed to set player health scale", e);
|
||||
} catch (Throwable e) {
|
||||
plugin.log(Level.WARNING, String.format("Failed setting the health scale of %s to %s",
|
||||
player.getName(), this.healthScale), e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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
|
||||
*/
|
||||
// Returns the max health of a player, accounting for health boost potion effects
|
||||
private static double getMaxHealth(@NotNull Player player) {
|
||||
double maxHealth = Objects.requireNonNull(
|
||||
player.getAttribute(Attribute.GENERIC_MAX_HEALTH), "Max health attribute was null"
|
||||
).getBaseValue();
|
||||
// Get the base value of the attribute (ignore armor, items that give health boosts, etc.)
|
||||
double maxHealth = getMaxHealthAttribute(player).getBaseValue();
|
||||
|
||||
// If the player has additional health bonuses from synchronized potion effects,
|
||||
// subtract these from this number as they are synchronized separately
|
||||
// Subtract health boost potion effects from stored max health
|
||||
if (player.hasPotionEffect(PotionEffectType.HEALTH_BOOST) && maxHealth > 20d) {
|
||||
final PotionEffect healthBoost = Objects.requireNonNull(
|
||||
player.getPotionEffect(PotionEffectType.HEALTH_BOOST), "Health boost effect was null"
|
||||
);
|
||||
final double boostEffect = 4 * (healthBoost.getAmplifier() + 1);
|
||||
maxHealth -= boostEffect;
|
||||
maxHealth -= (4 * (healthBoost.getAmplifier() + 1));
|
||||
}
|
||||
|
||||
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
|
||||
public double getHealth() {
|
||||
return health;
|
||||
@@ -1093,7 +1093,7 @@ public abstract class BukkitData implements Data {
|
||||
final Player player = user.getPlayer();
|
||||
player.setGameMode(org.bukkit.GameMode.valueOf(gameMode));
|
||||
player.setAllowFlight(allowFlight);
|
||||
player.setFlying(isFlying);
|
||||
player.setFlying(allowFlight && isFlying);
|
||||
}
|
||||
|
||||
@NotNull
|
||||
|
||||
@@ -23,7 +23,6 @@ import de.themoep.minedown.adventure.MineDown;
|
||||
import dev.triumphteam.gui.builder.gui.StorageBuilder;
|
||||
import dev.triumphteam.gui.guis.Gui;
|
||||
import dev.triumphteam.gui.guis.StorageGui;
|
||||
import net.kyori.adventure.audience.Audience;
|
||||
import net.roxeez.advancement.display.FrameType;
|
||||
import net.william278.andjam.Toast;
|
||||
import net.william278.husksync.BukkitHuskSync;
|
||||
@@ -77,12 +76,6 @@ public class BukkitUser extends OnlineUser implements BukkitUserDataHolder {
|
||||
return player == null || !player.isOnline();
|
||||
}
|
||||
|
||||
@NotNull
|
||||
@Override
|
||||
public Audience getAudience() {
|
||||
return ((BukkitHuskSync) plugin).getAudiences().player(player);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void sendToast(@NotNull MineDown title, @NotNull MineDown description,
|
||||
@NotNull String iconMaterial, @NotNull String backgroundType) {
|
||||
|
||||
@@ -6,7 +6,6 @@ dependencies {
|
||||
api 'commons-io:commons-io:2.15.1'
|
||||
api 'org.apache.commons:commons-text:1.11.0'
|
||||
api 'de.themoep:minedown-adventure:1.7.2-SNAPSHOT'
|
||||
api 'net.kyori:adventure-api:4.14.0'
|
||||
api 'org.json:json:20231013'
|
||||
api 'com.google.code.gson:gson:2.10.1'
|
||||
api 'com.fatboyindustrial.gson-javatime-serialisers:gson-javatime-serialisers:1.1.2'
|
||||
@@ -18,6 +17,8 @@ dependencies {
|
||||
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 'com.github.plan-player-analytics:Plan:5.5.2272'
|
||||
compileOnly "redis.clients:jedis:$jedis_version"
|
||||
|
||||
@@ -22,6 +22,8 @@ package net.william278.husksync;
|
||||
import com.fatboyindustrial.gsonjavatime.Converters;
|
||||
import com.google.gson.Gson;
|
||||
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.desertwell.util.ThrowingConsumer;
|
||||
import net.william278.desertwell.util.UpdateChecker;
|
||||
@@ -48,6 +50,7 @@ import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.lang.reflect.InvocationTargetException;
|
||||
import java.text.SimpleDateFormat;
|
||||
import java.util.*;
|
||||
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) {
|
||||
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
|
||||
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
|
||||
|
||||
@@ -134,12 +134,23 @@ public class Settings {
|
||||
@YamlKey("redis.use_ssl")
|
||||
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
|
||||
@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")
|
||||
@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")
|
||||
@YamlKey("synchronization.max_user_data_snapshots")
|
||||
@@ -324,6 +335,21 @@ public class Settings {
|
||||
return redisUseSsl;
|
||||
}
|
||||
|
||||
@NotNull
|
||||
public String getRedisSentinelMaster() {
|
||||
return redisSentinelMaster;
|
||||
}
|
||||
|
||||
@NotNull
|
||||
public List<String> getRedisSentinelNodes() {
|
||||
return redisSentinelNodes;
|
||||
}
|
||||
|
||||
@NotNull
|
||||
public String getRedisSentinelPassword() {
|
||||
return redisSentinelPassword;
|
||||
}
|
||||
|
||||
@NotNull
|
||||
public DataSyncer.Mode getSyncMode() {
|
||||
return syncMode;
|
||||
|
||||
@@ -65,7 +65,7 @@ public abstract class EventListener {
|
||||
return;
|
||||
}
|
||||
plugin.lockPlayer(user.getUuid());
|
||||
plugin.runAsync(() -> plugin.getDataSyncer().saveUserData(user));
|
||||
plugin.getDataSyncer().saveUserData(user);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -24,15 +24,13 @@ import org.jetbrains.annotations.NotNull;
|
||||
import java.util.Locale;
|
||||
|
||||
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) {
|
||||
this.timeToLive = timeToLive;
|
||||
}
|
||||
public static final int TTL_1_YEAR = 60 * 60 * 24 * 7 * 52; // 1 year
|
||||
public static final int TTL_10_SECONDS = 10; // 10 seconds
|
||||
|
||||
@NotNull
|
||||
public String getKeyPrefix(@NotNull String clusterId) {
|
||||
@@ -44,8 +42,4 @@ public enum RedisKeyType {
|
||||
);
|
||||
}
|
||||
|
||||
public int getTimeToLive() {
|
||||
return timeToLive;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -24,14 +24,11 @@ import net.william278.husksync.data.DataSnapshot;
|
||||
import net.william278.husksync.user.User;
|
||||
import org.jetbrains.annotations.Blocking;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import redis.clients.jedis.Jedis;
|
||||
import redis.clients.jedis.JedisPool;
|
||||
import redis.clients.jedis.JedisPoolConfig;
|
||||
import redis.clients.jedis.JedisPubSub;
|
||||
import redis.clients.jedis.*;
|
||||
import redis.clients.jedis.exceptions.JedisException;
|
||||
import redis.clients.jedis.util.Pool;
|
||||
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.text.SimpleDateFormat;
|
||||
import java.util.*;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
@@ -39,7 +36,7 @@ import java.util.concurrent.TimeUnit;
|
||||
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 {
|
||||
|
||||
@@ -47,7 +44,7 @@ public class RedisManager extends JedisPubSub {
|
||||
|
||||
private final HuskSync plugin;
|
||||
private final String clusterId;
|
||||
private JedisPool jedisPool;
|
||||
private Pool<Jedis> jedisPool;
|
||||
private final Map<UUID, CompletableFuture<Optional<DataSnapshot.Packed>>> pendingRequests;
|
||||
|
||||
public RedisManager(@NotNull HuskSync plugin) {
|
||||
@@ -57,7 +54,7 @@ public class RedisManager extends JedisPubSub {
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize the redis connection pool
|
||||
* Initialize Redis connection pool
|
||||
*/
|
||||
@Blocking
|
||||
public void initialize() throws IllegalStateException {
|
||||
@@ -71,15 +68,22 @@ public class RedisManager extends JedisPubSub {
|
||||
config.setMaxIdle(0);
|
||||
config.setTestOnBorrow(true);
|
||||
config.setTestOnReturn(true);
|
||||
this.jedisPool = password.isEmpty()
|
||||
? new JedisPool(config, host, port, 0, useSSL)
|
||||
: new JedisPool(config, host, port, 0, password, useSSL);
|
||||
Set<String> redisSentinelNodes = new HashSet<>(plugin.getSettings().getRedisSentinelNodes());
|
||||
if (redisSentinelNodes.isEmpty()) {
|
||||
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
|
||||
try {
|
||||
jedisPool.getResource().ping();
|
||||
} 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);
|
||||
}
|
||||
|
||||
@@ -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 data the user's data to set
|
||||
* @param user the user to set data for
|
||||
* @param data the user's data to set
|
||||
* @param timeToLive The time to cache the data for
|
||||
*/
|
||||
@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()) {
|
||||
jedis.setex(
|
||||
getKey(RedisKeyType.DATA_UPDATE, user.getUuid(), clusterId),
|
||||
RedisKeyType.DATA_UPDATE.getTimeToLive(),
|
||||
getKey(RedisKeyType.LATEST_SNAPSHOT, user.getUuid(), clusterId),
|
||||
timeToLive,
|
||||
data.asBytes(plugin)
|
||||
);
|
||||
plugin.debug(String.format("[%s] Set %s key to redis at: %s", user.getUsername(),
|
||||
RedisKeyType.DATA_UPDATE.name(), new SimpleDateFormat("mm:ss.SSS").format(new Date())));
|
||||
plugin.debug(String.format("[%s] Set %s key on Redis", user.getUsername(), RedisKeyType.LATEST_SNAPSHOT));
|
||||
} 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 {
|
||||
jedis.del(getKey(RedisKeyType.DATA_CHECKOUT, user.getUuid(), clusterId));
|
||||
}
|
||||
plugin.debug(String.format("[%s] %s %s key to redis at: %s",
|
||||
checkedOut ? "set" : "removed", user.getUsername(), RedisKeyType.DATA_CHECKOUT.name(),
|
||||
new SimpleDateFormat("mm:ss.SSS").format(new Date())));
|
||||
plugin.debug(String.format("[%s] %s %s key to/from Redis", user.getUsername(),
|
||||
checkedOut ? "Set" : "Removed", RedisKeyType.DATA_CHECKOUT));
|
||||
} 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[] readData = jedis.get(key);
|
||||
if (readData != null) {
|
||||
plugin.debug("[" + user.getUsername() + "] Successfully read "
|
||||
+ RedisKeyType.DATA_CHECKOUT.name() + " key from redis at: " +
|
||||
new SimpleDateFormat("mm:ss.SSS").format(new Date()));
|
||||
return Optional.of(new String(readData, StandardCharsets.UTF_8));
|
||||
final String checkoutServer = new String(readData, StandardCharsets.UTF_8);
|
||||
plugin.debug(String.format("[%s] Waiting for %s %s key to be unset on Redis",
|
||||
user.getUsername(), checkoutServer, RedisKeyType.DATA_CHECKOUT));
|
||||
return Optional.of(checkoutServer);
|
||||
}
|
||||
} 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 " +
|
||||
RedisKeyType.DATA_CHECKOUT.name() + " key from redis at: " +
|
||||
new SimpleDateFormat("mm:ss.SSS").format(new Date()));
|
||||
plugin.debug(String.format("[%s] %s key not set on Redis", user.getUsername(),
|
||||
RedisKeyType.DATA_CHECKOUT));
|
||||
return Optional.empty();
|
||||
}
|
||||
|
||||
@@ -240,7 +242,7 @@ public class RedisManager extends JedisPubSub {
|
||||
try (Jedis jedis = jedisPool.getResource()) {
|
||||
final Set<String> keys = jedis.keys(keyFormat);
|
||||
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;
|
||||
}
|
||||
for (String key : keys) {
|
||||
@@ -249,12 +251,12 @@ public class RedisManager extends JedisPubSub {
|
||||
}
|
||||
}
|
||||
} 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
|
||||
*/
|
||||
@@ -263,17 +265,18 @@ public class RedisManager extends JedisPubSub {
|
||||
try (Jedis jedis = jedisPool.getResource()) {
|
||||
jedis.setex(
|
||||
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(),
|
||||
RedisKeyType.SERVER_SWITCH.name(), new SimpleDateFormat("mm:ss.SSS").format(new Date())));
|
||||
plugin.debug(String.format("[%s] Set %s key to Redis",
|
||||
user.getUsername(), RedisKeyType.SERVER_SWITCH));
|
||||
} 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
|
||||
* @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
|
||||
public Optional<DataSnapshot.Packed> getUserData(@NotNull User user) {
|
||||
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);
|
||||
if (dataByteArray == null) {
|
||||
plugin.debug("[" + user.getUsername() + "] Could not read " +
|
||||
RedisKeyType.DATA_UPDATE.name() + " key from redis at: " +
|
||||
new SimpleDateFormat("mm:ss.SSS").format(new Date()));
|
||||
plugin.debug(String.format("[%s] Waiting for %s key from Redis",
|
||||
user.getUsername(), RedisKeyType.LATEST_SNAPSHOT));
|
||||
return Optional.empty();
|
||||
}
|
||||
plugin.debug("[" + user.getUsername() + "] Successfully read "
|
||||
+ RedisKeyType.DATA_UPDATE.name() + " key from redis at: " +
|
||||
new SimpleDateFormat("mm:ss.SSS").format(new Date()));
|
||||
plugin.debug(String.format("[%s] Read %s key from Redis",
|
||||
user.getUsername(), RedisKeyType.LATEST_SNAPSHOT));
|
||||
|
||||
// Consume the key (delete from redis)
|
||||
jedis.del(key);
|
||||
@@ -299,7 +300,7 @@ public class RedisManager extends JedisPubSub {
|
||||
// Use Snappy to decompress the json
|
||||
return Optional.of(DataSnapshot.deserialize(plugin, dataByteArray));
|
||||
} 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();
|
||||
}
|
||||
}
|
||||
@@ -310,20 +311,18 @@ public class RedisManager extends JedisPubSub {
|
||||
final byte[] key = getKey(RedisKeyType.SERVER_SWITCH, user.getUuid(), clusterId);
|
||||
final byte[] readData = jedis.get(key);
|
||||
if (readData == null) {
|
||||
plugin.debug("[" + user.getUsername() + "] Could not read " +
|
||||
RedisKeyType.SERVER_SWITCH.name() + " key from redis at: " +
|
||||
new SimpleDateFormat("mm:ss.SSS").format(new Date()));
|
||||
plugin.debug(String.format("[%s] Waiting for %s key from Redis",
|
||||
user.getUsername(), RedisKeyType.SERVER_SWITCH));
|
||||
return false;
|
||||
}
|
||||
plugin.debug("[" + user.getUsername() + "] Successfully read "
|
||||
+ RedisKeyType.SERVER_SWITCH.name() + " key from redis at: " +
|
||||
new SimpleDateFormat("mm:ss.SSS").format(new Date()));
|
||||
plugin.debug(String.format("[%s] Read %s key from Redis",
|
||||
user.getUsername(), RedisKeyType.SERVER_SWITCH));
|
||||
|
||||
// Consume the key (delete from redis)
|
||||
jedis.del(key);
|
||||
return true;
|
||||
} 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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -114,6 +114,8 @@ public abstract class DataSyncer {
|
||||
}
|
||||
if (plugin.isDisabling() || timesRun.getAndIncrement() > maxListenAttempts) {
|
||||
task.get().cancel();
|
||||
plugin.debug(String.format("[%s] Redis timed out after %s attempts; setting from database",
|
||||
user.getUsername(), timesRun.get()));
|
||||
setUserFromDatabase(user);
|
||||
return;
|
||||
}
|
||||
@@ -132,8 +134,8 @@ public abstract class DataSyncer {
|
||||
* @since 3.1
|
||||
*/
|
||||
public enum Mode {
|
||||
DELAY(DelayDataSyncer::new),
|
||||
LOCKSTEP(LockstepDataSyncer::new);
|
||||
LOCKSTEP(LockstepDataSyncer::new),
|
||||
DELAY(DelayDataSyncer::new);
|
||||
|
||||
private final Function<HuskSync, ? extends DataSyncer> supplier;
|
||||
|
||||
|
||||
@@ -21,6 +21,7 @@ package net.william278.husksync.sync;
|
||||
|
||||
import net.william278.husksync.HuskSync;
|
||||
import net.william278.husksync.data.DataSnapshot;
|
||||
import net.william278.husksync.redis.RedisKeyType;
|
||||
import net.william278.husksync.user.OnlineUser;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
@@ -61,7 +62,7 @@ public class DelayDataSyncer extends DataSyncer {
|
||||
plugin.runAsync(() -> {
|
||||
plugin.getRedisManager().setUserServerSwitch(user);
|
||||
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);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -21,6 +21,7 @@ package net.william278.husksync.sync;
|
||||
|
||||
import net.william278.husksync.HuskSync;
|
||||
import net.william278.husksync.data.DataSnapshot;
|
||||
import net.william278.husksync.redis.RedisKeyType;
|
||||
import net.william278.husksync.user.OnlineUser;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
@@ -60,7 +61,7 @@ public class LockstepDataSyncer extends DataSyncer {
|
||||
public void saveUserData(@NotNull OnlineUser user) {
|
||||
plugin.runAsync(() -> {
|
||||
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.getDatabase().addSnapshot(user, data);
|
||||
});
|
||||
|
||||
@@ -20,6 +20,7 @@
|
||||
package net.william278.husksync.user;
|
||||
|
||||
import net.kyori.adventure.audience.Audience;
|
||||
import net.kyori.adventure.platform.AudienceProvider;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
public final class ConsoleUser implements CommandUser {
|
||||
@@ -27,8 +28,8 @@ public final class ConsoleUser implements CommandUser {
|
||||
@NotNull
|
||||
private final Audience audience;
|
||||
|
||||
public ConsoleUser(@NotNull Audience console) {
|
||||
this.audience = console;
|
||||
public ConsoleUser(@NotNull AudienceProvider audiences) {
|
||||
this.audience = audiences.console();
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -50,13 +50,11 @@ public abstract class OnlineUser extends User implements CommandUser, UserDataHo
|
||||
*/
|
||||
public abstract boolean isOffline();
|
||||
|
||||
/**
|
||||
* Get the player's adventure {@link Audience}
|
||||
*
|
||||
* @return the player's {@link Audience}
|
||||
*/
|
||||
@NotNull
|
||||
public abstract Audience getAudience();
|
||||
@Override
|
||||
public Audience getAudience() {
|
||||
return getPlugin().getAudience(getUuid());
|
||||
}
|
||||
|
||||
/**
|
||||
* 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) {
|
||||
getPlugin().fireEvent(getPlugin().getPreSyncEvent(this, snapshot), (event) -> {
|
||||
if (!isOffline()) {
|
||||
getPlugin().debug(String.format("Applying snapshot (%s) to %s (cause: %s)",
|
||||
snapshot.getShortId(), getUsername(), cause
|
||||
));
|
||||
UserDataHolder.super.applySnapshot(
|
||||
event.getData(), (succeeded) -> completeSync(succeeded, cause, getPlugin())
|
||||
);
|
||||
|
||||
@@ -11,7 +11,7 @@ data_manager_title: '[Преглеждане потребителският сн
|
||||
data_manager_timestamp: '[⌚ %1%](#ffc43b-#f5c962 show_text=&7Клеймо на Версията:\n&8Когато данните са били запазени)'
|
||||
data_manager_pinned: '[※ Закачен снапшот](#d8ff2b show_text=&7Закачен:\n&8Снапшота на този потребител няма да бъде автоматично завъртан.)'
|
||||
data_manager_cause: '[⚑ %1%](#23a825-#36f539 show_text=&7Причина на Запазване:\n&8Какво е накарало данните да бъдат запазени)'
|
||||
data_manager_server: '[Ⓢ %1%](#ff87b3-#f5538e show_text=&7Server:\n&8Name of the server the data was saved on)'
|
||||
data_manager_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_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'
|
||||
|
||||
@@ -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_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_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_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'
|
||||
|
||||
@@ -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_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_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_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'
|
||||
|
||||
@@ -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_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_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_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'
|
||||
|
||||
@@ -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_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_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_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'
|
||||
|
||||
@@ -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_pinned: '[※ ピン留めされたスナップショット](#d8ff2b show_text=&7ピン留め:\n&8このユーザーデータのスナップショットは自動的にローテーションされません。)'
|
||||
data_manager_cause: '[⚑ %1%](#23a825-#36f539 show_text=&7保存理由:\n&8データが保存された理由)'
|
||||
data_manager_server: '[Ⓢ %1%](#ff87b3-#f5538e show_text=&7Server:\n&8Name of the server the data was saved on)'
|
||||
data_manager_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_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'
|
||||
|
||||
@@ -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_pinned: '[※ 스냅샷 고정됨](#d8ff2b show_text=&7고정됨:\n&8이 유저의 데이터 스냅샷은 자동으로 갱신되지 않습니다.)'
|
||||
data_manager_cause: '[⚑ %1%](#23a825-#36f539 show_text=&7저장 사유:\n&8데이터가 저장된 사유입니다.)'
|
||||
data_manager_server: '[Ⓢ %1%](#ff87b3-#f5538e show_text=&7서버:\n&8데이터 저장이 이루어진 서버입니다.)'
|
||||
data_manager_server: '[☁ %1%](#ff87b3-#f5538e show_text=&7서버:\n&8데이터 저장이 이루어진 서버입니다.)'
|
||||
data_manager_size: '[⏏ %1%](color=#62a9f5-#7ab8fa show_text=&7스냅샷 크기:\n&8스냅샷 파일의 대략적인 크기입니다. (단위 KiB))\n'
|
||||
data_manger_status: '[%1%](red)[/](gray)[%2%](red)[×](gray)[❤](red show_text=&7체력) [%3%](yellow)[×](gray)[🍖](yellow show_text=&7허기) [ʟᴠ](green)[.](gray)[%4%](green show_text=&7경험치 레벨) [🏹 %5%](dark_aqua show_text=&7게임 모드)'
|
||||
data_manager_advancements_statistics: '[⭐ 도전 과제: %1%](color=#ffc43b-#f5c962 show_text=&7진행한 도전 과제:\n&8%2%) [⌛ 플레이 타임: %3%ʜʀs](color=#62a9f5-#7ab8fa show_text=&7인게임 플레이 시간\n&8⚠ 인게임 통계에 기반합니다.)\n'
|
||||
|
||||
@@ -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_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_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_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'
|
||||
|
||||
@@ -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_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_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_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'
|
||||
|
||||
@@ -11,7 +11,7 @@ data_manager_title: '[Просмотр снимка данных](#00fb9a) [%1%]
|
||||
data_manager_timestamp: '[⌚ %1%](#ffc43b-#f5c962 show_text=&7Время:\n&8Когда данные были сохранены)'
|
||||
data_manager_pinned: '[※ Снимок закреплен](#d8ff2b show_text=&7Закреплен:\n&8Этот снимок данных пользователя не будет автоматически применено.)'
|
||||
data_manager_cause: '[⚑ %1%](#23a825-#36f539 show_text=&7Причина сохранения:\n&8Что привело к сохранению данных)'
|
||||
data_manager_server: '[Ⓢ %1%](#ff87b3-#f5538e show_text=&7Сервер:\n&8Название сервера где данные были сохранены)'
|
||||
data_manager_server: '[☁ %1%](#ff87b3-#f5538e show_text=&7Сервер:\n&8Название сервера где данные были сохранены)'
|
||||
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_manager_advancements_statistics: '[⭐ Достижения: %1%](color=#ffc43b-#f5c962 show_text=&7Полученные вами достижения:\n&8%2%) [⌛ Отыгранное время: %3%ʜʀs](color=#62a9f5-#7ab8fa show_text=&7Сколько времени вы отыграли\n&8⚠ Строится по внутреигровой статистике)\n'
|
||||
|
||||
@@ -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_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_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_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'
|
||||
|
||||
@@ -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_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_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_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'
|
||||
|
||||
@@ -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_pinned: '[※ 置顶备份](#d8ff2b show_text=&7置顶:\n&8此数据备份不会按照备份时间自动排序.)'
|
||||
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_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'
|
||||
|
||||
@@ -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_pinned: '[※ 被標記的快照](#d8ff2b show_text=&7標記:\n&8此快照資料不會自動輪換更新)'
|
||||
data_manager_cause: '[⚑ %1%](#23a825-#36f539 show_text=&7保存原因:\n&8保存此快照的原因)'
|
||||
data_manager_server: '[Ⓢ %1%](#ff87b3-#f5538e show_text=&7Server:\n&8Name of the server the data was saved on)'
|
||||
data_manager_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_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'
|
||||
|
||||
@@ -60,8 +60,8 @@ redis:
|
||||
password: ''
|
||||
use_ssl: false
|
||||
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
|
||||
mode: DELAY
|
||||
# The data synchronization mode to use (LOCKSTEP or DELAY). LOCKSTEP is recommended for most networks. Docs: https://william278.net/docs/husksync/sync-modes
|
||||
mode: LOCKSTEP
|
||||
# The number of data snapshot backups that should be kept at once per user
|
||||
max_user_data_snapshots: 16
|
||||
# Number of hours between new snapshots being saved as backups (Use "0" to backup all snapshots)
|
||||
|
||||
@@ -3,8 +3,8 @@ HuskSync offers two built-in **synchronization modes** that utilise Redis and My
|
||||

|
||||
|
||||
## 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 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 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 `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`.
|
||||
|
||||
@@ -15,11 +15,22 @@ You can change which sync mode you are using by editing the `sync_mode` setting
|
||||
|
||||
```yaml
|
||||
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
|
||||
mode: DELAY
|
||||
# The data synchronization mode to use (LOCKSTEP or DELAY). LOCKSTEP is recommended for most networks. Docs: https://william278.net/docs/husksync/sync-modes
|
||||
mode: LOCKSTEP
|
||||
```
|
||||
</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
|
||||
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.
|
||||
@@ -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.
|
||||
|
||||
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)
|
||||
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.
|
||||
@@ -3,11 +3,11 @@ org.gradle.jvmargs='-Dfile.encoding=UTF-8'
|
||||
org.gradle.daemon=true
|
||||
javaVersion=16
|
||||
|
||||
plugin_version=3.1.2
|
||||
plugin_version=3.2
|
||||
plugin_archive=husksync
|
||||
plugin_description=A modern, cross-server player data synchronization system
|
||||
|
||||
jedis_version=5.1.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
|
||||
|
||||
@@ -17,7 +17,6 @@ shadowJar {
|
||||
relocate 'org.json', 'net.william278.husksync.libraries.json'
|
||||
relocate 'com.fatboyindustrial', '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.intellij', 'net.william278.husksync.libraries'
|
||||
relocate 'com.zaxxer', 'net.william278.husksync.libraries'
|
||||
|
||||
@@ -19,10 +19,14 @@
|
||||
|
||||
package net.william278.husksync;
|
||||
|
||||
import net.kyori.adventure.audience.Audience;
|
||||
import net.william278.husksync.listener.BukkitEventListener;
|
||||
import net.william278.husksync.listener.PaperEventListener;
|
||||
import org.bukkit.entity.Player;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
import java.util.UUID;
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
public class PaperHuskSync extends BukkitHuskSync {
|
||||
|
||||
@@ -32,4 +36,11 @@ public class PaperHuskSync extends BukkitHuskSync {
|
||||
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;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user