diff --git a/common/src/main/java/net/onelitefeather/cygnus/common/map/GameMap.java b/common/src/main/java/net/onelitefeather/cygnus/common/map/GameMap.java index da1fd82f..992e3f4c 100644 --- a/common/src/main/java/net/onelitefeather/cygnus/common/map/GameMap.java +++ b/common/src/main/java/net/onelitefeather/cygnus/common/map/GameMap.java @@ -31,11 +31,18 @@ public final class GameMap extends BaseMap { * @param survivorSpawns the spawn positions for the survivors * @param builders the builders for the map */ - public GameMap(String name, Pos spawn, Pos slenderSpawn, Set pageFaces, Set survivorSpawns, List builders) { + public GameMap( + String name, + Pos spawn, + Pos slenderSpawn, + Set pageFaces, + Set survivorSpawns, + List builders + ) { super(name, spawn, builders); this.slenderSpawn = slenderSpawn; - this.pageFaces = pageFaces != null ? Set.copyOf(pageFaces) : Set.of(); - this.survivorSpawns = survivorSpawns != null ? Set.copyOf(survivorSpawns) : Set.of(); + this.pageFaces = Set.copyOf(pageFaces); + this.survivorSpawns = Set.copyOf(survivorSpawns); } /** diff --git a/common/src/main/java/net/onelitefeather/cygnus/common/map/adapter/GameMapAdapter.java b/common/src/main/java/net/onelitefeather/cygnus/common/map/adapter/GameMapAdapter.java new file mode 100644 index 00000000..f171035e --- /dev/null +++ b/common/src/main/java/net/onelitefeather/cygnus/common/map/adapter/GameMapAdapter.java @@ -0,0 +1,89 @@ +package net.onelitefeather.cygnus.common.map.adapter; + +import com.google.gson.JsonDeserializationContext; +import com.google.gson.JsonDeserializer; +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; +import com.google.gson.JsonParseException; +import com.google.gson.reflect.TypeToken; +import net.minestom.server.coordinate.Pos; +import net.onelitefeather.cygnus.common.map.GameMap; +import net.onelitefeather.cygnus.common.page.PageResource; +import org.jetbrains.annotations.Nullable; + +import java.lang.reflect.Type; +import java.util.List; +import java.util.Set; + +/** + * Custom JsonDeserializer for {@link GameMap} to safely handle missing or null collections during deserialization. + * + * @author Jotras + * @version 1.0.0 + * @since 2.7.0 + */ +public final class GameMapAdapter implements JsonDeserializer { + + private static final String NAME_KEY = "name"; + private static final String SPAWN_KEY = "spawn"; + private static final String SLENDER_SPAWN_KEY = "slenderSpawn"; + private static final String PAGE_FACES_KEY = "pageFaces"; + private static final String SURVIVOR_SPAWNS_KEY = "survivorSpawns"; + private static final String BUILDERS_KEY = "builders"; + + private static final Type PAGE_FACES_TYPE = TypeToken.getParameterized(Set.class, PageResource.class).getType(); + private static final Type SURVIVOR_SPAWNS_TYPE = TypeToken.getParameterized(Set.class, Pos.class).getType(); + private static final Type BUILDERS_TYPE = TypeToken.getParameterized(List.class, String.class).getType(); + + /** + * Deserializes a {@link GameMap} from the given JSON element. + * Missing or {@code null} fields are replaced with safe defaults to prevent {@link NullPointerException}s + * during downstream processing (e.g. in {@link net.onelitefeather.cygnus.common.map.GameMapBuilder}). + * + * @param json the JSON element to deserialize + * @param typeOfT the type of the object to deserialize to + * @param context the deserialization context + * @return a fully initialized {@link GameMap} instance + * @throws JsonParseException if the JSON structure is fundamentally invalid + */ + @Override + public GameMap deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) throws JsonParseException { + JsonObject object = json.getAsJsonObject(); + + String name = deserializeOrDefault(object, NAME_KEY, String.class, context, ""); + Pos spawn = deserializeOrDefault(object, SPAWN_KEY, Pos.class, context, null); + Pos slenderSpawn = deserializeOrDefault(object, SLENDER_SPAWN_KEY, Pos.class, context, null); + Set pageFaces = deserializeOrDefault(object, PAGE_FACES_KEY, PAGE_FACES_TYPE, context, Set.of()); + Set survivorSpawns = deserializeOrDefault(object, SURVIVOR_SPAWNS_KEY, SURVIVOR_SPAWNS_TYPE, context, Set.of()); + List builders = deserializeOrDefault(object, BUILDERS_KEY, BUILDERS_TYPE, context, List.of()); + + return new GameMap(name, spawn, slenderSpawn, pageFaces, survivorSpawns, builders); + } + + /** + * Attempts to deserialize a value from the given {@link JsonObject} by key. + * Returns the provided default value if the key is absent, the value is {@code null}, + * or deserialization itself yields {@code null}. + * + * @param object the JSON object to read from + * @param key the field name to look up + * @param type the target type for deserialization + * @param context the deserialization context + * @param defaultValue the fallback value when the field is missing or {@code null} + * @param the expected return type + * @return the deserialized value, or {@code defaultValue} if unavailable + */ + private @Nullable T deserializeOrDefault( + JsonObject object, + String key, + Type type, + JsonDeserializationContext context, + @Nullable T defaultValue + ) { + if (!object.has(key) || object.get(key).isJsonNull()) { + return defaultValue; + } + T result = context.deserialize(object.get(key), type); + return result != null ? result : defaultValue; + } +} diff --git a/common/src/main/java/net/onelitefeather/cygnus/common/map/adapter/package-info.java b/common/src/main/java/net/onelitefeather/cygnus/common/map/adapter/package-info.java new file mode 100644 index 00000000..8a4d8003 --- /dev/null +++ b/common/src/main/java/net/onelitefeather/cygnus/common/map/adapter/package-info.java @@ -0,0 +1,4 @@ +@NotNullByDefault +package net.onelitefeather.cygnus.common.map.adapter; + +import org.jetbrains.annotations.NotNullByDefault; \ No newline at end of file diff --git a/common/src/main/java/net/onelitefeather/cygnus/common/map/filter/package-info.java b/common/src/main/java/net/onelitefeather/cygnus/common/map/filter/package-info.java index addb1e18..ca206e7b 100644 --- a/common/src/main/java/net/onelitefeather/cygnus/common/map/filter/package-info.java +++ b/common/src/main/java/net/onelitefeather/cygnus/common/map/filter/package-info.java @@ -1,4 +1,3 @@ - @NotNullByDefault package net.onelitefeather.cygnus.common.map.filter; diff --git a/common/src/main/java/net/onelitefeather/cygnus/common/util/GsonHelper.java b/common/src/main/java/net/onelitefeather/cygnus/common/util/GsonHelper.java index 092b5bee..9a2d5e8d 100644 --- a/common/src/main/java/net/onelitefeather/cygnus/common/util/GsonHelper.java +++ b/common/src/main/java/net/onelitefeather/cygnus/common/util/GsonHelper.java @@ -3,6 +3,8 @@ import com.google.gson.Gson; import net.minestom.server.coordinate.Pos; import net.minestom.server.coordinate.Vec; +import net.onelitefeather.cygnus.common.map.GameMap; +import net.onelitefeather.cygnus.common.map.adapter.GameMapAdapter; import net.onelitefeather.cygnus.common.page.PageResource; import net.onelitefeather.cygnus.common.page.adapter.PageResourceAdapter; import net.theevilreaper.aves.file.GsonFileHandler; @@ -27,6 +29,7 @@ public final class GsonHelper { .registerTypeAdapter(Pos.class, typeAdapter) .registerTypeAdapter(Vec.class, typeAdapter) .registerTypeAdapter(PageResource.class, new PageResourceAdapter()) + .registerTypeAdapter(GameMap.class, new GameMapAdapter()) .create(); FILE_HANDLER = new GsonFileHandler(GSON); } diff --git a/common/src/test/java/net/onelitefeather/cygnus/common/map/GameMapTest.java b/common/src/test/java/net/onelitefeather/cygnus/common/map/GameMapTest.java index 0dcbd550..5b681712 100644 --- a/common/src/test/java/net/onelitefeather/cygnus/common/map/GameMapTest.java +++ b/common/src/test/java/net/onelitefeather/cygnus/common/map/GameMapTest.java @@ -110,4 +110,15 @@ void testGameMapCopy() { assertEquals(originalMap.getSurvivorSpawns(), rebuiltMap.getSurvivorSpawns()); assertEquals(originalMap.getPageFaces(), rebuiltMap.getPageFaces()); } + + @Test + void testGameMapCopyWithEmptyCollections() { + GameMap mapWithEmptyCollections = new GameMap("Map", Pos.ZERO, null, Set.of(), Set.of(), List.of()); + GameMapBuilder builder = new GameMapBuilder(mapWithEmptyCollections); + + assertNotNull(builder.getSurvivorSpawns()); + assertTrue(builder.getSurvivorSpawns().isEmpty()); + assertNotNull(builder.getPageFaces()); + assertTrue(builder.getPageFaces().isEmpty()); + } }