Skip to content

Save Load System

Gurkirat Singh Randhawa edited this page Oct 19, 2026 · 6 revisions

Overview

The Save/Load system lets a player stop and come back to the same game. A save records the player's stats, inventory, pets, shields, active upgrades and potion effects, the selected difficulty, the current level and position, quest progress, perks and tortoises, the shop's stock, and the state of the world (collected loot, killed enemies, killed friendly NPCs and items lying on the ground). Loading rebuilds the saved level and puts all of that back.

There is one save slot. It is written when the player chooses Save in the pause menu, and automatically each time the player enters a new level.

What Gets Saved

Player

  • Health, stamina and gold
  • Position, and the level the player is in
  • The selected hotbar slot

Inventory

  • Every item in its exact slot, with its quantity and sell price
  • Weapons keep their type and tier
  • Consumables (potions and upgrade stones) keep their type, tier and full stack size

Pets and shop

  • Which pets the player owns, in their slot order
  • Which pet is active
  • Which shop listings are still in stock, so items bought out stay sold out

Shields, upgrades and temporary effects

  • A shield or ballistic shield that was picked up but not used yet
  • An active shield or ballistic shield, with its time left
  • Every active upgrade, with its tier and the time or kills it has left
  • Remaining shield hits from the Shield Durability upgrade
  • Active potion buffs (damage, speed, resistance), with their strength and time left
  • Regeneration in progress, with its heal per tick and time left

Quests and NPCs

  • Each friendly NPC's quest state (not started, in progress, done) and its active quest's progress
  • Which friendly NPCs were killed, so they do not come back
  • The quest counters (jumps, enemies killed, gold spent, shields collected), so quests in progress carry on from the right point
  • How many quests of each kind were completed, for the quest menu and the win screen

Perks and tortoises

  • Each perk's progress, and whether it is unlocked and active
  • Which tortoises were found
  • The tortoise total and the plan of which levels hold them

World

  • The selected difficulty mode
  • Which loot spawn points have already been collected, per level
  • Which enemies have already been killed, per level
  • One loot seed per visited level, so loot appears in the same places
  • Items lying on the ground that were dropped during play (enemy drops, items and gold the player dropped, death loot), with their positions

How It Works

Saving

  1. The player opens the pause menu and chooses Save, or walks through a door into a new level.
  2. PauseMenuActions.createSaveData(player) reads the current state from the player's components, the loot, enemy and NPC registries, the quest, perk and tortoise systems, the upgrade list and the entity service, and builds a GameSaveData object.
  3. SaveService.save(data) writes it to disk as JSON.
  4. A manual Save then unpauses the game and returns to the Main Menu. An automatic checkpoint does not interrupt play.

Timed effects (shields, upgrades, buffs, regeneration) are saved as the time they have left, not as a game clock time, because the game clock starts again from zero on every new screen.

Loading

  1. The player chooses Load on the Main Menu, which creates MainGameScreen with loadsave set to true.
  2. SaveService.hasSave() checks whether a save file exists. If it does not, the game starts fresh on Normal difficulty.
  3. Before the level is built, MainGameScreen restores the things that decide what the level contains:
    • the collected loot and killed enemy lists, so only what should still be there is spawned
    • the NPC quest states and killed NPC list, and the quest counters and completed quests
    • the perks, so components that check active perks when the player is created see the right ones
    • the found tortoises, the tortoise total and the tortoise plan
    • the loot seeds, so loot is placed where it was
    • the difficulty, so enemies are scaled correctly as they spawn
    • the saved level, so the correct map is loaded
  4. The level and a new player are created. Each friendly NPC gets its saved quest state back as it spawns, and a killed NPC is not spawned.
  5. LoadService.load(...) applies the rest of the save to the player, in this order:
    1. Health
    2. Shields. A held shield is given back, and an active shield is started again with the time it had left.
    3. Stamina
    4. Inventory. Starting items are cleared, then each saved item is rebuilt and put back in its saved slot. Weapons and consumables are rebuilt as fully working items. The selected slot is restored last.
    5. Position. Checked against the bounds of the level being loaded. An out of range position is ignored and the player stays at the level's spawn point.
    6. Pets. Owned pets are put back in their slots and the active pet is created.
    7. Shop stock. Listings that were not in stock when the game was saved are removed.
    8. Upgrades. Each saved upgrade is matched to its UpgradeNode by id and restored with its tier and remaining time or kills. Restoring fires the upgrade's own tier changed callback, which applies its effect to the new player. Remaining shield hits are then set.
    9. Potion buffs and regeneration. Each is applied again for the time it had left.
    10. Dropped loot. Each saved ground item is created again at its saved position. Positions outside the level are skipped.

