diff --git a/bukkit/src/main/java/net/william278/husksync/BukkitHuskSync.java b/bukkit/src/main/java/net/william278/husksync/BukkitHuskSync.java
index c4303063..f8686c64 100644
--- a/bukkit/src/main/java/net/william278/husksync/BukkitHuskSync.java
+++ b/bukkit/src/main/java/net/william278/husksync/BukkitHuskSync.java
@@ -56,7 +56,7 @@ import net.william278.husksync.sync.DataSyncer;
import net.william278.husksync.user.BukkitUser;
import net.william278.husksync.user.OnlineUser;
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.LegacyConverter;
import net.william278.toilet.BukkitToilet;
@@ -84,7 +84,7 @@ import java.util.stream.Collectors;
@NoArgsConstructor
@SuppressWarnings("unchecked")
public class BukkitHuskSync extends JavaPlugin implements HuskSync, BukkitTask.Supplier,
- BukkitEventDispatcher, BukkitMapPersister {
+ BukkitEventDispatcher, BukkitMapHandler {
/**
* Metrics ID for HuskSync on Bukkit.
diff --git a/bukkit/src/main/java/net/william278/husksync/data/BukkitUserDataHolder.java b/bukkit/src/main/java/net/william278/husksync/data/BukkitUserDataHolder.java
index 9eb93aaf..f6dcacb7 100644
--- a/bukkit/src/main/java/net/william278/husksync/data/BukkitUserDataHolder.java
+++ b/bukkit/src/main/java/net/william278/husksync/data/BukkitUserDataHolder.java
@@ -20,7 +20,7 @@
package net.william278.husksync.data;
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.inventory.PlayerInventory;
import org.jetbrains.annotations.NotNull;
@@ -163,7 +163,7 @@ public interface BukkitUserDataHolder extends UserDataHolder {
}
@NotNull
- default BukkitMapPersister getMapPersister() {
+ default BukkitMapHandler getMapPersister() {
return (BukkitHuskSync) getPlugin();
}
diff --git a/bukkit/src/main/java/net/william278/husksync/listener/BukkitEventListener.java b/bukkit/src/main/java/net/william278/husksync/listener/BukkitEventListener.java
index f00ca273..743b3736 100644
--- a/bukkit/src/main/java/net/william278/husksync/listener/BukkitEventListener.java
+++ b/bukkit/src/main/java/net/william278/husksync/listener/BukkitEventListener.java
@@ -135,7 +135,7 @@ public class BukkitEventListener extends EventListener implements BukkitJoinEven
@EventHandler(ignoreCancelled = true)
public void onMapInitialize(@NotNull MapInitializeEvent event) {
if (plugin.getSettings().getSynchronization().isPersistLockedMaps() && event.getMap().isLocked()) {
- getPlugin().runAsync(() -> ((BukkitHuskSync) plugin).renderMapFromFile(event.getMap()));
+ getPlugin().runAsync(() -> ((BukkitHuskSync) plugin).renderPersistedMap(event.getMap()));
}
}
diff --git a/bukkit/src/main/java/net/william278/husksync/util/BukkitMapPersister.java b/bukkit/src/main/java/net/william278/husksync/maps/BukkitMapHandler.java
similarity index 67%
rename from bukkit/src/main/java/net/william278/husksync/util/BukkitMapPersister.java
rename to bukkit/src/main/java/net/william278/husksync/maps/BukkitMapHandler.java
index d24756b1..353b4d19 100644
--- a/bukkit/src/main/java/net/william278/husksync/util/BukkitMapPersister.java
+++ b/bukkit/src/main/java/net/william278/husksync/maps/BukkitMapHandler.java
@@ -17,15 +17,14 @@
* limitations under the License.
*/
-package net.william278.husksync.util;
+package net.william278.husksync.maps;
import com.google.common.collect.Lists;
import de.tr7zw.changeme.nbtapi.NBT;
import de.tr7zw.changeme.nbtapi.iface.ReadWriteNBT;
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.redis.RedisManager;
import net.william278.mapdataapi.MapBanner;
import net.william278.mapdataapi.MapData;
import org.bukkit.Bukkit;
@@ -35,27 +34,29 @@ import org.bukkit.block.Container;
import org.bukkit.entity.Player;
import org.bukkit.inventory.ItemStack;
import org.bukkit.inventory.meta.BlockStateMeta;
+import org.bukkit.inventory.meta.BundleMeta;
import org.bukkit.inventory.meta.MapMeta;
import org.bukkit.map.*;
import org.jetbrains.annotations.ApiStatus;
+import org.jetbrains.annotations.Blocking;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.awt.*;
-import java.io.File;
+import java.io.IOException;
import java.util.List;
import java.util.*;
import java.util.function.Function;
import java.util.logging.Level;
-public interface BukkitMapPersister {
+public interface BukkitMapHandler {
// The map used to store HuskSync data in ItemStack NBT
String MAP_DATA_KEY = "husksync:persisted_locked_map";
- // The key used to store the serialized map data in NBT
- String MAP_PIXEL_DATA_KEY = "canvas_data";
- // The key used to store the map of World UIDs to MapView IDs in NBT
- String MAP_VIEW_ID_MAPPINGS_KEY = "id_mappings";
+ // Name of server the map originates from
+ String MAP_ORIGIN_KEY = "origin";
+ // Original map id
+ String MAP_ID_KEY = "id";
/**
* 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) {
forEachMap(box.getInventory().getContents(), function);
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;
}
+ @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 readMapData(@NotNull String serverName, int mapId) {
+ final Map.Entry readData = fetchMapData(serverName, mapId);
+ if (readData == null) {
+ return null;
+ }
+ return deserializeMapData(readData);
+ }
+
+ @Nullable
+ @Blocking
+ private Map.Entry fetchMapData(@NotNull String serverName, int mapId) {
+ return fetchMapData(serverName, mapId, true);
+ }
+
+ @Nullable
+ @Blocking
+ private Map.Entry 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 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 fetchReversedMapData(@NotNull String serverName, int mapId) {
+ // Lookup binding from Redis cache, then fetch data if found
+ Map.Entry 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 deserializeMapData(@NotNull Map.Entry 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 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
private ItemStack persistMapView(@NotNull ItemStack map, @NotNull Player delegateRenderer) {
final MapMeta meta = Objects.requireNonNull((MapMeta) map.getItemMeta());
@@ -131,95 +229,82 @@ public interface BukkitMapPersister {
// Persist map data
final ReadWriteNBT mapData = nbt.getOrCreateCompound(MAP_DATA_KEY);
- final String worldUid = view.getWorld().getUID().toString();
- mapData.setByteArray(MAP_PIXEL_DATA_KEY, canvas.extractMapData().toBytes());
- nbt.getOrCreateCompound(MAP_VIEW_ID_MAPPINGS_KEY).setInteger(worldUid, view.getId());
- getPlugin().debug(String.format("Saved data for locked map (#%s, UID: %s)", view.getId(), worldUid));
+ final String serverName = getPlugin().getServerName();
+ mapData.setString(MAP_ORIGIN_KEY, serverName);
+ mapData.setInteger(MAP_ID_KEY, meta.getMapId());
+ 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;
}
+ @SuppressWarnings("deprecation")
@NotNull
private ItemStack applyMapView(@NotNull ItemStack map) {
- final int dataVersion = getPlugin().getDataVersion(getPlugin().getMinecraftVersion());
final MapMeta meta = Objects.requireNonNull((MapMeta) map.getItemMeta());
NBT.get(map, nbt -> {
if (!nbt.hasTag(MAP_DATA_KEY)) {
return;
}
final ReadableNBT mapData = nbt.getCompound(MAP_DATA_KEY);
- final ReadableNBT mapIds = nbt.getCompound(MAP_VIEW_ID_MAPPINGS_KEY);
- if (mapData == null || mapIds == null) {
+ if (mapData == null) {
return;
}
- // Search for an existing map view
- Optional world = Optional.empty();
- for (String worldUid : mapIds.getKeys()) {
- world = getPlugin().getServer().getWorlds().stream()
- .map(w -> w.getUID().toString()).filter(u -> u.equals(worldUid))
- .findFirst();
- if (world.isPresent()) {
- break;
- }
- }
- if (world.isPresent()) {
- final String uid = world.get();
- final Optional 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;
- }
+ // Determine map ID
+ final String originServerName = mapData.getString(MAP_ORIGIN_KEY);
+ final String currentServerName = getPlugin().getServerName();
+ final int originalMapId = mapData.getInteger(MAP_ID_KEY);
+ int newId = currentServerName.equals(originServerName)
+ ? originalMapId : getBoundMapId(originServerName, originalMapId, currentServerName);
+ if (newId != -1) {
+ meta.setMapId(newId);
+ map.setItemMeta(meta);
+ getPlugin().debug(String.format("Map ID set to %s", newId));
+ return;
}
// Read the pixel data and generate a map view otherwise
- final MapData canvasData;
- try {
- 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;
- }
+ getPlugin().debug("Deserializing map data from NBT and generating view...");
+ final MapData canvasData = Objects.requireNonNull(readMapData(originServerName, originalMapId), "Pixel data null!").getKey();
// Add a renderer to the map with the data and save to file
final MapView view = generateRenderedMap(canvasData);
- final String worldUid = getDefaultMapWorld().getUID().toString();
meta.setMapView(view);
map.setItemMeta(meta);
- saveMapToFile(canvasData, view.getId());
- // Set the map view ID in NBT
- NBT.modify(map, editable -> {
- Objects.requireNonNull(editable.getCompound(MAP_VIEW_ID_MAPPINGS_KEY),
- "Map view ID mappings compound is null")
- .setInteger(worldUid, view.getId());
- });
- getPlugin().debug(String.format("Generated view (#%s) and updated map (UID: %s)", view.getId(), worldUid));
+ // Bind in the database & Redis
+ final int id = view.getId();
+ getRedisManager().bindMapIds(originServerName, originalMapId, currentServerName, id);
+ getPlugin().getDatabase().setMapBinding(originServerName, originalMapId, currentServerName, id);
+
+ getPlugin().debug(String.format("Bound map to view (#%s) on server %s", id, currentServerName));
});
return map;
}
- default void renderMapFromFile(@NotNull MapView view) {
- final File mapFile = new File(getMapCacheFolder(), view.getId() + ".dat");
- if (!mapFile.exists()) {
+ default void renderPersistedMap(@NotNull MapView view) {
+ if (getMapView(view.getId()).isPresent()) {
return;
}
- final MapData canvasData;
- try {
- canvasData = MapData.fromNbt(mapFile);
- } catch (Throwable e) {
- getPlugin().log(Level.WARNING, "Failed to deserialize map data from file", e);
+ @Nullable final Map.Entry data = readMapData(getPlugin().getServerName(), view.getId());
+ if (data == null) {
+ final World world = view.getWorld() == null ? getDefaultMapWorld() : view.getWorld();
+ getPlugin().debug("Not rendering map: no data in DB for world %s, map #%s."
+ .formatted(world.getName(), view.getId()));
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
// use view.removeRenderer() to remove all this maps renderers
view.getRenderers().forEach(view::removeRenderer);
@@ -233,32 +318,6 @@ public interface BukkitMapPersister {
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
@NotNull
private MapView generateRenderedMap(@NotNull MapData canvasData) {
@@ -362,6 +421,8 @@ public interface BukkitMapPersister {
@SuppressWarnings({"deprecation", "removal"})
class PersistentMapCanvas implements MapCanvas {
+ private static final String BANNER_PREFIX = "banner_";
+
private final int mapDataVersion;
private final MapView mapView;
private final int[][] pixels = new int[128][128];
@@ -451,7 +512,6 @@ public interface BukkitMapPersister {
@NotNull
private MapData extractMapData() {
final List banners = Lists.newArrayList();
- final String BANNER_PREFIX = "banner_";
for (int i = 0; i < getCursors().size(); i++) {
final MapCursor cursor = getCursors().getCursor(i);
//#if MC==12001
@@ -477,6 +537,9 @@ public interface BukkitMapPersister {
@NotNull
Map getMapViews();
+ @ApiStatus.Internal
+ RedisManager getRedisManager();
+
@ApiStatus.Internal
@NotNull
BukkitHuskSync getPlugin();
diff --git a/bukkit/src/main/java/net/william278/husksync/migrator/LegacyMigrator.java b/bukkit/src/main/java/net/william278/husksync/migrator/LegacyMigrator.java
index 3b9eff40..707879b2 100644
--- a/bukkit/src/main/java/net/william278/husksync/migrator/LegacyMigrator.java
+++ b/bukkit/src/main/java/net/william278/husksync/migrator/LegacyMigrator.java
@@ -146,7 +146,7 @@ public class LegacyMigrator extends Migrator {
try {
plugin.getDatabase().addSnapshot(data.user(), convertedData);
} 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;
}
diff --git a/common/build.gradle b/common/build.gradle
index c5b944cc..011cb8b8 100644
--- a/common/build.gradle
+++ b/common/build.gradle
@@ -6,6 +6,7 @@ dependencies {
api 'commons-io:commons-io:2.18.0'
api 'org.apache.commons:commons-text:1.13.0'
api 'net.william278:minedown:1.8.2'
+ api 'net.william278:mapdataapi:2.0'
api 'org.json:json:20250107'
api 'com.google.code.gson:gson:2.12.1'
api 'com.fatboyindustrial.gson-javatime-serialisers:gson-javatime-serialisers:1.1.2'
diff --git a/common/src/main/java/net/william278/husksync/command/EnderChestCommand.java b/common/src/main/java/net/william278/husksync/command/EnderChestCommand.java
index 81395e9b..b9cd1139 100644
--- a/common/src/main/java/net/william278/husksync/command/EnderChestCommand.java
+++ b/common/src/main/java/net/william278/husksync/command/EnderChestCommand.java
@@ -51,7 +51,7 @@ public class EnderChestCommand extends ItemsCommand {
}
// 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
.ofLocalizedDateTime(FormatStyle.MEDIUM, FormatStyle.SHORT)))
.ifPresent(viewer::sendMessage);
@@ -60,8 +60,8 @@ public class EnderChestCommand extends ItemsCommand {
final Data.Items.EnderChest enderChest = optionalEnderChest.get();
viewer.showGui(
enderChest,
- plugin.getLocales().getLocale("ender_chest_viewer_menu_title", user.getUsername())
- .orElse(new MineDown(String.format("%s's Ender Chest", user.getUsername()))),
+ plugin.getLocales().getLocale("ender_chest_viewer_menu_title", user.getName())
+ .orElse(new MineDown(String.format("%s's Ender Chest", user.getName()))),
allowEdit,
enderChest.getSlotCount(),
(itemsOnClose) -> {
diff --git a/common/src/main/java/net/william278/husksync/command/HuskSyncCommand.java b/common/src/main/java/net/william278/husksync/command/HuskSyncCommand.java
index ae8c41e4..355cbd41 100644
--- a/common/src/main/java/net/william278/husksync/command/HuskSyncCommand.java
+++ b/common/src/main/java/net/william278/husksync/command/HuskSyncCommand.java
@@ -69,7 +69,8 @@ public class HuskSyncCommand extends PluginCommand {
AboutMenu.Credit.of("HookWoods").description("Code"),
AboutMenu.Credit.of("Preva1l").description("Code"),
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",
AboutMenu.Credit.of("Namiu").description("Japanese (ja-jp)"),
AboutMenu.Credit.of("anchelthe").description("Spanish (es-es)"),
diff --git a/common/src/main/java/net/william278/husksync/command/InventoryCommand.java b/common/src/main/java/net/william278/husksync/command/InventoryCommand.java
index 0eadbb0a..95f85b02 100644
--- a/common/src/main/java/net/william278/husksync/command/InventoryCommand.java
+++ b/common/src/main/java/net/william278/husksync/command/InventoryCommand.java
@@ -52,7 +52,7 @@ public class InventoryCommand extends ItemsCommand {
}
// Display opening message
- plugin.getLocales().getLocale("inventory_viewer_opened", user.getUsername(),
+ plugin.getLocales().getLocale("inventory_viewer_opened", user.getName(),
snapshot.getTimestamp().format(DateTimeFormatter
.ofLocalizedDateTime(FormatStyle.MEDIUM, FormatStyle.SHORT)))
.ifPresent(viewer::sendMessage);
@@ -61,8 +61,8 @@ public class InventoryCommand extends ItemsCommand {
final Data.Items.Inventory inventory = optionalInventory.get();
viewer.showGui(
inventory,
- plugin.getLocales().getLocale("inventory_viewer_menu_title", user.getUsername())
- .orElse(new MineDown(String.format("%s's Inventory", user.getUsername()))),
+ plugin.getLocales().getLocale("inventory_viewer_menu_title", user.getName())
+ .orElse(new MineDown(String.format("%s's Inventory", user.getName()))),
allowEdit,
inventory.getSlotCount(),
(itemsOnClose) -> {
diff --git a/common/src/main/java/net/william278/husksync/command/PluginCommand.java b/common/src/main/java/net/william278/husksync/command/PluginCommand.java
index b1eda665..bc6c8c87 100644
--- a/common/src/main/java/net/william278/husksync/command/PluginCommand.java
+++ b/common/src/main/java/net/william278/husksync/command/PluginCommand.java
@@ -83,7 +83,7 @@ public abstract class PluginCommand extends Command {
() -> CommandSyntaxException.BUILT_IN_EXCEPTIONS.dispatcherUnknownArgument().createWithContext(reader)
);
}, (context, builder) -> {
- plugin.getOnlineUsers().forEach(u -> builder.suggest(u.getUsername()));
+ plugin.getOnlineUsers().forEach(u -> builder.suggest(u.getName()));
return builder.buildFuture();
});
}
diff --git a/common/src/main/java/net/william278/husksync/command/UserDataCommand.java b/common/src/main/java/net/william278/husksync/command/UserDataCommand.java
index dd1a7eef..b38fab36 100644
--- a/common/src/main/java/net/william278/husksync/command/UserDataCommand.java
+++ b/common/src/main/java/net/william278/husksync/command/UserDataCommand.java
@@ -113,7 +113,7 @@ public class UserDataCommand extends PluginCommand {
plugin.getLocales().getLocale("data_deleted",
version.toString().split("-")[0],
version.toString(),
- user.getUsername(),
+ user.getName(),
user.getUuid().toString())
.ifPresent(executor::sendMessage);
}
@@ -147,7 +147,7 @@ public class UserDataCommand extends PluginCommand {
plugin.getDataSyncer().saveData(user, data, (u, s) -> {
redis.getUserData(u).ifPresent(d -> redis.setUserData(u, s, RedisKeyType.TTL_1_YEAR));
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);
});
}
@@ -169,7 +169,7 @@ public class UserDataCommand extends PluginCommand {
plugin.getDatabase().pinSnapshot(user, data.getId());
}
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);
}
@@ -187,7 +187,7 @@ public class UserDataCommand extends PluginCommand {
final DataSnapshot.Packed userData = data.get();
final UserDataDumper dumper = UserDataDumper.create(userData, user, plugin);
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()))
.ifPresent(executor::sendMessage);
} catch (Throwable e) {
diff --git a/common/src/main/java/net/william278/husksync/database/Database.java b/common/src/main/java/net/william278/husksync/database/Database.java
index c03bc2ad..628d5b6c 100644
--- a/common/src/main/java/net/william278/husksync/database/Database.java
+++ b/common/src/main/java/net/william278/husksync/database/Database.java
@@ -26,6 +26,7 @@ import net.william278.husksync.data.DataSnapshot;
import net.william278.husksync.user.User;
import org.jetbrains.annotations.Blocking;
import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
@@ -56,8 +57,8 @@ public abstract class Database {
@SuppressWarnings("SameParameterValue")
@NotNull
protected final String[] getSchemaStatements(@NotNull String schemaFileName) throws IOException {
- return formatStatementTables(new String(Objects.requireNonNull(plugin.getResource(schemaFileName))
- .readAllBytes(), StandardCharsets.UTF_8)).split(";");
+ return Arrays.stream(formatStatementTables(new String(Objects.requireNonNull(plugin.getResource(schemaFileName))
+ .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) {
final Settings.DatabaseSettings settings = plugin.getSettings().getDatabase();
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 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 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 all {@link User} entries from the database.
* This should only be used when preparing tables for a data migration.
@@ -283,7 +338,9 @@ public abstract class Database {
@Getter
public enum TableName {
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;
diff --git a/common/src/main/java/net/william278/husksync/database/MongoDbDatabase.java b/common/src/main/java/net/william278/husksync/database/MongoDbDatabase.java
index 0f4c248b..68daaf63 100644
--- a/common/src/main/java/net/william278/husksync/database/MongoDbDatabase.java
+++ b/common/src/main/java/net/william278/husksync/database/MongoDbDatabase.java
@@ -35,13 +35,11 @@ import org.bson.conversions.Bson;
import org.bson.types.Binary;
import org.jetbrains.annotations.Blocking;
import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
import java.time.Instant;
import java.time.OffsetDateTime;
-import java.util.List;
-import java.util.Optional;
-import java.util.TimeZone;
-import java.util.UUID;
+import java.util.*;
import java.util.logging.Level;
public class MongoDbDatabase extends Database {
@@ -50,11 +48,15 @@ public class MongoDbDatabase extends Database {
private final String usersTable;
private final String userDataTable;
+ private final String mapDataTable;
+ private final String mapIdsTable;
public MongoDbDatabase(@NotNull HuskSync plugin) {
super(plugin);
this.usersTable = plugin.getSettings().getDatabase().getTableName(TableName.USERS);
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
@@ -74,9 +76,15 @@ public class MongoDbDatabase extends Database {
if (mongoCollectionHelper.getCollection(userDataTable) == null) {
mongoCollectionHelper.createCollection(userDataTable);
}
+ if (mongoCollectionHelper.getCollection(mapDataTable) == null) {
+ mongoCollectionHelper.createCollection(mapDataTable);
+ }
+ if (mongoCollectionHelper.getCollection(mapIdsTable) == null) {
+ mongoCollectionHelper.createCollection(mapIdsTable);
+ }
} catch (Exception e) {
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 {
getUser(user.getUuid()).ifPresentOrElse(
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
try {
Document filter = new Document("uuid", existingUser.getUuid());
@@ -108,7 +116,7 @@ public class MongoDbDatabase extends Database {
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);
} catch (MongoException 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
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);
} catch (MongoException 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 getMapData(@NotNull String serverName, int mapId) {
+ try {
+ Document filter = new Document("server_name", serverName).append("map_id", mapId);
+ FindIterable 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 getMapBinding(@NotNull String serverName, int mapId) {
+ final Document filter = new Document("to_server_name", serverName).append("to_id", mapId);
+ final FindIterable 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 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
@Override
public void wipeDatabase() {
diff --git a/common/src/main/java/net/william278/husksync/database/MySqlDatabase.java b/common/src/main/java/net/william278/husksync/database/MySqlDatabase.java
index 3637b0b0..8779c102 100644
--- a/common/src/main/java/net/william278/husksync/database/MySqlDatabase.java
+++ b/common/src/main/java/net/william278/husksync/database/MySqlDatabase.java
@@ -27,6 +27,7 @@ import net.william278.husksync.data.DataSnapshot;
import net.william278.husksync.user.User;
import org.jetbrains.annotations.Blocking;
import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
import java.io.ByteArrayInputStream;
import java.io.IOException;
@@ -127,11 +128,11 @@ public class MySqlDatabase extends Database {
}
} catch (SQLException e) {
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) {
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) {
getUser(user.getUuid()).ifPresentOrElse(
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
try (Connection connection = getConnection()) {
try (PreparedStatement statement = connection.prepareStatement(formatStatementTables("""
@@ -148,11 +149,12 @@ public class MySqlDatabase extends Database {
SET `username`=?
WHERE `uuid`=?"""))) {
- statement.setString(1, user.getUsername());
+ statement.setString(1, user.getName());
statement.setString(2, existingUser.getUuid().toString());
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) {
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 (?,?);"""))) {
statement.setString(1, user.getUuid().toString());
- statement.setString(2, user.getUsername());
+ statement.setString(2, user.getName());
statement.executeUpdate();
}
} 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 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 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
public void wipeDatabase() {
try (Connection connection = getConnection()) {
diff --git a/common/src/main/java/net/william278/husksync/database/PostgresDatabase.java b/common/src/main/java/net/william278/husksync/database/PostgresDatabase.java
index d00a202f..a776ab17 100644
--- a/common/src/main/java/net/william278/husksync/database/PostgresDatabase.java
+++ b/common/src/main/java/net/william278/husksync/database/PostgresDatabase.java
@@ -27,6 +27,7 @@ import net.william278.husksync.data.DataSnapshot;
import net.william278.husksync.user.User;
import org.jetbrains.annotations.Blocking;
import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
import java.io.IOException;
import java.sql.*;
@@ -120,11 +121,11 @@ public class PostgresDatabase extends Database {
}
} catch (SQLException e) {
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) {
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) {
getUser(user.getUuid()).ifPresentOrElse(
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
try (Connection connection = getConnection()) {
try (PreparedStatement statement = connection.prepareStatement(formatStatementTables("""
@@ -141,11 +142,11 @@ public class PostgresDatabase extends Database {
SET username=?
WHERE uuid=?;"""))) {
- statement.setString(1, user.getUsername());
+ statement.setString(1, user.getName());
statement.setObject(2, existingUser.getUuid());
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) {
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 (?,?);"""))) {
statement.setObject(1, user.getUuid());
- statement.setString(2, user.getUsername());
+ statement.setString(2, user.getName());
statement.executeUpdate();
}
} 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 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 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
public void wipeDatabase() {
try (Connection connection = getConnection()) {
diff --git a/common/src/main/java/net/william278/husksync/maps/AdaptableMapData.java b/common/src/main/java/net/william278/husksync/maps/AdaptableMapData.java
new file mode 100644
index 00000000..813d5e81
--- /dev/null
+++ b/common/src/main/java/net/william278/husksync/maps/AdaptableMapData.java
@@ -0,0 +1,45 @@
+/*
+ * This file is part of HuskSync, licensed under the Apache License 2.0.
+ *
+ * Copyright (c) William278
+ * 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);
+ }
+
+}
diff --git a/common/src/main/java/net/william278/husksync/redis/RedisKeyType.java b/common/src/main/java/net/william278/husksync/redis/RedisKeyType.java
index fa1a1c36..26a973f7 100644
--- a/common/src/main/java/net/william278/husksync/redis/RedisKeyType.java
+++ b/common/src/main/java/net/william278/husksync/redis/RedisKeyType.java
@@ -27,7 +27,10 @@ public enum RedisKeyType {
LATEST_SNAPSHOT,
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_10_SECONDS = 10; // 10 seconds
diff --git a/common/src/main/java/net/william278/husksync/redis/RedisManager.java b/common/src/main/java/net/william278/husksync/redis/RedisManager.java
index d778db1d..73386774 100644
--- a/common/src/main/java/net/william278/husksync/redis/RedisManager.java
+++ b/common/src/main/java/net/william278/husksync/redis/RedisManager.java
@@ -25,6 +25,7 @@ import net.william278.husksync.data.DataSnapshot;
import net.william278.husksync.user.User;
import org.jetbrains.annotations.Blocking;
import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
import redis.clients.jedis.*;
import redis.clients.jedis.exceptions.JedisException;
import redis.clients.jedis.util.Pool;
@@ -261,7 +262,7 @@ public class RedisManager extends JedisPubSub {
timeToLive,
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) {
plugin.log(Level.SEVERE, "An exception occurred setting user data on Redis", e);
}
@@ -273,7 +274,7 @@ public class RedisManager extends JedisPubSub {
jedis.del(
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) {
plugin.log(Level.SEVERE, "An exception occurred clearing user data on Redis", e);
}
@@ -291,11 +292,11 @@ public class RedisManager extends JedisPubSub {
} else {
if (jedis.del(key.getBytes(StandardCharsets.UTF_8)) == 0) {
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;
}
}
- 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));
} catch (Throwable e) {
plugin.log(Level.SEVERE, "An exception occurred setting checkout to", e);
@@ -310,13 +311,13 @@ public class RedisManager extends JedisPubSub {
if (readData != null) {
final String checkoutServer = new String(readData, StandardCharsets.UTF_8);
plugin.debug(String.format("[%s] Waiting for %s %s key to be unset on Redis",
- user.getUsername(), checkoutServer, RedisKeyType.DATA_CHECKOUT));
+ user.getName(), checkoutServer, RedisKeyType.DATA_CHECKOUT));
return Optional.of(checkoutServer);
}
} catch (Throwable 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));
return Optional.empty();
}
@@ -354,7 +355,7 @@ public class RedisManager extends JedisPubSub {
new byte[0]
);
plugin.debug(String.format("[%s] Set %s key to Redis",
- user.getUsername(), RedisKeyType.SERVER_SWITCH));
+ user.getName(), RedisKeyType.SERVER_SWITCH));
} catch (Throwable 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);
if (dataByteArray == null) {
plugin.debug(String.format("[%s] Waiting for %s key from Redis",
- user.getUsername(), RedisKeyType.LATEST_SNAPSHOT));
+ user.getName(), RedisKeyType.LATEST_SNAPSHOT));
return Optional.empty();
}
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)
jedis.del(key);
@@ -397,11 +398,11 @@ public class RedisManager extends JedisPubSub {
final byte[] readData = jedis.get(key);
if (readData == null) {
plugin.debug(String.format("[%s] Waiting for %s key from Redis",
- user.getUsername(), RedisKeyType.SERVER_SWITCH));
+ user.getName(), RedisKeyType.SERVER_SWITCH));
return false;
}
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)
jedis.del(key);
@@ -439,6 +440,97 @@ public class RedisManager extends JedisPubSub {
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 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 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
public void terminate() {
enabled = false;
@@ -459,4 +551,16 @@ public class RedisManager extends JedisPubSub {
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);
+ }
+
}
diff --git a/common/src/main/java/net/william278/husksync/sync/DataSyncer.java b/common/src/main/java/net/william278/husksync/sync/DataSyncer.java
index 749f5c41..0d7d0e57 100644
--- a/common/src/main/java/net/william278/husksync/sync/DataSyncer.java
+++ b/common/src/main/java/net/william278/husksync/sync/DataSyncer.java
@@ -163,7 +163,7 @@ public abstract class DataSyncer {
() -> user.completeSync(true, DataSnapshot.UpdateCause.NEW_USER, plugin)
);
} 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);
}
}
@@ -188,7 +188,7 @@ public abstract class DataSyncer {
if (plugin.isDisabling() || timesRun.getAndIncrement() > maxListenAttempts) {
task.get().cancel();
plugin.debug(String.format("[%s] Redis timed out after %s attempts; setting from database",
- user.getUsername(), timesRun.get()));
+ user.getName(), timesRun.get()));
setUserFromDatabase(user);
return;
}
diff --git a/common/src/main/java/net/william278/husksync/user/OnlineUser.java b/common/src/main/java/net/william278/husksync/user/OnlineUser.java
index c64a4881..f0d86645 100644
--- a/common/src/main/java/net/william278/husksync/user/OnlineUser.java
+++ b/common/src/main/java/net/william278/husksync/user/OnlineUser.java
@@ -127,7 +127,7 @@ public abstract class OnlineUser extends User implements CommandUser, UserDataHo
getPlugin().fireEvent(getPlugin().getPreSyncEvent(this, snapshot), (event) -> {
if (!isOffline()) {
getPlugin().debug(String.format("Applying snapshot (%s) to %s (cause: %s)",
- snapshot.getShortId(), getUsername(), cause.getDisplayName()
+ snapshot.getShortId(), getName(), cause.getDisplayName()
));
UserDataHolder.super.applySnapshot(
event.getData(), (succeeded) -> completeSync(succeeded, cause, getPlugin())
diff --git a/common/src/main/java/net/william278/husksync/util/DataSnapshotList.java b/common/src/main/java/net/william278/husksync/util/DataSnapshotList.java
index 9b2c8eb0..c2419076 100644
--- a/common/src/main/java/net/william278/husksync/util/DataSnapshotList.java
+++ b/common/src/main/java/net/william278/husksync/util/DataSnapshotList.java
@@ -49,7 +49,7 @@ public class DataSnapshotList {
.map(snapshot -> plugin.getLocales()
.getRawLocale(!snapshot.isInvalid() ? "data_list_item" : "data_list_item_invalid",
getNumberIcon(snapshotNumber.getAndIncrement()),
- dataOwner.getUsername(),
+ dataOwner.getName(),
snapshot.getId().toString(),
snapshot.getShortId(),
snapshot.isPinned() ? "※" : " ",
@@ -63,10 +63,10 @@ public class DataSnapshotList {
.orElse("• " + snapshot.getId())).toList(),
plugin.getLocales().getBaseChatList(6)
.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%")
.orElse(""))
- .setCommand("/husksync:userdata list " + dataOwner.getUsername())
+ .setCommand("/husksync:userdata list " + dataOwner.getName())
.build());
}
diff --git a/common/src/main/java/net/william278/husksync/util/DataSnapshotOverview.java b/common/src/main/java/net/william278/husksync/util/DataSnapshotOverview.java
index 582a37b9..d7497e8a 100644
--- a/common/src/main/java/net/william278/husksync/util/DataSnapshotOverview.java
+++ b/common/src/main/java/net/william278/husksync/util/DataSnapshotOverview.java
@@ -59,7 +59,7 @@ public class DataSnapshotOverview {
// Title message, timestamp, owner and cause.
final Locales locales = plugin.getLocales();
locales.getLocale("data_manager_title", snapshot.getShortId(), snapshot.getId().toString(),
- dataOwner.getUsername(), dataOwner.getUuid().toString())
+ dataOwner.getName(), dataOwner.getUuid().toString())
.ifPresent(user::sendMessage);
locales.getLocale("data_manager_timestamp",
snapshot.getTimestamp().format(DateTimeFormatter
@@ -107,13 +107,13 @@ public class DataSnapshotOverview {
if (user.hasPermission("husksync.command.inventory.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);
}
- 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);
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);
}
}
diff --git a/common/src/main/java/net/william278/husksync/util/UserDataDumper.java b/common/src/main/java/net/william278/husksync/util/UserDataDumper.java
index 4e1c2686..0fc488eb 100644
--- a/common/src/main/java/net/william278/husksync/util/UserDataDumper.java
+++ b/common/src/main/java/net/william278/husksync/util/UserDataDumper.java
@@ -179,7 +179,7 @@ public class UserDataDumper {
@NotNull
private String getFileName() {
return new StringJoiner("_")
- .add(user.getUsername())
+ .add(user.getName())
.add(snapshot.getTimestamp().format(DateTimeFormatter.ofPattern("yyyy-MM-dd_HH-mm-ss")))
.add(snapshot.getSaveCause().name().toLowerCase(Locale.ENGLISH))
.add(snapshot.getShortId())
diff --git a/common/src/main/resources/database/mariadb_schema.sql b/common/src/main/resources/database/mariadb_schema.sql
index bd851c11..793219f4 100644
--- a/common/src/main/resources/database/mariadb_schema.sql
+++ b/common/src/main/resources/database/mariadb_schema.sql
@@ -29,4 +29,28 @@ CREATE TABLE IF NOT EXISTS `%user_data_table%`
FOREIGN KEY (`player_uuid`) REFERENCES `%users_table%` (`uuid`) ON DELETE CASCADE
) ENGINE = InnoDB
DEFAULT CHARSET = utf8mb4
- COLLATE = utf8mb4_unicode_ci;
\ No newline at end of file
+ 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;
diff --git a/common/src/main/resources/database/mysql_schema.sql b/common/src/main/resources/database/mysql_schema.sql
index efef8797..e02597cd 100644
--- a/common/src/main/resources/database/mysql_schema.sql
+++ b/common/src/main/resources/database/mysql_schema.sql
@@ -26,4 +26,26 @@ CREATE TABLE IF NOT EXISTS `%user_data_table%`
PRIMARY KEY (`version_uuid`, `player_uuid`),
FOREIGN KEY (`player_uuid`) REFERENCES `%users_table%` (`uuid`) ON DELETE CASCADE
) CHARACTER SET utf8
- COLLATE utf8_unicode_ci;
\ No newline at end of file
+ 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;
diff --git a/common/src/main/resources/database/postgresql_schema.sql b/common/src/main/resources/database/postgresql_schema.sql
index 5b4b8ee9..615ab632 100644
--- a/common/src/main/resources/database/postgresql_schema.sql
+++ b/common/src/main/resources/database/postgresql_schema.sql
@@ -19,4 +19,24 @@ CREATE TABLE IF NOT EXISTS "%user_data_table%"
PRIMARY KEY (version_uuid, player_uuid),
FOREIGN KEY (player_uuid) REFERENCES "%users_table%" (uuid) ON DELETE CASCADE
-);
\ No newline at end of file
+);
+
+-- 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
+);
diff --git a/docs/Data-Snapshot-API.md b/docs/Data-Snapshot-API.md
index 118ca886..e0d6b2c4 100644
--- a/docs/Data-Snapshot-API.md
+++ b/docs/Data-Snapshot-API.md
@@ -37,7 +37,7 @@ huskSyncAPI.getUser(uuid).thenAccept(optionalUser -> {
}
// 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());
});
```
@@ -51,7 +51,7 @@ huskSyncAPI.getUser(uuid).thenAccept(optionalUser -> {
```java
// Get an online user
OnlineUser user = huskSyncAPI.getUser(player);
-System.out.println("Hello, %s!", user.getUsername());
+System.out.println("Hello, %s!", user.getName());
```
@@ -67,7 +67,7 @@ System.out.println("Hello, %s!", user.getUsername());
// Get a user's current data
huskSyncAPI.getCurrentData(user).thenAccept(optionalSnapshot -> {
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;
}
@@ -88,7 +88,7 @@ huskSyncAPI.getCurrentData(user).thenAccept(optionalSnapshot -> {
// Get a user's latest saved snapshot
huskSyncAPI.getLatestSnapshot(user).thenAccept(optionalSnapshot -> {
if (optionalSnapshot.isEmpty()) {
- System.out.println("%s has no saved snapshots!", user.getUsername());
+ System.out.println("%s has no saved snapshots!", user.getName());
return;
}
@@ -108,7 +108,7 @@ huskSyncAPI.getLatestSnapshot(user).thenAccept(optionalSnapshot -> {
// Get a user's saved snapshots
huskSyncAPI.getSnapshots(user).thenAccept(optionalSnapshots -> {
if (optionalSnapshots.isEmpty()) {
- System.out.println("%s has no saved snapshots!", user.getUsername());
+ System.out.println("%s has no saved snapshots!", user.getName());
return;
}
diff --git a/test/config.yml b/test/config.yml
index 28a7df3d..80e50a60 100644
--- a/test/config.yml
+++ b/test/config.yml
@@ -9,7 +9,7 @@ check_for_updates: false
cluster_id: ''
# Enable development debug logging
-debug_logging: false
+debug_logging: true
# Whether to enable the Player Analytics hook.
# Docs: https://william278.net/docs/husksync/plan-hook