feat(save): SaveCodec capture/apply over a fresh world; tolerate null collections
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,126 @@
|
||||
package thb.jeanluc.adventure.save;
|
||||
|
||||
import thb.jeanluc.adventure.game.GameSession;
|
||||
import thb.jeanluc.adventure.game.QuestLog;
|
||||
import thb.jeanluc.adventure.model.Player;
|
||||
import thb.jeanluc.adventure.model.Room;
|
||||
import thb.jeanluc.adventure.model.World;
|
||||
import thb.jeanluc.adventure.model.item.Item;
|
||||
import thb.jeanluc.adventure.model.item.SwitchableItem;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* Converts between a live {@link GameSession} and a {@link SaveData} snapshot.
|
||||
* Pure (no disk): {@link #capture} reads a session; {@link #apply} overlays a
|
||||
* snapshot onto a session built from a freshly loaded world.
|
||||
*/
|
||||
public final class SaveCodec {
|
||||
|
||||
private SaveCodec() {
|
||||
}
|
||||
|
||||
/** Reads the mutable state of {@code session} into a {@link SaveData}. */
|
||||
public static SaveData capture(GameSession session) {
|
||||
World w = session.getWorld();
|
||||
Player p = session.getPlayer();
|
||||
|
||||
Map<String, List<String>> roomItems = new LinkedHashMap<>();
|
||||
for (Room room : w.getRooms().values()) {
|
||||
if (!room.getItems().isEmpty()) {
|
||||
roomItems.put(room.getId(), new ArrayList<>(room.getItems().keySet()));
|
||||
}
|
||||
}
|
||||
Map<String, Boolean> switches = new LinkedHashMap<>();
|
||||
for (Item item : w.getItems().values()) {
|
||||
if (item instanceof SwitchableItem sw) {
|
||||
switches.put(sw.getId(), sw.isOn());
|
||||
}
|
||||
}
|
||||
Map<String, Integer> stages = new LinkedHashMap<>();
|
||||
QuestLog log = session.getQuestLog();
|
||||
for (String id : log.active()) {
|
||||
stages.put(id, log.stageIndex(id));
|
||||
}
|
||||
|
||||
return new SaveData(
|
||||
SaveData.CURRENT_VERSION,
|
||||
w.getTitle(),
|
||||
session.getSlotName(),
|
||||
System.currentTimeMillis(),
|
||||
session.getTurn(),
|
||||
p.getCurrentRoom().getId(),
|
||||
new ArrayList<>(p.getVisitedRoomIds()),
|
||||
p.getGold(),
|
||||
new ArrayList<>(p.getInventory().keySet()),
|
||||
new ArrayList<>(session.getState().all()),
|
||||
stages,
|
||||
new ArrayList<>(log.completed()),
|
||||
roomItems,
|
||||
switches);
|
||||
}
|
||||
|
||||
/**
|
||||
* Overlays {@code data} onto {@code session} (whose world was just loaded
|
||||
* from YAML). Throws {@link SaveException} if the save references ids that
|
||||
* no longer exist in the current world.
|
||||
*/
|
||||
public static void apply(SaveData data, GameSession session) {
|
||||
World w = session.getWorld();
|
||||
Player p = session.getPlayer();
|
||||
|
||||
// 1. Clear all item placements, then redistribute from the save.
|
||||
for (Room room : w.getRooms().values()) {
|
||||
room.getItems().clear();
|
||||
}
|
||||
p.getInventory().clear();
|
||||
for (Map.Entry<String, List<String>> e : data.roomItemIds().entrySet()) {
|
||||
Room room = requireRoom(w, e.getKey());
|
||||
for (String itemId : e.getValue()) {
|
||||
room.addItem(requireItem(w, itemId));
|
||||
}
|
||||
}
|
||||
for (String itemId : data.inventoryItemIds()) {
|
||||
p.addItem(requireItem(w, itemId));
|
||||
}
|
||||
|
||||
// 2. Switch states.
|
||||
for (Map.Entry<String, Boolean> e : data.switchStates().entrySet()) {
|
||||
if (requireItem(w, e.getKey()) instanceof SwitchableItem sw) {
|
||||
sw.setState(e.getValue());
|
||||
}
|
||||
}
|
||||
|
||||
// 3. Player position + visited + gold.
|
||||
p.setCurrentRoom(requireRoom(w, data.currentRoomId()));
|
||||
p.getVisitedRoomIds().clear();
|
||||
p.getVisitedRoomIds().addAll(data.visitedRoomIds());
|
||||
p.setGold(data.gold());
|
||||
|
||||
// 4. Flags, quests, turn.
|
||||
session.getState().restore(data.flags());
|
||||
session.getQuestLog().restore(data.questStages(), data.questCompleted());
|
||||
session.setTurn(data.turn());
|
||||
}
|
||||
|
||||
private static Room requireRoom(World w, String id) {
|
||||
Room r = w.getRooms().get(id);
|
||||
if (r == null) {
|
||||
throw new SaveException("Save refers to unknown room '" + id
|
||||
+ "'. The game content may have changed.");
|
||||
}
|
||||
return r;
|
||||
}
|
||||
|
||||
private static Item requireItem(World w, String id) {
|
||||
Item i = w.getItems().get(id);
|
||||
if (i == null) {
|
||||
throw new SaveException("Save refers to unknown item '" + id
|
||||
+ "'. The game content may have changed.");
|
||||
}
|
||||
return i;
|
||||
}
|
||||
}
|
||||
@@ -26,4 +26,19 @@ public record SaveData(
|
||||
) {
|
||||
/** Current on-disk schema version. */
|
||||
public static final int CURRENT_VERSION = 1;
|
||||
|
||||
/**
|
||||
* Compact canonical constructor: normalizes any null collection component
|
||||
* to an empty immutable collection so a truncated/hand-edited save JSON
|
||||
* degrades gracefully instead of NPE-ing in {@link SaveCodec#apply}.
|
||||
*/
|
||||
public SaveData {
|
||||
if (visitedRoomIds == null) visitedRoomIds = List.of();
|
||||
if (inventoryItemIds == null) inventoryItemIds = List.of();
|
||||
if (flags == null) flags = List.of();
|
||||
if (questStages == null) questStages = Map.of();
|
||||
if (questCompleted == null) questCompleted = List.of();
|
||||
if (roomItemIds == null) roomItemIds = Map.of();
|
||||
if (switchStates == null) switchStates = Map.of();
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user