The order matters in two places. Upgrades are restored before potion buffs because both can change the player's base attack, and this order lets them stack and expire correctly. Everything that depends on health is restored after health.

Moving between levels

  • Entering a level through a door saves a checkpoint.
  • A level that was already visited is rebuilt with its saved loot seed, so its loot stays in the same places and collected loot does not come back.

Dying and Try Again

  • Try Again revives the player at the start of Level 1, not in the level where they died. Level 1 is rebuilt with its saved loot seed and a new player is created.
  • Killed enemies, collected loot, loot seeds, quests and NPCs, perks, tortoises and the difficulty are all kept.
  • The player's items and gold are dropped next to where they died, so they can go back and pick them up.
  • Try Again does not read or write the save file.

Starting and restarting

  • Start and Restart always begin a fresh game. Collected loot, killed enemies, quests and NPCs, perks and found tortoises are cleared, a new tortoise plan is made, and loot is placed with new seeds.
  • The difficulty chosen on the difficulty screen is kept for a new game, and Restart keeps the difficulty being played.
  • Neither one deletes the save file. The existing save is replaced the next time the game saves, which happens on entering the next level.

Older save files

New fields have been added to the save file over time. A save made before a field existed simply loads that field with its default value (no upgrades, no buffs, no dropped loot, no quests, hotbar slot 1), so older saves still load. Fields that cannot have a safe default are null in older saves and are skipped: shop stock and perks are left as they are, and shield hits are not changed. A save without a tortoise total gets a new one.

Save File

  • Location: AscentOfTheMortal/save.json in the user's home directory
  • Format: JSON, written and read with the project's FileLoader using Location.EXTERNAL

Example (shortened):

{
  "health": 45,
  "stamina": 80.0,
  "gold": 60,
  "activeSlot": 2,
  "posX": 14.2,
  "posY": 6.0,
  "level": "maps/level1-greek.json",
  "difficulty": "HARD",
  "items": [
    { "slot": 1, "name": "Sword", "itemType": "WEAPON", "quantity": 1, "maxQuantity": 1,
      "sellPrice": 20, "weaponType": "SWORD", "damage": 20, "weaponTier": 2 },
    { "slot": 3, "name": "Health Potion", "itemType": "CONSUMABLE", "quantity": 6,
      "maxQuantity": 9, "sellPrice": 10, "consumableType": "HEALTH_POTION" }
  ],
  "ownedPetNames": ["Bird"],
  "activePetName": "Bird",
  "shopItemSlots": [0, 2, 3],
  "upgrades": [
    { "id": "player_speed", "tier": 2, "remainingSeconds": 14.0, "remainingKills": 0 }
  ],
  "shieldHits": 0,
  "shieldHeld": false,
  "shieldRemainingMillis": 4200,
  "ballisticShieldHeld": true,
  "ballisticShieldRemainingMillis": 0,
  "buffs": [
    { "stat": "DAMAGE", "magnitude": 1.5, "remainingSeconds": 18.0 }
  ],
  "regenHealPerTick": 0,
  "regenRemainingSeconds": 0.0,
  "droppedLoot": [
    { "x": 9.5, "y": 4.0,
      "item": { "slot": 0, "name": "Gold", "itemType": "CURRENCY", "quantity": 5, "maxQuantity": 99 } }
  ],
  "collectedLootIds": ["Level 1 - Out of the Underworld:12,7"],
  "killedEnemyIds": ["Level 1 - Out of the Underworld:20,5"],
  "lootSeedsByRoom": { "maps/level1-greek.json": 1234 },
  "npcs": [
    { "id": "<npc id>@Level 1 - Out of the Underworld:8,3", "dialogueState": "CHECKING",
      "amountXToDo": 5, "questType": "jumpquest", "amountToDo": 5, "snapshot": 2.0 }
  ],
  "killedNpcIds": [],
  "questJumps": 4,
  "questEnemiesKilled": 2,
  "questGoldSpent": 0,
  "questShieldsCollected": 1,
  "completedQuestsByKind": { "jumpquest": 1 },
  "perks": [
    { "id": "thickSkin", "progress": 1, "unlocked": true, "active": true }
  ],
  "foundTortoiseIds": ["level1_a"],
  "tortoiseTotal": 6,
  "tortoisePlan": { "Level 1 - Out of the Underworld": 2 }
}

