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