c4e7302567
- 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.
189 lines
11 KiB
GDScript
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
|