371 lines
12 KiB
GDScript
371 lines
12 KiB
GDScript
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
|