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

feat: rework locked maps syncing (#464)

* Better maps syncing (#2)

* Do not create new views for maps from current world

* Fix maps in shulkers not converting

* Add bundle support for map conversion

* Rework map sync

* Fix empty statements in database

* Fix missing imports

* Rename connectMapIds -> bindMapIds

* Use data adapter to save maps

* Split Mongo readMapData

* Split MySQL readMapData

* Split Postgres readMapData

* Update database schemas

Use server names instead of world UUIDs

* Update Database class

* Update MongoDbDatabase class

* Update MySqlDatabase class

* Update PostgresDatabase class

* Update BukkitMapPersister class

Use server names instead of world UUIDs

* Remove unused code

* Add my nickname to contributors :)

* Start implementing Redis map caching

* Continue implementing Redis map caching

* Bind map ids on Redis before writing to DB

* Finish implementing Redis map data caching

* refactor: decouple new map logic Redis caching from DB

* test: enable debug logging in test suite

* docs: update docs with new username method

* feat: adjust a method name

---------

Co-authored-by: Sóla Lusøt <60041069+solaluset@users.noreply.github.com>
This commit is contained in:
William
2025-03-07 16:06:27 +00:00
committed by GitHub
parent fbb8ec3048
commit 904c65ba39
28 changed files with 821 additions and 165 deletions

View File

@@ -56,7 +56,7 @@ import net.william278.husksync.sync.DataSyncer;
import net.william278.husksync.user.BukkitUser; import net.william278.husksync.user.BukkitUser;
import net.william278.husksync.user.OnlineUser; import net.william278.husksync.user.OnlineUser;
import net.william278.husksync.util.BukkitLegacyConverter; import net.william278.husksync.util.BukkitLegacyConverter;
import net.william278.husksync.util.BukkitMapPersister; import net.william278.husksync.maps.BukkitMapHandler;
import net.william278.husksync.util.BukkitTask; import net.william278.husksync.util.BukkitTask;
import net.william278.husksync.util.LegacyConverter; import net.william278.husksync.util.LegacyConverter;
import net.william278.toilet.BukkitToilet; import net.william278.toilet.BukkitToilet;
@@ -84,7 +84,7 @@ import java.util.stream.Collectors;
@NoArgsConstructor @NoArgsConstructor
@SuppressWarnings("unchecked") @SuppressWarnings("unchecked")
public class BukkitHuskSync extends JavaPlugin implements HuskSync, BukkitTask.Supplier, public class BukkitHuskSync extends JavaPlugin implements HuskSync, BukkitTask.Supplier,
BukkitEventDispatcher, BukkitMapPersister { BukkitEventDispatcher, BukkitMapHandler {
/** /**
* Metrics ID for <a href="https://bstats.org/plugin/bukkit/HuskSync%20-%20Bukkit/13140">HuskSync on Bukkit</a>. * Metrics ID for <a href="https://bstats.org/plugin/bukkit/HuskSync%20-%20Bukkit/13140">HuskSync on Bukkit</a>.

View File

@@ -20,7 +20,7 @@
package net.william278.husksync.data; package net.william278.husksync.data;
import net.william278.husksync.BukkitHuskSync; import net.william278.husksync.BukkitHuskSync;
import net.william278.husksync.util.BukkitMapPersister; import net.william278.husksync.maps.BukkitMapHandler;
import org.bukkit.entity.Player; import org.bukkit.entity.Player;
import org.bukkit.inventory.PlayerInventory; import org.bukkit.inventory.PlayerInventory;
import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.NotNull;
@@ -163,7 +163,7 @@ public interface BukkitUserDataHolder extends UserDataHolder {
} }
@NotNull @NotNull
default BukkitMapPersister getMapPersister() { default BukkitMapHandler getMapPersister() {
return (BukkitHuskSync) getPlugin(); return (BukkitHuskSync) getPlugin();
} }

View File

@@ -135,7 +135,7 @@ public class BukkitEventListener extends EventListener implements BukkitJoinEven
@EventHandler(ignoreCancelled = true) @EventHandler(ignoreCancelled = true)
public void onMapInitialize(@NotNull MapInitializeEvent event) { public void onMapInitialize(@NotNull MapInitializeEvent event) {
if (plugin.getSettings().getSynchronization().isPersistLockedMaps() && event.getMap().isLocked()) { if (plugin.getSettings().getSynchronization().isPersistLockedMaps() && event.getMap().isLocked()) {
getPlugin().runAsync(() -> ((BukkitHuskSync) plugin).renderMapFromFile(event.getMap())); getPlugin().runAsync(() -> ((BukkitHuskSync) plugin).renderPersistedMap(event.getMap()));
} }
} }

View File

@@ -17,15 +17,14 @@
* limitations under the License. * limitations under the License.
*/ */
package net.william278.husksync.util; package net.william278.husksync.maps;
import com.google.common.collect.Lists; import com.google.common.collect.Lists;
import de.tr7zw.changeme.nbtapi.NBT; import de.tr7zw.changeme.nbtapi.NBT;
import de.tr7zw.changeme.nbtapi.iface.ReadWriteNBT; import de.tr7zw.changeme.nbtapi.iface.ReadWriteNBT;
import de.tr7zw.changeme.nbtapi.iface.ReadableNBT; import de.tr7zw.changeme.nbtapi.iface.ReadableNBT;
import net.querz.nbt.io.NBTUtil;
import net.querz.nbt.tag.CompoundTag;
import net.william278.husksync.BukkitHuskSync; import net.william278.husksync.BukkitHuskSync;
import net.william278.husksync.redis.RedisManager;
import net.william278.mapdataapi.MapBanner; import net.william278.mapdataapi.MapBanner;
import net.william278.mapdataapi.MapData; import net.william278.mapdataapi.MapData;
import org.bukkit.Bukkit; import org.bukkit.Bukkit;
@@ -35,27 +34,29 @@ import org.bukkit.block.Container;
import org.bukkit.entity.Player; import org.bukkit.entity.Player;
import org.bukkit.inventory.ItemStack; import org.bukkit.inventory.ItemStack;
import org.bukkit.inventory.meta.BlockStateMeta; import org.bukkit.inventory.meta.BlockStateMeta;
import org.bukkit.inventory.meta.BundleMeta;
import org.bukkit.inventory.meta.MapMeta; import org.bukkit.inventory.meta.MapMeta;
import org.bukkit.map.*; import org.bukkit.map.*;
import org.jetbrains.annotations.ApiStatus; import org.jetbrains.annotations.ApiStatus;
import org.jetbrains.annotations.Blocking;
import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable; import org.jetbrains.annotations.Nullable;
import java.awt.*; import java.awt.*;
import java.io.File; import java.io.IOException;
import java.util.List; import java.util.List;
import java.util.*; import java.util.*;
import java.util.function.Function; import java.util.function.Function;
import java.util.logging.Level; import java.util.logging.Level;
public interface BukkitMapPersister { public interface BukkitMapHandler {
// The map used to store HuskSync data in ItemStack NBT // The map used to store HuskSync data in ItemStack NBT
String MAP_DATA_KEY = "husksync:persisted_locked_map"; String MAP_DATA_KEY = "husksync:persisted_locked_map";
// The key used to store the serialized map data in NBT // Name of server the map originates from
String MAP_PIXEL_DATA_KEY = "canvas_data"; String MAP_ORIGIN_KEY = "origin";
// The key used to store the map of World UIDs to MapView IDs in NBT // Original map id
String MAP_VIEW_ID_MAPPINGS_KEY = "id_mappings"; String MAP_ID_KEY = "id";
/** /**
* Persist locked maps in an array of {@link ItemStack}s * Persist locked maps in an array of {@link ItemStack}s
@@ -99,11 +100,108 @@ public interface BukkitMapPersister {
} else if (item.getItemMeta() instanceof BlockStateMeta b && b.getBlockState() instanceof Container box) { } else if (item.getItemMeta() instanceof BlockStateMeta b && b.getBlockState() instanceof Container box) {
forEachMap(box.getInventory().getContents(), function); forEachMap(box.getInventory().getContents(), function);
b.setBlockState(box); b.setBlockState(box);
item.setItemMeta(b);
} else if (item.getItemMeta() instanceof BundleMeta bundle) {
bundle.setItems(List.of(forEachMap(bundle.getItems().toArray(ItemStack[]::new), function)));
item.setItemMeta(bundle);
} }
} }
return items; return items;
} }
@Blocking
private void writeMapData(@NotNull String serverName, int mapId, MapData data) {
final byte[] dataBytes = getPlugin().getDataAdapter().toBytes(new AdaptableMapData(data));
getRedisManager().setMapData(serverName, mapId, dataBytes);
getPlugin().getDatabase().saveMapData(serverName, mapId, dataBytes);
}
@Nullable
@Blocking
private Map.Entry<MapData, Boolean> readMapData(@NotNull String serverName, int mapId) {
final Map.Entry<byte[], Boolean> readData = fetchMapData(serverName, mapId);
if (readData == null) {
return null;
}
return deserializeMapData(readData);
}
@Nullable
@Blocking
private Map.Entry<byte[], Boolean> fetchMapData(@NotNull String serverName, int mapId) {
return fetchMapData(serverName, mapId, true);
}
@Nullable
@Blocking
private Map.Entry<byte[], Boolean> fetchMapData(@NotNull String serverName, int mapId, boolean doReverseLookup) {
// Read from Redis cache
final byte[] redisData = getRedisManager().getMapData(serverName, mapId);
if (redisData != null) {
return new AbstractMap.SimpleImmutableEntry<>(redisData, true);
}
// Read from database and set to Redis
@Nullable Map.Entry<byte[], Boolean> databaseData = getPlugin().getDatabase().getMapData(serverName, mapId);
if (databaseData != null) {
getRedisManager().setMapData(serverName, mapId, databaseData.getKey());
return databaseData;
}
// Otherwise, lookup a reverse map binding
if (doReverseLookup) {
return fetchReversedMapData(serverName, mapId);
}
return null;
}
@Nullable
private Map.Entry<byte[], Boolean> fetchReversedMapData(@NotNull String serverName, int mapId) {
// Lookup binding from Redis cache, then fetch data if found
Map.Entry<String, Integer> binding = getRedisManager().getReversedMapBound(serverName, mapId);
if (binding != null) {
return fetchMapData(binding.getKey(), binding.getValue(), false);
}
// Lookup binding from database, then set to Redis & fetch data if found
binding = getPlugin().getDatabase().getMapBinding(serverName, mapId);
if (binding != null) {
getRedisManager().bindMapIds(binding.getKey(), binding.getValue(), serverName, mapId);
return fetchMapData(binding.getKey(), binding.getValue(), false);
}
return null;
}
@Nullable
private Map.Entry<MapData, Boolean> deserializeMapData(@NotNull Map.Entry<byte[], Boolean> data) {
try {
return new AbstractMap.SimpleImmutableEntry<>(
getPlugin().getDataAdapter().fromBytes(data.getKey(), AdaptableMapData.class)
.getData(getPlugin().getDataVersion(getPlugin().getMinecraftVersion())),
data.getValue()
);
} catch (IOException e) {
getPlugin().log(Level.WARNING, "Failed to deserialize map data", e);
return null;
}
}
// Get the bound map ID
private int getBoundMapId(@NotNull String fromServerName, int fromMapId, @NotNull String toServerName) {
// Get the map ID from Redis, if set
final Optional<Integer> redisId = getRedisManager().getBoundMapId(fromServerName, fromMapId, toServerName);
if (redisId.isPresent()) {
return redisId.get();
}
// Get from the database; if found, set to Redis
final int result = getPlugin().getDatabase().getBoundMapId(fromServerName, fromMapId, toServerName);
if (result != -1) {
getPlugin().getRedisManager().bindMapIds(fromServerName, fromMapId, toServerName, result);
}
return result;
}
@NotNull @NotNull
private ItemStack persistMapView(@NotNull ItemStack map, @NotNull Player delegateRenderer) { private ItemStack persistMapView(@NotNull ItemStack map, @NotNull Player delegateRenderer) {
final MapMeta meta = Objects.requireNonNull((MapMeta) map.getItemMeta()); final MapMeta meta = Objects.requireNonNull((MapMeta) map.getItemMeta());
@@ -131,95 +229,82 @@ public interface BukkitMapPersister {
// Persist map data // Persist map data
final ReadWriteNBT mapData = nbt.getOrCreateCompound(MAP_DATA_KEY); final ReadWriteNBT mapData = nbt.getOrCreateCompound(MAP_DATA_KEY);
final String worldUid = view.getWorld().getUID().toString(); final String serverName = getPlugin().getServerName();
mapData.setByteArray(MAP_PIXEL_DATA_KEY, canvas.extractMapData().toBytes()); mapData.setString(MAP_ORIGIN_KEY, serverName);
nbt.getOrCreateCompound(MAP_VIEW_ID_MAPPINGS_KEY).setInteger(worldUid, view.getId()); mapData.setInteger(MAP_ID_KEY, meta.getMapId());
getPlugin().debug(String.format("Saved data for locked map (#%s, UID: %s)", view.getId(), worldUid)); if (readMapData(serverName, meta.getMapId()) == null) {
writeMapData(serverName, meta.getMapId(), canvas.extractMapData());
}
getPlugin().debug(String.format("Saved data for locked map (#%s, server: %s)", view.getId(), serverName));
}); });
return map; return map;
} }
@SuppressWarnings("deprecation")
@NotNull @NotNull
private ItemStack applyMapView(@NotNull ItemStack map) { private ItemStack applyMapView(@NotNull ItemStack map) {
final int dataVersion = getPlugin().getDataVersion(getPlugin().getMinecraftVersion());
final MapMeta meta = Objects.requireNonNull((MapMeta) map.getItemMeta()); final MapMeta meta = Objects.requireNonNull((MapMeta) map.getItemMeta());
NBT.get(map, nbt -> { NBT.get(map, nbt -> {
if (!nbt.hasTag(MAP_DATA_KEY)) { if (!nbt.hasTag(MAP_DATA_KEY)) {
return; return;
} }
final ReadableNBT mapData = nbt.getCompound(MAP_DATA_KEY); final ReadableNBT mapData = nbt.getCompound(MAP_DATA_KEY);
final ReadableNBT mapIds = nbt.getCompound(MAP_VIEW_ID_MAPPINGS_KEY); if (mapData == null) {
if (mapData == null || mapIds == null) {
return; return;
} }
// Search for an existing map view // Determine map ID
Optional<String> world = Optional.empty(); final String originServerName = mapData.getString(MAP_ORIGIN_KEY);
for (String worldUid : mapIds.getKeys()) { final String currentServerName = getPlugin().getServerName();
world = getPlugin().getServer().getWorlds().stream() final int originalMapId = mapData.getInteger(MAP_ID_KEY);
.map(w -> w.getUID().toString()).filter(u -> u.equals(worldUid)) int newId = currentServerName.equals(originServerName)
.findFirst(); ? originalMapId : getBoundMapId(originServerName, originalMapId, currentServerName);
if (world.isPresent()) { if (newId != -1) {
break; meta.setMapId(newId);
} map.setItemMeta(meta);
} getPlugin().debug(String.format("Map ID set to %s", newId));
if (world.isPresent()) { return;
final String uid = world.get();
final Optional<MapView> existingView = this.getMapView(mapIds.getInteger(uid));
if (existingView.isPresent()) {
final MapView view = existingView.get();
view.setLocked(true);
meta.setMapView(view);
map.setItemMeta(meta);
getPlugin().debug(String.format("View exists (#%s); updated map (UID: %s)", view.getId(), uid));
return;
}
} }
// Read the pixel data and generate a map view otherwise // Read the pixel data and generate a map view otherwise
final MapData canvasData; getPlugin().debug("Deserializing map data from NBT and generating view...");
try { final MapData canvasData = Objects.requireNonNull(readMapData(originServerName, originalMapId), "Pixel data null!").getKey();
getPlugin().debug("Deserializing map data from NBT and generating view...");
canvasData = MapData.fromByteArray(
dataVersion,
Objects.requireNonNull(mapData.getByteArray(MAP_PIXEL_DATA_KEY), "Pixel data null!"));
} catch (Throwable e) {
getPlugin().log(Level.WARNING, "Failed to deserialize map data from NBT", e);
return;
}
// Add a renderer to the map with the data and save to file // Add a renderer to the map with the data and save to file
final MapView view = generateRenderedMap(canvasData); final MapView view = generateRenderedMap(canvasData);
final String worldUid = getDefaultMapWorld().getUID().toString();
meta.setMapView(view); meta.setMapView(view);
map.setItemMeta(meta); map.setItemMeta(meta);
saveMapToFile(canvasData, view.getId());
// Set the map view ID in NBT // Bind in the database & Redis
NBT.modify(map, editable -> { final int id = view.getId();
Objects.requireNonNull(editable.getCompound(MAP_VIEW_ID_MAPPINGS_KEY), getRedisManager().bindMapIds(originServerName, originalMapId, currentServerName, id);
"Map view ID mappings compound is null") getPlugin().getDatabase().setMapBinding(originServerName, originalMapId, currentServerName, id);
.setInteger(worldUid, view.getId());
}); getPlugin().debug(String.format("Bound map to view (#%s) on server %s", id, currentServerName));
getPlugin().debug(String.format("Generated view (#%s) and updated map (UID: %s)", view.getId(), worldUid));
}); });
return map; return map;
} }
default void renderMapFromFile(@NotNull MapView view) { default void renderPersistedMap(@NotNull MapView view) {
final File mapFile = new File(getMapCacheFolder(), view.getId() + ".dat"); if (getMapView(view.getId()).isPresent()) {
if (!mapFile.exists()) {
return; return;
} }
final MapData canvasData; @Nullable final Map.Entry<MapData, Boolean> data = readMapData(getPlugin().getServerName(), view.getId());
try { if (data == null) {
canvasData = MapData.fromNbt(mapFile); final World world = view.getWorld() == null ? getDefaultMapWorld() : view.getWorld();
} catch (Throwable e) { getPlugin().debug("Not rendering map: no data in DB for world %s, map #%s."
getPlugin().log(Level.WARNING, "Failed to deserialize map data from file", e); .formatted(world.getName(), view.getId()));
return; return;
} }
if (data.getValue()) {
// from this server, doesn't need tweaking
return;
}
final MapData canvasData = data.getKey();
// Create a new map view renderer with the map data color at each pixel // Create a new map view renderer with the map data color at each pixel
// use view.removeRenderer() to remove all this maps renderers // use view.removeRenderer() to remove all this maps renderers
view.getRenderers().forEach(view::removeRenderer); view.getRenderers().forEach(view::removeRenderer);
@@ -233,32 +318,6 @@ public interface BukkitMapPersister {
setMapView(view); setMapView(view);
} }
default void saveMapToFile(@NotNull MapData data, int id) {
getPlugin().runAsync(() -> {
final File mapFile = new File(getMapCacheFolder(), id + ".dat");
if (mapFile.exists()) {
return;
}
try {
final CompoundTag rootTag = new CompoundTag();
rootTag.put("data", data.toNBT().getTag());
NBTUtil.write(rootTag, mapFile);
} catch (Throwable e) {
getPlugin().log(Level.WARNING, "Failed to serialize map data to file", e);
}
});
}
@NotNull
private File getMapCacheFolder() {
final File mapCache = new File(getPlugin().getDataFolder(), "maps");
if (!mapCache.exists() && !mapCache.mkdirs()) {
getPlugin().log(Level.WARNING, "Failed to create maps folder");
}
return mapCache;
}
// Sets the renderer of a map, and returns the generated MapView // Sets the renderer of a map, and returns the generated MapView
@NotNull @NotNull
private MapView generateRenderedMap(@NotNull MapData canvasData) { private MapView generateRenderedMap(@NotNull MapData canvasData) {
@@ -362,6 +421,8 @@ public interface BukkitMapPersister {
@SuppressWarnings({"deprecation", "removal"}) @SuppressWarnings({"deprecation", "removal"})
class PersistentMapCanvas implements MapCanvas { class PersistentMapCanvas implements MapCanvas {
private static final String BANNER_PREFIX = "banner_";
private final int mapDataVersion; private final int mapDataVersion;
private final MapView mapView; private final MapView mapView;
private final int[][] pixels = new int[128][128]; private final int[][] pixels = new int[128][128];
@@ -451,7 +512,6 @@ public interface BukkitMapPersister {
@NotNull @NotNull
private MapData extractMapData() { private MapData extractMapData() {
final List<MapBanner> banners = Lists.newArrayList(); final List<MapBanner> banners = Lists.newArrayList();
final String BANNER_PREFIX = "banner_";
for (int i = 0; i < getCursors().size(); i++) { for (int i = 0; i < getCursors().size(); i++) {
final MapCursor cursor = getCursors().getCursor(i); final MapCursor cursor = getCursors().getCursor(i);
//#if MC==12001 //#if MC==12001
@@ -477,6 +537,9 @@ public interface BukkitMapPersister {
@NotNull @NotNull
Map<Integer, MapView> getMapViews(); Map<Integer, MapView> getMapViews();
@ApiStatus.Internal
RedisManager getRedisManager();
@ApiStatus.Internal @ApiStatus.Internal
@NotNull @NotNull
BukkitHuskSync getPlugin(); BukkitHuskSync getPlugin();

View File

@@ -146,7 +146,7 @@ public class LegacyMigrator extends Migrator {
try { try {
plugin.getDatabase().addSnapshot(data.user(), convertedData); plugin.getDatabase().addSnapshot(data.user(), convertedData);
} catch (Throwable e) { } catch (Throwable e) {
plugin.log(Level.SEVERE, "Failed to migrate legacy data for " + data.user().getUsername() + ": " + e.getMessage()); plugin.log(Level.SEVERE, "Failed to migrate legacy data for " + data.user().getName() + ": " + e.getMessage());
return; return;
} }

View File

@@ -6,6 +6,7 @@ dependencies {
api 'commons-io:commons-io:2.18.0' api 'commons-io:commons-io:2.18.0'
api 'org.apache.commons:commons-text:1.13.0' api 'org.apache.commons:commons-text:1.13.0'
api 'net.william278:minedown:1.8.2' api 'net.william278:minedown:1.8.2'
api 'net.william278:mapdataapi:2.0'
api 'org.json:json:20250107' api 'org.json:json:20250107'
api 'com.google.code.gson:gson:2.12.1' api 'com.google.code.gson:gson:2.12.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'

View File

@@ -51,7 +51,7 @@ public class EnderChestCommand extends ItemsCommand {
} }
// Display opening message // Display opening message
plugin.getLocales().getLocale("ender_chest_viewer_opened", user.getUsername(), plugin.getLocales().getLocale("ender_chest_viewer_opened", user.getName(),
snapshot.getTimestamp().format(DateTimeFormatter snapshot.getTimestamp().format(DateTimeFormatter
.ofLocalizedDateTime(FormatStyle.MEDIUM, FormatStyle.SHORT))) .ofLocalizedDateTime(FormatStyle.MEDIUM, FormatStyle.SHORT)))
.ifPresent(viewer::sendMessage); .ifPresent(viewer::sendMessage);
@@ -60,8 +60,8 @@ public class EnderChestCommand extends ItemsCommand {
final Data.Items.EnderChest enderChest = optionalEnderChest.get(); final Data.Items.EnderChest enderChest = optionalEnderChest.get();
viewer.showGui( viewer.showGui(
enderChest, enderChest,
plugin.getLocales().getLocale("ender_chest_viewer_menu_title", user.getUsername()) plugin.getLocales().getLocale("ender_chest_viewer_menu_title", user.getName())
.orElse(new MineDown(String.format("%s's Ender Chest", user.getUsername()))), .orElse(new MineDown(String.format("%s's Ender Chest", user.getName()))),
allowEdit, allowEdit,
enderChest.getSlotCount(), enderChest.getSlotCount(),
(itemsOnClose) -> { (itemsOnClose) -> {

View File

@@ -69,7 +69,8 @@ public class HuskSyncCommand extends PluginCommand {
AboutMenu.Credit.of("HookWoods").description("Code"), AboutMenu.Credit.of("HookWoods").description("Code"),
AboutMenu.Credit.of("Preva1l").description("Code"), AboutMenu.Credit.of("Preva1l").description("Code"),
AboutMenu.Credit.of("hanbings").description("Code (Fabric porting)"), AboutMenu.Credit.of("hanbings").description("Code (Fabric porting)"),
AboutMenu.Credit.of("Stampede2011").description("Code (Fabric mixins)")) AboutMenu.Credit.of("Stampede2011").description("Code (Fabric mixins)"),
AboutMenu.Credit.of("VinerDream").description("Code"))
.credits("Translators", .credits("Translators",
AboutMenu.Credit.of("Namiu").description("Japanese (ja-jp)"), AboutMenu.Credit.of("Namiu").description("Japanese (ja-jp)"),
AboutMenu.Credit.of("anchelthe").description("Spanish (es-es)"), AboutMenu.Credit.of("anchelthe").description("Spanish (es-es)"),

View File

@@ -52,7 +52,7 @@ public class InventoryCommand extends ItemsCommand {
} }
// Display opening message // Display opening message
plugin.getLocales().getLocale("inventory_viewer_opened", user.getUsername(), plugin.getLocales().getLocale("inventory_viewer_opened", user.getName(),
snapshot.getTimestamp().format(DateTimeFormatter snapshot.getTimestamp().format(DateTimeFormatter
.ofLocalizedDateTime(FormatStyle.MEDIUM, FormatStyle.SHORT))) .ofLocalizedDateTime(FormatStyle.MEDIUM, FormatStyle.SHORT)))
.ifPresent(viewer::sendMessage); .ifPresent(viewer::sendMessage);
@@ -61,8 +61,8 @@ public class InventoryCommand extends ItemsCommand {
final Data.Items.Inventory inventory = optionalInventory.get(); final Data.Items.Inventory inventory = optionalInventory.get();
viewer.showGui( viewer.showGui(
inventory, inventory,
plugin.getLocales().getLocale("inventory_viewer_menu_title", user.getUsername()) plugin.getLocales().getLocale("inventory_viewer_menu_title", user.getName())
.orElse(new MineDown(String.format("%s's Inventory", user.getUsername()))), .orElse(new MineDown(String.format("%s's Inventory", user.getName()))),
allowEdit, allowEdit,
inventory.getSlotCount(), inventory.getSlotCount(),
(itemsOnClose) -> { (itemsOnClose) -> {

View File

@@ -83,7 +83,7 @@ public abstract class PluginCommand extends Command {
() -> CommandSyntaxException.BUILT_IN_EXCEPTIONS.dispatcherUnknownArgument().createWithContext(reader) () -> CommandSyntaxException.BUILT_IN_EXCEPTIONS.dispatcherUnknownArgument().createWithContext(reader)
); );
}, (context, builder) -> { }, (context, builder) -> {
plugin.getOnlineUsers().forEach(u -> builder.suggest(u.getUsername())); plugin.getOnlineUsers().forEach(u -> builder.suggest(u.getName()));
return builder.buildFuture(); return builder.buildFuture();
}); });
} }

View File

@@ -113,7 +113,7 @@ public class UserDataCommand extends PluginCommand {
plugin.getLocales().getLocale("data_deleted", plugin.getLocales().getLocale("data_deleted",
version.toString().split("-")[0], version.toString().split("-")[0],
version.toString(), version.toString(),
user.getUsername(), user.getName(),
user.getUuid().toString()) user.getUuid().toString())
.ifPresent(executor::sendMessage); .ifPresent(executor::sendMessage);
} }
@@ -147,7 +147,7 @@ public class UserDataCommand extends PluginCommand {
plugin.getDataSyncer().saveData(user, data, (u, s) -> { plugin.getDataSyncer().saveData(user, data, (u, s) -> {
redis.getUserData(u).ifPresent(d -> redis.setUserData(u, s, RedisKeyType.TTL_1_YEAR)); redis.getUserData(u).ifPresent(d -> redis.setUserData(u, s, RedisKeyType.TTL_1_YEAR));
redis.sendUserDataUpdate(u, s); redis.sendUserDataUpdate(u, s);
plugin.getLocales().getLocale("data_restored", u.getUsername(), u.getUuid().toString(), plugin.getLocales().getLocale("data_restored", u.getName(), u.getUuid().toString(),
s.getShortId(), s.getId().toString()).ifPresent(executor::sendMessage); s.getShortId(), s.getId().toString()).ifPresent(executor::sendMessage);
}); });
} }
@@ -169,7 +169,7 @@ public class UserDataCommand extends PluginCommand {
plugin.getDatabase().pinSnapshot(user, data.getId()); plugin.getDatabase().pinSnapshot(user, data.getId());
} }
plugin.getLocales().getLocale(data.isPinned() ? "data_unpinned" : "data_pinned", data.getShortId(), plugin.getLocales().getLocale(data.isPinned() ? "data_unpinned" : "data_pinned", data.getShortId(),
data.getId().toString(), user.getUsername(), user.getUuid().toString()) data.getId().toString(), user.getName(), user.getUuid().toString())
.ifPresent(executor::sendMessage); .ifPresent(executor::sendMessage);
} }
@@ -187,7 +187,7 @@ public class UserDataCommand extends PluginCommand {
final DataSnapshot.Packed userData = data.get(); final DataSnapshot.Packed userData = data.get();
final UserDataDumper dumper = UserDataDumper.create(userData, user, plugin); final UserDataDumper dumper = UserDataDumper.create(userData, user, plugin);
try { try {
plugin.getLocales().getLocale("data_dumped", userData.getShortId(), user.getUsername(), plugin.getLocales().getLocale("data_dumped", userData.getShortId(), user.getName(),
(type == DumpType.WEB ? dumper.toWeb() : dumper.toFile())) (type == DumpType.WEB ? dumper.toWeb() : dumper.toFile()))
.ifPresent(executor::sendMessage); .ifPresent(executor::sendMessage);
} catch (Throwable e) { } catch (Throwable e) {

View File

@@ -26,6 +26,7 @@ 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 org.jetbrains.annotations.Nullable;
import java.io.IOException; import java.io.IOException;
import java.nio.charset.StandardCharsets; import java.nio.charset.StandardCharsets;
@@ -56,8 +57,8 @@ public abstract class Database {
@SuppressWarnings("SameParameterValue") @SuppressWarnings("SameParameterValue")
@NotNull @NotNull
protected final String[] getSchemaStatements(@NotNull String schemaFileName) throws IOException { protected final String[] getSchemaStatements(@NotNull String schemaFileName) throws IOException {
return formatStatementTables(new String(Objects.requireNonNull(plugin.getResource(schemaFileName)) return Arrays.stream(formatStatementTables(new String(Objects.requireNonNull(plugin.getResource(schemaFileName))
.readAllBytes(), StandardCharsets.UTF_8)).split(";"); .readAllBytes(), StandardCharsets.UTF_8)).split(";")).filter(s -> !s.isBlank()).toArray(String[]::new);
} }
/** /**
@@ -70,7 +71,9 @@ public abstract class Database {
protected final String formatStatementTables(@NotNull String sql) { protected final String formatStatementTables(@NotNull String sql) {
final Settings.DatabaseSettings settings = plugin.getSettings().getDatabase(); final Settings.DatabaseSettings settings = plugin.getSettings().getDatabase();
return sql.replaceAll("%users_table%", settings.getTableName(TableName.USERS)) return sql.replaceAll("%users_table%", settings.getTableName(TableName.USERS))
.replaceAll("%user_data_table%", settings.getTableName(TableName.USER_DATA)); .replaceAll("%user_data_table%", settings.getTableName(TableName.USER_DATA))
.replaceAll("%map_data_table%", settings.getTableName(TableName.MAP_DATA))
.replaceAll("%map_ids_table%", settings.getTableName(TableName.MAP_IDS));
} }
/** /**
@@ -246,6 +249,58 @@ public abstract class Database {
}); });
} }
/**
* Write map data to a database
*
* @param serverName Name of the server the map originates from
* @param mapId Original map ID
* @param data Map data
*/
@Blocking
public abstract void saveMapData(@NotNull String serverName, int mapId, byte @NotNull [] data);
/**
* Read map data from a database
*
* @param serverName Name of the server the map originates from
* @param mapId Original map ID
* @return Map.Entry (key: map data, value: is from current world)
*/
@Blocking
public abstract @Nullable Map.Entry<byte[], Boolean> getMapData(@NotNull String serverName, int mapId);
/**
* Get a map server -> ID binding in the database
*
* @param serverName Name of the server the map originates from
* @param mapId Original map ID
* @return Map.Entry (key: server name, value: map ID)
*/
@Blocking
public abstract @Nullable Map.Entry<String, Integer> getMapBinding(@NotNull String serverName, int mapId);
/**
* Bind map IDs across different servers
*
* @param fromServerName Name of the server the map originates from
* @param fromMapId Original map ID
* @param toServerName Name of the new server
* @param toMapId New map ID
*/
@Blocking
public abstract void setMapBinding(@NotNull String fromServerName, int fromMapId, @NotNull String toServerName, int toMapId);
/**
* Get map ID for the new server
*
* @param fromServerName Name of the server the map originates from
* @param fromMapId Original map ID
* @param toServerName Name of the new server
* @return New map ID or -1 if not found
*/
@Blocking
public abstract int getBoundMapId(@NotNull String fromServerName, int fromMapId, @NotNull String toServerName);
/** /**
* Wipes <b>all</b> {@link User} entries from the database. * Wipes <b>all</b> {@link User} entries from the database.
* <b>This should only be used when preparing tables for a data migration.</b> * <b>This should only be used when preparing tables for a data migration.</b>
@@ -283,7 +338,9 @@ public abstract class Database {
@Getter @Getter
public enum TableName { public enum TableName {
USERS("husksync_users"), USERS("husksync_users"),
USER_DATA("husksync_user_data"); USER_DATA("husksync_user_data"),
MAP_DATA("husksync_map_data"),
MAP_IDS("husksync_map_ids");
private final String defaultName; private final String defaultName;

View File

@@ -35,13 +35,11 @@ import org.bson.conversions.Bson;
import org.bson.types.Binary; import org.bson.types.Binary;
import org.jetbrains.annotations.Blocking; import org.jetbrains.annotations.Blocking;
import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.time.Instant; import java.time.Instant;
import java.time.OffsetDateTime; import java.time.OffsetDateTime;
import java.util.List; import java.util.*;
import java.util.Optional;
import java.util.TimeZone;
import java.util.UUID;
import java.util.logging.Level; import java.util.logging.Level;
public class MongoDbDatabase extends Database { public class MongoDbDatabase extends Database {
@@ -50,11 +48,15 @@ public class MongoDbDatabase extends Database {
private final String usersTable; private final String usersTable;
private final String userDataTable; private final String userDataTable;
private final String mapDataTable;
private final String mapIdsTable;
public MongoDbDatabase(@NotNull HuskSync plugin) { public MongoDbDatabase(@NotNull HuskSync plugin) {
super(plugin); super(plugin);
this.usersTable = plugin.getSettings().getDatabase().getTableName(TableName.USERS); this.usersTable = plugin.getSettings().getDatabase().getTableName(TableName.USERS);
this.userDataTable = plugin.getSettings().getDatabase().getTableName(TableName.USER_DATA); this.userDataTable = plugin.getSettings().getDatabase().getTableName(TableName.USER_DATA);
this.mapDataTable = plugin.getSettings().getDatabase().getTableName(TableName.MAP_DATA);
this.mapIdsTable = plugin.getSettings().getDatabase().getTableName(TableName.MAP_IDS);
} }
@Override @Override
@@ -74,9 +76,15 @@ public class MongoDbDatabase extends Database {
if (mongoCollectionHelper.getCollection(userDataTable) == null) { if (mongoCollectionHelper.getCollection(userDataTable) == null) {
mongoCollectionHelper.createCollection(userDataTable); mongoCollectionHelper.createCollection(userDataTable);
} }
if (mongoCollectionHelper.getCollection(mapDataTable) == null) {
mongoCollectionHelper.createCollection(mapDataTable);
}
if (mongoCollectionHelper.getCollection(mapIdsTable) == null) {
mongoCollectionHelper.createCollection(mapIdsTable);
}
} catch (Exception e) { } catch (Exception e) {
throw new IllegalStateException("Failed to establish a connection to the MongoDB database. " + throw new IllegalStateException("Failed to establish a connection to the MongoDB database. " +
"Please check the supplied database credentials in the config file", e); "Please check the supplied database credentials in the config file", e);
} }
} }
@@ -99,7 +107,7 @@ public class MongoDbDatabase extends Database {
try { try {
getUser(user.getUuid()).ifPresentOrElse( getUser(user.getUuid()).ifPresentOrElse(
existingUser -> { existingUser -> {
if (!existingUser.getUsername().equals(user.getUsername())) { if (!existingUser.getName().equals(user.getName())) {
// Update a user's name if it has changed in the database // Update a user's name if it has changed in the database
try { try {
Document filter = new Document("uuid", existingUser.getUuid()); Document filter = new Document("uuid", existingUser.getUuid());
@@ -108,7 +116,7 @@ public class MongoDbDatabase extends Database {
throw new MongoException("User document returned null!"); throw new MongoException("User document returned null!");
} }
Bson updates = Updates.set("username", user.getUsername()); Bson updates = Updates.set("username", user.getName());
mongoCollectionHelper.updateDocument(usersTable, doc, updates); mongoCollectionHelper.updateDocument(usersTable, doc, updates);
} catch (MongoException e) { } catch (MongoException e) {
plugin.log(Level.SEVERE, "Failed to insert a user into the database", e); plugin.log(Level.SEVERE, "Failed to insert a user into the database", e);
@@ -118,7 +126,7 @@ public class MongoDbDatabase extends Database {
() -> { () -> {
// Insert new player data into the database // Insert new player data into the database
try { try {
Document doc = new Document("uuid", user.getUuid()).append("username", user.getUsername()); Document doc = new Document("uuid", user.getUuid()).append("username", user.getName());
mongoCollectionHelper.insertDocument(usersTable, doc); mongoCollectionHelper.insertDocument(usersTable, doc);
} catch (MongoException e) { } catch (MongoException e) {
plugin.log(Level.SEVERE, "Failed to insert a user into the database", e); plugin.log(Level.SEVERE, "Failed to insert a user into the database", e);
@@ -345,6 +353,85 @@ public class MongoDbDatabase extends Database {
} }
} }
@Blocking
@Override
public void saveMapData(@NotNull String serverName, int mapId, byte @NotNull [] data) {
try {
Document doc = new Document("server_name", serverName)
.append("map_id", mapId)
.append("data", new Binary(data));
mongoCollectionHelper.insertDocument(mapDataTable, doc);
} catch (MongoException e) {
plugin.log(Level.SEVERE, "Failed to write map data to the database", e);
}
}
@Blocking
@Override
public @Nullable Map.Entry<byte[], Boolean> getMapData(@NotNull String serverName, int mapId) {
try {
Document filter = new Document("server_name", serverName).append("map_id", mapId);
FindIterable<Document> iterable = mongoCollectionHelper.getCollection(mapDataTable).find(filter);
Document doc = iterable.first();
if (doc != null) {
final Binary bin = doc.get("data", Binary.class);
return Map.entry(bin.getData(), true);
}
} catch (MongoException e) {
plugin.log(Level.SEVERE, "Failed to get map data from the database", e);
}
return null;
}
@Blocking
@Override
public @Nullable Map.Entry<String, Integer> getMapBinding(@NotNull String serverName, int mapId) {
final Document filter = new Document("to_server_name", serverName).append("to_id", mapId);
final FindIterable<Document> iterable = mongoCollectionHelper.getCollection(mapIdsTable).find(filter);
final Document doc = iterable.first();
if (doc != null) {
return new AbstractMap.SimpleImmutableEntry<>(
doc.getString("server_name"),
doc.getInteger("to_id")
);
}
return null;
}
@Blocking
@Override
public void setMapBinding(@NotNull String fromServerName, int fromMapId, @NotNull String toServerName, int toMapId) {
try {
final Document doc = new Document("from_server_name", fromServerName)
.append("from_id", fromMapId)
.append("to_server_name", toServerName)
.append("to_id", toMapId);
mongoCollectionHelper.insertDocument(mapIdsTable, doc);
} catch (MongoException e) {
plugin.log(Level.SEVERE, "Failed to connect map IDs in the database", e);
}
}
@Blocking
@Override
public int getBoundMapId(@NotNull String fromServerName, int fromMapId, @NotNull String toServerName) {
try {
final Document filter = new Document("from_server_name", fromServerName)
.append("from_id", fromMapId)
.append("to_server_name", toServerName);
final FindIterable<Document> iterable = mongoCollectionHelper.getCollection(mapIdsTable).find(filter);
final Document doc = iterable.first();
if (doc != null) {
return doc.getInteger("to_id");
}
return -1;
} catch (MongoException e) {
plugin.log(Level.SEVERE, "Failed to get new map id from the database", e);
return -1;
}
}
@Blocking @Blocking
@Override @Override
public void wipeDatabase() { public void wipeDatabase() {

View File

@@ -27,6 +27,7 @@ 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 org.jetbrains.annotations.Nullable;
import java.io.ByteArrayInputStream; import java.io.ByteArrayInputStream;
import java.io.IOException; import java.io.IOException;
@@ -127,11 +128,11 @@ public class MySqlDatabase extends Database {
} }
} catch (SQLException e) { } catch (SQLException e) {
throw new IllegalStateException("Failed to create database tables. Please ensure you are running MySQL v8.0+ " + throw new IllegalStateException("Failed to create database tables. Please ensure you are running MySQL v8.0+ " +
"and that your connecting user account has privileges to create tables.", e); "and that your connecting user account has privileges to create tables.", e);
} }
} catch (SQLException | IOException e) { } catch (SQLException | IOException e) {
throw new IllegalStateException("Failed to establish a connection to the MySQL database. " + throw new IllegalStateException("Failed to establish a connection to the MySQL database. " +
"Please check the supplied database credentials in the config file", e); "Please check the supplied database credentials in the config file", e);
} }
} }
@@ -140,7 +141,7 @@ public class MySqlDatabase extends Database {
public void ensureUser(@NotNull User user) { public void ensureUser(@NotNull User user) {
getUser(user.getUuid()).ifPresentOrElse( getUser(user.getUuid()).ifPresentOrElse(
existingUser -> { existingUser -> {
if (!existingUser.getUsername().equals(user.getUsername())) { if (!existingUser.getName().equals(user.getName())) {
// Update a user's name if it has changed in the database // Update a user's name if it has changed in the database
try (Connection connection = getConnection()) { try (Connection connection = getConnection()) {
try (PreparedStatement statement = connection.prepareStatement(formatStatementTables(""" try (PreparedStatement statement = connection.prepareStatement(formatStatementTables("""
@@ -148,11 +149,12 @@ public class MySqlDatabase extends Database {
SET `username`=? SET `username`=?
WHERE `uuid`=?"""))) { WHERE `uuid`=?"""))) {
statement.setString(1, user.getUsername()); statement.setString(1, user.getName());
statement.setString(2, existingUser.getUuid().toString()); statement.setString(2, existingUser.getUuid().toString());
statement.executeUpdate(); statement.executeUpdate();
} }
plugin.log(Level.INFO, "Updated " + user.getUsername() + "'s name in the database (" + existingUser.getUsername() + " -> " + user.getUsername() + ")"); plugin.log(Level.INFO, "Updated " + user.getName() + "'s name in the database ("
+ existingUser.getName() + " -> " + user.getName() + ")");
} catch (SQLException e) { } catch (SQLException e) {
plugin.log(Level.SEVERE, "Failed to update a user's name on the database", e); plugin.log(Level.SEVERE, "Failed to update a user's name on the database", e);
} }
@@ -166,7 +168,7 @@ public class MySqlDatabase extends Database {
VALUES (?,?);"""))) { VALUES (?,?);"""))) {
statement.setString(1, user.getUuid().toString()); statement.setString(1, user.getUuid().toString());
statement.setString(2, user.getUsername()); statement.setString(2, user.getName());
statement.executeUpdate(); statement.executeUpdate();
} }
} catch (SQLException e) { } catch (SQLException e) {
@@ -433,6 +435,120 @@ public class MySqlDatabase extends Database {
} }
} }
@Blocking
@Override
public void saveMapData(@NotNull String serverName, int mapId, byte @NotNull [] data) {
try (Connection connection = getConnection()) {
try (PreparedStatement statement = connection.prepareStatement(formatStatementTables("""
INSERT INTO `%map_data_table%`
(`server_name`,`map_id`,`data`)
VALUES (?,?,?);"""))) {
statement.setString(1, serverName);
statement.setInt(2, mapId);
statement.setBlob(3, new ByteArrayInputStream(data));
statement.executeUpdate();
}
} catch (SQLException | DataAdapter.AdaptionException e) {
plugin.log(Level.SEVERE, "Failed to write map data to the database", e);
}
}
@Blocking
@Override
public @Nullable Map.Entry<byte[], Boolean> getMapData(@NotNull String serverName, int mapId) {
try (Connection connection = getConnection()) {
try (PreparedStatement statement = connection.prepareStatement(formatStatementTables("""
SELECT `data`
FROM `%map_data_table%`
WHERE `server_name`=? AND `map_id`=?
LIMIT 1;"""))) {
statement.setString(1, serverName);
statement.setInt(2, mapId);
final ResultSet resultSet = statement.executeQuery();
if (resultSet.next()) {
final Blob blob = resultSet.getBlob("data");
final byte[] dataByteArray = blob.getBytes(1, (int) blob.length());
blob.free();
return Map.entry(dataByteArray, true);
}
}
} catch (SQLException | DataAdapter.AdaptionException e) {
plugin.log(Level.SEVERE, "Failed to get map data from the database", e);
}
return null;
}
@Blocking
@Override
public @Nullable Map.Entry<String, Integer> getMapBinding(@NotNull String serverName, int mapId) {
try (Connection connection = getConnection()) {
try (PreparedStatement statement = connection.prepareStatement(formatStatementTables("""
SELECT `from_server_name`, `from_id`
FROM `%map_ids_table%`
WHERE `to_server_name`=? AND `to_id`=?
LIMIT 1;
"""))) {
statement.setString(1, serverName);
statement.setInt(2, mapId);
final ResultSet resultSet = statement.executeQuery();
if (resultSet.next()) {
return new AbstractMap.SimpleImmutableEntry<>(
resultSet.getString("from_server_name"),
resultSet.getInt("from_id")
);
}
}
} catch (SQLException | DataAdapter.AdaptionException e) {
plugin.log(Level.SEVERE, "Failed to get map data from the database", e);
}
return null;
}
@Blocking
@Override
public void setMapBinding(@NotNull String fromServerName, int fromMapId, @NotNull String toServerName, int toMapId) {
try (Connection connection = getConnection()) {
try (PreparedStatement statement = connection.prepareStatement(formatStatementTables("""
INSERT INTO `%map_ids_table%`
(`from_server_name`,`from_id`,`to_server_name`,`to_id`)
VALUES (?,?,?,?);"""))) {
statement.setString(1, fromServerName);
statement.setInt(2, fromMapId);
statement.setString(3, toServerName);
statement.setInt(4, toMapId);
statement.executeUpdate();
}
} catch (SQLException | DataAdapter.AdaptionException e) {
plugin.log(Level.SEVERE, "Failed to connect map IDs in the database", e);
}
}
@Blocking
@Override
public int getBoundMapId(@NotNull String fromServerName, int fromMapId, @NotNull String toServerName) {
try (Connection connection = getConnection()) {
try (PreparedStatement statement = connection.prepareStatement(formatStatementTables("""
SELECT `to_id`
FROM `%map_ids_table%`
WHERE `from_server_name`=? AND `from_id`=? AND `to_server_name`=?
LIMIT 1;"""))) {
statement.setString(1, fromServerName);
statement.setInt(2, fromMapId);
statement.setString(3, toServerName);
final ResultSet resultSet = statement.executeQuery();
if (resultSet.next()) {
return resultSet.getInt("to_id");
}
}
} catch (SQLException | DataAdapter.AdaptionException e) {
plugin.log(Level.SEVERE, "Failed to get new map id from the database", e);
}
return -1;
}
@Override @Override
public void wipeDatabase() { public void wipeDatabase() {
try (Connection connection = getConnection()) { try (Connection connection = getConnection()) {

View File

@@ -27,6 +27,7 @@ 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 org.jetbrains.annotations.Nullable;
import java.io.IOException; import java.io.IOException;
import java.sql.*; import java.sql.*;
@@ -120,11 +121,11 @@ public class PostgresDatabase extends Database {
} }
} catch (SQLException e) { } catch (SQLException e) {
throw new IllegalStateException("Failed to create database tables. Please ensure you are running PostgreSQL " + throw new IllegalStateException("Failed to create database tables. Please ensure you are running PostgreSQL " +
"and that your connecting user account has privileges to create tables.", e); "and that your connecting user account has privileges to create tables.", e);
} }
} catch (SQLException | IOException e) { } catch (SQLException | IOException e) {
throw new IllegalStateException("Failed to establish a connection to the PostgreSQL database. " + throw new IllegalStateException("Failed to establish a connection to the PostgreSQL database. " +
"Please check the supplied database credentials in the config file", e); "Please check the supplied database credentials in the config file", e);
} }
} }
@@ -133,7 +134,7 @@ public class PostgresDatabase extends Database {
public void ensureUser(@NotNull User user) { public void ensureUser(@NotNull User user) {
getUser(user.getUuid()).ifPresentOrElse( getUser(user.getUuid()).ifPresentOrElse(
existingUser -> { existingUser -> {
if (!existingUser.getUsername().equals(user.getUsername())) { if (!existingUser.getName().equals(user.getName())) {
// Update a user's name if it has changed in the database // Update a user's name if it has changed in the database
try (Connection connection = getConnection()) { try (Connection connection = getConnection()) {
try (PreparedStatement statement = connection.prepareStatement(formatStatementTables(""" try (PreparedStatement statement = connection.prepareStatement(formatStatementTables("""
@@ -141,11 +142,11 @@ public class PostgresDatabase extends Database {
SET username=? SET username=?
WHERE uuid=?;"""))) { WHERE uuid=?;"""))) {
statement.setString(1, user.getUsername()); statement.setString(1, user.getName());
statement.setObject(2, existingUser.getUuid()); statement.setObject(2, existingUser.getUuid());
statement.executeUpdate(); statement.executeUpdate();
} }
plugin.log(Level.INFO, "Updated " + user.getUsername() + "'s name in the database (" + existingUser.getUsername() + " -> " + user.getUsername() + ")"); plugin.log(Level.INFO, "Updated " + user.getName() + "'s name in the database (" + existingUser.getName() + " -> " + user.getName() + ")");
} catch (SQLException e) { } catch (SQLException e) {
plugin.log(Level.SEVERE, "Failed to update a user's name on the database", e); plugin.log(Level.SEVERE, "Failed to update a user's name on the database", e);
} }
@@ -159,7 +160,7 @@ public class PostgresDatabase extends Database {
VALUES (?,?);"""))) { VALUES (?,?);"""))) {
statement.setObject(1, user.getUuid()); statement.setObject(1, user.getUuid());
statement.setString(2, user.getUsername()); statement.setString(2, user.getName());
statement.executeUpdate(); statement.executeUpdate();
} }
} catch (SQLException e) { } catch (SQLException e) {
@@ -430,6 +431,118 @@ public class PostgresDatabase extends Database {
} }
} }
@Blocking
@Override
public void saveMapData(@NotNull String serverName, int mapId, byte @NotNull [] data) {
try (Connection connection = getConnection()) {
try (PreparedStatement statement = connection.prepareStatement(formatStatementTables("""
INSERT INTO %map_data_table%
(server_name,map_id,data)
VALUES (?,?,?);"""))) {
statement.setString(1, serverName);
statement.setInt(2, mapId);
statement.setBytes(3, data);
statement.executeUpdate();
}
} catch (SQLException | DataAdapter.AdaptionException e) {
plugin.log(Level.SEVERE, "Failed to write map data to the database", e);
}
}
@Blocking
@Override
public @Nullable Map.Entry<byte[], Boolean> getMapData(@NotNull String serverName, int mapId) {
try (Connection connection = getConnection()) {
try (PreparedStatement statement = connection.prepareStatement(formatStatementTables("""
SELECT data
FROM %map_data_table%
WHERE server_name=? AND map_id=?
LIMIT 1;"""))) {
statement.setString(1, serverName);
statement.setInt(2, mapId);
final ResultSet resultSet = statement.executeQuery();
if (resultSet.next()) {
final byte[] data = resultSet.getBytes("data");
return new AbstractMap.SimpleImmutableEntry<>(data, true);
}
return null;
}
} catch (SQLException | DataAdapter.AdaptionException e) {
plugin.log(Level.SEVERE, "Failed to get map data from the database", e);
}
return null;
}
@Blocking
@Override
public @Nullable Map.Entry<String, Integer> getMapBinding(@NotNull String serverName, int mapId) {
try (Connection connection = getConnection()) {
try (PreparedStatement statement = connection.prepareStatement(formatStatementTables("""
SELECT from_server_name, from_id
FROM %map_ids_table%
WHERE to_server_name=? AND to_id=?
LIMIT 1;
"""))) {
statement.setString(1, serverName);
statement.setInt(2, mapId);
final ResultSet resultSet = statement.executeQuery();
if (resultSet.next()) {
return new AbstractMap.SimpleImmutableEntry<>(
resultSet.getString("from_server_name"),
resultSet.getInt("from_id")
);
}
}
} catch (SQLException | DataAdapter.AdaptionException e) {
plugin.log(Level.SEVERE, "Failed to get map data from the database", e);
}
return null;
}
@Blocking
@Override
public void setMapBinding(@NotNull String fromServerName, int fromMapId, @NotNull String toServerName, int toMapId) {
try (Connection connection = getConnection()) {
try (PreparedStatement statement = connection.prepareStatement(formatStatementTables("""
INSERT INTO %map_ids_table%
(from_server_name,from_id,to_server_name,to_id)
VALUES (?,?,?,?);"""))) {
statement.setString(1, fromServerName);
statement.setInt(2, fromMapId);
statement.setString(3, toServerName);
statement.setInt(4, toMapId);
statement.executeUpdate();
}
} catch (SQLException | DataAdapter.AdaptionException e) {
plugin.log(Level.SEVERE, "Failed to connect map IDs in the database", e);
}
}
@Blocking
@Override
public int getBoundMapId(@NotNull String fromServerName, int fromMapId, @NotNull String toServerName) {
try (Connection connection = getConnection()) {
try (PreparedStatement statement = connection.prepareStatement(formatStatementTables("""
SELECT to_id
FROM %map_ids_table%
WHERE from_server_name=? AND from_id=? AND to_server_name=?
LIMIT 1;"""))) {
statement.setString(1, fromServerName);
statement.setInt(2, fromMapId);
statement.setString(3, toServerName);
final ResultSet resultSet = statement.executeQuery();
if (resultSet.next()) {
return resultSet.getInt("to_id");
}
}
} catch (SQLException | DataAdapter.AdaptionException e) {
plugin.log(Level.SEVERE, "Failed to get new map id from the database", e);
}
return -1;
}
@Override @Override
public void wipeDatabase() { public void wipeDatabase() {
try (Connection connection = getConnection()) { try (Connection connection = getConnection()) {

View File

@@ -0,0 +1,45 @@
/*
* 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.maps;
import com.google.gson.annotations.SerializedName;
import lombok.AllArgsConstructor;
import org.jetbrains.annotations.NotNull;
import net.william278.husksync.adapter.Adaptable;
import net.william278.mapdataapi.MapData;
import java.io.IOException;
@AllArgsConstructor
public class AdaptableMapData implements Adaptable {
@SerializedName("data")
private final byte[] data;
public AdaptableMapData(@NotNull MapData data) {
this(data.toBytes());
}
@NotNull
public MapData getData(int dataVersion) throws IOException {
return MapData.fromByteArray(dataVersion, data);
}
}

View File

@@ -27,7 +27,10 @@ public enum RedisKeyType {
LATEST_SNAPSHOT, LATEST_SNAPSHOT,
SERVER_SWITCH, SERVER_SWITCH,
DATA_CHECKOUT; DATA_CHECKOUT,
MAP_ID,
MAP_ID_REVERSED,
MAP_DATA;
public static final int TTL_1_YEAR = 60 * 60 * 24 * 7 * 52; // 1 year public static final int TTL_1_YEAR = 60 * 60 * 24 * 7 * 52; // 1 year
public static final int TTL_10_SECONDS = 10; // 10 seconds public static final int TTL_10_SECONDS = 10; // 10 seconds

View File

@@ -25,6 +25,7 @@ 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 org.jetbrains.annotations.Nullable;
import redis.clients.jedis.*; import redis.clients.jedis.*;
import redis.clients.jedis.exceptions.JedisException; import redis.clients.jedis.exceptions.JedisException;
import redis.clients.jedis.util.Pool; import redis.clients.jedis.util.Pool;
@@ -261,7 +262,7 @@ public class RedisManager extends JedisPubSub {
timeToLive, timeToLive,
data.asBytes(plugin) data.asBytes(plugin)
); );
plugin.debug(String.format("[%s] Set %s key on Redis", user.getUsername(), RedisKeyType.LATEST_SNAPSHOT)); plugin.debug(String.format("[%s] Set %s key on Redis", user.getName(), RedisKeyType.LATEST_SNAPSHOT));
} catch (Throwable e) { } catch (Throwable e) {
plugin.log(Level.SEVERE, "An exception occurred setting user data on Redis", e); plugin.log(Level.SEVERE, "An exception occurred setting user data on Redis", e);
} }
@@ -273,7 +274,7 @@ public class RedisManager extends JedisPubSub {
jedis.del( jedis.del(
getKey(RedisKeyType.LATEST_SNAPSHOT, user.getUuid(), clusterId) getKey(RedisKeyType.LATEST_SNAPSHOT, user.getUuid(), clusterId)
); );
plugin.debug(String.format("[%s] Cleared %s on Redis", user.getUsername(), RedisKeyType.LATEST_SNAPSHOT)); plugin.debug(String.format("[%s] Cleared %s on Redis", user.getName(), RedisKeyType.LATEST_SNAPSHOT));
} catch (Throwable e) { } catch (Throwable e) {
plugin.log(Level.SEVERE, "An exception occurred clearing user data on Redis", e); plugin.log(Level.SEVERE, "An exception occurred clearing user data on Redis", e);
} }
@@ -291,11 +292,11 @@ public class RedisManager extends JedisPubSub {
} else { } else {
if (jedis.del(key.getBytes(StandardCharsets.UTF_8)) == 0) { if (jedis.del(key.getBytes(StandardCharsets.UTF_8)) == 0) {
plugin.debug(String.format("[%s] %s key not set on Redis when attempting removal (%s)", plugin.debug(String.format("[%s] %s key not set on Redis when attempting removal (%s)",
user.getUsername(), RedisKeyType.DATA_CHECKOUT, key)); user.getName(), RedisKeyType.DATA_CHECKOUT, key));
return; return;
} }
} }
plugin.debug(String.format("[%s] %s %s key %s Redis (%s)", user.getUsername(), plugin.debug(String.format("[%s] %s %s key %s Redis (%s)", user.getName(),
checkedOut ? "Set" : "Removed", RedisKeyType.DATA_CHECKOUT, checkedOut ? "to" : "from", key)); checkedOut ? "Set" : "Removed", RedisKeyType.DATA_CHECKOUT, checkedOut ? "to" : "from", key));
} catch (Throwable e) { } catch (Throwable e) {
plugin.log(Level.SEVERE, "An exception occurred setting checkout to", e); plugin.log(Level.SEVERE, "An exception occurred setting checkout to", e);
@@ -310,13 +311,13 @@ public class RedisManager extends JedisPubSub {
if (readData != null) { if (readData != null) {
final String checkoutServer = new String(readData, StandardCharsets.UTF_8); final String checkoutServer = new String(readData, StandardCharsets.UTF_8);
plugin.debug(String.format("[%s] Waiting for %s %s key to be unset on Redis", plugin.debug(String.format("[%s] Waiting for %s %s key to be unset on Redis",
user.getUsername(), checkoutServer, RedisKeyType.DATA_CHECKOUT)); user.getName(), checkoutServer, RedisKeyType.DATA_CHECKOUT));
return Optional.of(checkoutServer); return Optional.of(checkoutServer);
} }
} catch (Throwable e) { } catch (Throwable e) {
plugin.log(Level.SEVERE, "An exception occurred getting 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(String.format("[%s] %s key not set on Redis", user.getUsername(), plugin.debug(String.format("[%s] %s key not set on Redis", user.getName(),
RedisKeyType.DATA_CHECKOUT)); RedisKeyType.DATA_CHECKOUT));
return Optional.empty(); return Optional.empty();
} }
@@ -354,7 +355,7 @@ public class RedisManager extends JedisPubSub {
new byte[0] new byte[0]
); );
plugin.debug(String.format("[%s] Set %s key to Redis", plugin.debug(String.format("[%s] Set %s key to Redis",
user.getUsername(), RedisKeyType.SERVER_SWITCH)); user.getName(), RedisKeyType.SERVER_SWITCH));
} catch (Throwable e) { } catch (Throwable e) {
plugin.log(Level.SEVERE, "An exception occurred setting a user's server switch key from Redis", e); plugin.log(Level.SEVERE, "An exception occurred setting a user's server switch key from Redis", e);
} }
@@ -373,11 +374,11 @@ public class RedisManager extends JedisPubSub {
final byte[] dataByteArray = jedis.get(key); final byte[] dataByteArray = jedis.get(key);
if (dataByteArray == null) { if (dataByteArray == null) {
plugin.debug(String.format("[%s] Waiting for %s key from Redis", plugin.debug(String.format("[%s] Waiting for %s key from Redis",
user.getUsername(), RedisKeyType.LATEST_SNAPSHOT)); user.getName(), RedisKeyType.LATEST_SNAPSHOT));
return Optional.empty(); return Optional.empty();
} }
plugin.debug(String.format("[%s] Read %s key from Redis", plugin.debug(String.format("[%s] Read %s key from Redis",
user.getUsername(), RedisKeyType.LATEST_SNAPSHOT)); user.getName(), RedisKeyType.LATEST_SNAPSHOT));
// Consume the key (delete from redis) // Consume the key (delete from redis)
jedis.del(key); jedis.del(key);
@@ -397,11 +398,11 @@ public class RedisManager extends JedisPubSub {
final byte[] readData = jedis.get(key); final byte[] readData = jedis.get(key);
if (readData == null) { if (readData == null) {
plugin.debug(String.format("[%s] Waiting for %s key from Redis", plugin.debug(String.format("[%s] Waiting for %s key from Redis",
user.getUsername(), RedisKeyType.SERVER_SWITCH)); user.getName(), RedisKeyType.SERVER_SWITCH));
return false; return false;
} }
plugin.debug(String.format("[%s] Read %s key from Redis", plugin.debug(String.format("[%s] Read %s key from Redis",
user.getUsername(), RedisKeyType.SERVER_SWITCH)); user.getName(), RedisKeyType.SERVER_SWITCH));
// Consume the key (delete from redis) // Consume the key (delete from redis)
jedis.del(key); jedis.del(key);
@@ -439,6 +440,97 @@ public class RedisManager extends JedisPubSub {
return "unknown"; return "unknown";
} }
@Blocking
public void bindMapIds(@NotNull String fromServer, int fromId, @NotNull String toServer, int toId) {
try (Jedis jedis = jedisPool.getResource()) {
jedis.setex(
getMapIdKey(fromServer, fromId, toServer, clusterId),
RedisKeyType.TTL_1_YEAR,
String.valueOf(toId).getBytes(StandardCharsets.UTF_8)
);
jedis.setex(
getReversedMapIdKey(toServer, toId, clusterId),
RedisKeyType.TTL_1_YEAR,
String.format("%s:%s", fromServer, fromId).getBytes(StandardCharsets.UTF_8)
);
plugin.debug(String.format("Bound map %s:%s -> %s:%s on Redis", fromServer, fromId, toServer, toId));
} catch (Throwable e) {
plugin.log(Level.SEVERE, "An exception occurred binding map ids on Redis", e);
}
}
@Blocking
public Optional<Integer> getBoundMapId(@NotNull String fromServer, int fromId, @NotNull String toServer) {
try (Jedis jedis = jedisPool.getResource()) {
final byte[] readData = jedis.get(getMapIdKey(fromServer, fromId, toServer, clusterId));
if (readData == null) {
plugin.debug(String.format("[%s:%s] No bound map id for server %s Redis",
fromServer, fromId, toServer));
return Optional.empty();
}
plugin.debug(String.format("[%s:%s] Read bound map id for server %s from Redis",
fromServer, fromId, toServer));
return Optional.of(Integer.parseInt(new String(readData, StandardCharsets.UTF_8)));
} catch (Throwable e) {
plugin.log(Level.SEVERE, "An exception occurred getting bound map id from Redis", e);
return Optional.empty();
}
}
@Blocking
public @Nullable Map.Entry<String, Integer> getReversedMapBound(@NotNull String toServer, int toId) {
try (Jedis jedis = jedisPool.getResource()) {
final byte[] readData = jedis.get(getReversedMapIdKey(toServer, toId, clusterId));
if (readData == null) {
plugin.debug(String.format("[%s:%s] No reversed map bound on Redis",
toServer, toId));
return null;
}
plugin.debug(String.format("[%s:%s] Read reversed map bound from Redis",
toServer, toId));
String[] parts = new String(readData, StandardCharsets.UTF_8).split(":");
return Map.entry(parts[0], Integer.parseInt(parts[1]));
} catch (Throwable e) {
plugin.log(Level.SEVERE, "An exception occurred reading reversed map bound from Redis", e);
return null;
}
}
@Blocking
public void setMapData(@NotNull String serverName, int mapId, byte[] data) {
try (Jedis jedis = jedisPool.getResource()) {
jedis.setex(
getMapDataKey(serverName, mapId, clusterId),
RedisKeyType.TTL_1_YEAR,
data
);
plugin.debug(String.format("Set map data %s:%s on Redis", serverName, mapId));
} catch (Throwable e) {
plugin.log(Level.SEVERE, "An exception occurred setting map data on Redis", e);
}
}
@Blocking
public byte @Nullable [] getMapData(@NotNull String serverName, int mapId) {
try (Jedis jedis = jedisPool.getResource()) {
final byte[] readData = jedis.get(getMapDataKey(serverName, mapId, clusterId));
if (readData == null) {
plugin.debug(String.format("[%s:%s] No map data on Redis",
serverName, mapId));
return null;
}
plugin.debug(String.format("[%s:%s] Read map data from Redis",
serverName, mapId));
return readData;
} catch (Throwable e) {
plugin.log(Level.SEVERE, "An exception occurred reading map data from Redis", e);
return null;
}
}
@Blocking @Blocking
public void terminate() { public void terminate() {
enabled = false; enabled = false;
@@ -459,4 +551,16 @@ public class RedisManager extends JedisPubSub {
return String.format("%s:%s", keyType.getKeyPrefix(clusterId), uuid); return String.format("%s:%s", keyType.getKeyPrefix(clusterId), uuid);
} }
private static byte[] getMapIdKey(@NotNull String fromServer, int fromId, @NotNull String toServer, @NotNull String clusterId) {
return String.format("%s:%s:%s:%s", RedisKeyType.MAP_ID.getKeyPrefix(clusterId), fromServer, fromId, toServer).getBytes(StandardCharsets.UTF_8);
}
private static byte[] getReversedMapIdKey(@NotNull String toServer, int toId, @NotNull String clusterId) {
return String.format("%s:%s:%s", RedisKeyType.MAP_ID_REVERSED.getKeyPrefix(clusterId), toServer, toId).getBytes(StandardCharsets.UTF_8);
}
private static byte[] getMapDataKey(@NotNull String serverName, int mapId, @NotNull String clusterId) {
return String.format("%s:%s:%s", RedisKeyType.MAP_DATA.getKeyPrefix(clusterId), serverName, mapId).getBytes(StandardCharsets.UTF_8);
}
} }

View File

@@ -163,7 +163,7 @@ public abstract class DataSyncer {
() -> user.completeSync(true, DataSnapshot.UpdateCause.NEW_USER, plugin) () -> user.completeSync(true, DataSnapshot.UpdateCause.NEW_USER, plugin)
); );
} catch (Throwable e) { } catch (Throwable e) {
plugin.log(Level.WARNING, "Failed to set %s's data from the database".formatted(user.getUsername()), e); plugin.log(Level.WARNING, "Failed to set %s's data from the database".formatted(user.getName()), e);
user.completeSync(false, DataSnapshot.UpdateCause.SYNCHRONIZED, plugin); user.completeSync(false, DataSnapshot.UpdateCause.SYNCHRONIZED, plugin);
} }
} }
@@ -188,7 +188,7 @@ 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", plugin.debug(String.format("[%s] Redis timed out after %s attempts; setting from database",
user.getUsername(), timesRun.get())); user.getName(), timesRun.get()));
setUserFromDatabase(user); setUserFromDatabase(user);
return; return;
} }

View File

@@ -127,7 +127,7 @@ public abstract class OnlineUser extends User implements CommandUser, UserDataHo
getPlugin().fireEvent(getPlugin().getPreSyncEvent(this, snapshot), (event) -> { getPlugin().fireEvent(getPlugin().getPreSyncEvent(this, snapshot), (event) -> {
if (!isOffline()) { if (!isOffline()) {
getPlugin().debug(String.format("Applying snapshot (%s) to %s (cause: %s)", getPlugin().debug(String.format("Applying snapshot (%s) to %s (cause: %s)",
snapshot.getShortId(), getUsername(), cause.getDisplayName() snapshot.getShortId(), getName(), cause.getDisplayName()
)); ));
UserDataHolder.super.applySnapshot( UserDataHolder.super.applySnapshot(
event.getData(), (succeeded) -> completeSync(succeeded, cause, getPlugin()) event.getData(), (succeeded) -> completeSync(succeeded, cause, getPlugin())

View File

@@ -49,7 +49,7 @@ public class DataSnapshotList {
.map(snapshot -> plugin.getLocales() .map(snapshot -> plugin.getLocales()
.getRawLocale(!snapshot.isInvalid() ? "data_list_item" : "data_list_item_invalid", .getRawLocale(!snapshot.isInvalid() ? "data_list_item" : "data_list_item_invalid",
getNumberIcon(snapshotNumber.getAndIncrement()), getNumberIcon(snapshotNumber.getAndIncrement()),
dataOwner.getUsername(), dataOwner.getName(),
snapshot.getId().toString(), snapshot.getId().toString(),
snapshot.getShortId(), snapshot.getShortId(),
snapshot.isPinned() ? "" : " ", snapshot.isPinned() ? "" : " ",
@@ -63,10 +63,10 @@ public class DataSnapshotList {
.orElse("" + snapshot.getId())).toList(), .orElse("" + snapshot.getId())).toList(),
plugin.getLocales().getBaseChatList(6) plugin.getLocales().getBaseChatList(6)
.setHeaderFormat(plugin.getLocales() .setHeaderFormat(plugin.getLocales()
.getRawLocale("data_list_title", dataOwner.getUsername(), .getRawLocale("data_list_title", dataOwner.getName(),
"%first_item_on_page_index%", "%last_item_on_page_index%", "%total_items%") "%first_item_on_page_index%", "%last_item_on_page_index%", "%total_items%")
.orElse("")) .orElse(""))
.setCommand("/husksync:userdata list " + dataOwner.getUsername()) .setCommand("/husksync:userdata list " + dataOwner.getName())
.build()); .build());
} }

View File

@@ -59,7 +59,7 @@ public class DataSnapshotOverview {
// Title message, timestamp, owner and cause. // Title message, timestamp, owner and cause.
final Locales locales = plugin.getLocales(); final Locales locales = plugin.getLocales();
locales.getLocale("data_manager_title", snapshot.getShortId(), snapshot.getId().toString(), locales.getLocale("data_manager_title", snapshot.getShortId(), snapshot.getId().toString(),
dataOwner.getUsername(), dataOwner.getUuid().toString()) dataOwner.getName(), dataOwner.getUuid().toString())
.ifPresent(user::sendMessage); .ifPresent(user::sendMessage);
locales.getLocale("data_manager_timestamp", locales.getLocale("data_manager_timestamp",
snapshot.getTimestamp().format(DateTimeFormatter snapshot.getTimestamp().format(DateTimeFormatter
@@ -107,13 +107,13 @@ public class DataSnapshotOverview {
if (user.hasPermission("husksync.command.inventory.edit") if (user.hasPermission("husksync.command.inventory.edit")
&& user.hasPermission("husksync.command.enderchest.edit")) { && user.hasPermission("husksync.command.enderchest.edit")) {
locales.getLocale("data_manager_item_buttons", dataOwner.getUsername(), snapshot.getId().toString()) locales.getLocale("data_manager_item_buttons", dataOwner.getName(), snapshot.getId().toString())
.ifPresent(user::sendMessage); .ifPresent(user::sendMessage);
} }
locales.getLocale("data_manager_management_buttons", dataOwner.getUsername(), snapshot.getId().toString()) locales.getLocale("data_manager_management_buttons", dataOwner.getName(), snapshot.getId().toString())
.ifPresent(user::sendMessage); .ifPresent(user::sendMessage);
if (user.hasPermission("husksync.command.userdata.dump")) { if (user.hasPermission("husksync.command.userdata.dump")) {
locales.getLocale("data_manager_system_buttons", dataOwner.getUsername(), snapshot.getId().toString()) locales.getLocale("data_manager_system_buttons", dataOwner.getName(), snapshot.getId().toString())
.ifPresent(user::sendMessage); .ifPresent(user::sendMessage);
} }
} }

View File

@@ -179,7 +179,7 @@ public class UserDataDumper {
@NotNull @NotNull
private String getFileName() { private String getFileName() {
return new StringJoiner("_") return new StringJoiner("_")
.add(user.getUsername()) .add(user.getName())
.add(snapshot.getTimestamp().format(DateTimeFormatter.ofPattern("yyyy-MM-dd_HH-mm-ss"))) .add(snapshot.getTimestamp().format(DateTimeFormatter.ofPattern("yyyy-MM-dd_HH-mm-ss")))
.add(snapshot.getSaveCause().name().toLowerCase(Locale.ENGLISH)) .add(snapshot.getSaveCause().name().toLowerCase(Locale.ENGLISH))
.add(snapshot.getShortId()) .add(snapshot.getShortId())

View File

@@ -29,4 +29,28 @@ CREATE TABLE IF NOT EXISTS `%user_data_table%`
FOREIGN KEY (`player_uuid`) REFERENCES `%users_table%` (`uuid`) ON DELETE CASCADE FOREIGN KEY (`player_uuid`) REFERENCES `%users_table%` (`uuid`) ON DELETE CASCADE
) ENGINE = InnoDB ) ENGINE = InnoDB
DEFAULT CHARSET = utf8mb4 DEFAULT CHARSET = utf8mb4
COLLATE = utf8mb4_unicode_ci; COLLATE = utf8mb4_unicode_ci;
-- Create the map data table if it does not exist
CREATE TABLE IF NOT EXISTS `%map_data_table%`
(
`server_name` varchar(32) NOT NULL,
`map_id` int NOT NULL,
`data` longblob NOT NULL,
PRIMARY KEY (`server_name`, `map_id`)
) ENGINE = InnoDB
DEFAULT CHARSET = utf8mb4
COLLATE = utf8mb4_unicode_ci;
-- Create the map ids table if it does not exist
CREATE TABLE IF NOT EXISTS `%map_ids_table%`
(
`from_server_name` varchar(32) NOT NULL,
`from_id` int NOT NULL,
`to_server_name` varchar(32) NOT NULL,
`to_id` int NOT NULL,
PRIMARY KEY (`from_server_name`, `from_id`, `to_server_name`),
FOREIGN KEY (`from_server_name`, `from_id`) REFERENCES `%map_data_table%` (`server_name`, `map_id`) ON DELETE CASCADE
) ENGINE = InnoDB
DEFAULT CHARSET = utf8mb4
COLLATE = utf8mb4_unicode_ci;

View File

@@ -26,4 +26,26 @@ CREATE TABLE IF NOT EXISTS `%user_data_table%`
PRIMARY KEY (`version_uuid`, `player_uuid`), PRIMARY KEY (`version_uuid`, `player_uuid`),
FOREIGN KEY (`player_uuid`) REFERENCES `%users_table%` (`uuid`) ON DELETE CASCADE FOREIGN KEY (`player_uuid`) REFERENCES `%users_table%` (`uuid`) ON DELETE CASCADE
) CHARACTER SET utf8 ) CHARACTER SET utf8
COLLATE utf8_unicode_ci; COLLATE utf8_unicode_ci;
# Create the map data table if it does not exist
CREATE TABLE IF NOT EXISTS `%map_data_table%`
(
`server_name` varchar(32) NOT NULL,
`map_id` int NOT NULL,
`data` longblob NOT NULL,
PRIMARY KEY (`server_name`, `map_id`)
) CHARACTER SET utf8
COLLATE utf8_unicode_ci;
# Create the map ids table if it does not exist
CREATE TABLE IF NOT EXISTS `%map_ids_table%`
(
`from_server_name` varchar(32) NOT NULL,
`from_id` int NOT NULL,
`to_server_name` varchar(32) NOT NULL,
`to_id` int NOT NULL,
PRIMARY KEY (`from_server_name`, `from_id`, `to_server_name`),
FOREIGN KEY (`from_server_name`, `from_id`) REFERENCES `%map_data_table%` (`server_name`, `map_id`) ON DELETE CASCADE
) CHARACTER SET utf8
COLLATE utf8_unicode_ci;

View File

@@ -19,4 +19,24 @@ CREATE TABLE IF NOT EXISTS "%user_data_table%"
PRIMARY KEY (version_uuid, player_uuid), PRIMARY KEY (version_uuid, player_uuid),
FOREIGN KEY (player_uuid) REFERENCES "%users_table%" (uuid) ON DELETE CASCADE FOREIGN KEY (player_uuid) REFERENCES "%users_table%" (uuid) ON DELETE CASCADE
); );
-- Create the map data table if it does not exist
CREATE TABLE IF NOT EXISTS "%map_data_table%"
(
server_name varchar(32) NOT NULL,
map_id int NOT NULL,
data bytea NOT NULL,
PRIMARY KEY (server_name, map_id)
);
-- Create the map ids table if it does not exist
CREATE TABLE IF NOT EXISTS "%map_ids_table%"
(
from_server_name varchar(32) NOT NULL,
from_id int NOT NULL,
to_server_name varchar(32) NOT NULL,
to_id int NOT NULL,
PRIMARY KEY (from_server_name, from_id, to_server_name),
FOREIGN KEY (from_server_name, from_id) REFERENCES "%map_data_table%" (server_name, map_id) ON DELETE CASCADE
);

View File

@@ -37,7 +37,7 @@ huskSyncAPI.getUser(uuid).thenAccept(optionalUser -> {
} }
// The User object provides methods for getting a user's UUID and username // The User object provides methods for getting a user's UUID and username
System.out.println("Found %s", optionalUser.get().getUsername()); System.out.println("Found %s", optionalUser.get().getName());
}); });
``` ```
</details> </details>
@@ -51,7 +51,7 @@ huskSyncAPI.getUser(uuid).thenAccept(optionalUser -> {
```java ```java
// Get an online user // Get an online user
OnlineUser user = huskSyncAPI.getUser(player); OnlineUser user = huskSyncAPI.getUser(player);
System.out.println("Hello, %s!", user.getUsername()); System.out.println("Hello, %s!", user.getName());
``` ```
</details> </details>
@@ -67,7 +67,7 @@ System.out.println("Hello, %s!", user.getUsername());
// Get a user's current data // Get a user's current data
huskSyncAPI.getCurrentData(user).thenAccept(optionalSnapshot -> { huskSyncAPI.getCurrentData(user).thenAccept(optionalSnapshot -> {
if (optionalSnapshot.isEmpty()) { if (optionalSnapshot.isEmpty()) {
System.out.println("Couldn't get data for %s", user.getUsername()); System.out.println("Couldn't get data for %s", user.getName());
return; return;
} }
@@ -88,7 +88,7 @@ huskSyncAPI.getCurrentData(user).thenAccept(optionalSnapshot -> {
// Get a user's latest saved snapshot // Get a user's latest saved snapshot
huskSyncAPI.getLatestSnapshot(user).thenAccept(optionalSnapshot -> { huskSyncAPI.getLatestSnapshot(user).thenAccept(optionalSnapshot -> {
if (optionalSnapshot.isEmpty()) { if (optionalSnapshot.isEmpty()) {
System.out.println("%s has no saved snapshots!", user.getUsername()); System.out.println("%s has no saved snapshots!", user.getName());
return; return;
} }
@@ -108,7 +108,7 @@ huskSyncAPI.getLatestSnapshot(user).thenAccept(optionalSnapshot -> {
// Get a user's saved snapshots // Get a user's saved snapshots
huskSyncAPI.getSnapshots(user).thenAccept(optionalSnapshots -> { huskSyncAPI.getSnapshots(user).thenAccept(optionalSnapshots -> {
if (optionalSnapshots.isEmpty()) { if (optionalSnapshots.isEmpty()) {
System.out.println("%s has no saved snapshots!", user.getUsername()); System.out.println("%s has no saved snapshots!", user.getName());
return; return;
} }

View File

@@ -9,7 +9,7 @@ check_for_updates: false
cluster_id: '' cluster_id: ''
# Enable development debug logging # Enable development debug logging
debug_logging: false debug_logging: true
# Whether to enable the Player Analytics hook. # Whether to enable the Player Analytics hook.
# Docs: https://william278.net/docs/husksync/plan-hook # Docs: https://william278.net/docs/husksync/plan-hook