class_name RegionalSimulationFacade extends RefCounted const SCHEMA_VERSION := 1 const JOBS_PER_TICK := 8 const DEFAULT_WORLD_SEED := 1337 const WORLD_ID := SimulationIds.REGIONAL_WORLD_BOSNIA const LOCATION_JAJCE := SimulationIds.REGIONAL_LOCATION_JAJCE const LOCATION_TRAVNIK := SimulationIds.REGIONAL_LOCATION_TRAVNIK const SETTLEMENT_JAJCE := SimulationIds.REGIONAL_SETTLEMENT_JAJCE const SETTLEMENT_TRAVNIK := SimulationIds.REGIONAL_SETTLEMENT_TRAVNIK const ROUTE_JAJCE_TRAVNIK := SimulationIds.REGIONAL_ROUTE_JAJCE_TRAVNIK const CARAVAN_BLUE := SimulationIds.REGIONAL_CARAVAN_BLUE const PERSON_ANA := SimulationIds.REGIONAL_PERSON_ANA const COHORT_CARRIERS := SimulationIds.REGIONAL_COHORT_CARRIERS const POLITY_JAJCE := SimulationIds.REGIONAL_POLITY_JAJCE const POLITY_TRAVNIK := SimulationIds.REGIONAL_POLITY_TRAVNIK const RELATION_JAJCE_TRAVNIK := &"relation_jajce_travnik" const TRADE_GOODS := SimulationIds.RESOURCE_REGIONAL_TRADE_GOODS const ROUTE_TRAVEL_TICKS := 12 const JAJCE_INITIAL_TRADE_GOODS := 0.0 const TRAVNIK_INITIAL_TRADE_GOODS := 4.0 const CARAVAN_CAPACITY := 10.0 var _world_seed := DEFAULT_WORLD_SEED var _last_processed_tick := 0 var _active_location_id := LOCATION_JAJCE var _service: RegionalCaravanService var _last_error := "" static func create_jajce_route( world_seed: int = DEFAULT_WORLD_SEED, last_processed_tick: int = 0 ) -> RegionalSimulationFacade: if last_processed_tick < 0: return null var service := _create_jajce_service(last_processed_tick) if service == null: return null var facade := RegionalSimulationFacade.new() facade._world_seed = world_seed facade._last_processed_tick = last_processed_tick facade._active_location_id = LOCATION_JAJCE facade._service = service return facade static func from_dictionary(envelope: Dictionary) -> RegionalSimulationFacade: if not _has_exact_envelope_fields(envelope): return null if ( not envelope["schema_version"] is int or not envelope["world_seed"] is int or not envelope["last_processed_tick"] is int or not envelope["active_location_id"] is String or not envelope["caravan_service"] is Dictionary or int(envelope["schema_version"]) != SCHEMA_VERSION or int(envelope["last_processed_tick"]) < 0 ): return null var service := RegionalCaravanService.from_dictionary(envelope["caravan_service"]) var last_processed_tick := int(envelope["last_processed_tick"]) var active_location_id := StringName(envelope["active_location_id"]) if ( service == null or not _has_required_jajce_route(service) or active_location_id != LOCATION_JAJCE or service.get_world_state().get_location(active_location_id) == null or int(service.get_scheduler().get_cursor()["last_drain_tick"]) != last_processed_tick ): return null var facade := RegionalSimulationFacade.new() facade._world_seed = int(envelope["world_seed"]) facade._last_processed_tick = last_processed_tick facade._active_location_id = active_location_id facade._service = service return facade func to_dictionary() -> Dictionary: return { "schema_version": SCHEMA_VERSION, "world_seed": _world_seed, "last_processed_tick": _last_processed_tick, "active_location_id": String(_active_location_id), "caravan_service": _service.to_dictionary(), } func checksum() -> String: return JSON.stringify(to_dictionary()).sha256_text() func get_last_processed_tick() -> int: return _last_processed_tick func get_world_seed() -> int: return _world_seed func get_active_location_id() -> StringName: return _active_location_id func get_service() -> RegionalCaravanService: return _service func get_last_error() -> String: return _last_error func depart_caravan( group_id: StringName, origin_settlement_id: StringName, destination_settlement_id: StringName, route_id: StringName, cargo_to_load: Dictionary, presentation_mode: StringName = &"never" ) -> bool: _last_error = "" if _service == null or not _service.is_valid(): return _fail("regional caravan service is unavailable or invalid") var normalized_cargo: Variant = RegionalStateSupport.normalize_amount_ledger(cargo_to_load) if ( normalized_cargo == null or normalized_cargo.size() != 1 or not normalized_cargo.has(String(TRADE_GOODS)) ): return _fail("regional facade cargo must contain only the trade-goods ledger item") if not _service.depart( group_id, origin_settlement_id, destination_settlement_id, route_id, normalized_cargo, _last_processed_tick, presentation_mode ): return _fail(_service.get_last_error()) return true func depart_jajce_delivery( amount: float = TRAVNIK_INITIAL_TRADE_GOODS, presentation_mode: StringName = &"never" ) -> bool: return depart_caravan( CARAVAN_BLUE, SETTLEMENT_TRAVNIK, SETTLEMENT_JAJCE, ROUTE_JAJCE_TRAVNIK, {TRADE_GOODS: amount}, presentation_mode ) func advance_to_tick(target_tick: int, presentation_mode: StringName = &"never") -> bool: _last_error = "" if _service == null or not _service.is_valid(): return _fail("regional caravan service is unavailable or invalid") if target_tick < _last_processed_tick: return _fail( "regional tick cannot move backward from %d to %d" % [_last_processed_tick, target_tick] ) if target_tick == _last_processed_tick: return true var snapshot := to_dictionary() var next_tick := _last_processed_tick + 1 while next_tick <= target_tick: var unsupported_job_type := _first_unsupported_due_job_type(next_tick) if not unsupported_job_type.is_empty(): return _rollback( snapshot, "scheduled regional job type '%s' has no registered handler" % unsupported_job_type ) var due_count := _service.get_scheduler().get_due_count(next_tick) var expected_count := mini(due_count, JOBS_PER_TICK) var executed := _service.process_due(next_tick, JOBS_PER_TICK, presentation_mode) if not _service.get_last_error().is_empty() or executed.size() != expected_count: var service_error := _service.get_last_error() if service_error.is_empty(): service_error = ( "regional scheduler executed %d of %d expected jobs" % [executed.size(), expected_count] ) return _rollback(snapshot, service_error) _last_processed_tick = next_tick next_tick += 1 return true func _first_unsupported_due_job_type(current_tick: int) -> StringName: var registered_job_types := _service.get_registered_job_types() for job: ScheduledJobRecord in _service.get_scheduler().get_all_sorted(): if job.get_due_tick() > current_tick: break if job.get_job_type() not in registered_job_types: return job.get_job_type() return &"" func _rollback(snapshot: Dictionary, error_message: String) -> bool: var restored_service := RegionalCaravanService.from_dictionary(snapshot["caravan_service"]) if restored_service == null: return _fail("%s; facade rollback failed" % error_message) _service = restored_service _last_processed_tick = int(snapshot["last_processed_tick"]) return _fail(error_message) func _fail(message: String) -> bool: _last_error = message if not message.is_empty() else "regional facade operation failed" return false static func _has_exact_envelope_fields(envelope: Dictionary) -> bool: return ( envelope.size() == 5 and ( envelope . has_all( [ "schema_version", "world_seed", "last_processed_tick", "active_location_id", "caravan_service", ] ) ) ) static func _has_required_jajce_route(service: RegionalCaravanService) -> bool: var state := service.get_world_state() if state == null or state.get_world_id() != WORLD_ID: return false var route := state.get_route(ROUTE_JAJCE_TRAVNIK) var caravan := state.get_mobile_group(CARAVAN_BLUE) return ( state.get_location(LOCATION_JAJCE) != null and state.get_location(LOCATION_TRAVNIK) != null and state.get_settlement(SETTLEMENT_JAJCE) != null and state.get_settlement(SETTLEMENT_TRAVNIK) != null and state.get_person(PERSON_ANA) != null and state.get_population_cohort(COHORT_CARRIERS) != null and state.get_polity(POLITY_JAJCE) != null and state.get_polity(POLITY_TRAVNIK) != null and state.get_diplomatic_relation(RELATION_JAJCE_TRAVNIK) != null and route != null and route.connects(LOCATION_JAJCE, LOCATION_TRAVNIK) and route.get_travel_ticks() == ROUTE_TRAVEL_TICKS and caravan != null and caravan.get_group_type() == &"caravan" and caravan.get_cargo_capacity() == CARAVAN_CAPACITY and _regional_trade_goods_are_valid(state) ) static func _regional_trade_goods_are_valid(state: RegionalWorldState) -> bool: var total := 0.0 for settlement: SettlementStateRecord in state.get_all_settlements(): var settlement_ledger := settlement.get_stockpile_ledger() for item_id: Variant in settlement_ledger: if StringName(item_id) != TRADE_GOODS: return false total += settlement.get_stockpile_amount(TRADE_GOODS) for group: MobileGroupStateRecord in state.get_all_mobile_groups(): var group_ledger := group.get_cargo_ledger() for item_id: Variant in group_ledger: if StringName(item_id) != TRADE_GOODS: return false total += group.get_cargo_amount(TRADE_GOODS) return is_equal_approx(total, TRAVNIK_INITIAL_TRADE_GOODS) static func _create_jajce_service(last_processed_tick: int) -> RegionalCaravanService: var jajce_location := LocationStateRecord.create( LOCATION_JAJCE, WORLD_ID, &"settlement", "Jajce", SpatialAddress.create(WORLD_ID, LOCATION_JAJCE, Vector3.ZERO) ) var travnik_location := LocationStateRecord.create( LOCATION_TRAVNIK, WORLD_ID, &"settlement", "Travnik", SpatialAddress.create(WORLD_ID, LOCATION_TRAVNIK, Vector3.ZERO) ) var jajce_settlement := SettlementStateRecord.create( SETTLEMENT_JAJCE, LOCATION_JAJCE, "Jajce", POLITY_JAJCE, 2, &"founders_jajce", 0, {} ) var travnik_settlement := SettlementStateRecord.create( SETTLEMENT_TRAVNIK, LOCATION_TRAVNIK, "Travnik", POLITY_TRAVNIK, 1, &"founders_travnik", 0, {TRADE_GOODS: TRAVNIK_INITIAL_TRADE_GOODS} ) var route := RouteStateRecord.create( ROUTE_JAJCE_TRAVNIK, LOCATION_JAJCE, LOCATION_TRAVNIK, ROUTE_TRAVEL_TICKS, true ) var caravan := MobileGroupStateRecord.create_at_location( CARAVAN_BLUE, &"caravan", "Blue caravan", POLITY_TRAVNIK, SpatialAddress.create(WORLD_ID, LOCATION_TRAVNIK, Vector3.ZERO), [PERSON_ANA], {COHORT_CARRIERS: 3}, {}, CARAVAN_CAPACITY ) var ana := PersonStateRecord.create_in_group( PERSON_ANA, "Ana", &"merchant", POLITY_TRAVNIK, LOCATION_TRAVNIK, CARAVAN_BLUE ) var carriers := PopulationCohortRecord.create_in_group( COHORT_CARRIERS, &"adult_carriers", POLITY_TRAVNIK, 3, CARAVAN_BLUE ) var jajce_polity := PolityStateRecord.create( POLITY_JAJCE, "Jajce council", LOCATION_JAJCE, [SETTLEMENT_JAJCE] ) var travnik_polity := PolityStateRecord.create( POLITY_TRAVNIK, "Travnik council", LOCATION_TRAVNIK, [SETTLEMENT_TRAVNIK] ) var relation := DiplomaticRelationRecord.create( RELATION_JAJCE_TRAVNIK, POLITY_JAJCE, POLITY_TRAVNIK, &"trade", 0.4, 0 ) if ( jajce_location == null or travnik_location == null or jajce_settlement == null or travnik_settlement == null or route == null or caravan == null or ana == null or carriers == null or jajce_polity == null or travnik_polity == null or relation == null ): return null var locations: Array[LocationStateRecord] = [jajce_location, travnik_location] var settlements: Array[SettlementStateRecord] = [jajce_settlement, travnik_settlement] var routes: Array[RouteStateRecord] = [route] var groups: Array[MobileGroupStateRecord] = [caravan] var persons: Array[PersonStateRecord] = [ana] var cohorts: Array[PopulationCohortRecord] = [carriers] var polities: Array[PolityStateRecord] = [jajce_polity, travnik_polity] var relations: Array[DiplomaticRelationRecord] = [relation] var world_state := RegionalWorldState.create( WORLD_ID, locations, settlements, routes, groups, persons, cohorts, polities, relations ) if world_state == null: return null var service := RegionalCaravanService.create( world_state, RegionalJobScheduler.new(), WorldEventStore.new() ) if service == null: return null service.process_due(last_processed_tick, JOBS_PER_TICK, &"never") return service if service.get_last_error().is_empty() else null