From c0709f82bd2638759c4f772225e40e4b6da4c6a9 Mon Sep 17 00:00:00 2001 From: William278 Date: Tue, 15 Nov 2022 18:29:37 +0000 Subject: [PATCH] Add the ability to synchronise/persist locked maps cross-server, close #14 --- bukkit/build.gradle | 3 + .../husksync/data/BukkitMapHandler.java | 121 ++++++++++++++++++ .../husksync/data/BukkitSerializer.java | 12 +- .../william278/husksync/config/Settings.java | 3 +- 4 files changed, 137 insertions(+), 2 deletions(-) create mode 100644 bukkit/src/main/java/net/william278/husksync/data/BukkitMapHandler.java diff --git a/bukkit/build.gradle b/bukkit/build.gradle index de84b40a..52e7e08e 100644 --- a/bukkit/build.gradle +++ b/bukkit/build.gradle @@ -3,6 +3,7 @@ dependencies { implementation 'org.bstats:bstats-bukkit:3.0.0' implementation 'net.william278:mpdbdataconverter:1.0.1' implementation 'net.william278:hsldataconverter:1.0' + implementation 'net.william278:MapDataAPI:1.0' implementation 'me.lucko:commodore:2.2' implementation 'net.kyori:adventure-platform-bukkit:4.1.2' implementation 'dev.triumphteam:triumph-gui:3.1.3' @@ -31,6 +32,8 @@ shadowJar { relocate 'dev.dejvokep', 'net.william278.husksync.libraries' relocate 'net.william278.desertwell', 'net.william278.husksync.libraries.desertwell' relocate 'net.william278.paginedown', 'net.william278.husksync.libraries.paginedown' + relocate 'net.william278.mapdataapi', 'net.william278.husksync.libraries.mapdataapi' + relocate 'net.querz', 'net.william278.husksync.libraries.nbt' relocate 'me.lucko.commodore', 'net.william278.husksync.libraries.commodore' relocate 'net.byteflux.libby', 'net.william278.husksync.libraries.libby' diff --git a/bukkit/src/main/java/net/william278/husksync/data/BukkitMapHandler.java b/bukkit/src/main/java/net/william278/husksync/data/BukkitMapHandler.java new file mode 100644 index 00000000..0c2d6e33 --- /dev/null +++ b/bukkit/src/main/java/net/william278/husksync/data/BukkitMapHandler.java @@ -0,0 +1,121 @@ +package net.william278.husksync.data; + +import net.william278.husksync.BukkitHuskSync; +import net.william278.mapdataapi.MapData; +import org.bukkit.Bukkit; +import org.bukkit.Material; +import org.bukkit.NamespacedKey; +import org.bukkit.entity.Player; +import org.bukkit.inventory.ItemStack; +import org.bukkit.inventory.meta.MapMeta; +import org.bukkit.map.MapCanvas; +import org.bukkit.map.MapRenderer; +import org.bukkit.map.MapView; +import org.bukkit.persistence.PersistentDataType; +import org.jetbrains.annotations.NotNull; + +import java.io.IOException; +import java.util.Objects; +import java.util.logging.Level; + +/** + * Handles the persistence of {@link MapData} into {@link ItemStack}s. + */ +public class BukkitMapHandler { + + private static final BukkitHuskSync plugin = BukkitHuskSync.getInstance(); + private static final NamespacedKey MAP_DATA_KEY = new NamespacedKey(plugin, "map_data"); + + /** + * Get the {@link MapData} from the given {@link ItemStack} and persist it in its' data container + * + * @param itemStack the {@link ItemStack} to get the {@link MapData} from + */ + public static void persistMapData(@NotNull ItemStack itemStack) { + if (itemStack.getType() != Material.FILLED_MAP) { + return; + } + final MapMeta mapMeta = (MapMeta) itemStack.getItemMeta(); + if (mapMeta == null || !mapMeta.hasMapView()) { + return; + } + + // Get the map view + final MapView mapView = mapMeta.getMapView(); + if (mapView == null || !mapView.isLocked() || mapView.isVirtual()) { + return; + } + final int mapId = mapView.getId(); + if (mapId < 0) { + return; + } + + // Get the map data + try { + if (!itemStack.getItemMeta().getPersistentDataContainer().has(MAP_DATA_KEY, PersistentDataType.STRING)) { + itemStack.getItemMeta().getPersistentDataContainer().set(MAP_DATA_KEY, PersistentDataType.STRING, + MapData.getFromFile(Bukkit.getWorlds().get(0).getWorldFolder(), mapId).toString()); + } + } catch (IOException e) { + plugin.getLogger().log(Level.WARNING, "Failed to serialize map data for map " + mapId + ")"); + } + } + + /** + * Set the map data of the given {@link ItemStack} to the given {@link MapData}, applying a map view to the item stack + * + * @param itemStack the {@link ItemStack} to set the map data of + */ + public static void setMapRenderer(@NotNull ItemStack itemStack) { + if (itemStack.getType() != Material.FILLED_MAP) { + return; + } + + final MapMeta mapMeta = (MapMeta) itemStack.getItemMeta(); + if (mapMeta == null) { + return; + } + + if (!itemStack.getItemMeta().getPersistentDataContainer().has(MAP_DATA_KEY, PersistentDataType.STRING)) { + return; + } + + try { + final String serializedData = Objects.requireNonNull(itemStack + .getItemMeta().getPersistentDataContainer().get(MAP_DATA_KEY, PersistentDataType.STRING)); + final MapData mapData = MapData.fromString(serializedData); + + // Create a new map view renderer with the map data color at each pixel + final MapView mapView = mapMeta.getMapView(); + if (mapView == null) { + return; + } + mapView.getRenderers().forEach(mapView::removeRenderer); + mapView.addRenderer(new BukkitMapDataRenderer(mapData)); + } catch (IOException e) { + plugin.getLogger().log(Level.WARNING, "Failed to deserialize map data for a player"); + } + } + + /** + * Renders {@link MapData} to a bukkit {@link MapView}. + */ + public static class BukkitMapDataRenderer extends MapRenderer { + + private final MapData mapData; + + protected BukkitMapDataRenderer(@NotNull MapData mapData) { + this.mapData = mapData; + } + + @Override + public void render(@NotNull MapView map, @NotNull MapCanvas canvas, @NotNull Player player) { + for (int i = 0; i < 128; i++) { + for (int j = 0; j < 128; j++) { + canvas.setPixel(i, j, (byte) mapData.getColorAt(i, j).intValue()); + } + } + map.setLocked(true); + } + } +} diff --git a/bukkit/src/main/java/net/william278/husksync/data/BukkitSerializer.java b/bukkit/src/main/java/net/william278/husksync/data/BukkitSerializer.java index 5ee42eb1..b93992e7 100644 --- a/bukkit/src/main/java/net/william278/husksync/data/BukkitSerializer.java +++ b/bukkit/src/main/java/net/william278/husksync/data/BukkitSerializer.java @@ -1,6 +1,7 @@ package net.william278.husksync.data; import net.william278.husksync.BukkitHuskSync; +import net.william278.husksync.config.Settings; import org.bukkit.inventory.ItemStack; import org.bukkit.potion.PotionEffect; import org.bukkit.util.io.BukkitObjectInputStream; @@ -40,7 +41,11 @@ public class BukkitSerializer { bukkitOutputStream.writeInt(inventoryContents.length); // Write each serialize each ItemStack to the output stream + final boolean persistLockedMaps = BukkitHuskSync.getInstance().getSettings().getSynchronizationFeature(Settings.SynchronizationFeature.LOCKED_MAPS); for (ItemStack inventoryItem : inventoryContents) { + if (persistLockedMaps) { + BukkitMapHandler.persistMapData(inventoryItem); + } bukkitOutputStream.writeObject(serializeItemStack(inventoryItem)); } @@ -89,8 +94,13 @@ public class BukkitSerializer { // Set the ItemStacks in the array from deserialized ItemStack data int slotIndex = 0; + final boolean persistLockedMaps = BukkitHuskSync.getInstance().getSettings().getSynchronizationFeature(Settings.SynchronizationFeature.LOCKED_MAPS); for (ItemStack ignored : inventoryContents) { - inventoryContents[slotIndex] = deserializeItemStack(bukkitInputStream.readObject()); + final ItemStack deserialized = deserializeItemStack(bukkitInputStream.readObject()); + if (persistLockedMaps) { + BukkitMapHandler.setMapRenderer(deserialized); + } + inventoryContents[slotIndex] = deserialized; slotIndex++; } diff --git a/common/src/main/java/net/william278/husksync/config/Settings.java b/common/src/main/java/net/william278/husksync/config/Settings.java index b1523ec0..9280c547 100644 --- a/common/src/main/java/net/william278/husksync/config/Settings.java +++ b/common/src/main/java/net/william278/husksync/config/Settings.java @@ -180,7 +180,8 @@ public class Settings { GAME_MODE(true), STATISTICS(true), PERSISTENT_DATA_CONTAINER(false), - LOCATION(false); + LOCATION(false), + LOCKED_MAPS(true); private final boolean enabledByDefault;