242 lines
11 KiB
GDScript
242 lines
11 KiB
GDScript
extends Node
|
|
class_name Owniverse
|
|
signal production_applied(id: String)
|
|
const E = preload("res://world/owniverse/math/exact.gd")
|
|
## Owns the state and modules. Rules live in the modules below.
|
|
const SimulationTime = preload("res://world/owniverse/simulation_time.gd")
|
|
const WorldParameters = preload("res://world/owniverse/world_parameters.gd")
|
|
var catalog = preload("res://world/owniverse/entity_catalog.gd").new()
|
|
var presentation = preload("res://world/owniverse/item_presentation.gd").new()
|
|
var batches = preload("res://world/owniverse/object_batches.gd").new(self)
|
|
var ledger = preload("res://world/owniverse/object_ledger.gd").new(self)
|
|
var production = preload("res://world/owniverse/manual_production.gd").new(self)
|
|
var automatic = preload("res://world/owniverse/automatic_production.gd").new(self)
|
|
var stellar = preload("res://world/owniverse/stellar_rules.gd").new(self)
|
|
var apocalypse = preload("res://world/owniverse/apocalypse_controller.gd").new(self)
|
|
var life = preload("res://world/owniverse/life_evolution.gd").new(self)
|
|
var environment = preload("res://world/owniverse/universe_environment.gd").new(self)
|
|
var evolution = preload("res://world/owniverse/evolution_coordinator.gd").new(self)
|
|
var civilization_flow = preload("res://world/owniverse/civilization_flow.gd").new(self)
|
|
var profile_enabled: bool = false
|
|
var profile: Dictionary = {}
|
|
## Runtime-only quality controller. It is deliberately not serialized.
|
|
var adaptive_quality: int = 0
|
|
var adaptive_cycle_ema_ms: float = 0.0
|
|
var adaptive_recovery_cycles: int = 0
|
|
var adaptive_generation_phase: int = 0
|
|
## Runtime-only cadence: 5, 4, 3, or 2 full simulation cycles per second.
|
|
var adaptive_cadence: bool = true
|
|
var cadence_level: int = 0
|
|
var cadence_recovery_cycles: int = 0
|
|
const CADENCE_STEPS: Array[float] = [0.2, 0.25, 1.0 / 3.0, 0.5]
|
|
var frame_step_seconds: float = 0.2
|
|
|
|
func update_adaptive_quality(cycle_usec: int) -> void:
|
|
var cycle_ms: float = float(cycle_usec) / 1000.0
|
|
adaptive_cycle_ema_ms = cycle_ms if adaptive_cycle_ema_ms <= 0.0 else adaptive_cycle_ema_ms * 0.8 + cycle_ms * 0.2
|
|
if adaptive_cycle_ema_ms > 300.0:
|
|
adaptive_quality = 3
|
|
adaptive_recovery_cycles = 0
|
|
elif adaptive_quality >= 2 and adaptive_cycle_ema_ms > 200.0:
|
|
adaptive_quality = 3
|
|
adaptive_recovery_cycles = 0
|
|
elif adaptive_cycle_ema_ms > 220.0:
|
|
adaptive_quality = maxi(adaptive_quality, 2)
|
|
adaptive_recovery_cycles = 0
|
|
elif adaptive_cycle_ema_ms > 200.0:
|
|
adaptive_quality = maxi(adaptive_quality, 1)
|
|
adaptive_recovery_cycles = 0
|
|
elif adaptive_cycle_ema_ms < 140.0:
|
|
adaptive_recovery_cycles += 1
|
|
if adaptive_recovery_cycles >= 30:
|
|
adaptive_quality = maxi(0, adaptive_quality - 1)
|
|
adaptive_recovery_cycles = 0
|
|
else:
|
|
adaptive_recovery_cycles = 0
|
|
if adaptive_cadence:
|
|
var step: float = CADENCE_STEPS[cadence_level]
|
|
if adaptive_cycle_ema_ms > step * 900.0 and cadence_level < CADENCE_STEPS.size() - 1:
|
|
cadence_level += 1
|
|
frame_step_seconds = CADENCE_STEPS[cadence_level]
|
|
cadence_recovery_cycles = 0
|
|
elif adaptive_cycle_ema_ms < step * 650.0 and cadence_level > 0:
|
|
cadence_recovery_cycles += 1
|
|
if cadence_recovery_cycles >= 30:
|
|
cadence_level -= 1
|
|
frame_step_seconds = CADENCE_STEPS[cadence_level]
|
|
cadence_recovery_cycles = 0
|
|
else:
|
|
cadence_recovery_cycles = 0
|
|
if adaptive_quality >= 3:
|
|
adaptive_generation_phase = 1 - adaptive_generation_phase
|
|
|
|
func adaptive_timer_usec() -> int:
|
|
return Time.get_ticks_usec() - (frame_budget.waiting_usec if frame_busy and frame_budget != null else 0)
|
|
|
|
func profile_start() -> int:
|
|
if not profile_enabled: return 0
|
|
return Time.get_ticks_usec() - (frame_budget.waiting_usec if frame_busy and frame_budget != null else 0)
|
|
|
|
func profile_end(section: String, started: int) -> void:
|
|
if not profile_enabled: return
|
|
var sample: Array = profile.get(section, [0, 0])
|
|
sample[0] += profile_start() - started
|
|
sample[1] += 1
|
|
profile[section] = sample
|
|
|
|
var state: Dictionary = {
|
|
'version': SimulationTime.STATE_VERSION,
|
|
'time': {'datetime': 0, 'year': 0},
|
|
'speed': 1,
|
|
'speed_in_year': 1,
|
|
'energy': WorldParameters.INITIAL_ENERGY,
|
|
'world_parameters': WorldParameters.defaults(),
|
|
'trends': WorldParameters.trend_defaults(),
|
|
'batches': {},
|
|
'batch_origins': {},
|
|
'binds': {},
|
|
'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': {},
|
|
'apocalypse': {'phase': 0, 'type': 0, 'warning': 0, 'stage': 1, 'tick': 0, 'end_ticks': 0},
|
|
'stats': {'total_amount': {}},
|
|
}
|
|
|
|
var frame_busy: bool = false
|
|
var frame_commits: int = 0
|
|
var frame_clock: Dictionary = {}
|
|
var frame_budget: RefCounted
|
|
var _requested_speed: int = 0
|
|
var _queued_productions: Array = []
|
|
var production_queue_busy: bool = false
|
|
var _production_queue_cancelled: bool = false
|
|
var frame_active_clock: Dictionary = {}
|
|
var _resume_clock: Dictionary = {}
|
|
var frame_elapsed_seconds: float = 0.0
|
|
const production_slice_usec: int = 4000
|
|
|
|
## The HUD follows requested time; world state remains at the last committed cycle.
|
|
func display_clock() -> Dictionary:
|
|
return frame_clock if not frame_clock.is_empty() else state.time
|
|
|
|
## Keep the requested clock advancing while a transaction spans render frames.
|
|
func evolute_frame(delta: float, tree: SceneTree) -> void:
|
|
if not is_finite(delta) or delta < 0.0 or not is_finite(delta * pow(10.0, speed - 1)): return
|
|
if E.ge(state.get("apocalypse", {}).get("phase", 0), 2):
|
|
frame_clock = state.time.duplicate()
|
|
frame_elapsed_seconds = 0.0
|
|
return
|
|
if frame_clock.is_empty(): frame_clock = state.time.duplicate()
|
|
frame_clock = SimulationTime.advance(frame_clock, delta, speed)
|
|
frame_elapsed_seconds += delta
|
|
if frame_busy or production_queue_busy: return
|
|
var next_clock: Dictionary = _resume_clock
|
|
if next_clock.is_empty() and not _queued_productions.is_empty():
|
|
_start_production_drain(tree)
|
|
return
|
|
if next_clock.is_empty():
|
|
# Cadence limits starts; each transaction consumes the whole pending interval.
|
|
if frame_elapsed_seconds + 1e-12 < frame_step_seconds: return
|
|
next_clock = frame_clock.duplicate()
|
|
frame_elapsed_seconds = 0.0
|
|
frame_busy = true
|
|
frame_budget = preload("res://world/owniverse/frame_budget.gd").new(tree)
|
|
frame_active_clock = next_clock
|
|
_resume_clock = {}
|
|
evolution.run_frame(frame_active_clock, frame_budget)
|
|
|
|
func request_production(id: String, amount: Variant, resource: bool = false) -> bool:
|
|
if E.gt(state.get("apocalypse", {}).get("phase", 0), 0): return false
|
|
if frame_busy or production_queue_busy:
|
|
_queued_productions.append({"id": id, "amount": amount, "resource": resource})
|
|
return true
|
|
_apply_production({"id": id, "amount": amount, "resource": resource})
|
|
return false
|
|
|
|
func _apply_production(command: Dictionary) -> void:
|
|
if command.resource:
|
|
production.produce_hs(command.id, command.amount)
|
|
else:
|
|
production.produce(command.id, command.amount)
|
|
production_applied.emit(command.id)
|
|
|
|
func _start_production_drain(tree: SceneTree) -> void:
|
|
if production_queue_busy or _queued_productions.is_empty(): return
|
|
production_queue_busy = true
|
|
_production_queue_cancelled = false
|
|
_drain_productions(tree)
|
|
|
|
func _drain_productions(tree: SceneTree) -> void:
|
|
var keep_alive: Owniverse = self
|
|
while not _queued_productions.is_empty():
|
|
var slice_started: int = Time.get_ticks_usec()
|
|
while not _queued_productions.is_empty() and Time.get_ticks_usec() - slice_started < production_slice_usec:
|
|
var command: Dictionary = _queued_productions.pop_front()
|
|
_apply_production(command)
|
|
if _queued_productions.is_empty(): break
|
|
await tree.process_frame
|
|
if keep_alive._production_queue_cancelled: return
|
|
production_queue_busy = false
|
|
|
|
## Persist work requests, never the partially calculated transaction.
|
|
func frame_checkpoint() -> Dictionary:
|
|
return {"requested_clock": frame_clock, "active_clock": frame_active_clock if frame_busy else {}, "speed": speed, "productions": _queued_productions, "elapsed_seconds": frame_elapsed_seconds}
|
|
|
|
func restore_frame_checkpoint(checkpoint: Dictionary) -> void:
|
|
frame_clock = checkpoint.get("requested_clock", {})
|
|
_resume_clock = checkpoint.get("active_clock", {})
|
|
var reserved_clock: Dictionary = _resume_clock if not _resume_clock.is_empty() else state.time
|
|
var pending_years: Variant = E.maximum(0, E.subtract(frame_clock.get("datetime", reserved_clock.datetime), reserved_clock.datetime))
|
|
frame_elapsed_seconds = float(checkpoint.get("elapsed_seconds", E.approx(pending_years) / pow(10.0, int(checkpoint.get("speed", state.speed)) - 1)))
|
|
_queued_productions = checkpoint.get("productions", [])
|
|
if _resume_clock.is_empty():
|
|
speed = checkpoint.get("speed", state.speed)
|
|
else:
|
|
_requested_speed = checkpoint.get("speed", state.speed)
|
|
|
|
func finish_frame() -> void:
|
|
frame_busy = false
|
|
frame_commits += 1
|
|
if _requested_speed != 0:
|
|
speed = _requested_speed
|
|
_requested_speed = 0
|
|
if not _queued_productions.is_empty():
|
|
_start_production_drain(frame_budget.tree)
|
|
|
|
func _notification(what: int) -> void:
|
|
if what == NOTIFICATION_PREDELETE:
|
|
_production_queue_cancelled = true
|
|
if frame_budget != null:
|
|
frame_budget.cancelled = true
|
|
|
|
var speed: int :
|
|
get:
|
|
return _requested_speed if _requested_speed != 0 else state.speed
|
|
set(value):
|
|
var requested_speed: int = clampi(value, 1, Global.SPEED_LIMIT)
|
|
if E.eq(state.get("apocalypse", {}).get("phase", 0), 1): requested_speed = 1
|
|
if frame_busy:
|
|
_requested_speed = requested_speed
|
|
return
|
|
state.speed = requested_speed
|
|
state.speed_in_year = E.power(10, E.subtract(state.speed, 1))
|
|
|
|
func evolute(delta: Variant) -> void:
|
|
if not E.finite(delta) or E.approx(delta) <= 0.0 or E.approx(delta) * E.approx(state.speed_in_year) >= 1e27:
|
|
return
|
|
var clock: Dictionary = SimulationTime.advance(state.time, E.approx(delta), speed)
|
|
if not E.finite(clock.datetime): return
|
|
evolution.evolute_to_datetime(clock.datetime, clock)
|