Files
owniverse/world/owniverse/evolution_coordinator.gd
T
Kirill c4e7302567 Add new tests and implement frame budget management
- Introduced `civilization_benchmark.gd` and `legacy_quantity_math.gd` for benchmarking and legacy quantity calculations.
- Created `test_dynamic_math.gd`, `test_frame_evolution.gd`, and `test_sparse_batches.gd` to validate mathematical operations and frame evolution logic.
- Implemented `frame_budget.gd` to manage cooperative work on the main thread, ensuring no partially calculated state is published.
- Added UID files for all new scripts to maintain unique identifiers.
2026-10-01 23:43:34 +03:00

189 lines
11 KiB
GDScript

extends "res://world/owniverse/simulation_module.gd"
const E = preload("res://world/owniverse/math/exact.gd")
const CivilizationEvolution = preload("res://world/owniverse/civilization_evolution.gd")
const CivilizationState = preload("res://world/owniverse/civilization_state.gd")
const SimulationTime = preload("res://world/owniverse/simulation_time.gd")
const WorldParameters = preload("res://world/owniverse/world_parameters.gd")
const QuantityMath = preload("res://world/owniverse/quantity_math.gd")
func initialize_life_batches() -> void:
CivilizationState.detach_space_civilizations(world.state, Global.CivilizationRules)
CivilizationState.enforce_capacity(world, world.state, Global.CivilizationRules, world.state.time.datetime)
var physical: Dictionary = world.ledger.physical_amounts(world.state)
for id in world.catalog.entities:
if world.catalog.entities[id].life_rule.is_empty():
continue
var tracked: Variant = 0
for batch : Dictionary in world.state.batches.get(id, []):
tracked = E.add(tracked, batch.amount)
world.batches.append_batch(world.state, id, E.maximum(0, E.subtract(physical.get(id, 0), tracked)), world.state.time.datetime, world.state.time.datetime)
func run_frame(clock: Dictionary, budget: RefCounted) -> void:
var _keep_alive: RefCounted = self
await evolute_to_datetime(clock.datetime, clock, budget)
if not budget.cancelled and world != null: world.finish_frame()
func evolute_to_datetime(datetime: Variant, clock: Dictionary = {}, budget: RefCounted = null) -> void:
var _keep_alive: RefCounted = self
# Reconcile only when simulation advances.
# Fractional frame updates cannot change capacity or populations.
var processed_year: Variant = SimulationTime.processed_year(datetime)
if E.gt(processed_year, world.state.time.year):
var profile_initialize: int = world.profile_start()
initialize_life_batches()
world.profile_end("initialize", profile_initialize)
var previous: Dictionary = world.state
var next: Dictionary = previous.duplicate_deep()
next.version = SimulationTime.STATE_VERSION
next.speed = clampi(E.integer(previous.speed), 1, Global.SPEED_LIMIT)
next.speed_in_year = E.power(10, E.subtract(next.speed, 1))
var from_year: Variant = previous.time.year
var to_year: Variant = processed_year
if E.gt(to_year, from_year):
var quantities: Dictionary = world.ledger.physical_amounts(previous)
var distribution: Dictionary = QuantityMath.normalize_distribution(quantities)
next.distribution = distribution
var profile_production: int = world.profile_start()
world.automatic.produce_for_period(previous, next, quantities, distribution, from_year, to_year)
world.profile_end("production", profile_production)
var profile_generations: int = world.profile_start()
if budget != null:
await evolve_batches(next, to_year, budget)
if budget.cancelled: return
else:
evolve_batches(next, to_year)
world.profile_end("generations", profile_generations)
world.ledger.refresh_groups(next)
world.ledger.clamp_auto_resources(next)
var profile_trends: int = world.profile_start()
WorldParameters.advance(previous, next, Global.CivilizationRules, E.subtract(to_year, from_year))
world.profile_end("trends", profile_trends)
next.time = clock if not clock.is_empty() else {"datetime": datetime, "year": to_year}
# Publish only after every phase has finished; previous stays untouched.
world.state = E.bound_state(next)
if E.gt(to_year, from_year):
world.presentation.refresh(world.state, world.catalog)
func evolve_batches(target: Dictionary, to_year: Variant, budget: RefCounted = null) -> void:
var _keep_alive: RefCounted = self
# A long cycle may contain several generations; preserve actual birth intervals.
var completed: Dictionary = {}
while await evolve_batch_generation(target, to_year, completed, budget):
if budget != null and not await budget.pause(): return
func evolve_batch_generation(target: Dictionary, to_year: Variant, completed: Dictionary = { }, budget: RefCounted = null) -> bool:
var _keep_alive: RefCounted = self
# Apply direct completions at the original next-generation boundary, so
# binding transfers, the blast environment and capacity checks keep order.
if budget != null and not await budget.pause(): return false
var completed_now: Dictionary = completed.duplicate(true)
completed.clear()
var quantities: Dictionary = world.ledger.physical_amounts(target)
var expired: Dictionary = {}
var preview: Dictionary = {"batches": {}}
for id in target.batches:
if (world.catalog.entities.has(id) and E.gt(world.catalog.entities[id].destroy_level, 0)):
preview.batches[id] = target.batches[id].duplicate(true)
for id in target.batches.keys():
if ((not world.catalog.entities.has(id)) or E.le(world.catalog.entities[id].destroy_level, 0)):
continue
var dying: Variant = 0
for part : Dictionary in completed_now.get(id, []):
dying = E.add(dying, part.amount)
for part : Dictionary in world.batches.withdraw_batches(preview, id, to_year):
dying = E.add(dying, part.amount)
if E.gt(quantities.get(id, 0), 0):
world.stellar.destroy_planets(target, id, E.divide(dying, quantities[id]), world.catalog.entities[id].destroy_level)
quantities = world.ledger.physical_amounts(target)
# Withdraw all types before creating successors: iteration order cannot affect them.
for id in target.batches.keys():
if world.catalog.entities.has(id):
expired[id] = E.add(completed_now.get(id, []), world.batches.withdraw_batches(target, id, to_year))
var has_expired: bool = false
for parts : Array in expired.values():
if (not parts.is_empty()):
has_expired = true
break
if (not has_expired):
return false
var fractions: Dictionary = {}
var outputs: Dictionary = {}
var current_quantities: Dictionary = quantities
var star_budget: Dictionary = CivilizationState.stellar_budget(world, target, Global.CivilizationRules, current_quantities)
var civilization_transaction: Dictionary = await CivilizationEvolution.prepare(world, target, expired, to_year, star_budget, Global.CivilizationRules, current_quantities, budget)
if budget != null and budget.cancelled: return false
for id in expired:
if budget != null and not await budget.pause(): return false
var total: Variant = 0
for part : Dictionary in expired[id]:
total = E.add(total, part.amount)
fractions[id] = (E.clamp_value(E.divide(total, quantities[id]), 0, 1) if E.gt(quantities.get(id, 0), 0) else 0)
var combined: Dictionary = {}
for part : Dictionary in expired[id]:
if (not part.has("products")):
part.products = world.stellar.evolution_products(target, world.catalog.entities[id], part.amount)
for successor in part.products.keys():
if Global.CivilizationRules.has(id):
continue
var requested: Variant = part.products[successor]
var admitted: Variant = CivilizationState.admit_transition(star_budget, Global.CivilizationRules, id, successor, requested)
part.products[successor] = admitted
if E.lt(admitted, requested):
part.products[id] = E.subtract(E.add(part.products.get(id, 0), requested), admitted)
for successor in part.products:
combined[successor] = E.add(combined.get(successor, 0), part.products[successor])
outputs[id] = {}
if E.gt(total, 0):
for successor in combined:
outputs[id][successor] = E.divide(combined[successor], total)
var incoming: Dictionary = world.ledger.transfer_evolution(target, fractions, outputs)
var new_owners: Dictionary = {}
var blast_power: Variant = 0
var environment: Dictionary = world.environment.stellar_environment(target)
for id in expired:
for part : Dictionary in expired[id]:
for successor in part.products:
if budget != null and not await budget.pause(): return false
var amount: Variant = part.products[successor]
if E.eq(successor, "blast"):
blast_power = E.add(blast_power, amount)
continue
if (not world.catalog.entities.has(successor)):
continue
if (E.eq(successor, id) and (((not world.catalog.entities[id].life_rule.is_empty()) or Global.CivilizationRules.has(id)))):
target.amount[id] = E.add(target.amount.get(id, 0), amount)
else:
amount = world.ledger.credit(target, successor, amount)
var produces: bool = not world.automatic.yearly_products(world.catalog.entities[successor]).is_empty()
var before_batches: Array = target.batches.get(successor, []).duplicate(true) if produces else []
var cutoff: Variant = (to_year if world.stellar.uses_short_lived_fast_path(successor) else E.neg(INF))
var profile_newborn: int = world.profile_start()
var integral: Variant
if budget != null:
integral = await world.batches.append_batch(target, successor, amount, part.from, part.to, completed, cutoff, budget)
if budget.cancelled: return false
else:
integral = world.batches.append_batch(target, successor, amount, part.from, part.to, completed, cutoff)
if produces:
world.automatic.produce_newborn_lifetime(target, successor, amount, part.from, part.to, to_year, before_batches, integral)
world.profile_end("newborn_batches", profile_newborn)
new_owners[successor] = E.add(new_owners.get(successor, 0), amount)
world.ledger.accept_transfers(target, incoming)
world.ledger.bind_new_owners(target, new_owners)
world.ledger.round_object_allocations(target)
if budget != null:
await CivilizationEvolution.finish(world, target, civilization_transaction, to_year, Global.CivilizationRules, budget)
if budget.cancelled: return false
else:
CivilizationEvolution.finish(world, target, civilization_transaction, to_year, Global.CivilizationRules)
world.stellar.apply_blast(target, blast_power, environment)
CivilizationState.enforce_capacity(world, target, Global.CivilizationRules, to_year)
return true