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:
@@ -10,6 +10,9 @@ var entities: Dictionary[String, OwniverseEntity] = {}
|
||||
var entity_groups: Dictionary[String, Array] = {}
|
||||
var entity_event_groups: Dictionary[String, Array] = {}
|
||||
|
||||
var _group_members: Dictionary = {}
|
||||
var _indexed_entity_count: int = -1
|
||||
|
||||
var items_to_update: Dictionary[String, float] = {}
|
||||
|
||||
var auto_production: Array = []
|
||||
@@ -32,6 +35,18 @@ var state: Dictionary = {
|
||||
'amount': {},
|
||||
'distribution': {},
|
||||
'auto_resources': {},
|
||||
'auto_star_deficits': {},
|
||||
'production_remainders': {},
|
||||
'resource_remainders': {'basic': {}, 'automatic': {}},
|
||||
'evolution_remainders': {},
|
||||
'life_remainders': {},
|
||||
'planet_remainders': {},
|
||||
'blast_remainders': {},
|
||||
'destruction_remainders': {},
|
||||
'civilization_remainders': {},
|
||||
'civilization_stats': {},
|
||||
'civilization_homes': {},
|
||||
'ethics': {},
|
||||
'stats': {
|
||||
'total_amount': {},
|
||||
}
|
||||
@@ -57,19 +72,9 @@ func evolute(delta: float) -> void:
|
||||
_evolute_to_datetime(datetime)
|
||||
|
||||
|
||||
## Legacy saves may have up to 0.9 unprocessed years. Process them exactly once,
|
||||
## without changing accumulated time or discarding production/evolution.
|
||||
func migrate_time_precision() -> void:
|
||||
if state.version == SimulationTime.LEGACY_VERSION:
|
||||
_evolute_to_datetime(float(state.time.datetime))
|
||||
|
||||
|
||||
# Old saves have counts but no life ages. Start only missing cohorts at load time.
|
||||
func initialize_life_batches() -> void:
|
||||
CivilizationState.detach_space_civilizations(state, Global.CivilizationRules)
|
||||
CivilizationState.enforce_capacity(self, state, Global.CivilizationRules, state.time.datetime)
|
||||
if state.get("life_version", 0) >= 3:
|
||||
return
|
||||
var physical: Dictionary = _physical_amounts(state)
|
||||
for id in entities:
|
||||
if entities[id].life_rule.is_empty():
|
||||
@@ -78,19 +83,22 @@ func initialize_life_batches() -> void:
|
||||
for batch: Dictionary in state.batches.get(id, []):
|
||||
tracked += batch.amount
|
||||
_append_batch(state, id, maxf(0.0, physical.get(id, 0.0) - tracked), state.time.datetime, state.time.datetime)
|
||||
state.life_version = 3
|
||||
new_state = state
|
||||
|
||||
|
||||
func _evolute_to_datetime(datetime: float) -> void:
|
||||
initialize_life_batches()
|
||||
# Reconcile only when simulation advances.
|
||||
# Fractional frame updates cannot change capacity or populations.
|
||||
var processed_year: float = SimulationTime.processed_year(datetime)
|
||||
if processed_year > state.time.year:
|
||||
initialize_life_batches()
|
||||
var previous: Dictionary = state
|
||||
var next: Dictionary = previous.duplicate_deep()
|
||||
next.version = SimulationTime.STATE_VERSION
|
||||
next.speed = clampi(int(previous.speed), 1, Global.SPEED_LIMIT)
|
||||
next.speed_in_year = pow(10.0, next.speed - 1)
|
||||
var from_year: float = previous.time.year
|
||||
var to_year: float = SimulationTime.processed_year(datetime)
|
||||
var to_year: float = processed_year
|
||||
if to_year > from_year:
|
||||
var quantities: Dictionary = _physical_amounts(previous)
|
||||
var distribution: Dictionary = _normalize_distribution(quantities)
|
||||
@@ -147,13 +155,26 @@ func _add_time_delta(delta: float) -> Dictionary:
|
||||
return {"datetime": new_datetime, "year": SimulationTime.processed_year(new_datetime)}
|
||||
|
||||
|
||||
## Rebuild after changing group_as or replacing definitions without changing their count.
|
||||
## Adding/removing definitions is detected automatically during initialization.
|
||||
func rebuild_group_index() -> void:
|
||||
_group_members.clear()
|
||||
for id in entities:
|
||||
var group: String = entities[id].group_as
|
||||
if not _group_members.has(group):
|
||||
_group_members[group] = []
|
||||
_group_members[group].append(id)
|
||||
_indexed_entity_count = entities.size()
|
||||
|
||||
|
||||
func _ensure_group_index() -> void:
|
||||
if _indexed_entity_count != entities.size():
|
||||
rebuild_group_index()
|
||||
|
||||
|
||||
func _is_group(id: String) -> bool:
|
||||
if entity_groups.has(id):
|
||||
return true
|
||||
for entity: OwniverseEntity in entities.values():
|
||||
if entity.group_as == id:
|
||||
return true
|
||||
return false
|
||||
_ensure_group_index()
|
||||
return entity_groups.has(id) or _group_members.has(id)
|
||||
|
||||
|
||||
func _physical_amounts(source: Dictionary) -> Dictionary:
|
||||
@@ -171,11 +192,8 @@ func _physical_amounts(source: Dictionary) -> Dictionary:
|
||||
func _members(id: String) -> Array:
|
||||
if not _is_group(id):
|
||||
return [id]
|
||||
var members: Array = []
|
||||
for key in entities:
|
||||
if entities[key].group_as == id:
|
||||
members.append(key)
|
||||
return members
|
||||
# Callers may modify the returned array without changing the index.
|
||||
return _group_members.get(id, []).duplicate()
|
||||
|
||||
|
||||
func _available(source: Dictionary, id: String) -> float:
|
||||
@@ -342,6 +360,12 @@ func _append_batch(target: Dictionary, id: String, amount: float,
|
||||
var window_index: float = floorf((cursor - origin) / batch_duration)
|
||||
if origin + (window_index + 1.0) * batch_duration <= cursor:
|
||||
window_index += 1.0
|
||||
var batches_by_start: Dictionary = {}
|
||||
for existing: Dictionary in batches:
|
||||
var start: float = existing.get("created_from", -INF)
|
||||
if not batches_by_start.has(start):
|
||||
batches_by_start[start] = []
|
||||
batches_by_start[start].append(existing)
|
||||
while true:
|
||||
var window_start: float = origin + window_index * batch_duration
|
||||
var window_end: float = origin + (window_index + 1.0) * batch_duration
|
||||
@@ -353,7 +377,7 @@ func _append_batch(target: Dictionary, id: String, amount: float,
|
||||
if duration > 0.0 and part_end < created_to:
|
||||
part_amount = floorf(amount * (part_end - created_from) / duration) - (amount - remaining_amount)
|
||||
var batch: Dictionary = {}
|
||||
for existing: Dictionary in batches:
|
||||
for existing: Dictionary in batches_by_start.get(window_start, []):
|
||||
if existing.get("created_from", -INF) == window_start and existing.start_year == window_start + lifespan and existing.end_year == window_end + lifespan:
|
||||
batch = existing
|
||||
break
|
||||
@@ -366,6 +390,9 @@ func _append_batch(target: Dictionary, id: String, amount: float,
|
||||
"amount": 0.0,
|
||||
}
|
||||
batches.append(batch)
|
||||
if not batches_by_start.has(window_start):
|
||||
batches_by_start[window_start] = []
|
||||
batches_by_start[window_start].append(batch)
|
||||
batch.amount += part_amount
|
||||
remaining_amount -= part_amount
|
||||
if duration == 0.0 or part_end >= created_to:
|
||||
@@ -548,12 +575,13 @@ func _produce_newborn_lifetime(target: Dictionary, id: String, count: float,
|
||||
if products.is_empty() or count <= 0.0:
|
||||
return
|
||||
var object_years: float = count * maxf(0.0, to_year - (born_from + born_to) * 0.5)
|
||||
var previous_amounts: Dictionary = {}
|
||||
for old: Dictionary in before_batches:
|
||||
var start: float = old.get("created_from", -INF)
|
||||
if not previous_amounts.has(start):
|
||||
previous_amounts[start] = old.amount
|
||||
for batch: Dictionary in target.batches.get(id, []):
|
||||
var added: float = batch.amount
|
||||
for old: Dictionary in before_batches:
|
||||
if old.get("created_from", -INF) == batch.get("created_from", INF):
|
||||
added -= old.amount
|
||||
break
|
||||
var added: float = batch.amount - previous_amounts.get(batch.get("created_from", INF), 0.0)
|
||||
object_years -= maxf(0.0, added) * _expired_years(to_year, batch.start_year, batch.end_year)
|
||||
object_years = maxf(0.0, object_years)
|
||||
for resource in products:
|
||||
@@ -623,7 +651,10 @@ func _evolve_batches(target: Dictionary, to_year: float) -> void:
|
||||
func _evolve_batch_generation(target: Dictionary, to_year: float) -> bool:
|
||||
var quantities: Dictionary = _physical_amounts(target)
|
||||
var expired: Dictionary = {}
|
||||
var preview: Dictionary = target.duplicate(true)
|
||||
var preview: Dictionary = {"batches": {}}
|
||||
for id in target.batches:
|
||||
if entities.has(id) and entities[id].destroy_level > 0:
|
||||
preview.batches[id] = target.batches[id].duplicate(true)
|
||||
for id in target.batches.keys():
|
||||
if not entities.has(id) or entities[id].destroy_level <= 0:
|
||||
continue
|
||||
@@ -741,7 +772,11 @@ func _evolve_batch_generation(target: Dictionary, to_year: float) -> bool:
|
||||
|
||||
# Plan all bindings on a copy, reserving specific types before broader groups.
|
||||
func _bind_full(target: Dictionary, amounts: Dictionary) -> bool:
|
||||
var trial: Dictionary = target.duplicate(true)
|
||||
# Binding only mutates free amounts and owners' holdings.
|
||||
var trial: Dictionary = {
|
||||
"amount": target.amount.duplicate(),
|
||||
"binds": target.binds.duplicate(true),
|
||||
}
|
||||
var requests: Array = []
|
||||
for owner_id in amounts:
|
||||
if not entities.has(owner_id):
|
||||
@@ -777,7 +812,12 @@ func _try_production(target: Dictionary, entity: OwniverseEntity, amount: float,
|
||||
automatic: bool = false) -> bool:
|
||||
if amount <= 0.0 or amount != floorf(amount):
|
||||
return false
|
||||
var trial: Dictionary = target.duplicate(true)
|
||||
var trial: Dictionary = target.duplicate()
|
||||
trial.amount = target.amount.duplicate()
|
||||
trial.binds = target.binds.duplicate(true)
|
||||
trial.auto_resources = target.auto_resources.duplicate()
|
||||
trial.batches = target.batches.duplicate()
|
||||
var copied_batches: Dictionary = {}
|
||||
for resource in entity.cost:
|
||||
var cost: float = roundf(maxf(0.0, entity.cost[resource]) * amount)
|
||||
if automatic and trial.auto_resources.get(resource, 0.0) < cost:
|
||||
@@ -792,6 +832,10 @@ func _try_production(target: Dictionary, entity: OwniverseEntity, amount: float,
|
||||
if automatic:
|
||||
trial.auto_resources[resource] = maxf(0.0, trial.auto_resources.get(resource, 0.0) - cost)
|
||||
for id in taken:
|
||||
# Isolate only batches actually touched by this transaction.
|
||||
if trial.batches.has(id) and not copied_batches.has(id):
|
||||
trial.batches[id] = trial.batches[id].duplicate(true)
|
||||
copied_batches[id] = true
|
||||
var factor: float = 1.0 - taken[id] / before[id] if before.get(id, 0.0) > 0.0 else 0.0
|
||||
_scale_batches(trial, id, factor)
|
||||
if not _bind_new_owners(trial, {entity.id: amount}, true):
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
extends RefCounted
|
||||
## datetime is accumulated time; year is the last processed tenth of a year.
|
||||
const STATE_VERSION: String = "0.1.1"
|
||||
const LEGACY_VERSION: String = "0.1.0"
|
||||
|
||||
static func processed_year(datetime: float) -> float:
|
||||
# Remove only sub-nanotick accumulation noise at decimal boundaries.
|
||||
|
||||
Reference in New Issue
Block a user