Key Classes & Methods

SaveService (com.csse3200.game.files)

  • save(GameSaveData data), writes the save data to disk
  • load(), reads and returns a GameSaveData from disk, or an empty one if the file cannot be read
  • hasSave(), returns whether a save file exists

LoadService (com.csse3200.game.files)

  • load(Entity player, float mapWidth, float mapHeight, List<UpgradeNode> upgrades), checks that a save exists, reads it, and passes it to apply
  • apply(Entity player, GameSaveData data, float mapWidth, float mapHeight, List<UpgradeNode> upgrades), restores the given data onto the player. It is separate from load so the restore logic can be unit tested without a save file on disk.
  • restorePerks(List<SavedPerk> perks), resets every perk and then restores the saved ones, never activating more than the active perk limit. Called by MainGameScreen before the player is created.

PauseMenuActions (com.csse3200.game.pausemenu)

  • createSaveData(Entity player), builds a GameSaveData from the current game state
  • saveCheckpoint(), saves without leaving the game. Used when entering a new level.
  • The pause menu's Save option saves and then returns to the Main Menu

GameSaveData, the data container written to and read from disk

Field Type Meaning
health, gold int Player health and gold
stamina float Player stamina
items List<SavedItem> Inventory contents
activeSlot int Selected hotbar slot
posX, posY float Player position
level String Map path of the level to load
difficulty String EASY, NORMAL or HARD
lootSeedsByRoom Map<String, Long> One loot seed per visited level
collectedLootIds List<String> Level loot already collected
killedEnemyIds List<String> Enemies already killed
ownedPetNames List<String> Pets the player owns, in slot order
activePetName String The active pet, or null
shopItemSlots List<Integer> Shop listings still in stock, or null in older saves
upgrades List<SavedUpgrade> Active upgrades
shieldHits Integer Remaining shield hits, or null in older saves
shieldHeld, ballisticShieldHeld boolean A shield picked up but not used yet
shieldRemainingMillis, ballisticShieldRemainingMillis long Time left on an active shield, or 0
buffs List<SavedBuff> Active potion buffs
regenHealPerTick int Heal per tick of an active regeneration, or 0
regenRemainingSeconds float Time left on that regeneration
droppedLoot List<SavedLoot> Items lying on the ground
npcs List<SavedNpc> Quest state of every friendly NPC met so far
killedNpcIds List<String> Friendly NPCs that were killed
questJumps, questEnemiesKilled, questGoldSpent, questShieldsCollected int Quest counters
completedQuestsByKind Map<String, Integer> Completed quests per kind
perks List<SavedPerk> Every perk's state, or null in older saves
foundTortoiseIds List<String> Tortoises found so far
tortoiseTotal int How many tortoises this game has, or 0 in older saves
tortoisePlan Map<String, Integer> How many tortoises each level holds

SavedItem, a snapshot of one item

  • int slot, String name, String itemType, int quantity, int maxQuantity
  • Integer sellPrice, null in older saves
  • String weaponType, Integer damage, Integer weaponTier, set only for weapons
  • String consumableType, set only for consumables

SavedUpgrade, a snapshot of one active upgrade

  • String id, int tier, float remainingSeconds, int remainingKills

SavedBuff, a snapshot of one active potion buff

  • String stat, float magnitude, float remainingSeconds

SavedLoot, a snapshot of one item on the ground

  • SavedItem item, float x, float y

SavedNpc, a snapshot of one friendly NPC's quest

  • String id, String dialogueState, int amountXToDo
  • String questType, int amountToDo, float snapshot, describing the active quest

