211 lines
15 KiB
GDScript
211 lines
15 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))
|
|
return expect(result.is_success(), String(kind) + ": " + result.message)
|
|
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())
|
|
func setup(operations: bool) -> bool:
|
|
registry(true)
|
|
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)
|
|
for y: int in [1,2,3]:
|
|
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(runtime.definition_registry.get_facility(&"guest_stairs").build_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) or not build(&"premium_room",Vector3i(5,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() == 2,"two paid active jobs"): 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")
|
|
for id: StringName in identity[&"jobs"]:
|
|
var job: ConstructionJob = runtime.construction_service.get_job(id)
|
|
expect(not job.is_terminal() and job.progress > 0 and job.reserved_cells.size() == 6,"old-size paid 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}
|