Refactor save management; enhance save_data function with verification and plaintext copy options, and update load_data logic for improved error handling

This commit is contained in:
Kirill
2026-09-13 22:57:25 +03:00
parent c22b21a537
commit dea744bc30
7 changed files with 380 additions and 260 deletions
+154 -43
View File
@@ -75,9 +75,32 @@ func evolute(delta: float) -> void:
# Publish only after every phase has finished; previous stays untouched.
new_state = next
state = next
for id in next.amount:
if next.amount[id] != previous.amount.get(id, 0.0):
items_to_update[id] = next.amount[id]
if to_year > from_year:
refresh_item_amounts()
## Aggregate only for presentation; never write aliases back into simulation state.
func get_item_amounts(total_produced: bool = false) -> Dictionary[String, float]:
var source: Dictionary = state.stats.total_amount if total_produced else state.amount
var result: Dictionary[String, float] = {}
for id in source:
result[id] = 0.0
for id in source:
# group_as counters are already sums and must not be counted a second time.
if _is_group(id):
continue
var item_id: String = id
if entities.has(id) and entities[id].count_as != "":
item_id = entities[id].count_as
result[item_id] = result.get(item_id, 0.0) + float(source[id])
for id in source:
if _is_group(id):
result[id] = float(source[id])
return result
func refresh_item_amounts() -> void:
items_to_update = get_item_amounts()
func _add_time_delta(delta: float) -> Dictionary:
@@ -335,20 +358,31 @@ func _withdraw_batches(target: Dictionary, id: String, to_year: float) -> Array:
return expired
func _evolution_outputs(entity: OwniverseEntity) -> Dictionary:
if entity.output_or.is_empty():
return entity.output_and.duplicate(true)
# Expected quantities keep evolution independent of frame rate and RNG order.
var chance: float = clampf(entity.output_or.get("chance", 0.0), 0.0, 1.0)
func _add_output_terms(outputs: Dictionary, terms: String, count: float) -> void:
if count <= 0.0:
return
for term in terms.split("&"):
var parts: PackedStringArray = term.strip_edges().split(":")
var id: String = parts[0].strip_edges()
var amount: float = float(parts[1]) if parts.size() == 2 else 1.0
if id != "" and amount > 0.0:
outputs[id] = outputs.get(id, 0.0) + count * amount
func _evolution_products(target: Dictionary, entity: OwniverseEntity, count: float) -> Dictionary:
var outputs: Dictionary = {}
for branch in ["negative", "positive"]:
var weight: float = chance if branch == "positive" else 1.0 - chance
for term in str(entity.output_or.get(branch, "")).split("&"):
var parts: PackedStringArray = term.split(":")
var id: String = parts[0]
var amount: float = float(parts[1]) if parts.size() == 2 else 1.0
if id != "" and weight > 0.0:
outputs[id] = outputs.get(id, 0.0) + amount * weight
for id in entity.output_and:
outputs[id] = count * maxf(0.0, entity.output_and[id])
if not entity.output_or.is_empty():
# The parser stores the percentage after '%', e.g. 0.1 means 0.1 percent.
var chance: float = clampf(entity.output_or.get("chance", 0.0) / 100.0, 0.0, 1.0)
if not target.has("evolution_remainders"):
target.evolution_remainders = {}
var expected: float = count * chance + target.evolution_remainders.get(entity.id, 0.0)
var positive: float = minf(count, floorf(snappedf(expected, 1.0e-12)))
target.evolution_remainders[entity.id] = maxf(0.0, expected - positive)
_add_output_terms(outputs, str(entity.output_or.get("positive", "")), positive)
_add_output_terms(outputs, str(entity.output_or.get("negative", "")), count - positive)
return outputs
@@ -379,7 +413,15 @@ func _evolve_batch_generation(target: Dictionary, to_year: float) -> bool:
for part: Dictionary in expired[id]:
total += part.amount
fractions[id] = clampf(total / quantities[id], 0.0, 1.0) if quantities.get(id, 0.0) > 0.0 else 0.0
outputs[id] = _evolution_outputs(entities[id])
var combined: Dictionary = {}
for part: Dictionary in expired[id]:
part.products = _evolution_products(target, entities[id], part.amount)
for successor in part.products:
combined[successor] = combined.get(successor, 0.0) + part.products[successor]
outputs[id] = {}
if total > 0.0:
for successor in combined:
outputs[id][successor] = combined[successor] / total
# Use a frozen binding table so simultaneous transitions cannot cascade by type order.
var bindings: Dictionary = target.binds.duplicate(true)
@@ -406,8 +448,8 @@ func _evolve_batch_generation(target: Dictionary, to_year: float) -> bool:
var new_owners: Dictionary = {}
for id in expired:
for part: Dictionary in expired[id]:
for successor in outputs[id]:
var amount: float = part.amount * maxf(0.0, outputs[id][successor])
for successor in part.products:
var amount: float = part.products[successor]
amount = _credit(target, successor, amount)
_append_batch(target, successor, amount, part.from, part.to)
new_owners[successor] = new_owners.get(successor, 0.0) + amount
@@ -421,7 +463,74 @@ func _evolve_batch_generation(target: Dictionary, to_year: float) -> bool:
return true
func _bind_new_owners(target: Dictionary, amounts: Dictionary) -> void:
# 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)
var requests: Array = []
for owner in amounts:
if not entities.has(owner):
return false
for id in entities[owner].bind:
var quantity: float = amounts[owner] * maxf(0.0, entities[owner].bind[id])
if not is_equal_approx(quantity, roundf(quantity)):
return false
if quantity > 0.0:
requests.append({"owner": owner, "id": id, "amount": roundf(quantity), "members": _members(id).size()})
requests.sort_custom(func(a: Dictionary, b: Dictionary) -> bool:
if a.members != b.members:
return a.members < b.members
return a.id < b.id if a.id != b.id else a.owner < b.owner)
for request: Dictionary in requests:
var taken: Dictionary = _take_available(trial, request.id, request.amount)
var total: float = 0.0
for quantity in taken.values():
total += quantity
if total != request.amount:
return false
if not trial.binds.has(request.owner):
trial.binds[request.owner] = {}
for id in taken:
trial.binds[request.owner][id] = trial.binds[request.owner].get(id, 0.0) + taken[id]
target.amount = trial.amount
target.binds = trial.binds
return true
# Neither costs nor bindings are committed unless the whole batch is funded.
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)
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:
return false
var before: Dictionary = _physical_amounts(trial)
var taken: Dictionary = _take_available(trial, resource, cost)
var total: float = 0.0
for quantity in taken.values():
total += quantity
if total != cost:
return false
if automatic:
trial.auto_resources[resource] = maxf(0.0, trial.auto_resources.get(resource, 0.0) - cost)
for id in taken:
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):
return false
_refresh_groups(trial)
_clamp_auto_resources(trial)
for field in ["amount", "binds", "batches", "auto_resources"]:
if trial.has(field):
target[field] = trial[field]
return true
func _bind_new_owners(target: Dictionary, amounts: Dictionary, require_full: bool = false) -> bool:
if require_full:
return _bind_full(target, amounts)
var requests: Dictionary = {}
for owner in amounts:
if not entities.has(owner):
@@ -449,6 +558,8 @@ func _bind_new_owners(target: Dictionary, amounts: Dictionary) -> void:
target.binds[owner][id] = target.binds[owner].get(id, 0.0) + quantity
target.amount[id] = maxf(0.0, target.amount.get(id, 0.0) - quantity)
return true
# Integrate survival over the period, including partial batch expiry.
func _object_years(source: Dictionary, id: String, quantity: float,
@@ -619,16 +730,9 @@ func _produce_for_period(previous: Dictionary, target: Dictionary, quantities: D
var actual: float = minf(desired, _auto_capacity(target, entity, budget))
if actual <= 0.0:
continue
for resource in entity.cost:
var cost: float = roundf(maxf(0.0, entity.cost[resource]) * actual)
budget[resource] = maxf(0.0, budget.get(resource, 0.0) - cost)
var before: Dictionary = _physical_amounts(target)
var taken: Dictionary = _take_available(target, resource, cost)
for consumed_id in taken:
var factor: float = 1.0 - taken[consumed_id] / before[consumed_id] if before.get(consumed_id, 0.0) > 0.0 else 0.0
_scale_batches(target, consumed_id, factor)
_bind_new_owners(target, {id: actual})
_round_object_allocations(target)
if not _try_production(target, entity, actual, true):
continue
budget = target.auto_resources
created[id] = actual
total_created += actual
for id in candidates:
@@ -685,8 +789,8 @@ func produce(item_id: String, desired_amount: float) -> float:
if max_producible <= 0:
return 0.0
apply_costs(entity, max_producible)
apply_binds(item_id, entity, max_producible)
if not _try_production(state, entity, max_producible):
return 0.0
# Add produced entities
_process_enitity(item_id, max_producible)
@@ -723,7 +827,7 @@ func produce_hs(entity_id: String, value: float) -> float:
# Decimal costs are represented as integer fractions; combine their denominators.
func _production_step(entity: OwniverseEntity) -> float:
var step: float = 1.0
for value in entity.cost.values():
for value in entity.cost.values() + entity.bind.values():
var cost: float = maxf(0.0, value)
if cost == 0.0:
continue
@@ -753,6 +857,14 @@ func calculate_max_producible(entity: OwniverseEntity, desired_amount: float = 1
requirements[resource] = maxf(0.0, entity.cost[resource])
for resource in entity.bind:
requirements[resource] = requirements.get(resource, 0.0) + maxf(0.0, entity.bind[resource])
var has_requirements: bool = false
for quantity in requirements.values():
if quantity > 0.0:
has_requirements = true
break
# No recipe means unavailable for production, not unlimited free objects.
if not has_requirements:
return 0.0
var available: float = maxf(0.0, desired_amount)
for resource in requirements:
if requirements[resource] > 0.0:
@@ -776,19 +888,20 @@ func apply_costs(entity: OwniverseEntity, amount: float) -> void:
for id in taken:
var factor: float = 1.0 - taken[id] / quantities[id] if quantities.get(id, 0.0) > 0.0 else 0.0
_scale_batches(state, id, factor)
items_to_update[id] = state.amount[id]
_refresh_groups(state)
_clamp_auto_resources(state)
refresh_item_amounts()
## Keep concrete bound types, so their ages and transitions remain traceable.
func apply_binds(item_id: String, _entity: OwniverseEntity, amount: float) -> void:
_bind_new_owners(state, {item_id: amount})
_round_object_allocations(state)
func apply_binds(item_id: String, _entity: OwniverseEntity, amount: float) -> bool:
if not _bind_new_owners(state, {item_id: amount}, true):
return false
_refresh_groups(state)
_clamp_auto_resources(state)
for id in state.amount:
items_to_update[id] = state.amount[id]
refresh_item_amounts()
return true
## Computes a normalized distribution of dictionary values (sum equals 1.0)
@@ -822,7 +935,7 @@ func consume_from_batches(entity: OwniverseEntity, current_year: float) -> float
state.binds[owner][entity.id] *= 1.0 - fraction
_round_object_allocations(state)
_refresh_groups(state)
items_to_update[entity.id] = state.amount[entity.id]
refresh_item_amounts()
return total
@@ -877,15 +990,13 @@ func _process_enitity(entity_id: String, amount: float) -> void:
state.amount[entity_id] = state.amount.get(entity_id, 0.0) + amount
state.stats.total_amount[entity_id] = state.stats.total_amount.get(entity_id, 0.0) + maxf(0.0, amount)
# Update items to display
items_to_update[entity_id] = state.amount[entity_id]
# Handle group updates if entity belongs to a group
var entity = entities[entity_id]
if entity.group_as != "":
state.amount[entity.group_as] = state.amount.get(entity.group_as, 0.0) + amount
items_to_update[entity.group_as] = state.amount[entity.group_as]
state.stats.total_amount[entity.group_as] = state.stats.total_amount.get(entity.group_as, 0.0) + maxf(0.0, amount)
refresh_item_amounts()
-176
View File
@@ -1,176 +0,0 @@
extends Node
class_name State
const FLOAT_THRESHOLD: float = 1.0E14
var data: Dictionary = {
'time': {
'timestamp': 0.0,
'year': 0.0,
},
'timestamp': 0.0,
'year': 0.0,
'speed_in_year': 0,
'energy': 1E14,
}
var entities: Dictionary[String, OwniverseEntity] = {}
var producing_entities: Array[OwniverseEntity] = []
# TODO to save !!!
var auto_production_rest: Dictionary = {
'H': 0.0,
'S': 0.0,
'auto': 0.0,
}
var evolving_entities: Array[OwniverseEntity] = []
# TODO to save !!!
var batches: Dictionary[String, Array] = {}
# TODO to save !!!
var gamount: Dictionary[String, float] = {}
var distribution: Dictionary[String, float] = {}
# new functions
func evolute(prev_state: State, delta: float, current_speed_in_year: float) -> float:
var _new_year: float = _add_year_delta(delta, current_speed_in_year)
if data.year == _new_year:
return data.year
return _new_year
func _add_year_delta(delta: float, current_speed_in_year: float) -> float:
var _new_year:float = floorf(data.year + delta * current_speed_in_year)
#print(data.year, " | ", _new_year, " | ", data)
return _new_year
# end of new functions
func add(entity: OwniverseEntity, year: float, amount: float):
var _batch_array: Array = batches[entity.id]
var _from_year: float = year + entity.lifespan - entity.lifespan_delta
var _to_year: float = year + entity.lifespan + entity.lifespan_delta
if _batch_array.size() != 0 and _batch_array[-1].to_year > year + entity.lifespan:
_batch_array[-1].amount += amount
else:
var _batch := {
'from_year': _from_year,
'to_year': _to_year,
'amount': amount,
}
_batch_array.append(_batch)
#print('---------')
#for id in batches:
#print(id)
#
func process(from_year: float, to_year: float) -> void:
# производим
# эволюционируем
# записываем новые
var _to_produce := {}
var _to_evolute := {}
for _entity_id in batches:
_to_produce[_entity_id] = 0
_to_evolute[_entity_id] = 0
var _new_batch_array := []
for _batch: Dictionary in batches[_entity_id]:
# production
_to_produce[_entity_id] += _batch.amount
# evolution
if _batch.to_year < to_year:
_to_produce[_entity_id] += _batch.amount
else:
var _max_from_year: float = max(from_year, _batch.from_year)
var _min_to_year: float = min(to_year, _batch.to_year)
var _part_of_amount: float = _batch.amount \
* (_min_to_year - _max_from_year) \
/ (_batch.to_year - _batch.from_year)
_to_evolute[_entity_id] += _part_of_amount
var _rest_of_amount: float = _batch.amount - _part_of_amount
if _rest_of_amount > 0:
_new_batch_array.append({
'from_year': _batch._from_year,
'to_year': _batch._to_year,
'amount': _rest_of_amount,
})
# entity level
batches[_entity_id] = _new_batch_array
func produce(from_year: float, to_year: float):
var _time_delta = to_year - from_year
# get entity array for production
for _entity: OwniverseEntity in producing_entities:
var _to_produce: float = 0.0
for _batch: Dictionary in batches[_entity.id]:
_to_produce += _batch.amount
_to_produce *= _time_delta * _entity.product_per_year_amount
if _entity.auto > 0:
var _auto_production_part: float = _entity.auto * _to_produce
_to_produce -= _auto_production_part
auto_production_rest.auto += _auto_production_part
auto_production_rest[_entity.product_per_year_type] += _to_produce
print("auto prod rest: ", auto_production_rest)
auto_production_rest["H"] = produce_hs("H", auto_production_rest["H"])
auto_production_rest["S"] = produce_hs("S", auto_production_rest["S"])
func produce_hs(entity_id: String, value: float) -> float:
var _to_produce: float = floorf(value)
gamount[entity_id] += _to_produce
# TODO double double math
if _to_produce > data["energy"]:
_to_produce = data["energy"]
data["energy"] -= _to_produce
return value - _to_produce
func produce_auto() -> float:
return 0.0
func init_entity(entity: OwniverseEntity) -> void:
entities[entity.id] = entity
gamount[entity.id] = 0.0
if entity.product_per_year_type != "":
producing_entities.append(entity)
if entity.lifespan > 0:
evolving_entities.append(entity)
batches[entity.id] = []
func update_distribution(_distribute_by: Dictionary[String, float]) -> void:
distribution = {}
var _total: float = 0
for id in _distribute_by:
if _distribute_by[id] != 0:
var _value: float = _distribute_by[id]
_total += _value
distribution[id] = _value
if _total == 0: return
for id in distribution:
distribution[id] /= _total
#func _add_float(a: float, b: Array[float]) -> Array[float]:
#if a < FLOAT_THRESHOLD:
#return [a+b[0], 0]
#else:
#return [a+b[0], 0]
-1
View File
@@ -1 +0,0 @@
uid://mgnxt6kbf80n