Record original-alignment implementation and traceability checkpoint
Capture current source, content data, tests, asset licenses, and original-specification traceability. Original implementation remains incomplete. The accepted matching UI generation passed 154 suites and 12,253 checks, with eight rendered staff-contract screens reviewed. Latest native Windows and full-original acceptance remain outstanding.
This commit is contained in:
@@ -0,0 +1,115 @@
|
||||
class_name FacilityCleaningBridge
|
||||
extends RefCounted
|
||||
|
||||
## Jobs live in StaffService and their effects in FacilityCleanlinessService.
|
||||
## No independent clock, movement, or hidden completion timer.
|
||||
const TASK_TYPE := &"facility_clean"
|
||||
var _runtime: Variant
|
||||
|
||||
func configure(runtime: Variant) -> void:
|
||||
_runtime = runtime
|
||||
|
||||
static func is_deployed(worker: StaffRuntime, room: FacilityInstance, staff: StaffService) -> bool:
|
||||
if worker == null or room == null or room.definition_id != &"cleaning_room" or worker.role_id != &"cleaner" or worker.assigned_facility_id != room.instance_id or not worker.can_take_shift(): return false
|
||||
var task := staff.get_task(worker.current_task_id)
|
||||
return task != null and task.task_type == TASK_TYPE and not task.is_terminal() and task.assigned_staff_id == worker.staff_id and worker.state in [StaffRuntime.State.MOVING_TO_TASK, StaffRuntime.State.WORKING]
|
||||
|
||||
func can_dispatch(worker: StaffRuntime) -> bool:
|
||||
if worker == null or worker.role_id != &"cleaner": return false
|
||||
var room: FacilityInstance = _runtime.facility_service.get_instance(worker.assigned_facility_id)
|
||||
return room != null and room.definition_id == &"cleaning_room" and room.operation_state == FacilityInstance.OperationState.OPERATING and _runtime.staff_commute.is_present(worker, room)
|
||||
|
||||
func task_can_work(task: StaffTask, worker: StaffRuntime) -> bool:
|
||||
if task == null or worker == null or not is_deployed(worker, _runtime.facility_service.get_instance(worker.assigned_facility_id), _runtime.staff_service): return false
|
||||
if task.state == StaffTask.State.WORKING and (worker.current_cell != task.target_cell or not _runtime.path_service.has_cell(task.target_cell) or _runtime.is_staff_in_transit(worker.staff_id)): return false
|
||||
var room: FacilityInstance = _runtime.facility_service.get_instance(worker.assigned_facility_id)
|
||||
return room.operation_state == FacilityInstance.OperationState.OPERATING and _runtime.facility_service.has_instance(task.target_entity_id) and not _runtime.facility_cleanliness.get_facility(task.target_entity_id).is_empty()
|
||||
|
||||
func tick(minute: int) -> void:
|
||||
var pending: Dictionary = {}
|
||||
for task: StaffTask in _runtime.staff_service.get_tasks():
|
||||
if task.task_type != TASK_TYPE or task.is_terminal(): continue
|
||||
var worker: StaffRuntime = _runtime.staff_service.get_staff(task.assigned_staff_id)
|
||||
if not _runtime.facility_service.has_instance(task.target_entity_id) or (worker != null and not task_can_work(task, worker)):
|
||||
_runtime.staff_service.cancel_task(task.task_id, &"CLEANING_UNAVAILABLE", minute)
|
||||
continue
|
||||
if pending.has(task.target_entity_id):
|
||||
_runtime.staff_service.cancel_task(task.task_id, &"DUPLICATE_CLEANING", minute)
|
||||
continue
|
||||
if task.is_dispatchable():
|
||||
for candidate: StaffRuntime in _runtime.staff_service.get_staff_members():
|
||||
if can_dispatch(candidate) and prepare_route(task, candidate): break
|
||||
pending[task.target_entity_id] = true
|
||||
var rules: Dictionary = _runtime.cleaning_policy.create_snapshot()
|
||||
for id: StringName in _runtime.facility_cleanliness.get_dirty_facility_ids(rules["clean_at_or_below"]):
|
||||
if pending.has(id): continue
|
||||
var facility: FacilityInstance = _runtime.facility_service.get_instance(id)
|
||||
var cells := access_cells(facility, _runtime.path_service)
|
||||
if cells.is_empty(): continue
|
||||
_runtime.staff_service.create_task(TASK_TYPE, 50, minute, {
|
||||
&"target_entity_id": id, &"target_cell": cells[0],
|
||||
&"required_role_tags": [&"cleaner"], &"task_tags": [&"cleaning"],
|
||||
&"estimated_minutes": rules["work_minutes"], &"interrupt_level": 0,
|
||||
&"preserve_progress_on_interrupt": false,
|
||||
})
|
||||
|
||||
static func access_cells(facility: FacilityInstance, path: PathService, walkable_only := true) -> Array[Vector3i]:
|
||||
var result: Array[Vector3i] = []
|
||||
if facility == null: return result
|
||||
var occupied := facility.occupied_cells.duplicate()
|
||||
if occupied.is_empty(): occupied.append(facility.origin)
|
||||
for cell: Vector3i in occupied:
|
||||
for offset: Vector3i in [Vector3i.ZERO, Vector3i.LEFT, Vector3i.RIGHT]:
|
||||
var candidate := cell + offset
|
||||
if (not walkable_only or path.has_cell(candidate)) and not result.has(candidate): result.append(candidate)
|
||||
result.sort_custom(func(a: Vector3i,b: Vector3i)->bool:
|
||||
if a.z != b.z: return a.z < b.z
|
||||
if a.y != b.y: return a.y < b.y
|
||||
return a.x < b.x)
|
||||
return result
|
||||
|
||||
func completed(task: StaffTask, worker: StaffRuntime) -> bool:
|
||||
if task == null or worker == null or task.state != StaffTask.State.COMPLETED or worker.current_cell != task.target_cell: return false
|
||||
return _runtime.facility_cleanliness.record_cleaning(task.target_entity_id, task.task_id, _runtime.cleaning_policy.create_snapshot()["recovery_per_completed_task"])
|
||||
|
||||
func status(facility_id: StringName) -> String:
|
||||
for task: StaffTask in _runtime.staff_service.get_tasks():
|
||||
if task.task_type != TASK_TYPE or task.target_entity_id != facility_id or task.is_terminal(): continue
|
||||
if task.state == StaffTask.State.WORKING:
|
||||
var rate := work_units_per_minute(_runtime.staff_service.get_staff(task.assigned_staff_id))
|
||||
if rate > 1:
|
||||
return "清掃中(%d倍速・残り%d分)" % [rate,ceili(float(task.estimated_minutes-task.progress_minutes)/rate)]
|
||||
return "清掃中 %d/%d分" % [task.progress_minutes,task.estimated_minutes]
|
||||
if task.state in [StaffTask.State.MOVING,StaffTask.State.RESERVED]: return "清掃員が移動中"
|
||||
return "清掃待ち(清掃員室・配置・経路を確認)"
|
||||
var row: Dictionary = _runtime.facility_cleanliness.get_facility(facility_id)
|
||||
if not row.is_empty() and row["current"] <= _runtime.cleaning_policy.create_snapshot()["clean_at_or_below"]: return "清掃待ち(作業経路を確認)"
|
||||
return ""
|
||||
|
||||
|
||||
## Preserve a reachable reserved destination; reselect only if it is cut off.
|
||||
## A moving cleaner waits in place if every access is isolated, never teleports.
|
||||
func prepare_route(task: StaffTask, worker: StaffRuntime) -> bool:
|
||||
if task == null or worker == null: return false
|
||||
var facility: FacilityInstance = _runtime.facility_service.get_instance(task.target_entity_id)
|
||||
var cells := access_cells(facility, _runtime.path_service)
|
||||
if task.target_cell in cells and not _runtime.path_service.find_path(worker.current_cell, task.target_cell, [0,1], [&"staff"]).is_empty(): return true
|
||||
var best: Variant = null
|
||||
var best_cost := 9223372036854775807
|
||||
for cell: Vector3i in cells:
|
||||
var route: Array[Vector3i] = _runtime.path_service.find_path(worker.current_cell, cell, [0,1], [&"staff"])
|
||||
if route.is_empty(): continue
|
||||
var cost: int = _runtime.path_service.calculate_path_cost(route, [&"staff"])
|
||||
if cost >= 0 and cost < best_cost:
|
||||
best = cell
|
||||
best_cost = cost
|
||||
if best == null: return false
|
||||
task.target_cell = best
|
||||
return true
|
||||
|
||||
## Original 6.2: Fine cleans three times as fast. Task progress remains work
|
||||
## units, so an eight-unit task completes on the third one-minute tick.
|
||||
static func work_units_per_minute(worker: StaffRuntime) -> int:
|
||||
if worker != null and worker.staff_id == &"heroine_fine" and worker.role_id == &"cleaner" and worker.employment_kind == StaffRuntime.EmploymentKind.UNIQUE:
|
||||
return 3
|
||||
return 1
|
||||
@@ -0,0 +1 @@
|
||||
uid://cuqm1vvtndcsg
|
||||
@@ -0,0 +1,36 @@
|
||||
class_name FacilityCleaningPolicy
|
||||
extends RefCounted
|
||||
|
||||
## Approved trial values live in data, never in scheduler defaults.
|
||||
var last_error := ""
|
||||
var _values: Dictionary = {}
|
||||
|
||||
func load_file(path: String = "res://data/environment/cleaning_policy.json") -> bool:
|
||||
var parsed: Variant = JSON.parse_string(FileAccess.get_file_as_string(path))
|
||||
if not parsed is Dictionary:
|
||||
last_error = "Cleaning policy must be a JSON object"
|
||||
return false
|
||||
return configure(parsed)
|
||||
|
||||
func configure(values: Dictionary) -> bool:
|
||||
var keys := ["schema_version", "initial_cleanliness", "maximum_cleanliness", "clean_at_or_below", "work_minutes", "recovery_per_completed_task"]
|
||||
if values.size() != keys.size():
|
||||
last_error = "Unexpected cleaning policy fields"
|
||||
return false
|
||||
var candidate: Dictionary = {}
|
||||
for key: String in keys:
|
||||
var value: Variant = values.get(key)
|
||||
# JSON numbers arrive as floats; accept only finite exact small integers.
|
||||
if typeof(value) not in [TYPE_INT, TYPE_FLOAT] or not is_finite(float(value)) or float(value) != floor(float(value)) or float(value) < 0 or float(value) > 2147483647:
|
||||
last_error = "Invalid cleaning policy integer: " + key
|
||||
return false
|
||||
candidate[key] = int(value)
|
||||
if candidate["schema_version"] != 1 or candidate["maximum_cleanliness"] <= 0 or candidate["initial_cleanliness"] > candidate["maximum_cleanliness"] or candidate["clean_at_or_below"] >= candidate["maximum_cleanliness"] or candidate["work_minutes"] <= 0 or candidate["recovery_per_completed_task"] <= 0:
|
||||
last_error = "Invalid cleaning policy bounds"
|
||||
return false
|
||||
_values = candidate
|
||||
last_error = ""
|
||||
return true
|
||||
|
||||
func create_snapshot() -> Dictionary:
|
||||
return _values.duplicate(true)
|
||||
@@ -0,0 +1 @@
|
||||
uid://cxaxvqpfyjfkc
|
||||
@@ -0,0 +1,123 @@
|
||||
class_name FacilityCleanlinessService
|
||||
extends RefCounted
|
||||
|
||||
## Parameterized cleanliness state. Runtime supplies approved initial/max values
|
||||
## and completed-use/task IDs; this model invents no operating balance values.
|
||||
var last_error := ""
|
||||
var _facilities: Dictionary = {}
|
||||
var _receipts: Array[Dictionary] = []
|
||||
var _receipt_index: Dictionary = {}
|
||||
|
||||
func register_facility(id: StringName, initial: int, maximum: int) -> bool:
|
||||
if id.is_empty() or _facilities.has(id) or maximum <= 0 or initial < 0 or initial > maximum:
|
||||
return _fail("Invalid or duplicate cleanliness facility")
|
||||
_facilities[id] = {"id": String(id), "initial": initial, "maximum": maximum, "current": initial}
|
||||
last_error = ""
|
||||
return true
|
||||
|
||||
func unregister_facility(id: StringName) -> bool:
|
||||
if not _facilities.has(id): return _fail("Unknown cleanliness facility")
|
||||
_facilities.erase(id)
|
||||
var retained: Array[Dictionary] = []
|
||||
_receipt_index.clear()
|
||||
for receipt: Dictionary in _receipts:
|
||||
if StringName(receipt["facility_id"]) != id:
|
||||
retained.append(receipt)
|
||||
_receipt_index[_receipt_key(receipt)] = receipt
|
||||
_receipts = retained
|
||||
last_error = ""
|
||||
return true
|
||||
|
||||
func record_use(id: StringName, service_id: StringName, soil_amount: int) -> bool:
|
||||
return _apply(id, service_id, soil_amount, "use")
|
||||
|
||||
func record_cleaning(id: StringName, task_id: StringName, recovery_amount: int) -> bool:
|
||||
return _apply(id, task_id, recovery_amount, "clean")
|
||||
|
||||
func get_facility(id: StringName) -> Dictionary:
|
||||
return (_facilities.get(id, {}) as Dictionary).duplicate(true)
|
||||
|
||||
func get_dirty_facility_ids(threshold: int) -> Array[StringName]:
|
||||
var result: Array[StringName] = []
|
||||
for id: StringName in _facilities:
|
||||
if int(_facilities[id]["current"]) <= threshold:
|
||||
result.append(id)
|
||||
result.sort_custom(_id_before)
|
||||
return result
|
||||
|
||||
func create_snapshot() -> Dictionary:
|
||||
var rows: Array[Dictionary] = []
|
||||
var ids: Array = _facilities.keys()
|
||||
ids.sort_custom(_id_before)
|
||||
for id: StringName in ids: rows.append(_facilities[id].duplicate(true))
|
||||
return {"schema_version": 1, "facilities": rows, "receipts": _receipts.duplicate(true)}
|
||||
|
||||
func restore_snapshot(snapshot: Dictionary) -> bool:
|
||||
if not _keys(snapshot, ["schema_version", "facilities", "receipts"]) or typeof(snapshot.get("schema_version")) != TYPE_INT or snapshot["schema_version"] != 1:
|
||||
return _fail("Unsupported cleanliness snapshot")
|
||||
if not snapshot["facilities"] is Array or not snapshot["receipts"] is Array:
|
||||
return _fail("Invalid cleanliness collections")
|
||||
var candidate := FacilityCleanlinessService.new()
|
||||
var expected: Dictionary = {}
|
||||
for value: Variant in snapshot["facilities"]:
|
||||
if not value is Dictionary or not _keys(value, ["id", "initial", "maximum", "current"]):
|
||||
return _fail("Invalid cleanliness facility row")
|
||||
if typeof(value["id"]) != TYPE_STRING or typeof(value["initial"]) != TYPE_INT or typeof(value["maximum"]) != TYPE_INT or typeof(value["current"]) != TYPE_INT:
|
||||
return _fail("Invalid cleanliness facility value types")
|
||||
var id := StringName(value["id"])
|
||||
if not candidate.register_facility(id, value["initial"], value["maximum"]):
|
||||
return _fail(candidate.last_error)
|
||||
expected[id] = value["current"]
|
||||
for value: Variant in snapshot["receipts"]:
|
||||
if not value is Dictionary or not _keys(value, ["facility_id", "source_id", "kind", "amount"]):
|
||||
return _fail("Invalid cleanliness receipt")
|
||||
if typeof(value["facility_id"]) != TYPE_STRING or typeof(value["source_id"]) != TYPE_STRING or typeof(value["kind"]) != TYPE_STRING or typeof(value["amount"]) != TYPE_INT:
|
||||
return _fail("Invalid cleanliness receipt value types")
|
||||
if candidate._receipt_index.has(_receipt_key(value)) or not candidate._apply(StringName(value["facility_id"]), StringName(value["source_id"]), value["amount"], value["kind"]):
|
||||
return _fail("Invalid, duplicated or dangling cleanliness receipt")
|
||||
for id: StringName in expected:
|
||||
if candidate._facilities[id]["current"] != expected[id]:
|
||||
return _fail("Cleanliness value does not match its completed work")
|
||||
_facilities = candidate._facilities
|
||||
_receipts = candidate._receipts
|
||||
_receipt_index = candidate._receipt_index
|
||||
last_error = ""
|
||||
return true
|
||||
|
||||
func _apply(id: StringName, source_id: StringName, amount: int, kind: String) -> bool:
|
||||
if not _facilities.has(id) or source_id.is_empty() or amount < 0 or kind not in ["use", "clean"]:
|
||||
return _fail("Invalid cleanliness effect")
|
||||
var receipt := {"facility_id": String(id), "source_id": String(source_id), "kind": kind, "amount": amount}
|
||||
var key := _receipt_key(receipt)
|
||||
if _receipt_index.has(key):
|
||||
if _receipt_index[key] != receipt: return _fail("Conflicting cleanliness completion replay")
|
||||
last_error = ""
|
||||
return true
|
||||
var row: Dictionary = _facilities[id]
|
||||
# Clamp the operand before addition/subtraction to avoid int overflow.
|
||||
if kind == "use": row["current"] -= mini(amount, int(row["current"]))
|
||||
else: row["current"] += mini(amount, int(row["maximum"]) - int(row["current"]))
|
||||
_receipts.append(receipt)
|
||||
_receipt_index[key] = receipt
|
||||
last_error = ""
|
||||
return true
|
||||
|
||||
static func _receipt_key(receipt: Dictionary) -> String:
|
||||
return String(receipt["kind"]) + ":" + String(receipt["source_id"])
|
||||
|
||||
static func _keys(value: Dictionary, expected: Array) -> bool:
|
||||
if value.size() != expected.size(): return false
|
||||
for key: Variant in value:
|
||||
if typeof(key) not in [TYPE_STRING, TYPE_STRING_NAME] or String(key) not in expected: return false
|
||||
for key: String in expected:
|
||||
if not value.has(key): return false
|
||||
return true
|
||||
|
||||
func _fail(message: String) -> bool:
|
||||
last_error = message
|
||||
return false
|
||||
|
||||
# StringName's built-in ordering is process-local. Persisted row ordering and
|
||||
# dispatch priority must instead use the stable textual facility identifier.
|
||||
static func _id_before(a: StringName, b: StringName) -> bool:
|
||||
return String(a) < String(b)
|
||||
@@ -0,0 +1 @@
|
||||
uid://dt4s42nme54s1
|
||||
@@ -0,0 +1,94 @@
|
||||
extends RefCounted
|
||||
|
||||
## Weather state and scheduled transitions. The caller owns generation policy;
|
||||
## this model invents neither probabilities nor rain/fog balance multipliers.
|
||||
const VERSION := 1
|
||||
const KINDS := [&"clear", &"rain", &"storm", &"mana_fog"]
|
||||
const MAX_MINUTE := 2147483647
|
||||
const MAX_PENDING := 4096
|
||||
var _minute := -1
|
||||
var _kind: StringName = &""
|
||||
var _since := -1
|
||||
var _pending: Array[Dictionary] = []
|
||||
|
||||
func initialize(kind: Variant, minute: Variant) -> bool:
|
||||
if _minute >= 0 or not _valid_kind(kind) or not _valid_minute(minute):
|
||||
return false
|
||||
_minute = minute
|
||||
_since = minute
|
||||
_kind = StringName(kind)
|
||||
return true
|
||||
|
||||
func get_kind() -> StringName:
|
||||
return _kind
|
||||
|
||||
func get_minute() -> int:
|
||||
return _minute
|
||||
|
||||
func permits_new_arrivals() -> bool:
|
||||
return _minute >= 0 and _kind != &"storm"
|
||||
|
||||
func schedule(kind: Variant, minute: Variant) -> Dictionary:
|
||||
if _minute < 0 or not _valid_kind(kind) or not _valid_minute(minute) or minute <= _minute:
|
||||
return {&"ok": false, &"code": &"INVALID_TRANSITION"}
|
||||
for row: Dictionary in _pending:
|
||||
if row[&"minute"] == minute:
|
||||
return {&"ok": row[&"kind"] == StringName(kind), &"replayed": row[&"kind"] == StringName(kind)}
|
||||
if _pending.size() >= MAX_PENDING:
|
||||
return {&"ok": false, &"code": &"SCHEDULE_FULL"}
|
||||
_pending.append({&"kind": StringName(kind), &"minute": minute})
|
||||
_pending.sort_custom(func(a: Dictionary, b: Dictionary) -> bool: return a[&"minute"] < b[&"minute"])
|
||||
return {&"ok": true, &"replayed": false}
|
||||
|
||||
func advance_to(minute: Variant) -> Dictionary:
|
||||
if _minute < 0 or not _valid_minute(minute) or minute < _minute:
|
||||
return {&"ok": false, &"code": &"INVALID_TIME"}
|
||||
var changes: Array[Dictionary] = []
|
||||
var replayed: bool = minute == _minute
|
||||
while not _pending.is_empty() and int(_pending[0][&"minute"]) <= minute:
|
||||
var transition: Dictionary = _pending.pop_front()
|
||||
if transition[&"kind"] == _kind:
|
||||
continue
|
||||
changes.append({&"from": _kind, &"to": transition[&"kind"], &"minute": transition[&"minute"]})
|
||||
_kind = transition[&"kind"]
|
||||
_since = transition[&"minute"]
|
||||
_minute = minute
|
||||
return {&"ok": true, &"replayed": replayed, &"changes": changes}
|
||||
|
||||
func create_snapshot() -> Dictionary:
|
||||
return {&"version": VERSION, &"minute": _minute, &"kind": _kind,
|
||||
&"since": _since, &"pending": _pending.duplicate(true)}
|
||||
|
||||
func restore_snapshot(source: Dictionary) -> bool:
|
||||
if not _keys(source, ["version", "minute", "kind", "since", "pending"]) \
|
||||
or typeof(source["version"]) != TYPE_INT or source["version"] != VERSION \
|
||||
or not _valid_minute(source["minute"]) or not _valid_minute(source["since"]) \
|
||||
or source["since"] > source["minute"] or not _valid_kind(source["kind"]) \
|
||||
or not source["pending"] is Array or source["pending"].size() > MAX_PENDING:
|
||||
return false
|
||||
var previous: int = source["minute"]
|
||||
var pending: Array[Dictionary] = []
|
||||
for value: Variant in source["pending"]:
|
||||
if not value is Dictionary or not _keys(value, ["kind", "minute"]) \
|
||||
or not _valid_kind(value["kind"]) or not _valid_minute(value["minute"]) \
|
||||
or value["minute"] <= previous:
|
||||
return false
|
||||
previous = value["minute"]
|
||||
pending.append({&"kind": StringName(value["kind"]), &"minute": value["minute"]})
|
||||
_minute = source["minute"]
|
||||
_kind = StringName(source["kind"])
|
||||
_since = source["since"]
|
||||
_pending = pending
|
||||
return true
|
||||
|
||||
static func _valid_kind(value: Variant) -> bool:
|
||||
return typeof(value) in [TYPE_STRING, TYPE_STRING_NAME] and StringName(value) in KINDS
|
||||
|
||||
static func _valid_minute(value: Variant) -> bool:
|
||||
return typeof(value) == TYPE_INT and value >= 0 and value <= MAX_MINUTE
|
||||
|
||||
static func _keys(source: Dictionary, expected: Array) -> bool:
|
||||
if source.size() != expected.size(): return false
|
||||
for key: Variant in source:
|
||||
if typeof(key) not in [TYPE_STRING, TYPE_STRING_NAME] or String(key) not in expected: return false
|
||||
return true
|
||||
@@ -0,0 +1 @@
|
||||
uid://bihm0hldfc1im
|
||||
Reference in New Issue
Block a user