Files

242 lines
18 KiB
GDScript

extends RefCounted
## All seeds, actor IDs, positions, food quantities and schedules are diagnostic inputs.
var checks := 0
var failures := 0
var runtime: Variant
var clock: Variant
var state: Variant
var directory: String
var mode: String
var slot: String
var serial := 0
var events: Array = []
var identity: Dictionary = {}
const MEMBERS := [SaveService.WORLD_FILE, SaveService.MANIFEST_FILE, SaveService.THUMBNAIL_FILE, SaveService.JOURNAL_FILE]
func expect(ok: bool, label: String) -> bool:
checks += 1
if not ok:
failures += 1
push_error("P08 mixed: " + label)
return ok
func command(kind: StringName, payload: Dictionary) -> bool:
serial += 1
var result: CommandResult = runtime.submit_command(kind, payload, &"player", &"", 0, -1, StringName("p08.%d" % serial))
var label := String(kind) + ": " + result.message
if not result.is_success(): label += " payload=" + var_to_str(payload) + " details=" + var_to_str(result.details)
return expect(result.is_success(), label)
func registry(old: bool) -> void:
var paths: Array[String] = FacilityCatalog.FACILITY_PATHS + FacilityCatalog.INFRASTRUCTURE_FACILITY_PATHS + FacilityCatalog.RESCUE_FACILITY_PATHS
paths.append_array(["res://tests/fixtures/legacy_premium_room.tres", "res://tests/fixtures/legacy_staff_dormitory.tres"] if old else ["res://data/facilities/expansion/premium_room.tres", "res://data/facilities/expansion/staff_dormitory.tres"])
expect(runtime.definition_registry.rebuild_from_paths(paths)[&"valid"], "definition registry")
func build(id: StringName, cell: Vector3i, complete := true) -> bool:
if not command(&"build_facility", {&"definition_id":id, &"origin":cell}): return false
if complete: clock.advance_minutes(runtime.definition_registry.get_facility(id).build_minutes)
return true
func instance(id: StringName) -> FacilityInstance:
for room: FacilityInstance in runtime.facility_service.get_instances():
if room.definition_id == id: return room
return null
## Scalar text avoids loss of integer precision; raw Variant bytes accompany JSON.
func typed(value: Variant) -> Dictionary:
var result := {"type":type_string(typeof(value))}
if value is Dictionary:
result["entries"] = []
for key: Variant in value: result["entries"].append([typed(key),typed(value[key])])
elif value is Array:
result["element_type"] = value.get_typed_builtin()
result["element_class"] = String(value.get_typed_class_name())
result["items"] = []
for item: Variant in value: result["items"].append(typed(item))
else: result["text"] = var_to_str(value)
return result
func put(name: String, value: Variant) -> void:
var path := directory.path_join(name)
if not expect(not FileAccess.file_exists(path+".var") and not FileAccess.file_exists(path+".json"), "fresh proof "+name): return
var file := FileAccess.open(path+".var",FileAccess.WRITE)
if not expect(file != null,"Variant output"): return
file.store_buffer(var_to_bytes(value)); file.close()
file = FileAccess.open(path+".json",FileAccess.WRITE)
if not expect(file != null,"typed output"): return
file.store_string(JSON.stringify(typed(value))); file.close()
func get_proof(name: String) -> Variant:
return bytes_to_var(FileAccess.get_file_as_bytes(directory.path_join(name+".var")))
func capture(name: String, value: Variant) -> void:
put(mode+"-"+name,value)
if mode == "reader": expect(var_to_bytes(value) == var_to_bytes(get_proof("writer-"+name)),"whole cross-PID "+name)
func copy_package(source: String, destination: String) -> void:
expect(DirAccess.make_dir_recursive_absolute(destination) == OK,"package directory")
for member: String in MEMBERS:
var data := FileAccess.get_file_as_bytes(source.path_join(member))
if not expect(not data.is_empty() and not FileAccess.file_exists(destination.path_join(member)),"fresh complete member "+member): continue
var file := FileAccess.open(destination.path_join(member),FileAccess.WRITE)
if not expect(file != null,"member writable"): continue
file.store_buffer(data); file.close()
expect(data == FileAccess.get_file_as_bytes(destination.path_join(member)),"whole member copy verified")
func packages() -> Dictionary:
var hashes := {}
for suffix: String in ["",SaveService.MIGRATION_BACKUP_SUFFIX]:
for member: String in MEMBERS: hashes[suffix+"/"+member] = FileAccess.get_sha256(ProjectSettings.globalize_path(slot+suffix).path_join(member))
return hashes
func observe(event: SimulationDomainEvent) -> void: events.append(event.to_dictionary())
## Controlled diagnostic input; costs come only from registered domain definitions.
func prepare_diagnostic_budget() -> bool:
var planned: Array[StringName] = [&"warehouse",&"kitchen",&"infirmary",&"staff_door",&"reception",&"reception",&"guest_elevator",&"staff_dormitory",&"premium_room",&"premium_room",&"service_stairs",&"service_stairs",&"service_stairs",&"guest_stairs",&"guest_stairs",&"guest_stairs"]
var needed := {&"gold":0,&"materials":0}
var costs: Array = []
for id: StringName in planned:
var definition: FacilityDefinition = runtime.definition_registry.get_facility(id)
if not expect(definition != null,"budget definition "+String(id)): return false
costs.append({&"definition_id":id,&"cost":definition.construction_cost.duplicate(true)})
for resource: StringName in needed: needed[resource] += int(definition.construction_cost.get(resource,0))
var signing := int(runtime.mvp_content_catalog.staff_hire_payload(&"staff_gina").get(&"signing_cost",0))
var food := 8 * int(SupplyCatalog.get_item(SupplyCatalog.RAW_FOOD)[&"unit_price"])
needed[&"gold"] += signing + food
var deltas := {}
for resource: StringName in needed:
var deficit: int = maxi(0,int(needed[resource])-state.get_resource(resource))
if deficit > 0: deltas[resource] = deficit
identity[&"diagnostic_budget"] = {&"seed":42588,&"costs":costs,&"signing":signing,&"food_quantity":8,&"food_cost":food,&"needed":needed,&"initial":state.resources.duplicate(true),&"previous_change_resources":{},&"deltas":deltas}
return deltas.is_empty() or command(&"change_resources",{&"deltas":deltas})
func setup(operations: bool) -> bool:
registry(true)
if not prepare_diagnostic_budget(): return false
if operations and not expect(runtime.enable_production_operations().get(&"ok",false),"Operations enabled at Day 1 06:00"): return false
for row: Array in [[&"warehouse",Vector3i(-8,0,1)],[&"kitchen",Vector3i(2,0,1)],[&"infirmary",Vector3i(6,0,1)],[&"staff_door",Vector3i(0,0,1)],[&"reception",Vector3i(-6,0,0)],[&"reception",Vector3i(5,0,0)],[&"guest_elevator",Vector3i.ZERO]]:
if not build(row[0],row[1],false): return false
clock.advance_minutes(180)
# Completed stairs supply the next floor's landing through the domain projector.
# Ground-floor landings come from the initialized world; never inject path cells.
var stair_minutes: int = maxi(runtime.definition_registry.get_facility(&"service_stairs").build_minutes,runtime.definition_registry.get_facility(&"guest_stairs").build_minutes)
for y: int in [1,2,3]:
if not expect(runtime.path_service.has_cell(Vector3i(-9,y-1,1)) and runtime.path_service.has_cell(Vector3i(4,y-1,0)),"completed lower stair landings for floor %d" % y): return false
if not build(&"service_stairs",Vector3i(-9,y,1),false) or not build(&"guest_stairs",Vector3i(4,y,0),false): return false
clock.advance_minutes(stair_minutes)
if not build(&"staff_dormitory",Vector3i(-8,2,1)): return false
identity[&"dorm"] = instance(&"staff_dormitory").instance_id
if not build(&"premium_room",Vector3i(-6,2,0),false): return false
clock.advance_minutes(7)
identity[&"jobs"] = []
for job: ConstructionJob in runtime.construction_service.get_jobs():
if not job.is_terminal(): identity[&"jobs"].append(job.job_id)
if not expect(identity[&"jobs"].size() == 1,"one paid progressing job"): return false
var warehouse := instance(&"warehouse")
var kitchen := instance(&"kitchen")
if not expect(warehouse != null and kitchen != null,"completed logistics endpoints"): return false
if not command(&"hire_staff_candidate",{&"candidate_id":&"staff_gina"}): return false
if not command(&"set_staff_work_assignment",{&"staff_id":&"staff_gina",&"facility_id":warehouse.instance_id,&"template_id":&"DAY"}): return false
if not command(&"purchase_supplies",{&"warehouse_id":warehouse.instance_id,&"item_id":SupplyCatalog.RAW_FOOD,&"quantity":8}): return false
if not command(&"set_kitchen_production",{&"facility_id":kitchen.instance_id,&"enabled":true,&"target_stock":4}): return false
var cargo_ready := false
for minute: int in range(60):
clock.advance_minutes(1)
if not runtime.staff_service.get_staff(&"staff_gina").carried_inventory_id.is_empty(): cargo_ready = true; break
if not expect(cargo_ready,"real purchased cargo picked up"): return false
for id: StringName in [&"patient",&"carrier"]:
if not command(&"register_staff",{&"staff_id":id,&"role_id":&"porter",&"options":{&"current_cell":Vector3i(-3,0,1) if id == &"patient" else Vector3i(-1,0,1),&"health":0 if id == &"patient" else 100,&"state":StaffRuntime.State.AVAILABLE,&"role_tags":[&"medic"],&"permitted_task_tags":[&"rescue"]}}): return false
var carrying := false
for minute: int in range(6):
clock.advance_minutes(1)
for task: StaffTask in runtime.staff_service.get_tasks():
if task.task_type == &"rescue_transport" and task.state == StaffTask.State.MOVING:
carrying = true; identity[&"rescue_task"] = task.task_id
if carrying: break
if not expect(carrying,"actual rescue transport"): return false
for index: int in range(5):
if not command(&"spawn_guest",{&"guest_id":StringName("p08.passenger.%d" % index),&"guest_type_id":&"traveler",&"gold":100,&"options":{&"current_cell":Vector3i.ZERO,&"state":GuestRuntime.State.MOVING,&"state_entered_at":clock.absolute_minute-1,&"planned_exit_at":clock.absolute_minute+2000,&"path_handle":{&"target_cell":Vector3i(0,1,0)}}}): return false
clock.advance_minutes(1)
if not command(&"spawn_guest",{&"guest_id":&"p08.walker",&"guest_type_id":&"traveler",&"gold":100,&"options":{&"current_cell":Vector3i(4,0,0),&"state":GuestRuntime.State.MOVING,&"state_entered_at":clock.absolute_minute-1,&"planned_exit_at":clock.absolute_minute+2000,&"path_handle":{&"target_cell":Vector3i(4,3,0)}}}): return false
clock.advance_minutes(1)
identity[&"ev_requests"] = []
for trip: Dictionary in runtime.elevators.get_report()[&"trips"]: identity[&"ev_requests"].append(trip[&"request_id"])
identity[&"logistics_requests"] = []
for request: LogisticsRequest in runtime.logistics_requests.values():
if not request.is_terminal(): identity[&"logistics_requests"].append(request.request_id)
expect(not runtime.staff_service.get_staff(&"staff_gina").carried_inventory_id.is_empty(),"cargo in SAME save as patient and EV")
expect(runtime.staff_service.get_staff(&"patient").health == 0,"patient remains untreated")
var car: Dictionary = runtime.elevators.get_report()[&"shafts"][0]
expect(car[&"passenger_count"] == 4 and car[&"waiting_count"] == 1,"passengers and queue coexist")
expect(runtime.create_snapshot()[&"systems"][&"runtime"][&"movement_credits"].size() > 0,"fractional movement credit")
if not build(&"premium_room",Vector3i(5,2,0),false): return false
for candidate: ConstructionJob in runtime.construction_service.get_jobs():
if not candidate.is_terminal() and not identity[&"jobs"].has(candidate.job_id): identity[&"jobs"].append(candidate.job_id)
if not expect(identity[&"jobs"].size() == 2,"two paid active jobs"): return false
identity[&"starting_job"] = identity[&"jobs"][1]
for id: StringName in identity[&"jobs"]:
var job: ConstructionJob = runtime.construction_service.get_job(id)
expect(not job.is_terminal() and (job.progress == 0 if id == identity[&"starting_job"] else job.progress > 0) and not job.construction_cost.is_empty() and job.reserved_cells.size() == 6,"old-size paid starting/progressing jobs remain active")
return failures == 0
func invariant() -> void:
expect(runtime.staff_service.validate_invariants()[&"ok"],"staff ownership")
expect(SupplyLogisticsBridge.validate_crosslinks(runtime.logistics_requests,runtime.staff_service,runtime.inventory_service,runtime.facility_service),"cargo crosslinks")
var snap: Dictionary = runtime.create_snapshot()
expect(runtime.restore_snapshot(snap),"whole Runtime ownership/support/transport validation")
expect(var_to_bytes(snap) == var_to_bytes(runtime.create_snapshot()),"whole canonical typed state")
func run(tree: SceneTree, output: String, role: String, operations: bool, refusal: bool) -> Dictionary:
directory = output; mode = role
slot = "user://p08-%d/formal" % Time.get_ticks_usec()
tree.root.get_node("GameClock").set_paused(true); tree.root.get_node("GameClock").set_process(false)
tree.root.get_node("SimulationRuntime").configure_save_policy({&"enabled":false,&"daily_checkpoint_enabled":false})
runtime = load("res://autoload/simulation_runtime.gd").new()
clock = load("res://autoload/game_clock.gd").new(); state = load("res://autoload/game_state.gd").new()
clock.reset(1,6,0); state.reset(); expect(runtime.initialize(clock,state,42588)[&"valid"],"current initialization")
clock.set_paused(true); runtime.configure_save_policy({&"enabled":false,&"daily_checkpoint_enabled":false,&"auto_migrate":false})
if mode == "writer":
expect(DirAccess.make_dir_recursive_absolute(directory) == OK,"proof directory")
if not setup(operations): return finish()
registry(false)
var baseline: Dictionary = runtime.create_snapshot()
expect(SaveService.save_snapshot(slot,baseline)[&"ok"],"formal original saved")
if refusal:
var initial: Dictionary = runtime.migrate_facility_dimensions_at_path(slot)
expect(initial.get(&"ok",false) and initial.get(&"backup_created",false),"real prior migration backup")
expect(runtime.staff_service.create_task(&"maintenance",10,clock.absolute_minute,{&"target_entity_id":identity[&"dorm"],&"target_cell":Vector3i(-9,3,1),&"estimated_minutes":30},&"p08.late").is_success(),"later old entrance task")
if not command(&"register_staff",{&"staff_id":&"maintainer",&"role_id":&"porter",&"options":{&"current_cell":Vector3i(-9,1,1),&"state":StaffRuntime.State.AVAILABLE}}): return finish()
expect(runtime.staff_service.assign_task_to_staff(&"p08.late",&"maintainer",clock.absolute_minute).is_success(),"real task ownership")
expect(SaveService.save_snapshot(slot,runtime.create_snapshot())[&"ok"],"obstructed source saved")
put("identity",identity); put("original",runtime.create_snapshot())
copy_package(ProjectSettings.globalize_path(slot),directory.path_join("original/formal"))
var live: Dictionary = runtime.create_snapshot()
var before := packages(); put("before-members",before)
var first: Dictionary = runtime.prepare_facility_dimension_snapshot(identity[&"jobs"][0])
expect(first.get(&"ok",false),"earlier candidate independently migratable")
put("first-candidate",first)
var preview: Dictionary = runtime.preview_facility_dimensions_at_path(slot); put("preview",preview)
var result: Dictionary = runtime.migrate_facility_dimensions_at_path(slot,&"",String(preview.get(&"source_digest",""))); put("migration",result)
expect(var_to_bytes(live) == var_to_bytes(runtime.create_snapshot()),"live unchanged")
put("after-live",runtime.create_snapshot())
if refusal:
expect(not result.get(&"ok",true) and result.get(&"details",{}).get(&"rolled_back",false) and result.get(&"details",{}).get(&"facility_id",&"") == identity[&"dorm"],"later refusal rolls back earlier candidates")
expect(before == packages(),"original and existing backup unchanged")
else: expect(preview.get(&"ok",false) and result.get(&"ok",false) and result.get(&"backup_created",false),"positive whole batch")
put("after-members",packages())
copy_package(ProjectSettings.globalize_path(slot),directory.path_join("result/formal"))
copy_package(ProjectSettings.globalize_path(slot+SaveService.MIGRATION_BACKUP_SUFFIX),directory.path_join("backup/formal"))
else:
registry(false); identity = get_proof("identity")
copy_package(directory.path_join("result/formal"),ProjectSettings.globalize_path(slot))
copy_package(directory.path_join("backup/formal"),ProjectSettings.globalize_path(slot+SaveService.MIGRATION_BACKUP_SUFFIX))
var loaded := SaveService.load_snapshot(slot)
expect(loaded.get(&"ok",false) and runtime.restore_snapshot(loaded.get(&"snapshot",{})),"formal continuation restore")
expect(runtime.get_operations_report().get(&"enabled",false) == operations,"actual Operations mode")
invariant(); capture("checkpoint",runtime.create_snapshot())
expect(runtime.domain_events.subscribe(&"*",&"p08.uncapped",Callable(self,"observe")),"uncapped events")
for minute: int in range(160):
clock.advance_minutes(1); capture("minute-%03d" % (minute+1),runtime.create_snapshot())
capture("events",events)
for id: StringName in identity[&"jobs"]:
var job: ConstructionJob = runtime.construction_service.get_job(id)
expect(job.state == ConstructionJob.State.COMPLETED and runtime.facility_service.get_instance(job.facility_instance_id).occupied_cells.size() == (6 if refusal else 4),"paid job completes once with expected shape")
expect(runtime.staff_service.get_staff(&"patient").health == 100,"patient treated")
for id: StringName in identity[&"logistics_requests"]: expect(runtime.logistics_requests[id].state == LogisticsRequest.State.COMPLETED,"original cargo delivered")
for id: StringName in identity[&"ev_requests"]:
var count := 0
for event: Dictionary in events:
if event["event_type"] == "ELEVATOR_ARRIVED" and event["payload"].get(&"request_id",&"") == id: count += 1
expect(count == 1,"original EV request completes exactly once")
invariant()
put(mode+"-process",{&"pid":OS.get_process_id(),&"operations":operations,&"refusal":refusal,&"checks":checks,&"failures":failures})
return finish()
func finish() -> Dictionary:
runtime.free(); clock.free(); state.free()
return {"checks":checks,"failures":failures}