179 lines
7.2 KiB
GDScript3
179 lines
7.2 KiB
GDScript3
|
|
extends SceneTree
|
||
|
|
|
||
|
|
# Forgejo #225: the run survives being written to disk and read back.
|
||
|
|
#
|
||
|
|
# The interesting part is not "does it write a file" but whether the things
|
||
|
|
# JSON cannot hold survive: Vector2 positions, chunk coordinates used as
|
||
|
|
# dictionary KEYS, and Inventory objects nested inside the buildings array.
|
||
|
|
# Those are what a naive save silently flattens.
|
||
|
|
#
|
||
|
|
# Covers:
|
||
|
|
# A. a rich run round-trips field for field
|
||
|
|
# B. Vector2i dictionary keys come back as Vector2i, not as strings
|
||
|
|
# C. inventories come back with their contents AND their capacity
|
||
|
|
# D. a save from a newer build is refused, with a reason
|
||
|
|
# E. a damaged file is refused rather than half-applied
|
||
|
|
# F. a field the save predates keeps its new-game value
|
||
|
|
|
||
|
|
# Mirrored rather than read off SaveGame: a bare ClassName.CONST in a
|
||
|
|
# SceneTree script has broken method resolution for a whole file here before.
|
||
|
|
const TEST_PATH: String = "user://test_savegame.json"
|
||
|
|
|
||
|
|
var game_state: Node
|
||
|
|
|
||
|
|
|
||
|
|
func _initialize() -> void:
|
||
|
|
game_state = root.get_node("GameState")
|
||
|
|
|
||
|
|
|
||
|
|
func _process(_delta: float) -> bool:
|
||
|
|
game_state.buildings.clear()
|
||
|
|
game_state.extractors.clear()
|
||
|
|
game_state.reset_progression()
|
||
|
|
game_state.reset_new_game()
|
||
|
|
game_state.reset_quests()
|
||
|
|
|
||
|
|
# --- a run with something in every awkward corner -------------------
|
||
|
|
game_state.research_points = 42
|
||
|
|
game_state.unlocked = ["shipyards", "coordination"]
|
||
|
|
game_state.rocks = 1234
|
||
|
|
game_state.fleet_position = Vector2(987.5, -321.25)
|
||
|
|
game_state.wave_number = 7
|
||
|
|
game_state.quest_index = 5
|
||
|
|
game_state.explored_chunks[Vector2i(3, -4)] = true
|
||
|
|
game_state.explored_chunks[Vector2i(-11, 0)] = true
|
||
|
|
game_state.visited_planets["some-planet-id"] = true
|
||
|
|
var b_id: int = game_state.add_building(GameState.BUILDING_TRANSPORT_HUB, Vector2(600.0, -150.0))
|
||
|
|
var record: Dictionary = game_state.find_building(b_id)
|
||
|
|
record.inventory.total_capacity = 88
|
||
|
|
record.inventory.set_amount(GameState.RESOURCE_ORE, 17)
|
||
|
|
game_state.fleet_inventory.set_amount(GameState.RESOURCE_GAS, 9)
|
||
|
|
|
||
|
|
var before: Dictionary = _snapshot()
|
||
|
|
|
||
|
|
if not SaveGame.save(game_state, TEST_PATH):
|
||
|
|
return _fail("the run would not save")
|
||
|
|
if not SaveGame.has_save(TEST_PATH):
|
||
|
|
return _fail("saved, but has_save() cannot see the file")
|
||
|
|
|
||
|
|
# Wipe it back to a fresh game, so a load that does nothing cannot pass.
|
||
|
|
game_state.buildings.clear()
|
||
|
|
game_state.extractors.clear()
|
||
|
|
game_state.reset_progression()
|
||
|
|
game_state.reset_new_game()
|
||
|
|
game_state.reset_quests()
|
||
|
|
game_state.explored_chunks.clear()
|
||
|
|
if _snapshot() == before:
|
||
|
|
return _fail("wiping the run changed nothing, so a load would prove nothing")
|
||
|
|
|
||
|
|
var result: Dictionary = SaveGame.load_into(game_state, TEST_PATH)
|
||
|
|
if not bool(result.get("ok", false)):
|
||
|
|
return _fail("the run would not load back: %s" % result.get("reason", ""))
|
||
|
|
|
||
|
|
# --- A. field for field ---------------------------------------------
|
||
|
|
var after: Dictionary = _snapshot()
|
||
|
|
for key in before:
|
||
|
|
if after.get(key) != before[key]:
|
||
|
|
return _fail("'%s' did not survive the round trip: %s -> %s" \
|
||
|
|
% [key, before[key], after.get(key)])
|
||
|
|
print("a run with %d fields round-tripped intact" % before.size())
|
||
|
|
|
||
|
|
# --- B. chunk coordinates are still coordinates ----------------------
|
||
|
|
if not game_state.explored_chunks.has(Vector2i(3, -4)):
|
||
|
|
return _fail("chunk keys came back as something other than Vector2i: %s" \
|
||
|
|
% str(game_state.explored_chunks.keys()))
|
||
|
|
print("explored chunks came back keyed by Vector2i")
|
||
|
|
|
||
|
|
# --- C. inventories keep contents and capacity -----------------------
|
||
|
|
var loaded: Dictionary = game_state.find_building(b_id)
|
||
|
|
if loaded.is_empty():
|
||
|
|
return _fail("the transport hub did not come back")
|
||
|
|
if int(loaded.inventory.amount(GameState.RESOURCE_ORE)) != 17:
|
||
|
|
return _fail("the hub's ore did not survive: %d" % loaded.inventory.amount(GameState.RESOURCE_ORE))
|
||
|
|
if int(loaded.inventory.total_capacity) != 88:
|
||
|
|
return _fail("the hub's capacity did not survive: %d" % loaded.inventory.total_capacity)
|
||
|
|
if int(game_state.fleet_inventory.amount(GameState.RESOURCE_GAS)) != 9:
|
||
|
|
return _fail("the fleet hold's gas did not survive")
|
||
|
|
print("inventories came back with their contents and their capacity")
|
||
|
|
|
||
|
|
# --- D. a save from a newer build is refused --------------------------
|
||
|
|
_rewrite_version(999)
|
||
|
|
var newer: Dictionary = SaveGame.load_into(game_state, TEST_PATH)
|
||
|
|
if bool(newer.get("ok", false)):
|
||
|
|
return _fail("a save from a newer build loaded anyway")
|
||
|
|
if not String(newer.get("reason", "")).to_lower().contains("newer"):
|
||
|
|
return _fail("a newer save was refused without saying why: '%s'" % newer.get("reason", ""))
|
||
|
|
print("a newer save is refused: %s" % newer.get("reason", ""))
|
||
|
|
|
||
|
|
# --- E. a damaged file is refused, not half-applied -------------------
|
||
|
|
var f: FileAccess = FileAccess.open(TEST_PATH, FileAccess.WRITE)
|
||
|
|
f.store_string("{ this is not json")
|
||
|
|
f.close()
|
||
|
|
var marker: int = game_state.research_points
|
||
|
|
var damaged: Dictionary = SaveGame.load_into(game_state, TEST_PATH)
|
||
|
|
if bool(damaged.get("ok", false)):
|
||
|
|
return _fail("a damaged save reported success")
|
||
|
|
if game_state.research_points != marker:
|
||
|
|
return _fail("a damaged save was partly applied before it failed")
|
||
|
|
print("a damaged save is refused: %s" % damaged.get("reason", ""))
|
||
|
|
|
||
|
|
# --- F. a field the save predates keeps its new-game value ------------
|
||
|
|
# Adding progression must not invalidate every existing save, so a
|
||
|
|
# missing field is left alone rather than cleared.
|
||
|
|
SaveGame.save(game_state, TEST_PATH)
|
||
|
|
_drop_field("research_points")
|
||
|
|
game_state.research_points = 77
|
||
|
|
var partial: Dictionary = SaveGame.load_into(game_state, TEST_PATH)
|
||
|
|
if not bool(partial.get("ok", false)):
|
||
|
|
return _fail("a save missing one field was refused entirely: %s" % partial.get("reason", ""))
|
||
|
|
if game_state.research_points != 77:
|
||
|
|
return _fail("a missing field was cleared instead of left alone (%d)" % game_state.research_points)
|
||
|
|
print("a field the save predates keeps its current value")
|
||
|
|
|
||
|
|
SaveGame.clear(TEST_PATH)
|
||
|
|
print("TEST_SAVE_GAME: PASS")
|
||
|
|
quit(0)
|
||
|
|
return true
|
||
|
|
|
||
|
|
|
||
|
|
# The fields this test compares. Deliberately a hand-picked spread rather
|
||
|
|
# than SaveGame.FIELDS: comparing the list against itself would pass even if
|
||
|
|
# the codec dropped everything.
|
||
|
|
func _snapshot() -> Dictionary:
|
||
|
|
return {
|
||
|
|
"research_points": game_state.research_points,
|
||
|
|
"unlocked": game_state.unlocked.duplicate(),
|
||
|
|
"rocks": game_state.rocks,
|
||
|
|
"fleet_position": game_state.fleet_position,
|
||
|
|
"wave_number": game_state.wave_number,
|
||
|
|
"quest_index": game_state.quest_index,
|
||
|
|
"building_count": game_state.buildings.size(),
|
||
|
|
"chunk_count": game_state.explored_chunks.size(),
|
||
|
|
"visited": game_state.visited_planets.size(),
|
||
|
|
}
|
||
|
|
|
||
|
|
|
||
|
|
func _rewrite_version(to: int) -> void:
|
||
|
|
var file: FileAccess = FileAccess.open(TEST_PATH, FileAccess.READ)
|
||
|
|
var data: Dictionary = JSON.parse_string(file.get_as_text())
|
||
|
|
file.close()
|
||
|
|
data["version"] = to
|
||
|
|
file = FileAccess.open(TEST_PATH, FileAccess.WRITE)
|
||
|
|
file.store_string(JSON.stringify(data))
|
||
|
|
file.close()
|
||
|
|
|
||
|
|
|
||
|
|
func _drop_field(name: String) -> void:
|
||
|
|
var file: FileAccess = FileAccess.open(TEST_PATH, FileAccess.READ)
|
||
|
|
var data: Dictionary = JSON.parse_string(file.get_as_text())
|
||
|
|
file.close()
|
||
|
|
data["fields"].erase(name)
|
||
|
|
file = FileAccess.open(TEST_PATH, FileAccess.WRITE)
|
||
|
|
file.store_string(JSON.stringify(data))
|
||
|
|
file.close()
|
||
|
|
|
||
|
|
|
||
|
|
func _fail(message: String) -> bool:
|
||
|
|
print("TEST_SAVE_GAME: FAIL (%s)" % message)
|
||
|
|
quit(1)
|
||
|
|
return true
|