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

Compare commits

...

10 Commits
3.8 ... 3.8.1

Author SHA1 Message Date
William278
f6d860335f fix: ensure map view for ID is set when applying, close #498 2025-05-10 23:45:05 +01:00
William278
5cea4665a1 fix: don't update ItemMeta of empty containers, close #499
This causes a `minecraft:block_entity_data` component to be added to the item, which causes stuff not to stack.
2025-05-10 23:27:52 +01:00
William278
34b183a35e build: bump runtime deps 2025-05-10 22:55:35 +01:00
William278
61298c24bb refactor: improve data identifier map structure, fix #492 2025-05-10 13:57:03 +01:00
William278
af9d32895e build: bump dependencies 2025-05-10 13:40:21 +01:00
ilightwas
52fa67432c fix: enable the userdata view command with no snapshot uuid args (#491)
Documentation states it should show the latest snapshot
2025-05-10 13:39:04 +01:00
William278
404f18d81f fix: add mojang-mapped, preload NBT-API
improves compatibility with 1.21.5/paper
2025-04-21 00:17:08 +01:00
William278
9ee8ea1c84 feat: auto-upgrade legacy map data, close #490 2025-04-17 20:54:31 +01:00
William278
64f845e293 build: bump nbt-api to 2.15.0 2025-04-17 20:31:04 +01:00
William278
30d1acc67e test: bump test suite to 1.21.5 2025-04-16 18:37:26 +01:00
13 changed files with 115 additions and 33 deletions

View File

@@ -89,7 +89,7 @@ allprojects {
}
dependencies {
testImplementation(platform("org.junit:junit-bom:5.12.1"))
testImplementation(platform("org.junit:junit-bom:5.12.2"))
testImplementation 'org.junit.jupiter:junit-jupiter'
testRuntimeOnly 'org.junit.platform:junit-platform-launcher'
testCompileOnly 'org.jetbrains:annotations:26.0.2'

View File

@@ -16,9 +16,9 @@ dependencies {
implementation 'net.william278:mapdataapi:2.0'
implementation 'org.bstats:bstats-bukkit:3.1.0'
implementation 'net.kyori:adventure-platform-bukkit:4.3.4'
implementation 'dev.triumphteam:triumph-gui:3.1.11'
implementation 'dev.triumphteam:triumph-gui:3.1.12'
implementation 'space.arim.morepaperlib:morepaperlib:0.4.4'
implementation 'de.tr7zw:item-nbt-api:2.14.2-SNAPSHOT'
implementation 'de.tr7zw:item-nbt-api:2.15.0'
compileOnly "io.papermc.paper:paper-api:${paper_api_version}"
compileOnly 'com.github.retrooper:packetevents-spigot:2.7.0'

View File

@@ -23,6 +23,7 @@ import com.google.common.collect.Lists;
import com.google.common.collect.Maps;
import com.google.common.collect.Sets;
import com.google.gson.Gson;
import de.tr7zw.changeme.nbtapi.NBT;
import de.tr7zw.changeme.nbtapi.utils.DataFixerUtil;
import lombok.AccessLevel;
import lombok.Getter;
@@ -149,6 +150,12 @@ public class BukkitHuskSync extends JavaPlugin implements HuskSync, BukkitTask.S
// Check compatibility
checkCompatibility();
// Preload NBT-API
if (!NBT.preloadApi()) {
log(Level.SEVERE, "Failed to load NBT API. HuskSync will not be initialized!");
return;
}
// Register commands
initialize("commands", (plugin) -> getUniform().register(PluginCommand.Type.create(this)));

View File

@@ -22,6 +22,7 @@ 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.ReadableItemNBT;
import de.tr7zw.changeme.nbtapi.iface.ReadableNBT;
import net.william278.husksync.BukkitHuskSync;
import net.william278.husksync.redis.RedisManager;
@@ -43,7 +44,9 @@ import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.awt.*;
import java.io.File;
import java.io.IOException;
import java.nio.file.Path;
import java.util.List;
import java.util.*;
import java.util.function.Function;
@@ -53,6 +56,8 @@ public interface BukkitMapHandler {
// The map used to store HuskSync data in ItemStack NBT
String MAP_DATA_KEY = "husksync:persisted_locked_map";
// The legacy map key used to store pixel data (3.7.3 and below)
String MAP_LEGACY_PIXEL_DATA_KEY = "husksync:canvas_data";
// Name of server the map originates from
String MAP_ORIGIN_KEY = "origin";
// Original map id
@@ -97,11 +102,12 @@ public interface BukkitMapHandler {
}
if (item.getType() == Material.FILLED_MAP && item.hasItemMeta()) {
items[i] = function.apply(item);
} else if (item.getItemMeta() instanceof BlockStateMeta b && b.getBlockState() instanceof Container box) {
} else if (item.getItemMeta() instanceof BlockStateMeta b && b.getBlockState() instanceof Container box
&& !box.getInventory().isEmpty()) {
forEachMap(box.getInventory().getContents(), function);
b.setBlockState(box);
item.setItemMeta(b);
} else if (item.getItemMeta() instanceof BundleMeta bundle) {
} else if (item.getItemMeta() instanceof BundleMeta bundle && bundle.hasItems()) {
bundle.setItems(List.of(forEachMap(bundle.getItems().toArray(ItemStack[]::new), function)));
item.setItemMeta(bundle);
}
@@ -260,21 +266,29 @@ public interface BukkitMapHandler {
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;
final MapView view = Bukkit.getMap(newId);
if (view != null) {
meta.setMapView(view);
meta.setMapId(newId);
map.setItemMeta(meta);
getPlugin().debug(String.format("Map ID set to #%s", newId));
return;
}
getPlugin().debug(String.format("Map ID #%s not saved on this server, creating...", newId));
}
// Read the pixel data and generate a map view otherwise
// Read the pixel data from the ItemStack and generate a map view otherwise
getPlugin().debug("Deserializing map data from NBT and generating view...");
final @Nullable Map.Entry<MapData, Boolean> readMapData = readMapData(originServerName, originalMapId);
@Nullable Map.Entry<MapData, Boolean> readMapData = readMapData(originServerName, originalMapId);
if (readMapData == null && nbt.hasTag(MAP_LEGACY_PIXEL_DATA_KEY)) {
readMapData = readLegacyMapItemData(nbt);
}
// If map data was found, add a renderer to the MapView
if (readMapData == null) {
getPlugin().debug("Read pixel data was not found in database, skipping...");
return;
}
// Add a renderer to the map with the data and save to file
final MapData canvasData = Objects.requireNonNull(readMapData, "Pixel data null!").getKey();
final MapView view = generateRenderedMap(canvasData);
meta.setMapView(view);
@@ -284,6 +298,7 @@ public interface BukkitMapHandler {
final int id = view.getId();
getRedisManager().bindMapIds(originServerName, originalMapId, currentServerName, id);
getPlugin().getDatabase().setMapBinding(originServerName, originalMapId, currentServerName, id);
meta.setMapId(id);
getPlugin().debug(String.format("Bound map to view (#%s) on server %s", id, currentServerName));
});
@@ -295,16 +310,21 @@ public interface BukkitMapHandler {
return;
}
@Nullable final Map.Entry<MapData, Boolean> data = readMapData(getPlugin().getServerName(), view.getId());
// Read map data, or
@Nullable Map.Entry<MapData, Boolean> data = readMapData(getPlugin().getServerName(), view.getId());
if (data == null) {
data = readLegacyMapFileData(view.getId());
}
// Don't render maps with no data
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;
}
// Don't render persisted maps on this server
if (data.getValue()) {
// from this server, doesn't need tweaking
return;
}
@@ -420,6 +440,36 @@ public interface BukkitMapHandler {
);
}
// Legacy - read maps from item stacks
@Nullable
@Blocking
private Map.Entry<MapData, Boolean> readLegacyMapItemData(@NotNull ReadableItemNBT nbt) {
final int dataVer = getPlugin().getDataVersion(getPlugin().getMinecraftVersion());
try {
return new AbstractMap.SimpleImmutableEntry<>(MapData.fromByteArray(dataVer,
Objects.requireNonNull(nbt.getByteArray(MAP_LEGACY_PIXEL_DATA_KEY))), false);
} catch (IOException e) {
getPlugin().log(Level.WARNING, "Failed to read legacy map data", e);
return null;
}
}
// Legacy - read maps from files
@Nullable
private Map.Entry<MapData, Boolean> readLegacyMapFileData(int mapId) {
final Path path = getPlugin().getDataFolder().toPath().resolve("maps").resolve(mapId + ".dat");
final File file = path.toFile();
if (!file.exists()) {
return null;
}
try {
return new AbstractMap.SimpleImmutableEntry<>(MapData.fromNbt(file), false);
} catch (IOException e) {
getPlugin().log(Level.WARNING, "Failed to read legacy map file", e);
return null;
}
}
/**
* A {@link MapCanvas} implementation used for pre-rendering maps to be converted into {@link MapData}
*/

View File

@@ -26,7 +26,7 @@ dependencies {
compileOnly 'net.kyori:adventure-api:4.20.0'
compileOnly 'net.kyori:adventure-platform-api:4.3.4'
compileOnly "net.kyori:adventure-text-serializer-plain:4.20.0"
compileOnly 'com.google.guava:guava:33.4.6-jre'
compileOnly 'com.google.guava:guava:33.4.8-jre'
compileOnly 'com.github.plan-player-analytics:Plan:5.5.2272'
compileOnly "redis.clients:jedis:$jedis_version"
compileOnly "com.mysql:mysql-connector-j:$mysql_driver_version"
@@ -37,7 +37,7 @@ dependencies {
testImplementation "redis.clients:jedis:$jedis_version"
testImplementation "org.xerial.snappy:snappy-java:$snappy_version"
testImplementation 'com.google.guava:guava:33.4.6-jre'
testImplementation 'com.google.guava:guava:33.4.8-jre'
testImplementation 'com.github.plan-player-analytics:Plan:5.5.2272'
testCompileOnly 'de.exlll:configlib-yaml:4.5.0'
testCompileOnly 'org.jetbrains:annotations:26.0.2'

View File

@@ -215,11 +215,17 @@ public class UserDataCommand extends PluginCommand {
@NotNull
private CommandProvider view() {
return (sub) -> sub.addSyntax((ctx) -> {
final User user = ctx.getArgument("username", User.class);
final UUID version = ctx.getArgument("version", UUID.class);
viewSnapshot(user(sub, ctx), user, version);
}, user("username"), versionUuid());
return (sub) -> {
sub.addSyntax((ctx) -> {
final User user = ctx.getArgument("username", User.class);
final UUID version = ctx.getArgument("version", UUID.class);
viewSnapshot(user(sub, ctx), user, version);
}, user("username"), versionUuid());
sub.addSyntax((ctx) -> {
final User user = ctx.getArgument("username", User.class);
viewLatestSnapshot(user(sub, ctx), user);
}, user("username"));
};
}
@NotNull

View File

@@ -31,7 +31,10 @@ public interface DataHolder {
Map<Identifier, Data> getData();
default Optional<? extends Data> getData(@NotNull Identifier id) {
return getData().entrySet().stream().filter(e -> e.getKey().equals(id)).map(Map.Entry::getValue).findFirst();
if (getData().containsKey(id)) {
return Optional.of(getData().get(id));
}
return Optional.empty();
}
default void setData(@NotNull Identifier identifier, @NotNull Data data) {

View File

@@ -406,7 +406,9 @@ public class DataSnapshot {
return deserialized.entrySet().stream()
.collect(Collectors.toMap(
entry -> entry.getKey().toString(),
entry -> plugin.serializeData(entry.getKey(), entry.getValue())
entry -> plugin.serializeData(entry.getKey(), entry.getValue()),
(a, b) -> a,
HashMap::new
));
}

View File

@@ -38,7 +38,7 @@ import java.util.stream.Stream;
* Identifiers of different types of {@link Data}s
*/
@Getter
public class Identifier {
public class Identifier implements Comparable<Identifier> {
// Namespace for built-in identifiers
private static final @KeyPattern String DEFAULT_NAMESPACE = "husksync";
@@ -276,6 +276,14 @@ public class Identifier {
return Map.entry(getKeyValue(), enabledByDefault);
}
// Comparable; always sort this Identifier after any dependencies
@Override
public int compareTo(@NotNull Identifier o) {
if (this.dependsOn(o)) return 1;
if (o.dependsOn(this)) return -1;
return this.key.compareTo(o.key);
}
/**
* Compares two identifiers based on their dependencies.
* <p>

View File

@@ -26,7 +26,9 @@ import org.jetbrains.annotations.NotNull;
import java.util.HashMap;
import java.util.Map;
import java.util.TreeMap;
import java.util.logging.Level;
import java.util.stream.Collectors;
/**
* A holder of data in the form of {@link Data}s, which can be synced
@@ -46,7 +48,11 @@ public interface UserDataHolder extends DataHolder {
.filter(Identifier::isEnabled)
.map(id -> Map.entry(id, getData(id)))
.filter(data -> data.getValue().isPresent())
.collect(HashMap::new, (map, data) -> map.put(data.getKey(), data.getValue().get()), HashMap::putAll);
.collect(Collectors.toMap(
Map.Entry::getKey,
entry -> entry.getValue().get(),
(a, b) -> a, HashMap::new
));
}
/**

View File

@@ -4,16 +4,16 @@ org.gradle.daemon=true
javaVersion=21
# Plugin metadata
plugin_version=3.8
plugin_version=3.8.1
plugin_archive=husksync
plugin_description=A modern, cross-server player data synchronization system
# General settings
jedis_version=5.2.0
mysql_driver_version=9.2.0
mariadb_driver_version=3.5.1
jedis_version=6.0.0
mysql_driver_version=9.3.0
mariadb_driver_version=3.5.3
postgres_driver_version=42.7.5
mongodb_driver_version=5.3.1
mongodb_driver_version=5.5.0
snappy_version=1.1.10.7
# Fabric settings

View File

@@ -13,7 +13,7 @@ from tqdm import tqdm
class Parameters:
root_dir = './servers/'
proxy_version = "3.4.0-SNAPSHOT"
minecraft_version = '1.21.4'
minecraft_version = '1.21.5'
eula_agreement = 'true'
backend_names = ['alpha', 'beta']