Refactor save manager validation and update state structure
- Updated the save manager to enforce a new format version (2) and improved validation checks for the save data structure. - Enhanced the `validate_data` function to ensure all required fields are present and valid. - Adjusted the handling of legacy saves and introduced new checks for various state fields. - Improved number formatting in the global script for better readability of large numbers. - Updated tests to reflect changes in the save manager and validation logic, ensuring compatibility with the new state structure. - Added new tests for group indexing and production transactions to ensure correctness of entity management and resource production.
This commit is contained in:
+60
-75
@@ -5,7 +5,7 @@ extends Node
|
||||
const SETTING_SECTION := "Settings"
|
||||
const SAVE_PATH = "user://settings.cfg"
|
||||
|
||||
const FORMAT_VERSION: int = 1
|
||||
const FORMAT_VERSION: int = 2
|
||||
const SimulationTime = preload("res://world/owniverse/simulation_time.gd")
|
||||
const STATE_VERSION: String = SimulationTime.STATE_VERSION
|
||||
var last_error: String = ""
|
||||
@@ -113,44 +113,44 @@ func _plain_tree(value: Variant) -> bool:
|
||||
|
||||
## Return a diagnostic path on failure; an empty string means the structure is valid.
|
||||
func validate_data(data: Variant) -> String:
|
||||
if not data is Dictionary or not data.get("state") is Dictionary:
|
||||
if not data is Dictionary or not _plain_tree(data):
|
||||
return "save must contain plain finite values"
|
||||
if data.get("format_version") != FORMAT_VERSION or not data.get("app_version") is String:
|
||||
return "unsupported save format"
|
||||
if not data.get("ui") is Dictionary or not data.ui.get("current_page") is int or data.ui.current_page < 0:
|
||||
return "invalid ui.current_page"
|
||||
if not data.get("state") is Dictionary:
|
||||
return "state must be a Dictionary"
|
||||
if not _plain_tree(data):
|
||||
return "unsupported value or non-finite number"
|
||||
# Legacy files contained only {state}; accept their known state schema.
|
||||
if data.has("format_version") and (not data.format_version is int or data.format_version != FORMAT_VERSION):
|
||||
return "unsupported format_version"
|
||||
if data.has("ui"):
|
||||
if not data.ui is Dictionary or not data.ui.get("current_page") is int or data.ui.current_page < 0:
|
||||
return "invalid ui.current_page"
|
||||
var saved: Dictionary = data.state
|
||||
if saved.get("version") not in [STATE_VERSION, SimulationTime.LEGACY_VERSION]:
|
||||
var required := [
|
||||
"version", "time", "speed", "speed_in_year", "energy", "batches", "batch_origins",
|
||||
"binds", "amount", "distribution", "auto_resources", "auto_star_deficits",
|
||||
"production_remainders", "resource_remainders", "evolution_remainders", "life_remainders",
|
||||
"planet_remainders", "blast_remainders", "destruction_remainders", "civilization_remainders",
|
||||
"civilization_stats", "civilization_homes", "ethics", "stats",
|
||||
]
|
||||
if saved.size() != required.size():
|
||||
return "unsupported state structure"
|
||||
for field in required:
|
||||
if not saved.has(field):
|
||||
return "missing state." + field
|
||||
if saved.version != STATE_VERSION:
|
||||
return "unsupported state.version"
|
||||
if not saved.get("time") is Dictionary:
|
||||
return "state.time must be a Dictionary"
|
||||
if not _number(saved.time.get("datetime")) or not _number(saved.time.get("year")):
|
||||
if not saved.time is Dictionary or not _number(saved.time.get("datetime")) or not _number(saved.time.get("year")):
|
||||
return "invalid state.time"
|
||||
if not is_finite(float(saved.time.datetime) * 10.0):
|
||||
return "state.time exceeds supported precision range"
|
||||
var expected_year: float = SimulationTime.processed_year(saved.time.datetime)
|
||||
if saved.version == SimulationTime.LEGACY_VERSION:
|
||||
expected_year = floorf(saved.time.datetime)
|
||||
if saved.time.year != expected_year:
|
||||
return "state.time.year does not match the processed time for this version"
|
||||
if not _number(saved.get("speed"), true) or saved.speed < 1 or saved.speed > Global.SPEED_LIMIT:
|
||||
if not is_finite(float(saved.time.datetime) * 10.0) or saved.time.year != SimulationTime.processed_year(saved.time.datetime):
|
||||
return "invalid processed time"
|
||||
if not _number(saved.speed, true) or saved.speed < 1 or saved.speed > Global.SPEED_LIMIT:
|
||||
return "invalid state.speed"
|
||||
if not _number(saved.get("speed_in_year")) or saved.speed_in_year != pow(10.0, saved.speed - 1):
|
||||
if not _number(saved.speed_in_year) or saved.speed_in_year != pow(10.0, saved.speed - 1):
|
||||
return "invalid state.speed_in_year"
|
||||
if not _number(saved.get("energy"), true):
|
||||
return "invalid state.energy"
|
||||
if not _number_map(saved.get("amount"), true):
|
||||
return "invalid state.amount"
|
||||
if not saved.get("stats") is Dictionary or not _number_map(saved.stats.get("total_amount"), true):
|
||||
if not _number(saved.energy, true) or not _number_map(saved.amount, true):
|
||||
return "invalid state resources"
|
||||
if not saved.stats is Dictionary or not _number_map(saved.stats.get("total_amount"), true):
|
||||
return "invalid state.stats.total_amount"
|
||||
for id in saved.amount:
|
||||
if not saved.stats.total_amount.has(id):
|
||||
return "missing total_amount for " + id
|
||||
if not saved.get("binds") is Dictionary or not saved.get("batches") is Dictionary:
|
||||
if saved.amount.keys() != saved.stats.total_amount.keys():
|
||||
return "amount and total_amount keys differ"
|
||||
if not saved.binds is Dictionary or not saved.batches is Dictionary:
|
||||
return "state.binds and state.batches must be Dictionaries"
|
||||
for owner_id in saved.binds:
|
||||
if not saved.amount.has(owner_id) or not _number_map(saved.binds[owner_id], true):
|
||||
@@ -162,57 +162,42 @@ func validate_data(data: Variant) -> String:
|
||||
if not saved.amount.has(id) or not saved.batches[id] is Array:
|
||||
return "invalid state.batches." + id
|
||||
for batch in saved.batches[id]:
|
||||
if not batch is Dictionary:
|
||||
return "batch must be a Dictionary"
|
||||
if not _number(batch.get("amount"), true) or not _number(batch.get("start_year")) or not _number(batch.get("end_year")):
|
||||
if not batch is Dictionary or not _number(batch.get("amount"), true) or not _number(batch.get("start_year")) or not _number(batch.get("end_year")):
|
||||
return "invalid batch in " + id
|
||||
if batch.end_year < batch.start_year:
|
||||
return "reversed batch interval in " + id
|
||||
if batch.has("created_from") or batch.has("created_to"):
|
||||
if not _number(batch.get("created_from")) or not _number(batch.get("created_to")):
|
||||
return "invalid batch creation interval in " + id
|
||||
if batch.created_to <= batch.created_from or batch.start_year < batch.created_from:
|
||||
return "reversed batch creation interval in " + id
|
||||
if batch.has("processed_until"):
|
||||
if not _number(batch.processed_until) or batch.processed_until < batch.start_year or batch.processed_until > batch.end_year:
|
||||
return "invalid batch.processed_until in " + id
|
||||
if batch.has("created_from") != batch.has("created_to"):
|
||||
return "incomplete batch creation interval in " + id
|
||||
if batch.has("created_from") and (not _number(batch.created_from) or not _number(batch.created_to) or batch.created_to <= batch.created_from or batch.start_year < batch.created_from):
|
||||
return "invalid batch creation interval in " + id
|
||||
if batch.has("processed_until") and (not _number(batch.processed_until) or batch.processed_until < batch.start_year or batch.processed_until > batch.end_year):
|
||||
return "invalid batch.processed_until in " + id
|
||||
if batch.has("withdraw_remainder") and (not _number(batch.withdraw_remainder) or batch.withdraw_remainder > batch.amount):
|
||||
return "invalid batch.withdraw_remainder in " + id
|
||||
for field in ["batch_origins", "distribution", "auto_resources", "production_remainders", "auto_star_deficits", "evolution_remainders", "planet_remainders", "blast_remainders", "destruction_remainders"]:
|
||||
if saved.has(field) and not _number_map(saved[field], field == "auto_resources", field != "auto_star_deficits"):
|
||||
for field in ["batch_origins", "distribution", "production_remainders", "evolution_remainders", "planet_remainders", "blast_remainders", "destruction_remainders", "life_remainders", "civilization_remainders", "civilization_stats"]:
|
||||
if not _number_map(saved[field], false, false):
|
||||
return "invalid state." + field
|
||||
if saved.has("ethics"):
|
||||
if not saved.ethics is Dictionary:
|
||||
return "invalid state.ethics"
|
||||
for id in saved.ethics:
|
||||
if not Global.CivilizationRules.has(id) or not _number_map(saved.ethics[id], true):
|
||||
return "invalid state.ethics." + str(id)
|
||||
for ethic in saved.ethics[id]:
|
||||
if ethic not in ["m", "l", "w", "d", "c", "b"]:
|
||||
return "invalid ethic " + str(ethic)
|
||||
if saved.has("life_version") and saved.life_version not in [1, 2, 3]:
|
||||
return "invalid state.life_version"
|
||||
if saved.has("civilization_remainders") and not _number_map(saved.civilization_remainders, false, false):
|
||||
return "invalid state.civilization_remainders"
|
||||
if saved.has("civilization_stats") and not _number_map(saved.civilization_stats):
|
||||
return "invalid state.civilization_stats"
|
||||
if saved.has("civilization_homes"):
|
||||
if not saved.civilization_homes is Dictionary:
|
||||
return "invalid state.civilization_homes"
|
||||
for id in saved.civilization_homes:
|
||||
if not _number_map(saved.civilization_homes[id], true):
|
||||
return "invalid civilization homes"
|
||||
if saved.has("life_remainders") and not _number_map(saved.life_remainders, false, false):
|
||||
return "invalid state.life_remainders"
|
||||
if saved.has("resource_remainders"):
|
||||
if not saved.resource_remainders is Dictionary:
|
||||
return "invalid state.resource_remainders"
|
||||
for channel in ["basic", "automatic"]:
|
||||
# Signed allocation errors are valid, even though resource balances are whole.
|
||||
if not _number_map(saved.resource_remainders.get(channel), false, false):
|
||||
return "invalid state.resource_remainders." + channel
|
||||
if not _number_map(saved.auto_resources, true) or not _number_map(saved.auto_star_deficits, false, false):
|
||||
return "invalid automatic production state"
|
||||
if not saved.resource_remainders is Dictionary or saved.resource_remainders.keys().size() != 2 or not saved.resource_remainders.has("basic") or not saved.resource_remainders.has("automatic"):
|
||||
return "invalid state.resource_remainders"
|
||||
for channel in ["basic", "automatic"]:
|
||||
if not _number_map(saved.resource_remainders[channel], false, false):
|
||||
return "invalid state.resource_remainders." + channel
|
||||
if not saved.ethics is Dictionary or not saved.civilization_homes is Dictionary:
|
||||
return "invalid civilization state"
|
||||
for id in saved.ethics:
|
||||
if not Global.CivilizationRules.has(id) or not _number_map(saved.ethics[id], true):
|
||||
return "invalid state.ethics." + str(id)
|
||||
for ethic in saved.ethics[id]:
|
||||
if ethic not in ["m", "l", "w", "d", "c", "b"]:
|
||||
return "invalid ethic " + str(ethic)
|
||||
for id in saved.civilization_homes:
|
||||
if not _number_map(saved.civilization_homes[id], true):
|
||||
return "invalid civilization homes"
|
||||
return ""
|
||||
|
||||
|
||||
#endregion
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user