SavedPerk, a snapshot of one perk

  • String id, int progress, boolean unlocked, boolean active

Loot persistence (com.csse3200.game.components.loot)

  • LootId.of(roomName, position), a stable id per loot spawn point
  • LootRegistry, tracks collected loot ids for the current session, loadable from and exportable to save data
  • PersistentLootIdComponent, attached to each loot entity placed by the level. Marks its id collected when the entity is disposed, but only if the loot was actually picked up, so loot left behind when the player changes level still comes back.
  • LootPickupComponent.getItem() and isCollected(), used to find and save loot that was dropped during play. Dropped loot has no PersistentLootIdComponent, which is how it is told apart from level loot.

Enemy persistence (com.csse3200.game.entities.spawn.enemies)

  • EnemyId.of(roomName, position), a stable id per enemy spawn point
  • EnemyRegistry, tracks killed enemy ids for the current session, loadable from and exportable to save data
  • PersistentEnemyIdComponent, attached to each enemy. Marks its id killed when the enemy's death event fires, so an enemy that is only removed because the player left the level is not counted as killed.

NPC and quest persistence (com.csse3200.game.entities.spawn)

  • Each friendly NPC's id is its marker id plus its spawn point, for example <npc id>@Level 1 - Out of the Underworld:8,3
  • NpcQuestRegistry, keeps each NPC's quest state and the killed NPC list. It is loadable from and exportable to save data, and reset() clears it (and the quest counters) for a new game.
  • PersistentNpcComponent, attached to each friendly NPC by LevelGameArea. It gives the NPC its saved quest state when it is created, and marks it killed when its death event fires.
  • QuestLedger (com.csse3200.game.win), counts completed quests per kind. QuestDisplay uses it to refill the completed list in the quest menu after a load.

Perk and tortoise persistence

  • PerkService (com.csse3200.game.perks), holds every perk's state. resetAll() is called for a new game.
  • TortoiseLedger (com.csse3200.game.win), tracks found tortoises and the total
  • TortoisePlan and TortoiseFactory (com.csse3200.game.perks.tortoises), the plan of which levels hold tortoises, and which tortoises were already found so they are not spawned again

Methods added to other systems so they can be saved and restored

  • InventoryComponent.setItem(slot, item), places an item directly into a slot
  • UpgradeNode.getRemainingSeconds(), getRemainingKills() and restore(tier, seconds, kills)
  • PlayerRegenComponent.getRemainingSeconds()
  • ShopComponent.restorePurchasedPets(names) and PetManagerComponent.getActivePetName()
  • ShieldComponent and BallisticShieldComponent: getRemainingMillis(), restoreHeldShield(held) and restoreActive(remainingMillis)
  • Quest: exportCounters(), restoreCounters(...), exportQuest(npcId), restoreQuest(...) and resetAll()
  • DialogueComponent: getQuestState(), getQuestType() and restoreQuestState(state)
  • QuestGiverComponent: getAmountXToDo() and restoreAmountXToDo(amount)
  • PerkService.restorePerk(id, progress, unlocked, active)
  • TortoiseFactory.restoreFound(ids)

Potion buffs need no extra methods. They are read with PlayerBuffComponent.getActiveBuffs() and restored with applyBuff(...). Shop stock uses the shop's existing getItemCatalog() and setItemListing(...).

The quest, NPC and perk restore methods ignore values they do not recognise (an unknown quest state, a negative amount, a perk id that does not exist), so a damaged or edited save does not crash the game.

UML Diagrams

Class Diagram

