Files
owniverse/world/owniverse/owniverse.gd
T

210 lines
9.0 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 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
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_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': {},
'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 frame_step_seconds: float = 0.2
const production_slice_usec: int = 4000
## 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 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 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):
if frame_busy:
_requested_speed = clampi(value, 1, Global.SPEED_LIMIT)
return
state.speed = clampi(value, 1, Global.SPEED_LIMIT)
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)