Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Rework locked maps syncing #441

Open
wants to merge 18 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -133,7 +133,7 @@ public void onWorldSave(@NotNull WorldSaveEvent event) {
@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).renderMapFromDb(event.getMap()));
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,10 +31,8 @@
import org.bukkit.Bukkit;
import org.bukkit.Material;
import org.bukkit.World;
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.MapMeta;
import org.bukkit.map.*;
import org.jetbrains.annotations.ApiStatus;
Expand All @@ -45,17 +43,16 @@
import java.io.File;
import java.util.List;
import java.util.*;
import java.util.function.Function;
import java.util.logging.Level;

public interface BukkitMapPersister {

// 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";
// ID of world 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
Expand Down Expand Up @@ -99,11 +96,16 @@ private ItemStack[] forEachMap(ItemStack[] items, @NotNull Function<ItemStack, I
} 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;
}

@SuppressWarnings("deprecation")
@NotNull
private ItemStack persistMapView(@NotNull ItemStack map, @NotNull Player delegateRenderer) {
final MapMeta meta = Objects.requireNonNull((MapMeta) map.getItemMeta());
Expand Down Expand Up @@ -131,14 +133,19 @@ private ItemStack persistMapView(@NotNull ItemStack map, @NotNull Player delegat

// 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 UUID worldUid = view.getWorld().getUID();
final String worldUidString = worldUid.toString();
mapData.setString(MAP_ORIGIN_KEY, worldUidString);
mapData.setInteger(MAP_ID_KEY, meta.getMapId());
if (getPlugin().getDatabase().readMapData(worldUid, meta.getMapId()) == null) {
getPlugin().getDatabase().writeMapData(worldUid, meta.getMapId(), canvas.extractMapData().toBytes());
}
getPlugin().debug(String.format("Saved data for locked map (#%s, UID: %s)", view.getId(), worldUidString));
});
return map;
}

@SuppressWarnings("deprecation")
@NotNull
private ItemStack applyMapView(@NotNull ItemStack map) {
final int dataVersion = getPlugin().getDataVersion(getPlugin().getMinecraftVersion());
Expand All @@ -148,32 +155,29 @@ private ItemStack applyMapView(@NotNull ItemStack map) {
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<String> 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;
}
final UUID originWorldId = UUID.fromString(mapData.getString(MAP_ORIGIN_KEY));
final UUID currentWorldId = getDefaultMapWorld().getUID();
final int originalMapId = mapData.getInteger(MAP_ID_KEY);
int newId;
if (currentWorldId.equals(originWorldId)) {
newId = originalMapId;
} else {
newId = getPlugin().getDatabase().getNewMapId(
originWorldId,
originalMapId,
currentWorldId
);
}
if (world.isPresent()) {
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;
}

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
Expand All @@ -182,39 +186,41 @@ private ItemStack applyMapView(@NotNull ItemStack map) {
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!"));
Objects.requireNonNull(getPlugin().getDatabase().readMapData(originWorldId, originalMapId), "Pixel data null!").getKey());
} 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
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));
getPlugin().getDatabase().connectMapIds(originWorldId, originalMapId, currentWorldId, view.getId());

getPlugin().debug(String.format("Connected map to view (#%s) in world %s", view.getId(), currentWorldId));
});
return map;
}

default void renderMapFromFile(@NotNull MapView view) {
final File mapFile = new File(getMapCacheFolder(), view.getId() + ".dat");
if (!mapFile.exists()) {
default void renderMapFromDb(@NotNull MapView view) {
@Nullable Map.Entry<byte[], Boolean> data = getPlugin().getDatabase().readMapData(getDefaultMapWorld().getUID(), view.getId());
if (data == null) {
getPlugin().log(Level.WARNING, "Cannot render map: no data in DB for world " + getDefaultMapWorld().getUID() + ", map " + view.getId());
return;
}

if (data.getValue()) {
// from this server, doesn't need tweaking
return;
}

final MapData canvasData;
try {
canvasData = MapData.fromNbt(mapFile);
canvasData = MapData.fromByteArray(
getPlugin().getDataVersion(getPlugin().getMinecraftVersion()),
data.getKey()
);
} catch (Throwable e) {
getPlugin().log(Level.WARNING, "Failed to deserialize map data from file", e);
return;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
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;
Expand Down Expand Up @@ -56,8 +57,8 @@ protected Database(@NotNull HuskSync plugin) {
@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);
}

/**
Expand All @@ -70,7 +71,9 @@ protected final String[] getSchemaStatements(@NotNull String schemaFileName) thr
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));
}

/**
Expand Down Expand Up @@ -246,6 +249,48 @@ public final void pinSnapshot(@NotNull User user, @NotNull UUID versionUuid) {
});
}

/**
* Write map data to a database
*
* @param worldId ID of the world the map originates from
* @param mapId Original map ID
* @param data Map data
*/
@Blocking
public abstract void writeMapData(@NotNull UUID worldId, int mapId, byte @NotNull [] data);

/**
* Read map data from a database
*
* @param worldId ID of the world 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> readMapData(@NotNull UUID worldId, int mapId);

/**
* Connect map IDs across different worlds
*
* @param fromWorldId ID of the world the map originates from
* @param fromMapId Original map ID
* @param toWorldId ID of the new world
* @param toMapId New map ID
*/
@Blocking
public abstract void connectMapIds(@NotNull UUID fromWorldId, int fromMapId, @NotNull UUID toWorldId, int toMapId);

/**
* Get map ID for the new world
*
* @param fromWorldId ID of the world the map originates from
* @param fromMapId Original map ID
* @param toWorldId ID of the new world
* @return New map ID or -1 if not found
*/
@Blocking
public abstract int getNewMapId(@NotNull UUID fromWorldId, int fromMapId, @NotNull UUID toWorldId);

/**
* Wipes <b>all</b> {@link User} entries from the database.
* <b>This should only be used when preparing tables for a data migration.</b>
Expand Down Expand Up @@ -283,7 +328,9 @@ public enum Type {
@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;

Expand Down
Loading