classDiagram
    class PauseMenuActions {
        +createSaveData(Entity) GameSaveData
        +saveCheckpoint() void
        -save() void
    }
    class SaveService {
        +save(GameSaveData) void
        +load() GameSaveData
        +hasSave() boolean
    }
    class LoadService {
        +load(Entity, float, float, List) void
        +apply(Entity, GameSaveData, float, float, List) void
        +restorePerks(List) void
    }
    class MainGameScreen {
        +MainGameScreen(GdxGame, boolean)
    }
    class GameSaveData {
        +int health
        +float stamina
        +int gold
        +int activeSlot
        +String level
        +String difficulty
        +List items
        +List upgrades
        +List buffs
        +List droppedLoot
        +List npcs
        +List perks
    }
    class SavedItem
    class SavedUpgrade
    class SavedBuff
    class SavedLoot
    class SavedNpc
    class SavedPerk
    class LootRegistry
    class EnemyRegistry
    class NpcQuestRegistry
    class PersistentNpcComponent
    class QuestLedger
    class PerkService
    class TortoiseLedger
    class TortoisePlan
    class UpgradeNode {
        +restore(int, float, int) boolean
    }

    PauseMenuActions ..> GameSaveData : builds
    PauseMenuActions ..> SaveService : saves with
    PauseMenuActions ..> LootRegistry : exports
    PauseMenuActions ..> EnemyRegistry : exports
    PauseMenuActions ..> NpcQuestRegistry : exports
    PauseMenuActions ..> PerkService : exports
    PauseMenuActions ..> TortoiseLedger : exports
    MainGameScreen ..> SaveService : reads level and world state
    MainGameScreen ..> LootRegistry : restores
    MainGameScreen ..> EnemyRegistry : restores
    MainGameScreen ..> NpcQuestRegistry : restores
    MainGameScreen ..> QuestLedger : restores
    MainGameScreen ..> TortoiseLedger : restores
    MainGameScreen ..> TortoisePlan : restores
    MainGameScreen ..> LoadService : restores player and perks
    LoadService ..> SaveService : reads with
    LoadService ..> UpgradeNode : restores
    LoadService ..> PerkService : restores
    PersistentNpcComponent ..> NpcQuestRegistry : gets and records NPC state
    SaveService ..> GameSaveData : reads and writes
    GameSaveData ..> SavedItem : contains
    GameSaveData ..> SavedUpgrade : contains
    GameSaveData ..> SavedBuff : contains
    GameSaveData ..> SavedLoot : contains
    GameSaveData ..> SavedNpc : contains
    GameSaveData ..> SavedPerk : contains
    SavedLoot ..> SavedItem : contains
Loading

Sequence Diagram: Saving

sequenceDiagram
    actor Player
    participant PauseMenu as PauseMenuDisplay
    participant Actions as PauseMenuActions
    participant Save as SaveService
    participant File as save.json

    Player->>PauseMenu: choose Save
    PauseMenu->>Actions: saveClicked event
    Actions->>Actions: createSaveData(player)
    Actions->>Save: save(data)
    Save->>File: write JSON
    Actions->>Actions: unpause, then exit to Main Menu
Loading

Sequence Diagram: Loading

sequenceDiagram
    actor Player
    participant Menu as Main Menu
    participant Screen as MainGameScreen
    participant Save as SaveService
    participant Area as LevelGameArea
    participant Load as LoadService

    Player->>Menu: choose Load
    Menu->>Screen: new MainGameScreen(game, true)
    Screen->>Save: hasSave()
    Screen->>Save: load()
    Screen->>Screen: restore loot, enemy and NPC registries, quests, tortoises
    Screen->>Load: restorePerks(perks)
    Screen->>Screen: restore loot seeds, difficulty, level
    Screen->>Area: create saved level, NPCs and new player
    Screen->>Load: load(player, mapWidth, mapHeight, upgrades)
    Load->>Save: load()
    Load->>Load: apply(player, data, ...)
Loading

Where It's Triggered

  • Save, PauseMenuActions.save(), wired to the Save option in the pause menu. It saves, unpauses, then returns to the Main Menu.
  • Checkpoint save, PauseMenuActions.saveCheckpoint(), called by MainGameScreen.transitionTo(...) each time the player enters a new level through a door. The debug tp terminal command moves between levels without saving.
  • Load, the Main Menu's Load button creates MainGameScreen with loadsave set to true. The screen restores world state before building the level, then calls LoadService.load(...) for the player.
  • Start and Restart, create MainGameScreen with loadsave set to false. Nothing is read from the save file.
  • Death, "Try Again" on the death screen lets the player pick a perk and then revives them at the start of Level 1 (see Dying and Try Again). It does not read or write the save file.
  • Death and win screens, the pause menu cannot be opened while either screen is showing, so the game cannot be saved while the player is dead. KeyboardPauseInput.setPauseBlocked(...) ignores the pause key while MainGameScreen reports one of these screens as visible.
  • Main Menu, leaving to the Main Menu from the pause menu does not write to the save file. Only Save and the level checkpoint do.

