feat: integrate regional caravan persistence
This commit is contained in:
@@ -12,6 +12,9 @@ const NpcTickDebugLog := preload("res://simulation/debug/npc_tick_debug_log.gd")
|
||||
const RelationshipSystemScript := preload("res://simulation/relationships/RelationshipSystem.gd")
|
||||
const EventKnowledgeSystemScript := preload("res://simulation/knowledge/EventKnowledgeSystem.gd")
|
||||
const ConflictSystemScript := preload("res://simulation/conflict/ConflictSystem.gd")
|
||||
const RegionalSimulationFacadeScript := preload(
|
||||
"res://simulation/regional/RegionalSimulationFacade.gd"
|
||||
)
|
||||
|
||||
signal npc_task_changed(npc: SimNPC, old_task: StringName, new_task: StringName)
|
||||
signal village_changed(village: SimVillage)
|
||||
@@ -84,6 +87,8 @@ var conflict_system := ConflictSystemScript.new()
|
||||
var player_quest_system := PlayerQuestSystem.new()
|
||||
var player_needs := PlayerNeedsSystem.new()
|
||||
var player_negotiation := PlayerNegotiationSystem.new()
|
||||
var _regional_simulation: RegionalSimulationFacade
|
||||
var _restoring_save_manifest := false
|
||||
var _last_player_tier := PlayerStandingRecord.TIER_STRANGER
|
||||
var storage_states: Dictionary:
|
||||
get:
|
||||
@@ -165,6 +170,9 @@ func _ready() -> void:
|
||||
clock = SimulationClock.new(tick_interval)
|
||||
clock.cycle_duration_seconds = cycle_duration_seconds
|
||||
clock.elapsed_ticks = int(0.25 * cycle_duration_seconds / tick_interval)
|
||||
if not _initialize_regional_simulation(tick_count):
|
||||
set_process(false)
|
||||
return
|
||||
village.debug_logs = debug_logs
|
||||
economy.configure(village, debug_logs)
|
||||
animal_care.configure(economy, active_world_adapter)
|
||||
@@ -248,7 +256,17 @@ func get_wander_offset(npc_id: int) -> Vector3:
|
||||
|
||||
|
||||
func simulate_tick() -> void:
|
||||
tick_count += 1
|
||||
var next_tick := tick_count + 1
|
||||
if _regional_simulation == null or not _regional_simulation.advance_to_tick(next_tick):
|
||||
var regional_error := (
|
||||
_regional_simulation.get_last_error()
|
||||
if _regional_simulation != null
|
||||
else "regional facade is missing"
|
||||
)
|
||||
push_error("SimulationManager: regional tick failed: " + regional_error)
|
||||
set_process(false)
|
||||
return
|
||||
tick_count = next_tick
|
||||
if debug_logs:
|
||||
print("--- Tick ", tick_count, " ---")
|
||||
animal_care.advance(tick_count)
|
||||
@@ -2408,6 +2426,34 @@ func get_state_checksum() -> String:
|
||||
return create_state_record().to_json().sha256_text()
|
||||
|
||||
|
||||
func get_regional_state_checksum() -> String:
|
||||
return _regional_simulation.checksum() if _regional_simulation != null else ""
|
||||
|
||||
|
||||
func get_world_state_checksum() -> String:
|
||||
var manifest := create_save_manifest()
|
||||
return manifest.checksum() if manifest != null else ""
|
||||
|
||||
|
||||
func get_regional_snapshot() -> Dictionary:
|
||||
return (
|
||||
_regional_simulation.to_dictionary().duplicate(true) if _regional_simulation != null else {}
|
||||
)
|
||||
|
||||
|
||||
func request_regional_jajce_delivery(
|
||||
amount: float = 1.0, presentation_mode: StringName = &"never"
|
||||
) -> bool:
|
||||
if (
|
||||
not uses_combined_save_manifest()
|
||||
or _regional_simulation == null
|
||||
or not is_finite(amount)
|
||||
or amount <= 0.0
|
||||
):
|
||||
return false
|
||||
return _regional_simulation.depart_jajce_delivery(amount, presentation_mode)
|
||||
|
||||
|
||||
func get_latest_decision(npc_id: int) -> ActionSelectionResult:
|
||||
return latest_decisions.get(npc_id)
|
||||
|
||||
@@ -2484,6 +2530,59 @@ func serialize_state() -> String:
|
||||
return create_state_record().to_json()
|
||||
|
||||
|
||||
func uses_combined_save_manifest() -> bool:
|
||||
var scope := event_log.get_scope()
|
||||
return (
|
||||
StringName(scope.get("world_id", &"")) == SimulationIds.REGIONAL_WORLD_BOSNIA
|
||||
and (StringName(scope.get("location_id", &"")) == SimulationIds.REGIONAL_LOCATION_JAJCE)
|
||||
)
|
||||
|
||||
|
||||
func can_serialize_local_state_only() -> bool:
|
||||
if uses_combined_save_manifest() or _regional_simulation == null:
|
||||
return false
|
||||
var idle_facade := RegionalSimulationFacadeScript.create_jajce_route(
|
||||
simulation_seed, tick_count
|
||||
)
|
||||
return idle_facade != null and idle_facade.checksum() == _regional_simulation.checksum()
|
||||
|
||||
|
||||
func create_save_manifest() -> SimulationSaveManifest:
|
||||
if _regional_simulation == null:
|
||||
return null
|
||||
return SimulationSaveManifest.create(create_state_record(), _regional_simulation)
|
||||
|
||||
|
||||
func serialize_save_manifest() -> String:
|
||||
var manifest := create_save_manifest()
|
||||
return manifest.to_json() if manifest != null else ""
|
||||
|
||||
|
||||
func restore_save_manifest(manifest: SimulationSaveManifest) -> bool:
|
||||
if manifest == null:
|
||||
return false
|
||||
var local_candidate := manifest.get_local_state()
|
||||
var regional_candidate := manifest.get_regional_facade()
|
||||
if local_candidate == null or regional_candidate == null:
|
||||
return false
|
||||
var previous_local := create_state_record()
|
||||
var previous_regional := _regional_simulation
|
||||
_regional_simulation = regional_candidate
|
||||
_restoring_save_manifest = true
|
||||
var restored := restore_state(local_candidate)
|
||||
_restoring_save_manifest = false
|
||||
if restored:
|
||||
_publish_restored_state()
|
||||
return true
|
||||
_regional_simulation = previous_regional
|
||||
_restoring_save_manifest = true
|
||||
var rolled_back := restore_state(previous_local)
|
||||
_restoring_save_manifest = false
|
||||
if not rolled_back:
|
||||
push_error("SimulationManager: failed to roll back rejected save manifest")
|
||||
return false
|
||||
|
||||
|
||||
func restore_state_from_json(json_text: String) -> bool:
|
||||
var record := SimulationStateRecord.from_json(json_text)
|
||||
if record == null:
|
||||
@@ -2578,8 +2677,24 @@ func restore_state(record: SimulationStateRecord) -> bool:
|
||||
conflict_system.register_npc_combatants(npcs)
|
||||
conflict_system.configure(economy, tick_interval)
|
||||
_sync_player_combatant()
|
||||
if not _restoring_save_manifest and not _initialize_regional_simulation(tick_count):
|
||||
return false
|
||||
if not _restoring_save_manifest:
|
||||
_publish_restored_state()
|
||||
return true
|
||||
|
||||
|
||||
func _publish_restored_state() -> void:
|
||||
village_changed.emit(village)
|
||||
state_restored.emit()
|
||||
|
||||
|
||||
func _initialize_regional_simulation(current_tick: int) -> bool:
|
||||
var facade := RegionalSimulationFacadeScript.create_jajce_route(simulation_seed, current_tick)
|
||||
if facade == null:
|
||||
push_error("SimulationManager: could not initialize the Jajce regional facade")
|
||||
return false
|
||||
_regional_simulation = facade
|
||||
return true
|
||||
|
||||
|
||||
|
||||
@@ -8,6 +8,15 @@ const WORLD_CORE := &"world_core"
|
||||
const LOCATION_JAJCE := &"settlement_jajce"
|
||||
const REGIONAL_WORLD_BOSNIA := &"regional_bosnia"
|
||||
const REGIONAL_LOCATION_JAJCE := &"location_jajce"
|
||||
const REGIONAL_LOCATION_TRAVNIK := &"location_travnik"
|
||||
const REGIONAL_SETTLEMENT_JAJCE := LOCATION_JAJCE
|
||||
const REGIONAL_SETTLEMENT_TRAVNIK := &"settlement_travnik"
|
||||
const REGIONAL_POLITY_JAJCE := &"polity_jajce"
|
||||
const REGIONAL_POLITY_TRAVNIK := &"polity_travnik"
|
||||
const REGIONAL_ROUTE_JAJCE_TRAVNIK := &"route_jajce_travnik"
|
||||
const REGIONAL_CARAVAN_BLUE := &"group_blue_caravan"
|
||||
const REGIONAL_PERSON_ANA := &"person_ana"
|
||||
const REGIONAL_COHORT_CARRIERS := &"cohort_carriers"
|
||||
const ACTIVITY_GUARD_POST := &"guard_post"
|
||||
const ACTIVITY_STUDY_DESK := &"study_desk"
|
||||
const ACTION_HANDLER_ACTIVITY_METRIC_DELTA := &"activity_metric_delta"
|
||||
@@ -51,6 +60,7 @@ const TARGET_FREE := &"free"
|
||||
|
||||
const RESOURCE_FOOD := &"food"
|
||||
const RESOURCE_WOOD := &"wood"
|
||||
const RESOURCE_REGIONAL_TRADE_GOODS := &"regional_trade_goods"
|
||||
|
||||
const ANIMAL_DUNJA := &"goat_dunja"
|
||||
const ANIMAL_ZORA := &"goat_zora"
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
[gd_resource type="Resource" script_class="ItemDefinition" load_steps=2 format=3]
|
||||
|
||||
[ext_resource type="Script" path="res://simulation/definitions/ItemDefinition.gd" id="1"]
|
||||
|
||||
[resource]
|
||||
script = ExtResource("1")
|
||||
item_id = &"regional_trade_goods"
|
||||
display_name = "Regional Trade Goods"
|
||||
category = &"resource"
|
||||
@@ -1,4 +1,4 @@
|
||||
[gd_resource type="Resource" script_class="SimulationContentPack" load_steps=94 format=3]
|
||||
[gd_resource type="Resource" script_class="SimulationContentPack" load_steps=95 format=3]
|
||||
|
||||
[ext_resource type="Script" path="res://simulation/definitions/SimulationContentPack.gd" id="1_pack"]
|
||||
[ext_resource type="Resource" path="res://simulation/definitions/actions/defend.tres" id="2_defend"]
|
||||
@@ -92,6 +92,7 @@
|
||||
[ext_resource type="Resource" path="res://simulation/definitions/social_consequences/commitment_broken_hostility.tres" id="89_broken_hostility"]
|
||||
[ext_resource type="Resource" path="res://simulation/definitions/social_consequences/commitment_released_obligation.tres" id="90_released_obligation"]
|
||||
[ext_resource type="Resource" path="res://simulation/definitions/social_consequences/commitment_superseded_obligation.tres" id="91_superseded_obligation"]
|
||||
[ext_resource type="Resource" path="res://simulation/definitions/items/regional_trade_goods.tres" id="93_regional_trade_goods"]
|
||||
|
||||
[resource]
|
||||
script = ExtResource("1_pack")
|
||||
@@ -101,7 +102,7 @@ required_handler_ids = [&"activity_metric_delta"]
|
||||
actions = [ExtResource("2_defend"), ExtResource("3_deposit_food"), ExtResource("4_deposit_wood"), ExtResource("5_eat"), ExtResource("6_feed_animal"), ExtResource("7_gather_food"), ExtResource("8_gather_wood"), ExtResource("9_patrol"), ExtResource("10_rest"), ExtResource("11_sleep"), ExtResource("12_study"), ExtResource("13_wander"), ExtResource("14_withdraw_food")]
|
||||
professions = [ExtResource("15_farmer"), ExtResource("16_guard"), ExtResource("17_scholar"), ExtResource("18_wanderer"), ExtResource("19_woodcutter")]
|
||||
capability_tags = [ExtResource("28_item_food"), ExtResource("29_item_medicine"), ExtResource("30_item_wood"), ExtResource("31_resource_harvestable"), ExtResource("32_storage_village")]
|
||||
items = [ExtResource("20_claw"), ExtResource("21_food"), ExtResource("22_sword"), ExtResource("23_wood"), ExtResource("27_herb")]
|
||||
items = [ExtResource("20_claw"), ExtResource("21_food"), ExtResource("22_sword"), ExtResource("23_wood"), ExtResource("27_herb"), ExtResource("93_regional_trade_goods")]
|
||||
resources = [ExtResource("33_berry_patch"), ExtResource("34_herb_patch"), ExtResource("35_tree")]
|
||||
storages = [ExtResource("36_apothecary"), ExtResource("37_pantry"), ExtResource("38_woodpile")]
|
||||
enemies = [ExtResource("24_raider"), ExtResource("25_wolf"), ExtResource("26_boar")]
|
||||
|
||||
@@ -15,16 +15,36 @@ func _init(directory: String = DEFAULT_DIRECTORY) -> void:
|
||||
|
||||
func save(manager: Node, slot_name: String = DEFAULT_SLOT) -> bool:
|
||||
last_error = ""
|
||||
if manager == null or not manager.has_method("serialize_state"):
|
||||
return _fail("Save source cannot serialize simulation state")
|
||||
if (
|
||||
manager == null
|
||||
or not (
|
||||
manager.has_method("serialize_save_manifest") or manager.has_method("serialize_state")
|
||||
)
|
||||
):
|
||||
return _fail("Save source cannot serialize authoritative state")
|
||||
if not _is_valid_slot_name(slot_name):
|
||||
return _fail("Invalid save slot name")
|
||||
|
||||
var json_text: String = manager.serialize_state()
|
||||
var use_manifest := manager.has_method("serialize_save_manifest")
|
||||
if use_manifest and manager.has_method("uses_combined_save_manifest"):
|
||||
use_manifest = bool(manager.call("uses_combined_save_manifest"))
|
||||
if (
|
||||
not use_manifest
|
||||
and manager.has_method("can_serialize_local_state_only")
|
||||
and not bool(manager.call("can_serialize_local_state_only"))
|
||||
):
|
||||
return _fail("Save source has regional authority outside its active manifest scope")
|
||||
var json_text: String
|
||||
if use_manifest:
|
||||
json_text = String(manager.call("serialize_save_manifest"))
|
||||
elif manager.has_method("serialize_state"):
|
||||
json_text = String(manager.call("serialize_state"))
|
||||
else:
|
||||
return _fail("Save source cannot serialize its active authority scope")
|
||||
if json_text.to_utf8_buffer().size() > MAX_SAVE_BYTES:
|
||||
return _fail("Save exceeds the supported size limit")
|
||||
if SimulationStateRecord.from_json(json_text) == null:
|
||||
return _fail("Simulation produced an invalid save record")
|
||||
if not _is_valid_save_json(json_text):
|
||||
return _fail("Simulation produced an invalid save payload")
|
||||
if not _ensure_directory():
|
||||
return false
|
||||
|
||||
@@ -39,7 +59,7 @@ func save(manager: Node, slot_name: String = DEFAULT_SLOT) -> bool:
|
||||
file.store_string(json_text)
|
||||
file.flush()
|
||||
file.close()
|
||||
if _read_record(temporary_path) == null:
|
||||
if _read_payload(temporary_path).is_empty():
|
||||
_remove_if_present(temporary_path)
|
||||
return _fail("Temporary save failed validation")
|
||||
|
||||
@@ -60,19 +80,29 @@ func save(manager: Node, slot_name: String = DEFAULT_SLOT) -> bool:
|
||||
|
||||
func load_into(manager: Node, slot_name: String = DEFAULT_SLOT) -> bool:
|
||||
last_error = ""
|
||||
if manager == null or not manager.has_method("restore_state"):
|
||||
return _fail("Save target cannot restore simulation state")
|
||||
if manager == null:
|
||||
return _fail("Save target is missing")
|
||||
if not _is_valid_slot_name(slot_name):
|
||||
return _fail("Invalid save slot name")
|
||||
|
||||
var final_path := get_slot_path(slot_name)
|
||||
var record := _read_record(final_path)
|
||||
if record == null:
|
||||
record = _read_record(final_path + ".bak")
|
||||
if record == null:
|
||||
var payload := _read_payload(final_path)
|
||||
if payload.is_empty():
|
||||
payload = _read_payload(final_path + ".bak")
|
||||
if payload.is_empty():
|
||||
return _fail("Save slot is missing, invalid, or unsupported")
|
||||
if not bool(manager.restore_state(record)):
|
||||
return _fail("Simulation refused the validated save record")
|
||||
if payload.has("manifest"):
|
||||
if not manager.has_method("restore_save_manifest"):
|
||||
return _fail("Save target cannot restore a combined save manifest")
|
||||
if not bool(manager.call("restore_save_manifest", payload["manifest"])):
|
||||
return _fail("Simulation refused the validated save manifest")
|
||||
elif payload.has("local_state"):
|
||||
if not manager.has_method("restore_state"):
|
||||
return _fail("Save target cannot restore legacy local state")
|
||||
if not bool(manager.call("restore_state", payload["local_state"])):
|
||||
return _fail("Simulation refused the validated legacy save record")
|
||||
else:
|
||||
return _fail("Save slot payload kind is unsupported")
|
||||
return true
|
||||
|
||||
|
||||
@@ -80,7 +110,10 @@ func has_slot(slot_name: String = DEFAULT_SLOT) -> bool:
|
||||
if not _is_valid_slot_name(slot_name):
|
||||
return false
|
||||
var final_path := get_slot_path(slot_name)
|
||||
return _read_record(final_path) != null or _read_record(final_path + ".bak") != null
|
||||
return (
|
||||
not _read_payload(final_path).is_empty()
|
||||
or not _read_payload(final_path + ".bak").is_empty()
|
||||
)
|
||||
|
||||
|
||||
func delete_slot(slot_name: String = DEFAULT_SLOT) -> bool:
|
||||
@@ -97,18 +130,29 @@ func get_slot_path(slot_name: String = DEFAULT_SLOT) -> String:
|
||||
return save_directory.path_join(slot_name + ".json")
|
||||
|
||||
|
||||
func _read_record(path: String) -> SimulationStateRecord:
|
||||
func _read_payload(path: String) -> Dictionary:
|
||||
if not FileAccess.file_exists(path):
|
||||
return null
|
||||
return {}
|
||||
var file := FileAccess.open(path, FileAccess.READ)
|
||||
if file == null:
|
||||
return null
|
||||
return {}
|
||||
if file.get_length() > MAX_SAVE_BYTES:
|
||||
file.close()
|
||||
return null
|
||||
return {}
|
||||
var json_text := file.get_as_text()
|
||||
file.close()
|
||||
return SimulationStateRecord.from_json(json_text)
|
||||
var manifest := SimulationSaveManifest.from_json(json_text)
|
||||
if manifest != null:
|
||||
return {"manifest": manifest}
|
||||
var local_state := SimulationStateRecord.from_json(json_text)
|
||||
return {"local_state": local_state} if local_state != null else {}
|
||||
|
||||
|
||||
func _is_valid_save_json(json_text: String) -> bool:
|
||||
return (
|
||||
SimulationSaveManifest.from_json(json_text) != null
|
||||
or SimulationStateRecord.from_json(json_text) != null
|
||||
)
|
||||
|
||||
|
||||
func _ensure_directory() -> bool:
|
||||
|
||||
@@ -0,0 +1,325 @@
|
||||
class_name SimulationSaveManifest
|
||||
extends RefCounted
|
||||
|
||||
const SCHEMA_NAME := "the_steward.save_manifest"
|
||||
const SCHEMA_VERSION := 1
|
||||
const MANIFEST_FIELDS := [
|
||||
"schema",
|
||||
"schema_version",
|
||||
"local_state",
|
||||
"local_checksum",
|
||||
"regional_state",
|
||||
"regional_checksum",
|
||||
]
|
||||
const MAX_SAFE_JSON_INTEGER := 9007199254740991.0
|
||||
|
||||
var _local_state: SimulationStateRecord
|
||||
var _regional_facade: RegionalSimulationFacade
|
||||
var _local_checksum := ""
|
||||
var _regional_checksum := ""
|
||||
|
||||
|
||||
static func create(
|
||||
local_state: SimulationStateRecord, regional_facade: RegionalSimulationFacade
|
||||
) -> SimulationSaveManifest:
|
||||
if local_state == null or regional_facade == null:
|
||||
return null
|
||||
var local_copy := _canonicalize_local_state(local_state)
|
||||
var regional_copy := _parse_regional_facade(regional_facade.to_dictionary())
|
||||
if local_copy == null or regional_copy == null:
|
||||
return null
|
||||
return _assemble(local_copy, regional_copy)
|
||||
|
||||
|
||||
static func from_dictionary(record_data: Dictionary) -> SimulationSaveManifest:
|
||||
return _from_dictionary(record_data, false)
|
||||
|
||||
|
||||
static func _from_dictionary(
|
||||
record_data: Dictionary, allow_json_number_types: bool
|
||||
) -> SimulationSaveManifest:
|
||||
if not _has_exact_fields(record_data, MANIFEST_FIELDS):
|
||||
return null
|
||||
if (
|
||||
not record_data["schema"] is String
|
||||
or String(record_data["schema"]) != SCHEMA_NAME
|
||||
or not record_data["schema_version"] is int
|
||||
or int(record_data["schema_version"]) != SCHEMA_VERSION
|
||||
or not record_data["local_state"] is Dictionary
|
||||
or not record_data["local_checksum"] is String
|
||||
or not record_data["regional_state"] is Dictionary
|
||||
or not record_data["regional_checksum"] is String
|
||||
):
|
||||
return null
|
||||
|
||||
var local_state := _parse_local_state(record_data["local_state"], allow_json_number_types)
|
||||
var regional_facade := _parse_regional_facade(
|
||||
record_data["regional_state"], allow_json_number_types
|
||||
)
|
||||
if local_state == null or regional_facade == null:
|
||||
return null
|
||||
var local_checksum := _checksum_dictionary(local_state.to_dictionary())
|
||||
var regional_checksum := _checksum_dictionary(regional_facade.to_dictionary())
|
||||
if (
|
||||
String(record_data["local_checksum"]) != local_checksum
|
||||
or String(record_data["regional_checksum"]) != regional_checksum
|
||||
):
|
||||
return null
|
||||
return _assemble(local_state, regional_facade)
|
||||
|
||||
|
||||
static func from_json(json_text: String) -> SimulationSaveManifest:
|
||||
var parser := JSON.new()
|
||||
if parser.parse(json_text) != OK:
|
||||
return null
|
||||
var parsed: Variant = parser.data
|
||||
if not parsed is Dictionary:
|
||||
return null
|
||||
var normalized: Dictionary = parsed.duplicate(true)
|
||||
if not normalized.has("schema_version"):
|
||||
return null
|
||||
var normalized_version: Variant = _restore_json_integer_types(normalized["schema_version"])
|
||||
if not normalized_version is int:
|
||||
return null
|
||||
normalized["schema_version"] = normalized_version
|
||||
if normalized.get("local_state") is Dictionary:
|
||||
var normalized_local_state: Variant = _normalize_json_local_boundary_integers(
|
||||
normalized["local_state"]
|
||||
)
|
||||
if normalized_local_state == null:
|
||||
return null
|
||||
normalized["local_state"] = normalized_local_state
|
||||
if normalized.get("regional_state") is Dictionary:
|
||||
normalized["regional_state"] = _restore_json_integer_types(normalized["regional_state"])
|
||||
return _from_dictionary(normalized, true)
|
||||
|
||||
|
||||
func to_dictionary() -> Dictionary:
|
||||
return {
|
||||
"schema": SCHEMA_NAME,
|
||||
"schema_version": SCHEMA_VERSION,
|
||||
"local_state": _local_state.to_dictionary(),
|
||||
"local_checksum": _local_checksum,
|
||||
"regional_state": _regional_facade.to_dictionary(),
|
||||
"regional_checksum": _regional_checksum,
|
||||
}
|
||||
|
||||
|
||||
func to_json() -> String:
|
||||
return JSON.stringify(to_dictionary())
|
||||
|
||||
|
||||
func get_local_state() -> SimulationStateRecord:
|
||||
return _parse_local_state(_local_state.to_dictionary())
|
||||
|
||||
|
||||
func get_regional_facade() -> RegionalSimulationFacade:
|
||||
return _parse_regional_facade(_regional_facade.to_dictionary())
|
||||
|
||||
|
||||
func checksum() -> String:
|
||||
return to_json().sha256_text()
|
||||
|
||||
|
||||
static func _assemble(
|
||||
local_state: SimulationStateRecord, regional_facade: RegionalSimulationFacade
|
||||
) -> SimulationSaveManifest:
|
||||
if not _boundaries_match(local_state, regional_facade):
|
||||
return null
|
||||
var manifest := SimulationSaveManifest.new()
|
||||
manifest._local_state = local_state
|
||||
manifest._regional_facade = regional_facade
|
||||
manifest._local_checksum = _checksum_dictionary(local_state.to_dictionary())
|
||||
manifest._regional_checksum = _checksum_dictionary(regional_facade.to_dictionary())
|
||||
return manifest
|
||||
|
||||
|
||||
static func _parse_local_state(
|
||||
value: Variant, allow_json_number_types: bool = false
|
||||
) -> SimulationStateRecord:
|
||||
if not value is Dictionary:
|
||||
return null
|
||||
var source: Dictionary = value
|
||||
var parsed := SimulationStateRecord.from_dictionary(source)
|
||||
if (
|
||||
parsed == null
|
||||
or not _canonical_tree_matches(parsed.to_dictionary(), source, allow_json_number_types)
|
||||
):
|
||||
return null
|
||||
return parsed
|
||||
|
||||
|
||||
static func _canonicalize_local_state(local_state: SimulationStateRecord) -> SimulationStateRecord:
|
||||
var source := local_state.to_dictionary()
|
||||
var source_simulation: Variant = source.get("simulation")
|
||||
if not source_simulation is Dictionary:
|
||||
return null
|
||||
for field: String in ["seed", "tick_count"]:
|
||||
if not source_simulation.get(field) is int:
|
||||
return null
|
||||
var parser := JSON.new()
|
||||
if parser.parse(JSON.stringify(source)) != OK:
|
||||
return null
|
||||
if not parser.data is Dictionary:
|
||||
return null
|
||||
var canonical_data: Variant = _normalize_json_local_boundary_integers(parser.data)
|
||||
if canonical_data == null:
|
||||
return null
|
||||
var canonical := SimulationStateRecord.from_dictionary(canonical_data)
|
||||
if canonical == null:
|
||||
return null
|
||||
return _parse_local_state(canonical.to_dictionary())
|
||||
|
||||
|
||||
static func _parse_regional_facade(
|
||||
value: Variant, allow_json_number_types: bool = false
|
||||
) -> RegionalSimulationFacade:
|
||||
if not value is Dictionary:
|
||||
return null
|
||||
var source: Dictionary = value
|
||||
var parsed := RegionalSimulationFacade.from_dictionary(source)
|
||||
if (
|
||||
parsed == null
|
||||
or not _canonical_tree_matches(parsed.to_dictionary(), source, allow_json_number_types)
|
||||
):
|
||||
return null
|
||||
return parsed
|
||||
|
||||
|
||||
static func _boundaries_match(
|
||||
local_state: SimulationStateRecord, regional_facade: RegionalSimulationFacade
|
||||
) -> bool:
|
||||
var local_world_id := StringName(local_state.simulation.get("world_id", &""))
|
||||
var local_location_id := StringName(local_state.simulation.get("location_id", &""))
|
||||
var local_seed_value: Variant = local_state.simulation.get("seed")
|
||||
var local_tick_value: Variant = local_state.simulation.get("tick_count")
|
||||
if not local_seed_value is int or not local_tick_value is int:
|
||||
return false
|
||||
var local_seed := int(local_seed_value)
|
||||
var local_tick := int(local_tick_value)
|
||||
if local_world_id.is_empty() or local_location_id.is_empty() or local_tick < 0:
|
||||
return false
|
||||
if (
|
||||
local_seed != regional_facade.get_world_seed()
|
||||
or local_location_id != regional_facade.get_active_location_id()
|
||||
or local_tick != regional_facade.get_last_processed_tick()
|
||||
):
|
||||
return false
|
||||
var service: RegionalCaravanService = regional_facade.get_service()
|
||||
if service == null:
|
||||
return false
|
||||
var world_state := service.get_world_state()
|
||||
if world_state == null or world_state.get_world_id() != local_world_id:
|
||||
return false
|
||||
var location := world_state.get_location(local_location_id)
|
||||
return location != null and location.get_world_id() == local_world_id
|
||||
|
||||
|
||||
static func _checksum_dictionary(value: Dictionary) -> String:
|
||||
return JSON.stringify(_canonical_checksum_value(value)).sha256_text()
|
||||
|
||||
|
||||
static func _canonical_checksum_value(value: Variant) -> Variant:
|
||||
if _is_number(value):
|
||||
var number := float(value)
|
||||
if is_finite(number) and number == floorf(number) and absf(number) <= MAX_SAFE_JSON_INTEGER:
|
||||
return int(number)
|
||||
return number
|
||||
match typeof(value):
|
||||
TYPE_ARRAY:
|
||||
var normalized_array: Array = []
|
||||
for item: Variant in value:
|
||||
normalized_array.append(_canonical_checksum_value(item))
|
||||
return normalized_array
|
||||
TYPE_DICTIONARY:
|
||||
var normalized_dictionary: Dictionary = {}
|
||||
for key: Variant in value:
|
||||
normalized_dictionary[key] = _canonical_checksum_value(value[key])
|
||||
return normalized_dictionary
|
||||
return value
|
||||
|
||||
|
||||
static func _has_exact_fields(value: Dictionary, fields: Array) -> bool:
|
||||
return value.size() == fields.size() and value.has_all(fields)
|
||||
|
||||
|
||||
static func _canonical_tree_matches(
|
||||
canonical: Variant, source: Variant, allow_json_number_types: bool
|
||||
) -> bool:
|
||||
if allow_json_number_types and _is_number(canonical) and _is_number(source):
|
||||
return float(canonical) == float(source)
|
||||
if typeof(canonical) != typeof(source):
|
||||
return false
|
||||
match typeof(canonical):
|
||||
TYPE_ARRAY:
|
||||
if canonical.size() != source.size():
|
||||
return false
|
||||
for index in canonical.size():
|
||||
if not _canonical_tree_matches(
|
||||
canonical[index], source[index], allow_json_number_types
|
||||
):
|
||||
return false
|
||||
return true
|
||||
TYPE_DICTIONARY:
|
||||
if canonical.size() != source.size():
|
||||
return false
|
||||
for key: Variant in canonical:
|
||||
if (
|
||||
not source.has(key)
|
||||
or not _canonical_tree_matches(
|
||||
canonical[key], source[key], allow_json_number_types
|
||||
)
|
||||
):
|
||||
return false
|
||||
return true
|
||||
return canonical == source
|
||||
|
||||
|
||||
static func _is_number(value: Variant) -> bool:
|
||||
return value is int or value is float
|
||||
|
||||
|
||||
static func _normalize_json_local_boundary_integers(value: Dictionary) -> Variant:
|
||||
var normalized := value.duplicate(true)
|
||||
var simulation: Variant = normalized.get("simulation")
|
||||
if not simulation is Dictionary:
|
||||
return normalized
|
||||
for field: String in ["seed", "tick_count"]:
|
||||
if not simulation.has(field):
|
||||
continue
|
||||
var normalized_value: Variant = _restore_json_integer_types(simulation[field])
|
||||
if not normalized_value is int:
|
||||
return null
|
||||
simulation[field] = normalized_value
|
||||
normalized["simulation"] = simulation
|
||||
return normalized
|
||||
|
||||
|
||||
static func _restore_json_integer_types(value: Variant) -> Variant:
|
||||
match typeof(value):
|
||||
TYPE_FLOAT:
|
||||
var number := float(value)
|
||||
if (
|
||||
is_finite(number)
|
||||
and number == floorf(number)
|
||||
and absf(number) <= MAX_SAFE_JSON_INTEGER
|
||||
):
|
||||
return int(number)
|
||||
return number
|
||||
TYPE_ARRAY:
|
||||
var normalized_array: Array = []
|
||||
for item: Variant in value:
|
||||
normalized_array.append(_restore_json_integer_types(item))
|
||||
return normalized_array
|
||||
TYPE_DICTIONARY:
|
||||
var normalized_dictionary: Dictionary = {}
|
||||
for key: Variant in value:
|
||||
var normalized_value: Variant = _restore_json_integer_types(value[key])
|
||||
if String(key).ends_with("_ledger") and normalized_value is Dictionary:
|
||||
for ledger_key: Variant in normalized_value:
|
||||
if not _is_number(normalized_value[ledger_key]):
|
||||
return null
|
||||
normalized_value[ledger_key] = float(normalized_value[ledger_key])
|
||||
normalized_dictionary[key] = normalized_value
|
||||
return normalized_dictionary
|
||||
return value
|
||||
@@ -0,0 +1 @@
|
||||
uid://cd8g03rq1kh16
|
||||
@@ -0,0 +1,370 @@
|
||||
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
|
||||
@@ -0,0 +1 @@
|
||||
uid://brlai5b70t6e1
|
||||
Reference in New Issue
Block a user