9
0
mirror of https://github.com/WiIIiam278/HuskSync.git synced 2025-12-24 00:59:18 +00:00

Compare commits

..

14 Commits
3.2 ... 3.2.1

Author SHA1 Message Date
William
414246f243 fix: Handle Bukkit objects that don't fully implement Keyed 2023-12-26 14:57:40 +00:00
William
a3e269c00b docs: document /husksync status 2023-12-26 14:42:28 +00:00
William
bf9f29ffe9 refactor: Improve display of /husksync status 2023-12-26 14:41:39 +00:00
William
29bd2e1319 feat: Add /husksync status report menu 2023-12-26 14:28:41 +00:00
William
2475a9b3c6 docs: Fix license headers 2023-12-26 12:49:07 +00:00
William
2a52cc9086 ci: bump github-wiki-action to v4 2023-12-26 12:42:59 +00:00
William
237abf9698 deps: bump adventure-platform to 4.3.2 2023-12-26 12:41:24 +00:00
William
adbc264532 Merge remote-tracking branch 'origin/master' 2023-12-26 12:41:20 +00:00
jhqwqmc
f9cfec7d03 Update zh-cn.yml (#220) 2023-12-26 12:40:27 +00:00
William
29805bfe04 docs: bump to 3.2.1 2023-12-26 12:39:42 +00:00
William
8d2e5a6a52 fix: Enum#valueOf throwing on legacy stat-map conversion 2023-12-26 12:39:24 +00:00
William
d4f61bd646 refactor: catch Throwable, not Exception 2023-12-26 12:38:07 +00:00
William
55173be04b docs: More on updated default sync mode 2023-12-21 19:11:56 +00:00
William
e7078c9542 docs: Document updated default sync mode 2023-12-21 19:04:22 +00:00
28 changed files with 235 additions and 73 deletions

View File

@@ -19,9 +19,6 @@ jobs:
- name: 'Checkout for CI 🛎️'
uses: actions/checkout@v4
- name: 'Push Docs to Github Wiki 📄️'
uses: Andrew-Chen-Wang/github-wiki-action@v3
env:
WIKI_DIR: 'docs/'
GH_TOKEN: ${{ github.token }}
GH_MAIL: 'actions@github.com'
GH_NAME: 'github-actions[bot]'
uses: Andrew-Chen-Wang/github-wiki-action@v4
with:
path: 'docs'

View File

@@ -7,7 +7,7 @@ dependencies {
implementation 'net.william278:mapdataapi:1.0.3'
implementation 'net.william278:andjam:1.0.2'
implementation 'me.lucko:commodore:2.2'
implementation 'net.kyori:adventure-platform-bukkit:4.3.1'
implementation 'net.kyori:adventure-platform-bukkit:4.3.2'
implementation 'dev.triumphteam:triumph-gui:3.1.7'
implementation 'space.arim.morepaperlib:morepaperlib:0.4.3'
implementation 'de.tr7zw:item-nbt-api:2.12.2'

View File

@@ -48,6 +48,8 @@ import java.util.logging.Level;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import static net.william278.husksync.util.BukkitKeyedAdapter.*;
public abstract class BukkitData implements Data {
@Override
@@ -608,7 +610,7 @@ public abstract class BukkitData implements Data {
Map.Entry::getKey,
entry -> entry.getValue().entrySet().stream()
.flatMap(blockEntry -> {
Material material = Material.matchMaterial(blockEntry.getKey());
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))
@@ -622,7 +624,7 @@ public abstract class BukkitData implements Data {
Map.Entry::getKey,
entry -> entry.getValue().entrySet().stream()
.flatMap(itemEntry -> {
Material material = Material.matchMaterial(itemEntry.getKey());
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))
@@ -661,20 +663,6 @@ public abstract class BukkitData implements Data {
return new StatisticsMap(genericStats, blockStats, itemStats, entityStats);
}
@Nullable
private static Statistic matchStatistic(@NotNull String key) {
return Arrays.stream(Statistic.values())
.filter(stat -> stat.getKey().toString().equals(key))
.findFirst().orElse(null);
}
@Nullable
private static EntityType matchEntityType(@NotNull String key) {
return Arrays.stream(EntityType.values())
.filter(entityType -> entityType.getKey().toString().equals(key))
.findFirst().orElse(null);
}
@Override
public void apply(@NotNull BukkitUser user, @NotNull BukkitHuskSync plugin) throws IllegalStateException {
genericStatistics.forEach((stat, value) -> applyStat(user, stat, null, value));
@@ -708,7 +696,8 @@ public abstract class BukkitData implements Data {
public Map<String, Map<String, Integer>> getBlockStatistics() {
return blockStatistics.entrySet().stream().filter(entry -> entry.getKey() != null).collect(
TreeMap::new,
(m, e) -> m.put(e.getKey().getKey().toString(), convertStatistics(e.getValue())), TreeMap::putAll
(m, e) -> getKeyName(e.getKey()).ifPresent(key -> m.put(key, convertStatistics(e.getValue()))),
TreeMap::putAll
);
}
@@ -717,7 +706,8 @@ public abstract class BukkitData implements Data {
public Map<String, Map<String, Integer>> getItemStatistics() {
return itemStatistics.entrySet().stream().filter(entry -> entry.getKey() != null).collect(
TreeMap::new,
(m, e) -> m.put(e.getKey().getKey().toString(), convertStatistics(e.getValue())), TreeMap::putAll
(m, e) -> getKeyName(e.getKey()).ifPresent(key -> m.put(key, convertStatistics(e.getValue()))),
TreeMap::putAll
);
}
@@ -726,7 +716,8 @@ public abstract class BukkitData implements Data {
public Map<String, Map<String, Integer>> getEntityStatistics() {
return entityStatistics.entrySet().stream().filter(entry -> entry.getKey() != null).collect(
TreeMap::new,
(m, e) -> m.put(e.getKey().getKey().toString(), convertStatistics(e.getValue())), TreeMap::putAll
(m, e) -> getKeyName(e.getKey()).ifPresent(key -> m.put(key, convertStatistics(e.getValue()))),
TreeMap::putAll
);
}
@@ -734,13 +725,8 @@ public abstract class BukkitData implements Data {
private <T extends Keyed> Map<String, Integer> convertStatistics(@NotNull Map<T, Integer> stats) {
return stats.entrySet().stream().filter(entry -> entry.getKey() != null).collect(
TreeMap::new,
(m, e) -> {
try {
m.put(e.getKey().getKey().toString(), e.getValue());
} catch (Throwable t) {
// Ignore; skip elements with invalid keys (e.g., legacy materials)
}
}, TreeMap::putAll
(m, e) -> getKeyName(e.getKey()).ifPresent(key -> m.put(key, e.getValue())),
TreeMap::putAll
);
}

View File

@@ -143,7 +143,7 @@ public class MpdbMigrator extends Migrator {
});
plugin.log(Level.INFO, "Migration complete for " + dataToMigrate.size() + " users in " + ((System.currentTimeMillis() - startTime) / 1000) + " seconds!");
return true;
} catch (Exception e) {
} catch (Throwable e) {
plugin.log(Level.SEVERE, "Error while migrating data: " + e.getMessage() + " - are your source database credentials correct?");
return false;
}

View File

@@ -40,6 +40,8 @@ import java.util.Arrays;
import java.util.function.Consumer;
import java.util.logging.Level;
import static net.william278.husksync.util.BukkitKeyedAdapter.matchMaterial;
/**
* Bukkit platform implementation of an {@link OnlineUser}
*/
@@ -80,7 +82,7 @@ public class BukkitUser extends OnlineUser implements BukkitUserDataHolder {
public void sendToast(@NotNull MineDown title, @NotNull MineDown description,
@NotNull String iconMaterial, @NotNull String backgroundType) {
try {
final Material material = Material.matchMaterial(iconMaterial);
final Material material = matchMaterial(iconMaterial);
Toast.builder((BukkitHuskSync) plugin)
.setTitle(title.toComponent())
.setDescription(description.toComponent())

View File

@@ -0,0 +1,74 @@
/*
* This file is part of HuskSync, licensed under the Apache License 2.0.
*
* Copyright (c) William278 <will27528@gmail.com>
* Copyright (c) contributors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package net.william278.husksync.util;
import org.bukkit.Keyed;
import org.bukkit.Material;
import org.bukkit.Statistic;
import org.bukkit.entity.EntityType;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.Arrays;
import java.util.Optional;
// Utility class for adapting "Keyed" Bukkit objects
public final class BukkitKeyedAdapter {
@Nullable
public static Statistic matchStatistic(@NotNull String key) {
try {
return Arrays.stream(Statistic.values())
.filter(stat -> stat.getKey().toString().equals(key))
.findFirst().orElse(null);
} catch (Throwable e) {
return null;
}
}
@Nullable
public static EntityType matchEntityType(@NotNull String key) {
try {
return Arrays.stream(EntityType.values())
.filter(entityType -> entityType.getKey().toString().equals(key))
.findFirst().orElse(null);
} catch (Throwable e) {
return null;
}
}
@Nullable
public static Material matchMaterial(@NotNull String key) {
try {
return Material.matchMaterial(key);
} catch (Throwable e) {
return null;
}
}
public static Optional<String> getKeyName(@NotNull Keyed keyed) {
try {
return Optional.of(keyed.getKey().toString());
} catch (Throwable e) {
return Optional.empty();
}
}
}

View File

@@ -19,6 +19,7 @@
package net.william278.husksync.util;
import com.google.common.collect.Maps;
import net.william278.husksync.HuskSync;
import net.william278.husksync.adapter.DataAdapter;
import net.william278.husksync.data.BukkitData;
@@ -43,6 +44,8 @@ import java.time.OffsetDateTime;
import java.util.*;
import java.util.logging.Level;
import static net.william278.husksync.util.BukkitKeyedAdapter.*;
public class BukkitLegacyConverter extends LegacyConverter {
public BukkitLegacyConverter(@NotNull HuskSync plugin) {
@@ -197,36 +200,44 @@ public class BukkitLegacyConverter extends LegacyConverter {
@NotNull
private BukkitData.Statistics readStatisticMaps(@NotNull JSONObject untyped, @NotNull JSONObject blocks,
@NotNull JSONObject items, @NotNull JSONObject entities) {
final Map<Statistic, Integer> genericStats = new HashMap<>();
untyped.keys().forEachRemaining(stat -> genericStats.put(Statistic.valueOf(stat), untyped.getInt(stat)));
// Read generic stats
final Map<Statistic, Integer> genericStats = Maps.newHashMap();
untyped.keys().forEachRemaining(stat -> genericStats.put(matchStatistic(stat), untyped.getInt(stat)));
final Map<Statistic, Map<Material, Integer>> blockStats = new HashMap<>();
blocks.keys().forEachRemaining(stat -> {
final JSONObject blockStat = blocks.getJSONObject(stat);
final Map<Material, Integer> blockMap = new HashMap<>();
blockStat.keys().forEachRemaining(block -> blockMap.put(Material.valueOf(block), blockStat.getInt(block)));
blockStats.put(Statistic.valueOf(stat), blockMap);
});
// Read block & item stats
final Map<Statistic, Map<Material, Integer>> blockStats, itemStats;
blockStats = readMaterialStatistics(blocks);
itemStats = readMaterialStatistics(items);
final Map<Statistic, Map<Material, Integer>> itemStats = new HashMap<>();
items.keys().forEachRemaining(stat -> {
final JSONObject itemStat = items.getJSONObject(stat);
final Map<Material, Integer> itemMap = new HashMap<>();
itemStat.keys().forEachRemaining(item -> itemMap.put(Material.valueOf(item), itemStat.getInt(item)));
itemStats.put(Statistic.valueOf(stat), itemMap);
});
final Map<Statistic, Map<EntityType, Integer>> entityStats = new HashMap<>();
// Read entity stats
final Map<Statistic, Map<EntityType, Integer>> entityStats = Maps.newHashMap();
entities.keys().forEachRemaining(stat -> {
final JSONObject entityStat = entities.getJSONObject(stat);
final Map<EntityType, Integer> entityMap = new HashMap<>();
entityStat.keys().forEachRemaining(entity -> entityMap.put(EntityType.valueOf(entity), entityStat.getInt(entity)));
entityStats.put(Statistic.valueOf(stat), entityMap);
entityStat.keys().forEachRemaining(entity -> entityMap.put(matchEntityType(entity), entityStat.getInt(entity)));
entityStats.put(matchStatistic(stat), entityMap);
});
return BukkitData.Statistics.from(genericStats, blockStats, itemStats, entityStats);
}
@NotNull
private Map<Statistic, Map<Material, Integer>> readMaterialStatistics(@NotNull JSONObject items) {
final Map<Statistic, Map<Material, Integer>> itemStats = Maps.newHashMap();
items.keys().forEachRemaining(stat -> {
final JSONObject itemStat = items.getJSONObject(stat);
final Map<Material, Integer> itemMap = Maps.newHashMap();
itemStat.keys().forEachRemaining(item -> {
final Material material = matchMaterial(item);
if (material != null) {
itemMap.put(material, itemStat.getInt(item));
}
});
itemStats.put(matchStatistic(stat), itemMap);
});
return itemStats;
}
// Deserialize a legacy item stack array
@NotNull
public ItemStack[] deserializeLegacyItemStacks(@NotNull String items) {

View File

@@ -1,5 +1,6 @@
husksync {
update;
about;
status;
reload;
}

View File

@@ -18,7 +18,7 @@ dependencies {
}
compileOnly 'net.kyori:adventure-api:4.15.0'
compileOnly 'net.kyori:adventure-platform-api:4.3.1'
compileOnly 'net.kyori:adventure-platform-api:4.3.2'
compileOnly 'org.jetbrains:annotations:24.1.0'
compileOnly 'com.github.plan-player-analytics:Plan:5.5.2272'
compileOnly "redis.clients:jedis:$jedis_version"

View File

@@ -21,6 +21,9 @@ package net.william278.husksync.command;
import de.themoep.minedown.adventure.MineDown;
import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.JoinConfiguration;
import net.kyori.adventure.text.event.HoverEvent;
import net.kyori.adventure.text.format.NamedTextColor;
import net.kyori.adventure.text.format.TextColor;
import net.william278.desertwell.about.AboutMenu;
import net.william278.desertwell.util.UpdateChecker;
@@ -28,10 +31,12 @@ import net.william278.husksync.HuskSync;
import net.william278.husksync.migrator.Migrator;
import net.william278.husksync.user.CommandUser;
import net.william278.husksync.user.OnlineUser;
import org.apache.commons.text.WordUtils;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.*;
import java.util.function.Function;
import java.util.logging.Level;
import java.util.stream.Collectors;
@@ -39,6 +44,7 @@ public class HuskSyncCommand extends Command implements TabProvider {
private static final Map<String, Boolean> SUB_COMMANDS = Map.of(
"about", false,
"status", true,
"reload", true,
"migrate", true,
"update", true
@@ -92,6 +98,13 @@ public class HuskSyncCommand extends Command implements TabProvider {
switch (subCommand) {
case "about" -> executor.sendMessage(aboutMenu.toComponent());
case "status" -> {
getPlugin().getLocales().getLocale("system_status_header").ifPresent(executor::sendMessage);
executor.sendMessage(Component.join(
JoinConfiguration.newlines(),
Arrays.stream(StatusLine.values()).map(s -> s.get(plugin)).toList()
));
}
case "reload" -> {
try {
plugin.loadConfigs();
@@ -182,4 +195,63 @@ public class HuskSyncCommand extends Command implements TabProvider {
};
}
private enum StatusLine {
PLUGIN_VERSION(plugin -> Component.text("v" + plugin.getPluginVersion().toStringWithoutMetadata())
.appendSpace().append(plugin.getPluginVersion().getMetadata().isBlank() ? Component.empty()
: Component.text("(build " + plugin.getPluginVersion().getMetadata() + ")"))),
PLATFORM_TYPE(plugin -> Component.text(WordUtils.capitalizeFully(plugin.getPlatformType()))),
LANGUAGE(plugin -> Component.text(plugin.getSettings().getLanguage())),
MINECRAFT_VERSION(plugin -> Component.text(plugin.getMinecraftVersion().toString())),
JAVA_VERSION(plugin -> Component.text(System.getProperty("java.version"))),
JAVA_VENDOR(plugin -> Component.text(System.getProperty("java.vendor"))),
SYNC_MODE(plugin -> Component.text(WordUtils.capitalizeFully(plugin.getSettings().getSyncMode().toString()))),
DELAY_LATENCY(plugin -> Component.text(plugin.getSettings().getNetworkLatencyMilliseconds() + "ms")),
SERVER_NAME(plugin -> Component.text(plugin.getServerName())),
DATABASE_TYPE(plugin -> Component.text(plugin.getSettings().getDatabaseType().getDisplayName())),
IS_DATABASE_LOCAL(plugin -> getLocalhostBoolean(plugin.getSettings().getMySqlHost())),
USING_REDIS_SENTINEL(plugin -> getBoolean(!plugin.getSettings().getRedisSentinelMaster().isBlank())),
USING_REDIS_PASSWORD(plugin -> getBoolean(!plugin.getSettings().getRedisPassword().isBlank())),
REDIS_USING_SSL(plugin -> getBoolean(plugin.getSettings().redisUseSsl())),
IS_REDIS_LOCAL(plugin -> getLocalhostBoolean(plugin.getSettings().getRedisHost())),
DATA_TYPES(plugin -> Component.join(
JoinConfiguration.commas(true),
plugin.getRegisteredDataTypes().stream().map(i -> {
boolean enabled = plugin.getSettings().isSyncFeatureEnabled(i);
return Component.textOfChildren(Component
.text(i.toString()).appendSpace().append(Component.text(enabled ? '✔' : '❌')))
.color(enabled ? NamedTextColor.GREEN : NamedTextColor.RED)
.hoverEvent(HoverEvent.showText(Component.text(enabled ? "Enabled" : "Disabled")));
}).toList()
));
private final Function<HuskSync, Component> supplier;
StatusLine(@NotNull Function<HuskSync, Component> supplier) {
this.supplier = supplier;
}
@NotNull
private Component get(@NotNull HuskSync plugin) {
return Component
.text("").appendSpace()
.append(Component.text(
WordUtils.capitalizeFully(name().replaceAll("_", " ")),
TextColor.color(0x848484)
))
.append(Component.text(':')).append(Component.space().color(NamedTextColor.WHITE))
.append(supplier.apply(plugin));
}
@NotNull
private static Component getBoolean(boolean value) {
return Component.text(value ? "Yes" : "No", value ? NamedTextColor.GREEN : NamedTextColor.RED);
}
@NotNull
private static Component getLocalhostBoolean(@NotNull String value) {
return getBoolean(value.equals("127.0.0.1") || value.equals("0.0.0.0")
|| value.equals("localhost") || value.equals("::1"));
}
}
}

View File

@@ -115,7 +115,7 @@ public class DataDumper {
} else {
return "(Failed to upload to logs site, got: " + connection.getResponseCode() + ")";
}
} catch (Exception e) {
} catch (Throwable e) {
plugin.log(Level.SEVERE, "Failed to upload data to logs site", e);
}
return "(Failed to upload to logs site)";

View File

@@ -48,6 +48,7 @@ save_cause_converted_from_v2: 'converted from v2'
up_to_date: '[HuskSync](#00fb9a bold) [| You are running the latest version of HuskSync (v%1%).](#00fb9a)'
update_available: '[HuskSync](#ff7e5e bold) [| A new version of HuskSync is available: v%1% (running: v%2%).](#ff7e5e)'
reload_complete: '[HuskSync](#00fb9a bold) [| Презаредихме конфигурацията и файловете със съобщения.](#00fb9a)\n[⚠ Ensure config files are up-to-date on all servers!](#00fb9a)\n[A restart is needed for config changes to take effect.](#00fb9a italic)'
system_status_header: '[HuskSync](#00fb9a bold) [| System status report:](#00fb9a)'
error_invalid_syntax: '[Грешка:](#ff3300) [Неправилен синтаксис. Използвайте:](#ff7e5e) [%1%](#ff7e5e italic show_text=&#ff7e5e&Click to suggest suggest_command=%1%)'
error_invalid_player: '[Грешка:](#ff3300) [Не можахме да открием играч с това име.](#ff7e5e)'
error_no_permission: '[Грешка:](#ff3300) [Нямате право да използвате тази команда](#ff7e5e)'

View File

@@ -48,6 +48,7 @@ save_cause_converted_from_v2: 'Import von v2'
reload_complete: '[HuskSync](#00fb9a bold) [| Die Konfigurations- und Sprachdateien wurden neu geladen.](#00fb9a)\n[⚠ Stelle sicher, dass die Konfigurationsdateien auf allen Servern aktuell sind!](#00fb9a)\n[Ein Neustart wird benötigt, damit Konfigurations-Änderungen wirkbar werden.](#00fb9a italic)'
up_to_date: '[HuskSync](#00fb9a bold) [| Du verwendest die neuste Version von HuskSync (v%1%).](#00fb9a)'
update_available: '[HuskSync](#ff7e5e bold) [| Eine neue Version von HuskSync ist verfügbar: v%1% (Aktuelle Version: v%2%).](#ff7e5e)'
system_status_header: '[HuskSync](#00fb9a bold) [| System status report:](#00fb9a)'
error_invalid_syntax: '[Fehler:](#ff3300) [Falsche Syntax. Nutze:](#ff7e5e) [%1%](#ff7e5e italic show_text=&#ff7e5e&Click to suggest suggest_command=%1%)'
error_invalid_player: '[Fehler:](#ff3300) [Es konnte kein Spieler mit diesem Namen gefunden werden.](#ff7e5e)'
error_no_permission: '[Fehler:](#ff3300) [Du hast nicht die benötigten Berechtigungen um diesen Befehl auszuführen](#ff7e5e)'

View File

@@ -48,6 +48,7 @@ save_cause_converted_from_v2: 'converted from v2'
up_to_date: '[HuskSync](#00fb9a bold) [| You are running the latest version of HuskSync (v%1%).](#00fb9a)'
update_available: '[HuskSync](#ff7e5e bold) [| A new version of HuskSync is available: v%1% (running: v%2%).](#ff7e5e)'
reload_complete: '[HuskSync](#00fb9a bold) [| Reloaded config and message files.](#00fb9a)\n[⚠ Ensure config files are up-to-date on all servers!](#00fb9a)\n[A restart is needed for config changes to take effect.](#00fb9a italic)'
system_status_header: '[HuskSync](#00fb9a bold) [| System status report:](#00fb9a)'
error_invalid_syntax: '[Error:](#ff3300) [Incorrect syntax. Usage:](#ff7e5e) [%1%](#ff7e5e italic show_text=&#ff7e5e&Click to suggest suggest_command=%1%)'
error_invalid_player: '[Error:](#ff3300) [Could not find a player by that name.](#ff7e5e)'
error_no_permission: '[Error:](#ff3300) [You do not have permission to execute this command](#ff7e5e)'

View File

@@ -48,6 +48,7 @@ save_cause_converted_from_v2: 'converted from v2'
up_to_date: '[HuskSync](#00fb9a bold) [| You are running the latest version of HuskSync (v%1%).](#00fb9a)'
update_available: '[HuskSync](#ff7e5e bold) [| A new version of HuskSync is available: v%1% (running: v%2%).](#ff7e5e)'
reload_complete: '[HuskSync](#00fb9a bold) [| Recargada la configuración y los archivos de lenguaje.](#00fb9a)\n[⚠ Ensure config files are up-to-date on all servers!](#00fb9a)\n[A restart is needed for config changes to take effect.](#00fb9a italic)'
system_status_header: '[HuskSync](#00fb9a bold) [| System status report:](#00fb9a)'
error_invalid_syntax: '[Error:](#ff3300) [Sintanxis incorrecta. Usa:](#ff7e5e) [%1%](#ff7e5e italic show_text=&#ff7e5e&Click to suggest suggest_command=%1%)'
error_invalid_player: '[Error:](#ff3300) [No se ha podido encontrar un jugador con ese nombre.](#ff7e5e)'
error_no_permission: '[Error:](#ff3300) [No tienes permisos para ejecutar este comando.](#ff7e5e)'

View File

@@ -48,6 +48,7 @@ save_cause_converted_from_v2: 'converted from v2'
up_to_date: '[HuskSync](#00fb9a bold) [| Il plugin è all''ultima versione disponibile (v%1%).](#00fb9a)'
update_available: '[HuskSync](#ff7e5e bold) [| Disponibile una nuova versione: v%1% (running: v%2%).](#ff7e5e)'
reload_complete: '[HuskSync](#00fb9a bold) [| Configurazione e messaggi ricaricati.](#00fb9a)\n[⚠ Ensure config files are up-to-date on all servers!](#00fb9a)\n[A restart is needed for config changes to take effect.](#00fb9a italic)'
system_status_header: '[HuskSync](#00fb9a bold) [| System status report:](#00fb9a)'
error_invalid_syntax: '[Errore:](#ff3300) [Sintassi errata. Usa:](#ff7e5e) [%1%](#ff7e5e italic show_text=&#ff7e5e&Click to suggest suggest_command=%1%)'
error_invalid_player: '[Errore:](#ff3300) [Impossibile trovare un giocatore con questo nome.](#ff7e5e)'
error_no_permission: '[Errore:](#ff3300) [Non hai il permesso di usare questo comando](#ff7e5e)'

View File

@@ -48,6 +48,7 @@ save_cause_converted_from_v2: 'converted from v2'
up_to_date: '[HuskSync](#00fb9a bold) [| HuskSyncの最新バージョンを実行しています(v%1%).](#00fb9a)'
update_available: '[HuskSync](#ff7e5e bold) [| HuskSyncの最新バージョンが更新されています: v%1% (実行中: v%2%).](#ff7e5e)'
reload_complete: '[HuskSync](#00fb9a bold) [| 設定ファイルとメッセージファイルを再読み込みしました。](#00fb9a)\n[⚠ すべてのサーバーで設定ファイルが最新であることを確認してください!](#00fb9a)\n[設定の変更を有効にするには再起動が必要です。](#00fb9a italic)'
system_status_header: '[HuskSync](#00fb9a bold) [| System status report:](#00fb9a)'
error_invalid_syntax: '[Error:](#ff3300) [構文が正しくありません。使用法:](#ff7e5e) [%1%](#ff7e5e italic show_text=&#ff7e5e&クリックでサジェスト suggest_command=%1%)'
error_invalid_player: '[Error:](#ff3300) [そのプレイヤーは見つかりませんでした](#ff7e5e)'
error_no_permission: '[Error:](#ff3300) [このコマンドを実行する権限がありません](#ff7e5e)'

View File

@@ -48,6 +48,7 @@ save_cause_converted_from_v2: 'converted from v2'
up_to_date: '[HuskSync](#00fb9a bold) [| 가장 최신 버전의 HuskSync를 실행 중입니다 (v%1%).](#00fb9a)'
update_available: '[HuskSync](#ff7e5e bold) [| 새로운 버전의 HuskSync가 존재합니다: v%1% (현재 버전: v%2%).](#ff7e5e)'
reload_complete: '[HuskSync](#00fb9a bold) [| 콘피그와 메시지 파일을 다시 불러왔습니다.](#00fb9a)\n[⚠ 모든 서버의 컨피그 파일을 변경하였는지 확인하세요!](#00fb9a)\n[몇몇 설정은 재시작 후에 적용됩니다.](#00fb9a italic)'
system_status_header: '[HuskSync](#00fb9a bold) [| System status report:](#00fb9a)'
error_invalid_syntax: '[오류:](#ff3300) [잘못된 사용법. 사용법:](#ff7e5e) [%1%](#ff7e5e italic show_text=&#ff7e5e&클릭하여 입력할 수 있습니다. suggest_command=%1%)'
error_invalid_player: '[오류:](#ff3300) [해당 이름의 사용자를 찾을 수 없습니다.](#ff7e5e)'
error_no_permission: '[오류:](#ff3300) [해당 명령어를 사용할 권한이 없습니다.](#ff7e5e)'

View File

@@ -48,6 +48,7 @@ save_cause_converted_from_v2: 'converted from v2'
up_to_date: '[HuskSync](#00fb9a bold) [| Je gebruikt de nieuwste versie van HuskSync (v%1%).](#00fb9a)'
update_available: '[HuskSync](#ff7e5e bold) [| Er is een nieuwe versie van HuskSync beschikbaar: v%1% (huidige versie: v%2%).](#ff7e5e)'
reload_complete: '[HuskSync](#00fb9a bold) [| Configuratie- en berichtbestanden opnieuw geladen.](#00fb9a)\n[⚠ Controleer of de configuratiebestanden up-to-date zijn op alle servers!](#00fb9a)\n[Een herstart is nodig voor de configuratiewijzigingen van kracht te laten worden.](#00fb9a italic)'
system_status_header: '[HuskSync](#00fb9a bold) [| System status report:](#00fb9a)'
error_invalid_syntax: '[Error:](#ff3300) [Onjuiste syntaxis. Gebruik:](#ff7e5e) [%1%](#ff7e5e italic show_text=&#ff7e5e&Click to suggest suggest_command=%1%)'
error_invalid_player: '[Error:](#ff3300) [Kan geen speler met die naam vinden.](#ff7e5e)'
error_no_permission: '[Error:](#ff3300) [Je hebt geen toestemming om deze opdracht uit te voeren](#ff7e5e)'

View File

@@ -48,6 +48,7 @@ save_cause_converted_from_v2: 'converted from v2'
up_to_date: '[HuskSync](#00fb9a bold) [| You are running the latest version of HuskSync (v%1%).](#00fb9a)'
update_available: '[HuskSync](#ff7e5e bold) [| A new version of HuskSync is available: v%1% (running: v%2%).](#ff7e5e)'
reload_complete: '[HuskSync](#00fb9a bold) [| Arquivos de configuração e mensagens recarregados.](#00fb9a)\n[⚠ Ensure config files are up-to-date on all servers!](#00fb9a)\n[A restart is needed for config changes to take effect.](#00fb9a italic)'
system_status_header: '[HuskSync](#00fb9a bold) [| System status report:](#00fb9a)'
error_invalid_syntax: '[Error:](#ff3300) [Sintaxe incorreta. Utilize:](#ff7e5e) [%1%](#ff7e5e italic show_text=&#ff7e5e&Click to suggest suggest_command=%1%)'
error_invalid_player: '[Error:](#ff3300) [Não foi possível encontrar um jogador com esse nome.](#ff7e5e)'
error_no_permission: '[Error:](#ff3300) [Você não tem permissão para executar este comando](#ff7e5e)'

View File

@@ -48,6 +48,7 @@ save_cause_converted_from_v2: 'конвертация с v2'
up_to_date: '[HuskSync](#00fb9a bold) [| Вы используете последнюю версию HuskSync (v%1%).](#00fb9a)'
update_available: '[HuskSync](#ff7e5e bold) [| Доступна новая версия HuskSync: v%1% (текущая: v%2%).](#ff7e5e)'
reload_complete: '[HuskSync](#00fb9a bold) [| Конфигурация и файлы локализации перезагружены.](#00fb9a)\n[⚠ Убедитесь, что файлы конфигурации обновлены на всех серверах!](#00fb9a)\n[Необходима перезагрузка для вступления изменений конфигурации в силу.](#00fb9a italic)'
system_status_header: '[HuskSync](#00fb9a bold) [| System status report:](#00fb9a)'
error_invalid_syntax: '[Ошибка:](#ff3300) [Неправильный синтаксис. Используйте:](#ff7e5e) [%1%](#ff7e5e italic show_text=&#ff7e5e&Click to suggest suggest_command=%1%)'
error_invalid_player: '[Ошибка:](#ff3300) [Не удалось найти игрока с данным именем.](#ff7e5e)'
error_no_permission: '[Ошибка:](#ff3300) [У вас недостаточно прав для выполнения данной команды.](#ff7e5e)'

View File

@@ -48,6 +48,7 @@ save_cause_converted_from_v2: 'converted from v2'
up_to_date: '[HuskSync](#00fb9a bold) [| HuskSync\''in en son sürümünü kullanıyorsunuz (v%1%).](#00fb9a)'
update_available: '[HuskSync](#ff7e5e bold) [| HuskSync\''in yeni bir sürümü mevcut: v%1% (kullanılan sürüm: v%2%).](#ff7e5e)'
reload_complete: '[HuskSync](#00fb9a bold) [| Yapılandırma ve mesaj dosyaları yeniden yüklendi.](#00fb9a)\n[⚠ Lütfen yapılandırma dosyalarının tüm sunucularda güncel olduğundan emin olun!](#00fb9a)\n[Yapılandırma değişikliklerinin etkili olabilmesi için bir yeniden başlatma gereklidir.](#00fb9a italic)'
system_status_header: '[HuskSync](#00fb9a bold) [| System status report:](#00fb9a)'
error_invalid_syntax: '[Hata:](#ff3300) [Yanlış sözdizimi. Kullanım:](#ff7e5e) [%1%](#ff7e5e italic show_text=&#ff7e5e&Öneri için tıklayın Suggest_command=%1%)'
error_invalid_player: '[Hata:](#ff3300) [Bu isimde bir oyuncu bulunamadı.](#ff7e5e)'
error_no_permission: '[Hata:](#ff3300) [Bu komutu gerçekleştirmek için izniniz yok](#ff7e5e)'

View File

@@ -48,6 +48,7 @@ save_cause_converted_from_v2: 'converted from v2'
up_to_date: '[HuskSync](#00fb9a bold) [| You are running the latest version of HuskSync (v%1%).](#00fb9a)'
update_available: '[HuskSync](#ff7e5e bold) [| A new version of HuskSync is available: v%1% (running: v%2%).](#ff7e5e)'
reload_complete: '[HuskSync](#00fb9a bold) [| Перезавантажено конфіґ та файли повідомлень.](#00fb9a)\n[⚠ Ensure config files are up-to-date on all servers!](#00fb9a)\n[A restart is needed for config changes to take effect.](#00fb9a italic)'
system_status_header: '[HuskSync](#00fb9a bold) [| System status report:](#00fb9a)'
error_invalid_syntax: '[Помилка:](#ff3300) [Неправильний синтакс. Використання:](#ff7e5e) [%1%](#ff7e5e italic show_text=&#ff7e5e&Click to suggest suggest_command=%1%)'
error_invalid_player: '[Помилка:](#ff3300) [Гравця не знайдено](#ff7e5e)'
error_no_permission: '[Помилка:](#ff3300) [Ввас немає дозволу на використання цієї команди](#ff7e5e)'

View File

@@ -34,20 +34,21 @@ list_page_jumper_button: '[%1%](show_text=&7跳转到页面 %1% run_command=%2%
list_page_jumper_current_page: '[%1%](#00fb9a)'
list_page_jumper_separator: ' '
list_page_jumper_group_separator: '…'
save_cause_disconnect: 'disconnect'
save_cause_world_save: 'world save'
save_cause_death: 'death'
save_cause_server_shutdown: 'server shutdown'
save_cause_inventory_command: 'inventory command'
save_cause_enderchest_command: 'enderchest command'
save_cause_backup_restore: 'backup restore'
save_cause_disconnect: '断开连接'
save_cause_world_save: '保存世界'
save_cause_death: '死亡'
save_cause_server_shutdown: '服务器关闭'
save_cause_inventory_command: '背包命令'
save_cause_enderchest_command: '末影箱命令'
save_cause_backup_restore: '备份还原'
save_cause_api: 'API'
save_cause_mpdb_migration: 'MPDB migration'
save_cause_legacy_migration: 'legacy migration'
save_cause_converted_from_v2: 'converted from v2'
save_cause_mpdb_migration: 'MPDB迁移'
save_cause_legacy_migration: '旧版迁移'
save_cause_converted_from_v2: '从v2转换'
up_to_date: '[HuskSync](#00fb9a bold) [| 你正在使用最新版本的HuskSync (v%1%)](#00fb9a)'
update_available: '[HuskSync](#ff7e5e bold) [| 一个新版本的HuskSync已经可以更新: v%1% (当前: v%2%)](#ff7e5e)'
reload_complete: '[HuskSync](#00fb9a bold) [| 插件配置和语言文件已重载.](#00fb9a)\n[⚠ 确保所有服务器上的配置文件都是最新的!](#00fb9a)\n[需要重新启动配置更改才能生效.](#00fb9a italic)'
system_status_header: '[HuskSync](#00fb9a bold) [| System status report:](#00fb9a)'
error_invalid_syntax: ':](#ff3300) [格式错误, 使用方法:](#ff7e5e) [%1%](#ff7e5e italic show_text=&#ff7e5e&点击建议 suggest_command=%1%)'
error_invalid_player: '[错误:](#ff3300) [无法找到目标玩家.](#ff7e5e)'
error_no_permission: '[错误:](#ff3300) [你没有执行此指令的权限](#ff7e5e)'

View File

@@ -48,6 +48,7 @@ save_cause_converted_from_v2: 'converted from v2'
up_to_date: '[HuskSync](#00fb9a bold) [| 您運行的是最新版本的 HuskSync (v%1%).](#00fb9a)'
update_available: '[HuskSync](#ff7e5e bold) [| 發現可用的新版本: v%1% (running: v%2%).](#ff7e5e)'
reload_complete: '[HuskSync](#00fb9a bold) [| 已重新載入配置和訊息文件](#00fb9a)\n[⚠ Ensure config files are up-to-date on all servers!](#00fb9a)\n[A restart is needed for config changes to take effect.](#00fb9a italic)'
system_status_header: '[HuskSync](#00fb9a bold) [| System status report:](#00fb9a)'
error_invalid_syntax: '[錯誤:](#ff3300) [語法不正確,用法:](#ff7e5e) [%1%](#ff7e5e italic show_text=&#ff7e5e&Click to suggest suggest_command=%1%)'
error_invalid_player: '[錯誤:](#ff3300) [找不到這位玩家](#ff7e5e)'
error_no_permission: '[錯誤:](#ff3300) [您沒有權限執行這個指令](#ff7e5e)'

View File

@@ -11,7 +11,7 @@ This page contains a table of HuskSync commands and their required permission no
<tbody>
<!-- /husksync command -->
<tr>
<td rowspan="5"><code>/husksync</code></td>
<td rowspan="6"><code>/husksync</code></td>
<td><code>/husksync</code></td>
<td>View & manage plugin system information</td>
<td><code>husksync.command.husksync</code></td>
@@ -21,6 +21,11 @@ This page contains a table of HuskSync commands and their required permission no
<td>View information about the plugin</td>
<td><code>husksync.command.husksync.about</code></td>
</tr>
<tr>
<td><code>/husksync status</code></td>
<td>View plugin system status information</td>
<td><code>husksync.command.husksync.status</code></td>
</tr>
<tr>
<td><code>/husksync reload</code></td>
<td>Reload the plugin configuration</td>

View File

@@ -3,7 +3,7 @@ HuskSync offers two built-in **synchronization modes** that utilise Redis and My
![Overall architecture of the synchronisation systems](https://raw.githubusercontent.com/WiIIiam278/HuskSync/master/images/system-diagram.png)
## Available Modes
* 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 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, and was made the default in v3.2.
* 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`.
@@ -31,6 +31,8 @@ The `LOCKSTEP` sync mode works as described below:
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)
`LOCKSTEP` has been the default sync mode since HuskSync v3.2, and is recommended for most networks.
## 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.
@@ -40,6 +42,4 @@ The `DELAY` sync mode works as described below:
* If present, it will continuously attempt to read for a `DATA_UPDATE` key; when read, their data will be set from the snapshot deserialized from Redis.
* If not present, their data will be pulled from the database (as though they joined the network)
`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.
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.

View File

@@ -3,7 +3,7 @@ org.gradle.jvmargs='-Dfile.encoding=UTF-8'
org.gradle.daemon=true
javaVersion=16
plugin_version=3.2
plugin_version=3.2.1
plugin_archive=husksync
plugin_description=A modern, cross-server player data synchronization system