Testing

Unit tests live in source/core/src/test/com/csse3200/game/.

Test class What it covers
files/SaveServiceTest Writing, reading and replacing the save file, and a missing or broken save file
files/GameSaveDataTest Default values, older saves missing newer fields, and a JSON round trip of the newer fields
files/LoadServiceTest Restoring stats, position, inventory slots, selected slot, weapon tier, sell price, potion stacks, pets, upgrades, shield hits, buffs, regeneration and dropped loot, plus invalid data and older saves
files/LoadServiceItemRestoreTest Rebuilding items: potion tiers, weapons without a tier or damage, unknown types and damaged entries
files/LoadServiceShopStockTest Restoring shop stock, older saves without it, and number types read back from JSON
files/LoadServicePerkRestoreTest restorePerks: reset, restore, the active perk limit, and invalid entries
pausemenu/PauseMenuActionsTest Everything written by createSaveData, and a full save then load round trip onto a new player
pausemenu/PauseMenuActionsSaveFieldsTest createSaveData writing shields, shop stock, perks, NPC quests, quest counters and tortoises
components/player/ShieldSaveLoadTest Held and active shields of both types, saved as time left and restored on a new player
components/loot/LootRegistryTest Marking, exporting and loading collected loot
components/loot/LootIdTest Loot id format and uniqueness
components/loot/PersistentLootIdComponentTest Loot is only marked collected when it was actually picked up
upgrades/UpgradeNodeRestoreTest UpgradeNode.restore, including rejected values and normal expiry afterwards
Quests/QuestSaveLoadTest Exporting and restoring quest counters and active quests, and rejecting invalid values
entities/spawn/NpcQuestRegistryTest Saving and restoring NPC quest state and killed NPCs, keeping quests after Try Again, invalid saved data, and resetting for a new game
perks/tortoises/TortoiseFactoryRestoreTest Restoring which tortoises were found

Run every save/load test from the source folder:

./gradlew :core:test --tests "*SaveService*" --tests "*GameSaveData*" --tests "*LoadService*" --tests "*PauseMenuActions*" --tests "*ShieldSaveLoad*" --tests "*LootRegistry*" --tests "*LootId*" --tests "*PersistentLootId*" --tests "*UpgradeNodeRestore*" --tests "*QuestSaveLoad*" --tests "*NpcQuestRegistry*" --tests "*TortoiseFactoryRestore*"

None of these tests read or write the real save file. SaveServiceTest writes to a temporary folder that is deleted after each test, and the rest build their save data in memory. Tests that use shared static state (perks, quests, NPCs, tortoises, the loot registry) reset it before and after each test.

The MainGameScreen wiring needs the running game and is checked manually: save from the pause menu, close the game completely, reopen it and choose Load. To check loot placement, search the console for Loot seed for. A level should log the same seed every time it is built in the same game, and after a load.

Adding a New System to Save/Load

  1. Add a field to GameSaveData. Give it a sensible default so older saves still load, or make it null and skip it on load if there is no safe default.
  2. In PauseMenuActions.createSaveData, read the value from the system and store it in the field.
  3. In LoadService, add a private loadXxx(...) method that puts the value back, and call it from apply. Place it after anything it depends on.
  4. If the value must be known before the level or player is built (as with difficulty, the registries and perks), restore it in the MainGameScreen constructor instead.
  5. If the system keeps static state, clear it in the new game branch of MainGameScreen, or a new game will keep the previous game's state.
  6. Prefer calling the system's existing public methods. If a new method is needed, add one that only reads or restores state, ignores invalid values, and tell the owning team.
  7. Add tests to LoadServiceTest and PauseMenuActionsTest.

Known Limitations

  • One save slot. Saving always replaces the previous save.
  • Dropped loot does not record its level. Items on the ground are saved with a position only, so on load they are placed in whichever level is being loaded, if the position fits inside it. For example, loot dropped on death in Level 2 and saved after Try Again comes back in Level 1.

Clone this wiki locally