class_name PeepingRoomService extends RefCounted ## Non-graphic state/reference machinery; production definitions remain pending. const SECTION_VERSION := 1 const ALLOWED_TARGETS := [&"public_bath", &"adult_private_room"] const OCCUPANCY_KIND := &"special_peeping_room" const ID_PREFIX := &"peeping_room" const ROOM_KEYS := [&"room_id", &"origin", &"footprint", &"cells", &"target_instance_id", &"target_definition_id", &"target_history", &"link_state", &"stopped", &"stop_reason"] const EVIDENCE_KEYS := [&"evidence_id", &"room_id", &"target_instance_id", &"source_event_key", &"minute"] var _rooms: Dictionary = {} var _evidence: Dictionary = {} var _world: WorldService var _facilities: FacilityService var _construction: ConstructionService var _path: PathService var _ids: StableIdGenerator func configure(world: WorldService, facilities: FacilityService, construction: ConstructionService, path: PathService, ids: StableIdGenerator) -> void: _world = world _facilities = facilities _construction = construction _path = path _ids = ids static func result(reason: StringName = &"OK") -> Dictionary: return {&"ok": reason == &"OK", &"reason": reason} func is_empty() -> bool: return _rooms.is_empty() and _evidence.is_empty() func get_room(room_id: StringName) -> Dictionary: return (_rooms.get(room_id, {}) as Dictionary).duplicate(true) ## Pure validation. own_room is supplied internally only for explicit relink/restore. func validate_placement(origin: Vector3i, footprint: Vector2i = Vector2i(2, 1), target_id: StringName = &"", own_room: StringName = &"") -> Dictionary: if origin.z != WorldService.LAYER_BACK: return result(&"WRONG_LAYER") if footprint != Vector2i(2, 1): return result(&"WRONG_FOOTPRINT") if target_id == &"vip_suite": return result(&"PENDING_P10_01") var cells := _world.cells_for_footprint(origin, footprint) var targets: Array[StringName] = [] var missing := false for cell: Vector3i in cells: if not _world.is_in_bounds(cell): return result(&"OUT_OF_BOUNDS") if cell.y < 0 and not _world.is_excavated(cell): return result(&"NOT_EXCAVATED") var front := Vector3i(cell.x, cell.y, WorldService.LAYER_FRONT) if _world.get_occupant_kind(front) == &"construction": return result(&"TARGET_INCOMPLETE") var occupant := _world.get_occupant(front) if occupant.is_empty(): missing = true elif occupant not in targets: targets.append(occupant) if targets.size() > 1: return result(&"MULTIPLE_TARGETS") if missing: return result(&"TARGET_MISSING" if targets.is_empty() else &"PARTIAL_TARGET") var resolved_id := targets[0] if not target_id.is_empty() and target_id != resolved_id: return result(&"TARGET_MISMATCH") var target := _facilities.get_instance(resolved_id) if target == null: return result(&"TARGET_UNKNOWN") if target.definition_id not in ALLOWED_TARGETS: return result(&"TARGET_NOT_ALLOWED") if target.origin.z != WorldService.LAYER_FRONT: return result(&"TARGET_WRONG_LAYER") var built := _construction.get_facility(resolved_id) if built.is_empty(): return result(&"TARGET_INCOMPLETE") if built.get(&"definition_id") != target.definition_id: return result(&"TARGET_MISMATCH") for cell: Vector3i in cells: var front := Vector3i(cell.x, cell.y, WorldService.LAYER_FRONT) if front not in target.occupied_cells or front not in built[&"occupied_cells"]: return result(&"PARTIAL_TARGET") # Reject cutting an existing corridor; never construct an implicit bypass. if _world.is_walkable(cell) or _path.has_cell(cell): return result(&"CORRIDOR_CONFLICT") var occupancy := _world.validate_occupancy(cells) if not occupancy[&"ok"]: if own_room.is_empty() or _world.get_entity_cells(own_room) != cells or _world.get_entity_kind(own_room) != OCCUPANCY_KIND: return result(&"BACK_OCCUPIED") var report := result() report.merge({&"cells": cells, &"target_instance_id": resolved_id, &"target_definition_id": target.definition_id}) return report ## CONTROLLED_FIXTURE: tests only. No Runtime executor or palette calls this. func controlled_fixture_place(origin: Vector3i, target_id: StringName = &"") -> Dictionary: var report := validate_placement(origin, Vector2i(2, 1), target_id) if not report[&"ok"]: return report var room_id := _ids.next_id(ID_PREFIX) if room_id.is_empty(): return result(&"ID_EXHAUSTED") if not _world.occupy(room_id, report[&"cells"], OCCUPANCY_KIND): return result(&"BACK_OCCUPIED") _rooms[room_id] = {&"room_id": room_id, &"origin": origin, &"footprint": Vector2i(2, 1), &"cells": report[&"cells"].duplicate(), &"target_instance_id": report[&"target_instance_id"], &"target_definition_id": report[&"target_definition_id"], &"target_history": [report[&"target_instance_id"]], &"link_state": &"LINKED", &"stopped": true, &"stop_reason": &"CONTROLLED_FIXTURE"} report[&"room_id"] = room_id return report func target_removed(target_id: StringName) -> void: for room: Dictionary in _rooms.values(): if room[&"target_instance_id"] == target_id: room[&"link_state"] = &"TARGET_LOST" room[&"stopped"] = true room[&"stop_reason"] = &"TARGET_LOST" func detect_target_loss() -> void: for room: Dictionary in _rooms.values(): if room[&"link_state"] == &"TARGET_LOST": continue if not _target_still_present(room): target_removed(room[&"target_instance_id"]) func _target_still_present(room: Dictionary) -> bool: var target := _facilities.get_instance(room[&"target_instance_id"]) if target == null or target.definition_id != room[&"target_definition_id"]: return false for cell: Vector3i in room[&"cells"]: if _world.get_occupant(Vector3i(cell.x, cell.y, WorldService.LAYER_FRONT)) != target.instance_id: return false return true func relink(room_id: StringName, target_id: StringName) -> Dictionary: if not _rooms.has(room_id): return result(&"ROOM_MISSING") if target_id.is_empty(): return result(&"TARGET_MISSING") var room: Dictionary = _rooms[room_id] var report := validate_placement(room[&"origin"], room[&"footprint"], target_id, room_id) if not report[&"ok"]: return report room[&"target_instance_id"] = target_id room[&"target_definition_id"] = report[&"target_definition_id"] if target_id not in room[&"target_history"]: room[&"target_history"].append(target_id) room[&"link_state"] = &"LINKED" room[&"stopped"] = true room[&"stop_reason"] = &"RELINKED_PENDING_RESUME" return result() func _collection_precondition(room_id: StringName, source_event_key: StringName, minute: int) -> Dictionary: if String(source_event_key).strip_edges().is_empty(): return result(&"SOURCE_EVENT_KEY_REQUIRED") if minute < 0: return result(&"INVALID_MINUTE") if not _rooms.has(room_id): return result(&"ROOM_MISSING") detect_target_loss() var room: Dictionary = _rooms[room_id] # Required precedence: stopped, power, closed, construction, lost target. if room[&"stopped"]: return result(&"STOPPED") var target := _facilities.get_instance(room[&"target_instance_id"]) if target != null: if not target.power_available: return result(&"NO_POWER") if not target.operator_open or not target.is_within_opening_hours(minute): return result(&"CLOSED") if _construction.get_facility(target.instance_id).is_empty(): return result(&"UNDER_CONSTRUCTION") if room[&"link_state"] == &"TARGET_LOST" or target == null: return result(&"TARGET_LOST") return result() func record_evidence(room_id: StringName, source_event_key: StringName, minute: int) -> Dictionary: var report := _collection_precondition(room_id, source_event_key, minute) return report if not report[&"ok"] else result(&"RECORDER_ELIGIBILITY_PENDING_D04_D08_P16") ## CONTROLLED_FIXTURE: explicit test state, never a production resume command. func controlled_fixture_set_stopped(room_id: StringName, stopped: bool) -> bool: if not _rooms.has(room_id): return false detect_target_loss() var room: Dictionary = _rooms[room_id] if not stopped and room[&"link_state"] == &"TARGET_LOST": return false room[&"stopped"] = stopped room[&"stop_reason"] = &"CONTROLLED_FIXTURE" if stopped else &"" return true ## CONTROLLED_FIXTURE: sole ledger write entry point, opaque event keys only. func controlled_fixture_record_evidence(room_id: StringName, source_event_key: StringName, minute: int) -> Dictionary: var report := _collection_precondition(room_id, source_event_key, minute) if not report[&"ok"]: return report var target_id: StringName = _rooms[room_id][&"target_instance_id"] var evidence_id := _evidence_id(room_id, target_id, source_event_key) var duplicated := _evidence.has(evidence_id) if not duplicated: _evidence[evidence_id] = {&"evidence_id": evidence_id, &"room_id": room_id, &"target_instance_id": target_id, &"source_event_key": source_event_key, &"minute": minute} return {&"ok": true, &"reason": &"OK", &"evidence_id": evidence_id, &"duplicated": duplicated} static func _evidence_id(room_id: StringName, target_id: StringName, event_key: StringName) -> StringName: # Length-prefixed UTF-8 hex is injective; delimiters in input cannot alias. var pieces: Array[String] = [] for value: StringName in [room_id, target_id, event_key]: var bytes := String(value).to_utf8_buffer() pieces.append("%d:%s" % [bytes.size(), bytes.hex_encode()]) return StringName("peeping_evidence:" + ":".join(pieces)) func create_snapshot() -> Dictionary: var rooms: Array[Dictionary] = [] var evidence: Array[Dictionary] = [] var keys := _rooms.keys() keys.sort() for key: StringName in keys: rooms.append(_rooms[key].duplicate(true)) keys = _evidence.keys() keys.sort() for key: StringName in keys: evidence.append(_evidence[key].duplicate(true)) return {&"version": SECTION_VERSION, &"rooms": rooms, &"evidence": evidence} static func _exact(value: Variant, keys: Array) -> bool: if not value is Dictionary or value.size() != keys.size(): return false for key: Variant in value: if typeof(key) != TYPE_STRING_NAME or key not in keys: return false return true ## Save format 15 has a fixed purchase-key allowlist outside this task's scope. ## Validate this independent section before passing only the original keys to it. static func valid_runtime_section(data: Dictionary, minute: int) -> bool: if not data.get(&"special_peeping_rooms") is Dictionary: return false for key: Variant in data: if key == &"special_peeping_rooms" and typeof(key) != TYPE_STRING_NAME: return false for key: StringName in [&"world", &"path", &"facilities", &"construction", &"id_generator"]: if not data.get(key) is Dictionary: return false var world := WorldService.new() var path := PathService.new() var facilities := FacilityService.new() var ids := StableIdGenerator.new() if not world.restore_snapshot(data[&"world"]) or not path.restore_snapshot(data[&"path"]) or not facilities.restore_snapshot(data[&"facilities"]) or not ids.restore_snapshot(data[&"id_generator"]): return false var construction := ConstructionService.new(world) if not construction.restore_snapshot(data[&"construction"]): return false var service := PeepingRoomService.new() service.configure(world, facilities, construction, path, ids) return service.restore_section(data[&"special_peeping_rooms"], minute) ## Staged restore validates both directions of world ownership before committing. func restore_section(section: Variant, minute: int) -> bool: var rooms: Dictionary = {} var evidence: Dictionary = {} if section != null: if not _exact(section, [&"version", &"rooms", &"evidence"]): return false if typeof(section[&"version"]) != TYPE_INT or section[&"version"] != SECTION_VERSION: return false if not section[&"rooms"] is Array or not section[&"evidence"] is Array: return false if section[&"rooms"].is_empty(): return false # absence represents empty state var known: Array = _facilities.create_snapshot()[&"known_instance_ids"] for raw: Variant in section[&"rooms"]: if not _exact(raw, ROOM_KEYS): return false for key: StringName in [&"room_id", &"target_instance_id", &"target_definition_id", &"link_state", &"stop_reason"]: if typeof(raw[key]) != TYPE_STRING_NAME: return false var room_id: StringName = raw[&"room_id"] var suffix := String(room_id).trim_prefix("peeping_room.") if suffix.length() != 12 or not suffix.is_valid_int() or int(suffix) <= 0 or String(room_id) != "peeping_room.%012d" % int(suffix): return false if int(suffix) > _ids.get_counter(ID_PREFIX) or rooms.has(room_id): return false if typeof(raw[&"origin"]) != TYPE_VECTOR3I or typeof(raw[&"footprint"]) != TYPE_VECTOR2I: return false if raw[&"origin"].z != WorldService.LAYER_BACK or raw[&"footprint"] != Vector2i(2, 1): return false var cells := _world.cells_for_footprint(raw[&"origin"], raw[&"footprint"]) if not raw[&"cells"] is Array or raw[&"cells"] != cells: return false for cell: Variant in raw[&"cells"]: if typeof(cell) != TYPE_VECTOR3I: return false if _world.get_entity_cells(room_id) != cells or _world.get_entity_kind(room_id) != OCCUPANCY_KIND: return false for cell: Vector3i in cells: if _world.is_walkable(cell) or _path.has_cell(cell): return false if raw[&"target_definition_id"] not in ALLOWED_TARGETS: return false if typeof(raw[&"stopped"]) != TYPE_BOOL or not raw[&"target_history"] is Array: return false var history := {} for target: Variant in raw[&"target_history"]: if typeof(target) != TYPE_STRING_NAME or String(target) not in known or history.has(target): return false history[target] = true if not history.has(raw[&"target_instance_id"]): return false var current_target := _facilities.get_instance(raw[&"target_instance_id"]) if current_target != null and current_target.definition_id != raw[&"target_definition_id"]: return false if raw[&"link_state"] == &"TARGET_LOST": if not raw[&"stopped"] or raw[&"stop_reason"] != &"TARGET_LOST" or _target_still_present(raw): return false elif raw[&"link_state"] == &"LINKED": if not validate_placement(raw[&"origin"], raw[&"footprint"], raw[&"target_instance_id"], room_id)[&"ok"]: return false if _facilities.get_instance(raw[&"target_instance_id"]).definition_id != raw[&"target_definition_id"]: return false if raw[&"stopped"]: if raw[&"stop_reason"] not in [&"CONTROLLED_FIXTURE", &"RELINKED_PENDING_RESUME"]: return false elif raw[&"stop_reason"] != &"": return false else: return false rooms[room_id] = raw.duplicate(true) for raw: Variant in section[&"evidence"]: if not _exact(raw, EVIDENCE_KEYS): return false for key: StringName in [&"evidence_id", &"room_id", &"target_instance_id", &"source_event_key"]: if typeof(raw[key]) != TYPE_STRING_NAME or raw[key].is_empty(): return false if String(raw[&"source_event_key"]).strip_edges().is_empty(): return false if typeof(raw[&"minute"]) != TYPE_INT or raw[&"minute"] < 0 or raw[&"minute"] > minute: return false if not rooms.has(raw[&"room_id"]) or raw[&"target_instance_id"] not in rooms[raw[&"room_id"]][&"target_history"]: return false var expected := _evidence_id(raw[&"room_id"], raw[&"target_instance_id"], raw[&"source_event_key"]) if expected != raw[&"evidence_id"] or evidence.has(expected): return false evidence[expected] = raw.duplicate(true) for occupied: Dictionary in _world.create_snapshot()[&"occupancies"]: if occupied[&"kind"] == OCCUPANCY_KIND or String(occupied[&"entity_id"]).begins_with("peeping_room."): if not rooms.has(occupied[&"entity_id"]): return false _rooms = rooms _evidence = evidence return true