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 🛎️'
|
- 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 }}
|
||||||
|
|||||||
@@ -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'
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -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;
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|
||||||
|
|||||||
@@ -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);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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())
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -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'
|
||||||
|
|||||||
@@ -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'
|
||||||
|
|||||||
@@ -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'
|
||||||
|
|||||||
@@ -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'
|
||||||
|
|||||||
@@ -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'
|
||||||
|
|||||||
@@ -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'
|
||||||
|
|||||||
@@ -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'
|
||||||
|
|||||||
@@ -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'
|
||||||
|
|||||||
@@ -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'
|
||||||
|
|||||||
@@ -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'
|
||||||
|
|||||||
@@ -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'
|
||||||
|
|||||||
@@ -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'
|
||||||
|
|||||||
@@ -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'
|
||||||
|
|||||||
@@ -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'
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -3,8 +3,8 @@ HuskSync offers two built-in **synchronization modes** that utilise Redis and My
|
|||||||

|

|
||||||
|
|
||||||
## 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)
|
|
||||||
@@ -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
|
||||||
|
|||||||
@@ -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'
|
||||||
|
|||||||
@@ -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;
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user