diff --git a/docs/plans/original-completion-2026-10-02/checkpoints/native-hot-loop-r325-r329/.gdignore b/docs/plans/original-completion-2026-10-02/checkpoints/native-hot-loop-r325-r329/.gdignore new file mode 100644 index 0000000..e69de29 diff --git a/docs/plans/original-completion-2026-10-02/checkpoints/native-hot-loop-r325-r329/000-p23-02-native-hot-loop-build-actual-independent-review-r327.json.gz b/docs/plans/original-completion-2026-10-02/checkpoints/native-hot-loop-r325-r329/000-p23-02-native-hot-loop-build-actual-independent-review-r327.json.gz new file mode 100644 index 0000000..7f1a294 Binary files /dev/null and b/docs/plans/original-completion-2026-10-02/checkpoints/native-hot-loop-r325-r329/000-p23-02-native-hot-loop-build-actual-independent-review-r327.json.gz differ diff --git a/docs/plans/original-completion-2026-10-02/checkpoints/native-hot-loop-r325-r329/001-p23-02-native-hot-loop-build-preexecution-independent-review-r327.json.gz b/docs/plans/original-completion-2026-10-02/checkpoints/native-hot-loop-r325-r329/001-p23-02-native-hot-loop-build-preexecution-independent-review-r327.json.gz new file mode 100644 index 0000000..9c19828 Binary files /dev/null and b/docs/plans/original-completion-2026-10-02/checkpoints/native-hot-loop-r325-r329/001-p23-02-native-hot-loop-build-preexecution-independent-review-r327.json.gz differ diff --git a/docs/plans/original-completion-2026-10-02/checkpoints/native-hot-loop-r325-r329/002-p23-02-native-hot-loop-build-preexecution-root-verification-r327.json.gz b/docs/plans/original-completion-2026-10-02/checkpoints/native-hot-loop-r325-r329/002-p23-02-native-hot-loop-build-preexecution-root-verification-r327.json.gz new file mode 100644 index 0000000..af31fdd Binary files /dev/null and b/docs/plans/original-completion-2026-10-02/checkpoints/native-hot-loop-r325-r329/002-p23-02-native-hot-loop-build-preexecution-root-verification-r327.json.gz differ diff --git a/docs/plans/original-completion-2026-10-02/checkpoints/native-hot-loop-r325-r329/003-p23-02-native-hot-loop-build-preview-r327.json b/docs/plans/original-completion-2026-10-02/checkpoints/native-hot-loop-r325-r329/003-p23-02-native-hot-loop-build-preview-r327.json new file mode 100644 index 0000000..79f0b7d --- /dev/null +++ b/docs/plans/original-completion-2026-10-02/checkpoints/native-hot-loop-r325-r329/003-p23-02-native-hot-loop-build-preview-r327.json @@ -0,0 +1,56 @@ +{ + "plan": [ + [ + "compile-062", + [ + "/Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/bin/clang++", + "-o", + "/Users/korizatou114/Projects/\u3068\u308a\u306b\u304f\u5de5\u623f\u30b2\u30fc\u30e0\u5236\u4f5c\u90e8/artifacts/original-completion-2026-10-02/p23-02-native-hot-loop-build-r327/objects/062-tree_numeric_core.os", + "-c", + "-std=c++17", + "-isystem", + "/Users/korizatou114/Projects/\u3068\u308a\u306b\u304f\u5de5\u623f\u30b2\u30fc\u30e0\u5236\u4f5c\u90e8/artifacts/original-completion-2026-10-02/p23-02-native-sdk-lifetime-candidate-r131/sdk/include", + "-isystem", + "/Users/korizatou114/Projects/\u3068\u308a\u306b\u304f\u5de5\u623f\u30b2\u30fc\u30e0\u5236\u4f5c\u90e8/artifacts/original-completion-2026-10-02/p23-02-native-sdk-lifetime-candidate-r131/sdk/gen/include", + "-Wall", + "-Wextra", + "-Werror", + "-fexceptions", + "-arch", + "arm64", + "-isysroot", + "/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk", + "-fvisibility=hidden", + "-O3", + "-fPIC", + "-DMACOS_ENABLED", + "-DUNIX_ENABLED", + "-DTHREADS_ENABLED", + "-DDEBUG_ENABLED", + "-DNDEBUG", + "-DGDEXTENSION", + "-I/Users/korizatou114/Projects/\u3068\u308a\u306b\u304f\u5de5\u623f\u30b2\u30fc\u30e0\u5236\u4f5c\u90e8/artifacts/original-completion-2026-10-02/p23-02-native-hot-loop-candidate-r326/source", + "/Users/korizatou114/Projects/\u3068\u308a\u306b\u304f\u5de5\u623f\u30b2\u30fc\u30e0\u5236\u4f5c\u90e8/artifacts/original-completion-2026-10-02/p23-02-native-hot-loop-candidate-r326/source/tree_numeric_core.cpp" + ] + ], + [ + "link", + [ + "/Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/bin/clang++", + "-o", + "/Users/korizatou114/Projects/\u3068\u308a\u306b\u304f\u5de5\u623f\u30b2\u30fc\u30e0\u5236\u4f5c\u90e8/artifacts/original-completion-2026-10-02/p23-02-native-hot-loop-build-r327/libtree_numeric_core.macos.template_debug.dylib", + "-arch", + "arm64", + "-isysroot", + "/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk", + "-fvisibility=hidden", + "-Wl,-S", + "-Wl,-x", + "-Wl,-dead_strip", + "-shared", + "/Users/korizatou114/Projects/\u3068\u308a\u306b\u304f\u5de5\u623f\u30b2\u30fc\u30e0\u5236\u4f5c\u90e8/artifacts/original-completion-2026-10-02/p23-02-native-hot-loop-build-r327/objects/062-tree_numeric_core.os", + "/Users/korizatou114/Projects/\u3068\u308a\u306b\u304f\u5de5\u623f\u30b2\u30fc\u30e0\u5236\u4f5c\u90e8/artifacts/original-completion-2026-10-02/p23-02-native-sdk-lifetime-build-r133/libgodot-cpp.macos.template_debug.arm64.a" + ] + ] + ] +} diff --git a/docs/plans/original-completion-2026-10-02/checkpoints/native-hot-loop-r325-r329/004-ended.json b/docs/plans/original-completion-2026-10-02/checkpoints/native-hot-loop-r325-r329/004-ended.json new file mode 100644 index 0000000..0c5dcdb --- /dev/null +++ b/docs/plans/original-completion-2026-10-02/checkpoints/native-hot-loop-r325-r329/004-ended.json @@ -0,0 +1,49 @@ +{ + "pid": 34717, + "argv": [ + "/Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/bin/clang++", + "-o", + "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/artifacts/original-completion-2026-10-02/p23-02-native-hot-loop-build-r327/objects/062-tree_numeric_core.os", + "-c", + "-std=c++17", + "-isystem", + "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/artifacts/original-completion-2026-10-02/p23-02-native-sdk-lifetime-candidate-r131/sdk/include", + "-isystem", + "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/artifacts/original-completion-2026-10-02/p23-02-native-sdk-lifetime-candidate-r131/sdk/gen/include", + "-Wall", + "-Wextra", + "-Werror", + "-fexceptions", + "-arch", + "arm64", + "-isysroot", + "/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk", + "-fvisibility=hidden", + "-O3", + "-fPIC", + "-DMACOS_ENABLED", + "-DUNIX_ENABLED", + "-DTHREADS_ENABLED", + "-DDEBUG_ENABLED", + "-DNDEBUG", + "-DGDEXTENSION", + "-I/Users/korizatou114/Projects/とりにく工房ゲーム制作部/artifacts/original-completion-2026-10-02/p23-02-native-hot-loop-candidate-r326/source", + "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/artifacts/original-completion-2026-10-02/p23-02-native-hot-loop-candidate-r326/source/tree_numeric_core.cpp" + ], + "pgid": 34717, + "ended_utc": "2026-10-12T03:51:07.694369+00:00", + "exit_code": 0, + "duration_seconds": 1.6463062919999993, + "cleanup": [ + { + "attempt": 0, + "pgid": 34717, + "members_before": [], + "group_signal": null, + "tracked_parent_signal": null + } + ], + "remaining": [], + "cleanup_errors": [], + "foreign_process_or_group_signals": 0 +} diff --git a/docs/plans/original-completion-2026-10-02/checkpoints/native-hot-loop-r325-r329/005-started.json b/docs/plans/original-completion-2026-10-02/checkpoints/native-hot-loop-r325-r329/005-started.json new file mode 100644 index 0000000..0d3d014 --- /dev/null +++ b/docs/plans/original-completion-2026-10-02/checkpoints/native-hot-loop-r325-r329/005-started.json @@ -0,0 +1,45 @@ +{ + "pid": 34717, + "owner_pid": 34670, + "pgid": 34717, + "argv": [ + "/Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/bin/clang++", + "-o", + "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/artifacts/original-completion-2026-10-02/p23-02-native-hot-loop-build-r327/objects/062-tree_numeric_core.os", + "-c", + "-std=c++17", + "-isystem", + "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/artifacts/original-completion-2026-10-02/p23-02-native-sdk-lifetime-candidate-r131/sdk/include", + "-isystem", + "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/artifacts/original-completion-2026-10-02/p23-02-native-sdk-lifetime-candidate-r131/sdk/gen/include", + "-Wall", + "-Wextra", + "-Werror", + "-fexceptions", + "-arch", + "arm64", + "-isysroot", + "/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk", + "-fvisibility=hidden", + "-O3", + "-fPIC", + "-DMACOS_ENABLED", + "-DUNIX_ENABLED", + "-DTHREADS_ENABLED", + "-DDEBUG_ENABLED", + "-DNDEBUG", + "-DGDEXTENSION", + "-I/Users/korizatou114/Projects/とりにく工房ゲーム制作部/artifacts/original-completion-2026-10-02/p23-02-native-hot-loop-candidate-r326/source", + "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/artifacts/original-completion-2026-10-02/p23-02-native-hot-loop-candidate-r326/source/tree_numeric_core.cpp" + ], + "started_utc": "2026-10-12T03:51:06.111332+00:00", + "initial_members": [ + { + "pid": 34717, + "ppid": 34670, + "pgid": 34717, + "args": "/Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/bin/clang++ -o /Users/korizatou114/Projects/とりにく工房ゲーム制作部/artifacts/original-completion-2026-10-02/p23-02-native-hot-loop-build-r327/objects/062-tree_numeric_core.os -c -std=c++17 -isystem /Users/korizatou114/Projects/とりにく工房ゲーム制作部/artifacts/original-completion-2026-10-02/p23-02-native-sdk-lifetime-candidate-r131/sdk/include -isystem /Users/korizatou114/Projects/とりにく工房ゲーム制作部/artifacts/original-completion-2026-10-02/p23-02-native-sdk-lifetime-candidate-r131/sdk/gen/include -Wall -Wextra -Werror -fexceptions -arch arm64 -isysroot /Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk -fvisibility=hidden -O3 -fPIC -DMACOS_ENABLED -DUNIX_ENABLED -DTHREADS_ENABLED -DDEBUG_ENABLED -DNDEBUG -DGDEXTENSION -I/Users/korizatou114/Projects/とりにく工房ゲーム制作部/artifacts/original-completion-2026-10-02/p23-02-native-hot-loop-candidate-r326/source /Users/korizatou114/Projects/とりにく工房ゲーム制作部/artifacts/original-completion-2026-10-02/p23-02-native-hot-loop-candidate-r326/source/tree_numeric_core.cpp" + } + ], + "owned_new_session": true +} diff --git a/docs/plans/original-completion-2026-10-02/checkpoints/native-hot-loop-r325-r329/006-stderr.log b/docs/plans/original-completion-2026-10-02/checkpoints/native-hot-loop-r325-r329/006-stderr.log new file mode 100644 index 0000000..e69de29 diff --git a/docs/plans/original-completion-2026-10-02/checkpoints/native-hot-loop-r325-r329/007-stdout.log b/docs/plans/original-completion-2026-10-02/checkpoints/native-hot-loop-r325-r329/007-stdout.log new file mode 100644 index 0000000..e69de29 diff --git a/docs/plans/original-completion-2026-10-02/checkpoints/native-hot-loop-r325-r329/008-compile-062-phase.json b/docs/plans/original-completion-2026-10-02/checkpoints/native-hot-loop-r325-r329/008-compile-062-phase.json new file mode 100644 index 0000000..422e439 --- /dev/null +++ b/docs/plans/original-completion-2026-10-02/checkpoints/native-hot-loop-r325-r329/008-compile-062-phase.json @@ -0,0 +1,28 @@ +{ + "name": "compile-062", + "result": { + "pid": 34717, + "exit_code": 0, + "timed_out": false, + "error_lines": [], + "duration_seconds": 1.6466069169999997 + }, + "capture": { + "passed": true, + "cleanup": [ + { + "attempt": 0, + "pgid": 34717, + "members_before": [], + "group_signal": null, + "tracked_parent_signal": null + } + ], + "cleanup_errors": [], + "all_captured_ended": true, + "foreign_process_or_group_signals": 0 + }, + "full_input_inventory_before_verified": true, + "full_input_inventory_after_verified": true, + "post_input_error": null +} diff --git a/docs/plans/original-completion-2026-10-02/checkpoints/native-hot-loop-r325-r329/009-execution.json b/docs/plans/original-completion-2026-10-02/checkpoints/native-hot-loop-r325-r329/009-execution.json new file mode 100644 index 0000000..77f63b1 --- /dev/null +++ b/docs/plans/original-completion-2026-10-02/checkpoints/native-hot-loop-r325-r329/009-execution.json @@ -0,0 +1,173 @@ +{ + "passed": true, + "error": null, + "phases": [ + { + "name": "compile-062", + "result": { + "pid": 34717, + "exit_code": 0, + "timed_out": false, + "error_lines": [], + "duration_seconds": 1.6466069169999997 + }, + "capture": { + "passed": true, + "cleanup": [ + { + "attempt": 0, + "pgid": 34717, + "members_before": [], + "group_signal": null, + "tracked_parent_signal": null + } + ], + "cleanup_errors": [], + "all_captured_ended": true, + "foreign_process_or_group_signals": 0 + }, + "full_input_inventory_before_verified": true, + "full_input_inventory_after_verified": true, + "post_input_error": null + }, + { + "name": "link", + "result": { + "pid": 34736, + "exit_code": 0, + "timed_out": false, + "error_lines": [], + "duration_seconds": 0.210839 + }, + "capture": { + "passed": true, + "cleanup": [ + { + "attempt": 0, + "pgid": 34736, + "members_before": [], + "group_signal": null, + "tracked_parent_signal": null + } + ], + "cleanup_errors": [], + "all_captured_ended": true, + "foreign_process_or_group_signals": 0 + }, + "full_input_inventory_before_verified": true, + "full_input_inventory_after_verified": true, + "post_input_error": null + } + ], + "cleanup": [ + [ + { + "attempt": 0, + "pgid": 34717, + "members_before": [], + "group_signal": null, + "tracked_parent_signal": null + } + ], + [ + { + "attempt": 0, + "pgid": 34736, + "members_before": [], + "group_signal": null, + "tracked_parent_signal": null + } + ] + ], + "cleanup_errors": [], + "binary_checks": { + "file": "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/artifacts/original-completion-2026-10-02/p23-02-native-hot-loop-build-r327/libtree_numeric_core.macos.template_debug.dylib: Mach-O 64-bit dynamically linked shared library arm64\n", + "arch": "arm64\n", + "dependencies": "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/artifacts/original-completion-2026-10-02/p23-02-native-hot-loop-build-r327/libtree_numeric_core.macos.template_debug.dylib:\n\t/Users/korizatou114/Projects/とりにく工房ゲーム制作部/artifacts/original-completion-2026-10-02/p23-02-native-hot-loop-build-r327/libtree_numeric_core.macos.template_debug.dylib (compatibility version 0.0.0, current version 0.0.0)\n\t/usr/lib/libc++.1.dylib (compatibility version 1.0.0, current version 2100.43.0)\n\t/usr/lib/libSystem.B.dylib (compatibility version 1.0.0, current version 1356.0.0)\n", + "exports": "0000000000000828 T _tree_numeric_core_init\n" + }, + "object": { + "path": "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/artifacts/original-completion-2026-10-02/p23-02-native-hot-loop-build-r327/objects/062-tree_numeric_core.os", + "bytes": 368776, + "sha256": "4ac5a21e47a4441374131310c44474497b5fb5b28f6c6e75a5ed17243616687d" + }, + "library": { + "path": "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/artifacts/original-completion-2026-10-02/p23-02-native-hot-loop-build-r327/libtree_numeric_core.macos.template_debug.dylib", + "bytes": 417184, + "sha256": "64608dccb3441594603fa5561fad3e2e90b055f32e4823c4864d2486a298602c" + }, + "original_inputs_preserved": true, + "bridge_compilations": 1, + "link_attempts": 1, + "spawned_children": [ + { + "phase": "compile-062", + "pid": 34717, + "argv": [ + "/Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/bin/clang++", + "-o", + "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/artifacts/original-completion-2026-10-02/p23-02-native-hot-loop-build-r327/objects/062-tree_numeric_core.os", + "-c", + "-std=c++17", + "-isystem", + "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/artifacts/original-completion-2026-10-02/p23-02-native-sdk-lifetime-candidate-r131/sdk/include", + "-isystem", + "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/artifacts/original-completion-2026-10-02/p23-02-native-sdk-lifetime-candidate-r131/sdk/gen/include", + "-Wall", + "-Wextra", + "-Werror", + "-fexceptions", + "-arch", + "arm64", + "-isysroot", + "/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk", + "-fvisibility=hidden", + "-O3", + "-fPIC", + "-DMACOS_ENABLED", + "-DUNIX_ENABLED", + "-DTHREADS_ENABLED", + "-DDEBUG_ENABLED", + "-DNDEBUG", + "-DGDEXTENSION", + "-I/Users/korizatou114/Projects/とりにく工房ゲーム制作部/artifacts/original-completion-2026-10-02/p23-02-native-hot-loop-candidate-r326/source", + "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/artifacts/original-completion-2026-10-02/p23-02-native-hot-loop-candidate-r326/source/tree_numeric_core.cpp" + ] + }, + { + "phase": "link", + "pid": 34736, + "argv": [ + "/Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/bin/clang++", + "-o", + "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/artifacts/original-completion-2026-10-02/p23-02-native-hot-loop-build-r327/libtree_numeric_core.macos.template_debug.dylib", + "-arch", + "arm64", + "-isysroot", + "/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk", + "-fvisibility=hidden", + "-Wl,-S", + "-Wl,-x", + "-Wl,-dead_strip", + "-shared", + "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/artifacts/original-completion-2026-10-02/p23-02-native-hot-loop-build-r327/objects/062-tree_numeric_core.os", + "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/artifacts/original-completion-2026-10-02/p23-02-native-sdk-lifetime-build-r133/libgodot-cpp.macos.template_debug.arm64.a" + ] + } + ], + "SDK_compilations": 0, + "SDK_archive_reused": { + "path": "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/artifacts/original-completion-2026-10-02/p23-02-native-sdk-lifetime-build-r133/libgodot-cpp.macos.template_debug.arm64.a", + "bytes": 2284912, + "sha256": "4d54f0980c3f445f3013cddb859135b619f28a91205fc8b9fbc9d10bc76c52c3" + }, + "SDK_archive_rebuilt": false, + "old_bridge_object_reused": false, + "Godot_loaded": false, + "selected": false, + "unit_accepted": false, + "performance_accepted": false, + "limits": [ + "Exact r326 Guest batch bridge one-object compile and link to authenticated unchanged r133 SDK archive only; no SDK recompilation, Godot load, protocol, performance or unit acceptance." + ] +} diff --git a/docs/plans/original-completion-2026-10-02/checkpoints/native-hot-loop-r325-r329/010-input.json.gz b/docs/plans/original-completion-2026-10-02/checkpoints/native-hot-loop-r325-r329/010-input.json.gz new file mode 100644 index 0000000..1306bc5 Binary files /dev/null and b/docs/plans/original-completion-2026-10-02/checkpoints/native-hot-loop-r325-r329/010-input.json.gz differ diff --git a/docs/plans/original-completion-2026-10-02/checkpoints/native-hot-loop-r325-r329/011-libtree_numeric_core.macos.template_debug.dylib.gz b/docs/plans/original-completion-2026-10-02/checkpoints/native-hot-loop-r325-r329/011-libtree_numeric_core.macos.template_debug.dylib.gz new file mode 100644 index 0000000..7a5e029 Binary files /dev/null and b/docs/plans/original-completion-2026-10-02/checkpoints/native-hot-loop-r325-r329/011-libtree_numeric_core.macos.template_debug.dylib.gz differ diff --git a/docs/plans/original-completion-2026-10-02/checkpoints/native-hot-loop-r325-r329/012-ended.json b/docs/plans/original-completion-2026-10-02/checkpoints/native-hot-loop-r325-r329/012-ended.json new file mode 100644 index 0000000..4b6a6b3 --- /dev/null +++ b/docs/plans/original-completion-2026-10-02/checkpoints/native-hot-loop-r325-r329/012-ended.json @@ -0,0 +1,35 @@ +{ + "pid": 34736, + "argv": [ + "/Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/bin/clang++", + "-o", + "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/artifacts/original-completion-2026-10-02/p23-02-native-hot-loop-build-r327/libtree_numeric_core.macos.template_debug.dylib", + "-arch", + "arm64", + "-isysroot", + "/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk", + "-fvisibility=hidden", + "-Wl,-S", + "-Wl,-x", + "-Wl,-dead_strip", + "-shared", + "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/artifacts/original-completion-2026-10-02/p23-02-native-hot-loop-build-r327/objects/062-tree_numeric_core.os", + "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/artifacts/original-completion-2026-10-02/p23-02-native-sdk-lifetime-build-r133/libgodot-cpp.macos.template_debug.arm64.a" + ], + "pgid": 34736, + "ended_utc": "2026-10-12T03:51:08.344270+00:00", + "exit_code": 0, + "duration_seconds": 0.21052816699999966, + "cleanup": [ + { + "attempt": 0, + "pgid": 34736, + "members_before": [], + "group_signal": null, + "tracked_parent_signal": null + } + ], + "remaining": [], + "cleanup_errors": [], + "foreign_process_or_group_signals": 0 +} diff --git a/docs/plans/original-completion-2026-10-02/checkpoints/native-hot-loop-r325-r329/013-started.json b/docs/plans/original-completion-2026-10-02/checkpoints/native-hot-loop-r325-r329/013-started.json new file mode 100644 index 0000000..0ca43d0 --- /dev/null +++ b/docs/plans/original-completion-2026-10-02/checkpoints/native-hot-loop-r325-r329/013-started.json @@ -0,0 +1,31 @@ +{ + "pid": 34736, + "owner_pid": 34670, + "pgid": 34736, + "argv": [ + "/Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/bin/clang++", + "-o", + "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/artifacts/original-completion-2026-10-02/p23-02-native-hot-loop-build-r327/libtree_numeric_core.macos.template_debug.dylib", + "-arch", + "arm64", + "-isysroot", + "/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk", + "-fvisibility=hidden", + "-Wl,-S", + "-Wl,-x", + "-Wl,-dead_strip", + "-shared", + "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/artifacts/original-completion-2026-10-02/p23-02-native-hot-loop-build-r327/objects/062-tree_numeric_core.os", + "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/artifacts/original-completion-2026-10-02/p23-02-native-sdk-lifetime-build-r133/libgodot-cpp.macos.template_debug.arm64.a" + ], + "started_utc": "2026-10-12T03:51:08.170135+00:00", + "initial_members": [ + { + "pid": 34736, + "ppid": 34670, + "pgid": 34736, + "args": "/Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/bin/clang++ -o /Users/korizatou114/Projects/とりにく工房ゲーム制作部/artifacts/original-completion-2026-10-02/p23-02-native-hot-loop-build-r327/libtree_numeric_core.macos.template_debug.dylib -arch arm64 -isysroot /Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk -fvisibility=hidden -Wl,-S -Wl,-x -Wl,-dead_strip -shared /Users/korizatou114/Projects/とりにく工房ゲーム制作部/artifacts/original-completion-2026-10-02/p23-02-native-hot-loop-build-r327/objects/062-tree_numeric_core.os /Users/korizatou114/Projects/とりにく工房ゲーム制作部/artifacts/original-completion-2026-10-02/p23-02-native-sdk-lifetime-build-r133/libgodot-cpp.macos.template_debug.arm64.a" + } + ], + "owned_new_session": true +} diff --git a/docs/plans/original-completion-2026-10-02/checkpoints/native-hot-loop-r325-r329/014-stderr.log b/docs/plans/original-completion-2026-10-02/checkpoints/native-hot-loop-r325-r329/014-stderr.log new file mode 100644 index 0000000..e69de29 diff --git a/docs/plans/original-completion-2026-10-02/checkpoints/native-hot-loop-r325-r329/015-stdout.log b/docs/plans/original-completion-2026-10-02/checkpoints/native-hot-loop-r325-r329/015-stdout.log new file mode 100644 index 0000000..e69de29 diff --git a/docs/plans/original-completion-2026-10-02/checkpoints/native-hot-loop-r325-r329/016-link-phase.json b/docs/plans/original-completion-2026-10-02/checkpoints/native-hot-loop-r325-r329/016-link-phase.json new file mode 100644 index 0000000..c671532 --- /dev/null +++ b/docs/plans/original-completion-2026-10-02/checkpoints/native-hot-loop-r325-r329/016-link-phase.json @@ -0,0 +1,28 @@ +{ + "name": "link", + "result": { + "pid": 34736, + "exit_code": 0, + "timed_out": false, + "error_lines": [], + "duration_seconds": 0.210839 + }, + "capture": { + "passed": true, + "cleanup": [ + { + "attempt": 0, + "pgid": 34736, + "members_before": [], + "group_signal": null, + "tracked_parent_signal": null + } + ], + "cleanup_errors": [], + "all_captured_ended": true, + "foreign_process_or_group_signals": 0 + }, + "full_input_inventory_before_verified": true, + "full_input_inventory_after_verified": true, + "post_input_error": null +} diff --git a/docs/plans/original-completion-2026-10-02/checkpoints/native-hot-loop-r325-r329/017-062-tree_numeric_core.os.gz b/docs/plans/original-completion-2026-10-02/checkpoints/native-hot-loop-r325-r329/017-062-tree_numeric_core.os.gz new file mode 100644 index 0000000..99ee5f9 Binary files /dev/null and b/docs/plans/original-completion-2026-10-02/checkpoints/native-hot-loop-r325-r329/017-062-tree_numeric_core.os.gz differ diff --git a/docs/plans/original-completion-2026-10-02/checkpoints/native-hot-loop-r325-r329/018-p23-02-native-hot-loop-build-r327.py.gz b/docs/plans/original-completion-2026-10-02/checkpoints/native-hot-loop-r325-r329/018-p23-02-native-hot-loop-build-r327.py.gz new file mode 100644 index 0000000..7b9a35e Binary files /dev/null and b/docs/plans/original-completion-2026-10-02/checkpoints/native-hot-loop-r325-r329/018-p23-02-native-hot-loop-build-r327.py.gz differ diff --git a/docs/plans/original-completion-2026-10-02/checkpoints/native-hot-loop-r325-r329/019-p23-02-native-hot-loop-build-root-verification-r327.json.gz b/docs/plans/original-completion-2026-10-02/checkpoints/native-hot-loop-r325-r329/019-p23-02-native-hot-loop-build-root-verification-r327.json.gz new file mode 100644 index 0000000..1a412fe Binary files /dev/null and b/docs/plans/original-completion-2026-10-02/checkpoints/native-hot-loop-r325-r329/019-p23-02-native-hot-loop-build-root-verification-r327.json.gz differ diff --git a/docs/plans/original-completion-2026-10-02/checkpoints/native-hot-loop-r325-r329/020-input.json b/docs/plans/original-completion-2026-10-02/checkpoints/native-hot-loop-r325-r329/020-input.json new file mode 100644 index 0000000..5495174 --- /dev/null +++ b/docs/plans/original-completion-2026-10-02/checkpoints/native-hot-loop-r325-r329/020-input.json @@ -0,0 +1,59 @@ +{ + "plan_id": "P23-02", + "status": "uncompiled_implementation_candidate", + "parent": { + "path": "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/artifacts/original-completion-2026-10-02/p23-02-native-guest-bulk-candidate-r313-r02/input.json", + "bytes": 4775, + "sha256": "80734ba89f84a086ef5b79a0443296a73ec5aab0478edcfaabb3dfd89450d71f" + }, + "contract": { + "path": "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/artifacts/original-completion-2026-10-02/p23-02-native-hot-loop-work-contract-r325.json", + "bytes": 2313, + "sha256": "bbb488bbb0229c31711982867375f41487ec7f5c9343eb6eb5903b8321e05602" + }, + "candidate_pins": { + "scripts/guest_service.gd": { + "path": "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/artifacts/original-completion-2026-10-02/p23-02-native-hot-loop-candidate-r326/scripts/guest_service.gd", + "bytes": 62505, + "sha256": "77ce52ebeed6a89f3e397de01572b17ea3e3cb246f7daca9145a1a192389969e" + }, + "scripts/simulation_runtime.gd": { + "path": "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/artifacts/original-completion-2026-10-02/p23-02-native-hot-loop-candidate-r326/scripts/simulation_runtime.gd", + "bytes": 612068, + "sha256": "7eb24adbf91fe53cde14954dda12abd5efb05130de7d356d8df281ee99065e86" + }, + "source/guest_candidate_batch.inc": { + "path": "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/artifacts/original-completion-2026-10-02/p23-02-native-hot-loop-candidate-r326/source/guest_candidate_batch.inc", + "bytes": 28222, + "sha256": "3623a7ca03b96118c927662fc5a95c1f73e20251d5ef4b04ec519603c3c05ba9" + }, + "source/tree_core.hpp": { + "path": "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/artifacts/original-completion-2026-10-02/p23-02-native-hot-loop-candidate-r326/source/tree_core.hpp", + "bytes": 3467, + "sha256": "5f67d85bfc91d5c7830aabd8089f4bca05883045d671d4e683d7c532cf3a34a1" + }, + "source/tree_numeric_core.cpp": { + "path": "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/artifacts/original-completion-2026-10-02/p23-02-native-hot-loop-candidate-r326/source/tree_numeric_core.cpp", + "bytes": 56138, + "sha256": "a2434ef2fbc9624aa3b62746cebde2cacdce78712d4d866774df54fbd291bee3" + } + }, + "compiled_Guest_keys": 99, + "scripts_unchanged": [ + { + "path": "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/artifacts/original-completion-2026-10-02/p23-02-native-hot-loop-candidate-r326/scripts/guest_service.gd", + "bytes": 62505, + "sha256": "77ce52ebeed6a89f3e397de01572b17ea3e3cb246f7daca9145a1a192389969e" + }, + { + "path": "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/artifacts/original-completion-2026-10-02/p23-02-native-hot-loop-candidate-r326/scripts/simulation_runtime.gd", + "bytes": 612068, + "sha256": "7eb24adbf91fe53cde14954dda12abd5efb05130de7d356d8df281ee99065e86" + } + ], + "selected": false, + "adopted": false, + "unit_accepted": false, + "performance_accepted": false, + "formal_completed_added": 0 +} diff --git a/docs/plans/original-completion-2026-10-02/checkpoints/native-hot-loop-r325-r329/021-guest_service.gd b/docs/plans/original-completion-2026-10-02/checkpoints/native-hot-loop-r325-r329/021-guest_service.gd new file mode 100644 index 0000000..d8d8b51 --- /dev/null +++ b/docs/plans/original-completion-2026-10-02/checkpoints/native-hot-loop-r325-r329/021-guest_service.gd @@ -0,0 +1,1708 @@ +class_name GuestService +extends RefCounted + +## Deterministic guest needs, decisions and lifecycle authority. +## +## Facility, path and economy integrations intentionally cross Dictionary and +## Callable boundaries. GuestService therefore remains independently testable +## and cannot accidentally mutate another service's private state. + +signal guest_spawned(guest: GuestRuntime) +signal guest_state_changed(guest_id: StringName, old_state: int, new_state: int, reason: StringName) +signal guest_queue_abandoned(guest_id: StringName, facility_id: StringName, request_id: StringName) +signal guest_recovery_started(guest_id: StringName, recovery_kind: StringName, target_cell: Vector3i) +signal guest_despawned(guest_id: StringName, persistent_record: Dictionary) + +const GuestDefinitionType = preload("res://scripts/guest/guest_definition.gd") +const GuestRuntimeType = preload("res://scripts/guest/guest_runtime.gd") + +const SNAPSHOT_VERSION := 2 +const MAX_TRANSITIONS_PER_TICK := 4 +const PATH_FAILURE_LIMIT := 3 +const STUCK_REPLAN_MINUTES := 60 +const DEFAULT_WAIT_HARD_LIMIT := 120 +const DEFAULT_EXIT_THRESHOLD := 15 +const PRIMARY_NEEDS: Array[StringName] = [&"hunger", &"sleep", &"leisure", &"intimacy"] + +const NEED_IDS: Array[StringName] = [ + &"hunger", + &"sleep", + &"hygiene", + &"medical", + &"leisure", + &"intimacy", +] +const LEGACY_NEED_IDS: Array[StringName] = [ + &"hunger", + &"rest", + &"fun", + &"social", + &"safety", +] + +var current_minute := -1 +var last_error := "" + +var _definitions: Dictionary = {} +var _guests: Dictionary = {} +var _persistent_records: Dictionary = {} +var _decision_rejections: Dictionary = {} +var _diagnostics: Array[Dictionary] = [] +var _sorted_guest_ids_cache: Array[StringName] = [] +var _guest_order_dirty := true +var _pending_spawn_notifications: Dictionary = {} +var _external_mutation_blocked := false + + +func register_definition(definition: Variant) -> bool: + if definition == null or not definition is GuestDefinition: + return _fail("Guest definition has an invalid type.") + var typed_definition: GuestDefinition = definition + if not typed_definition.is_valid(): + return _fail("Guest definition is invalid.") + if _definitions.has(typed_definition.guest_type_id): + return _fail("Duplicate guest definition: %s" % typed_definition.guest_type_id) + _definitions[typed_definition.guest_type_id] = typed_definition + last_error = "" + return true + + +func create_definition(guest_type_id: StringName, options: Dictionary = {}) -> GuestDefinition: + var definition := GuestDefinitionType.new().configure(guest_type_id, options) + return definition if register_definition(definition) else null + + +func get_definition(guest_type_id: StringName) -> GuestDefinition: + return _definitions.get(guest_type_id) as GuestDefinition + + +func get_definitions() -> Array[GuestDefinition]: + var result: Array[GuestDefinition] = [] + for guest_type_id: StringName in _sorted_ids(_definitions.keys()): + result.append(_definitions[guest_type_id]) + return result + + +func register_guest(guest: Variant, emit_notification := true) -> bool: + if _external_mutation_blocked: + return _fail("Guest mutation is blocked while committed notifications are delivered.") + if guest == null or not guest is GuestRuntime: + return _fail("Guest runtime has an invalid type.") + var runtime: GuestRuntime = guest + if not runtime.is_valid(): + return _fail("Guest runtime is invalid.") + if not _definitions.has(runtime.guest_type_id): + return _fail("Guest runtime references an unknown definition.") + if _guests.has(runtime.guest_id) or _persistent_records.has(runtime.guest_id): + return _fail("Duplicate guest id: %s" % runtime.guest_id) + _guests[runtime.guest_id] = runtime + _guest_order_dirty = true + if emit_notification: + guest_spawned.emit(runtime) + else: + _pending_spawn_notifications[runtime.guest_id] = true + last_error = "" + return true + + +func spawn_guest( + guest_id: StringName, + guest_type_id: StringName, + absolute_minute: int, + options: Dictionary = {}, + emit_notification := true +) -> GuestRuntime: + var definition := get_definition(guest_type_id) + if definition == null or absolute_minute < 0: + _fail("A known definition and non-negative spawn minute are required.") + return null + var runtime_options := options.duplicate(true) + if not _has_value(runtime_options, &"wallet_gold"): + runtime_options[&"wallet_gold"] = definition.wealth_min + if not _has_value(runtime_options, &"credit_limit_gold"): + runtime_options[&"credit_limit_gold"] = definition.credit_limit_gold + if not _has_value(runtime_options, &"patience_remaining"): + runtime_options[&"patience_remaining"] = definition.base_patience + if not _has_value(runtime_options, &"state"): + runtime_options[&"state"] = GuestRuntimeType.State.SPAWNING + if not _has_value(runtime_options, &"state_entered_at"): + runtime_options[&"state_entered_at"] = absolute_minute + if not _has_value(runtime_options, &"planned_exit_at"): + runtime_options[&"planned_exit_at"] = absolute_minute + 720 + var runtime := GuestRuntimeType.new().configure(guest_id, guest_type_id, runtime_options) + if not register_guest(runtime, emit_notification): + return null + return runtime + + +func commit_guest_spawned_notification(guest_id: StringName) -> bool: + if not _pending_spawn_notifications.has(guest_id) or not _guests.has(guest_id): + return _fail("Guest has no pending spawn notification: %s" % guest_id) + _pending_spawn_notifications.erase(guest_id) + guest_spawned.emit(_guests[guest_id] as GuestRuntime) + last_error = "" + return true + + +func discard_guest_spawned_notification(guest_id: StringName) -> bool: + if not _pending_spawn_notifications.has(guest_id): + return false + _pending_spawn_notifications.erase(guest_id) + return true + + +func has_pending_guest_spawned_notification(guest_id: StringName) -> bool: + return _pending_spawn_notifications.has(guest_id) + + +func set_external_mutation_blocked(blocked: bool) -> void: + _external_mutation_blocked = blocked + + +func has_guest(guest_id: StringName) -> bool: + return _guests.has(guest_id) + + +func get_guest(guest_id: StringName) -> GuestRuntime: + return _guests.get(guest_id) as GuestRuntime + + +func get_guests() -> Array[GuestRuntime]: + var result: Array[GuestRuntime] = [] + for guest_id: StringName in _ordered_guest_ids(): + result.append(_guests[guest_id]) + return result + + +func guest_count() -> int: + return _guests.size() + + +func get_persistent_record(guest_id: StringName) -> Dictionary: + if not _persistent_records.has(guest_id): + return {} + return (_persistent_records[guest_id] as Dictionary).duplicate(true) + + +func get_diagnostics() -> Array[Dictionary]: + return _diagnostics.duplicate(true) + + +func advance_one_minute( + absolute_minute: int, + context_by_guest: Dictionary = {} +) -> Dictionary: + if absolute_minute < 0 or absolute_minute <= current_minute: + return _error_result("Guest minute must advance monotonically.") + var ordered_guest_ids := _ordered_guest_ids() + # The common no-context tick has no external values to validate or isolate. + # Avoid allocating one duplicated context plus a resolved-context entry per + # Guest while retaining the same deterministic order and report contract. + if context_by_guest.is_empty(): + return _advance_with_shared_need_context( + absolute_minute, + ordered_guest_ids, + {} + ) + var resolved_contexts: Dictionary = {} + var default_context: Dictionary = {} + var raw_default: Variant = _value(context_by_guest, &"default", {}) + if raw_default is Dictionary: + default_context = raw_default + # Production supplies one common default context. Validate it once, then + # share the same read-only Dictionary across the population. Any per-Guest + # override keeps using the all-context preflight below, preserving atomic + # rejection and the malformed-override contract. + if context_by_guest.size() == 1 and _has_value(context_by_guest, &"default"): + # Historically an unused default was not validated when the population + # was empty. Preserve that edge case and the first-Guest error wording. + if not ordered_guest_ids.is_empty(): + var default_error := _need_context_error(default_context) + if not default_error.is_empty(): + return _error_result( + "Invalid Need context for %s: %s" % [ + ordered_guest_ids[0], + default_error, + ] + ) + return _advance_with_shared_need_context( + absolute_minute, + ordered_guest_ids, + default_context + ) + for guest_id: StringName in ordered_guest_ids: + var context := default_context + var raw_context: Variant = null + var has_guest_context := false + if context_by_guest.has(guest_id): + has_guest_context = true + raw_context = context_by_guest[guest_id] + else: + var string_guest_id := String(guest_id) + if context_by_guest.has(string_guest_id): + has_guest_context = true + raw_context = context_by_guest[string_guest_id] + if raw_context is Dictionary: + context = raw_context + elif has_guest_context: + # Preserve the original contract: a malformed per-guest override + # suppresses the default context rather than inheriting it. + context = {} + var context_error := _need_context_error(context) + if not context_error.is_empty(): + return _error_result("Invalid Need context for %s: %s" % [guest_id, context_error]) + resolved_contexts[guest_id] = context.duplicate(true) + + ## All contexts are preflighted before mutating any guest, so a malformed + ## batched update cannot partially advance the population or the minute. + var reports: Array[Dictionary] = [] + for guest_id: StringName in ordered_guest_ids: + var context: Dictionary = resolved_contexts[guest_id] + var guest: GuestRuntime = _guests[guest_id] + var definition: GuestDefinition = _definitions.get(guest.guest_type_id) as GuestDefinition + if definition == null: + reports.append(_error_result("Guest definition is missing.")) + else: + reports.append(_update_needs_runtime(guest, definition, context)) + current_minute = absolute_minute + last_error = "" + return { + &"ok": true, + &"minute": absolute_minute, + &"guest_reports": reports, + } + + +func _advance_with_shared_need_context( + absolute_minute: int, + ordered_guest_ids: Array[StringName], + context: Dictionary +) -> Dictionary: + var reports: Array[Dictionary] = [] + for guest_id: StringName in ordered_guest_ids: + var guest: GuestRuntime = _guests[guest_id] + var definition: GuestDefinition = _definitions.get( + guest.guest_type_id + ) as GuestDefinition + if definition == null: + reports.append(_error_result("Guest definition is missing.")) + else: + reports.append(_update_needs_runtime(guest, definition, context)) + current_minute = absolute_minute + last_error = "" + return { + &"ok": true, + &"minute": absolute_minute, + &"guest_reports": reports, + } + + +func update_needs(guest_id: StringName, context: Dictionary = {}) -> Dictionary: + var guest := get_guest(guest_id) + if guest == null: + return _error_result("Unknown guest: %s" % guest_id) + var definition := get_definition(guest.guest_type_id) + if definition == null: + return _error_result("Guest definition is missing.") + var context_error := _need_context_error(context) + if not context_error.is_empty(): + return _error_result("Invalid Need context: %s" % context_error) + last_error = "" + return _update_needs_runtime(guest, definition, context) + + +func _update_needs_runtime( + guest: GuestRuntime, + definition: GuestDefinition, + context: Dictionary +) -> Dictionary: + var before_hunger := int(guest.needs[&"hunger"]) + var before_sleep := int(guest.needs[&"sleep"]) + var before_hygiene := int(guest.needs[&"hygiene"]) + var before_medical := int(guest.needs[&"medical"]) + var before_leisure := int(guest.needs[&"leisure"]) + var before_intimacy := int(guest.needs[&"intimacy"]) + var delta_hunger := int(definition.need_rates[&"hunger"]) + var delta_sleep := int(definition.need_rates[&"sleep"]) + var delta_hygiene := int(definition.need_rates[&"hygiene"]) + var delta_medical := int(definition.need_rates[&"medical"]) + var delta_leisure := int(definition.need_rates[&"leisure"]) + var delta_intimacy := int(definition.need_rates[&"intimacy"]) + + ## State effects are accumulated first; every Need is clamped exactly once. + if guest.state == GuestRuntimeType.State.MOVING \ + or guest.state == GuestRuntimeType.State.REPLANNING: + delta_hunger += 2 + delta_sleep += 2 + elif guest.state == GuestRuntimeType.State.QUEUING: + delta_hunger += 1 + + if context.is_empty(): + if guest.state != GuestRuntimeType.State.RECEIVING_SERVICE: + delta_leisure += 1 + else: + if bool(_value(context, &"work_like_activity", false)): + delta_sleep += 1 + if bool(_value( + context, + &"unstimulated", + guest.state != GuestRuntimeType.State.RECEIVING_SERVICE + )): + delta_leisure += 1 + delta_hygiene += int(_value(context, &"dirt", 0)) + delta_medical += int(_value(context, &"injury", 0)) + delta_medical += int(_value(context, &"illness", 0)) + if definition.adult_verified: + var intimacy_key := &"intimacy_night" \ + if bool(_value(context, &"is_night", false)) \ + else &"intimacy_day" + delta_intimacy += int(_value(context, intimacy_key, 0)) + var raw_effects: Variant = _value(context, &"need_effects", {}) + var normalized_effects: Dictionary = _normalized_partial_need_map(raw_effects) + delta_hunger += int(normalized_effects.get(&"hunger", 0)) + delta_sleep += int(normalized_effects.get(&"sleep", 0)) + delta_hygiene += int(normalized_effects.get(&"hygiene", 0)) + delta_medical += int(normalized_effects.get(&"medical", 0)) + delta_leisure += int(normalized_effects.get(&"leisure", 0)) + delta_intimacy += int(normalized_effects.get(&"intimacy", 0)) + + var after_hunger := clampi(before_hunger + delta_hunger, 0, 100) + var after_sleep := clampi(before_sleep + delta_sleep, 0, 100) + var after_hygiene := clampi(before_hygiene + delta_hygiene, 0, 100) + var after_medical := clampi(before_medical + delta_medical, 0, 100) + var after_leisure := clampi(before_leisure + delta_leisure, 0, 100) + var after_intimacy := clampi(before_intimacy + delta_intimacy, 0, 100) + guest.needs[&"hunger"] = after_hunger + guest.needs[&"sleep"] = after_sleep + guest.needs[&"hygiene"] = after_hygiene + guest.needs[&"medical"] = after_medical + guest.needs[&"leisure"] = after_leisure + guest.needs[&"intimacy"] = after_intimacy + return { + &"ok": true, + &"guest_id": String(guest.guest_id), + &"before": { + "hunger": before_hunger, + "hygiene": before_hygiene, + "intimacy": before_intimacy, + "leisure": before_leisure, + "medical": before_medical, + "sleep": before_sleep, + }, + &"deltas": { + "hunger": delta_hunger, + "hygiene": delta_hygiene, + "intimacy": delta_intimacy, + "leisure": delta_leisure, + "medical": delta_medical, + "sleep": delta_sleep, + }, + &"after": { + "hunger": after_hunger, + "hygiene": after_hygiene, + "intimacy": after_intimacy, + "leisure": after_leisure, + "medical": after_medical, + "sleep": after_sleep, + }, + } + + +## Returns canonical candidate copies in facility_id order. Supported canonical +## fields are documented by the filtering helpers below; aliases allow existing +## FacilityService projections to be adapted without a hard dependency. +func extract_candidates( + guest_id: StringName, + facilities: Array, + context: Dictionary = {} +) -> Array[Dictionary]: + var guest := get_guest(guest_id) + if guest == null: + _fail("Unknown guest: %s" % guest_id) + return [] + var definition := get_definition(guest.guest_type_id) + if definition == null: + _fail("Guest definition is missing.") + return [] + var ordered: Array[Dictionary] = [] + for raw_candidate: Variant in facilities: + if not raw_candidate is Dictionary: + continue + var candidate: Dictionary = raw_candidate.duplicate(true) + var facility_id := _facility_id(candidate) + if facility_id.is_empty(): + continue + candidate[&"facility_id"] = facility_id + ordered.append(candidate) + ordered.sort_custom( + func(a: Dictionary, b: Dictionary) -> bool: + return String(a[&"facility_id"]) < String(b[&"facility_id"]) + ) + + var accepted: Array[Dictionary] = [] + var rejected: Array[Dictionary] = [] + for candidate: Dictionary in ordered: + var reason := _candidate_rejection_reason(guest, definition, candidate, context) + if reason.is_empty(): + accepted.append(candidate) + else: + rejected.append({ + &"facility_id": String(candidate[&"facility_id"]), + &"reason": String(reason), + }) + _decision_rejections[guest_id] = rejected + last_error = "" + return accepted + + +func get_last_candidate_rejections(guest_id: StringName) -> Array[Dictionary]: + return (_decision_rejections.get(guest_id, []) as Array).duplicate(true) + + +func evaluate_candidate(guest_id: StringName, candidate: Dictionary) -> Dictionary: + var guest := get_guest(guest_id) + if guest == null: + return _error_result("Unknown guest: %s" % guest_id) + var definition := get_definition(guest.guest_type_id) + if definition == null: + return _error_result("Guest definition is missing.") + if not _candidate_need_maps_are_valid(candidate): + return _error_result("Candidate Need map is invalid.") + var need_relief := _need_relief(guest, candidate) + var preference := int(_value( + candidate, + &"preference", + definition.preference_for(_value(candidate, &"facility_tags", _value(candidate, &"tags", []))) + )) + var expected_quality := int(_value( + candidate, + &"expected_quality", + _value(candidate, &"quality", 0) + )) + var novelty := int(_value( + candidate, + &"novelty", + 0 if guest.visited_facility_ids.has(_facility_id(candidate)) else 10 + )) + var group_fit := int(_value(candidate, &"group_fit", 0)) + var price_burden := int(_value(candidate, &"price_burden", _price_burden(guest, candidate))) + var travel_minutes := maxi(int(_value(candidate, &"travel_minutes", 0)), 0) + var wait_minutes := maxi(int(_value( + candidate, + &"wait_minutes", + _value(candidate, &"expected_wait", 0) + )), 0) + var risk := maxi(int(_value(candidate, &"risk", _value(candidate, &"risk_level", 0))), 0) + var default_risk_sensitivity := (100 - definition.risk_tolerance + 24) / 25 + var risk_sensitivity := maxi(int(_value( + candidate, + &"risk_sensitivity", + default_risk_sensitivity + )), 0) + var repetition_penalty := int(_value( + candidate, + &"repetition_penalty", + 10 if guest.visited_facility_ids.has(_facility_id(candidate)) else 0 + )) + var urgency := int(_value(candidate, &"urgency", _candidate_urgency(guest, candidate))) + var utility := ( + need_relief * 8 + + preference * 5 + + expected_quality * 3 + + novelty * 2 + + group_fit * 2 + - price_burden * 5 + - travel_minutes * 2 + - wait_minutes * 3 + - risk * risk_sensitivity + - repetition_penalty + ) + var result := candidate.duplicate(true) + result[&"facility_id"] = _facility_id(candidate) + result[&"need_priority"] = _primary_need_priority(guest, candidate) + result[&"utility"] = utility + result[&"urgency"] = urgency + result[&"travel_minutes"] = travel_minutes + result[&"utility_terms"] = { + &"need_relief": need_relief, + &"preference": preference, + &"expected_quality": expected_quality, + &"novelty": novelty, + &"group_fit": group_fit, + &"price_burden": price_burden, + &"travel_minutes": travel_minutes, + &"wait_minutes": wait_minutes, + &"risk": risk, + &"risk_sensitivity": risk_sensitivity, + &"repetition_penalty": repetition_penalty, + } + return result + + +func rank_candidates(guest_id: StringName, candidates: Array) -> Array[Dictionary]: + var ranked: Array[Dictionary] = [] + for raw_candidate: Variant in candidates: + if raw_candidate is Dictionary: + var evaluated := evaluate_candidate(guest_id, raw_candidate) + if bool(evaluated.get(&"ok", true)): + ranked.append(evaluated) + ranked.sort_custom(_candidate_precedes) + return ranked + + +func select_best_candidate( + guest_id: StringName, + candidates: Array, + exit_threshold: int = DEFAULT_EXIT_THRESHOLD +) -> Dictionary: + var ranked := rank_candidates(guest_id, candidates) + for candidate: Dictionary in ranked: + if int(candidate[&"utility"]) >= exit_threshold: + return candidate.duplicate(true) + return {} + + +func decide( + guest_id: StringName, + facilities: Array, + absolute_minute: int, + context: Dictionary = {} +) -> Dictionary: + var guest := get_guest(guest_id) + if guest == null: + return _error_result("Unknown guest: %s" % guest_id) + var filtered := extract_candidates(guest_id, facilities, context) + var ranked := rank_candidates(guest_id, filtered) + return _decide_ranked_candidates(guest, guest_id, ranked, absolute_minute, context) + + +func decide_with_native_candidates( + core: Object, guest_id: StringName, facilities: Array, absolute_minute: int, context: Dictionary = {} +) -> Dictionary: + if get_script().resource_path != "res://scripts/guest/guest_service.gd" or core == null or core.get_class() != &"TheTreeNumericCore" or core.get_script() != null or not core.has_method(&"guest_candidate_batch"): + return decide(guest_id, facilities, absolute_minute, context) + var batch: Dictionary = core.call(&"guest_candidate_batch", self, guest_id, facilities, context) + if not bool(batch.get(&"supported", false)): + return decide(guest_id, facilities, absolute_minute, context) + var guest := get_guest(guest_id) + _decision_rejections[guest_id] = batch[&"rejected"] + last_error = "" + return _decide_ranked_candidates(guest, guest_id, batch[&"ranked"], absolute_minute, context) + + +func _decide_ranked_candidates( + guest: GuestRuntime, guest_id: StringName, ranked: Array[Dictionary], absolute_minute: int, context: Dictionary +) -> Dictionary: + var threshold := int(_value(context, &"exit_threshold", DEFAULT_EXIT_THRESHOLD)) + var reserve_callback: Callable = _value(context, &"reserve_callback", Callable()) + for candidate: Dictionary in ranked: + if int(candidate[&"utility"]) < threshold: + continue + if reserve_callback.is_valid(): + var reservation_result: Variant = reserve_callback.call( + guest_id, + candidate[&"facility_id"], + candidate.duplicate(true) + ) + if not _callback_succeeded(reservation_result): + continue + if not _transition_is_available(guest, GuestRuntimeType.State.MOVING, absolute_minute): + return _error_result("Guest cannot enter MOVING in this tick.") + guest.target_facility_id = candidate[&"facility_id"] + guest.path_handle = _copy(_value(candidate, &"path_handle", null)) + _apply_transition(guest, GuestRuntimeType.State.MOVING, absolute_minute, &"FACILITY_SELECTED") + last_error = "" + return { + &"ok": true, + &"action": &"MOVE_TO_FACILITY", + &"candidate": candidate.duplicate(true), + } + if not _transition_is_available(guest, GuestRuntimeType.State.LEAVING, absolute_minute): + return _error_result("Guest cannot enter LEAVING in this tick.") + guest.target_facility_id = &"" + guest.path_handle = null + guest.exit_reason = StringName(_value(context, &"exit_reason", &"NO_SUITABLE_FACILITY")) + _apply_transition(guest, GuestRuntimeType.State.LEAVING, absolute_minute, guest.exit_reason) + last_error = "" + return {&"ok": true, &"action": &"LEAVE", &"candidate": {}} + + +func transition_guest( + guest_id: StringName, + new_state: int, + absolute_minute: int, + reason: StringName = &"" +) -> Dictionary: + var guest := get_guest(guest_id) + if guest == null: + return _error_result("Unknown guest: %s" % guest_id) + if new_state < GuestRuntimeType.State.SPAWNING or new_state > GuestRuntimeType.State.INCAPACITATED: + return _error_result("Unknown guest state.") + if guest.state == new_state: + return {&"ok": true, &"changed": false, &"state": new_state} + if not _is_allowed_transition(guest.state, new_state): + return _error_result( + "Invalid guest transition: %s -> %s" % [ + GuestRuntimeType.State.keys()[guest.state], + GuestRuntimeType.State.keys()[new_state], + ] + ) + if not _transition_is_available(guest, new_state, absolute_minute): + _force_loop_guard(guest, absolute_minute) + return { + &"ok": false, + &"changed": true, + &"loop_guard": true, + &"state": guest.state, + } + _apply_transition(guest, new_state, absolute_minute, reason) + last_error = "" + return {&"ok": true, &"changed": true, &"state": new_state} + + +func enter_queue( + guest_id: StringName, + facility_id: StringName, + request_id: StringName, + absolute_minute: int, + initial_patience: int = -1, + enqueue_callback: Callable = Callable() +) -> Dictionary: + var guest := get_guest(guest_id) + if guest == null or facility_id.is_empty() or request_id.is_empty(): + return _error_result("Guest, facility and request are required for queue entry.") + if not _transition_is_available(guest, GuestRuntimeType.State.QUEUING, absolute_minute): + return _error_result("Guest cannot enter QUEUING in this tick.") + if enqueue_callback.is_valid(): + var external_result: Variant = enqueue_callback.call(guest_id, facility_id, request_id) + if not _callback_succeeded(external_result): + return _error_result("Facility queue rejected the guest.") + var definition := get_definition(guest.guest_type_id) + var resolved_patience := initial_patience + if resolved_patience < 0: + resolved_patience = definition.base_patience if definition != null else 0 + guest.target_facility_id = facility_id + guest.queue_request_id = request_id + guest.patience_remaining = maxi(resolved_patience, 0) + _apply_transition(guest, GuestRuntimeType.State.QUEUING, absolute_minute, &"QUEUE_ENTERED") + last_error = "" + return {&"ok": true, &"patience_remaining": guest.patience_remaining} + + +## Closure asks the guest to choose again, without charging a patience penalty. +## The callback releases the owning facility request and inventory reservations. +func release_queue_for_replanning( + guest_id: StringName, + absolute_minute: int, + release_callback: Callable = Callable() +) -> Dictionary: + if _external_mutation_blocked: + return _error_result("Guest mutation is blocked while committed notifications are delivered.") + var guest := get_guest(guest_id) + if guest == null or guest.state != GuestRuntimeType.State.QUEUING: + return _error_result("Guest is not in a queue.") + if not _transition_is_available(guest, GuestRuntimeType.State.DECIDING, absolute_minute): + return _error_result("Guest cannot atomically replan the queue in this tick.") + var facility_id := guest.target_facility_id + var request_id := guest.queue_request_id + if facility_id.is_empty() or request_id.is_empty(): + return _error_result("Queued guest is missing a facility or request.") + if release_callback.is_valid(): + var external_result: Variant = release_callback.call(guest_id, facility_id, request_id) + if not _callback_succeeded(external_result): + return _error_result("Facility queue removal failed; guest state was not changed.") + guest.target_facility_id = &"" + guest.queue_request_id = &"" + guest.path_handle = null + _apply_transition(guest, GuestRuntimeType.State.DECIDING, absolute_minute, &"FACILITY_CLOSED") + last_error = "" + return { + &"ok": true, + &"facility_id": facility_id, + &"request_id": request_id, + &"patience_remaining": guest.patience_remaining, + } + + +func tick_queue_patience( + guest_id: StringName, + absolute_minute: int, + recovery: int = 0, + leave_queue_callback: Callable = Callable() +) -> Dictionary: + var guest := get_guest(guest_id) + if guest == null or guest.state != GuestRuntimeType.State.QUEUING: + return _error_result("Guest is not in a queue.") + var next_patience := maxi(guest.patience_remaining - 1 + recovery, 0) + if next_patience > 0: + guest.patience_remaining = next_patience + last_error = "" + return {&"ok": true, &"abandoned": false, &"patience_remaining": next_patience} + ## Preflight every internal mutation before invoking the external queue owner. + if not _transition_is_available(guest, GuestRuntimeType.State.ABANDONING, absolute_minute): + return _error_result("Guest cannot atomically abandon the queue in this tick.") + var facility_id := guest.target_facility_id + var request_id := guest.queue_request_id + if leave_queue_callback.is_valid(): + var external_result: Variant = leave_queue_callback.call( + guest_id, + facility_id, + request_id + ) + if not _callback_succeeded(external_result): + return _error_result("Facility queue removal failed; guest state was not changed.") + guest.patience_remaining = 0 + guest.target_facility_id = &"" + guest.queue_request_id = &"" + guest.path_handle = null + _apply_transition(guest, GuestRuntimeType.State.ABANDONING, absolute_minute, &"PATIENCE_EXHAUSTED") + guest_queue_abandoned.emit(guest_id, facility_id, request_id) + last_error = "" + return {&"ok": true, &"abandoned": true, &"patience_remaining": 0} + + +func begin_service( + guest_id: StringName, + facility_id: StringName, + service_id: StringName, + absolute_minute: int +) -> Dictionary: + var guest := get_guest(guest_id) + if guest == null or facility_id.is_empty() or service_id.is_empty(): + return _error_result("Guest, facility and service are required.") + if not _transition_is_available(guest, GuestRuntimeType.State.RECEIVING_SERVICE, absolute_minute): + return _error_result("Guest cannot begin service in this tick.") + guest.target_facility_id = facility_id + guest.current_service_id = service_id + guest.queue_request_id = &"" + _apply_transition(guest, GuestRuntimeType.State.RECEIVING_SERVICE, absolute_minute, &"SERVICE_STARTED") + last_error = "" + return {&"ok": true, &"service_id": service_id} + + +func interrupt_service( + guest_id: StringName, + service_id: StringName, + absolute_minute: int, + interrupt_callback: Callable = Callable() +) -> Dictionary: + var guest := get_guest(guest_id) + if guest == null or service_id.is_empty(): + return _error_result("Guest and service are required.") + if guest.interrupted_service_ids.has(service_id): + return {&"ok": true, &"replayed": true, &"service_id": service_id} + if guest.current_service_id != service_id: + return _error_result("Guest is not receiving the specified service.") + if not _transition_is_available(guest, GuestRuntimeType.State.INTERRUPTED, absolute_minute): + return _error_result("Guest cannot enter INTERRUPTED in this tick.") + if interrupt_callback.is_valid(): + var external_result: Variant = interrupt_callback.call(guest_id, service_id) + if not _callback_succeeded(external_result): + return _error_result("Service interruption callback failed.") + guest.interrupted_service_ids.append(service_id) + guest.interrupted_service_ids.sort_custom( + func(a: StringName, b: StringName) -> bool: return String(a) < String(b) + ) + guest.current_service_id = &"" + _apply_transition(guest, GuestRuntimeType.State.INTERRUPTED, absolute_minute, &"FACILITY_STOPPED") + last_error = "" + return {&"ok": true, &"replayed": false, &"service_id": service_id} + + +func complete_service( + guest_id: StringName, + service_id: StringName, + absolute_minute: int, + satisfaction_sample: int, + need_effects: Dictionary = {} +) -> Dictionary: + var guest := get_guest(guest_id) + if guest == null or guest.current_service_id != service_id: + return _error_result("Guest is not receiving the specified service.") + var normalized_effects: Variant = _normalized_partial_need_map(need_effects) + if not normalized_effects is Dictionary: + return _error_result("Service Need effects must use canonical integer Need IDs.") + if not _transition_is_available(guest, GuestRuntimeType.State.DECIDING, absolute_minute): + return _error_result("Guest cannot complete service in this tick.") + var sample := clampi(satisfaction_sample, 0, 100) + guest.satisfaction_total += sample + guest.satisfaction_samples += 1 + for need_id: StringName in NEED_IDS: + var effect := int(normalized_effects.get(need_id, 0)) + guest.needs[need_id] = clampi(int(guest.needs[need_id]) + effect, 0, 100) + if not guest.target_facility_id.is_empty() \ + and not guest.visited_facility_ids.has(guest.target_facility_id): + guest.visited_facility_ids.append(guest.target_facility_id) + guest.visited_facility_ids.sort_custom( + func(a: StringName, b: StringName) -> bool: return String(a) < String(b) + ) + guest.current_service_id = &"" + guest.queue_request_id = &"" + guest.target_facility_id = &"" + _apply_transition(guest, GuestRuntimeType.State.DECIDING, absolute_minute, &"SERVICE_COMPLETED") + last_error = "" + return {&"ok": true, &"satisfaction_sample": sample} + + +func record_path_failure( + guest_id: StringName, + absolute_minute: int, + recovery_targets: Array = [], + reachability_callback: Callable = Callable() +) -> Dictionary: + var guest := get_guest(guest_id) + if guest == null: + return _error_result("Unknown guest: %s" % guest_id) + guest.path_failure_count += 1 + if guest.path_failure_count < PATH_FAILURE_LIMIT: + if guest.state != GuestRuntimeType.State.REPLANNING: + var transition := transition_guest( + guest_id, + GuestRuntimeType.State.REPLANNING, + absolute_minute, + &"PATH_INVALID" + ) + if not bool(transition.get(&"ok", false)): + return transition + last_error = "" + return { + &"ok": true, + &"recovery_started": false, + &"failure_count": guest.path_failure_count, + } + + var selected := _select_recovery_target(guest, recovery_targets, reachability_callback) + if selected.is_empty(): + ## A virtual emergency egress is preferable to leaving an entity stranded. + selected = { + &"kind": &"emergency_despawn", + &"cell": guest.current_cell, + &"path_handle": {&"virtual_evacuation": true}, + } + if not _transition_is_available(guest, GuestRuntimeType.State.LEAVING, absolute_minute): + return _error_result("Guest cannot enter safe recovery in this tick.") + guest.recovery_kind = StringName(_value(selected, &"kind", &"emergency_exit")) + guest.recovery_target_cell = _vector3i(_value(selected, &"cell", guest.current_cell)) + guest.path_handle = _copy(_value(selected, &"path_handle", { + &"recovery": true, + &"target_cell": guest.recovery_target_cell, + })) + guest.target_facility_id = &"" + guest.queue_request_id = &"" + guest.current_service_id = &"" + guest.exit_reason = &"PATH_RECOVERY" + _apply_transition(guest, GuestRuntimeType.State.LEAVING, absolute_minute, &"PATH_FAILURE_LIMIT") + _diagnostics.append({ + &"code": &"GUEST_PATH_RECOVERY", + &"guest_id": String(guest_id), + &"minute": absolute_minute, + &"failure_count": guest.path_failure_count, + &"recovery_kind": String(guest.recovery_kind), + &"target_cell": _cell_to_array(guest.recovery_target_cell), + }) + guest_recovery_started.emit(guest_id, guest.recovery_kind, guest.recovery_target_cell) + last_error = "" + return { + &"ok": true, + &"recovery_started": true, + &"failure_count": guest.path_failure_count, + &"recovery_kind": guest.recovery_kind, + &"target_cell": guest.recovery_target_cell, + } + + +func record_path_success(guest_id: StringName, cell: Vector3i, absolute_minute: int) -> bool: + var guest := get_guest(guest_id) + if guest == null or absolute_minute < 0: + return _fail("A valid guest and minute are required.") + guest.current_cell = cell + guest.last_progress_cell = cell + guest.last_progress_at = absolute_minute + guest.path_failure_count = 0 + guest.recovery_kind = &"" + last_error = "" + return true + + +func note_movement( + guest_id: StringName, + cell: Vector3i, + absolute_minute: int, + recovery_targets: Array = [], + reachability_callback: Callable = Callable() +) -> Dictionary: + var guest := get_guest(guest_id) + if guest == null: + return _error_result("Unknown guest: %s" % guest_id) + if cell != guest.last_progress_cell: + record_path_success(guest_id, cell, absolute_minute) + return {&"ok": true, &"progressed": true, &"replanned": false} + guest.current_cell = cell + if absolute_minute - guest.last_progress_at < STUCK_REPLAN_MINUTES: + return {&"ok": true, &"progressed": false, &"replanned": false} + guest.last_progress_at = absolute_minute + var result := record_path_failure( + guest_id, + absolute_minute, + recovery_targets, + reachability_callback + ) + result[&"progressed"] = false + result[&"replanned"] = true + return result + + +func begin_leaving( + guest_id: StringName, + absolute_minute: int, + reason: StringName +) -> Dictionary: + var guest := get_guest(guest_id) + if guest == null: + return _error_result("Unknown guest: %s" % guest_id) + if not _transition_is_available(guest, GuestRuntimeType.State.LEAVING, absolute_minute): + return _error_result("Guest cannot begin leaving in this tick.") + guest.target_facility_id = &"" + guest.queue_request_id = &"" + guest.current_service_id = &"" + guest.exit_reason = reason + _apply_transition(guest, GuestRuntimeType.State.LEAVING, absolute_minute, reason) + last_error = "" + return {&"ok": true} + + +func despawn_guest(guest_id: StringName, absolute_minute: int) -> Dictionary: + var guest := get_guest(guest_id) + if guest == null: + return _error_result("Unknown guest: %s" % guest_id) + if guest.state != GuestRuntimeType.State.LEAVING \ + and guest.state != GuestRuntimeType.State.DESPAWNING: + return _error_result("Only a leaving guest can despawn.") + if guest.state != GuestRuntimeType.State.DESPAWNING: + if not _transition_is_available(guest, GuestRuntimeType.State.DESPAWNING, absolute_minute): + return _error_result("Guest cannot enter DESPAWNING in this tick.") + _apply_transition(guest, GuestRuntimeType.State.DESPAWNING, absolute_minute, &"EXIT_REACHED") + var record := { + &"guest_id": String(guest.guest_id), + &"guest_type_id": String(guest.guest_type_id), + &"debt_gold": guest.debt_gold, + &"unresolved_incident_ids": _names_to_strings(guest.unresolved_incident_ids), + &"outstanding_reservation_ids": _names_to_strings(guest.outstanding_reservation_ids), + } + _persistent_records[guest_id] = record + _guests.erase(guest_id) + _pending_spawn_notifications.erase(guest_id) + _guest_order_dirty = true + _decision_rejections.erase(guest_id) + guest_despawned.emit(guest_id, record.duplicate(true)) + last_error = "" + return {&"ok": true, &"persistent_record": record.duplicate(true)} + + +func create_snapshot() -> Dictionary: + var saved_definitions: Array[Dictionary] = [] + for guest_type_id: StringName in _sorted_ids(_definitions.keys()): + saved_definitions.append((_definitions[guest_type_id] as GuestDefinition).create_snapshot()) + var saved_guests: Array[Dictionary] = [] + for guest_id: StringName in _ordered_guest_ids(): + saved_guests.append((_guests[guest_id] as GuestRuntime).create_snapshot()) + var saved_records: Array[Dictionary] = [] + for guest_id: StringName in _sorted_ids(_persistent_records.keys()): + saved_records.append((_persistent_records[guest_id] as Dictionary).duplicate(true)) + return { + &"version": SNAPSHOT_VERSION, + &"current_minute": current_minute, + &"definitions": saved_definitions, + &"guests": saved_guests, + &"persistent_records": saved_records, + &"diagnostics": _diagnostics.duplicate(true), + } + + +func restore_snapshot(snapshot: Dictionary) -> bool: + var raw_version: Variant = snapshot.get(&"version", snapshot.get("version", null)) + if not raw_version is int or int(raw_version) not in [1, SNAPSHOT_VERSION]: + return _fail("Unsupported GuestService snapshot version.") + var prepared: Variant = snapshot.duplicate(true) + if int(raw_version) == 1: + prepared = _migrate_v1_snapshot(snapshot) + if not prepared is Dictionary: + return _fail("GuestService v1 Need schema is invalid or mixed.") + var candidate: Dictionary = prepared + if not candidate.get(&"definitions", null) is Array \ + or not candidate.get(&"guests", null) is Array \ + or not candidate.get(&"persistent_records", null) is Array \ + or not candidate.get(&"diagnostics", null) is Array: + return _fail("GuestService snapshot arrays are missing.") + var next_minute := int(candidate.get(&"current_minute", -2)) + if next_minute < -1: + return _fail("GuestService snapshot minute is invalid.") + + ## Parse into temporary containers. Live state is replaced only after every + ## cross-reference and invariant has been validated. + var next_definitions: Dictionary = {} + for raw_definition: Variant in candidate[&"definitions"]: + if not raw_definition is Dictionary: + return _fail("Guest definition snapshot has an invalid type.") + var definition := GuestDefinitionType.from_snapshot(raw_definition) + if definition == null or next_definitions.has(definition.guest_type_id): + return _fail("Guest definition snapshot is invalid or duplicated.") + next_definitions[definition.guest_type_id] = definition + + var next_guests: Dictionary = {} + for raw_guest: Variant in candidate[&"guests"]: + if not raw_guest is Dictionary: + return _fail("Guest runtime snapshot has an invalid type.") + var guest := GuestRuntimeType.from_snapshot(raw_guest) + if guest == null or next_guests.has(guest.guest_id): + return _fail("Guest runtime snapshot is invalid or duplicated.") + if not next_definitions.has(guest.guest_type_id): + return _fail("Guest runtime snapshot references an unknown definition.") + next_guests[guest.guest_id] = guest + + var next_records: Dictionary = {} + for raw_record: Variant in candidate[&"persistent_records"]: + if not raw_record is Dictionary or not _persistent_record_is_valid(raw_record): + return _fail("Persistent guest record is invalid.") + var record_id := StringName(raw_record.get(&"guest_id", "")) + if next_records.has(record_id) or next_guests.has(record_id): + return _fail("Persistent guest id is duplicated or still active.") + next_records[record_id] = raw_record.duplicate(true) + + var next_diagnostics: Array[Dictionary] = [] + for raw_diagnostic: Variant in candidate[&"diagnostics"]: + if not raw_diagnostic is Dictionary: + return _fail("Guest diagnostic snapshot has an invalid type.") + next_diagnostics.append(raw_diagnostic.duplicate(true)) + + _definitions = next_definitions + _guests = next_guests + _guest_order_dirty = true + _persistent_records = next_records + _decision_rejections = {} + _diagnostics = next_diagnostics + _pending_spawn_notifications.clear() + _external_mutation_blocked = false + current_minute = next_minute + last_error = "" + return true + + +func validate_invariants() -> Dictionary: + var errors: Array[String] = [] + for guest_type_id: StringName in _sorted_ids(_definitions.keys()): + var definition: GuestDefinition = _definitions[guest_type_id] + if not definition.is_valid() or definition.guest_type_id != guest_type_id: + errors.append("Invalid definition: %s" % guest_type_id) + for guest_id: StringName in _sorted_ids(_guests.keys()): + var guest: GuestRuntime = _guests[guest_id] + if not guest.is_valid() or guest.guest_id != guest_id: + errors.append("Invalid runtime: %s" % guest_id) + elif not _definitions.has(guest.guest_type_id): + errors.append("Unknown guest type for: %s" % guest_id) + if _persistent_records.has(guest_id): + errors.append("Guest is both active and persisted: %s" % guest_id) + for guest_id: StringName in _sorted_ids(_persistent_records.keys()): + if not _persistent_record_is_valid(_persistent_records[guest_id]): + errors.append("Invalid persistent record: %s" % guest_id) + return {&"valid": errors.is_empty(), &"errors": errors} + + +func _migrate_v1_snapshot(snapshot: Dictionary) -> Variant: + var definitions: Variant = snapshot.get(&"definitions", snapshot.get("definitions", null)) + var guests: Variant = snapshot.get(&"guests", snapshot.get("guests", null)) + if not definitions is Array or not guests is Array: + return null + var migrated := snapshot.duplicate(true) + for index: int in range(definitions.size()): + var raw_definition: Variant = definitions[index] + if not raw_definition is Dictionary: + return null + var translated_rates: Variant = _translate_v1_need_map( + _value(raw_definition, &"need_rates", null) + ) + if not translated_rates is Dictionary: + return null + migrated[&"definitions"][index][&"need_rates"] = translated_rates + for index: int in range(guests.size()): + var raw_guest: Variant = guests[index] + if not raw_guest is Dictionary: + return null + var translated_needs: Variant = _translate_v1_need_map( + _value(raw_guest, &"needs", null) + ) + if not translated_needs is Dictionary: + return null + migrated[&"guests"][index][&"needs"] = translated_needs + migrated[&"version"] = SNAPSHOT_VERSION + return migrated + + +static func _translate_v1_need_map(source: Variant) -> Variant: + if not source is Dictionary or source.size() != LEGACY_NEED_IDS.size(): + return null + var legacy: Dictionary = {} + for raw_key: Variant in source: + if not raw_key is String and not raw_key is StringName: + return null + var legacy_id := StringName(raw_key) + if not LEGACY_NEED_IDS.has(legacy_id) or legacy.has(legacy_id): + return null + var raw_value: Variant = source[raw_key] + if not raw_value is int: + return null + legacy[legacy_id] = raw_value + if legacy.size() != LEGACY_NEED_IDS.size(): + return null + return { + &"hunger": legacy[&"hunger"], + &"sleep": legacy[&"rest"], + &"hygiene": 0, + &"medical": legacy[&"safety"], + &"leisure": legacy[&"fun"], + &"intimacy": legacy[&"social"], + } + + +func _need_context_error(context: Dictionary) -> String: + for field_id: StringName in [ + &"dirt", + &"injury", + &"illness", + &"intimacy_day", + &"intimacy_night", + ]: + if not _has_value(context, field_id): + continue + var raw_value: Variant = _value(context, field_id, null) + if not raw_value is int or int(raw_value) < 0 or int(raw_value) > 100: + return "%s must be an integer from 0 through 100." % field_id + for field_id: StringName in [&"is_night", &"work_like_activity", &"unstimulated"]: + if _has_value(context, field_id) and not _value(context, field_id, null) is bool: + return "%s must be boolean." % field_id + if _has_value(context, &"need_effects"): + var effects: Variant = _normalized_partial_need_map( + _value(context, &"need_effects", null) + ) + if not effects is Dictionary: + return "need_effects must contain only canonical integer Need IDs." + return "" + + +static func _normalized_partial_need_map(source: Variant) -> Variant: + if not source is Dictionary or source.size() > NEED_IDS.size(): + return null + var result: Dictionary = {} + for raw_key: Variant in source: + if not raw_key is String and not raw_key is StringName: + return null + var need_id := StringName(raw_key) + if not NEED_IDS.has(need_id) or result.has(need_id): + return null + var raw_value: Variant = source[raw_key] + if not raw_value is int: + return null + result[need_id] = raw_value + return result + + +static func _candidate_need_maps_are_valid(candidate: Dictionary) -> bool: + for field_id: StringName in [&"need_relief", &"need_effects"]: + if not _has_value(candidate, field_id): + continue + var raw_value: Variant = _value(candidate, field_id, null) + if field_id == &"need_relief" and raw_value is int: + continue + if not _normalized_partial_need_map(raw_value) is Dictionary: + return false + return true + + +func _candidate_rejection_reason( + guest: GuestRuntime, + definition: GuestDefinition, + candidate: Dictionary, + context: Dictionary +) -> StringName: + if not _candidate_need_maps_are_valid(candidate): + return &"INVALID_NEED_MAP" + if not bool(_value(candidate, &"inputs_available", true)): + return &"MISSING_INPUTS" + var adult_only := bool(_value( + candidate, + &"adult_only", + _value(candidate, &"requires_adult_verified", false) + )) + if adult_only: + if not definition.adult_verified: + return &"ADULT_NOT_VERIFIED" + if not bool(_value(context, &"adult_content_enabled", true)): + return &"CONTENT_FILTERED" + var required_tags := _name_array(_value(candidate, &"required_guest_tags", [])) + for tag: StringName in required_tags: + if not definition.tags.has(tag): + return &"MISSING_PERMISSION" + var forbidden_tags := _name_array(_value(candidate, &"forbidden_guest_tags", [])) + for tag: StringName in forbidden_tags: + if definition.tags.has(tag): + return &"FORBIDDEN_GUEST_TAG" + var allowed_types := _name_array(_value(candidate, &"allowed_guest_types", [])) + if not allowed_types.is_empty() and not allowed_types.has(guest.guest_type_id): + return &"GUEST_TYPE_NOT_ALLOWED" + if not bool(_value(candidate, &"is_open", _value(candidate, &"open", true))) \ + and not bool(_value(candidate, &"open_at_arrival", false)): + return &"FACILITY_CLOSED" + if not _candidate_is_reachable(guest, candidate, context): + return &"UNREACHABLE" + var travel_minutes := maxi(int(_value(candidate, &"travel_minutes", 0)), 0) + var decision_minute := int(_value(context, &"absolute_minute", current_minute)) + if decision_minute >= 0 and decision_minute + travel_minutes > guest.planned_exit_at: + return &"ETA_AFTER_EXIT" + if not _candidate_is_affordable(guest, definition, candidate): + return &"CANNOT_PAY" + var wait_minutes := maxi(int(_value( + candidate, + &"wait_minutes", + _value(candidate, &"expected_wait", 0) + )), 0) + var hard_limit := maxi(int(_value( + candidate, + &"wait_hard_limit", + _value(context, &"wait_hard_limit", DEFAULT_WAIT_HARD_LIMIT) + )), 0) + if wait_minutes >= hard_limit: + return &"WAIT_HARD_LIMIT" + var risk_level := clampi(int(_value(candidate, &"risk_level", _value(candidate, &"risk", 0))), 0, 100) + if risk_level > definition.risk_tolerance: + return &"RISK_REFUSED" + var illegal_level := clampi(int(_value(candidate, &"illegal_level", 0)), 0, 100) + if illegal_level > 100 - definition.legal_sensitivity \ + and not bool(_value(candidate, &"legal_permission_override", false)): + return &"ILLEGALITY_REFUSED" + var quality := clampi(int(_value(candidate, &"expected_quality", _value(candidate, &"quality", 0))), 0, 100) + var minimum_quality := clampi(int(_value(candidate, &"minimum_quality", definition.minimum_quality)), 0, 100) + if quality < minimum_quality: + return &"QUALITY_TOO_LOW" + return &"" + + +func _candidate_is_reachable( + guest: GuestRuntime, + candidate: Dictionary, + context: Dictionary +) -> bool: + var callback: Callable = _value(context, &"reachability_callback", Callable()) + if callback.is_valid(): + return _callback_succeeded(callback.call(guest, candidate.duplicate(true), context)) + return bool(_value(candidate, &"reachable", true)) + + +func _candidate_is_affordable( + guest: GuestRuntime, + definition: GuestDefinition, + candidate: Dictionary +) -> bool: + var price := _candidate_price(candidate) + var cash_available := guest.wallet_gold + var respect_reserve := bool(_value( + candidate, + &"respect_survival_reserve", + _candidate_is_discretionary(candidate) + )) + if respect_reserve: + cash_available = maxi(cash_available - _survival_reserve(guest, definition), 0) + if price <= cash_available: + return true + if not bool(_value(candidate, &"credit_allowed", _value(candidate, &"allow_credit", false))): + return false + var remaining_credit := maxi(guest.credit_limit_gold - guest.debt_gold, 0) + return price - cash_available <= remaining_credit + + +func _candidate_is_discretionary(candidate: Dictionary) -> bool: + if bool(_value(candidate, &"discretionary", false)): + return true + var category := StringName(_value(candidate, &"service_category", &"")) + if category in [&"leisure", &"entertainment", &"gambling", &"intimacy", &"tourism"]: + return true + var facility_tags := _name_array(_value( + candidate, + &"facility_tags", + _value(candidate, &"tags", []) + )) + for tag: StringName in [&"leisure", &"entertainment", &"gambling", &"adult", &"intimacy"]: + if facility_tags.has(tag): + return true + return false + + +func _survival_reserve(guest: GuestRuntime, definition: GuestDefinition) -> int: + if guest.max_addiction() >= 75: + return 0 + if definition.tags.has(&"spendthrift"): + return guest.wallet_gold / 10 + if definition.tags.has(&"frugal"): + return guest.wallet_gold / 2 + return guest.wallet_gold * 3 / 10 + + +func _candidate_price(candidate: Dictionary) -> int: + return maxi(int(_value( + candidate, + &"estimated_total_gold", + _value(candidate, &"price_gold", _value(candidate, &"quoted_price", 0)) + )), 0) + + +func _price_burden(guest: GuestRuntime, candidate: Dictionary) -> int: + var price := _candidate_price(candidate) + var purchasing_power := maxi(guest.wallet_gold + maxi(guest.credit_limit_gold - guest.debt_gold, 0), 1) + return clampi((price * 100 + purchasing_power - 1) / purchasing_power, 0, 100) + + +func _need_relief(guest: GuestRuntime, candidate: Dictionary) -> int: + var raw_relief: Variant = _value(candidate, &"need_relief", 0) + if raw_relief is int: + return maxi(int(raw_relief), 0) + if not raw_relief is Dictionary: + return 0 + var normalized: Dictionary = _normalized_partial_need_map(raw_relief) + var best := 0 + for need_id: StringName in NEED_IDS: + var relief := maxi(int(normalized.get(need_id, 0)), 0) + best = maxi(best, relief * int(guest.needs[need_id]) / 100) + return best + + +func _candidate_urgency(guest: GuestRuntime, candidate: Dictionary) -> int: + var raw_relief: Variant = _value(candidate, &"need_relief", {}) + if not raw_relief is Dictionary: + return 0 + var normalized: Dictionary = _normalized_partial_need_map(raw_relief) + var result := 0 + for need_id: StringName in NEED_IDS: + if int(normalized.get(need_id, 0)) > 0: + result = maxi(result, int(guest.needs[need_id])) + return result + + +func _primary_need_priority(guest: GuestRuntime, candidate: Dictionary) -> int: + var relief: Variant = _value(candidate, &"need_relief", {}) + if not relief is Dictionary: + return 0 + var normalized: Dictionary = _normalized_partial_need_map(relief) + var priority := 0 + for need: StringName in PRIMARY_NEEDS: + if int(normalized.get(need, 0)) > 0: + priority = maxi(priority, int(guest.needs[need])) + return priority + + +func get_observation(guest_id: StringName) -> Dictionary: + var guest := get_guest(guest_id) + if guest == null: + return {} + var highest := 0 + var primary: Array[StringName] = [] + for need: StringName in PRIMARY_NEEDS: + var value := int(guest.needs[need]) + if value > highest: + highest = value + primary.clear() + if value == highest and value > 0: + primary.append(need) + var observed_needs: Dictionary = {} + for need: StringName in NEED_IDS: + observed_needs[need] = guest.needs[need] + return {&"guest_id": guest.guest_id, &"state": guest.state, &"needs": observed_needs, + &"primary_needs": primary, &"target_facility_id": guest.target_facility_id, + &"wallet_gold": guest.wallet_gold, &"patience": guest.patience_remaining} + + +func _candidate_precedes(a: Dictionary, b: Dictionary) -> bool: + var a_priority := int(a.get(&"need_priority", 0)) + var b_priority := int(b.get(&"need_priority", 0)) + if a_priority != b_priority: + return a_priority > b_priority + var a_utility := int(a[&"utility"]) + var b_utility := int(b[&"utility"]) + if a_utility != b_utility: + return a_utility > b_utility + var a_urgency := int(a[&"urgency"]) + var b_urgency := int(b[&"urgency"]) + if a_urgency != b_urgency: + return a_urgency > b_urgency + var a_travel := int(a[&"travel_minutes"]) + var b_travel := int(b[&"travel_minutes"]) + if a_travel != b_travel: + return a_travel < b_travel + return String(a[&"facility_id"]) < String(b[&"facility_id"]) + + +func _select_recovery_target( + guest: GuestRuntime, + targets: Array, + reachability_callback: Callable +) -> Dictionary: + var ordered: Array[Dictionary] = [] + for raw_target: Variant in targets: + if raw_target is Dictionary: + var target: Dictionary = raw_target.duplicate(true) + target[&"kind"] = StringName(_value(target, &"kind", &"emergency_exit")) + target[&"cell"] = _vector3i(_value(target, &"cell", guest.current_cell)) + ordered.append(target) + ordered.sort_custom( + func(a: Dictionary, b: Dictionary) -> bool: + var a_priority := _recovery_priority(StringName(a[&"kind"])) + var b_priority := _recovery_priority(StringName(b[&"kind"])) + if a_priority != b_priority: + return a_priority < b_priority + return _cell_precedes(a[&"cell"], b[&"cell"]) + ) + for target: Dictionary in ordered: + var reachable := bool(_value(target, &"reachable", true)) + if reachability_callback.is_valid(): + reachable = _callback_succeeded(reachability_callback.call( + guest.current_cell, + target[&"cell"], + guest.guest_id, + target.duplicate(true) + )) + if reachable: + return target + return {} + + +func _recovery_priority(kind: StringName) -> int: + match kind: + &"guidance", &"guidance_point": + return 0 + &"entrance", &"main_entrance": + return 1 + &"emergency_exit", &"emergency_egress": + return 2 + _: + return 3 + + +func _transition_is_available(guest: GuestRuntime, new_state: int, minute: int) -> bool: + if minute < 0 or not _is_allowed_transition(guest.state, new_state): + return false + var count := guest.transitions_this_tick if guest.transition_tick == minute else 0 + return count < MAX_TRANSITIONS_PER_TICK + + +func _apply_transition( + guest: GuestRuntime, + new_state: int, + absolute_minute: int, + reason: StringName +) -> void: + var old_state := guest.state + if guest.transition_tick != absolute_minute: + guest.transition_tick = absolute_minute + guest.transitions_this_tick = 0 + guest.transitions_this_tick += 1 + guest.state = new_state as GuestRuntime.State + guest.state_entered_at = absolute_minute + guest_state_changed.emit(guest.guest_id, old_state, new_state, reason) + + +func _force_loop_guard(guest: GuestRuntime, absolute_minute: int) -> void: + var old_state := guest.state + guest.state = GuestRuntimeType.State.DECIDING + guest.state_entered_at = absolute_minute + guest.transition_tick = absolute_minute + guest.transitions_this_tick = MAX_TRANSITIONS_PER_TICK + guest.target_facility_id = &"" + guest.queue_request_id = &"" + guest.current_service_id = &"" + guest.path_handle = null + _diagnostics.append({ + &"code": &"AI_LOOP_GUARD", + &"guest_id": String(guest.guest_id), + &"minute": absolute_minute, + }) + guest_state_changed.emit( + guest.guest_id, + old_state, + GuestRuntimeType.State.DECIDING, + &"AI_LOOP_GUARD" + ) + + +func _is_allowed_transition(from_state: int, to_state: int) -> bool: + if from_state == to_state: + return true + if to_state in [ + GuestRuntimeType.State.LEAVING, + GuestRuntimeType.State.FLEEING, + GuestRuntimeType.State.INCAPACITATED, + ]: + return from_state != GuestRuntimeType.State.DESPAWNING + match from_state: + GuestRuntimeType.State.SPAWNING: + return to_state in [GuestRuntimeType.State.ENTERING, GuestRuntimeType.State.DECIDING] + GuestRuntimeType.State.ENTERING: + return to_state in [GuestRuntimeType.State.DECIDING, GuestRuntimeType.State.MOVING] + GuestRuntimeType.State.DECIDING: + return to_state in [GuestRuntimeType.State.MOVING, GuestRuntimeType.State.QUEUING] + GuestRuntimeType.State.MOVING: + return to_state in [ + GuestRuntimeType.State.QUEUING, + GuestRuntimeType.State.RECEIVING_SERVICE, + GuestRuntimeType.State.DECIDING, + GuestRuntimeType.State.REPLANNING, + ] + GuestRuntimeType.State.QUEUING: + return to_state in [ + GuestRuntimeType.State.RECEIVING_SERVICE, + GuestRuntimeType.State.ABANDONING, + GuestRuntimeType.State.DECIDING, + ] + GuestRuntimeType.State.RECEIVING_SERVICE: + return to_state in [GuestRuntimeType.State.DECIDING, GuestRuntimeType.State.INTERRUPTED] + GuestRuntimeType.State.REPLANNING: + return to_state in [GuestRuntimeType.State.MOVING, GuestRuntimeType.State.DECIDING] + GuestRuntimeType.State.ABANDONING, GuestRuntimeType.State.INTERRUPTED: + return to_state == GuestRuntimeType.State.DECIDING + GuestRuntimeType.State.FLEEING: + return to_state in [GuestRuntimeType.State.LEAVING, GuestRuntimeType.State.DESPAWNING] + GuestRuntimeType.State.LEAVING: + return to_state == GuestRuntimeType.State.DESPAWNING + _: + return false + + +static func _callback_succeeded(result: Variant) -> bool: + if result == null: + return true + if result is bool: + return result + if result is Dictionary: + return bool(result.get(&"ok", result.get("ok", result.get(&"success", result.get("success", false))))) + if result is Object and result.has_method("is_success"): + return bool(result.call("is_success")) + return false + + +static func _persistent_record_is_valid(record: Dictionary) -> bool: + if StringName(record.get(&"guest_id", "")).is_empty(): + return false + if StringName(record.get(&"guest_type_id", "")).is_empty(): + return false + if int(record.get(&"debt_gold", -1)) < 0: + return false + return record.get(&"unresolved_incident_ids", null) is Array \ + and record.get(&"outstanding_reservation_ids", null) is Array + + +static func _facility_id(candidate: Dictionary) -> StringName: + return StringName(_value( + candidate, + &"facility_id", + _value(candidate, &"instance_id", &"") + )) + + +static func _name_array(source: Variant) -> Array[StringName]: + var result: Array[StringName] = [] + if not source is Array: + return result + for raw_value: Variant in source: + var value := StringName(raw_value) + if not value.is_empty() and not result.has(value): + result.append(value) + result.sort_custom(func(a: StringName, b: StringName) -> bool: return String(a) < String(b)) + return result + + +static func _sorted_ids(source: Array) -> Array[StringName]: + var result: Array[StringName] = [] + for raw_value: Variant in source: + result.append(StringName(raw_value)) + result.sort_custom(func(a: StringName, b: StringName) -> bool: return String(a) < String(b)) + return result + + +func _ordered_guest_ids() -> Array[StringName]: + if _guest_order_dirty: + _sorted_guest_ids_cache = _sorted_ids(_guests.keys()) + _guest_order_dirty = false + return _sorted_guest_ids_cache + + +static func _value(source: Dictionary, key: StringName, fallback: Variant) -> Variant: + if source.has(key): + return source[key] + var string_key := String(key) + return source[string_key] if source.has(string_key) else fallback + + +static func _has_value(source: Dictionary, key: StringName) -> bool: + return source.has(key) or source.has(String(key)) + + +static func _need_report(values: Dictionary) -> Dictionary: + # Matches _string_key_dictionary's canonical lexical key order without + # allocating and sorting a six-key array for every guest and minute. + return { + "hunger": int(values[&"hunger"]), + "hygiene": int(values[&"hygiene"]), + "intimacy": int(values[&"intimacy"]), + "leisure": int(values[&"leisure"]), + "medical": int(values[&"medical"]), + "sleep": int(values[&"sleep"]), + } + + +static func _copy(value: Variant) -> Variant: + if value is Array or value is Dictionary: + return value.duplicate(true) + return value + + +static func _vector3i(value: Variant) -> Vector3i: + if value is Vector3i: + return value + if value is Array and value.size() == 3: + return Vector3i(int(value[0]), int(value[1]), int(value[2])) + return Vector3i.ZERO + + +static func _cell_to_array(cell: Vector3i) -> Array[int]: + return [cell.x, cell.y, cell.z] + + +static func _cell_precedes(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 + + +static func _names_to_strings(source: Array[StringName]) -> Array[String]: + var result: Array[String] = [] + for value: StringName in source: + result.append(String(value)) + return result + + +static func _string_key_dictionary(source: Dictionary) -> Dictionary: + var result: Dictionary = {} + var keys: Array = source.keys() + keys.sort_custom(func(a: Variant, b: Variant) -> bool: return String(a) < String(b)) + for raw_key: Variant in keys: + result[String(raw_key)] = source[raw_key] + return result + + +func _fail(message: String) -> bool: + last_error = message + return false + + +func _error_result(message: String) -> Dictionary: + last_error = message + return {&"ok": false, &"error": message} diff --git a/docs/plans/original-completion-2026-10-02/checkpoints/native-hot-loop-r325-r329/022-simulation_runtime.gd b/docs/plans/original-completion-2026-10-02/checkpoints/native-hot-loop-r325-r329/022-simulation_runtime.gd new file mode 100644 index 0000000..c9c204c --- /dev/null +++ b/docs/plans/original-completion-2026-10-02/checkpoints/native-hot-loop-r325-r329/022-simulation_runtime.gd @@ -0,0 +1,13548 @@ +class_name SimulationRuntimeService +extends Node + +signal command_completed(result: CommandResult) +signal snapshot_published(snapshot: Dictionary, digest: String) +# UI subscribers only need notification; exact snapshots remain available. +signal state_updated(snapshot: Dictionary) +signal definition_validation_failed(report: Dictionary) +signal save_operation_completed(result: Dictionary) +# Optional, read-only UI contribution collected at the actual checkpoint. +signal checkpoint_presentation_requested(presentation: Dictionary) + +const DEFAULT_WORLD_SEED := 20260827 +const STARTER_MANA_OUTPUT := 100 +const STARTER_MANA_SOURCE := &"world_tree.mana_source" +const STARTER_MANA_BUS := &"world_tree.mana_bus" +const PLAYER_CASH_ACCOUNT := &"PLAYER_CASH" +const CONSTRUCTION_EXPENSE_ACCOUNT := &"CONSTRUCTION_EXPENSE" +const OPERATING_EXPENSE_ACCOUNT := &"OPERATING_EXPENSE" +const AUTHORITY_FINE_ACCOUNT := &"AUTHORITY_FINE_EXPENSE" +const WAGE_EXPENSE_ACCOUNT := &"WAGE_EXPENSE" +const RUNTIME_SNAPSHOT_VERSION := 14 +const AUTO_PURCHASE_RUNTIME_SNAPSHOT_VERSION := 14 +const EVENT_THREAT_RUNTIME_SNAPSHOT_VERSION := 13 +const MIMI_PURCHASE_RUNTIME_SNAPSHOT_VERSION := 15 +const PurchaseQuoteV2 := preload("res://scripts/supply/supply_purchase_quote_v2.gd") +const AutoPurchasePlansType := preload("res://scripts/supply/supply_auto_purchase_quote_plans.gd") +const CLEANING_RUNTIME_SNAPSHOT_VERSION := 12 +const WEATHER_RUNTIME_SNAPSHOT_VERSION := 11 +const FacilityCleanliness = preload("res://scripts/environment/facility_cleanliness_service.gd") +const CleaningBridge = preload("res://scripts/environment/facility_cleaning_bridge.gd") +const CleaningPolicy = preload("res://scripts/environment/facility_cleaning_policy.gd") +const UPKEEP_RUNTIME_SNAPSHOT_VERSION := 10 +const WeatherTimeline = preload("res://scripts/environment/weather_timeline.gd") +const STAIRS_RUNTIME_SNAPSHOT_VERSION := 9 +const TRANSPORT_RUNTIME_SNAPSHOT_VERSION := 8 +const SUPPLY_RUNTIME_SNAPSHOT_VERSION := 7 +const INFRASTRUCTURE_RUNTIME_SNAPSHOT_VERSION := 6 +const LEGACY_RUNTIME_SNAPSHOT_VERSION := 1 +const DEFENSE_RUNTIME_SNAPSHOT_VERSION := 2 +const PATH_TRAFFIC_RUNTIME_SNAPSHOT_VERSION := 3 +const CONTENT_RUNTIME_SNAPSHOT_VERSION := 4 +const OPERATIONS_RUNTIME_SNAPSHOT_VERSION := 5 +const MVP_CONTENT_MANIFEST_VERSION := 2 +const OPERATIONS_RUNTIME_WRAPPER_VERSION := 1 +const OPERATIONS_START_MINUTE := 360 +const OPERATIONS_COORDINATOR_ORIGIN_MINUTE := 359 +const MAX_PERSONAL_ACTION_POINTS := 4 +const OPERATIONS_TEST_PREPARE_PHASE: StringName = &"PREPARE" +const OPERATIONS_TEST_AFTER_FIRST_MUTATION_PHASE: StringName = &"AFTER_FIRST_MUTATION" +const DEFAULT_OPERATIONS_DEMAND_PROFILES := [ + {&"facility_tag": &"hygiene", &"guest_class": &"cleric", &"time_band": &"MORNING"}, + {&"facility_tag": &"service", &"guest_class": &"diplomat", &"time_band": &"MORNING"}, + {&"facility_tag": &"lodging", &"guest_class": &"mage", &"time_band": &"DAY"}, + {&"facility_tag": &"service", &"guest_class": &"merchant", &"time_band": &"DAY"}, + {&"facility_tag": &"lodging", &"guest_class": &"noble", &"time_band": &"EVENING"}, + {&"facility_tag": &"dining", &"guest_class": &"warrior", &"time_band": &"EVENING"}, + {&"facility_tag": &"leisure", &"guest_class": &"outsider", &"time_band": &"NIGHT"}, + {&"facility_tag": &"underground", &"guest_class": &"rogue", &"time_band": &"NIGHT"}, +] +const MOVEMENT_CREDIT_SCALE := 1_000 +const STAIR_MOVEMENT_CREDIT := 1_500 +const MIN_MOVEMENT_CREDIT_PER_MINUTE := 250 +const DEFAULT_AUTOSAVE_PATH := "user://saves/autosave" +const SAVE_LIFECYCLE_VERSION := 1 +const DEFAULT_SAVE_POLICY := { + &"enabled": false, + &"daily_checkpoint_enabled": true, + &"autosave_path": DEFAULT_AUTOSAVE_PATH, + &"autosave_generations": SaveService.DEFAULT_AUTOSAVE_GENERATIONS, + &"auto_migrate": true, +} +const RNG_STREAM_IDS: Array[StringName] = [ + &"world", + &"guest_spawn", + &"guest_decision", + &"staff", + &"relationship", + &"authority", + &"defense", + &"event", + &"loot", +] +const DEFENSE_COMMAND_TYPES: Array[StringName] = [ + &"create_defense_raid", + &"register_defense_combatant", + &"register_defense_asset", + &"start_defense_raid", + &"set_defense_retreat_policy", + &"queue_defense_skill", + &"resolve_defense_round", + &"begin_next_defense_wave", + &"retreat_defenders", + &"cancel_defense_raid", +] + +var definition_registry := DefinitionRegistry.new() +var command_queue := CommandQueue.new() +var id_generator := StableIdGenerator.new() +var domain_events := DomainEventStream.new() +var world_service := WorldService.new() +var path_service := PathService.new() +var mana_network_service := ManaNetworkService.new() +var weather := WeatherTimeline.new() +var facility_cleanliness := FacilityCleanliness.new() +var cleaning_policy := CleaningPolicy.new() +var cleaning_bridge := CleaningBridge.new() +var facility_upkeep := FacilityUpkeepAccrual.new() +var upkeep_settlement := FacilityUpkeepSettlement.new() +var economy_service: EconomyService +var construction_resource_state: LedgerBackedResourceState +var operating_resource_state: LedgerBackedResourceState +var inventory_service := InventoryService.new() +var logistics_dispatcher := LogisticsDispatcher.new() +var logistics_requests: Dictionary = {} +var kitchen_production := KitchenProductionService.new() +var supply_management := SupplyManagementService.new() +var auto_purchase_plans := AutoPurchasePlansType.new() +var supply_logistics := SupplyLogisticsBridge.new() +var elevators := ElevatorService.new() +var legacy_stairs := LegacyStairService.new() +var staff_commute := StaffCommuteService.new() +var facility_service := FacilityService.new() +var guest_service := GuestService.new() +var staff_service := StaffService.new() +var mvp_content_catalog := MvpContentCatalog.new() +var mvp_content_report: Dictionary = {} +# Cache only catalog-derived immutable metadata, never GuestDefinition objects. +var _mvp_manifest_cache_catalog: MvpContentCatalog +var _mvp_manifest_cache_generation := -1 +var _mvp_manifest_cache: Dictionary = {} +var progression_service := ProgressionService.new() +var operations_coordinator: OperationsCoordinator +var relationship_service := RelationshipService.new() +var event_service := EventService.new() +var authority_service := AuthorityService.new() +var defense_service := DefenseService.new() +var construction_service: ConstructionService +var excavation_service: ExcavationService +var spatial_subslot_service: SpatialSubslotService +var route_subslot_path_adapter: RouteSubslotPathAdapter +var facility_infrastructure_projector: FacilityInfrastructureProjector +var latest_snapshot: Dictionary = {} +var _latest_digest := "" +var _latest_digest_dirty := false +var latest_digest: String: + get: + if _latest_digest_dirty: + _latest_digest = SimulationDigest.compute(latest_snapshot) + _latest_digest_dirty = false + return _latest_digest +var latest_save_operation_result: Dictionary = {} +var latest_restore_safety_report: Dictionary = {} +var world_seed := DEFAULT_WORLD_SEED +var save_migration_registry := SaveMigrationRegistry.new() + +var _clock: SimulationKernel +var _state: Variant +var _rng_streams: Dictionary = {} +var _executors: Dictionary = {} +var _next_command_id := 1 +var _staffing_projection_enabled := false +var _movement_credits: Dictionary = {} +var _hired_staff_candidate_ids: Dictionary = {} +var _save_policy: Dictionary = DEFAULT_SAVE_POLICY.duplicate(true) +var _last_daily_checkpoint_day := -1 +var _pending_checkpoints: Array[Dictionary] = [] +var _next_checkpoint_request_id := 1 +var _initialized := false +var _active_command_minute := -1 +var _operations_enabled := false +# Transient in-tick execution guards. Persisted timing lives inside Defense. +var _defense_round_attempt_minute := -1 +var _defense_round_attempted_raids: Dictionary = {} +var _defense_automatic_failed_minute := -1 + +var _operations_tick_blocked := false +var _operations_runtime_failure_latched := false +var _operations_runtime_failure_report: Dictionary = {} +var _operations_deferred_runtime_minute := -1 +var _operations_resume_publish_pending := false +var _personal_action_points := 0 +var _operations_test_boundary_hooks: Dictionary = {} +var _operations_transaction_active := false +var _operations_queued_domain_events: Array[SimulationDomainEvent] = [] +var _operations_queued_guest_spawn_notifications: Array[StringName] = [] +var _operations_flushing_guest_spawn_notifications: Dictionary = {} +var _operations_queued_economy_notifications: Array[StringName] = [] +var _operations_flushing_economy_notifications: Dictionary = {} +var _infrastructure_failure_latched := false +var _infrastructure_failure_report: Dictionary = {} +var _construction_completion_transaction_active := false +var _construction_committing_notifications := false +var _construction_queued_domain_events: Array[SimulationDomainEvent] = [] + +func _ready() -> void: + # The autoload is also present in headless test SceneTrees, so merely entering + # the tree must never opt into disk writes. The playable main scene enables + # the production policy explicitly. + if not _initialized: + initialize(GameClock, GameState, DEFAULT_WORLD_SEED) + +func initialize(clock: SimulationKernel, state: Variant, seed := DEFAULT_WORLD_SEED) -> Dictionary: + if _initialized and _clock != null: + _clock.unregister_tick_handler(_clock.TickPhase.TIME_BOUNDARIES, &"simulation_runtime.0_weather") + _clock.unregister_tick_handler(_clock.TickPhase.TIME_BOUNDARIES, &"simulation_runtime.a_upkeep") + _clock.unregister_tick_handler(_clock.TickPhase.TIME_BOUNDARIES, &"simulation_runtime.operations") + _clock.unregister_tick_handler(_clock.TickPhase.APPLY_COMMANDS, &"simulation_runtime.commands") + _clock.unregister_tick_handler(_clock.TickPhase.PUBLISH_SNAPSHOT, &"simulation_runtime.snapshot") + _clock.unregister_tick_handler(_clock.TickPhase.CONSTRUCTION, &"simulation_runtime.construction") + _clock.unregister_tick_handler(_clock.TickPhase.MANA, &"simulation_runtime.mana") + _clock.unregister_tick_handler(_clock.TickPhase.LOGISTICS, &"simulation_runtime.logistics") + _clock.unregister_tick_handler(_clock.TickPhase.FACILITY_COMPLETION, &"simulation_runtime.facilities") + _clock.unregister_tick_handler(_clock.TickPhase.VITALS_AND_ENVIRONMENT, &"simulation_runtime.guest_vitals") + _clock.unregister_tick_handler(_clock.TickPhase.FACILITY_QUEUES, &"simulation_runtime.guest_queues") + _clock.unregister_tick_handler(_clock.TickPhase.INCIDENTS_AND_EVENTS, &"simulation_runtime.authority_incidents") + _clock.unregister_tick_handler(_clock.TickPhase.INCIDENTS_AND_EVENTS, &"simulation_runtime.defense") + _clock.unregister_tick_handler(_clock.TickPhase.INCIDENTS_AND_EVENTS, &"simulation_runtime.relationship_events") + _clock.unregister_tick_handler(_clock.TickPhase.AI_DECISIONS, &"simulation_runtime.staff_ai") + _clock.unregister_tick_handler(_clock.TickPhase.PUBLISH_SNAPSHOT, &"simulation_runtime.zz_save_checkpoints") + _detach_defense_service(defense_service) + _clear_mvp_content_manifest_cache() + _clock = clock + _state = state + world_seed = seed + mvp_content_catalog = MvpContentCatalog.new() + mvp_content_report = mvp_content_catalog.rebuild() + progression_service = ProgressionService.new() + var initial_progression_ok := progression_service.configure(1, { + &"asset_gold": maxi(int(_state.get_resource(&"gold")), 0), + &"faith_lifetime": maxi(int(_state.get_resource(&"faith")), 0), + }) + _hired_staff_candidate_ids.clear() + command_queue = CommandQueue.new() + id_generator = StableIdGenerator.new() + domain_events = DomainEventStream.new() + world_service = WorldService.new() + path_service = PathService.new() + path_service.set_vertical_walk_requires_connector(true) + legacy_stairs = LegacyStairService.new() + legacy_stairs.configure(path_service) + _movement_credits.clear() + _connect_path_performance_signal() + mana_network_service = ManaNetworkService.new() + _initialize_world(maxi(int(_state.world_tree_rank), 1)) + _initialize_mana_network() + _initialize_economy() + _initialize_s2_operations() + _staffing_projection_enabled = false + _initialize_s3_actors() + _initialize_s4_relationship_events() + _initialize_s5_authority() + _initialize_s5_defense() + construction_service = ConstructionService.new(world_service, construction_resource_state) + construction_service.current_minute = _clock.absolute_minute + _connect_construction_signals() + var initial_infrastructure_ok := _initialize_infrastructure_services() + _next_command_id = 1 + _last_daily_checkpoint_day = -1 + _pending_checkpoints.clear() + _next_checkpoint_request_id = 1 + _active_command_minute = -1 + operations_coordinator = null + _operations_enabled = false + _operations_tick_blocked = false + _operations_runtime_failure_latched = false + _operations_runtime_failure_report.clear() + _operations_deferred_runtime_minute = -1 + _operations_resume_publish_pending = false + _personal_action_points = 0 + _operations_test_boundary_hooks.clear() + _operations_transaction_active = false + _operations_queued_domain_events.clear() + _operations_queued_guest_spawn_notifications.clear() + _operations_flushing_guest_spawn_notifications.clear() + _operations_queued_economy_notifications.clear() + _operations_flushing_economy_notifications.clear() + _infrastructure_failure_latched = false + _infrastructure_failure_report.clear() + _construction_completion_transaction_active = false + _construction_committing_notifications = false + _construction_queued_domain_events.clear() + latest_save_operation_result.clear() + latest_restore_safety_report.clear() + _initialize_rng_streams() + supply_logistics = SupplyLogisticsBridge.new() + supply_logistics.configure(self) + cleaning_bridge.configure(self) + elevators = ElevatorService.new() + elevators.configure(path_service) + elevators.synchronize([], _clock.absolute_minute) + staff_commute = StaffCommuteService.new() + staff_commute.configure(self) + _executors.clear() + register_command_executor(&"change_resources", Callable(self, "_execute_change_resources")) + register_command_executor(&"spend_resources", Callable(self, "_execute_spend_resources")) + register_command_executor(&"repay_loan_principal", Callable(self, "_execute_repay_loan_principal")) + register_command_executor(&"build_facility", Callable(self, "_execute_build_facility")) + register_command_executor(&"cancel_construction", Callable(self, "_execute_cancel_construction")) + register_command_executor(&"demolish_facility", Callable(self, "_execute_demolish_facility")) + register_command_executor(&"set_elevator_operation", Callable(self, "_execute_set_elevator_operation")) + register_command_executor(&"start_excavation", Callable(self, "_execute_start_excavation")) + register_command_executor(&"pause_excavation", Callable(self, "_execute_pause_excavation")) + register_command_executor(&"resume_excavation", Callable(self, "_execute_resume_excavation")) + register_command_executor(&"set_world_tree_rank", Callable(self, "_execute_set_world_tree_rank")) + register_command_executor(&"update_progression_metrics", Callable(self, "_execute_update_progression_metrics")) + register_command_executor(&"complete_progression_quest", Callable(self, "_execute_complete_progression_quest")) + register_command_executor(&"acknowledge_operations_pause", Callable(self, "_execute_acknowledge_operations_pause")) + register_command_executor(&"purchase_supplies", Callable(self, "_execute_purchase_supplies")) + register_command_executor(&"set_supply_auto_purchase", Callable(self, "_execute_set_supply_auto_purchase")) + register_command_executor(&"set_kitchen_production", Callable(self, "_execute_set_kitchen_production")) + register_command_executor(&"add_inventory_stack", Callable(self, "_execute_add_inventory_stack")) + register_command_executor(&"create_logistics_request", Callable(self, "_execute_create_logistics_request")) + register_command_executor(&"dispatch_logistics", Callable(self, "_execute_dispatch_logistics")) + register_command_executor(&"advance_logistics_request", Callable(self, "_execute_advance_logistics_request")) + register_command_executor(&"register_guest_wallet", Callable(self, "_execute_register_guest_wallet")) + register_command_executor(&"create_service_request", Callable(self, "_execute_create_service_request")) + register_command_executor(&"set_facility_conditions", Callable(self, "_execute_set_facility_conditions")) + register_command_executor(&"set_facility_policy", Callable(self, "_execute_set_facility_policy")) + register_command_executor(&"leave_facility_queue", Callable(self, "_execute_leave_facility_queue")) + register_command_executor(&"spawn_guest", Callable(self, "_execute_spawn_guest")) + register_command_executor(&"transition_guest", Callable(self, "_execute_transition_guest")) + register_command_executor(&"decide_guest", Callable(self, "_execute_decide_guest")) + register_command_executor(&"register_staff", Callable(self, "_execute_register_staff")) + register_command_executor(&"hire_staff_candidate", Callable(self, "_execute_hire_staff_candidate")) + register_command_executor(&"create_staff_shift", Callable(self, "_execute_create_staff_shift")) + register_command_executor(&"set_staff_schedule_override", Callable(self, "_execute_set_staff_schedule_override")) + register_command_executor(&"set_staff_work_assignment", Callable(self, "_execute_set_staff_work_assignment")) + register_command_executor(&"create_staff_task", Callable(self, "_execute_create_staff_task")) + register_command_executor(&"begin_staff_task", Callable(self, "_execute_begin_staff_task")) + register_command_executor(&"progress_staff_task", Callable(self, "_execute_progress_staff_task")) + register_command_executor(&"create_relationship", Callable(self, "_execute_create_relationship")) + register_command_executor(&"change_relationship", Callable(self, "_execute_change_relationship")) + register_command_executor(&"attack_relationship_shield", Callable(self, "_execute_attack_relationship_shield")) + register_command_executor(&"request_route_lock", Callable(self, "_execute_request_route_lock")) + register_command_executor(&"confirm_route_lock", Callable(self, "_execute_confirm_route_lock")) + register_command_executor(&"register_event_definition", Callable(self, "_execute_register_event_definition")) + register_command_executor(&"enqueue_event", Callable(self, "_execute_enqueue_event")) + register_command_executor(&"reserve_event_choice_cost", Callable(self, "_execute_reserve_event_choice_cost")) + register_command_executor(&"submit_event_choice", Callable(self, "_execute_submit_event_choice")) + register_command_executor(&"report_authority_incident", Callable(self, "_execute_report_authority_incident")) + register_command_executor(&"register_authority_evidence", Callable(self, "_execute_register_authority_evidence")) + register_command_executor(&"discover_authority_evidence", Callable(self, "_execute_discover_authority_evidence")) + register_command_executor(&"evaluate_authority_inspection", Callable(self, "_execute_evaluate_authority_inspection")) + register_command_executor(&"schedule_authority_inspection", Callable(self, "_execute_schedule_authority_inspection")) + register_command_executor(&"start_authority_inspection", Callable(self, "_execute_start_authority_inspection")) + register_command_executor(&"arrive_authority_inspection", Callable(self, "_execute_arrive_authority_inspection")) + register_command_executor(&"plan_authority_inspection", Callable(self, "_execute_plan_authority_inspection")) + register_command_executor(&"advance_authority_inspection", Callable(self, "_execute_advance_authority_inspection")) + register_command_executor(&"inspect_authority_waypoint", Callable(self, "_execute_inspect_authority_waypoint")) + register_command_executor(&"resolve_authority_inspection", Callable(self, "_execute_resolve_authority_inspection")) + register_command_executor(&"depart_authority_inspection", Callable(self, "_execute_depart_authority_inspection")) + register_command_executor(&"create_defense_raid", Callable(self, "_execute_create_defense_raid")) + register_command_executor(&"register_defense_combatant", Callable(self, "_execute_register_defense_combatant")) + register_command_executor(&"register_defense_asset", Callable(self, "_execute_register_defense_asset")) + register_command_executor(&"start_defense_raid", Callable(self, "_execute_start_defense_raid")) + register_command_executor(&"set_defense_retreat_policy", Callable(self, "_execute_set_defense_retreat_policy")) + register_command_executor(&"queue_defense_skill", Callable(self, "_execute_queue_defense_skill")) + register_command_executor(&"resolve_defense_round", Callable(self, "_execute_resolve_defense_round")) + register_command_executor(&"begin_next_defense_wave", Callable(self, "_execute_begin_next_defense_wave")) + register_command_executor(&"retreat_defenders", Callable(self, "_execute_retreat_defenders")) + register_command_executor(&"cancel_defense_raid", Callable(self, "_execute_cancel_defense_raid")) + # Weather commits before upkeep and arrivals, including same-minute retries. + _clock.unregister_tick_handler(_clock.TickPhase.TIME_BOUNDARIES, &"simulation_runtime.0_weather") + _clock.register_tick_handler(_clock.TickPhase.TIME_BOUNDARIES, &"simulation_runtime.0_weather", Callable(self, "_advance_weather")) + _clock.unregister_tick_handler(_clock.TickPhase.TIME_BOUNDARIES, &"simulation_runtime.a_upkeep") + _clock.register_tick_handler(_clock.TickPhase.TIME_BOUNDARIES, &"simulation_runtime.a_upkeep", Callable(self, "_advance_facility_upkeep")) + _clock.register_tick_handler( + _clock.TickPhase.APPLY_COMMANDS, + &"simulation_runtime.commands", + Callable(self, "_process_tick_commands") + ) + _clock.register_tick_handler( + _clock.TickPhase.PUBLISH_SNAPSHOT, + &"simulation_runtime.snapshot", + Callable(self, "_publish_snapshot") + ) + _clock.register_tick_handler( + _clock.TickPhase.MANA, + &"simulation_runtime.mana", + Callable(self, "_advance_mana") + ) + _clock.register_tick_handler( + _clock.TickPhase.CONSTRUCTION, + &"simulation_runtime.construction", + Callable(self, "_advance_construction") + ) + _clock.register_tick_handler( + _clock.TickPhase.LOGISTICS, + &"simulation_runtime.logistics", + Callable(self, "_advance_inventory_and_logistics") + ) + _clock.register_tick_handler( + _clock.TickPhase.FACILITY_COMPLETION, + &"simulation_runtime.facilities", + Callable(self, "_advance_facilities") + ) + _clock.register_tick_handler( + _clock.TickPhase.VITALS_AND_ENVIRONMENT, + &"simulation_runtime.guest_vitals", + Callable(self, "_advance_guest_vitals") + ) + _clock.register_tick_handler( + _clock.TickPhase.FACILITY_QUEUES, + &"simulation_runtime.guest_queues", + Callable(self, "_advance_guest_queues") + ) + _clock.register_tick_handler( + _clock.TickPhase.INCIDENTS_AND_EVENTS, + &"simulation_runtime.authority_incidents", + Callable(self, "_advance_authority_incidents") + ) + _clock.register_tick_handler( + _clock.TickPhase.INCIDENTS_AND_EVENTS, + &"simulation_runtime.defense", + Callable(self, "_advance_defense") + ) + _clock.register_tick_handler( + _clock.TickPhase.INCIDENTS_AND_EVENTS, + &"simulation_runtime.relationship_events", + Callable(self, "_advance_relationship_events") + ) + _clock.register_tick_handler( + _clock.TickPhase.AI_DECISIONS, + &"simulation_runtime.staff_ai", + Callable(self, "_advance_staff_ai") + ) + # The `zz` ID deliberately orders persistence after the published simulation + # snapshot at the final phase of a fully completed minute. + _clock.register_tick_handler( + _clock.TickPhase.PUBLISH_SNAPSHOT, + &"simulation_runtime.zz_save_checkpoints", + Callable(self, "_process_save_checkpoints") + ) + _initialized = true + var report := definition_registry.rebuild_from_paths( + FacilityCatalog.FACILITY_PATHS + FacilityCatalog.INFRASTRUCTURE_FACILITY_PATHS + FacilityCatalog.RESCUE_FACILITY_PATHS + FacilityCatalog.ORIGINAL_FACILITY_PATHS) + var catalog_registration_ok := _catalog_definitions_are_registered(guest_service) + report[&"content_catalog"] = mvp_content_report.duplicate(true) + report[&"progression_initialized"] = initial_progression_ok + report[&"catalog_guest_definitions_registered"] = catalog_registration_ok + report[&"infrastructure_initialized"] = initial_infrastructure_ok + report[&"cleaning_policy_valid"] = not cleaning_policy.create_snapshot().is_empty() + report[&"valid"] = bool(report[&"valid"]) \ + and bool(mvp_content_report.get(&"valid", false)) \ + and catalog_registration_ok \ + and initial_progression_ok \ + and initial_infrastructure_ok \ + and bool(report[&"cleaning_policy_valid"]) \ + and int(_state.world_tree_rank) == ProgressionService.MIN_RANK + if not report[&"valid"]: + _clock.set_paused(true) + definition_validation_failed.emit(report) + _publish_snapshot(_clock.absolute_minute) + return report + +func register_command_executor(command_type: StringName, executor: Callable) -> bool: + if command_type.is_empty() or not executor.is_valid() or _executors.has(command_type): + return false + _executors[command_type] = executor + return true + +func unregister_command_executor(command_type: StringName) -> void: + _executors.erase(command_type) + +func submit_command( + command_type: StringName, + payload: Dictionary = {}, + actor_id: StringName = &"player", + target_id: StringName = &"", + priority := 0, + execute_at := -1, + idempotency_key: StringName = &"" +) -> CommandResult: + if not _initialized: + return CommandResult.failed(CommandResult.Code.EXECUTOR_FAILED, 0, "Simulation runtime is not initialized.") + if _operations_enabled and operations_coordinator != null and operations_coordinator.is_terminal(): + return CommandResult.failed( + CommandResult.Code.VALIDATION_FAILED, 0, + "ゲーム終了後は操作できません。帳簿の確認・保存・読み込みは利用できます。", + {&"reason": &"BANKRUPT"} + ) + var scheduled_tick := _clock.absolute_minute if execute_at < 0 else execute_at + var command := SimulationCommand.new( + _next_command_id, + _clock.absolute_minute, + scheduled_tick, + priority, + command_type, + actor_id, + target_id, + payload, + idempotency_key + ) + var enqueue_result := command_queue.enqueue(command) + if not enqueue_result.is_success(): + return enqueue_result + _next_command_id += 1 + if _clock.paused and scheduled_tick <= _clock.absolute_minute: + var resume_snapshot_published := _process_commands(_clock.absolute_minute) + if not resume_snapshot_published: + _publish_snapshot(_clock.absolute_minute) + var completed := command_queue.get_processed_result(command.command_id) + if completed != null: + return completed + return enqueue_result + +func get_rng_stream(stream_id: StringName) -> DeterministicRng: + return _rng_streams.get(stream_id) as DeterministicRng + +func create_snapshot() -> Dictionary: + var rng_data := {} + for stream_id: StringName in _sorted_name_keys(_rng_streams): + var stream: DeterministicRng = _rng_streams[stream_id] + rng_data[String(stream_id)] = stream.snapshot() + var owned_systems := { + &"runtime": { + &"runtime_schema_version": RUNTIME_SNAPSHOT_VERSION, + &"weather": weather.create_snapshot(), + &"world_seed": world_seed, + &"next_command_id": _next_command_id, + &"staffing_projection_enabled": _staffing_projection_enabled, + &"save_lifecycle": { + &"version": SAVE_LIFECYCLE_VERSION, + &"last_daily_checkpoint_day": _last_daily_checkpoint_day, + }, + &"command_queue": command_queue.create_snapshot(), + &"id_generator": id_generator.create_snapshot(), + &"domain_events": domain_events.create_snapshot(), + &"rng_streams": rng_data, + &"world": world_service.create_snapshot(), + &"path": path_service.create_snapshot(), + &"movement_credits": _serialize_movement_credits(), + &"mana": mana_network_service.create_snapshot(), + &"economy": economy_service.create_snapshot(), + &"facility_upkeep": facility_upkeep.create_snapshot(), + &"upkeep_settlement": upkeep_settlement.create_snapshot(), + &"inventory": inventory_service.create_snapshot(), + &"kitchen_production": kitchen_production.create_snapshot(), + &"auto_purchase_plans": auto_purchase_plans.create_snapshot(), + &"elevators": elevators.create_snapshot(), + &"legacy_stairs": legacy_stairs.create_snapshot(), + &"logistics_dispatcher": logistics_dispatcher.create_snapshot(), + &"logistics_requests": _serialize_logistics_requests(), + &"facilities": facility_service.create_snapshot(), + &"facility_cleanliness": facility_cleanliness.create_snapshot(), + &"guests": guest_service.create_snapshot(), + &"staff": staff_service.create_snapshot(), + &"catalog_manifest": _create_mvp_content_manifest(mvp_content_catalog), + &"progression": progression_service.create_snapshot(), + &"hired_staff_candidate_ids": _serialize_hired_staff_candidate_ids(), + &"operations": _create_operations_runtime_snapshot(), + &"relationships": relationship_service.create_snapshot(), + &"event_definitions": _serialize_event_definitions(), + &"events": event_service.create_snapshot(), + &"authority": authority_service.create_snapshot(), + &"defense": defense_service.create_snapshot(), + &"construction": construction_service.create_snapshot(), + &"excavation": excavation_service.create_snapshot(), + &"spatial_subslots": spatial_subslot_service.create_snapshot(), + &"route_projection": route_subslot_path_adapter.create_snapshot(), + &"facility_infrastructure": facility_infrastructure_projector.create_snapshot(), + }, + } + if PurchaseQuoteV2.has_history(owned_systems[&"runtime"]): + owned_systems[&"runtime"][&"runtime_schema_version"] = MIMI_PURCHASE_RUNTIME_SNAPSHOT_VERSION + # Every component above returns detached containers. Transfer this fresh + # tree into the envelope instead of deep-copying all component snapshots a + # second time. Keep generic capture's caller-owned-extra contract unchanged. + var snapshot := SimulationSnapshot.capture(_clock, _state) + snapshot[&"systems"] = owned_systems + return snapshot + +func restore_snapshot(snapshot: Dictionary) -> bool: + return _restore_snapshot_internal(snapshot, false) + + +func _restore_snapshot_internal(snapshot: Dictionary, recovery_mode: bool) -> bool: + var active_filter_profile := event_service.get_content_filter_profile() + var staff_repair_report := { + &"automatic_repairs_applied": false, + &"reopened_task_ids": [], + &"released_staff_ids": [], + &"release_failed_task_ids": [], + } + var event_lock_repair_report := { + &"automatic_repairs_applied": false, + &"released_actor_ids": [], + &"released_foreground_instance_id": &"", + &"reason": &"NONE", + } + var validation := SimulationSnapshot.validate(snapshot) + if not validation[&"ok"]: + return false + var systems: Variant = snapshot.get(&"systems", {}) + if not systems is Dictionary or not systems.has(&"runtime") or not systems[&"runtime"] is Dictionary: + return false + var runtime_data: Dictionary = systems[&"runtime"] + var restored_minute := int(snapshot[&"clock"].get(&"absolute_minute", -1)) + if restored_minute < 0: + return false + var runtime_schema_version_value: Variant = runtime_data.get( + &"runtime_schema_version", + runtime_data.get("runtime_schema_version", LEGACY_RUNTIME_SNAPSHOT_VERSION) + ) + if typeof(runtime_schema_version_value) != TYPE_INT: + return false + var runtime_schema_version := int(runtime_schema_version_value) + if runtime_schema_version not in [ + LEGACY_RUNTIME_SNAPSHOT_VERSION, + DEFENSE_RUNTIME_SNAPSHOT_VERSION, + PATH_TRAFFIC_RUNTIME_SNAPSHOT_VERSION, + CONTENT_RUNTIME_SNAPSHOT_VERSION, + OPERATIONS_RUNTIME_SNAPSHOT_VERSION, + INFRASTRUCTURE_RUNTIME_SNAPSHOT_VERSION, + SUPPLY_RUNTIME_SNAPSHOT_VERSION, + TRANSPORT_RUNTIME_SNAPSHOT_VERSION, + STAIRS_RUNTIME_SNAPSHOT_VERSION, + UPKEEP_RUNTIME_SNAPSHOT_VERSION, + WEATHER_RUNTIME_SNAPSHOT_VERSION, + CLEANING_RUNTIME_SNAPSHOT_VERSION, + EVENT_THREAT_RUNTIME_SNAPSHOT_VERSION, + RUNTIME_SNAPSHOT_VERSION, + MIMI_PURCHASE_RUNTIME_SNAPSHOT_VERSION, + ]: + return false + var has_purchase_v2_history := PurchaseQuoteV2.has_history(runtime_data) + if (runtime_schema_version == MIMI_PURCHASE_RUNTIME_SNAPSHOT_VERSION) != has_purchase_v2_history: + return false + if runtime_schema_version < OPERATIONS_RUNTIME_SNAPSHOT_VERSION \ + and not _legacy_operations_history_is_absent(runtime_data): + return false + if runtime_schema_version < INFRASTRUCTURE_RUNTIME_SNAPSHOT_VERSION \ + and not _legacy_infrastructure_history_is_absent(runtime_data): + return false + if runtime_schema_version >= OPERATIONS_RUNTIME_SNAPSHOT_VERSION: + if not _runtime_v5_outer_snapshot_shape_is_valid(snapshot): + return false + var runtime_current_keys: Array[StringName] = [ + &"runtime_schema_version", &"world_seed", &"next_command_id", + &"staffing_projection_enabled", &"save_lifecycle", &"command_queue", + &"id_generator", &"domain_events", &"rng_streams", &"world", &"path", + &"movement_credits", &"mana", &"economy", &"inventory", + &"logistics_dispatcher", &"logistics_requests", &"facilities", &"guests", + &"staff", &"catalog_manifest", &"progression", &"hired_staff_candidate_ids", + &"operations", &"relationships", &"event_definitions", &"events", + &"authority", &"defense", &"construction", + ] + if runtime_schema_version >= INFRASTRUCTURE_RUNTIME_SNAPSHOT_VERSION: + runtime_current_keys.append_array([ + &"excavation", &"spatial_subslots", &"route_projection", + &"facility_infrastructure", + ]) + if runtime_schema_version >= SUPPLY_RUNTIME_SNAPSHOT_VERSION: + runtime_current_keys.append(&"kitchen_production") + if runtime_schema_version >= TRANSPORT_RUNTIME_SNAPSHOT_VERSION: + runtime_current_keys.append(&"elevators") + if runtime_schema_version >= STAIRS_RUNTIME_SNAPSHOT_VERSION: + runtime_current_keys.append(&"legacy_stairs") + if runtime_schema_version >= UPKEEP_RUNTIME_SNAPSHOT_VERSION: + runtime_current_keys.append_array([&"facility_upkeep", &"upkeep_settlement"]) + if runtime_schema_version >= WEATHER_RUNTIME_SNAPSHOT_VERSION: + runtime_current_keys.append(&"weather") + if runtime_schema_version >= CLEANING_RUNTIME_SNAPSHOT_VERSION: + runtime_current_keys.append(&"facility_cleanliness") + if runtime_schema_version >= AUTO_PURCHASE_RUNTIME_SNAPSHOT_VERSION: + runtime_current_keys.append(&"auto_purchase_plans") + if not _dictionary_has_exact_canonical_keys(runtime_data, runtime_current_keys) \ + or typeof(runtime_data.get(&"runtime_schema_version", null)) != TYPE_INT \ + or typeof(runtime_data.get(&"world_seed", null)) != TYPE_INT \ + or typeof(runtime_data.get(&"next_command_id", null)) != TYPE_INT \ + or typeof(runtime_data.get(&"staffing_projection_enabled", null)) != TYPE_BOOL: + return false + var has_defense_snapshot := runtime_data.has(&"defense") or runtime_data.has("defense") + if runtime_schema_version >= DEFENSE_RUNTIME_SNAPSHOT_VERSION and not has_defense_snapshot: + return false + if runtime_schema_version >= PATH_TRAFFIC_RUNTIME_SNAPSHOT_VERSION \ + and not runtime_data.get(&"movement_credits", null) is Array: + return false + if runtime_schema_version >= CONTENT_RUNTIME_SNAPSHOT_VERSION \ + and (not runtime_data.get(&"catalog_manifest", null) is Dictionary \ + or not runtime_data.get(&"progression", null) is Dictionary \ + or not runtime_data.get(&"hired_staff_candidate_ids", null) is Array): + return false + if runtime_schema_version >= OPERATIONS_RUNTIME_SNAPSHOT_VERSION \ + and not runtime_data.get(&"operations", null) is Dictionary: + return false + if runtime_schema_version >= INFRASTRUCTURE_RUNTIME_SNAPSHOT_VERSION: + for infrastructure_key: StringName in [ + &"excavation", &"spatial_subslots", &"route_projection", + &"facility_infrastructure", + ]: + if not runtime_data.get(infrastructure_key, null) is Dictionary: + return false + if runtime_schema_version < SUPPLY_RUNTIME_SNAPSHOT_VERSION and runtime_data.has(&"kitchen_production"): + return false + if runtime_schema_version >= SUPPLY_RUNTIME_SNAPSHOT_VERSION and not runtime_data.get(&"kitchen_production", null) is Dictionary: + return false + if runtime_schema_version < TRANSPORT_RUNTIME_SNAPSHOT_VERSION and runtime_data.has(&"elevators"): + return false + if runtime_schema_version >= TRANSPORT_RUNTIME_SNAPSHOT_VERSION and not runtime_data.get(&"elevators", null) is Dictionary: + return false + if runtime_schema_version < STAIRS_RUNTIME_SNAPSHOT_VERSION and runtime_data.has(&"legacy_stairs"): + return false + if runtime_schema_version >= STAIRS_RUNTIME_SNAPSHOT_VERSION and not runtime_data.get(&"legacy_stairs", null) is Dictionary: + return false + if runtime_schema_version < AUTO_PURCHASE_RUNTIME_SNAPSHOT_VERSION and runtime_data.has(&"auto_purchase_plans"): + return false + if runtime_schema_version >= AUTO_PURCHASE_RUNTIME_SNAPSHOT_VERSION and not runtime_data.get(&"auto_purchase_plans", null) is Dictionary: + return false + var restored_catalog := MvpContentCatalog.new() + var restored_catalog_report := restored_catalog.rebuild() + if not bool(restored_catalog_report.get(&"valid", false)): + return false + if runtime_schema_version >= CONTENT_RUNTIME_SNAPSHOT_VERSION \ + and not _saved_content_manifest_matches(restored_catalog, runtime_data): + return false + var restored_last_daily_checkpoint_day := -1 + var save_lifecycle_value: Variant = runtime_data.get(&"save_lifecycle", {}) + if not save_lifecycle_value is Dictionary: + return false + var save_lifecycle: Dictionary = save_lifecycle_value + if not save_lifecycle.is_empty(): + if (runtime_schema_version >= OPERATIONS_RUNTIME_SNAPSHOT_VERSION \ + and (not _dictionary_has_exact_canonical_keys( + save_lifecycle, [&"version", &"last_daily_checkpoint_day"] + ) \ + or typeof(save_lifecycle.get(&"version", null)) != TYPE_INT \ + or typeof(save_lifecycle.get(&"last_daily_checkpoint_day", null)) != TYPE_INT)) \ + or int(save_lifecycle.get(&"version", -1)) != SAVE_LIFECYCLE_VERSION: + return false + restored_last_daily_checkpoint_day = int( + save_lifecycle.get(&"last_daily_checkpoint_day", -1) + ) + if restored_last_daily_checkpoint_day < -1: + return false + if not runtime_data.get(&"command_queue", null) is Dictionary \ + or not runtime_data.get(&"id_generator", null) is Dictionary \ + or not runtime_data.get(&"domain_events", null) is Dictionary \ + or not runtime_data.get(&"rng_streams", null) is Dictionary \ + or not runtime_data.get(&"world", null) is Dictionary \ + or not runtime_data.get(&"path", null) is Dictionary \ + or not runtime_data.get(&"mana", null) is Dictionary \ + or not runtime_data.get(&"economy", null) is Dictionary \ + or not runtime_data.get(&"inventory", null) is Dictionary \ + or not runtime_data.get(&"logistics_dispatcher", null) is Dictionary \ + or not runtime_data.get(&"logistics_requests", null) is Array \ + or not runtime_data.get(&"facilities", null) is Dictionary \ + or not runtime_data.get(&"guests", null) is Dictionary \ + or not runtime_data.get(&"staff", null) is Dictionary \ + or not runtime_data.get(&"relationships", null) is Dictionary \ + or not runtime_data.get(&"event_definitions", null) is Array \ + or not runtime_data.get(&"events", null) is Dictionary \ + or not runtime_data.get(&"authority", null) is Dictionary \ + or (has_defense_snapshot and not runtime_data.get(&"defense", null) is Dictionary) \ + or not runtime_data.get(&"construction", null) is Dictionary: + return false + var restored_queue := CommandQueue.new() + if not restored_queue.restore_snapshot(runtime_data[&"command_queue"]): + return false + if runtime_schema_version < AUTO_PURCHASE_RUNTIME_SNAPSHOT_VERSION: + # Old saves must not reinterpret an unsupported queued action as a new + # paid feature when loading into this release. Failed historical actions + # remain ordinary recorded history; only resumable pending work is gated. + for row: Dictionary in restored_queue.create_snapshot()["pending"]: + if row["command_type"] == "set_supply_auto_purchase": + return false + var restored_id_generator := StableIdGenerator.new() + if not restored_id_generator.restore_snapshot(runtime_data[&"id_generator"]): + return false + var restored_domain_events := DomainEventStream.new() + if not restored_domain_events.restore_snapshot(runtime_data[&"domain_events"]): + return false + if runtime_schema_version >= OPERATIONS_RUNTIME_SNAPSHOT_VERSION \ + and not _runtime_command_history_crosslinks_are_valid( + runtime_data[&"command_queue"], + int(runtime_data[&"next_command_id"]), + restored_domain_events + ): + return false + var restored_world := WorldService.new() + if not restored_world.restore_snapshot(runtime_data[&"world"]): + return false + var restored_path := PathService.new() + var restored_spatial := SpatialSubslotService.new() + var restored_route_adapter := RouteSubslotPathAdapter.new() + var restored_infrastructure_projector := FacilityInfrastructureProjector.new() + if runtime_schema_version >= INFRASTRUCTURE_RUNTIME_SNAPSHOT_VERSION: + # Configure against an empty Spatial source before any managed namespace is + # present. Then restore the source, rebuild the base+ROUTE topology, and + # finally restore Path traffic/actors and the two metadata owners. + if not restored_path.rebuild_graph(restored_world.get_walkable_cells()) \ + or not restored_route_adapter.configure( + restored_spatial, restored_path, restored_world.get_walkable_cells() + ) \ + or not restored_infrastructure_projector.configure( + restored_route_adapter, restored_spatial, restored_path + ) \ + or not restored_spatial.restore_snapshot(runtime_data[&"spatial_subslots"]): + return false + var prepared_projection := restored_route_adapter.synchronize_projection( + &"runtime.v6.restore.prepare", + restored_world.get_walkable_cells() + ) + if not prepared_projection.is_success() \ + or not _path_snapshot_cells_match_world( + runtime_data[&"path"], + restored_route_adapter.get_projected_walkable_cells() + ) \ + or not restored_path.restore_snapshot(runtime_data[&"path"]) \ + or not restored_route_adapter.restore_snapshot(runtime_data[&"route_projection"]) \ + or not restored_infrastructure_projector.restore_snapshot( + runtime_data[&"facility_infrastructure"] + ): + return false + else: + if runtime_schema_version >= PATH_TRAFFIC_RUNTIME_SNAPSHOT_VERSION \ + and not _path_snapshot_cells_match_world( + runtime_data[&"path"], + restored_world.get_walkable_cells() + ): + return false + if not restored_path.restore_snapshot(runtime_data[&"path"]) \ + or not restored_path.rebuild_graph(restored_world.get_walkable_cells()) \ + or not restored_route_adapter.configure( + restored_spatial, restored_path, restored_world.get_walkable_cells() + ) \ + or not restored_infrastructure_projector.configure( + restored_route_adapter, restored_spatial, restored_path + ): + return false + if runtime_schema_version < STAIRS_RUNTIME_SNAPSHOT_VERSION: + var legacy_path_data: Dictionary = runtime_data[&"path"] + if legacy_path_data.has(&"vertical_walk_requires_connector") or legacy_path_data.has(&"transit_actor_ids"): + return false + var restored_legacy_stairs := LegacyStairService.new() + restored_legacy_stairs.configure(restored_path) + if runtime_schema_version >= STAIRS_RUNTIME_SNAPSHOT_VERSION: + if runtime_data[&"path"].get(&"schema_version", -1) != PathService.SNAPSHOT_VERSION \ + or not restored_path.get_vertical_walk_requires_connector() \ + or not restored_legacy_stairs.restore_snapshot(runtime_data[&"legacy_stairs"]): + return false + else: + if restored_path.get_vertical_walk_requires_connector() or not restored_legacy_stairs.migrate_existing_vertical_walks(): + return false + var restored_mana := ManaNetworkService.new() + if not restored_mana.restore_snapshot(runtime_data[&"mana"]): + return false + var restored_economy := EconomyService.new() + if not restored_economy.restore_snapshot(runtime_data[&"economy"]): + return false + if not _loan_principal_ledger_is_valid(restored_economy): + return false + if runtime_schema_version < OPERATIONS_RUNTIME_SNAPSHOT_VERSION: + if not restored_economy.has_account(WAGE_EXPENSE_ACCOUNT) \ + and not restored_economy.register_account(WAGE_EXPENSE_ACCOUNT): + return false + elif not restored_economy.has_account(WAGE_EXPENSE_ACCOUNT): + return false + var restored_upkeep := FacilityUpkeepAccrual.new() + var restored_upkeep_settlement := FacilityUpkeepSettlement.new() + if runtime_schema_version >= UPKEEP_RUNTIME_SNAPSHOT_VERSION: + if not runtime_data.get(&"facility_upkeep") is Dictionary or not runtime_data.get(&"upkeep_settlement") is Dictionary: + return false + if not restored_upkeep.restore_snapshot(runtime_data[&"facility_upkeep"]) \ + or restored_upkeep.get_current_minute() != restored_minute \ + or not restored_upkeep_settlement.restore_snapshot(runtime_data[&"upkeep_settlement"], restored_economy): + return false + if not _upkeep_accrual_crosslinks_are_valid(restored_upkeep, restored_upkeep_settlement): + return false + else: + if runtime_data.has(&"facility_upkeep") or runtime_data.has(&"upkeep_settlement") \ + or restored_economy.has_account(FacilityUpkeepSettlement.EXPENSE) \ + or restored_economy.has_account(FacilityUpkeepSettlement.PAYABLE): + return false + if not FacilityUpkeepSettlement.prepare_accounts(restored_economy) or not restored_upkeep.start_at(restored_minute)[&"ok"]: + return false + var saved_resources: Variant = snapshot[&"game_state"].get(&"resources", null) + if not saved_resources is Dictionary \ + or int(saved_resources.get(&"gold", saved_resources.get("gold", -1))) \ + != restored_economy.get_balance(PLAYER_CASH_ACCOUNT): + return false + var restored_inventory := InventoryService.new() + if not restored_inventory.restore_snapshot(runtime_data[&"inventory"]): + return false + var restored_dispatcher := LogisticsDispatcher.new() + if not restored_dispatcher.restore_snapshot(runtime_data[&"logistics_dispatcher"]): + return false + var restored_logistics: Variant = _deserialize_logistics_requests(runtime_data[&"logistics_requests"]) + if restored_logistics == null: + return false + if restored_dispatcher.restored_snapshot_version == LogisticsDispatcher.LEGACY_SNAPSHOT_VERSION: + if runtime_schema_version >= PATH_TRAFFIC_RUNTIME_SNAPSHOT_VERSION \ + or not restored_dispatcher.migrate_legacy_assignment_ownership( + _derive_legacy_logistics_ownership( + restored_dispatcher, + restored_logistics, + restored_minute + ) + ): + return false + if not _validate_logistics_dispatcher_crosslinks( + restored_dispatcher, + restored_logistics, + restored_minute + ): + return false + var restored_facilities := FacilityService.new() + if not restored_facilities.restore_snapshot(runtime_data[&"facilities"]): + return false + var restored_guests := GuestService.new() + if not restored_guests.restore_snapshot(runtime_data[&"guests"]): + return false + if runtime_schema_version >= CONTENT_RUNTIME_SNAPSHOT_VERSION: + if not _saved_catalog_guest_definitions_match( + runtime_data[&"guests"], + restored_catalog + ): + return false + elif not _register_or_validate_legacy_catalog_definitions( + restored_guests, + restored_catalog + ): + return false + var restored_staff := StaffService.new() + if recovery_mode: + restored_staff.configure_callbacks( + Callable(), + func( + reservation_ids: Array[StringName], + task: StaffTask, + absolute_minute: int + ) -> Dictionary: + return _release_recovery_task_items( + restored_inventory, + reservation_ids, + task, + absolute_minute + ), + Callable() + ) + var restored_staff_ok := ( + restored_staff.restore_snapshot_with_orphan_recovery(runtime_data[&"staff"]) + if recovery_mode + else restored_staff.restore_snapshot(runtime_data[&"staff"]) + ) + if not restored_staff_ok: + return false + if recovery_mode: + staff_repair_report = restored_staff.last_recovery_repair_report.duplicate(true) + if not staff_repair_report[&"release_failed_task_ids"].is_empty(): + return false + var saved_rank_value: Variant = snapshot[&"game_state"].get(&"world_tree_rank", null) + if not saved_rank_value is int: + return false + var saved_rank := int(saved_rank_value) + var restored_progression := ProgressionService.new() + if runtime_schema_version >= CONTENT_RUNTIME_SNAPSHOT_VERSION: + if not restored_progression.restore_snapshot(runtime_data[&"progression"]): + return false + else: + # Pre-v4 saves never recorded promotion evidence. Rank 1 can be migrated + # from authoritative live facts; a later rank would require guessing which + # requirements and quests were actually satisfied, so it is rejected. + if saved_rank != ProgressionService.MIN_RANK: + return false + var legacy_guest_lifetime := restored_guests.guest_count() \ + + int((runtime_data[&"guests"] as Dictionary)[&"persistent_records"].size()) + if not restored_progression.configure(ProgressionService.MIN_RANK, { + &"active_above_ground_facilities": _count_active_facilities(restored_facilities, false), + &"active_underground_facilities": _count_active_facilities(restored_facilities, true), + &"asset_gold": restored_economy.get_balance(PLAYER_CASH_ACCOUNT), + &"faith_lifetime": maxi(int(saved_resources.get(&"faith", saved_resources.get("faith", 0))), 0), + &"guest_lifetime": legacy_guest_lifetime, + }): + return false + if saved_rank != restored_progression.current_rank \ + or not _strict_variants_equal( + restored_progression.get_rank_bounds(saved_rank), + restored_world.get_bounds() + ) \ + or not _progression_guest_lifetime_matches( + restored_progression, restored_guests + ): + return false + var restored_hired_staff_candidates: Variant + if runtime_schema_version >= CONTENT_RUNTIME_SNAPSHOT_VERSION: + restored_hired_staff_candidates = _deserialize_hired_staff_candidate_ids( + runtime_data[&"hired_staff_candidate_ids"], + restored_catalog + ) + if restored_hired_staff_candidates == null: + return false + else: + restored_hired_staff_candidates = _derive_legacy_hired_staff_candidate_ids( + restored_staff, + runtime_data[&"staff"], + restored_catalog + ) + if not _validate_catalog_staff_crosslinks( + restored_staff, + restored_hired_staff_candidates, + restored_catalog + ): + return false + var restored_operations_state := _parse_operations_runtime_snapshot( + runtime_data.get(&"operations", {}), + runtime_schema_version, + restored_minute, + restored_guests, + restored_staff, + restored_economy, + int(runtime_data.get(&"world_seed", DEFAULT_WORLD_SEED)), + snapshot[&"clock"].get(&"paused", false), + restored_upkeep_settlement + ) + if not bool(restored_operations_state.get(&"ok", false)): + return false + if bool(restored_operations_state.get(&"enabled", false)): + var restored_coordinator: OperationsCoordinator = restored_operations_state[&"coordinator"] + var expected_minimum := 0 + var expected_maximum := 0 + for period: Dictionary in restored_upkeep.get_report()[&"closed_periods"]: + if int(period[&"period_end"]) <= restored_coordinator.current_minute: + expected_minimum += 1 + if int(period[&"period_end"]) <= restored_minute: + expected_maximum += 1 + var actual_count := restored_upkeep_settlement.get_settled_period_count() + if actual_count < expected_minimum or actual_count > expected_maximum: + return false + var restored_movement_credits: Variant + if runtime_schema_version >= PATH_TRAFFIC_RUNTIME_SNAPSHOT_VERSION: + restored_movement_credits = _deserialize_movement_credits( + runtime_data[&"movement_credits"], + restored_minute + ) + if restored_movement_credits == null: + return false + else: + restored_movement_credits = _rebuild_legacy_path_actor_projection( + restored_path, + restored_guests, + restored_staff, + restored_minute + ) + if restored_movement_credits == null: + return false + if not _validate_runtime_path_crosslinks( + restored_path, + restored_guests, + restored_staff, + restored_movement_credits, + restored_minute + ): + return false + var restored_relationships := RelationshipService.new() + if not restored_relationships.restore_snapshot(runtime_data[&"relationships"]): + return false + var restored_events := EventService.new() + for raw_definition: Variant in runtime_data[&"event_definitions"]: + var definition := EventDefinition.from_variant(raw_definition) + if definition == null or not restored_events.register_definition(definition).is_success(): + return false + if not bool(restored_events.validate_definitions().get(&"ok", false)): + return false + var restored_events_ok := ( + restored_events.restore_snapshot_with_lock_recovery(runtime_data[&"events"]) + if recovery_mode + else restored_events.restore_snapshot(runtime_data[&"events"]) + ) + if not restored_events_ok: + return false + if recovery_mode: + event_lock_repair_report = restored_events.last_recovery_repair_report.duplicate(true) + var saved_filter_profile := restored_events.get_content_filter_profile() + var effective_filter_profile := _merge_content_filter_profiles( + active_filter_profile, + saved_filter_profile + ) + restored_events.configure_content_filter( + effective_filter_profile[&"filter_mode"], + effective_filter_profile[&"maximum_content_rating"], + effective_filter_profile[&"disabled_content_tags"] + ) + var restored_authority := AuthorityService.new() + if not restored_authority.restore_snapshot(runtime_data[&"authority"]): + return false + var restored_defense := DefenseService.new() + if has_defense_snapshot: + if not restored_defense.restore_snapshot(runtime_data[&"defense"]): + return false + else: + # Runtime snapshots predating the Defense aggregate migrate to an empty + # aggregate at the saved clock. No external entity or consequence is + # synthesized by this compatibility path. + if not restored_defense.advance_time(restored_minute).is_success(): + return false + var restored_staffing_projection := bool( + runtime_data.get(&"staffing_projection_enabled", false) + ) + var restored_runtime_domain_minute := restored_minute + if bool(restored_operations_state.get(&"enabled", false)): + var restored_operations_coordinator: OperationsCoordinator = ( + restored_operations_state.get(&"coordinator", null) + ) + if int(restored_operations_state.get(&"deferred_runtime_minute", -1)) \ + == restored_minute \ + or (restored_operations_coordinator != null \ + and restored_operations_coordinator.is_terminal()): + restored_runtime_domain_minute = restored_minute - 1 + if not _validate_runtime_crosslinks( + restored_facilities, + restored_guests, + restored_economy, + restored_inventory, + restored_staff, + restored_runtime_domain_minute, + restored_staffing_projection + ): + return false + if restored_authority.current_minute < restored_runtime_domain_minute \ + or restored_authority.current_minute > restored_minute \ + or restored_defense.current_minute < restored_runtime_domain_minute \ + or restored_defense.current_minute > restored_minute: + return false + if not _validate_authority_economy_crosslinks( + restored_authority, + restored_economy, + restored_authority.current_minute + ): + return false + if not _validate_defense_crosslinks( + restored_defense, + restored_staff, + restored_facilities, + restored_defense.current_minute + ): + return false + if not _validate_event_defense_crosslinks(restored_events, restored_defense): + return false + var restored_construction_resource := _create_resource_adapter( + restored_economy, + CONSTRUCTION_EXPENSE_ACCOUNT, + &"CONSTRUCTION", + &"CONSTRUCTION_REFUND", + &"construction_service" + ) + var restored_construction := ConstructionService.new(restored_world, restored_construction_resource) + if not restored_construction.restore_snapshot(runtime_data[&"construction"]): + return false + var restored_excavation := ExcavationService.new( + restored_world, restored_construction_resource + ) + if runtime_schema_version >= INFRASTRUCTURE_RUNTIME_SNAPSHOT_VERSION: + if not restored_excavation.restore_snapshot(runtime_data[&"excavation"]): + return false + else: + restored_excavation.current_minute = restored_construction.current_minute + if not _migrate_legacy_infrastructure_proxies( + restored_construction, + restored_facilities, + restored_infrastructure_projector + ) or not restored_progression.update_metrics({ + &"active_above_ground_facilities": _count_active_facilities( + restored_facilities, false + ), + &"active_underground_facilities": _count_active_facilities( + restored_facilities, true + ), + &"asset_gold": restored_economy.get_balance(PLAYER_CASH_ACCOUNT), + }) or not _validate_runtime_crosslinks( + restored_facilities, + restored_guests, + restored_economy, + restored_inventory, + restored_staff, + restored_runtime_domain_minute, + restored_staffing_projection + ): + return false + if not _validate_construction_facility_crosslinks( + restored_world, + restored_construction, + restored_facilities, + restored_infrastructure_projector + ) or not _validate_infrastructure_crosslinks( + restored_world, + restored_path, + restored_construction, + restored_construction_resource, + restored_excavation, + restored_spatial, + restored_route_adapter, + restored_infrastructure_projector + ): + return false + # Older saves used logical attendance. Migrate their staff capacity once; + # new saves must already agree with physical workplace positions. + for facility: FacilityInstance in restored_facilities.get_instances(): + var present_ids: Array[StringName] = [] + for staff_id: StringName in facility.active_staff_ids: + var worker := restored_staff.get_staff(staff_id) + if not StringName(restored_staff.get_work_assignment(staff_id).get(&"facility_id", &"")).is_empty() and not StaffCommuteService.is_at_facility(worker, facility) and not CleaningBridge.is_deployed(worker, facility, restored_staff): + if runtime_schema_version >= TRANSPORT_RUNTIME_SNAPSHOT_VERSION: + return false + if worker.role_id != &"porter" and worker.current_task_id.is_empty(): + worker.state = StaffRuntime.State.COMMUTING + else: + present_ids.append(staff_id) + if present_ids != facility.active_staff_ids: + restored_facilities.set_operational_status(facility.instance_id, + {&"active_staff_ids": present_ids}, restored_runtime_domain_minute) + var restored_elevators := ElevatorService.new() + restored_elevators.configure(restored_path) + if runtime_schema_version >= TRANSPORT_RUNTIME_SNAPSHOT_VERSION: + if not restored_elevators.restore_snapshot(runtime_data[&"elevators"]): + return false + else: + if not bool(restored_elevators.synchronize([], restored_runtime_domain_minute).get(&"ok", false)): + return false + if not _validate_elevator_crosslinks(restored_elevators, restored_path, restored_construction, + restored_guests, restored_staff, restored_runtime_domain_minute): + return false + for facility: FacilityInstance in restored_facilities.get_instances(): + for staff_id: StringName in facility.active_staff_ids: + if not restored_elevators.get_actor_trip(_staff_path_actor_id(staff_id)).is_empty() and not CleaningBridge.is_deployed(restored_staff.get_staff(staff_id), facility, restored_staff): + return false + var restored_kitchen := KitchenProductionService.new() + restored_kitchen.configure(restored_inventory, restored_facilities, definition_registry, restored_runtime_domain_minute) + if runtime_schema_version >= SUPPLY_RUNTIME_SNAPSHOT_VERSION: + if not restored_kitchen.restore_snapshot(runtime_data[&"kitchen_production"]): + return false + if restored_kitchen.current_minute != restored_runtime_domain_minute or not restored_kitchen.validate_crosslinks(): + return false + if runtime_schema_version == MIMI_PURCHASE_RUNTIME_SNAPSHOT_VERSION \ + and not PurchaseQuoteV2.valid_history(runtime_data, restored_economy): + return false + var restored_auto_purchase := AutoPurchasePlansType.new() + if runtime_schema_version >= AUTO_PURCHASE_RUNTIME_SNAPSHOT_VERSION: + if not restored_auto_purchase.restore_snapshot(runtime_data[&"auto_purchase_plans"], restored_facilities, restored_economy, restored_runtime_domain_minute): + return false + if not SupplyLogisticsBridge.validate_crosslinks(restored_logistics, restored_staff, restored_inventory, restored_facilities): + return false + var restored_weather := WeatherTimeline.new() + if runtime_schema_version >= WEATHER_RUNTIME_SNAPSHOT_VERSION: + if not runtime_data.get(&"weather") is Dictionary or not restored_weather.restore_snapshot(runtime_data[&"weather"]) or restored_weather.get_minute() != restored_minute: + return false + elif runtime_data.has(&"weather") or not restored_weather.initialize(&"clear", restored_minute): + return false + var restored_cleanliness := FacilityCleanliness.new() + if runtime_schema_version >= CLEANING_RUNTIME_SNAPSHOT_VERSION: + if not runtime_data.get(&"facility_cleanliness") is Dictionary or not restored_cleanliness.restore_snapshot(runtime_data[&"facility_cleanliness"]): + return false + else: + if runtime_data.has(&"facility_cleanliness"): + return false + var policy_values := cleaning_policy.create_snapshot() + for facility: FacilityInstance in restored_facilities.get_instances(): + if not restored_cleanliness.register_facility(facility.instance_id, policy_values["initial_cleanliness"], policy_values["maximum_cleanliness"]): + return false + if not _validate_cleanliness_crosslinks(restored_cleanliness, restored_facilities, restored_staff, restored_path): + return false + var restored_world_seed := int(runtime_data.get(&"world_seed", DEFAULT_WORLD_SEED)) + var restored_streams := {} + var saved_streams: Dictionary = runtime_data[&"rng_streams"] + for stream_key: Variant in saved_streams: + if not saved_streams[stream_key] is Dictionary: + return false + var normalized_stream_id := StringName(stream_key) + if normalized_stream_id.is_empty() or restored_streams.has(normalized_stream_id): + return false + var stream := DeterministicRng.from_root_seed(0, normalized_stream_id) as DeterministicRng + if stream == null or not stream.restore(saved_streams[stream_key]) \ + or stream.get_stream_id() != normalized_stream_id \ + or stream.get_root_seed() != restored_world_seed: + return false + restored_streams[normalized_stream_id] = stream + if not restored_streams.has(&"defense"): + if has_defense_snapshot: + return false + restored_streams[&"defense"] = DeterministicRng.from_root_seed(restored_world_seed, &"defense") + if runtime_schema_version >= DEFENSE_RUNTIME_SNAPSHOT_VERSION: + for required_stream_id: StringName in RNG_STREAM_IDS: + if not restored_streams.has(required_stream_id): + return false + var restored_next_id := int(runtime_data.get(&"next_command_id", 0)) + if restored_next_id <= 0: + return false + if not SimulationSnapshot.restore(snapshot, _clock, _state): + return false + world_seed = restored_world_seed + _next_command_id = restored_next_id + _staffing_projection_enabled = restored_staffing_projection + command_queue = restored_queue + id_generator = restored_id_generator + domain_events = restored_domain_events + _rng_streams = restored_streams + world_service = restored_world + path_service = restored_path + _movement_credits = (restored_movement_credits as Dictionary).duplicate(true) + _connect_path_performance_signal() + mana_network_service = restored_mana + weather = restored_weather + facility_cleanliness = restored_cleanliness + facility_upkeep = restored_upkeep + upkeep_settlement = restored_upkeep_settlement + economy_service = restored_economy + inventory_service = restored_inventory + kitchen_production = restored_kitchen + auto_purchase_plans = restored_auto_purchase + logistics_dispatcher = restored_dispatcher + logistics_requests = restored_logistics + facility_service = restored_facilities + guest_service = restored_guests + staff_service = restored_staff + elevators = restored_elevators + legacy_stairs = restored_legacy_stairs + staff_commute.configure(self) + supply_logistics.configure(self) + cleaning_bridge.configure(self) + mvp_content_catalog = restored_catalog + _clear_mvp_content_manifest_cache() + mvp_content_report = restored_catalog_report.duplicate(true) + progression_service = restored_progression + _hired_staff_candidate_ids = (restored_hired_staff_candidates as Dictionary).duplicate(true) + operations_coordinator = restored_operations_state.get(&"coordinator", null) + _operations_enabled = bool(restored_operations_state[&"enabled"]) + _personal_action_points = int(restored_operations_state[&"personal_action_points"]) + _operations_runtime_failure_latched = bool(restored_operations_state[&"runtime_failure_latched"]) + _operations_runtime_failure_report = ( + restored_operations_state[&"runtime_failure_report"] as Dictionary + ).duplicate(true) + _operations_tick_blocked = _operations_should_block_tick() + _operations_deferred_runtime_minute = int( + restored_operations_state[&"deferred_runtime_minute"] + ) + _operations_resume_publish_pending = false + _operations_test_boundary_hooks.clear() + _operations_transaction_active = false + _operations_queued_domain_events.clear() + _operations_queued_guest_spawn_notifications.clear() + _operations_flushing_guest_spawn_notifications.clear() + _operations_queued_economy_notifications.clear() + _operations_flushing_economy_notifications.clear() + relationship_service = restored_relationships + event_service = restored_events + authority_service = restored_authority + _detach_defense_service(defense_service) + defense_service = restored_defense + _reset_defense_round_attempts() + construction_resource_state = restored_construction_resource + operating_resource_state = _create_resource_adapter( + economy_service, + OPERATING_EXPENSE_ACCOUNT, + &"OPERATING_EXPENSE", + &"OPERATING_REFUND", + &"simulation_runtime" + ) + construction_service = restored_construction + excavation_service = restored_excavation + spatial_subslot_service = restored_spatial + route_subslot_path_adapter = restored_route_adapter + facility_infrastructure_projector = restored_infrastructure_projector + _infrastructure_failure_latched = false + _infrastructure_failure_report.clear() + _construction_completion_transaction_active = false + _construction_committing_notifications = false + _construction_queued_domain_events.clear() + _last_daily_checkpoint_day = restored_last_daily_checkpoint_day + _pending_checkpoints.clear() + _next_checkpoint_request_id = 1 + _connect_economy_signals() + _configure_facility_boundaries() + _connect_facility_signals() + _configure_staff_boundaries() + _connect_guest_signals() + _connect_staff_signals() + _configure_relationship_event_boundaries() + _connect_relationship_signals() + _connect_event_signals() + _configure_authority_boundaries() + _connect_authority_signals() + _configure_defense_boundaries() + _connect_defense_signals() + _connect_construction_signals() + var excavation_callback := Callable(self, "_on_excavation_completed") + if not excavation_service.excavation_completed.is_connected(excavation_callback): + excavation_service.excavation_completed.connect(excavation_callback) + if _operations_enabled: + _configure_operations_callbacks() + _register_operations_tick_handler() + else: + _clock.unregister_tick_handler( + _clock.TickPhase.TIME_BOUNDARIES, + &"simulation_runtime.operations" + ) + latest_restore_safety_report = { + &"report_version": 1, + &"recovery_mode": bool(recovery_mode), + &"repair_policy": &"WHITELISTED_AUTHORITATIVE_ONLY", + &"automatic_repairs_applied": ( + bool(staff_repair_report[&"automatic_repairs_applied"]) + or bool(event_lock_repair_report[&"automatic_repairs_applied"]) + ), + &"repairs": { + &"orphan_tasks": staff_repair_report, + &"orphan_event_locks": event_lock_repair_report, + }, + &"content_filter": _content_filter_restore_report( + active_filter_profile, + saved_filter_profile, + effective_filter_profile + ), + &"forbidden_repairs_applied": false, + &"forbidden_repair_categories": [ + &"synthesize_gold", + &"delete_facilities", + &"rewrite_ledger", + &"guess_route_locks", + ], + } + _publish_snapshot(_clock.absolute_minute) + return true + +func configure_save_policy(overrides: Dictionary) -> Dictionary: + var configured := _save_policy.duplicate(true) + if overrides.has(&"enabled"): + configured[&"enabled"] = bool(overrides[&"enabled"]) + if overrides.has(&"daily_checkpoint_enabled"): + configured[&"daily_checkpoint_enabled"] = bool(overrides[&"daily_checkpoint_enabled"]) + if overrides.has(&"auto_migrate"): + configured[&"auto_migrate"] = bool(overrides[&"auto_migrate"]) + if overrides.has(&"autosave_path"): + var autosave_path := str(overrides[&"autosave_path"]) + if not _save_policy_path_is_safe(autosave_path): + return { + &"ok": false, + &"code": &"INVALID_AUTOSAVE_PATH", + &"message": autosave_path, + } + configured[&"autosave_path"] = autosave_path + if overrides.has(&"autosave_generations"): + var generations := int(overrides[&"autosave_generations"]) + if generations < 1 or generations > SaveService.MAX_AUTOSAVE_GENERATIONS: + return { + &"ok": false, + &"code": &"INVALID_AUTOSAVE_GENERATIONS", + &"message": str(generations), + } + configured[&"autosave_generations"] = generations + _save_policy = configured + return { + &"ok": true, + &"code": &"OK", + &"policy": get_save_policy(), + } + + +func enable_production_save_policy() -> Dictionary: + return configure_save_policy({ + &"enabled": true, + &"daily_checkpoint_enabled": true, + &"autosave_path": DEFAULT_AUTOSAVE_PATH, + }) + + +func get_save_policy() -> Dictionary: + return _save_policy.duplicate(true) + + +func get_last_daily_checkpoint_day() -> int: + return _last_daily_checkpoint_day + + +func get_pending_checkpoint_count() -> int: + return _pending_checkpoints.size() + + +func get_latest_restore_safety_report() -> Dictionary: + return latest_restore_safety_report.duplicate(true) + + +func enable_production_operations() -> Dictionary: + if not _initialized or _clock == null or _state == null: + return _operations_api_failure(&"RUNTIME_NOT_INITIALIZED", "Runtime is not initialized.") + if _operations_enabled: + return _operations_api_failure(&"ALREADY_ENABLED", "Production operations are already enabled.") + if _clock.absolute_minute != OPERATIONS_START_MINUTE \ + or _clock.day != 1 or _clock.minute_of_day != OPERATIONS_START_MINUTE: + return _operations_api_failure( + &"INVALID_START_TIME", + "Production operations must start at Day 1 06:00." + ) + if not bool(mvp_content_report.get(&"valid", false)): + return _operations_api_failure(&"CONTENT_INVALID", "MVP content is unavailable.") + if _staff_payroll_arrears_total(staff_service) != 0: + return _operations_api_failure( + &"INITIAL_ARREARS_UNSUPPORTED", + "Production operations cannot infer pre-existing payroll arrears." + ) + if not (staff_service.create_snapshot().get(&"payroll_settlements", []) as Array).is_empty(): + return _operations_api_failure( + &"INITIAL_PAYROLL_HISTORY_UNSUPPORTED", + "Production operations require a fresh Day 1 payroll history." + ) + if _guest_aggregate_has_reserved_operations_id(guest_service): + return _operations_api_failure( + &"RESERVED_GUEST_ID_PRESENT", + "Production operations require the guest-auto namespace to be unused." + ) + var coordinator := OperationsCoordinator.new({ + &"start_minute": OPERATIONS_COORDINATOR_ORIGIN_MINUTE, + &"world_seed": world_seed, + }) + coordinator.configure_callbacks( + Callable(self, "_operations_daily_boundary"), + Callable(self, "_operations_arrival_boundary"), + Callable(self, "_operations_morning_boundary"), + Callable(self, "_operations_midnight_upkeep_boundary") + ) + var demand_configuration := _configure_default_operations_demand(coordinator) + if not demand_configuration.is_success(): + return _operations_api_failure( + &"DEMAND_CONFIGURATION_FAILED", + demand_configuration.message, + demand_configuration.details + ) + var population := coordinator.set_population_count(guest_service.guest_count()) + if not population.is_success(): + return _operations_api_failure( + &"INITIAL_POPULATION_INVALID", + population.message, + population.details + ) + var previous_coordinator := operations_coordinator + var previous_ap := _personal_action_points + var enable_external_before := _capture_operations_external_state() + if enable_external_before.is_empty(): + return _operations_api_failure( + &"INITIAL_CAPTURE_FAILED", + "Production Operations could not capture the initial Runtime state." + ) + operations_coordinator = coordinator + _personal_action_points = 0 + var advanced := coordinator.advance_to(OPERATIONS_START_MINUTE, true) + if not advanced.is_success() \ + or coordinator.current_minute != _clock.absolute_minute \ + or coordinator.get_population_count() != guest_service.guest_count() \ + or coordinator.get_arrears_total() != _operations_closed_arrears_total() \ + or not _operations_notifications_are_committable() \ + or not _commit_operations_domain_events() \ + or not _commit_operations_notifications(): + coordinator.discard_deferred_notifications() + _restore_operations_external_state(enable_external_before) + _discard_operations_domain_events() + operations_coordinator = previous_coordinator + _personal_action_points = previous_ap + return _operations_api_failure( + &"INITIAL_BOUNDARY_FAILED", + advanced.message if not advanced.is_success() else "Initial Operations cross-links failed.", + advanced.details + ) + _operations_enabled = true + _operations_tick_blocked = false + _operations_runtime_failure_latched = false + _operations_runtime_failure_report.clear() + _register_operations_tick_handler() + _pause_for_waiting_foreground_event() + _publish_snapshot(_clock.absolute_minute) + return { + &"ok": true, + &"code": &"OK", + &"enabled": true, + &"current_minute": coordinator.current_minute, + &"personal_action_points": _personal_action_points, + } + + +func acknowledge_operations_pause() -> Dictionary: + if not _operations_enabled or operations_coordinator == null: + return _operations_api_failure(&"OPERATIONS_DISABLED", "Production operations are disabled.") + if operations_coordinator.is_terminal(): + return _operations_api_failure(&"TERMINAL_STATE", "Bankruptcy cannot be acknowledged away.") + if operations_coordinator.is_external_quarantined(): + return _operations_api_failure( + &"EXTERNAL_ROLLBACK_FAILED", + "A verified snapshot restore is required." + ) + if _operations_deferred_runtime_minute < 0: + return _operations_api_failure( + &"NO_DEFERRED_RUNTIME_MINUTE", + "Operations have no deferred Runtime phase suffix." + ) + if _operations_deferred_runtime_minute != _clock.absolute_minute: + return _operations_api_failure( + &"DEFERRED_MINUTE_MISMATCH", + "The deferred Operations minute no longer matches GameClock." + ) + var had_runtime_failure := _operations_runtime_failure_latched + var acknowledged := operations_coordinator.acknowledge_pause() + if not acknowledged.is_success(): + return _operations_api_failure(&"ACKNOWLEDGE_FAILED", acknowledged.message, acknowledged.details) + var deferred_minute := _operations_deferred_runtime_minute + var deferred := operations_coordinator.advance_to(deferred_minute, true) + if not deferred.is_success(): + var partial_commit_ok := _commit_operations_domain_events() \ + and _commit_operations_notifications() + if not partial_commit_ok: + operations_coordinator.discard_deferred_notifications() + _discard_operations_domain_events() + _operations_runtime_failure_latched = true + _operations_runtime_failure_report = deferred.to_dictionary() + _operations_tick_blocked = true + _clock.set_paused(true) + return _operations_api_failure(&"RETRY_FAILED", deferred.message, deferred.details) + if operations_coordinator.current_minute != deferred_minute \ + or operations_coordinator.get_population_count() != guest_service.guest_count() \ + or operations_coordinator.get_arrears_total() != _operations_closed_arrears_total() \ + or not _operations_notifications_are_committable() \ + or not _commit_operations_domain_events() \ + or not _commit_operations_notifications(): + operations_coordinator.discard_deferred_notifications() + _discard_operations_domain_events() + _operations_runtime_failure_latched = true + _operations_runtime_failure_report = { + &"code": &"OPERATIONS_CROSSLINK_MISMATCH", + &"minute": deferred_minute, + } + _operations_tick_blocked = true + _clock.set_paused(true) + return _operations_api_failure( + &"RETRY_CROSSLINK_FAILED", + "Operations retry did not restore Runtime cross-links." + ) + _operations_runtime_failure_latched = false + _operations_runtime_failure_report.clear() + if operations_coordinator.is_terminal(): + _operations_deferred_runtime_minute = -1 + _operations_tick_blocked = true + _clock.set_paused(true) + _publish_snapshot(deferred_minute) + return { + &"ok": true, + &"code": &"TERMINAL_STATE", + &"had_runtime_failure": had_runtime_failure, + &"suffix_replayed": false, + } + if operations_coordinator.pause_requested: + _operations_tick_blocked = true + _clock.set_paused(true) + return _operations_api_failure( + &"PAUSE_REMAINS", + "Operations still require acknowledgement before Runtime phases can resume." + ) + _operations_deferred_runtime_minute = -1 + _operations_tick_blocked = false + _clock.set_paused(false) + _run_operations_deferred_suffix(deferred_minute) + _pause_for_waiting_foreground_event() + if operations_coordinator.get_population_count() != guest_service.guest_count() \ + or operations_coordinator.get_arrears_total() != _operations_closed_arrears_total(): + _operations_runtime_failure_latched = true + _operations_runtime_failure_report = { + &"code": &"DEFERRED_SUFFIX_CROSSLINK_MISMATCH", + &"minute": deferred_minute, + } + _operations_tick_blocked = true + _clock.set_paused(true) + return _operations_api_failure( + &"SUFFIX_CROSSLINK_FAILED", + "Deferred Runtime phases ended with invalid Operations cross-links." + ) + return { + &"ok": true, + &"code": &"OK", + &"had_runtime_failure": had_runtime_failure, + &"coordinator": acknowledged.to_dictionary(), + &"suffix_replayed": true, + &"deferred_runtime_minute": deferred_minute, + } + + +func _run_operations_deferred_suffix(absolute_minute: int) -> void: + _advance_mana(absolute_minute) + _advance_construction(absolute_minute) + _advance_inventory_and_logistics(absolute_minute) + _advance_facilities(absolute_minute) + _advance_guest_vitals(absolute_minute) + _advance_authority_incidents(absolute_minute) + _advance_defense(absolute_minute) + _advance_relationship_events(absolute_minute) + _advance_guest_queues(absolute_minute) + _advance_staff_ai(absolute_minute) + if _active_command_minute >= 0: + _operations_resume_publish_pending = true + else: + _publish_snapshot(absolute_minute) + _process_save_checkpoints(absolute_minute) + + +func get_operations_dashboard_report() -> Dictionary: + return {&"enabled":_operations_enabled, &"arrears_total":_operations_total_arrears(), + &"tick_blocked":_operations_tick_blocked, &"runtime_failure_latched":_operations_runtime_failure_latched, + &"personal_action_points":_personal_action_points, + &"deferred_runtime_minute":_operations_deferred_runtime_minute, + &"coordinator":operations_coordinator.get_dashboard_summary() if operations_coordinator != null else {}} + + +func get_operations_report() -> Dictionary: + return { + &"enabled": _operations_enabled, + &"arrears_total": _operations_total_arrears(), + &"tick_blocked": _operations_tick_blocked, + &"runtime_failure_latched": _operations_runtime_failure_latched, + &"runtime_failure_report": _operations_runtime_failure_report.duplicate(true), + &"deferred_runtime_minute": _operations_deferred_runtime_minute, + &"personal_action_points": _personal_action_points, + &"coordinator": ( + operations_coordinator.create_snapshot() + if operations_coordinator != null else {} + ), + } + + +func set_operations_test_boundary_hook(operation: StringName, callback: Callable) -> bool: + # Test-only fault injection is invoked at PREPARE and, for arrivals, once at + # AFTER_FIRST_MUTATION. The latter proves staged service notifications are + # discarded when a real partial apply is rolled back. + if operation not in [&"DAILY_CLOSE", &"ARRIVAL_PLAN", &"MORNING_BOUNDARY", &"MIDNIGHT_UPKEEP"]: + return false + if callback.is_valid(): + _operations_test_boundary_hooks[operation] = callback + else: + _operations_test_boundary_hooks.erase(operation) + return true + + +func _run_operations_test_boundary_hook( + operation: StringName, + payload: Dictionary, + phase: StringName, + token: Variant +) -> Dictionary: + var callback: Callable = _operations_test_boundary_hooks.get(operation, Callable()) + if not callback.is_valid(): + return {&"ok": true} + var returned: Variant = callback.call(payload.duplicate(true), phase, token) + if returned is bool: + return {&"ok": bool(returned), &"reason": &"TEST_HOOK_REJECTED"} + if returned is Dictionary: + var result: Dictionary = returned + if typeof(result.get(&"ok", null)) == TYPE_BOOL: + return result.duplicate(true) + return {&"ok": false, &"reason": &"TEST_HOOK_INVALID_RESULT"} + + +func _register_operations_tick_handler() -> void: + _clock.unregister_tick_handler( + _clock.TickPhase.TIME_BOUNDARIES, + &"simulation_runtime.operations" + ) + _clock.register_tick_handler( + _clock.TickPhase.TIME_BOUNDARIES, + &"simulation_runtime.operations", + Callable(self, "_advance_operations_time") + ) + + +func _configure_operations_callbacks() -> void: + if operations_coordinator == null: + return + operations_coordinator.configure_callbacks( + Callable(self, "_operations_daily_boundary"), + Callable(self, "_operations_arrival_boundary"), + Callable(self, "_operations_morning_boundary"), + Callable(self, "_operations_midnight_upkeep_boundary") + ) + + +func _configure_default_operations_demand(coordinator: OperationsCoordinator) -> CommandResult: + if coordinator == null or DEFAULT_OPERATIONS_DEMAND_PROFILES.size() != 8: + return CommandResult.failed( + CommandResult.Code.VALIDATION_FAILED, + 0, + "The canonical Operations demand profile set is unavailable." + ) + var allowed_classes: Dictionary = {} + for class_id: StringName in mvp_content_catalog.get_guest_class_ids(): + allowed_classes[class_id] = true + var seen_classes: Dictionary = {} + for raw_profile: Variant in DEFAULT_OPERATIONS_DEMAND_PROFILES: + if not raw_profile is Dictionary: + return CommandResult.failed( + CommandResult.Code.VALIDATION_FAILED, 0, + "A canonical Operations demand profile is malformed." + ) + var profile: Dictionary = raw_profile + var guest_class := StringName(profile[&"guest_class"]) + if not allowed_classes.has(guest_class) or seen_classes.has(guest_class): + return CommandResult.failed( + CommandResult.Code.VALIDATION_FAILED, 0, + "The canonical Operations demand classes are invalid." + ) + seen_classes[guest_class] = true + var configured := coordinator.set_demand_input( + StringName(profile[&"facility_tag"]), + guest_class, + StringName(profile[&"time_band"]), + { + &"base": 100, + &"reputation": 0, + &"unmet_need": 0, + &"event": 0, + &"recent_price_penalty": 0, + &"wait_penalty": 0, + } + ) + if not configured.is_success(): + return configured + if seen_classes.size() != allowed_classes.size(): + return CommandResult.failed( + CommandResult.Code.VALIDATION_FAILED, 0, + "Every MVP Guest class must have exactly one demand profile." + ) + return CommandResult.succeeded(0, {&"profile_count": seen_classes.size()}) + + +func get_facility_upkeep_report(facility_id: StringName) -> Dictionary: + var definition_id: StringName = &"" + var facility := facility_service.get_instance(facility_id) + if facility != null: + definition_id = facility.definition_id + elif construction_service != null: + for built: Dictionary in construction_service.get_facilities(): + if StringName(built[&"instance_id"]) == facility_id: + definition_id = StringName(built[&"definition_id"]) + break + var definition := definition_registry.get_facility(definition_id) + var balance := upkeep_settlement.get_facility_balance(facility_id) + var units := facility_upkeep.get_unsettled_units(facility_id, upkeep_settlement.get_settled_period_count()) + return { + &"facility_id": facility_id, + &"owned": definition != null, + &"daily_rate": definition.maintenance_gold if definition != null else 0, + &"unsettled_units": units + int(balance[&"fraction_units"]), + &"gold_denominator": FacilityUpkeepAccrual.MINUTES_PER_DAY, + &"arrears": balance[&"arrears"], + } + + +func _advance_facility_upkeep(minute: int) -> void: + if _operations_deferred_runtime_minute >= 0: + return + var rates: Dictionary = {} + for facility: FacilityInstance in facility_service.get_instances(): + var definition := definition_registry.get_facility(facility.definition_id) + if definition != null: + rates[String(facility.instance_id)] = definition.maintenance_gold + # Infrastructure modules are completed assets even when they have no service instance. + for facility: Dictionary in construction_service.get_facilities(): + var definition := definition_registry.get_facility(StringName(facility[&"definition_id"])) + if definition != null: + rates[String(facility[&"instance_id"])] = definition.maintenance_gold + var advanced := facility_upkeep.advance_to(minute, rates) + if not advanced[&"ok"]: + _clock.set_paused(true) + push_error("Facility upkeep accrual failed: %s" % advanced) + + +func _upkeep_accrual_crosslinks_are_valid(accrual: FacilityUpkeepAccrual, settlement: FacilityUpkeepSettlement) -> bool: + var periods: Array = accrual.get_report()[&"closed_periods"] + var receipts: Array = settlement.get_report()[&"receipts"] + if receipts.size() > periods.size(): + return false + for index in receipts.size(): + if receipts[index][&"period"] != periods[index]: + return false + return true + + +func _operations_closed_arrears_total() -> int: + # Coordinator arrears describe its last financial close. Interim calendar + # payments during a legacy 06:00 period must not rewrite that closed record. + var last_close := 0 + if operations_coordinator != null and operations_coordinator.get_open_financial_day() > 1: + last_close = operations_coordinator.get_daily_close_minute(operations_coordinator.get_open_financial_day() - 1) + return _staff_payroll_arrears_total(staff_service) + upkeep_settlement.get_arrears_through(last_close) + + +func _operations_total_arrears() -> int: + return _staff_payroll_arrears_total(staff_service) + upkeep_settlement.get_arrears_total() + + +func _advance_operations_time(absolute_minute: int) -> void: + if not _operations_enabled or operations_coordinator == null: + return + if _operations_deferred_runtime_minute >= 0: + _operations_tick_blocked = true + _clock.set_paused(true) + return + _operations_tick_blocked = false + var advanced := operations_coordinator.advance_to(absolute_minute, true) + if not advanced.is_success(): + # Earlier boundaries in the same minute can already be validly committed + # (for example demand before a failed arrival). Publish only those staged + # effects and notifications; the failing callback rolled itself back. + if not _commit_operations_domain_events() \ + or not _commit_operations_notifications(): + operations_coordinator.discard_deferred_notifications() + _discard_operations_domain_events() + _operations_runtime_failure_latched = true + _operations_runtime_failure_report = advanced.to_dictionary() + _operations_deferred_runtime_minute = absolute_minute + _operations_tick_blocked = true + _clock.set_paused(true) + return + if operations_coordinator.current_minute != absolute_minute \ + or operations_coordinator.get_population_count() != guest_service.guest_count() \ + or operations_coordinator.get_arrears_total() != _operations_closed_arrears_total() \ + or not _operations_notifications_are_committable() \ + or not _commit_operations_domain_events() \ + or not _commit_operations_notifications(): + operations_coordinator.discard_deferred_notifications() + _discard_operations_domain_events() + _operations_runtime_failure_latched = true + _operations_runtime_failure_report = { + &"code": &"OPERATIONS_CROSSLINK_MISMATCH", + &"minute": absolute_minute, + } + _operations_deferred_runtime_minute = absolute_minute + _operations_tick_blocked = true + _clock.set_paused(true) + return + _operations_runtime_failure_latched = false + _operations_runtime_failure_report.clear() + if operations_coordinator.is_terminal(): + _operations_deferred_runtime_minute = -1 + _operations_tick_blocked = true + _clock.set_paused(true) + return + if operations_coordinator.pause_requested: + _operations_deferred_runtime_minute = absolute_minute + _operations_tick_blocked = true + _clock.set_paused(true) + return + _operations_deferred_runtime_minute = -1 + _pause_for_waiting_foreground_event() + + +func _operations_should_block_tick() -> bool: + if not _operations_enabled or operations_coordinator == null: + return false + return _operations_deferred_runtime_minute >= 0 \ + or _operations_runtime_failure_latched \ + or operations_coordinator.pause_requested \ + or operations_coordinator.is_terminal() \ + or operations_coordinator.is_external_quarantined() + + +func _pause_for_waiting_foreground_event() -> void: + if event_service == null or _clock == null: + return + var foreground := event_service.get_foreground_instance() + if foreground != null and foreground.state == EventInstance.State.WAITING_CHOICE: + _clock.set_paused(true) + + +func _capture_operations_external_state() -> Dictionary: + # This is deliberately a composed transaction token, not a Runtime snapshot. + # It contains every aggregate that an Operations callback can reach, while + # excluding clock/coordinator state owned by the caller of the callback. + if not _begin_operations_external_transaction(): + return {} + return _build_operations_external_state() + + +func _build_operations_external_state() -> Dictionary: + var committed_minute := _runtime_aggregate_committed_minute() + if committed_minute < 0: + _discard_operations_domain_events() + return {} + var rng_snapshots: Dictionary = {} + for stream_id: StringName in _sorted_name_keys(_rng_streams): + var stream := _rng_streams.get(stream_id) as DeterministicRng + if stream == null: + _discard_operations_domain_events() + return {} + rng_snapshots[stream_id] = stream.snapshot() + return { + # TIME_BOUNDARIES runs before the Runtime suffix. During a normal + # Operations callback the Clock is already at X while minute-driven + # aggregates are still committed through X - 1. Capture that committed + # participant minute so rollback validates the state it actually saved. + &"captured_minute": committed_minute, + &"captured_clock_minute": _clock.absolute_minute, + &"captured_authority_minute": authority_service.current_minute, + &"captured_defense_minute": defense_service.current_minute, + &"game_state": _state.create_snapshot(), + &"command_queue": command_queue.create_snapshot(), + &"next_command_id": _next_command_id, + &"economy": economy_service.create_snapshot(), + &"facility_upkeep": facility_upkeep.create_snapshot(), + &"upkeep_settlement": upkeep_settlement.create_snapshot(), + &"inventory": inventory_service.create_snapshot(), + &"kitchen_production": kitchen_production.create_snapshot(), + &"auto_purchase_plans": auto_purchase_plans.create_snapshot(), + &"facilities": facility_service.create_snapshot(), + &"facility_cleanliness": facility_cleanliness.create_snapshot(), + &"guests": guest_service.create_snapshot(), + &"staff": staff_service.create_snapshot(), + &"relationships": relationship_service.create_snapshot(), + &"events": event_service.create_snapshot(), + &"authority": authority_service.create_snapshot(), + &"world": world_service.create_snapshot(), + &"path": path_service.create_snapshot(), + &"elevators": elevators.create_snapshot(), + &"legacy_stairs": legacy_stairs.create_snapshot(), + &"excavation": excavation_service.create_snapshot(), + &"spatial_subslots": spatial_subslot_service.create_snapshot(), + &"route_projection": route_subslot_path_adapter.create_snapshot(), + &"facility_infrastructure": facility_infrastructure_projector.create_snapshot(), + &"movement_credits": _movement_credits.duplicate(true), + &"domain_events": domain_events.create_snapshot(), + &"id_generator": id_generator.create_snapshot(), + &"rng_streams": rng_snapshots, + &"staffing_projection_enabled": _staffing_projection_enabled, + &"progression": progression_service.create_snapshot(), + &"hired_staff_candidate_ids": _hired_staff_candidate_ids.duplicate(true), + &"personal_action_points": _personal_action_points, + # The remaining aggregates are reachable through Event actions or signals. + &"mana": mana_network_service.create_snapshot(), + &"logistics_dispatcher": logistics_dispatcher.create_snapshot(), + &"logistics_requests": _serialize_logistics_requests(), + &"defense": defense_service.create_snapshot(), + &"construction": construction_service.create_snapshot(), + } + + +func _operations_external_committed_minute() -> int: + return _runtime_aggregate_committed_minute() + + +func _runtime_aggregate_committed_minute() -> int: + if facility_service == null or guest_service == null or inventory_service == null \ + or staff_service == null or kitchen_production == null: + return -1 + var minute := facility_service.current_minute + if minute < 0 or guest_service.current_minute != minute \ + or inventory_service.current_minute != minute \ + or staff_service.current_minute != minute \ + or kitchen_production.current_minute != minute \ + or elevators.current_minute != minute: + return -1 + return minute + + +func _restore_operations_external_state(token: Variant) -> bool: + if not token is Dictionary: + _discard_operations_domain_events() + return false + var saved: Dictionary = token + var required_dictionary_keys: Array[StringName] = [ + &"facility_upkeep", &"upkeep_settlement", &"facility_cleanliness", + &"game_state", &"command_queue", &"economy", &"inventory", &"kitchen_production", &"auto_purchase_plans", &"facilities", &"guests", + &"staff", &"relationships", &"events", &"authority", &"world", &"path", &"elevators", &"legacy_stairs", + &"excavation", &"spatial_subslots", &"route_projection", + &"facility_infrastructure", + &"movement_credits", &"domain_events", &"id_generator", &"rng_streams", + &"progression", &"hired_staff_candidate_ids", &"mana", + &"logistics_dispatcher", &"defense", &"construction", + ] + for key: StringName in required_dictionary_keys: + if not saved.get(key, null) is Dictionary: + _discard_operations_domain_events() + return false + if not saved.get(&"logistics_requests", null) is Array \ + or typeof(saved.get(&"staffing_projection_enabled", null)) != TYPE_BOOL \ + or typeof(saved.get(&"personal_action_points", null)) != TYPE_INT \ + or typeof(saved.get(&"captured_minute", null)) != TYPE_INT \ + or int(saved[&"captured_minute"]) < 0 \ + or typeof(saved.get(&"captured_clock_minute", null)) != TYPE_INT \ + or int(saved[&"captured_clock_minute"]) < int(saved[&"captured_minute"]) \ + or typeof(saved.get(&"captured_authority_minute", null)) != TYPE_INT \ + or int(saved[&"captured_authority_minute"]) < 0 \ + or typeof(saved.get(&"captured_defense_minute", null)) != TYPE_INT \ + or int(saved[&"captured_defense_minute"]) < 0 \ + or typeof(saved.get(&"next_command_id", null)) != TYPE_INT \ + or int(saved[&"next_command_id"]) <= 0: + _discard_operations_domain_events() + return false + # Validation/PREPARE failure did not mutate a participant. Avoid restore APIs + # here because GameState.restore_snapshot intentionally emits reset signals. + if _operations_external_state_matches(saved): + _discard_operations_domain_events() + return true + + # Ask every participant to restore even when an earlier participant rejects + # its snapshot. This avoids knowingly leaving a half-restored external world. + var state_ok: bool = _state.restore_snapshot(saved[&"game_state"]) + var command_queue_ok := command_queue.restore_snapshot(saved[&"command_queue"]) + var economy_ok := economy_service.restore_snapshot(saved[&"economy"]) + var upkeep_ok := facility_upkeep.restore_snapshot(saved[&"facility_upkeep"]) and upkeep_settlement.restore_snapshot(saved[&"upkeep_settlement"], economy_service) + var inventory_ok := inventory_service.restore_snapshot(saved[&"inventory"]) + var facilities_ok := facility_service.restore_snapshot(saved[&"facilities"]) + facilities_ok = facility_cleanliness.restore_snapshot(saved.get(&"facility_cleanliness", {})) and facilities_ok + var kitchen_ok := kitchen_production.restore_snapshot(saved[&"kitchen_production"]) + var auto_purchase_ok := auto_purchase_plans.restore_snapshot(saved[&"auto_purchase_plans"], facility_service, economy_service, int(saved[&"captured_minute"])) + var guests_ok := guest_service.restore_snapshot(saved[&"guests"]) + var staff_ok := staff_service.restore_snapshot(saved[&"staff"]) + var relationships_ok := relationship_service.restore_snapshot(saved[&"relationships"]) + var events_ok := event_service.restore_snapshot(saved[&"events"]) + var authority_ok := authority_service.restore_snapshot(saved[&"authority"]) + var world_ok := world_service.restore_snapshot(saved[&"world"]) + var spatial_ok := spatial_subslot_service.restore_snapshot(saved[&"spatial_subslots"]) + var route_snapshot: Dictionary = saved[&"route_projection"] + var projected_walkable: Variant = route_snapshot.get(&"projected_walkable_cells", null) + var topology_ok := projected_walkable is Array \ + and path_service.rebuild_graph(projected_walkable) + var path_ok := topology_ok and path_service.restore_snapshot(saved[&"path"]) + path_ok = elevators.restore_snapshot(saved[&"elevators"]) and path_ok + path_ok = legacy_stairs.restore_snapshot(saved[&"legacy_stairs"]) and path_ok + var route_ok := path_ok and route_subslot_path_adapter.restore_snapshot(route_snapshot) + var infrastructure_ok := route_ok and facility_infrastructure_projector.restore_snapshot( + saved[&"facility_infrastructure"] + ) + var domain_events_ok := domain_events.restore_snapshot(saved[&"domain_events"]) + var ids_ok := id_generator.restore_snapshot(saved[&"id_generator"]) + var progression_ok := progression_service.restore_snapshot(saved[&"progression"]) + var mana_ok := mana_network_service.restore_snapshot(saved[&"mana"]) + var dispatcher_ok := logistics_dispatcher.restore_snapshot(saved[&"logistics_dispatcher"]) + var defense_ok := defense_service.restore_snapshot(saved[&"defense"]) + var construction_ok := construction_service.restore_snapshot(saved[&"construction"]) + var excavation_ok := excavation_service.restore_snapshot(saved[&"excavation"]) + var restored_requests: Variant = _deserialize_logistics_requests(saved[&"logistics_requests"]) + var requests_ok := restored_requests is Dictionary + if requests_ok: + logistics_requests = restored_requests + _movement_credits = (saved[&"movement_credits"] as Dictionary).duplicate(true) + _staffing_projection_enabled = bool(saved[&"staffing_projection_enabled"]) + _hired_staff_candidate_ids = ( + saved[&"hired_staff_candidate_ids"] as Dictionary + ).duplicate(true) + _personal_action_points = int(saved[&"personal_action_points"]) + _next_command_id = int(saved[&"next_command_id"]) + + var rng_ok := true + var saved_rngs: Dictionary = saved[&"rng_streams"] + if saved_rngs.size() != _rng_streams.size(): + rng_ok = false + for raw_stream_id: Variant in saved_rngs: + var stream_id := StringName(raw_stream_id) + var stream := _rng_streams.get(stream_id) as DeterministicRng + if stream == null or not saved_rngs[raw_stream_id] is Dictionary \ + or not stream.restore(saved_rngs[raw_stream_id]): + rng_ok = false + + _discard_operations_domain_events() + var participants_ok := upkeep_ok and state_ok and command_queue_ok and economy_ok and inventory_ok and kitchen_ok and auto_purchase_ok \ + and facilities_ok and guests_ok and staff_ok and relationships_ok \ + and events_ok and authority_ok and world_ok and spatial_ok and path_ok \ + and route_ok and infrastructure_ok and excavation_ok \ + and domain_events_ok and ids_ok \ + and progression_ok and mana_ok and dispatcher_ok and defense_ok \ + and construction_ok and requests_ok and rng_ok + if not participants_ok: + return false + var validation_minute := int(saved[&"captured_minute"]) + var clock_validation_minute := int(saved[&"captured_clock_minute"]) + return _operations_external_state_matches(saved) \ + and bool(economy_service.validate_ledger().get(&"ok", false)) \ + and _validate_logistics_dispatcher_crosslinks( + logistics_dispatcher, logistics_requests, clock_validation_minute + ) \ + and _validate_runtime_crosslinks( + facility_service, guest_service, economy_service, inventory_service, + staff_service, validation_minute, _staffing_projection_enabled + ) \ + and _validate_runtime_path_crosslinks( + path_service, guest_service, staff_service, _movement_credits, + clock_validation_minute + ) \ + and _validate_authority_economy_crosslinks( + authority_service, economy_service, + int(saved[&"captured_authority_minute"]) + ) \ + and _validate_defense_crosslinks( + defense_service, staff_service, facility_service, + int(saved[&"captured_defense_minute"]) + ) \ + and _validate_construction_facility_crosslinks( + world_service, construction_service, facility_service, + facility_infrastructure_projector + ) \ + and _validate_infrastructure_crosslinks( + world_service, + path_service, + construction_service, + construction_resource_state, + excavation_service, + spatial_subslot_service, + route_subslot_path_adapter, + facility_infrastructure_projector + ) + + +func _operations_external_state_matches(saved: Dictionary) -> bool: + if not _strict_variants_equal(_state.create_snapshot(), saved[&"game_state"]) \ + or not _strict_variants_equal(command_queue.create_snapshot(), saved[&"command_queue"]) \ + or _next_command_id != int(saved[&"next_command_id"]) \ + or not _strict_variants_equal(facility_upkeep.create_snapshot(), saved[&"facility_upkeep"]) \ + or not _strict_variants_equal(upkeep_settlement.create_snapshot(), saved[&"upkeep_settlement"]) \ + or not _strict_variants_equal(economy_service.create_snapshot(), saved[&"economy"]) \ + or not _strict_variants_equal(inventory_service.create_snapshot(), saved[&"inventory"]) \ + or not _strict_variants_equal(kitchen_production.create_snapshot(), saved[&"kitchen_production"]) \ + or not _strict_variants_equal(auto_purchase_plans.create_snapshot(), saved[&"auto_purchase_plans"]) \ + or not _strict_variants_equal(facility_service.create_snapshot(), saved[&"facilities"]) \ + or not _strict_variants_equal(facility_cleanliness.create_snapshot(), saved.get(&"facility_cleanliness", {})) \ + or not _strict_variants_equal(guest_service.create_snapshot(), saved[&"guests"]) \ + or not _strict_variants_equal(staff_service.create_snapshot(), saved[&"staff"]) \ + or not _strict_variants_equal(relationship_service.create_snapshot(), saved[&"relationships"]) \ + or not _strict_variants_equal(event_service.create_snapshot(), saved[&"events"]) \ + or not _strict_variants_equal(authority_service.create_snapshot(), saved[&"authority"]) \ + or not _strict_variants_equal(world_service.create_snapshot(), saved[&"world"]) \ + or not _strict_variants_equal(path_service.create_snapshot(), saved[&"path"]) \ + or not _strict_variants_equal(elevators.create_snapshot(), saved[&"elevators"]) \ + or not _strict_variants_equal(legacy_stairs.create_snapshot(), saved[&"legacy_stairs"]) \ + or not _strict_variants_equal(excavation_service.create_snapshot(), saved[&"excavation"]) \ + or not _strict_variants_equal(spatial_subslot_service.create_snapshot(), saved[&"spatial_subslots"]) \ + or not _strict_variants_equal(route_subslot_path_adapter.create_snapshot(), saved[&"route_projection"]) \ + or not _strict_variants_equal(facility_infrastructure_projector.create_snapshot(), saved[&"facility_infrastructure"]) \ + or not _strict_variants_equal(_movement_credits, saved[&"movement_credits"]) \ + or not _strict_variants_equal(domain_events.create_snapshot(), saved[&"domain_events"]) \ + or not _strict_variants_equal(id_generator.create_snapshot(), saved[&"id_generator"]) \ + or not _strict_variants_equal(progression_service.create_snapshot(), saved[&"progression"]) \ + or not _strict_variants_equal(mana_network_service.create_snapshot(), saved[&"mana"]) \ + or not _strict_variants_equal(logistics_dispatcher.create_snapshot(), saved[&"logistics_dispatcher"]) \ + or not _strict_variants_equal(_serialize_logistics_requests(), saved[&"logistics_requests"]) \ + or not _strict_variants_equal(defense_service.create_snapshot(), saved[&"defense"]) \ + or not _strict_variants_equal(construction_service.create_snapshot(), saved[&"construction"]) \ + or _staffing_projection_enabled != bool(saved[&"staffing_projection_enabled"]) \ + or not _strict_variants_equal(_hired_staff_candidate_ids, saved[&"hired_staff_candidate_ids"]) \ + or _personal_action_points != int(saved[&"personal_action_points"]): + return false + var saved_rngs: Dictionary = saved[&"rng_streams"] + if saved_rngs.size() != _rng_streams.size(): + return false + for raw_stream_id: Variant in saved_rngs: + var stream := _rng_streams.get(StringName(raw_stream_id)) as DeterministicRng + if stream == null or not _strict_variants_equal( + stream.snapshot(), saved_rngs[raw_stream_id] + ): + return false + return true + + +func _begin_operations_external_transaction() -> bool: + if _operations_transaction_active: + # One Coordinator minute can contain daily, demand and arrival boundaries. + # Keep their Runtime side effects in one notification/event transaction + # until the Coordinator has committed the complete minute. + return true + _operations_transaction_active = true + _operations_queued_domain_events.clear() + _operations_queued_guest_spawn_notifications.clear() + _operations_queued_economy_notifications.clear() + return true + + +func _commit_operations_domain_events() -> bool: + if not _operations_transaction_active: + return true + var seen: Dictionary = {} + for event: SimulationDomainEvent in _operations_queued_domain_events: + if event == null or not event.is_valid() or seen.has(event.event_id) \ + or domain_events.has_published(event.event_id): + return false + seen[event.event_id] = true + var committed := domain_events.publish_batch(_operations_queued_domain_events) + if not committed: + return false + _operations_queued_domain_events.clear() + _operations_transaction_active = false + return true + + +func _operations_notifications_are_committable() -> bool: + if operations_coordinator == null or guest_service == null: + return false + var seen_guest_ids: Dictionary = {} + for guest_id: StringName in _operations_queued_guest_spawn_notifications: + if guest_id.is_empty() or seen_guest_ids.has(guest_id) \ + or not guest_service.has_guest(guest_id) \ + or not guest_service.has_pending_guest_spawned_notification(guest_id): + return false + seen_guest_ids[guest_id] = true + var seen_transaction_ids: Dictionary = {} + for transaction_id: StringName in _operations_queued_economy_notifications: + if transaction_id.is_empty() or seen_transaction_ids.has(transaction_id) \ + or not economy_service.has_pending_transaction_notification(transaction_id): + return false + seen_transaction_ids[transaction_id] = true + return true + + +func _stage_operations_economy_notification(result: CommandResult) -> bool: + if result == null or not result.is_success(): + return false + var transaction_id := StringName(result.details.get(&"transaction_id", &"")) + if transaction_id.is_empty() \ + or not economy_service.has_pending_transaction_notification(transaction_id) \ + or _operations_queued_economy_notifications.has(transaction_id): + return false + _operations_queued_economy_notifications.append(transaction_id) + # Preserve Runtime's authoritative GameState projection now; the matching raw + # Economy signal is delivered to external observers only after commit. + _on_ledger_transaction_posted( + transaction_id, economy_service.get_transaction_entries(transaction_id) + ) + return true + + +func _discard_operations_domain_events() -> void: + for guest_id: StringName in _operations_queued_guest_spawn_notifications: + guest_service.discard_guest_spawned_notification(guest_id) + for transaction_id: StringName in _operations_queued_economy_notifications: + economy_service.discard_transaction_notification(transaction_id) + _operations_queued_guest_spawn_notifications.clear() + _operations_flushing_guest_spawn_notifications.clear() + _operations_queued_economy_notifications.clear() + _operations_flushing_economy_notifications.clear() + _operations_queued_domain_events.clear() + _operations_transaction_active = false + + +func _commit_operations_notifications() -> bool: + if operations_coordinator == null: + return false + guest_service.set_external_mutation_blocked(true) + var coordinator_notifications_ok := operations_coordinator.commit_deferred_notifications() + var guest_notifications_ok := true + if coordinator_notifications_ok: + for guest_id: StringName in _operations_queued_guest_spawn_notifications: + _operations_flushing_guest_spawn_notifications[guest_id] = true + if not guest_service.commit_guest_spawned_notification(guest_id): + guest_notifications_ok = false + _operations_flushing_guest_spawn_notifications.erase(guest_id) + else: + guest_notifications_ok = false + var economy_notifications_ok := coordinator_notifications_ok and guest_notifications_ok + if economy_notifications_ok: + for transaction_id: StringName in _operations_queued_economy_notifications: + _operations_flushing_economy_notifications[transaction_id] = true + if not economy_service.commit_transaction_notification(transaction_id): + economy_notifications_ok = false + _operations_flushing_economy_notifications.erase(transaction_id) + guest_service.set_external_mutation_blocked(false) + _operations_queued_guest_spawn_notifications.clear() + _operations_flushing_guest_spawn_notifications.clear() + _operations_queued_economy_notifications.clear() + _operations_flushing_economy_notifications.clear() + return coordinator_notifications_ok and guest_notifications_ok and economy_notifications_ok + + +func _publish_domain_event(event: SimulationDomainEvent) -> bool: + if _construction_completion_transaction_active: + if event == null or not event.is_valid() or domain_events.has_published(event.event_id): + return false + for queued: SimulationDomainEvent in _construction_queued_domain_events: + if queued.event_id == event.event_id: + return false + _construction_queued_domain_events.append(event.copy()) + return true + if not _operations_transaction_active: + return domain_events.publish(event) + if event == null or not event.is_valid() or domain_events.has_published(event.event_id): + return false + for queued: SimulationDomainEvent in _operations_queued_domain_events: + if queued.event_id == event.event_id: + return false + _operations_queued_domain_events.append(event.copy()) + return true + + +func _operations_daily_boundary( + payload: Dictionary, + phase: StringName, + token: Variant +) -> Dictionary: + if phase == OperationsCoordinator.PHASE_CAPTURE: + var captured := _capture_operations_external_state() + return {&"ok": not captured.is_empty(), &"token": captured} + if phase == OperationsCoordinator.PHASE_VALIDATE: + return { + &"ok": token is Dictionary and not token.is_empty() \ + and _operations_daily_payload_is_valid(payload), + &"token": token, + } + if phase == OperationsCoordinator.PHASE_APPLY: + return _apply_operations_daily_close(payload, token) + if phase == OperationsCoordinator.PHASE_ROLLBACK: + return {&"ok": _restore_operations_external_state(token)} + return {&"ok": false} + + +func _operations_daily_payload_is_valid(payload: Dictionary) -> bool: + for key: StringName in [ + &"day", &"boundary_minute", &"period_start_minute", + &"period_end_minute_exclusive", + ]: + if typeof(payload.get(key, null)) != TYPE_INT: + return false + var day := int(payload[&"day"]) + var boundary_minute := int(payload[&"boundary_minute"]) + return day > 0 \ + and boundary_minute == _clock.absolute_minute \ + and boundary_minute == operations_coordinator.get_daily_close_minute(day) \ + and int(payload[&"period_start_minute"]) == operations_coordinator.get_financial_period_start(day) \ + and int(payload[&"period_end_minute_exclusive"]) == boundary_minute + + +func _settle_upkeep_through(boundary_minute: int) -> Dictionary: + var upkeep_paid := 0 + for period: Dictionary in facility_upkeep.get_closed_periods_from(upkeep_settlement.get_settled_period_count()): + if int(period[&"period_end"]) > boundary_minute: + continue + var upkeep_result := upkeep_settlement.settle(period, economy_service) + if not upkeep_result[&"ok"]: + return {&"ok": false, &"reason": &"UPKEEP_SETTLEMENT_FAILED"} + if not upkeep_result[&"replayed"]: + for row: Dictionary in upkeep_result[&"receipt"][&"rows"]: + upkeep_paid += int(row[&"paid"]) + if not String(upkeep_result[&"transaction_id"]).is_empty() and not _stage_operations_economy_notification(CommandResult.succeeded(0, {&"transaction_id": upkeep_result[&"transaction_id"]})): + return {&"ok": false, &"reason": &"UPKEEP_NOTIFICATION_STAGE_FAILED"} + return {&"ok": true, &"paid_total": upkeep_paid} + + +func _operations_midnight_upkeep_boundary(payload: Dictionary, phase: StringName, token: Variant) -> Dictionary: + if phase == OperationsCoordinator.PHASE_CAPTURE: + var captured := _capture_operations_external_state() + return {&"ok": not captured.is_empty(), &"token": captured} + if phase == OperationsCoordinator.PHASE_ROLLBACK: + return {&"ok": _restore_operations_external_state(token)} + var minute: Variant = payload.get(&"boundary_minute") + var valid: bool = token is Dictionary and not token.is_empty() and typeof(minute) == TYPE_INT \ + and int(minute) == _clock.absolute_minute and int(minute) % 1440 == 0 + if phase == OperationsCoordinator.PHASE_VALIDATE: + return {&"ok": valid, &"token": token} + if phase != OperationsCoordinator.PHASE_APPLY or not valid: + return {&"ok": false} + var prepared := _run_operations_test_boundary_hook(&"MIDNIGHT_UPKEEP", payload, OPERATIONS_TEST_PREPARE_PHASE, token) + if not bool(prepared.get(&"ok", false)): + return prepared + var result := _settle_upkeep_through(int(minute)) + if not bool(result[&"ok"]): + return result + var after := _run_operations_test_boundary_hook(&"MIDNIGHT_UPKEEP", payload, OPERATIONS_TEST_AFTER_FIRST_MUTATION_PHASE, token) + if not bool(after.get(&"ok", false)): + return after + return {&"ok": upkeep_settlement.validate_ledger(economy_service) \ + and int(_state.get_resource(&"gold")) == economy_service.get_balance(PLAYER_CASH_ACCOUNT)} + + +func _apply_operations_daily_close(payload: Dictionary, token: Variant) -> Dictionary: + if not token is Dictionary or token.is_empty() \ + or not _operations_daily_payload_is_valid(payload): + return {&"ok": false, &"reason": &"DAILY_CLOSE_PRECONDITION_FAILED"} + var prepare_hook := _run_operations_test_boundary_hook( + &"DAILY_CLOSE", payload, OPERATIONS_TEST_PREPARE_PHASE, token + ) + if not bool(prepare_hook.get(&"ok", false)): + return prepare_hook + var day := int(payload[&"day"]) + var preview := staff_service.preview_daily_payroll(day) + if not bool(preview.get(&"ok", false)) \ + or not preview.get(&"rows", null) is Array: + return {&"ok": false, &"reason": &"PAYROLL_PREVIEW_FAILED"} + var available_gold := economy_service.get_balance(PLAYER_CASH_ACCOUNT) + var remaining_gold := available_gold + var payments: Dictionary = {} + var expected_paid_total := 0 + var previous_staff_id := "" + for raw_row: Variant in preview[&"rows"]: + if not raw_row is Dictionary: + return {&"ok": false, &"reason": &"PAYROLL_PREVIEW_INVALID"} + var row: Dictionary = raw_row + var staff_id_value: Variant = row.get(&"staff_id", null) + var total_due_value: Variant = row.get(&"total_due", null) + if typeof(staff_id_value) not in [TYPE_STRING, TYPE_STRING_NAME] \ + or StringName(staff_id_value).is_empty() \ + or typeof(total_due_value) != TYPE_INT or int(total_due_value) < 0: + return {&"ok": false, &"reason": &"PAYROLL_PREVIEW_INVALID"} + var staff_text := String(staff_id_value) + if not previous_staff_id.is_empty() and staff_text <= previous_staff_id: + return {&"ok": false, &"reason": &"PAYROLL_ORDER_INVALID"} + previous_staff_id = staff_text + var payment := mini(int(total_due_value), remaining_gold) + payments[StringName(staff_id_value)] = payment + remaining_gold -= payment + expected_paid_total += payment + + var settlement_id := StringName("payroll-day-%08d" % day) + var settlement := staff_service.apply_daily_payroll_settlement( + settlement_id, day, payments + ) + if not settlement.is_success() \ + or int(settlement.details.get(&"paid_total", -1)) != expected_paid_total: + return { + &"ok": false, + &"reason": &"PAYROLL_SETTLEMENT_FAILED", + &"result": settlement.to_dictionary(), + } + if expected_paid_total > 0: + var wage_payment := economy_service.transfer( + PLAYER_CASH_ACCOUNT, + WAGE_EXPENSE_ACCOUNT, + expected_paid_total, + int(payload[&"boundary_minute"]), + &"WAGE", + settlement_id, + StringName("operations.wage.day.%08d" % day), + &"operations.payroll.settlement", + &"", + false + ) + if not wage_payment.is_success(): + return { + &"ok": false, + &"reason": &"WAGE_LEDGER_POST_FAILED", + &"result": wage_payment.to_dictionary(), + } + if not _stage_operations_economy_notification(wage_payment): + return { + &"ok": false, + &"reason": &"WAGE_NOTIFICATION_STAGE_FAILED", + &"result": wage_payment.to_dictionary(), + } + var upkeep_result := _settle_upkeep_through(int(payload[&"boundary_minute"])) + if not bool(upkeep_result[&"ok"]): + return upkeep_result + var upkeep_paid := int(upkeep_result[&"paid_total"]) + var financials := _build_operations_daily_financials( + int(payload[&"period_start_minute"]), + int(payload[&"period_end_minute_exclusive"]), + int(preview.get(&"new_wages_total", -1)), + preview[&"rows"] + ) + if not bool(financials.get(&"ok", false)): + return financials + var arrears_total := int(settlement.details.get(&"unpaid_after_total", -1)) + if arrears_total < 0 \ + or arrears_total != _staff_payroll_arrears_total(staff_service) \ + or economy_service.get_balance(PLAYER_CASH_ACCOUNT) != available_gold - expected_paid_total - upkeep_paid \ + or int(_state.get_resource(&"gold")) != economy_service.get_balance(PLAYER_CASH_ACCOUNT) \ + or not bool(economy_service.validate_ledger().get(&"ok", false)) \ + or not bool(staff_service.validate_invariants().get(&"ok", false)): + return {&"ok": false, &"reason": &"DAILY_CLOSE_POSTCONDITION_FAILED"} + var settlement_report := settlement.details.duplicate(true) + settlement_report[&"upkeep_arrears_total"] = upkeep_settlement.get_arrears_total() + settlement_report[&"arrears_total"] = arrears_total + int(settlement_report[&"upkeep_arrears_total"]) + return { + &"ok": true, + &"category_totals": financials[&"category_totals"], + &"reconciliation_difference": financials[&"reconciliation_difference"], + &"facility_profit_loss": financials[&"facility_profit_loss"], + &"anomalies": financials[&"anomalies"], + &"settlement": settlement_report, + } + + +func _build_operations_daily_financials( + period_start: int, + period_end_exclusive: int, + new_wages_total: int, + payroll_rows: Array +) -> Dictionary: + if period_start < 0 or period_end_exclusive <= period_start or new_wages_total < 0: + return {&"ok": false, &"reason": &"DAILY_FINANCIAL_PERIOD_INVALID"} + if not _loan_principal_ledger_is_valid(economy_service): + return {&"ok": false, &"reason": &"INVALID_LOAN_PRINCIPAL_LEDGER"} + var category_totals := { + &"revenue": 0, + &"refunds": 0, + &"cost_of_goods": 0, + &"wages": new_wages_total, + &"maintenance": 0, + &"utilities": 0, + &"taxes_and_interest": 0, + &"fines": 0, + &"construction_cash_flow": 0, + } + var facility_rows: Dictionary = {} + var excluded_categories: Array[StringName] = [ + &"EVENT_COST_RESERVED", &"EVENT_COST_RELEASED", &"INITIALIZATION", + &"TRANSFER", &"GENERATION", &"DESTRUCTION", &"MIGRATION", + &"SERVICE_CASH_SWEEP", &"WAGE", &"MAINTENANCE_PAYMENT", + ] + var category_mapping := { + &"SERVICE_SALE": &"revenue", + &"REFUND": &"refunds", + &"CONSTRUCTION": &"construction_cash_flow", + &"CONSTRUCTION_REFUND": &"construction_cash_flow", + &"AUTHORITY_FINE": &"fines", + &"OPERATING_EXPENSE": &"cost_of_goods", + &"STAFF_SIGNING_COST": &"cost_of_goods", + &"EVENT_ACTION": &"cost_of_goods", + &"EVENT_COST_COMMITTED": &"cost_of_goods", + &"DEFENSE_SKILL_COST": &"cost_of_goods", + &"MAINTENANCE_ACCRUAL": &"maintenance", + &"LOAN_PRINCIPAL_BORROWED": &"financing_principal_inflows", + &"LOAN_PRINCIPAL_REPAID": &"financing_principal_outflows", + } + var operating_refund_total := 0 + for entry: LedgerEntry in economy_service.get_entries(): + if entry.absolute_minute < period_start \ + or entry.absolute_minute >= period_end_exclusive: + continue + if entry.category in excluded_categories: + continue + if entry.category == &"OPERATING_REFUND": + operating_refund_total += entry.amount + continue + if not category_mapping.has(entry.category): + return { + &"ok": false, + &"reason": &"UNKNOWN_LEDGER_CATEGORY", + &"category": entry.category, + &"transaction_id": entry.transaction_id, + } + var category: StringName = category_mapping[entry.category] + category_totals[category] = int(category_totals.get(category, 0)) + entry.amount + if not entry.source_entity_id.is_empty() \ + and (facility_service.has_instance(entry.source_entity_id) or entry.category == &"MAINTENANCE_ACCRUAL") \ + and category not in [&"construction_cash_flow", &"financing_principal_inflows", &"financing_principal_outflows"]: + if not facility_rows.has(entry.source_entity_id): + facility_rows[entry.source_entity_id] = { + &"revenue": 0, &"expenses": 0, &"net_profit": 0, + } + var facility_row: Dictionary = facility_rows[entry.source_entity_id] + if category == &"revenue": + facility_row[&"revenue"] = int(facility_row[&"revenue"]) + entry.amount + else: + facility_row[&"expenses"] = int(facility_row[&"expenses"]) + entry.amount + var anomalies: Array[Dictionary] = [] + var wage_allocation := _allocate_operations_wages_to_facilities( + period_start, + period_end_exclusive, + new_wages_total, + payroll_rows, + facility_rows + ) + if not bool(wage_allocation.get(&"ok", false)): + return wage_allocation + anomalies.append_array(wage_allocation[&"anomalies"]) + var operating_refund_offset := mini( + operating_refund_total, + int(category_totals[&"cost_of_goods"]) + ) + category_totals[&"cost_of_goods"] = int(category_totals[&"cost_of_goods"]) \ + - operating_refund_offset + var excess_operating_refund := operating_refund_total - operating_refund_offset + if excess_operating_refund > 0: + category_totals[&"revenue"] = int(category_totals[&"revenue"]) \ + + excess_operating_refund + anomalies.append({ + &"code": &"OPERATING_REFUND_EXCEEDS_PERIOD_COST", + &"refund_total": operating_refund_total, + &"cost_offset": operating_refund_offset, + &"excess_revenue": excess_operating_refund, + }) + for facility_id: StringName in _sorted_name_keys(facility_rows): + var row: Dictionary = facility_rows[facility_id] + row[&"net_profit"] = int(row[&"revenue"]) - int(row[&"expenses"]) + var allocation_rows: Array[Dictionary] = [] + for facility_id: StringName in _sorted_name_keys( + wage_allocation[&"allocated_by_facility"] + ): + allocation_rows.append({ + &"facility_id": String(facility_id), + &"amount": int(wage_allocation[&"allocated_by_facility"][facility_id]), + }) + var unallocated_rows: Array[Dictionary] = [] + for staff_id: StringName in _sorted_name_keys( + wage_allocation[&"unallocated_by_staff"] + ): + unallocated_rows.append({ + &"staff_id": String(staff_id), + &"amount": int(wage_allocation[&"unallocated_by_staff"][staff_id]), + }) + var profit_loss_facility_ids: Array[String] = [] + for facility_id: StringName in _sorted_name_keys(facility_rows): + profit_loss_facility_ids.append(String(facility_id)) + anomalies.append({ + &"code": &"WAGE_ALLOCATION_EVIDENCE", + &"version": 1, + &"new_wages_total": new_wages_total, + &"allocated_total": int(wage_allocation[&"allocated_total"]), + &"unallocated_total": int(wage_allocation[&"unallocated_total"]), + &"allocations": allocation_rows, + &"unallocated_by_staff": unallocated_rows, + &"facility_ids": profit_loss_facility_ids, + }) + var daily_summary := economy_service.get_daily_summary(period_start / 1_440 + 1) + if daily_summary.is_empty(): + return {&"ok": false, &"reason": &"LEDGER_PERIOD_SUMMARY_MISSING"} + var difference := int(daily_summary.get(&"total_debit", 0)) \ + - int(daily_summary.get(&"total_credit", 0)) + return { + &"ok": true, + &"category_totals": category_totals, + &"reconciliation_difference": difference, + &"facility_profit_loss": facility_rows, + &"anomalies": anomalies, + } + + +func _allocate_operations_wages_to_facilities( + period_start: int, + period_end_exclusive: int, + new_wages_total: int, + payroll_rows: Array, + facility_rows: Dictionary +) -> Dictionary: + var new_wages_by_staff: Dictionary = {} + var observed_new_wages := 0 + var previous_staff_text := "" + for raw_row: Variant in payroll_rows: + if not raw_row is Dictionary: + return {&"ok": false, &"reason": &"PAYROLL_ALLOCATION_INPUT_INVALID"} + var row: Dictionary = raw_row + var staff_id_value: Variant = row.get(&"staff_id", null) + var new_wages_value: Variant = row.get(&"new_wages", null) + if typeof(staff_id_value) not in [TYPE_STRING, TYPE_STRING_NAME] \ + or StringName(staff_id_value).is_empty() \ + or typeof(new_wages_value) != TYPE_INT or int(new_wages_value) < 0: + return {&"ok": false, &"reason": &"PAYROLL_ALLOCATION_INPUT_INVALID"} + var staff_text := String(staff_id_value) + if not previous_staff_text.is_empty() and staff_text <= previous_staff_text: + return {&"ok": false, &"reason": &"PAYROLL_ALLOCATION_ORDER_INVALID"} + previous_staff_text = staff_text + new_wages_by_staff[StringName(staff_id_value)] = int(new_wages_value) + observed_new_wages += int(new_wages_value) + if observed_new_wages != new_wages_total: + return {&"ok": false, &"reason": &"PAYROLL_ALLOCATION_TOTAL_MISMATCH"} + + var weights_by_staff: Dictionary = {} + for shift: ShiftInstance in staff_service.get_shifts(): + if shift == null or shift.cancelled or shift.assigned_facility_id.is_empty() \ + or not new_wages_by_staff.has(shift.staff_id) \ + or int(new_wages_by_staff[shift.staff_id]) <= 0 \ + or not facility_service.has_instance(shift.assigned_facility_id): + continue + var overlap := maxi( + mini(shift.end_at, period_end_exclusive) - maxi(shift.start_at, period_start), + 0 + ) + if overlap <= 0: + continue + # Recorded duty is the strongest attribution fact. A future/unadvanced Shift + # still uses its scheduled overlap so daily wages never disappear merely + # because the Runtime was restored immediately before the close. + var recorded_minutes := shift.worked_minutes + shift.overtime_minutes + var weight := mini(recorded_minutes, overlap) if recorded_minutes > 0 else overlap + if weight <= 0: + continue + if not weights_by_staff.has(shift.staff_id): + weights_by_staff[shift.staff_id] = {} + var facility_weights: Dictionary = weights_by_staff[shift.staff_id] + facility_weights[shift.assigned_facility_id] = int( + facility_weights.get(shift.assigned_facility_id, 0) + ) + weight + + var anomalies: Array[Dictionary] = [] + var allocated_total := 0 + var allocated_by_facility: Dictionary = {} + var unallocated_by_staff: Dictionary = {} + for staff_id: StringName in _sorted_name_keys(new_wages_by_staff): + var wage := int(new_wages_by_staff[staff_id]) + if wage <= 0: + continue + var weights: Dictionary = weights_by_staff.get(staff_id, {}) + if weights.is_empty(): + unallocated_by_staff[staff_id] = wage + anomalies.append({ + &"code": &"UNALLOCATED_WAGES", + &"staff_id": staff_id, + &"amount": wage, + }) + continue + var total_weight := 0 + for facility_id: StringName in _sorted_name_keys(weights): + total_weight += int(weights[facility_id]) + if total_weight <= 0: + return {&"ok": false, &"reason": &"PAYROLL_ALLOCATION_WEIGHT_INVALID"} + var allocations: Dictionary = {} + var floor_total := 0 + var facility_ids := _sorted_name_keys(weights) + for facility_id: StringName in facility_ids: + var amount := wage * int(weights[facility_id]) / total_weight + allocations[facility_id] = amount + floor_total += amount + var remainder := wage - floor_total + for index in range(remainder): + var facility_id: StringName = facility_ids[index] + allocations[facility_id] = int(allocations[facility_id]) + 1 + for facility_id: StringName in facility_ids: + var amount := int(allocations[facility_id]) + if amount <= 0: + continue + if not facility_rows.has(facility_id): + facility_rows[facility_id] = { + &"revenue": 0, &"expenses": 0, &"net_profit": 0, + } + var facility_row: Dictionary = facility_rows[facility_id] + facility_row[&"expenses"] = int(facility_row[&"expenses"]) + amount + allocated_by_facility[facility_id] = int( + allocated_by_facility.get(facility_id, 0) + ) + amount + allocated_total += amount + if allocated_total > new_wages_total: + return {&"ok": false, &"reason": &"PAYROLL_ALLOCATION_EXCEEDS_TOTAL"} + return { + &"ok": true, + &"allocated_total": allocated_total, + &"unallocated_total": new_wages_total - allocated_total, + &"allocated_by_facility": allocated_by_facility, + &"unallocated_by_staff": unallocated_by_staff, + &"anomalies": anomalies, + } + + +func _operations_arrival_boundary( + payload: Dictionary, + phase: StringName, + token: Variant +) -> Dictionary: + if phase == OperationsCoordinator.PHASE_CAPTURE: + var captured := _capture_operations_external_state() + var population_cap := ( + int(operations_coordinator.get_config().get(&"population_cap", 0)) + if operations_coordinator != null else 0 + ) + return { + &"ok": not captured.is_empty(), + &"token": captured, + &"capacity_remaining": maxi(population_cap - guest_service.guest_count(), 0) if int(_state.get_resource(&"mana")) > 0 and weather.permits_new_arrivals() else 0, + } + if phase == OperationsCoordinator.PHASE_VALIDATE: + return { + &"ok": token is Dictionary and not token.is_empty() \ + and _operations_arrival_payload_is_valid(payload), + &"token": token, + } + if phase == OperationsCoordinator.PHASE_APPLY: + return _apply_operations_arrivals(payload, token) + if phase == OperationsCoordinator.PHASE_ROLLBACK: + return {&"ok": _restore_operations_external_state(token)} + return {&"ok": false} + + +func _operations_arrival_payload_is_valid(payload: Dictionary) -> bool: + var plans_value: Variant = payload.get(&"plans", null) + var total_value: Variant = payload.get(&"total_count", null) + var minute_value: Variant = payload.get(&"minute", null) + if not plans_value is Array or typeof(total_value) != TYPE_INT \ + or int(total_value) < 0 or typeof(minute_value) != TYPE_INT \ + or int(minute_value) != _clock.absolute_minute: + return false + var allowed_classes: Dictionary = {} + for class_id: StringName in mvp_content_catalog.get_guest_class_ids(): + allowed_classes[class_id] = true + var species_ids := mvp_content_catalog.get_species_ids() + if allowed_classes.size() != 8 or species_ids.size() != 4: + return false + var seen_guest_ids: Dictionary = {} + var seen_plan_ids: Dictionary = {} + var counted := 0 + for raw_plan: Variant in plans_value: + if not raw_plan is Dictionary: + return false + var plan: Dictionary = raw_plan + var plan_id_value: Variant = plan.get(&"plan_id", null) + var guest_class_value: Variant = plan.get(&"guest_class", null) + var facility_tag_value: Variant = plan.get(&"facility_tag", null) + var time_band_value: Variant = plan.get(&"time_band", null) + var plan_minute_value: Variant = plan.get(&"minute", null) + var count_value: Variant = plan.get(&"count", null) + var guest_ids_value: Variant = plan.get(&"guest_ids", null) + if typeof(plan_id_value) not in [TYPE_STRING, TYPE_STRING_NAME] \ + or not _operations_sequence_id_is_valid(String(plan_id_value), "arrival-plan-") \ + or seen_plan_ids.has(StringName(plan_id_value)) \ + or typeof(guest_class_value) not in [TYPE_STRING, TYPE_STRING_NAME] \ + or not allowed_classes.has(StringName(guest_class_value)) \ + or typeof(facility_tag_value) not in [TYPE_STRING, TYPE_STRING_NAME] \ + or StringName(facility_tag_value).is_empty() \ + or typeof(time_band_value) not in [TYPE_STRING, TYPE_STRING_NAME] \ + or StringName(time_band_value) not in OperationsCoordinator.TIME_BANDS \ + or typeof(plan_minute_value) != TYPE_INT \ + or int(plan_minute_value) != int(minute_value) \ + or typeof(count_value) != TYPE_INT or int(count_value) <= 0 \ + or not guest_ids_value is Array \ + or guest_ids_value.size() != int(count_value): + return false + seen_plan_ids[StringName(plan_id_value)] = true + for raw_guest_id: Variant in guest_ids_value: + if typeof(raw_guest_id) not in [TYPE_STRING, TYPE_STRING_NAME]: + return false + var guest_id := StringName(raw_guest_id) + var actor_id := _guest_path_actor_id(guest_id) + if not _operations_sequence_id_is_valid(String(guest_id), "guest-auto-") \ + or seen_guest_ids.has(guest_id) or guest_service.has_guest(guest_id) \ + or not guest_service.get_persistent_record(guest_id).is_empty() \ + or economy_service.has_account(_guest_wallet_account(guest_id)) \ + or path_service.has_actor_projection(actor_id) \ + or _movement_credits.has(actor_id): + return false + seen_guest_ids[guest_id] = true + counted += 1 + if counted != int(total_value): + return false + var population_cap := int(operations_coordinator.get_config().get(&"population_cap", 0)) + return guest_service.guest_count() + counted <= population_cap \ + and _operations_entrance_cell() != null + + +func _operations_sequence_id_is_valid(value: String, prefix: String) -> bool: + if not value.begins_with(prefix) or value.length() != prefix.length() + 8: + return false + var suffix := value.substr(prefix.length(), 8) + if not suffix.is_valid_int(): + return false + var sequence := int(suffix) + return sequence > 0 and value == "%s%08d" % [prefix, sequence] + + +func _operations_entrance_cell() -> Variant: + var bounds := world_service.get_bounds() + for x in range(int(bounds.get(&"min_x", 0)), int(bounds.get(&"max_x", -1)) + 1): + var cell := Vector3i(x, 0, WorldService.LAYER_FRONT) + if not world_service.is_in_bounds(cell) \ + or world_service.get_zone(cell) != WorldService.ZONE_BOUNDARY: + continue + var traffic := path_service.get_cell_traffic(cell) + if traffic.is_empty() or bool(traffic.get(&"temporary_block", false)): + continue + return cell + return null + + +func _apply_operations_arrivals(payload: Dictionary, token: Variant) -> Dictionary: + if not token is Dictionary or token.is_empty() \ + or not _operations_arrival_payload_is_valid(payload): + return {&"ok": false, &"reason": &"ARRIVAL_PRECONDITION_FAILED"} + var prepare_hook := _run_operations_test_boundary_hook( + &"ARRIVAL_PLAN", payload, OPERATIONS_TEST_PREPARE_PHASE, token + ) + if not bool(prepare_hook.get(&"ok", false)): + return prepare_hook + if int(progression_service.metrics.get(&"guest_lifetime", 0)) \ + > ProgressionService.MAX_SAFE_INTEGER - int(payload[&"total_count"]): + return {&"ok": false, &"reason": &"ARRIVAL_LIFETIME_LIMIT"} + var entrance_value: Variant = _operations_entrance_cell() + var spawn_rng := get_rng_stream(&"guest_spawn") + var species_ids := mvp_content_catalog.get_species_ids() + if entrance_value == null or spawn_rng == null or species_ids.size() != 4: + return {&"ok": false, &"reason": &"ARRIVAL_CONTENT_UNAVAILABLE"} + var entrance: Vector3i = entrance_value + var accepted_count := 0 + var arrivals: Array[Dictionary] = [] + for raw_plan: Variant in payload[&"plans"]: + var plan: Dictionary = raw_plan + var guest_class := StringName(plan[&"guest_class"]) + for raw_guest_id: Variant in plan[&"guest_ids"]: + var guest_id := StringName(raw_guest_id) + var species := species_ids[spawn_rng.randi_range(0, species_ids.size() - 1)] + var definition_id := StringName("%s_%s" % [guest_class, species]) + var definition := guest_service.get_definition(definition_id) + if definition == null or not definition.adult_verified \ + or definition.wealth_min < 0 or definition.wealth_max < definition.wealth_min: + return {&"ok": false, &"reason": &"ARRIVAL_DEFINITION_INVALID"} + var wallet_gold := spawn_rng.randi_range( + definition.wealth_min, definition.wealth_max + ) + var command := SimulationCommand.new( + 1, + _clock.absolute_minute, + _clock.absolute_minute, + 0, + &"spawn_guest", + &"operations_coordinator", + guest_id, + { + &"guest_id": guest_id, + &"guest_type_id": definition_id, + &"gold": wallet_gold, + &"options": { + &"current_cell": entrance, + &"needs": { + &"hunger": 0, &"sleep": 0, &"hygiene": 0, + &"medical": 0, &"leisure": 0, &"intimacy": 0, + }, + &"planned_exit_at": _clock.absolute_minute + 720, + }, + }, + StringName("operations.arrival.%s" % guest_id) + ) + var spawned := _execute_spawn_guest(command) + var guest := guest_service.get_guest(guest_id) + if not spawned.is_success() or guest == null \ + or guest.guest_type_id != definition_id \ + or guest.current_cell != entrance \ + or guest.wallet_gold != wallet_gold \ + or economy_service.get_balance(_guest_wallet_account(guest_id)) != wallet_gold: + return { + &"ok": false, + &"reason": &"ARRIVAL_SPAWN_FAILED", + &"guest_id": guest_id, + &"result": spawned.to_dictionary(), + } + var arrival_row := { + &"guest_id": guest_id, + &"plan_id": StringName(plan[&"plan_id"]), + &"guest_class": guest_class, + &"species": species, + &"definition_id": definition_id, + &"adult_verified": true, + &"wallet_gold": wallet_gold, + &"entrance_cell": entrance, + } + if not _publish_domain_event(SimulationDomainEvent.new( + id_generator.next_id(&"domain_event"), + &"GUEST_AUTO_ARRIVED", + _clock.absolute_minute, + &"operations_coordinator", + [guest_id, definition_id], + arrival_row, + 0 + )): + return {&"ok": false, &"reason": &"ARRIVAL_EVENT_FAILED"} + arrivals.append(arrival_row) + accepted_count += 1 + if accepted_count == 1: + var after_mutation_hook := _run_operations_test_boundary_hook( + &"ARRIVAL_PLAN", + payload, + OPERATIONS_TEST_AFTER_FIRST_MUTATION_PHASE, + token + ) + if not bool(after_mutation_hook.get(&"ok", false)): + return after_mutation_hook + var runtime_crosslinks_ok := _validate_runtime_crosslinks( + facility_service, guest_service, economy_service, inventory_service, + staff_service, _operations_external_committed_minute(), + _staffing_projection_enabled + ) + var path_crosslinks_ok := _validate_runtime_path_crosslinks( + path_service, guest_service, staff_service, _movement_credits, + _clock.absolute_minute + ) + if accepted_count != int(payload[&"total_count"]) \ + or not runtime_crosslinks_ok or not path_crosslinks_ok: + return { + &"ok": false, + &"reason": &"ARRIVAL_POSTCONDITION_FAILED", + &"accepted_count": accepted_count, + &"expected_count": int(payload[&"total_count"]), + &"runtime_crosslinks_ok": runtime_crosslinks_ok, + &"path_crosslinks_ok": path_crosslinks_ok, + } + return { + &"ok": true, + &"accepted_count": accepted_count, + } + + +func _operations_morning_boundary( + payload: Dictionary, + phase: StringName, + token: Variant +) -> Dictionary: + if phase == OperationsCoordinator.PHASE_CAPTURE: + var captured := _capture_operations_external_state() + return {&"ok": not captured.is_empty(), &"token": captured} + if phase == OperationsCoordinator.PHASE_VALIDATE: + return { + &"ok": token is Dictionary and not token.is_empty() \ + and _operations_morning_payload_is_valid(payload), + &"token": token, + } + if phase == OperationsCoordinator.PHASE_APPLY: + return _apply_operations_morning(payload, token) + if phase == OperationsCoordinator.PHASE_ROLLBACK: + return {&"ok": _restore_operations_external_state(token)} + return {&"ok": false} + + +func _operations_morning_payload_is_valid(payload: Dictionary) -> bool: + var day_value: Variant = payload.get(&"day", null) + var minute_value: Variant = payload.get(&"boundary_minute", null) + if typeof(day_value) != TYPE_INT or int(day_value) <= 0 \ + or typeof(minute_value) != TYPE_INT: + return false + var minute := int(minute_value) + return minute == _clock.absolute_minute \ + and posmod(minute, SimulationKernel.MINUTES_PER_DAY) == OPERATIONS_START_MINUTE \ + and int(day_value) == minute / SimulationKernel.MINUTES_PER_DAY + 1 + + +func _apply_operations_morning(payload: Dictionary, token: Variant) -> Dictionary: + if not token is Dictionary or token.is_empty() \ + or not _operations_morning_payload_is_valid(payload): + return {&"ok": false, &"reason": &"MORNING_PRECONDITION_FAILED"} + var prepare_hook := _run_operations_test_boundary_hook( + &"MORNING_BOUNDARY", payload, OPERATIONS_TEST_PREPARE_PHASE, token + ) + if not bool(prepare_hook.get(&"ok", false)): + return prepare_hook + var minute := int(payload[&"boundary_minute"]) + var before_ap := _personal_action_points + _personal_action_points = mini( + _personal_action_points + 2, + MAX_PERSONAL_ACTION_POINTS + ) + var authority_advance := authority_service.advance_time(minute) + if not authority_advance.is_success(): + return { + &"ok": false, + &"reason": &"MORNING_AUTHORITY_ADVANCE_FAILED", + &"result": authority_advance.to_dictionary(), + } + var authority_evaluation := authority_service.evaluate_inspection(minute) + if not authority_evaluation.is_success(): + return { + &"ok": false, + &"reason": &"MORNING_AUTHORITY_EVALUATION_FAILED", + &"result": authority_evaluation.to_dictionary(), + } + if not authority_evaluation.is_replay: + _publish_authority_evaluation(authority_evaluation.details) + _enqueue_pending_relationship_candidates() + # start_next_event owns exactly one expiry pass and one actor audit pass. + # Calling those helpers separately here would double-apply morning effects. + var event_start := event_service.start_next_event( + minute, + _build_event_context(minute) + ) + var no_event_ready := ( + not event_start.is_success() + and event_start.message == "No eligible Event candidate is ready." + ) + var foreground_already_active := ( + not event_start.is_success() + and event_start.message == "A foreground Event is already active." + ) + if not event_start.is_success() and not no_event_ready and not foreground_already_active: + return { + &"ok": false, + &"reason": &"MORNING_EVENT_START_FAILED", + &"result": event_start.to_dictionary(), + } + if _personal_action_points < 0 \ + or _personal_action_points > MAX_PERSONAL_ACTION_POINTS \ + or authority_service.current_minute != minute \ + or not bool(authority_service.validate_invariants().get(&"ok", false)) \ + or not bool(event_service.validate_invariants().get(&"ok", false)) \ + or not bool(relationship_service.validate_invariants().get(&"ok", false)): + return {&"ok": false, &"reason": &"MORNING_POSTCONDITION_FAILED"} + return { + &"ok": true, + &"event_payload": { + &"personal_action_points_before": before_ap, + &"personal_action_points_after": _personal_action_points, + &"authority_advance": authority_advance.to_dictionary(), + &"authority_evaluation": authority_evaluation.to_dictionary(), + &"event_start": event_start.to_dictionary(), + &"no_event_ready": no_event_ready, + &"foreground_already_active": foreground_already_active, + }, + } + + +func _create_operations_runtime_snapshot() -> Dictionary: + return { + &"wrapper_version": OPERATIONS_RUNTIME_WRAPPER_VERSION, + &"enabled": _operations_enabled, + &"personal_action_points": _personal_action_points, + &"runtime_failure_latched": _operations_runtime_failure_latched, + &"runtime_failure_report": _operations_runtime_failure_report.duplicate(true), + &"deferred_runtime_minute": _operations_deferred_runtime_minute, + &"coordinator": ( + operations_coordinator.create_snapshot() + if _operations_enabled and operations_coordinator != null else {} + ), + } + + +func _parse_operations_runtime_snapshot( + raw_wrapper: Variant, + runtime_schema_version: int, + restored_minute: int, + restored_guests: GuestService, + restored_staff: StaffService, + restored_economy: EconomyService, + restored_world_seed: int, + restored_clock_paused: Variant, + restored_upkeep_settlement: FacilityUpkeepSettlement +) -> Dictionary: + var disabled := { + &"ok": true, + &"enabled": false, + &"personal_action_points": 0, + &"runtime_failure_latched": false, + &"runtime_failure_report": {}, + &"deferred_runtime_minute": -1, + &"coordinator": null, + } + if runtime_schema_version < OPERATIONS_RUNTIME_SNAPSHOT_VERSION: + return disabled + if typeof(restored_clock_paused) != TYPE_BOOL \ + or not restored_economy.has_account(WAGE_EXPENSE_ACCOUNT): + return {&"ok": false} + if not raw_wrapper is Dictionary: + return {&"ok": false} + var wrapper: Dictionary = raw_wrapper + var expected_keys: Array[StringName] = [ + &"wrapper_version", &"enabled", &"personal_action_points", + &"runtime_failure_latched", &"runtime_failure_report", + &"deferred_runtime_minute", &"coordinator", + ] + if not _dictionary_has_exact_canonical_keys(wrapper, expected_keys): + return {&"ok": false} + if typeof(wrapper[&"wrapper_version"]) != TYPE_INT \ + or int(wrapper[&"wrapper_version"]) != OPERATIONS_RUNTIME_WRAPPER_VERSION \ + or typeof(wrapper[&"enabled"]) != TYPE_BOOL \ + or typeof(wrapper[&"personal_action_points"]) != TYPE_INT \ + or typeof(wrapper[&"runtime_failure_latched"]) != TYPE_BOOL \ + or not wrapper[&"runtime_failure_report"] is Dictionary \ + or typeof(wrapper[&"deferred_runtime_minute"]) != TYPE_INT \ + or not wrapper[&"coordinator"] is Dictionary: + return {&"ok": false} + var enabled := bool(wrapper[&"enabled"]) + var action_points := int(wrapper[&"personal_action_points"]) + var failure_latched := bool(wrapper[&"runtime_failure_latched"]) + var failure_report: Dictionary = wrapper[&"runtime_failure_report"] + var deferred_runtime_minute := int(wrapper[&"deferred_runtime_minute"]) + var coordinator_snapshot: Dictionary = wrapper[&"coordinator"] + if action_points < 0 or action_points > MAX_PERSONAL_ACTION_POINTS \ + or failure_latched != not failure_report.is_empty() \ + or (failure_latched \ + and not _operations_failure_report_is_valid(failure_report, restored_minute)): + return {&"ok": false} + if not enabled: + if action_points != 0 or failure_latched or deferred_runtime_minute != -1 \ + or not coordinator_snapshot.is_empty(): + return {&"ok": false} + return disabled + var restored_coordinator := OperationsCoordinator.new() + if not restored_coordinator.restore_snapshot(coordinator_snapshot): + return {&"ok": false} + var coordinator_current_matches := restored_coordinator.current_minute == restored_minute + var pending_boundary_minute := int( + coordinator_snapshot.get(&"pending_boundary_minute", -1) + ) + var deferred_boundary_minute := int( + coordinator_snapshot.get(&"deferred_boundary_minute", -1) + ) + var normal_shape := deferred_runtime_minute == -1 \ + and not failure_latched and not restored_coordinator.pause_requested \ + and not restored_coordinator.is_terminal() \ + and coordinator_current_matches + var failure_shape := deferred_runtime_minute == restored_minute \ + and failure_latched and bool(restored_clock_paused) \ + and restored_coordinator.current_minute == restored_minute - 1 \ + and pending_boundary_minute == restored_minute + var pause_shape := deferred_runtime_minute == restored_minute \ + and not failure_latched and bool(restored_clock_paused) \ + and restored_coordinator.pause_requested and coordinator_current_matches \ + and deferred_boundary_minute == restored_minute + var terminal_shape := deferred_runtime_minute == -1 \ + and not failure_latched and restored_coordinator.is_terminal() \ + and coordinator_current_matches and bool(restored_clock_paused) + if not bool(restored_coordinator.validate_invariants().get(&"ok", false)) \ + or restored_coordinator.get_origin_minute() != OPERATIONS_COORDINATOR_ORIGIN_MINUTE \ + or not (normal_shape or failure_shape or pause_shape or terminal_shape) \ + or restored_coordinator.get_population_count() != restored_guests.guest_count() \ + or restored_coordinator.get_arrears_total() != (_staff_payroll_arrears_total(restored_staff) + restored_upkeep_settlement.get_arrears_through(restored_coordinator.get_daily_close_minute(restored_coordinator.get_open_financial_day() - 1))) \ + or not _operations_payroll_crosslinks_are_valid( + restored_coordinator, restored_staff, restored_economy, restored_upkeep_settlement + ) \ + or not _operations_coordinator_configuration_is_canonical( + restored_coordinator, restored_world_seed + ) \ + or not _operations_arrival_guest_crosslinks_are_valid( + restored_coordinator, restored_guests + ) \ + or (restored_coordinator.is_external_quarantined() \ + and not (failure_shape or pause_shape)): + return {&"ok": false} + return { + &"ok": true, + &"enabled": true, + &"personal_action_points": action_points, + &"runtime_failure_latched": failure_latched, + &"runtime_failure_report": failure_report.duplicate(true), + &"deferred_runtime_minute": deferred_runtime_minute, + &"coordinator": restored_coordinator, + } + + +func _progression_guest_lifetime_matches( + progression: ProgressionService, + guests: GuestService +) -> bool: + if progression == null or guests == null: + return false + var guest_snapshot := guests.create_snapshot() + var persistent_value: Variant = guest_snapshot.get(&"persistent_records", null) + var metrics_value: Variant = progression.create_snapshot().get(&"metrics", null) + if not persistent_value is Array or not metrics_value is Dictionary: + return false + var lifetime_value: Variant = (metrics_value as Dictionary).get(&"guest_lifetime", null) + return typeof(lifetime_value) == TYPE_INT \ + and int(lifetime_value) == guests.guest_count() + persistent_value.size() + + +func _operations_arrival_guest_crosslinks_are_valid( + coordinator: OperationsCoordinator, + guests: GuestService +) -> bool: + if coordinator == null or guests == null: + return false + var definitions_by_guest: Dictionary = {} + for guest: GuestRuntime in guests.get_guests(): + definitions_by_guest[guest.guest_id] = guest.guest_type_id + var guest_snapshot := guests.create_snapshot() + var persistent_value: Variant = guest_snapshot.get(&"persistent_records", null) + if not persistent_value is Array: + return false + for raw_record: Variant in persistent_value: + if not raw_record is Dictionary: + return false + var guest_id := StringName((raw_record as Dictionary).get(&"guest_id", &"")) + var definition_id := StringName( + (raw_record as Dictionary).get(&"guest_type_id", &"") + ) + if guest_id.is_empty() or definition_id.is_empty() \ + or definitions_by_guest.has(guest_id): + return false + definitions_by_guest[guest_id] = definition_id + var history_ids: Dictionary = {} + var expected_sequence := 1 + for report: Dictionary in coordinator.get_arrival_reports(): + var plans_value: Variant = report.get(&"plans", null) + if not plans_value is Array: + return false + for raw_plan: Variant in plans_value: + if not raw_plan is Dictionary: + return false + var plan: Dictionary = raw_plan + var guest_class := StringName(plan.get(&"guest_class", &"")) + var ids_value: Variant = plan.get(&"guest_ids", null) + if guest_class.is_empty() or not ids_value is Array: + return false + for raw_guest_id: Variant in ids_value: + if typeof(raw_guest_id) not in [TYPE_STRING, TYPE_STRING_NAME]: + return false + var guest_id := StringName(raw_guest_id) + if guest_id != StringName("guest-auto-%08d" % expected_sequence) \ + or history_ids.has(guest_id) \ + or not definitions_by_guest.has(guest_id): + return false + var definition_id := String(definitions_by_guest[guest_id]) + if not definition_id.begins_with("%s_" % String(guest_class)): + return false + history_ids[guest_id] = true + expected_sequence += 1 + for raw_guest_id: Variant in definitions_by_guest: + var guest_id := StringName(raw_guest_id) + if String(guest_id).begins_with("guest-auto-") \ + and not history_ids.has(guest_id): + return false + return true + + +func _guest_aggregate_has_reserved_operations_id(guests: GuestService) -> bool: + if guests == null: + return true + for guest: GuestRuntime in guests.get_guests(): + if String(guest.guest_id).begins_with("guest-auto-"): + return true + var records: Variant = guests.create_snapshot().get(&"persistent_records", null) + if not records is Array: + return true + for raw_record: Variant in records: + if not raw_record is Dictionary \ + or String((raw_record as Dictionary).get(&"guest_id", "")).begins_with( + "guest-auto-" + ): + return true + return false + + +func _operations_payroll_crosslinks_are_valid( + coordinator: OperationsCoordinator, + staff: StaffService, + economy: EconomyService, + upkeep: FacilityUpkeepSettlement +) -> bool: + if coordinator == null or staff == null or economy == null: + return false + var staff_snapshot := staff.create_snapshot() + var settlement_values: Variant = staff_snapshot.get(&"payroll_settlements", null) + if not settlement_values is Array: + return false + var summaries := coordinator.get_day_summaries() + if summaries.size() != settlement_values.size(): + return false + var expected_wage_transactions: Dictionary = {} + var settlements_by_day: Dictionary = {} + for raw_settlement: Variant in settlement_values: + if not raw_settlement is Dictionary: + return false + var serialized: Dictionary = raw_settlement + var day_value: Variant = serialized.get(&"day", null) + var settlement_id_value: Variant = serialized.get(&"settlement_id", null) + if typeof(day_value) != TYPE_INT \ + or typeof(settlement_id_value) not in [TYPE_STRING, TYPE_STRING_NAME]: + return false + var day := int(day_value) + var settlement_id := StringName(settlement_id_value) + if day <= 0 or settlement_id != StringName("payroll-day-%08d" % day) \ + or settlements_by_day.has(day): + return false + var staff_record := staff.get_payroll_settlement(settlement_id) + if staff_record.is_empty(): + return false + settlements_by_day[day] = staff_record + var paid_total := int(staff_record.get(&"paid_total", -1)) + if paid_total < 0: + return false + var idempotency_key := StringName("operations.wage.day.%08d" % day) + var transaction_id := economy.get_transaction_id_for_idempotency_key(idempotency_key) + if paid_total == 0: + if not transaction_id.is_empty(): + return false + continue + if transaction_id.is_empty() or expected_wage_transactions.has(transaction_id): + return false + var entries := economy.get_transaction_entries(transaction_id) + if entries.size() != 1: + return false + var entry: LedgerEntry = entries[0] + if entry.absolute_minute != coordinator.get_daily_close_minute(day) \ + or entry.debit_account != WAGE_EXPENSE_ACCOUNT \ + or entry.credit_account != PLAYER_CASH_ACCOUNT \ + or entry.amount != paid_total or entry.category != &"WAGE" \ + or entry.source_entity_id != settlement_id \ + or entry.memo_key != &"operations.payroll.settlement" \ + or not entry.related_transaction_id.is_empty(): + return false + expected_wage_transactions[transaction_id] = true + + for summary: Dictionary in summaries: + var day := int(summary.get(&"day", -1)) + if not settlements_by_day.has(day): + return false + var summary_settlement_value: Variant = summary.get(&"settlement", null) + if not summary_settlement_value is Dictionary: + return false + var summary_settlement: Dictionary = summary_settlement_value.duplicate(true) + var staff_record: Dictionary = settlements_by_day[day] + var expected_categories: Variant = _operations_expected_category_totals( + economy, + coordinator.get_financial_period_start(day), + coordinator.get_daily_close_minute(day), + int(staff_record.get(&"new_wages_total", -1)) + ) + if expected_categories == null \ + or not _strict_variants_equal( + _operations_snapshot_comparison_value( + summary.get(&"category_totals", {}) + ), + _operations_snapshot_comparison_value(expected_categories) + ) \ + or not _operations_facility_wage_summary_is_valid( + summary, staff_record, staff, economy + ): + return false + var upkeep_debt := 0 + for receipt: Dictionary in upkeep.get_report()[&"receipts"]: + if int(receipt[&"period"][&"period_end"]) <= coordinator.get_daily_close_minute(day): + upkeep_debt = 0 + for row: Dictionary in receipt[&"rows"]: + upkeep_debt += int(row[&"arrears"]) + if summary_settlement.has(&"upkeep_arrears_total"): + if typeof(summary_settlement[&"upkeep_arrears_total"]) != TYPE_INT or int(summary_settlement[&"upkeep_arrears_total"]) != upkeep_debt: + return false + summary_settlement.erase(&"upkeep_arrears_total") + elif upkeep_debt != 0: + return false + if int(summary_settlement.get(&"arrears_total", -1)) \ + != int(staff_record.get(&"unpaid_after_total", -2)) + upkeep_debt \ + or summary_settlement.get(&"replayed", null) != false: + return false + summary_settlement.erase(&"arrears_total") + summary_settlement.erase(&"replayed") + # OperationsCoordinator persists history in a transport-safe form, so + # StringName identifiers become String values after a round-trip. Compare + # the redundant Staff/Coordinator evidence in that same canonical form; + # numeric and structural types remain strict. + if not _strict_variants_equal( + _operations_snapshot_comparison_value(summary_settlement), + _operations_snapshot_comparison_value(staff_record) + ): + return false + + for entry: LedgerEntry in economy.get_entries(): + if entry.category == &"WAGE" \ + and not expected_wage_transactions.has(entry.transaction_id): + return false + return true + + +## Existing liabilities only. This projection does not issue financing or infer terms. +func get_loan_principal_report() -> Dictionary: + if not _initialized or economy_service == null or _state == null: + return {&"available": false, &"loans": []} + var cash := economy_service.get_balance(PLAYER_CASH_ACCOUNT) + var pending: Dictionary = {} + for command: SimulationCommand in command_queue.get_pending_commands(): + if command.command_type != &"repay_loan_principal": + continue + var id: Variant = command.payload.get(&"loan_id", null) + if typeof(id) not in [TYPE_STRING, TYPE_STRING_NAME]: + continue + var key := StringName(id) + if not pending.has(key): pending[key] = [] + pending[key].append(command.command_id) + var loans: Array[Dictionary] = [] + for account: Dictionary in economy_service.get_accounts(): + var id := String(account[&"account_id"]) + if not id.begins_with("LOAN_LIABILITY:"): + continue + var loan := StringName(id.trim_prefix("LOAN_LIABILITY:")) + loans.append({&"loan_id": loan, &"outstanding_principal": -economy_service.get_balance(StringName(id)), &"pending_command_ids": (pending.get(loan, []) as Array).duplicate()}) + loans.sort_custom(func(left: Dictionary, right: Dictionary) -> bool: return String(left[&"loan_id"]) < String(right[&"loan_id"])) + var valid: bool = _state.get_resource(&"gold") == cash and (loans.is_empty() or _loan_principal_ledger_is_valid(economy_service)) + var ended: bool = _operations_enabled and operations_coordinator != null and operations_coordinator.is_terminal() + return {&"available": true, &"ledger_valid": valid, &"terminal": ended, &"cash": cash, &"next_command_id": _next_command_id, &"loans": loans} + + +## Atomic principal payment against an already attested loan liability. +## Loan issuance, rates, due dates and bank eligibility belong to the bank +## producer; this API does not create or approve any financing contract. +func repay_loan_principal( + loan_id: StringName, + amount: Variant, + idempotency_key: StringName +) -> CommandResult: + if not _initialized or economy_service == null or _state == null: + return CommandResult.failed(CommandResult.Code.EXECUTOR_FAILED, 0, "Simulation runtime is not initialized.") + if _operations_enabled and operations_coordinator != null and operations_coordinator.is_terminal(): + return CommandResult.failed(CommandResult.Code.VALIDATION_FAILED, 0, "Repayment is unavailable after the game ends.") + if loan_id.is_empty() or idempotency_key.is_empty() or typeof(amount) != TYPE_INT \ + or amount <= 0 or amount > OperationsCoordinator.MAX_FINANCIAL_AMOUNT: + return CommandResult.failed(CommandResult.Code.INVALID_PAYLOAD, 0, "A loan id, positive integer principal and idempotency key are required.") + if not economy_service.get_transaction_id_for_idempotency_key(idempotency_key).is_empty(): + return CommandResult.failed(CommandResult.Code.DUPLICATE_IDEMPOTENCY_KEY, 0, "The principal payment key has already been posted.") + if not _loan_principal_ledger_is_valid(economy_service): + return CommandResult.failed(CommandResult.Code.VALIDATION_FAILED, 0, "The principal ledger is inconsistent.") + var liability := StringName("LOAN_LIABILITY:" + String(loan_id)) + if not economy_service.has_account(liability): + return CommandResult.failed(CommandResult.Code.NOT_FOUND, 0, "The loan liability does not exist.") + var remaining := -economy_service.get_balance(liability) + var cash := economy_service.get_balance(PLAYER_CASH_ACCOUNT) + if amount > remaining or amount > cash or _state.get_resource(&"gold") != cash: + return CommandResult.failed(CommandResult.Code.VALIDATION_FAILED, 0, "Principal payment exceeds the outstanding principal or available Gold, or its mirror is inconsistent.") + # The single balanced posting is atomic. Defer receipts until the Gold + # mirror is synchronized, also respecting any enclosing effect scope. + economy_service.begin_notification_scope() + var result := economy_service.transfer( + PLAYER_CASH_ACCOUNT, liability, amount, _runtime_event_minute(), + &"LOAN_PRINCIPAL_REPAID", loan_id, idempotency_key + ) + if result.is_success(): + _state.set_resource(&"gold", economy_service.get_balance(PLAYER_CASH_ACCOUNT)) + result.details[&"loan_id"] = loan_id + result.details[&"paid_principal"] = amount + result.details[&"outstanding_principal"] = remaining - amount + economy_service.end_notification_scope() + return result + + +func _loan_principal_ledger_is_valid(economy: EconomyService) -> bool: + if economy == null: + return false + var outstanding: Dictionary = {} + for entry: LedgerEntry in economy.get_entries(): + if entry.category not in [&"LOAN_PRINCIPAL_BORROWED", &"LOAN_PRINCIPAL_REPAID"]: + continue + if entry.source_entity_id.is_empty(): + return false + var liability := StringName("LOAN_LIABILITY:" + String(entry.source_entity_id)) + if not economy.has_account(liability) or not economy.account_allows_negative(liability): + return false + var remaining := int(outstanding.get(liability, 0)) + if entry.category == &"LOAN_PRINCIPAL_BORROWED": + if entry.debit_account != PLAYER_CASH_ACCOUNT or entry.credit_account != liability \ + or entry.amount > OperationsCoordinator.MAX_FINANCIAL_AMOUNT - remaining: + return false + remaining += entry.amount + else: + if entry.debit_account != liability or entry.credit_account != PLAYER_CASH_ACCOUNT \ + or entry.amount > remaining: + return false + remaining -= entry.amount + outstanding[liability] = remaining + for account: Dictionary in economy.get_accounts(): + var id: StringName = account[&"account_id"] + if String(id).begins_with("LOAN_LIABILITY:"): + if not economy.account_allows_negative(id) or economy.get_balance(id) != -int(outstanding.get(id, 0)): + return false + return true + + +func _operations_expected_category_totals( + economy: EconomyService, + period_start: int, + period_end_exclusive: int, + new_wages_total: int +) -> Variant: + if economy == null or period_start < 0 or period_end_exclusive <= period_start \ + or new_wages_total < 0: + return null + if not _loan_principal_ledger_is_valid(economy): + return null + var totals := { + &"revenue": 0, &"refunds": 0, &"cost_of_goods": 0, + &"wages": new_wages_total, &"maintenance": 0, &"utilities": 0, + &"taxes_and_interest": 0, &"fines": 0, + &"construction_cash_flow": 0, + } + var excluded: Array[StringName] = [ + &"EVENT_COST_RESERVED", &"EVENT_COST_RELEASED", &"INITIALIZATION", + &"TRANSFER", &"GENERATION", &"DESTRUCTION", &"MIGRATION", + &"SERVICE_CASH_SWEEP", &"WAGE", &"MAINTENANCE_PAYMENT", + ] + var mapping := { + &"SERVICE_SALE": &"revenue", &"REFUND": &"refunds", + &"CONSTRUCTION": &"construction_cash_flow", + &"CONSTRUCTION_REFUND": &"construction_cash_flow", + &"AUTHORITY_FINE": &"fines", &"OPERATING_EXPENSE": &"cost_of_goods", + &"STAFF_SIGNING_COST": &"cost_of_goods", &"EVENT_ACTION": &"cost_of_goods", + &"EVENT_COST_COMMITTED": &"cost_of_goods", + &"DEFENSE_SKILL_COST": &"cost_of_goods", + &"MAINTENANCE_ACCRUAL": &"maintenance", + &"LOAN_PRINCIPAL_BORROWED": &"financing_principal_inflows", + &"LOAN_PRINCIPAL_REPAID": &"financing_principal_outflows", + } + var operating_refunds := 0 + for entry: LedgerEntry in economy.get_entries(): + if entry.absolute_minute < period_start \ + or entry.absolute_minute >= period_end_exclusive: + continue + if entry.category in excluded: + continue + if entry.category == &"OPERATING_REFUND": + operating_refunds += entry.amount + continue + if not mapping.has(entry.category): + return null + var category: StringName = mapping[entry.category] + totals[category] = int(totals.get(category, 0)) + entry.amount + var offset := mini(operating_refunds, int(totals[&"cost_of_goods"])) + totals[&"cost_of_goods"] = int(totals[&"cost_of_goods"]) - offset + totals[&"revenue"] = int(totals[&"revenue"]) + operating_refunds - offset + return totals + + +func _operations_facility_wage_summary_is_valid( + summary: Dictionary, + staff_record: Dictionary, + _staff: StaffService, + economy: EconomyService +) -> bool: + var facilities_value: Variant = summary.get(&"facility_profit_loss", null) + var anomalies_value: Variant = summary.get(&"anomalies", null) + if not facilities_value is Dictionary or not anomalies_value is Array: + return false + var evidence: Dictionary = {} + var observed_unallocated: Dictionary = {} + for raw_anomaly: Variant in anomalies_value: + if not raw_anomaly is Dictionary: + return false + var anomaly: Dictionary = raw_anomaly + match StringName(anomaly.get(&"code", &"")): + &"WAGE_ALLOCATION_EVIDENCE": + if not evidence.is_empty() or not _dictionary_has_exact_canonical_keys( + anomaly, + [&"code", &"version", &"new_wages_total", &"allocated_total", + &"unallocated_total", &"allocations", &"unallocated_by_staff", + &"facility_ids"] + ): + return false + evidence = anomaly + &"UNALLOCATED_WAGES": + if not _dictionary_has_exact_canonical_keys( + anomaly, [&"code", &"staff_id", &"amount"] + ): + return false + var staff_id := StringName(anomaly.get(&"staff_id", &"")) + var amount_value: Variant = anomaly.get(&"amount", null) + if staff_id.is_empty() or typeof(amount_value) != TYPE_INT \ + or int(amount_value) <= 0 or observed_unallocated.has(staff_id): + return false + observed_unallocated[staff_id] = int(amount_value) + &"OPERATING_REFUND_EXCEEDS_PERIOD_COST": + if not _dictionary_has_exact_canonical_keys( + anomaly, [&"code", &"refund_total", &"cost_offset", &"excess_revenue"] + ): + return false + _: + return false + if evidence.is_empty() or int(evidence.get(&"version", -1)) != 1 \ + or typeof(evidence.get(&"new_wages_total", null)) != TYPE_INT \ + or int(evidence[&"new_wages_total"]) \ + != int(staff_record.get(&"new_wages_total", -1)) \ + or typeof(evidence.get(&"allocated_total", null)) != TYPE_INT \ + or typeof(evidence.get(&"unallocated_total", null)) != TYPE_INT \ + or not evidence.get(&"allocations", null) is Array \ + or not evidence.get(&"unallocated_by_staff", null) is Array \ + or not evidence.get(&"facility_ids", null) is Array: + return false + var allocations: Dictionary = {} + var previous_facility := "" + var allocated_total := 0 + for raw_row: Variant in evidence[&"allocations"]: + if not raw_row is Dictionary or not _dictionary_has_exact_canonical_keys( + raw_row, [&"facility_id", &"amount"] + ): + return false + var facility_id := StringName((raw_row as Dictionary).get(&"facility_id", &"")) + var amount_value: Variant = (raw_row as Dictionary).get(&"amount", null) + if facility_id.is_empty() or typeof(amount_value) != TYPE_INT \ + or int(amount_value) <= 0 or allocations.has(facility_id) \ + or (not previous_facility.is_empty() \ + and String(facility_id) <= previous_facility): + return false + allocations[facility_id] = int(amount_value) + allocated_total += int(amount_value) + previous_facility = String(facility_id) + var unallocated: Dictionary = {} + var previous_staff := "" + var unallocated_total := 0 + for raw_row: Variant in evidence[&"unallocated_by_staff"]: + if not raw_row is Dictionary or not _dictionary_has_exact_canonical_keys( + raw_row, [&"staff_id", &"amount"] + ): + return false + var staff_id := StringName((raw_row as Dictionary).get(&"staff_id", &"")) + var amount_value: Variant = (raw_row as Dictionary).get(&"amount", null) + if staff_id.is_empty() or typeof(amount_value) != TYPE_INT \ + or int(amount_value) <= 0 or unallocated.has(staff_id) \ + or (not previous_staff.is_empty() and String(staff_id) <= previous_staff): + return false + unallocated[staff_id] = int(amount_value) + unallocated_total += int(amount_value) + previous_staff = String(staff_id) + var expected_facility_ids: Dictionary = {} + var previous_id := "" + for raw_id: Variant in evidence[&"facility_ids"]: + if typeof(raw_id) not in [TYPE_STRING, TYPE_STRING_NAME]: + return false + var facility_id := StringName(raw_id) + if facility_id.is_empty() or expected_facility_ids.has(facility_id) \ + or (not previous_id.is_empty() and String(facility_id) <= previous_id): + return false + expected_facility_ids[facility_id] = true + previous_id = String(facility_id) + var new_wages_total := int(evidence[&"new_wages_total"]) + if allocated_total != int(evidence[&"allocated_total"]) \ + or unallocated_total != int(evidence[&"unallocated_total"]) \ + or allocated_total + unallocated_total != new_wages_total \ + or not _strict_variants_equal(observed_unallocated, unallocated): + return false + for facility_id: StringName in allocations: + if not expected_facility_ids.has(facility_id): + return false + var facility_rows: Dictionary = facilities_value + if facility_rows.size() != expected_facility_ids.size(): + return false + for raw_id: Variant in facility_rows: + if not expected_facility_ids.has(StringName(raw_id)): + return false + var period_start := int(summary.get(&"period_start_minute", -1)) + var period_end := int(summary.get(&"period_end_minute_exclusive", -1)) + if period_start < 0 or period_end <= period_start: + return false + var base_rows: Dictionary = {} + for facility_id: StringName in expected_facility_ids: + base_rows[facility_id] = {&"revenue": 0, &"expenses": 0} + var excluded: Array[StringName] = [ + &"EVENT_COST_RESERVED", &"EVENT_COST_RELEASED", &"INITIALIZATION", + &"TRANSFER", &"GENERATION", &"DESTRUCTION", &"MIGRATION", + &"SERVICE_CASH_SWEEP", &"WAGE", &"MAINTENANCE_PAYMENT", &"OPERATING_REFUND", + ] + var mapping := { + &"SERVICE_SALE": &"revenue", &"REFUND": &"expenses", + &"AUTHORITY_FINE": &"expenses", &"OPERATING_EXPENSE": &"expenses", + &"STAFF_SIGNING_COST": &"expenses", &"EVENT_ACTION": &"expenses", + &"EVENT_COST_COMMITTED": &"expenses", &"DEFENSE_SKILL_COST": &"expenses", + &"MAINTENANCE_ACCRUAL": &"expenses", + } + for entry: LedgerEntry in economy.get_entries(): + if entry.absolute_minute < period_start or entry.absolute_minute >= period_end \ + or entry.category in excluded \ + or entry.category in [&"CONSTRUCTION", &"CONSTRUCTION_REFUND"] \ + or not expected_facility_ids.has(entry.source_entity_id): + continue + if not mapping.has(entry.category): + return false + var base: Dictionary = base_rows[entry.source_entity_id] + var key: StringName = mapping[entry.category] + base[key] = int(base[key]) + entry.amount + for facility_id: StringName in expected_facility_ids: + var row_value: Variant = facility_rows.get(facility_id, null) + if not row_value is Dictionary: + return false + var row: Dictionary = row_value + var base: Dictionary = base_rows[facility_id] + var expected_revenue := int(base[&"revenue"]) + var expected_expenses := int(base[&"expenses"]) \ + + int(allocations.get(facility_id, 0)) + if int(row.get(&"revenue", -1)) != expected_revenue \ + or int(row.get(&"expenses", -1)) != expected_expenses \ + or int(row.get(&"net_profit", 0)) != expected_revenue - expected_expenses: + return false + return true + + +func _operations_facility_wage_summary_from_live_shifts_is_valid( + summary: Dictionary, + staff_record: Dictionary, + staff: StaffService, + economy: EconomyService +) -> bool: + var facilities_value: Variant = summary.get(&"facility_profit_loss", null) + var rows_value: Variant = staff_record.get(&"rows", null) + var anomalies_value: Variant = summary.get(&"anomalies", null) + if not facilities_value is Dictionary or not rows_value is Array \ + or not anomalies_value is Array: + return false + var facility_rows: Dictionary = facilities_value + var period_start := int(summary.get(&"period_start_minute", -1)) + var period_end := int(summary.get(&"period_end_minute_exclusive", -1)) + if period_start < 0 or period_end <= period_start: + return false + var wages_by_staff: Dictionary = {} + for raw_row: Variant in rows_value: + if not raw_row is Dictionary: + return false + var row: Dictionary = raw_row + var staff_id := StringName(row.get(&"staff_id", &"")) + var wage_value: Variant = row.get(&"new_wages", null) + if staff_id.is_empty() or typeof(wage_value) != TYPE_INT \ + or int(wage_value) < 0 or wages_by_staff.has(staff_id): + return false + wages_by_staff[staff_id] = int(wage_value) + var weights_by_staff: Dictionary = {} + for shift: ShiftInstance in staff.get_shifts(): + if shift == null or shift.cancelled or shift.assigned_facility_id.is_empty() \ + or not wages_by_staff.has(shift.staff_id) \ + or int(wages_by_staff[shift.staff_id]) <= 0: + continue + var overlap := maxi( + mini(shift.end_at, period_end) - maxi(shift.start_at, period_start), 0 + ) + if overlap <= 0: + continue + var recorded := shift.worked_minutes + shift.overtime_minutes + var weight := mini(recorded, overlap) if recorded > 0 else overlap + if weight <= 0: + continue + if not weights_by_staff.has(shift.staff_id): + weights_by_staff[shift.staff_id] = {} + var weights: Dictionary = weights_by_staff[shift.staff_id] + weights[shift.assigned_facility_id] = int( + weights.get(shift.assigned_facility_id, 0) + ) + weight + var expected_allocations: Dictionary = {} + var expected_unallocated: Dictionary = {} + for staff_id: StringName in _sorted_name_keys(wages_by_staff): + var wage := int(wages_by_staff[staff_id]) + if wage <= 0: + continue + var weights: Dictionary = weights_by_staff.get(staff_id, {}) + if weights.is_empty(): + expected_unallocated[staff_id] = wage + continue + var facility_ids := _sorted_name_keys(weights) + var total_weight := 0 + for facility_id: StringName in facility_ids: + total_weight += int(weights[facility_id]) + if total_weight <= 0: + return false + var allocated := 0 + for facility_id: StringName in facility_ids: + var amount := wage * int(weights[facility_id]) / total_weight + expected_allocations[facility_id] = int( + expected_allocations.get(facility_id, 0) + ) + amount + allocated += amount + var remainder := wage - allocated + for index in range(remainder): + var facility_id: StringName = facility_ids[index] + expected_allocations[facility_id] = int(expected_allocations[facility_id]) + 1 + + var base_rows: Dictionary = {} + var excluded: Array[StringName] = [ + &"EVENT_COST_RESERVED", &"EVENT_COST_RELEASED", &"INITIALIZATION", + &"TRANSFER", &"GENERATION", &"DESTRUCTION", &"MIGRATION", + &"SERVICE_CASH_SWEEP", &"WAGE", &"MAINTENANCE_PAYMENT", &"OPERATING_REFUND", + ] + var mapping := { + &"SERVICE_SALE": &"revenue", &"REFUND": &"expense", + &"AUTHORITY_FINE": &"expense", &"OPERATING_EXPENSE": &"expense", + &"STAFF_SIGNING_COST": &"expense", &"EVENT_ACTION": &"expense", + &"EVENT_COST_COMMITTED": &"expense", &"DEFENSE_SKILL_COST": &"expense", + } + for entry: LedgerEntry in economy.get_entries(): + if entry.absolute_minute < period_start or entry.absolute_minute >= period_end \ + or entry.category in excluded \ + or entry.category in [&"CONSTRUCTION", &"CONSTRUCTION_REFUND"] \ + or entry.source_entity_id.is_empty() \ + or not facility_rows.has(entry.source_entity_id): + continue + if not mapping.has(entry.category): + return false + if not base_rows.has(entry.source_entity_id): + base_rows[entry.source_entity_id] = {&"revenue": 0, &"expenses": 0} + var base: Dictionary = base_rows[entry.source_entity_id] + var key: StringName = mapping[entry.category] + if key == &"revenue": + base[&"revenue"] = int(base[&"revenue"]) + entry.amount + else: + base[&"expenses"] = int(base[&"expenses"]) + entry.amount + for raw_facility_id: Variant in facility_rows: + var facility_id := StringName(raw_facility_id) + var row_value: Variant = facility_rows[raw_facility_id] + if facility_id.is_empty() or not row_value is Dictionary: + return false + var row: Dictionary = row_value + var base: Dictionary = base_rows.get( + facility_id, {&"revenue": 0, &"expenses": 0} + ) + if int(row.get(&"revenue", -1)) != int(base[&"revenue"]) \ + or int(row.get(&"expenses", -1)) \ + != int(base[&"expenses"]) + int(expected_allocations.get(facility_id, 0)): + return false + for facility_id: StringName in expected_allocations: + if not facility_rows.has(facility_id): + return false + var observed_unallocated: Dictionary = {} + for raw_anomaly: Variant in anomalies_value: + if not raw_anomaly is Dictionary: + return false + var anomaly: Dictionary = raw_anomaly + if StringName(anomaly.get(&"code", &"")) != &"UNALLOCATED_WAGES": + continue + var staff_id := StringName(anomaly.get(&"staff_id", &"")) + var amount_value: Variant = anomaly.get(&"amount", null) + if staff_id.is_empty() or typeof(amount_value) != TYPE_INT \ + or int(amount_value) <= 0 or observed_unallocated.has(staff_id): + return false + observed_unallocated[staff_id] = int(amount_value) + return _strict_variants_equal(observed_unallocated, expected_unallocated) + + +func _operations_coordinator_configuration_is_canonical( + coordinator: OperationsCoordinator, + expected_world_seed: int +) -> bool: + if coordinator == null: + return false + var expected := OperationsCoordinator.new({ + &"start_minute": OPERATIONS_COORDINATOR_ORIGIN_MINUTE, + &"world_seed": expected_world_seed, + }) + if not _strict_variants_equal(coordinator.get_config(), expected.get_config()): + return false + var profiles := coordinator.get_demand_profiles() + if profiles.size() != DEFAULT_OPERATIONS_DEMAND_PROFILES.size(): + return false + var expected_identities: Dictionary = {} + for raw_profile: Variant in DEFAULT_OPERATIONS_DEMAND_PROFILES: + var profile: Dictionary = raw_profile + var key := "%s|%s|%s" % [ + profile[&"facility_tag"], profile[&"guest_class"], profile[&"time_band"], + ] + expected_identities[key] = true + var seen: Dictionary = {} + var expected_components := { + &"base": 100, &"reputation": 0, &"unmet_need": 0, + &"event": 0, &"recent_price_penalty": 0, &"wait_penalty": 0, + } + for profile: Dictionary in profiles: + var key := "%s|%s|%s" % [ + profile.get(&"facility_tag", &""), + profile.get(&"guest_class", &""), + profile.get(&"time_band", &""), + ] + if not expected_identities.has(key) or seen.has(key) \ + or not _strict_variants_equal( + profile.get(&"components", {}), expected_components + ): + return false + seen[key] = true + return seen.size() == expected_identities.size() + + +func _staff_payroll_arrears_total(service: StaffService) -> int: + var total := 0 + for row: Dictionary in service.get_payroll_obligations(): + var unpaid: Variant = row.get(&"unpaid_wages", null) + if typeof(unpaid) != TYPE_INT or int(unpaid) < 0: + return -1 + total += int(unpaid) + return total + + +func _operations_failure_report_is_valid(report: Dictionary, minute: int) -> bool: + if report.size() == 2 \ + and _dictionary_has_exact_canonical_keys(report, [&"code", &"minute"]): + var simple_code: Variant = report.get(&"code", null) + var simple_minute: Variant = report.get(&"minute", null) + return typeof(simple_code) in [TYPE_STRING, TYPE_STRING_NAME] \ + and not String(simple_code).is_empty() \ + and typeof(simple_minute) == TYPE_INT and int(simple_minute) == minute + var result_keys: Array[StringName] = [ + &"code", &"command_id", &"target_id", &"message", + &"resolution_hints", &"details", &"is_replay", + ] + if not _dictionary_has_exact_canonical_keys(report, result_keys): + return false + var code_value: Variant = report.get(&"code", null) + var command_id_value: Variant = report.get(&"command_id", null) + var target_value: Variant = report.get(&"target_id", null) + var message_value: Variant = report.get(&"message", null) + var hints_value: Variant = report.get(&"resolution_hints", null) + var details_value: Variant = report.get(&"details", null) + if typeof(code_value) not in [TYPE_STRING, TYPE_STRING_NAME] \ + or String(code_value).is_empty() or String(code_value) == "OK" \ + or typeof(command_id_value) != TYPE_INT or int(command_id_value) < 0 \ + or typeof(target_value) not in [TYPE_STRING, TYPE_STRING_NAME] \ + or typeof(message_value) != TYPE_STRING \ + or not hints_value is Array or hints_value.size() > 32 \ + or not details_value is Dictionary \ + or typeof(report.get(&"is_replay", null)) != TYPE_BOOL: + return false + for raw_hint: Variant in hints_value: + if typeof(raw_hint) != TYPE_STRING or String(raw_hint).length() > 4_096: + return false + return JSON.stringify(details_value).length() <= 262_144 + + +func _dictionary_has_exact_canonical_keys( + source: Dictionary, + expected_keys: Array[StringName] +) -> bool: + if source.size() != expected_keys.size(): + return false + var seen: Dictionary = {} + for raw_key: Variant in source: + if typeof(raw_key) not in [TYPE_STRING, TYPE_STRING_NAME]: + return false + var key := StringName(raw_key) + if not expected_keys.has(key) or seen.has(key): + return false + seen[key] = true + return seen.size() == expected_keys.size() + + +func _operations_api_failure( + code: StringName, + message: String, + details: Dictionary = {} +) -> Dictionary: + return { + &"ok": false, + &"code": code, + &"message": message, + &"details": details.duplicate(true), + } + + +func register_save_migration_step( + from_version: int, + migration_id: StringName, + migrator: Callable +) -> Dictionary: + return save_migration_registry.register_step(from_version, migration_id, migrator) + + +func queue_checkpoint( + save_path := "", + metadata := {}, + use_autosave := false, + reason: StringName = &"manual" +) -> Dictionary: + if not _initialized: + return { + &"ok": false, + &"code": &"RUNTIME_NOT_INITIALIZED", + &"message": "Simulation runtime is not initialized.", + } + var resolved_path := str(save_path) + if resolved_path.is_empty(): + resolved_path = str(_save_policy[&"autosave_path"]) + var request_id := _next_checkpoint_request_id + _next_checkpoint_request_id += 1 + _pending_checkpoints.append({ + &"request_id": request_id, + &"save_path": resolved_path, + &"metadata": metadata.duplicate(true) if metadata is Dictionary else {}, + &"use_autosave": bool(use_autosave), + &"reason": reason if not reason.is_empty() else &"manual", + }) + return { + &"ok": true, + &"code": &"CHECKPOINT_QUEUED", + &"request_id": request_id, + &"save_path": resolved_path, + &"queued_for_tick_boundary": true, + &"queued_at_absolute_minute": _clock.absolute_minute, + } + + +func queue_manual_checkpoint(save_path: String, metadata := {}) -> Dictionary: + return queue_checkpoint(save_path, metadata, false, &"manual") + + +func save_to_path(save_path: String, metadata := {}) -> Dictionary: + return _perform_checkpoint_save(save_path, metadata, false, &"manual", 0) + + +func inspect_recovery_candidates( + save_path := "", + generation_count := -1 +) -> Dictionary: + var resolved_path := str(save_path) + if resolved_path.is_empty(): + resolved_path = str(_save_policy[&"autosave_path"]) + var resolved_generations := generation_count + if resolved_generations < 0: + resolved_generations = int(_save_policy[&"autosave_generations"]) + var started_at_us := Time.get_ticks_usec() + var result := SaveService.inspect_recovery_candidates( + resolved_path, + resolved_generations + ) + var runtime_duration_us := maxi(int(Time.get_ticks_usec() - started_at_us), 0) + _record_runtime_performance(&"load", runtime_duration_us) + return _with_operation_context(result, { + &"operation": &"inspect_recovery", + &"save_path": resolved_path, + &"runtime_duration_us": runtime_duration_us, + }) + + +func load_from_path(save_path: String) -> Dictionary: + var started_at_us := Time.get_ticks_usec() + var previous_paused := _clock.paused + _clock.set_paused(true) + var result := SaveService.load_snapshot(save_path) + if not result[&"ok"] and result[&"code"] == &"UNSUPPORTED_FORMAT" \ + and bool(_save_policy.get(&"auto_migrate", true)): + var migration := SaveService.migrate_save( + save_path, + save_migration_registry, + SaveService.FORMAT_VERSION + ) + _record_runtime_performance(&"save", int(migration.get(&"duration_us", 0))) + if migration[&"ok"]: + result = SaveService.load_snapshot(save_path) + else: + result = _with_operation_context(migration, { + &"operation": &"load", + &"migration_attempted": true, + }) + return _finish_load_operation( + result, + save_path, + previous_paused, + started_at_us, + &"load", + &"SAVE_LOADED", + &"SAVE_LOAD_FAILED" + ) + + +func load_with_recovery( + save_path := "", + generation_count := -1 +) -> Dictionary: + var resolved_path := str(save_path) + if resolved_path.is_empty(): + resolved_path = str(_save_policy[&"autosave_path"]) + var resolved_generations := generation_count + if resolved_generations < 0: + resolved_generations = int(_save_policy[&"autosave_generations"]) + var started_at_us := Time.get_ticks_usec() + var previous_paused := _clock.paused + # Recovery is a two-stage decision. SaveService establishes package, + # checksum, format, and generic snapshot validity; Runtime then tries every + # package-loadable candidate in the same stable order against authoritative + # domain and cross-link rules. Failed domain attempts never reach the commit + # boundary in _restore_snapshot_internal. + var live_snapshot_before := create_snapshot() + _clock.set_paused(true) + var package_inventory := SaveService.inspect_recovery_candidates( + resolved_path, + resolved_generations + ) + var result := _attempt_domain_valid_recovery( + resolved_path, + package_inventory + ) + return _finish_recovery_operation( + result, + resolved_path, + previous_paused, + started_at_us, + live_snapshot_before + ) + + +func _attempt_domain_valid_recovery( + save_path: String, + package_inventory: Dictionary +) -> Dictionary: + var package_report := package_inventory.duplicate(true) + # The selected package snapshot is an implementation detail of the package + # inspection pass. Every actual attempt is loaded again through the explicit, + # read-only candidate API so the final selection cannot accidentally reuse a + # different candidate's payload. + package_report.erase(&"snapshot") + var package_recovery_report: Dictionary = {} + var package_recovery_value: Variant = package_inventory.get(&"recovery_report", {}) + if package_recovery_value is Dictionary: + package_recovery_report = package_recovery_value.duplicate(true) + var raw_candidates_value: Variant = package_inventory.get(&"candidates", []) + var candidates: Array[Dictionary] = [] + if raw_candidates_value is Array: + for raw_candidate: Variant in raw_candidates_value: + if not raw_candidate is Dictionary: + continue + var candidate: Dictionary = raw_candidate.duplicate(true) + var package_loadable := bool(candidate.get(&"valid", false)) + candidate[&"package_loadable"] = package_loadable + candidate[&"package_status"] = &"VALID" if package_loadable else &"REJECTED" + candidate[&"package_rejected"] = not package_loadable + candidate[&"package_rejection_code"] = ( + &"" if package_loadable else StringName(candidate.get(&"code", &"PACKAGE_REJECTED")) + ) + candidate[&"domain_status"] = &"NOT_ATTEMPTED" + candidate[&"domain_attempted"] = false + candidate[&"domain_valid"] = false + candidate[&"domain_rejected"] = false + candidate[&"domain_rejection_code"] = &"" + candidate[&"failed_attempt_live_state_preserved"] = true + candidate[&"selected"] = false + candidates.append(candidate) + + if not bool(package_inventory.get(&"ok", false)) \ + and not _has_package_loadable_recovery_candidate(candidates): + return _recovery_failure_result( + StringName(package_inventory.get(&"code", &"NO_VALID_RECOVERY_CANDIDATE")), + str(package_inventory.get(&"message", save_path)), + candidates, + package_report, + package_recovery_report, + &"NO_PACKAGE_VALID_RECOVERY_CANDIDATE" + ) + + for candidate_index in range(candidates.size()): + var candidate: Dictionary = candidates[candidate_index] + if not bool(candidate[&"package_loadable"]): + continue + var candidate_id := StringName(candidate.get(&"id", &"")) + var generation := int(candidate.get(&"generation", -1)) + var loaded := SaveService.load_recovery_candidate( + save_path, + candidate_id, + generation + ) + if not bool(loaded.get(&"ok", false)): + # A candidate may disappear or become corrupt between inventory and + # its explicit load. Treat that race as a package rejection and keep + # walking the already-established priority order. + candidate[&"package_status"] = &"REJECTED" + candidate[&"package_rejected"] = true + candidate[&"package_load_succeeded"] = false + candidate[&"package_rejection_code"] = StringName( + loaded.get(&"code", &"PACKAGE_LOAD_FAILED") + ) + candidate[&"package_rejection_message"] = str(loaded.get(&"message", "")) + candidates[candidate_index] = candidate + continue + + candidate[&"package_load_succeeded"] = true + candidate[&"domain_attempted"] = true + var package_attempt_report: Dictionary = {} + var loaded_report_value: Variant = loaded.get(&"recovery_report", {}) + if loaded_report_value is Dictionary: + package_attempt_report = loaded_report_value.duplicate(true) + var attempt_live_guard := create_snapshot() + var attempt_pause_guard := _clock.paused + if not _restore_snapshot_internal(loaded[&"snapshot"], true): + candidate[&"domain_status"] = &"REJECTED" + candidate[&"domain_rejected"] = true + candidate[&"domain_rejection_code"] = &"DOMAIN_OR_CROSSLINK_VALIDATION_FAILED" + candidate[&"failed_attempt_live_state_preserved"] = ( + create_snapshot() == attempt_live_guard + and _clock.paused == attempt_pause_guard + ) + candidates[candidate_index] = candidate + continue + + candidate[&"domain_status"] = &"VALID" + candidate[&"domain_valid"] = true + candidate[&"selected"] = true + candidates[candidate_index] = candidate + var result := loaded.duplicate(true) + result[&"ok"] = true + result[&"code"] = &"OK" + result[&"selection_rule"] = package_inventory.get( + &"selection_rule", + &"FORMAL_BACKUP_MIGRATION_AUTOSAVE_NEWEST_FIRST" + ) + result[&"selected"] = candidate.duplicate(true) + result[&"candidates"] = candidates.duplicate(true) + result[&"candidate_attempts"] = candidates.duplicate(true) + result[&"package_candidate_report"] = package_report + result[&"package_recovery_report"] = package_attempt_report + result[&"restore_safety_report"] = latest_restore_safety_report.duplicate(true) + result[&"recovery_report"] = latest_restore_safety_report.duplicate(true) + result[&"recovery_report"][&"restore_committed"] = true + result[&"recovery_report"][&"selected_candidate_id"] = candidate_id + result[&"recovery_report"][&"package_rejections"] = _recovery_package_rejections(candidates) + result[&"recovery_report"][&"domain_rejections"] = _recovery_domain_rejections(candidates) + return result + + return _recovery_failure_result( + &"RESTORE_FAILED", + save_path, + candidates, + package_report, + package_recovery_report, + &"NO_DOMAIN_VALID_RECOVERY_CANDIDATE" + ) + + +func _finish_recovery_operation( + loaded_result: Dictionary, + save_path: String, + previous_paused: bool, + started_at_us: int, + live_snapshot_before: Dictionary +) -> Dictionary: + var result := loaded_result.duplicate(true) + if not bool(result.get(&"ok", false)): + _clock.set_paused(previous_paused) + var runtime_duration_us := maxi(int(Time.get_ticks_usec() - started_at_us), 0) + _record_runtime_performance(&"load", runtime_duration_us) + var live_state_preserved := true + if not bool(result.get(&"ok", false)): + live_state_preserved = create_snapshot() == live_snapshot_before + result = _with_operation_context(result, { + &"operation": &"recovery", + &"save_path": save_path, + &"runtime_duration_us": runtime_duration_us, + &"loaded_clock_paused": _clock.paused if result.get(&"ok", false) else previous_paused, + &"preload_pause_restored": true if result.get(&"ok", false) else _clock.paused == previous_paused, + &"live_state_preserved": live_state_preserved, + }) + if bool(result.get(&"ok", false)): + _complete_save_operation(result, &"SAVE_RECOVERY_LOADED") + else: + # A failed recovery is an observational operation. Publishing a persisted + # DomainEvent here would itself violate the promised all-live-state atomicity. + latest_save_operation_result = _save_event_payload(result) + save_operation_completed.emit(result.duplicate(true)) + return result + + +func _has_package_loadable_recovery_candidate(candidates: Array[Dictionary]) -> bool: + for candidate: Dictionary in candidates: + if bool(candidate.get(&"package_loadable", false)): + return true + return false + + +func _recovery_package_rejections(candidates: Array[Dictionary]) -> Array[Dictionary]: + var rejected: Array[Dictionary] = [] + for candidate: Dictionary in candidates: + if not bool(candidate.get(&"package_rejected", false)): + continue + rejected.append({ + &"id": StringName(candidate.get(&"id", &"")), + &"code": StringName(candidate.get(&"package_rejection_code", &"PACKAGE_REJECTED")), + }) + return rejected + + +func _recovery_domain_rejections(candidates: Array[Dictionary]) -> Array[Dictionary]: + var rejected: Array[Dictionary] = [] + for candidate: Dictionary in candidates: + if not bool(candidate.get(&"domain_rejected", false)): + continue + rejected.append({ + &"id": StringName(candidate.get(&"id", &"")), + &"code": StringName(candidate.get( + &"domain_rejection_code", + &"DOMAIN_OR_CROSSLINK_VALIDATION_FAILED" + )), + }) + return rejected + + +func _recovery_failure_result( + code: StringName, + message: String, + candidates: Array[Dictionary], + package_report: Dictionary, + package_recovery_report: Dictionary, + rejection_reason: StringName +) -> Dictionary: + var restore_safety_report := { + &"report_version": 2, + &"recovery_mode": true, + &"repair_policy": &"WHITELISTED_AUTHORITATIVE_ONLY", + &"automatic_repairs_applied": false, + &"restore_committed": false, + &"rejection_reason": rejection_reason, + &"package_rejections": _recovery_package_rejections(candidates), + &"domain_rejections": _recovery_domain_rejections(candidates), + &"forbidden_repairs_applied": false, + } + return { + &"ok": false, + &"code": code, + &"message": message, + &"selection_rule": package_report.get( + &"selection_rule", + &"FORMAL_BACKUP_MIGRATION_AUTOSAVE_NEWEST_FIRST" + ), + &"selected": {}, + &"candidates": candidates.duplicate(true), + &"candidate_attempts": candidates.duplicate(true), + &"package_candidate_report": package_report.duplicate(true), + &"package_recovery_report": package_recovery_report.duplicate(true), + &"restore_safety_report": restore_safety_report.duplicate(true), + &"recovery_report": restore_safety_report, + } + + +func load_recovery_candidate( + save_path: String, + candidate_id: StringName, + generation := -1 +) -> Dictionary: + var started_at_us := Time.get_ticks_usec() + var previous_paused := _clock.paused + _clock.set_paused(true) + var result := SaveService.load_recovery_candidate(save_path, candidate_id, generation) + return _finish_load_operation( + result, + save_path, + previous_paused, + started_at_us, + &"recovery_candidate", + &"SAVE_RECOVERY_CANDIDATE_LOADED", + &"SAVE_RECOVERY_FAILED" + ) + + +func migrate_save_path( + save_path: String, + target_version := SaveService.FORMAT_VERSION +) -> Dictionary: + var started_at_us := Time.get_ticks_usec() + var previous_paused := _clock.paused + _clock.set_paused(true) + var result := SaveService.migrate_save(save_path, save_migration_registry, target_version) + _clock.set_paused(previous_paused) + var runtime_duration_us := maxi(int(Time.get_ticks_usec() - started_at_us), 0) + _record_runtime_performance(&"save", runtime_duration_us) + result = _with_operation_context(result, { + &"operation": &"migration", + &"save_path": save_path, + &"runtime_duration_us": runtime_duration_us, + &"clock_pause_restored": _clock.paused == previous_paused, + }) + _complete_save_operation( + result, + &"SAVE_MIGRATION_SUCCEEDED" if result[&"ok"] else &"SAVE_MIGRATION_FAILED" + ) + return result + + +func _process_save_checkpoints(absolute_minute: int) -> void: + if _operations_tick_blocked: + return + if bool(_save_policy.get(&"enabled", false)) \ + and bool(_save_policy.get(&"daily_checkpoint_enabled", true)) \ + and posmod(absolute_minute, SimulationKernel.MINUTES_PER_DAY) == SimulationKernel.START_ABSOLUTE_MINUTE: + var checkpoint_day := absolute_minute / SimulationKernel.MINUTES_PER_DAY + 1 + if checkpoint_day != _last_daily_checkpoint_day: + var previous_checkpoint_day := _last_daily_checkpoint_day + _last_daily_checkpoint_day = checkpoint_day + var daily_result := _perform_checkpoint_save( + str(_save_policy[&"autosave_path"]), + { + &"display_name": "Daily Autosave", + &"checkpoint_day": checkpoint_day, + &"checkpoint_absolute_minute": absolute_minute, + }, + true, + &"daily_0600", + 0 + ) + if not daily_result[&"ok"]: + _last_daily_checkpoint_day = previous_checkpoint_day + + if _pending_checkpoints.is_empty(): + return + var requests := _pending_checkpoints.duplicate(true) + _pending_checkpoints.clear() + for request_value: Variant in requests: + if not request_value is Dictionary: + continue + var request: Dictionary = request_value + _perform_checkpoint_save( + str(request[&"save_path"]), + request[&"metadata"], + bool(request[&"use_autosave"]), + StringName(request[&"reason"]), + int(request[&"request_id"]) + ) + + +func _perform_checkpoint_save( + save_path: String, + metadata: Variant, + use_autosave: bool, + reason: StringName, + request_id: int +) -> Dictionary: + if not _initialized or _clock == null: + return { + &"ok": false, + &"code": &"RUNTIME_NOT_INITIALIZED", + &"message": "Simulation runtime is not initialized.", + } + var started_at_us := Time.get_ticks_usec() + # A rejected Path projection is not a complete Runtime save. Refuse before + # pausing or touching formal/tmp/backup files, and keep the refusal observable + # without publishing a persisted event into the live snapshot. + if path_service.create_snapshot().is_empty(): + var refused: Dictionary = { + &"ok": false, &"code": &"INVALID_SNAPSHOT", + &"message": "Incomplete Path snapshot; original save preserved.", + &"operation": &"autosave" if use_autosave else &"save", + &"save_path": save_path, &"reason": reason, &"request_id": request_id, + &"checkpoint_absolute_minute": _clock.absolute_minute, + &"runtime_duration_us": maxi(int(Time.get_ticks_usec() - started_at_us), 0), + &"clock_pause_restored": true, &"formal_save_preserved": true, + &"live_state_preserved": true, + } + latest_save_operation_result = _save_event_payload(refused) + save_operation_completed.emit(refused.duplicate(true)) + return refused + var previous_paused := _clock.paused + _clock.set_paused(true) + var snapshot := create_snapshot() + # Persistence pauses I/O, not game semantics. The package must retain the + # state that should resume after load rather than the temporary I/O pause. + snapshot[&"clock"][&"paused"] = previous_paused + var effective_metadata: Dictionary = metadata.duplicate(true) if metadata is Dictionary else {} + var presentation: Dictionary = {} + checkpoint_presentation_requested.emit(presentation) + if not effective_metadata.has(&"interface_selection") and presentation.get(&"interface_selection") is Dictionary: + effective_metadata[&"interface_selection"] = presentation[&"interface_selection"].duplicate(true) + effective_metadata[&"checkpoint_reason"] = reason + var result := ( + SaveService.save_autosave( + save_path, + snapshot, + effective_metadata, + int(_save_policy[&"autosave_generations"]) + ) + if use_autosave + else SaveService.save_snapshot(save_path, snapshot, effective_metadata) + ) + _clock.set_paused(previous_paused) + var runtime_duration_us := maxi(int(Time.get_ticks_usec() - started_at_us), 0) + _record_runtime_performance(&"save", runtime_duration_us) + result = _with_operation_context(result, { + &"operation": &"autosave" if use_autosave else &"save", + &"save_path": save_path, + &"reason": reason, + &"request_id": request_id, + &"checkpoint_absolute_minute": int(snapshot[&"clock"][&"absolute_minute"]), + &"runtime_duration_us": runtime_duration_us, + &"clock_pause_restored": _clock.paused == previous_paused, + }) + var event_type := ( + &"AUTOSAVE_CHECKPOINT_SUCCEEDED" + if use_autosave and result[&"ok"] + else &"AUTOSAVE_CHECKPOINT_FAILED" + if use_autosave + else &"SAVE_CHECKPOINT_SUCCEEDED" + if result[&"ok"] + else &"SAVE_CHECKPOINT_FAILED" + ) + _complete_save_operation(result, event_type) + return result + + +func _finish_load_operation( + loaded_result: Dictionary, + save_path: String, + previous_paused: bool, + started_at_us: int, + operation: StringName, + success_event_type: StringName, + failure_event_type: StringName +) -> Dictionary: + var result := loaded_result.duplicate(true) + var recovery_mode := operation in [&"recovery", &"recovery_candidate"] + var package_recovery_report: Variant = result.get(&"recovery_report", null) + if result[&"ok"] and not _restore_snapshot_internal(result[&"snapshot"], recovery_mode): + result[&"ok"] = false + result[&"source_code"] = result.get(&"code", &"OK") + result[&"code"] = &"RESTORE_FAILED" + result[&"message"] = save_path + result[&"recovery_report"] = { + &"report_version": 1, + &"recovery_mode": recovery_mode, + &"repair_policy": &"WHITELISTED_AUTHORITATIVE_ONLY", + &"automatic_repairs_applied": false, + &"restore_committed": false, + &"rejection_reason": &"DOMAIN_OR_CROSSLINK_VALIDATION_FAILED", + &"forbidden_repairs_applied": false, + } + elif result[&"ok"]: + result[&"restore_safety_report"] = latest_restore_safety_report.duplicate(true) + if recovery_mode: + result[&"recovery_report"] = latest_restore_safety_report.duplicate(true) + result[&"recovery_report"][&"restore_committed"] = true + if package_recovery_report is Dictionary: + result[&"package_recovery_report"] = package_recovery_report.duplicate(true) + if not result[&"ok"]: + _clock.set_paused(previous_paused) + var runtime_duration_us := maxi(int(Time.get_ticks_usec() - started_at_us), 0) + _record_runtime_performance(&"load", runtime_duration_us) + result = _with_operation_context(result, { + &"operation": operation, + &"save_path": save_path, + &"runtime_duration_us": runtime_duration_us, + &"loaded_clock_paused": _clock.paused if result[&"ok"] else previous_paused, + &"preload_pause_restored": true if result[&"ok"] else _clock.paused == previous_paused, + }) + _complete_save_operation( + result, + success_event_type if result[&"ok"] else failure_event_type + ) + return result + + +func _complete_save_operation(result: Dictionary, event_type: StringName) -> void: + latest_save_operation_result = _save_event_payload(result) + _publish_actor_event( + event_type, + &"save_service", + [], + latest_save_operation_result + ) + save_operation_completed.emit(result.duplicate(true)) + + +func _save_event_payload(result: Dictionary) -> Dictionary: + var payload := { + &"ok": bool(result.get(&"ok", false)), + &"code": StringName(result.get(&"code", &"UNKNOWN")), + &"message": str(result.get(&"message", "")), + &"operation": StringName(result.get(&"operation", &"unknown")), + &"save_path": str(result.get(&"save_path", result.get(&"path", ""))), + &"reason": StringName(result.get(&"reason", &"")), + &"request_id": int(result.get(&"request_id", 0)), + &"checkpoint_absolute_minute": int(result.get(&"checkpoint_absolute_minute", _clock.absolute_minute)), + &"duration_us": int(result.get(&"duration_us", 0)), + &"runtime_duration_us": int(result.get(&"runtime_duration_us", 0)), + } + var selected_value: Variant = result.get(&"selected", null) + if selected_value is Dictionary: + payload[&"selected_candidate_id"] = StringName(selected_value.get(&"id", &"")) + payload[&"selected_candidate_kind"] = StringName(selected_value.get(&"kind", &"")) + payload[&"selected_generation"] = int(selected_value.get(&"generation", -1)) + var manifest_value: Variant = result.get(&"manifest", null) + if manifest_value is Dictionary: + payload[&"save_id"] = str(manifest_value.get("save_id", "")) + payload[&"saved_absolute_minute"] = int(manifest_value.get("absolute_minute", -1)) + return payload + + +func _with_operation_context(result: Dictionary, context: Dictionary) -> Dictionary: + var enriched := result.duplicate(true) + for key: Variant in context: + enriched[key] = context[key] + return enriched + + +func _record_runtime_performance(metric_id: StringName, duration_us: int) -> void: + if _clock != null and _clock.performance_budget != null: + _clock.performance_budget.record_sample(metric_id, maxi(duration_us, 0)) + + +func _release_recovery_task_items( + inventory: InventoryService, + reservation_ids: Array[StringName], + task: StaffTask, + absolute_minute: int +) -> Dictionary: + var before := inventory.create_snapshot() + for reservation_id: StringName in reservation_ids: + var result := inventory.release_reservation( + reservation_id, + StringName("recovery.staff.task.release.%s.%s" % [task.task_id, reservation_id]), + absolute_minute + ) + if not bool(result.get(&"ok", false)): + inventory.restore_snapshot(before) + return result + return {&"ok": true} + + +func _merge_content_filter_profiles(current: Dictionary, saved: Dictionary) -> Dictionary: + var disabled_by_name: Dictionary = {} + for raw_tag: Variant in current.get(&"disabled_content_tags", []): + var tag := StringName(raw_tag) + if not tag.is_empty(): + disabled_by_name[tag] = true + for raw_tag: Variant in saved.get(&"disabled_content_tags", []): + var tag := StringName(raw_tag) + if not tag.is_empty(): + disabled_by_name[tag] = true + var disabled_tags: Array[StringName] = [] + for raw_tag: Variant in disabled_by_name: + disabled_tags.append(StringName(raw_tag)) + disabled_tags.sort_custom( + func(left: StringName, right: StringName) -> bool: + return String(left) < String(right) + ) + return { + &"filter_mode": maxi( + int(current.get(&"filter_mode", EventDefinition.FilterBehavior.NONE)), + int(saved.get(&"filter_mode", EventDefinition.FilterBehavior.NONE)) + ) as EventDefinition.FilterBehavior, + &"maximum_content_rating": mini( + int(current.get(&"maximum_content_rating", EventDefinition.ContentRating.GENERAL)), + int(saved.get(&"maximum_content_rating", EventDefinition.ContentRating.GENERAL)) + ) as EventDefinition.ContentRating, + &"disabled_content_tags": disabled_tags, + } + + +func _content_filter_restore_report( + current: Dictionary, + saved: Dictionary, + effective: Dictionary +) -> Dictionary: + var current_tags: Array = current.get(&"disabled_content_tags", []) + var saved_tags: Array = saved.get(&"disabled_content_tags", []) + var saved_was_weaker := ( + int(saved.get(&"filter_mode", 0)) < int(current.get(&"filter_mode", 0)) + or int(saved.get(&"maximum_content_rating", 0)) > int(current.get(&"maximum_content_rating", 0)) + ) + for raw_tag: Variant in current_tags: + if not saved_tags.has(StringName(raw_tag)) and not saved_tags.has(String(raw_tag)): + saved_was_weaker = true + break + return { + &"saved_profile_was_weaker": saved_was_weaker, + &"strict_profile_preserved": true, + &"current_before_load": _content_filter_profile_payload(current), + &"saved_profile": _content_filter_profile_payload(saved), + &"effective_profile": _content_filter_profile_payload(effective), + } + + +func _content_filter_profile_payload(profile: Dictionary) -> Dictionary: + var mode := clampi( + int(profile.get(&"filter_mode", EventDefinition.FilterBehavior.NONE)), + EventDefinition.FilterBehavior.NONE, + EventDefinition.FilterBehavior.DISABLE + ) + var rating := clampi( + int(profile.get(&"maximum_content_rating", EventDefinition.ContentRating.GENERAL)), + EventDefinition.ContentRating.GENERAL, + EventDefinition.ContentRating.RESTRICTED + ) + var tags: Array[String] = [] + for raw_tag: Variant in profile.get(&"disabled_content_tags", []): + tags.append(String(raw_tag)) + tags.sort() + return { + &"filter_mode": EventDefinition.FilterBehavior.keys()[mode], + &"maximum_content_rating": EventDefinition.ContentRating.keys()[rating], + &"disabled_content_tags": tags, + } + + +func _save_policy_path_is_safe(save_path: String) -> bool: + if not save_path.begins_with("user://") or save_path == "user://": + return false + var relative := save_path.trim_prefix("user://").replace("\\", "/") + if relative.is_empty() or relative.is_absolute_path(): + return false + for segment: String in relative.split("/", false): + if segment.is_empty() or segment == "." or segment == "..": + return false + return true + + +func _runtime_command_history_crosslinks_are_valid( + queue_snapshot: Dictionary, + next_command_id: int, + events: DomainEventStream +) -> bool: + if next_command_id <= 0 or events == null \ + or typeof(queue_snapshot.get("version", null)) != TYPE_INT \ + or int(queue_snapshot["version"]) != CommandQueue.SNAPSHOT_VERSION \ + or not queue_snapshot.get("accepted", null) is Array: + return false + var seen_ids: Dictionary = {} + var processed_by_id: Dictionary = {} + var command_idempotency: Dictionary = {} + for collection_key: String in ["pending", "in_flight", "processed"]: + var rows_value: Variant = queue_snapshot.get(collection_key, null) + if not rows_value is Array: + return false + if collection_key == "in_flight" and not rows_value.is_empty(): + # Runtime commands are synchronously completed in APPLY_COMMANDS and + # have no resumable in-flight execution path. + return false + for row_value: Variant in rows_value: + if not row_value is Dictionary: + return false + var row: Dictionary = row_value + var command_id_value: Variant = row.get("command_id", null) + if typeof(command_id_value) != TYPE_INT or int(command_id_value) <= 0 \ + or seen_ids.has(int(command_id_value)): + return false + var command_id := int(command_id_value) + seen_ids[command_id] = true + if collection_key == "processed": + processed_by_id[command_id] = row + else: + var raw_key: Variant = row.get("idempotency_key", "") + if typeof(raw_key) not in [TYPE_STRING, TYPE_STRING_NAME]: + return false + var key := StringName(raw_key) + if not key.is_empty(): + command_idempotency[command_id] = key + if seen_ids.size() != next_command_id - 1: + return false + for command_id in range(1, next_command_id): + if not seen_ids.has(command_id): + return false + var accepted_by_id: Dictionary = {} + var previous_accepted_id := 0 + for raw_metadata: Variant in queue_snapshot["accepted"]: + if not raw_metadata is Dictionary: + return false + var metadata: Dictionary = raw_metadata + var command_id_value: Variant = metadata.get("command_id", null) + var key_value: Variant = metadata.get("idempotency_key", null) + if metadata.size() != 2 or typeof(command_id_value) != TYPE_INT \ + or int(command_id_value) <= previous_accepted_id \ + or typeof(key_value) not in [TYPE_STRING, TYPE_STRING_NAME] \ + or not seen_ids.has(int(command_id_value)): + return false + var command_id := int(command_id_value) + if command_idempotency.has(command_id) \ + and StringName(command_idempotency[command_id]) != StringName(key_value): + return false + accepted_by_id[command_id] = StringName(key_value) + command_idempotency[command_id] = StringName(key_value) + previous_accepted_id = command_id + if accepted_by_id.size() != seen_ids.size(): + return false + var idempotency_value: Variant = queue_snapshot.get("idempotency", null) + if not idempotency_value is Dictionary: + return false + var mapped_ids: Dictionary = {} + for raw_key: Variant in idempotency_value: + if typeof(raw_key) not in [TYPE_STRING, TYPE_STRING_NAME] \ + or StringName(raw_key).is_empty() \ + or typeof(idempotency_value[raw_key]) != TYPE_INT: + return false + var command_id := int(idempotency_value[raw_key]) + if not seen_ids.has(command_id) or mapped_ids.has(command_id): + return false + mapped_ids[command_id] = StringName(raw_key) + for command_id: int in command_idempotency: + var accepted_key := StringName(command_idempotency[command_id]) + if accepted_key.is_empty(): + if mapped_ids.has(command_id): + return false + elif mapped_ids.get(command_id, &"") != accepted_key: + return false + + var completion_by_id: Dictionary = {} + for event: SimulationDomainEvent in events.recent_events(): + if event.event_type not in [&"COMMAND_COMPLETED", &"COMMAND_FAILED"]: + continue + var command_id := event.causation_command_id + if command_id <= 0 or not processed_by_id.has(command_id) \ + or completion_by_id.has(command_id) \ + or event.source_service != &"simulation_runtime" \ + or not _strict_variants_equal(event.payload, processed_by_id[command_id]): + return false + var result_code := String((processed_by_id[command_id] as Dictionary).get("code", "")) + if (result_code == "OK") != (event.event_type == &"COMMAND_COMPLETED"): + return false + completion_by_id[command_id] = true + # Completion events share a bounded stream with all other DomainEvents. Any + # retained completion must match exactly; absence alone is not corruption + # because unrelated events can legitimately evict it. + return true + + +func _legacy_operations_history_is_absent(runtime_data: Dictionary) -> bool: + # v1-v4 had no Operations wrapper or payroll settlement semantics. Reject a + # relabelled v5 payload instead of silently discarding production history. + if runtime_data.has(&"operations") or runtime_data.has("operations"): + return false + var staff_value: Variant = runtime_data.get(&"staff", null) + if staff_value is Dictionary: + var settlements: Variant = (staff_value as Dictionary).get(&"payroll_settlements", []) + if settlements is Array and not settlements.is_empty(): + return false + var economy_value: Variant = runtime_data.get(&"economy", null) + if economy_value is Dictionary: + var entries: Variant = (economy_value as Dictionary).get(&"entries", []) + if entries is Array: + for raw_entry: Variant in entries: + if raw_entry is Dictionary \ + and StringName((raw_entry as Dictionary).get(&"category", "")) == &"WAGE": + return false + return true + + +func _legacy_infrastructure_history_is_absent(runtime_data: Dictionary) -> bool: + # Infrastructure aggregates first exist in Runtime v6. A lower version with + # any look-alike field or retained v6-only history is a relabelled payload, + # not a migratable legacy save. + for key: StringName in [ + &"excavation", &"spatial_subslots", &"route_projection", + &"facility_infrastructure", + ]: + if runtime_data.has(key) or runtime_data.has(String(key)): + return false + var stream_value: Variant = runtime_data.get(&"domain_events", null) + if stream_value is Dictionary: + var recent_events: Variant = (stream_value as Dictionary).get("recent_events", []) + if recent_events is Array: + for raw_event: Variant in recent_events: + if raw_event is Dictionary and StringName( + (raw_event as Dictionary).get("event_type", "") + ) in [&"FACILITY_INFRASTRUCTURE_PROJECTED", &"EXCAVATION_COMPLETED"]: + return false + var queue_value: Variant = runtime_data.get(&"command_queue", null) + if queue_value is Dictionary: + for queue_key: String in ["pending", "in_flight"]: + var commands: Variant = (queue_value as Dictionary).get(queue_key, []) + if not commands is Array: + continue + for raw_command: Variant in commands: + if raw_command is Dictionary and StringName( + (raw_command as Dictionary).get("command_type", "") + ) in [&"start_excavation", &"pause_excavation", &"resume_excavation"]: + return false + return true + + +func _runtime_v5_outer_snapshot_shape_is_valid(snapshot: Dictionary) -> bool: + if not _dictionary_has_exact_canonical_keys( + snapshot, [&"schema_version", &"clock", &"game_state", &"systems"] + ) or typeof(snapshot.get(&"schema_version", null)) != TYPE_INT \ + or int(snapshot[&"schema_version"]) != SimulationSnapshot.SCHEMA_VERSION: + return false + var systems_value: Variant = snapshot.get(&"systems", null) + var clock_value: Variant = snapshot.get(&"clock", null) + var state_value: Variant = snapshot.get(&"game_state", null) + if not systems_value is Dictionary or not clock_value is Dictionary \ + or not state_value is Dictionary: + return false + var systems: Dictionary = systems_value + var clock_data: Dictionary = clock_value + var state_data: Dictionary = state_value + if not _dictionary_has_exact_canonical_keys(systems, [&"runtime"]) \ + or not systems[&"runtime"] is Dictionary \ + or not _dictionary_has_exact_canonical_keys( + clock_data, + [&"absolute_minute", &"speed", &"paused", &"snapshot_version"] + ) \ + or typeof(clock_data.get(&"absolute_minute", null)) != TYPE_INT \ + or int(clock_data[&"absolute_minute"]) < 0 \ + or typeof(clock_data.get(&"speed", null)) != TYPE_INT \ + or int(clock_data[&"speed"]) not in [1, 2, 3, 4] \ + or typeof(clock_data.get(&"paused", null)) != TYPE_BOOL \ + or typeof(clock_data.get(&"snapshot_version", null)) != TYPE_INT \ + or int(clock_data[&"snapshot_version"]) < 0: + return false + if not _dictionary_has_exact_canonical_keys( + state_data, + [&"resources", &"world_tree_rank", &"active_layer", &"revision"] + ) or not state_data[&"resources"] is Dictionary \ + or typeof(state_data.get(&"world_tree_rank", null)) != TYPE_INT \ + or int(state_data[&"world_tree_rank"]) < 1 \ + or int(state_data[&"world_tree_rank"]) > 7 \ + or typeof(state_data.get(&"active_layer", null)) != TYPE_INT \ + or int(state_data[&"active_layer"]) not in [0, 1] \ + or typeof(state_data.get(&"revision", null)) != TYPE_INT \ + or int(state_data[&"revision"]) < 0: + return false + var resources: Dictionary = state_data[&"resources"] + var seen_resources: Dictionary = {} + for raw_resource_id: Variant in resources: + if typeof(raw_resource_id) not in [TYPE_STRING, TYPE_STRING_NAME]: + return false + var resource_id := StringName(raw_resource_id) + if resource_id.is_empty() or seen_resources.has(resource_id) \ + or typeof(resources[raw_resource_id]) != TYPE_INT \ + or int(resources[raw_resource_id]) < 0: + return false + seen_resources[resource_id] = true + for required_resource: StringName in [&"gold", &"mana", &"materials", &"faith"]: + if not seen_resources.has(required_resource): + return false + return true + + +func _connect_path_performance_signal() -> void: + if path_service == null or not path_service.has_signal(&"path_query_measured"): + return + var callback := Callable(self, "_on_path_query_measured") + if not path_service.is_connected(&"path_query_measured", callback): + path_service.connect(&"path_query_measured", callback) + + +func _on_path_query_measured(duration_us: int) -> void: + _record_runtime_performance(&"path_query", duration_us) + +func _initialize_rng_streams() -> void: + _rng_streams.clear() + for stream_id in RNG_STREAM_IDS: + _rng_streams[stream_id] = DeterministicRng.from_root_seed(world_seed, stream_id) + +func _connect_construction_signals() -> void: + if construction_service == null: + return + var completed_callback := Callable(self, "_on_facility_completed") + var demolished_callback := Callable(self, "_on_facility_demolished") + if not construction_service.facility_completed.is_connected(completed_callback): + construction_service.facility_completed.connect(completed_callback) + if not construction_service.facility_demolished.is_connected(demolished_callback): + construction_service.facility_demolished.connect(demolished_callback) + + +func _initialize_infrastructure_services() -> bool: + spatial_subslot_service = SpatialSubslotService.new() + route_subslot_path_adapter = RouteSubslotPathAdapter.new() + if not route_subslot_path_adapter.configure( + spatial_subslot_service, + path_service, + world_service.get_walkable_cells() + ): + return false + facility_infrastructure_projector = FacilityInfrastructureProjector.new() + if not facility_infrastructure_projector.configure( + route_subslot_path_adapter, + spatial_subslot_service, + path_service + ): + return false + excavation_service = ExcavationService.new(world_service, construction_resource_state) + excavation_service.current_minute = _clock.absolute_minute + var excavation_callback := Callable(self, "_on_excavation_completed") + if not excavation_service.excavation_completed.is_connected(excavation_callback): + excavation_service.excavation_completed.connect(excavation_callback) + return true + +func rebuild_path_graph_from_world() -> bool: + if path_service == null or world_service == null: + return false + var walkable_cells: Array[Vector3i] = world_service.get_walkable_cells() + var projected_walkable_cells: Array[Vector3i] = walkable_cells.duplicate() + if route_subslot_path_adapter != null \ + and route_subslot_path_adapter.is_configured() \ + and spatial_subslot_service != null: + var seen_projected: Dictionary = {} + projected_walkable_cells.clear() + for cell: Vector3i in walkable_cells: + if not seen_projected.has(cell): + seen_projected[cell] = true + projected_walkable_cells.append(cell) + for cell: Vector3i in spatial_subslot_service.get_route_cells(): + if not seen_projected.has(cell): + seen_projected[cell] = true + projected_walkable_cells.append(cell) + var current_path_snapshot: Dictionary = path_service.create_snapshot() + var raw_actor_cells: Variant = current_path_snapshot.get(&"actor_cells", null) + var has_runtime_actor_state: bool = ( + not _movement_credits.is_empty() + or (raw_actor_cells is Array and not raw_actor_cells.is_empty()) + ) + # PathService correctly owns its own graph, but Runtime owns the cross-system + # Actor/Path projection contract. Reject a topology change that would evict + # a live Guest/Staff projection instead of allowing PathService to prune it. + if has_runtime_actor_state: + if _clock == null or not _validate_runtime_path_crosslinks( + path_service, + guest_service, + staff_service, + _movement_credits, + _clock.absolute_minute + ): + return false + var walkable_set: Dictionary = {} + for cell: Vector3i in projected_walkable_cells: + walkable_set[cell] = true + var expected_cells: Dictionary = _expected_runtime_actor_cells( + guest_service, + staff_service + ) + for actor_id: StringName in _sorted_name_keys(expected_cells): + if not walkable_set.has(expected_cells[actor_id]): + return false + if route_subslot_path_adapter != null \ + and route_subslot_path_adapter.is_configured(): + var synchronized := route_subslot_path_adapter.synchronize_projection( + StringName( + "runtime.base_sync.%08d" % (route_subslot_path_adapter.projection_revision + 1) + ), + walkable_cells + ) + return synchronized.is_success() and _prune_legacy_stairs_and_movement() + return path_service.rebuild_graph(walkable_cells) and _prune_legacy_stairs_and_movement() + +func _initialize_world(rank: int) -> void: + var bounds := _bounds_for_rank(rank) + world_service.reset(bounds) + _seed_boundary_cells(bounds) + rebuild_path_graph_from_world() + path_service.add_connector( + &"starter.staff_door", + Vector3i(0, 0, WorldService.LAYER_FRONT), + Vector3i(0, 0, WorldService.LAYER_BACK), + PathService.DEFAULT_CONNECTOR_COST, + true, + &"staff" + ) + +func _initialize_mana_network() -> void: + mana_network_service.register_source(STARTER_MANA_SOURCE, STARTER_MANA_OUTPUT) + mana_network_service.register_connector(STARTER_MANA_BUS) + mana_network_service.connect_nodes(STARTER_MANA_SOURCE, STARTER_MANA_BUS) + +func _initialize_economy() -> void: + economy_service = EconomyService.new({ + PLAYER_CASH_ACCOUNT: {&"allow_negative": false}, + CONSTRUCTION_EXPENSE_ACCOUNT: {&"allow_negative": false}, + OPERATING_EXPENSE_ACCOUNT: {&"allow_negative": false}, + AUTHORITY_FINE_ACCOUNT: {&"allow_negative": false}, + WAGE_EXPENSE_ACCOUNT: {&"allow_negative": false}, + }) + economy_service.initialize_account( + PLAYER_CASH_ACCOUNT, + int(_state.get_resource(&"gold")), + _clock.absolute_minute, + &"runtime.initial_player_cash" + ) + facility_cleanliness = FacilityCleanliness.new() + cleaning_policy = CleaningPolicy.new() + cleaning_policy.load_file() + weather = WeatherTimeline.new() + weather.initialize(&"clear", _clock.absolute_minute) + facility_upkeep = FacilityUpkeepAccrual.new() + upkeep_settlement = FacilityUpkeepSettlement.new() + FacilityUpkeepSettlement.prepare_accounts(economy_service) + facility_upkeep.start_at(_clock.absolute_minute) + construction_resource_state = _create_resource_adapter( + economy_service, + CONSTRUCTION_EXPENSE_ACCOUNT, + &"CONSTRUCTION", + &"CONSTRUCTION_REFUND", + &"construction_service" + ) + operating_resource_state = _create_resource_adapter( + economy_service, + OPERATING_EXPENSE_ACCOUNT, + &"OPERATING_EXPENSE", + &"OPERATING_REFUND", + &"simulation_runtime" + ) + _connect_economy_signals() + +func _create_resource_adapter( + economy: EconomyService, + counter_account: StringName, + expense_category: StringName, + refund_category: StringName, + source_entity_id: StringName +) -> LedgerBackedResourceState: + return LedgerBackedResourceState.new( + _state, + economy, + counter_account, + expense_category, + refund_category, + source_entity_id, + func() -> int: return _runtime_event_minute() + ) + +func _connect_economy_signals() -> void: + if economy_service == null: + return + economy_service.transaction_posted.connect(Callable(self, "_on_ledger_transaction_posted")) + +func _initialize_s2_operations() -> void: + inventory_service = InventoryService.new() + inventory_service.current_minute = _clock.absolute_minute + logistics_dispatcher = LogisticsDispatcher.new() + logistics_requests = {} + facility_service = FacilityService.new() + facility_service.current_minute = _clock.absolute_minute + _configure_facility_boundaries() + _connect_facility_signals() + auto_purchase_plans = AutoPurchasePlansType.new() + kitchen_production = KitchenProductionService.new() + kitchen_production.configure(inventory_service, facility_service, definition_registry, _clock.absolute_minute) + +func _initialize_s3_actors() -> void: + guest_service = GuestService.new() + guest_service.current_minute = _clock.absolute_minute + guest_service.create_definition(&"traveler", { + &"tags": [&"general", &"traveler"], + &"wealth_min": 120, + &"wealth_max": 450, + &"base_patience": 45, + &"risk_tolerance": 50, + &"preference_weights": {&"dining": 8, &"lodging": 7, &"social": 3}, + &"group_size_weights": {1: 70, 2: 25, 3: 5}, + &"legal_sensitivity": 65, + &"quality_sensitivity": 50, + &"need_rates": {&"hunger": 1, &"sleep": 1, &"hygiene": 0, &"medical": 0, &"leisure": 1, &"intimacy": 0}, + &"credit_limit_gold": 50, + &"minimum_quality": 25, + }) + guest_service.create_definition(&"merchant", { + &"tags": [&"general", &"merchant", &"frugal"], + &"wealth_min": 300, + &"wealth_max": 1_200, + &"base_patience": 35, + &"risk_tolerance": 35, + &"preference_weights": {&"lodging": 9, &"quiet": 7, &"service": 4}, + &"group_size_weights": {1: 60, 2: 40}, + &"legal_sensitivity": 80, + &"quality_sensitivity": 70, + &"need_rates": {&"hunger": 1, &"sleep": 1, &"hygiene": 0, &"medical": 0, &"leisure": 1, &"intimacy": 0}, + &"credit_limit_gold": 200, + &"minimum_quality": 40, + }) + guest_service.create_definition(&"adventurer", { + &"tags": [&"adventurer", &"general", &"spendthrift"], + &"wealth_min": 180, + &"wealth_max": 800, + &"base_patience": 30, + &"risk_tolerance": 85, + &"preference_weights": {&"dining": 6, &"social": 8, &"service": 2}, + &"group_size_weights": {1: 45, 2: 35, 3: 15, 4: 5}, + &"legal_sensitivity": 35, + &"quality_sensitivity": 35, + &"adult_verified": true, + &"need_rates": {&"hunger": 2, &"sleep": 1, &"hygiene": 0, &"medical": 0, &"leisure": 1, &"intimacy": 0}, + &"credit_limit_gold": 100, + &"minimum_quality": 20, + }) + if bool(mvp_content_report.get(&"valid", false)): + for definition: GuestDefinition in mvp_content_catalog.build_all_guest_definitions(): + if not guest_service.register_definition(definition): + mvp_content_report = mvp_content_report.duplicate(true) + mvp_content_report[&"valid"] = false + mvp_content_report[&"runtime_registration_error"] = guest_service.last_error + break + staff_service = StaffService.new() + staff_service.current_minute = _clock.absolute_minute + _configure_staff_boundaries() + _connect_guest_signals() + _connect_staff_signals() + +func _configure_staff_boundaries() -> void: + if staff_service == null: + return + staff_service.configure_arrival_driver(Callable(self, "_handle_rescue_arrival")) + staff_service.configure_work_progress_driver(Callable(self, "_automatic_staff_work_units")) + staff_service.configure_callbacks( + Callable(self, "_reserve_staff_task_items"), + Callable(self, "_release_staff_task_items"), + Callable(self, "_estimate_staff_task_travel") + ) + +func _connect_guest_signals() -> void: + if guest_service == null: + return + guest_service.guest_spawned.connect(Callable(self, "_on_guest_spawned")) + guest_service.guest_state_changed.connect(Callable(self, "_on_guest_state_changed")) + guest_service.guest_queue_abandoned.connect(Callable(self, "_on_guest_queue_abandoned")) + guest_service.guest_recovery_started.connect(Callable(self, "_on_guest_recovery_started")) + guest_service.guest_despawned.connect(Callable(self, "_on_guest_despawned")) + +func _connect_staff_signals() -> void: + if staff_service == null: + return + staff_service.staff_registered.connect(Callable(self, "_on_staff_registered")) + staff_service.staff_removed.connect(Callable(self, "_on_staff_removed")) + staff_service.shift_registered.connect(Callable(self, "_on_shift_registered")) + staff_service.shift_state_changed.connect(Callable(self, "_on_shift_state_changed")) + staff_service.task_registered.connect(Callable(self, "_on_staff_task_registered")) + staff_service.task_assigned.connect(Callable(self, "_on_staff_task_assigned")) + staff_service.task_reopened.connect(Callable(self, "_on_staff_task_reopened")) + staff_service.task_completed.connect(Callable(self, "_on_staff_task_completed")) + +func _initialize_s4_relationship_events() -> void: + relationship_service = RelationshipService.new() + event_service = EventService.new() + event_service.create_definition(&"relationship.threshold", { + &"category": &"RELATIONSHIP", + &"priority": 80, + &"foreground": true, + &"expires_after": 1_440, + &"transaction_policy": &"ROLLBACK_ALL", + &"missing_actor_policy": &"CANCEL", + &"content_rating": &"GENERAL", + &"choices": [ + { + &"choice_id": &"acknowledge", + &"text_key": &"event.relationship.threshold.acknowledge", + &"actions": [{ + &"action_id": &"record_acknowledgement", + &"type": &"SET_FLAG", + &"payload": {&"flag_id": &"relationship_threshold_acknowledged"}, + }], + }, + { + &"choice_id": &"defer", + &"text_key": &"event.relationship.threshold.defer", + &"actions": [], + }, + ], + }) + _configure_relationship_event_boundaries() + _connect_relationship_signals() + _connect_event_signals() + +func _configure_relationship_event_boundaries() -> void: + if relationship_service == null or event_service == null: + return + relationship_service.configure_candidate_callback( + Callable(self, "_on_relationship_threshold_candidate") + ) + event_service.configure_relationship_service(relationship_service) + event_service.configure_candidate_eligibility_guard(Callable(self, "_event_candidate_placement_permitted")) + event_service.configure_callbacks( + Callable(self, "_event_actor_exists"), + Callable(self, "_execute_event_action_boundary"), + Callable(self, "_reserve_event_cost"), + Callable(self, "_release_event_cost"), + Callable(self, "_commit_event_cost"), + Callable(self, "_event_entity_exists"), + Callable(self, "_event_path_exists") + ) + +## Original 6.2/6.4: Tomari's placement prevents floor trouble. This does not +## choose a joining date, probability, defense score, or narrative content. +func is_floor_trouble_protected(floor_y: int) -> bool: + var tomari := staff_service.get_staff(&"heroine_tomari") + if tomari == null or tomari.employment_kind != StaffRuntime.EmploymentKind.UNIQUE: + return false + var assignment := staff_service.get_work_assignment(tomari.staff_id) + var facility := facility_service.get_instance(StringName(assignment.get(&"facility_id", &""))) + return facility != null and facility.origin.y == floor_y + + +func _event_candidate_placement_permitted(definition: EventDefinition, context: Dictionary) -> bool: + if definition.category != &"TROUBLE": + return true + # A business trouble identifies its existing target facility. Missing or + # conflicting location data cannot silently bypass placement protection. + var target: Variant = context.get(&"facility_id", definition.location_rule.get(&"facility_id", null)) + if typeof(target) not in [TYPE_STRING, TYPE_STRING_NAME]: + return false + var declared: Variant = definition.location_rule.get(&"facility_id", target) + if typeof(declared) not in [TYPE_STRING, TYPE_STRING_NAME] or StringName(declared) != StringName(target): + return false + var facility := facility_service.get_instance(StringName(target)) + return facility != null and not is_floor_trouble_protected(facility.origin.y) + + +func _connect_relationship_signals() -> void: + if relationship_service == null: + return + relationship_service.relationship_registered.connect(Callable(self, "_on_relationship_registered")) + relationship_service.relationship_value_changed.connect(Callable(self, "_on_relationship_value_changed")) + relationship_service.shield_changed.connect(Callable(self, "_on_relationship_shield_changed")) + relationship_service.route_lock_changed.connect(Callable(self, "_on_relationship_route_lock_changed")) + +func _connect_event_signals() -> void: + if event_service == null: + return + event_service.event_enqueued.connect(Callable(self, "_on_event_enqueued")) + event_service.event_started.connect(Callable(self, "_on_event_started")) + event_service.event_finished.connect(Callable(self, "_on_event_finished")) + event_service.event_cancelled.connect(Callable(self, "_on_event_cancelled")) + event_service.choice_resolved.connect(Callable(self, "_on_event_choice_resolved")) + event_service.effect_transaction_started.connect(Callable(self, "_on_event_effect_transaction_started")) + event_service.effect_transaction_finished.connect(Callable(self, "_on_event_effect_transaction_finished")) + +func _initialize_s5_authority() -> void: + authority_service = AuthorityService.new() + authority_service.current_minute = _clock.absolute_minute + _configure_authority_boundaries() + _connect_authority_signals() + +func _configure_authority_boundaries() -> void: + if authority_service == null: + return + authority_service.configure_callbacks( + Callable(self, "_authority_line_of_sight"), + Callable(self, "_authority_path_query"), + Callable(self, "_apply_authority_resolution_boundary"), + Callable(self, "_roll_authority_inspection") + ) + +func _connect_authority_signals() -> void: + if authority_service == null: + return + authority_service.heat_changed.connect(Callable(self, "_on_authority_heat_changed")) + authority_service.incident_created.connect(Callable(self, "_on_authority_incident_created")) + authority_service.incident_detector_added.connect(Callable(self, "_on_authority_incident_detector_added")) + authority_service.evidence_registered.connect(Callable(self, "_on_authority_evidence_registered")) + authority_service.evidence_discovered.connect(Callable(self, "_on_authority_evidence_discovered")) + authority_service.inspection_scheduled.connect(Callable(self, "_on_authority_inspection_scheduled")) + authority_service.inspection_state_changed.connect(Callable(self, "_on_authority_inspection_state_changed")) + authority_service.resolution_applied.connect(Callable(self, "_on_authority_resolution_applied")) + +func _initialize_s5_defense() -> void: + _reset_defense_round_attempts() + defense_service = DefenseService.new() + defense_service.current_minute = _clock.absolute_minute + _configure_defense_boundaries() + _connect_defense_signals() + +func _defense_operation_minute() -> int: + return _active_command_minute if _active_command_minute >= 0 else _clock.absolute_minute + +func _runtime_event_minute() -> int: + return _active_command_minute if _active_command_minute >= 0 else _clock.absolute_minute + +func _configure_defense_boundaries() -> void: + if defense_service == null: + return + defense_service.configure_callbacks( + Callable(self, "_roll_defense_round"), + Callable(self, "_apply_defense_skill_boundary"), + Callable(self, "_apply_defense_outcome_boundary"), + Callable(self, "_apply_defense_rng_boundary") + ) + +func _connect_defense_signals() -> void: + if defense_service == null: + return + var bindings := [ + [&"raid_created", Callable(self, "_on_defense_raid_created")], + [&"raid_state_changed", Callable(self, "_on_defense_raid_state_changed")], + [&"active_skill_applied", Callable(self, "_on_defense_active_skill_applied")], + [&"round_resolved", Callable(self, "_on_defense_round_resolved")], + [&"raid_resolved", Callable(self, "_on_defense_raid_resolved")], + ] + for binding: Array in bindings: + var signal_name: StringName = binding[0] + var callback: Callable = binding[1] + if not defense_service.is_connected(signal_name, callback): + defense_service.connect(signal_name, callback) + +func _detach_defense_service(service: DefenseService) -> void: + if service == null: + return + # A caller may retain a RefCounted aggregate across Runtime initialize/load. + # Retiring it must sever both transaction callbacks and event projection so + # stale objects cannot consume the new Runtime's RNG/resources or emit ghost + # DomainEvents after an otherwise atomic restore. + service.configure_callbacks() + var bindings := [ + [&"raid_created", Callable(self, "_on_defense_raid_created")], + [&"raid_state_changed", Callable(self, "_on_defense_raid_state_changed")], + [&"active_skill_applied", Callable(self, "_on_defense_active_skill_applied")], + [&"round_resolved", Callable(self, "_on_defense_round_resolved")], + [&"raid_resolved", Callable(self, "_on_defense_raid_resolved")], + ] + for binding: Array in bindings: + var signal_name: StringName = binding[0] + var callback: Callable = binding[1] + if service.is_connected(signal_name, callback): + service.disconnect(signal_name, callback) + +func _configure_facility_boundaries() -> void: + if facility_service == null: + return + facility_service.configure_callbacks( + Callable(self, "_validate_service_start"), + Callable(self, "_complete_service_transaction") + ) + +func _connect_facility_signals() -> void: + if facility_service == null: + return + facility_service.instance_registered.connect(Callable(self, "_on_cleanliness_facility_registered")) + facility_service.instance_unregistered.connect(Callable(self, "_on_cleanliness_facility_unregistered")) + facility_service.facility_state_changed.connect(Callable(self, "_on_facility_state_changed")) + facility_service.service_started.connect(Callable(self, "_on_service_started")) + facility_service.service_interrupted.connect(Callable(self, "_on_service_interrupted")) + facility_service.service_completed.connect(Callable(self, "_on_service_completed")) + facility_service.service_failed.connect(Callable(self, "_on_service_failed")) + +func _seed_boundary_cells(bounds: Dictionary) -> void: + for x in range(int(bounds[&"min_x"]), int(bounds[&"max_x"]) + 1): + for layer in [WorldService.LAYER_FRONT, WorldService.LAYER_BACK]: + var cell := Vector3i(x, 0, layer) + world_service.set_walkable(cell, true) + world_service.set_support(cell, 0, true) + world_service.consume_dirty_cells() + +func _expand_world_to_rank(rank: int) -> bool: + var bounds := _bounds_for_rank(rank) + if not world_service.expand_bounds(bounds): + return false + _seed_boundary_cells(bounds) + return rebuild_path_graph_from_world() + +func _bounds_for_rank(rank: int) -> Dictionary: + var source: Variant = ProgressionService.RANK_BOUNDS.get(clampi(rank, 1, 7), {}) + return source.duplicate(true) if source is Dictionary else {} + + +func _sync_progression_live_metrics() -> bool: + return progression_service.update_metrics({ + &"active_above_ground_facilities": _count_active_facilities(facility_service, false), + &"active_underground_facilities": _count_active_facilities(facility_service, true), + &"asset_gold": economy_service.get_balance(PLAYER_CASH_ACCOUNT), + }) + + +func _count_active_facilities(service: FacilityService, underground: bool) -> int: + var count := 0 + for instance: FacilityInstance in service.get_instances(): + if instance.state not in [FacilityInstance.State.OPEN, FacilityInstance.State.DEGRADED]: + continue + if (instance.origin.y < 0) == underground: + count += 1 + return count + + +func _capture_rank_external_state() -> Dictionary: + return { + &"world": world_service.create_snapshot(), + &"path": path_service.create_snapshot(), + &"elevators": elevators.create_snapshot(), + &"legacy_stairs": legacy_stairs.create_snapshot(), + &"excavation": excavation_service.create_snapshot(), + &"spatial_subslots": spatial_subslot_service.create_snapshot(), + &"route_projection": route_subslot_path_adapter.create_snapshot(), + &"facility_infrastructure": facility_infrastructure_projector.create_snapshot(), + &"game_state": _state.create_snapshot(), + &"movement_credits": _movement_credits.duplicate(true), + &"domain_events": domain_events.create_snapshot(), + &"id_generator": id_generator.create_snapshot(), + } + + +func _apply_rank_external_state(target_rank: int, expected_bounds: Dictionary) -> bool: + if target_rank != int(_state.world_tree_rank) + 1 \ + or not _strict_variants_equal(_bounds_for_rank(target_rank), expected_bounds): + return false + if not _expand_world_to_rank(target_rank): + return false + if not _state.set_world_tree_rank(target_rank): + return false + return ( + int(_state.world_tree_rank) == target_rank + and _strict_variants_equal(world_service.get_bounds(), expected_bounds) + and _validate_infrastructure_crosslinks( + world_service, + path_service, + construction_service, + construction_resource_state, + excavation_service, + spatial_subslot_service, + route_subslot_path_adapter, + facility_infrastructure_projector + ) + and _validate_runtime_path_crosslinks( + path_service, + guest_service, + staff_service, + _movement_credits, + _clock.absolute_minute + ) + ) + + +func _rollback_rank_external_state(snapshot: Dictionary) -> bool: + if not snapshot.get(&"world", null) is Dictionary \ + or not snapshot.get(&"path", null) is Dictionary \ + or not snapshot.get(&"excavation", null) is Dictionary \ + or not snapshot.get(&"spatial_subslots", null) is Dictionary \ + or not snapshot.get(&"route_projection", null) is Dictionary \ + or not snapshot.get(&"facility_infrastructure", null) is Dictionary \ + or not snapshot.get(&"game_state", null) is Dictionary \ + or not snapshot.get(&"movement_credits", null) is Dictionary \ + or not snapshot.get(&"domain_events", null) is Dictionary \ + or not snapshot.get(&"id_generator", null) is Dictionary: + return false + var restored := world_service.restore_snapshot(snapshot[&"world"]) + restored = spatial_subslot_service.restore_snapshot(snapshot[&"spatial_subslots"]) and restored + var route_snapshot: Dictionary = snapshot[&"route_projection"] + var projected_walkable: Variant = route_snapshot.get(&"projected_walkable_cells", null) + if not projected_walkable is Array: + return false + restored = path_service.rebuild_graph(projected_walkable) and restored + restored = path_service.restore_snapshot(snapshot[&"path"]) and restored + restored = elevators.restore_snapshot(snapshot[&"elevators"]) and restored + restored = legacy_stairs.restore_snapshot(snapshot[&"legacy_stairs"]) and restored + restored = route_subslot_path_adapter.restore_snapshot(route_snapshot) and restored + restored = facility_infrastructure_projector.restore_snapshot( + snapshot[&"facility_infrastructure"] + ) and restored + restored = excavation_service.restore_snapshot(snapshot[&"excavation"]) and restored + restored = _state.restore_snapshot(snapshot[&"game_state"]) and restored + _movement_credits = (snapshot[&"movement_credits"] as Dictionary).duplicate(true) + restored = domain_events.restore_snapshot(snapshot[&"domain_events"]) and restored + restored = id_generator.restore_snapshot(snapshot[&"id_generator"]) and restored + if not restored: + return false + return ( + _strict_variants_equal(world_service.create_snapshot(), snapshot[&"world"]) + and _strict_variants_equal(path_service.create_snapshot(), snapshot[&"path"]) + and _strict_variants_equal(elevators.create_snapshot(), snapshot[&"elevators"]) + and _strict_variants_equal(legacy_stairs.create_snapshot(), snapshot[&"legacy_stairs"]) + and _strict_variants_equal( + excavation_service.create_snapshot(), snapshot[&"excavation"] + ) + and _strict_variants_equal( + spatial_subslot_service.create_snapshot(), snapshot[&"spatial_subslots"] + ) + and _strict_variants_equal( + route_subslot_path_adapter.create_snapshot(), snapshot[&"route_projection"] + ) + and _strict_variants_equal( + facility_infrastructure_projector.create_snapshot(), + snapshot[&"facility_infrastructure"] + ) + and _strict_variants_equal(_state.create_snapshot(), snapshot[&"game_state"]) + and _strict_variants_equal(_movement_credits, snapshot[&"movement_credits"]) + and _strict_variants_equal(domain_events.create_snapshot(), snapshot[&"domain_events"]) + and _strict_variants_equal(id_generator.create_snapshot(), snapshot[&"id_generator"]) + and _validate_infrastructure_crosslinks( + world_service, + path_service, + construction_service, + construction_resource_state, + excavation_service, + spatial_subslot_service, + route_subslot_path_adapter, + facility_infrastructure_projector + ) + and _validate_runtime_path_crosslinks( + path_service, + guest_service, + staff_service, + _movement_credits, + _clock.absolute_minute + ) + ) + +func _process_commands(absolute_minute: int) -> bool: + var previous_command_minute := _active_command_minute + _active_command_minute = absolute_minute + var results := command_queue.execute_ready(absolute_minute, Callable(self, "_execute_command")) + _active_command_minute = previous_command_minute + _synchronize_operations_population() + for result in results: + var entity_ids: Array[StringName] = [] + if not result.target_id.is_empty(): + entity_ids.append(result.target_id) + var event := SimulationDomainEvent.new( + id_generator.next_id(&"domain_event"), + &"COMMAND_COMPLETED" if result.is_success() else &"COMMAND_FAILED", + absolute_minute, + &"simulation_runtime", + entity_ids, + result.to_dictionary(), + result.command_id + ) + _publish_domain_event(event) + command_completed.emit(result) + if _operations_resume_publish_pending: + _operations_resume_publish_pending = false + _publish_snapshot(absolute_minute) + _process_save_checkpoints(absolute_minute) + return true + return false + +func _process_tick_commands(current_absolute_minute: int) -> void: + if _operations_enabled: + _operations_tick_blocked = _operations_should_block_tick() + if _operations_tick_blocked: + _clock.set_paused(true) + _clock.request_current_tick_abort() + return + _process_commands(current_absolute_minute + 1) + _synchronize_operations_population() + + +func _synchronize_operations_population() -> bool: + if not _operations_enabled or operations_coordinator == null \ + or _operations_transaction_active: + return true + var active_count := guest_service.guest_count() + if operations_coordinator.get_population_count() == active_count: + return true + var synchronized := operations_coordinator.set_population_count(active_count) + if synchronized.is_success(): + return true + _operations_runtime_failure_latched = true + _operations_runtime_failure_report = synchronized.to_dictionary() + _operations_tick_blocked = true + _clock.set_paused(true) + return false + +func _execute_command(command: SimulationCommand) -> CommandResult: + if not _executors.has(command.command_type): + return CommandResult.failed( + CommandResult.Code.INVALID_COMMAND, + command.command_id, + "No executor is registered for command type '%s'." % command.command_type + ) + if command.command_type in DEFENSE_COMMAND_TYPES: + var advanced := defense_service.advance_time(_defense_operation_minute()) + if not advanced.is_success(): + advanced.command_id = command.command_id + return advanced + var executor: Callable = _executors[command.command_type] + var returned: Variant = executor.call(command) + if returned is CommandResult: + return returned + return CommandResult.failed( + CommandResult.Code.EXECUTOR_FAILED, + command.command_id, + "Command executor returned an invalid result." + ) + +func _execute_change_resources(command: SimulationCommand) -> CommandResult: + var deltas: Variant = command.payload.get(&"deltas", command.payload.get("deltas", null)) + if not deltas is Dictionary: + return CommandResult.failed(CommandResult.Code.INVALID_PAYLOAD, command.command_id, "deltas must be a Dictionary.") + var state_before: Dictionary = _state.create_snapshot() + var economy_before := economy_service.create_snapshot() + var progression_before := progression_service.create_snapshot() + var events_before := domain_events.create_snapshot() + var ids_before := id_generator.create_snapshot() + if not _apply_command_resource_deltas(command, deltas): + return CommandResult.failed(CommandResult.Code.VALIDATION_FAILED, command.command_id, "Resource balance would become negative.") + var faith_delta := 0 + for raw_resource_id: Variant in deltas: + if StringName(raw_resource_id) == &"faith": + faith_delta += int(deltas[raw_resource_id]) + if faith_delta > 0: + var next_faith_lifetime := int(progression_service.metrics[&"faith_lifetime"]) + faith_delta + if not progression_service.update_metrics({&"faith_lifetime": next_faith_lifetime}): + economy_service.restore_snapshot(economy_before) + _state.restore_snapshot(state_before) + progression_service.restore_snapshot(progression_before) + domain_events.restore_snapshot(events_before) + id_generator.restore_snapshot(ids_before) + return CommandResult.failed( + CommandResult.Code.TRANSACTION_APPLY_FAILED, + command.command_id, + "Progression lifetime Faith could not be updated atomically." + ) + return CommandResult.succeeded(command.command_id, { + &"revision": _state.revision, + &"player_cash": economy_service.get_balance(PLAYER_CASH_ACCOUNT), + &"faith_lifetime": int(progression_service.metrics[&"faith_lifetime"]), + }) + +# Repay an existing attested liability through the ordinary command queue. +# No rate, issuance, eligibility or due-date values are supplied here. +func _execute_repay_loan_principal(command: SimulationCommand) -> CommandResult: + var loan_id: Variant = command.payload.get(&"loan_id", null) + if typeof(loan_id) not in [TYPE_STRING, TYPE_STRING_NAME] or String(loan_id).is_empty(): + return CommandResult.failed(CommandResult.Code.INVALID_PAYLOAD, command.command_id, "loan_id must be a nonempty String or StringName.") + var result := repay_loan_principal( + StringName(loan_id), command.payload.get(&"amount", null), command.idempotency_key + ) + result.command_id = command.command_id + result.target_id = StringName(loan_id) + return result + + +func _execute_spend_resources(command: SimulationCommand) -> CommandResult: + var cost: Variant = command.payload.get(&"cost", command.payload.get("cost", null)) + if not cost is Dictionary: + return CommandResult.failed(CommandResult.Code.INVALID_PAYLOAD, command.command_id, "cost must be a Dictionary.") + if not operating_resource_state.spend(cost): + return CommandResult.failed(CommandResult.Code.VALIDATION_FAILED, command.command_id, "Resources are insufficient.") + return CommandResult.succeeded(command.command_id, { + &"revision": _state.revision, + &"player_cash": economy_service.get_balance(PLAYER_CASH_ACCOUNT), + }) + +func _apply_command_resource_deltas(command: SimulationCommand, deltas: Dictionary) -> bool: + var normalized: Dictionary = {} + for raw_name: Variant in deltas: + var resource_name := StringName(raw_name) + normalized[resource_name] = int(normalized.get(resource_name, 0)) + int(deltas[raw_name]) + for resource_name: StringName in normalized: + var current := economy_service.get_balance(PLAYER_CASH_ACCOUNT) \ + if resource_name == &"gold" else int(_state.get_resource(resource_name)) + if current + int(normalized[resource_name]) < 0: + return false + + var before_state: Dictionary = _state.create_snapshot() + var before_economy: Dictionary = economy_service.create_snapshot() + var non_gold := normalized.duplicate(true) + var gold_delta := int(non_gold.get(&"gold", 0)) + non_gold.erase(&"gold") + if not non_gold.is_empty() and not _state.apply_resource_deltas(non_gold): + return false + if gold_delta != 0: + var ledger_result: CommandResult + var ledger_idempotency := StringName("runtime.resource.%08d" % command.command_id) + if gold_delta > 0: + ledger_result = economy_service.transfer( + EconomyService.SYSTEM_SOURCE, + PLAYER_CASH_ACCOUNT, + gold_delta, + _runtime_event_minute(), + &"GENERATION", + command.actor_id, + ledger_idempotency + ) + else: + ledger_result = economy_service.transfer( + PLAYER_CASH_ACCOUNT, + EconomyService.SYSTEM_SINK, + -gold_delta, + _runtime_event_minute(), + &"DESTRUCTION", + command.actor_id, + ledger_idempotency + ) + if not ledger_result.is_success(): + _state.restore_snapshot(before_state) + economy_service.restore_snapshot(before_economy) + return false + _state.set_resource(&"gold", economy_service.get_balance(PLAYER_CASH_ACCOUNT)) + return true + +func _execute_add_inventory_stack(command: SimulationCommand) -> CommandResult: + var owner_id := StringName(command.payload.get(&"owner_inventory_id", command.payload.get("owner_inventory_id", ""))) + var item_id := StringName(command.payload.get(&"item_id", command.payload.get("item_id", ""))) + var quantity := int(command.payload.get(&"quantity", command.payload.get("quantity", 0))) + var result := inventory_service.add_stack( + owner_id, + item_id, + quantity, + int(command.payload.get(&"quality", command.payload.get("quality", InventoryService.DEFAULT_QUALITY))), + int(command.payload.get(&"expires_at", command.payload.get("expires_at", -1))), + _runtime_event_minute(), + int(command.payload.get(&"unit_weight", command.payload.get("unit_weight", InventoryService.DEFAULT_UNIT_WEIGHT))), + StringName(command.payload.get(&"stack_id", command.payload.get("stack_id", ""))) + ) + return _command_result_from_dictionary(command.command_id, result, "Inventory stack could not be added.") + +func _execute_create_logistics_request(command: SimulationCommand) -> CommandResult: + var source_id := StringName(command.payload.get(&"source_inventory_id", command.payload.get("source_inventory_id", ""))) + var destination_id := StringName(command.payload.get(&"destination_inventory_id", command.payload.get("destination_inventory_id", ""))) + var raw_requirements: Variant = command.payload.get(&"item_requirements", command.payload.get("item_requirements", null)) + if source_id.is_empty() or destination_id.is_empty() or source_id == destination_id \ + or not raw_requirements is Dictionary: + return CommandResult.failed(CommandResult.Code.INVALID_PAYLOAD, command.command_id, "Source, destination, and item requirements are required.") + var requirements: Dictionary = {} + for raw_item_id: Variant in raw_requirements: + var item_id := StringName(raw_item_id) + var quantity := int(raw_requirements[raw_item_id]) + if item_id.is_empty() or quantity <= 0: + return CommandResult.failed(CommandResult.Code.INVALID_PAYLOAD, command.command_id, "Logistics requirements must be positive.") + requirements[item_id] = quantity + if requirements.is_empty(): + return CommandResult.failed(CommandResult.Code.INVALID_PAYLOAD, command.command_id, "At least one item is required.") + + var inventory_before := inventory_service.create_snapshot() + var id_before := id_generator.create_snapshot() + var request_id := StringName(command.payload.get(&"request_id", command.payload.get("request_id", ""))) + if request_id.is_empty(): + request_id = id_generator.next_id(&"logistics_request") + if logistics_requests.has(request_id): + id_generator.restore_snapshot(id_before) + return CommandResult.failed(CommandResult.Code.DUPLICATE_COMMAND_ID, command.command_id, "Logistics request id already exists.") + var reservation_ids: Array[StringName] = [] + var stack_ids: Array[StringName] = [] + var total_weight := 0 + var deadline := int(command.payload.get(&"deadline", command.payload.get("deadline", -1))) + for item_id: StringName in _sorted_name_keys(requirements): + var reserve_result := inventory_service.reserve_item( + source_id, + item_id, + int(requirements[item_id]), + request_id, + &"logistics", + _runtime_event_minute(), + deadline, + StringName("logistics.reserve.%s.%s" % [request_id, item_id]) + ) + if not bool(reserve_result.get(&"ok", false)): + inventory_service.restore_snapshot(inventory_before) + id_generator.restore_snapshot(id_before) + return _command_result_from_dictionary(command.command_id, reserve_result, "Logistics stock could not be reserved.") + for reservation_id: StringName in reserve_result.get(&"reservation_ids", []): + reservation_ids.append(reservation_id) + var reservation := inventory_service.get_reservation(reservation_id) + var stack := inventory_service.get_stack(reservation.stack_id) + stack_ids.append(reservation.stack_id) + total_weight += reservation.quantity * int(stack.get(&"unit_weight", 1)) + var request := LogisticsRequest.new().configure( + request_id, + source_id, + destination_id, + requirements, + int(command.payload.get(&"priority", command.payload.get("priority", 50))), + _runtime_event_minute(), + deadline, + total_weight + ) + if not request.mark_reserved(reservation_ids, stack_ids, _runtime_event_minute()): + inventory_service.restore_snapshot(inventory_before) + id_generator.restore_snapshot(id_before) + return CommandResult.failed(CommandResult.Code.TRANSACTION_APPLY_FAILED, command.command_id, "Logistics request could not enter RESERVED state.") + logistics_requests[request_id] = request + return CommandResult.succeeded(command.command_id, { + &"request_id": request_id, + &"reservation_ids": reservation_ids, + &"total_weight": total_weight, + }) + +func _execute_dispatch_logistics(command: SimulationCommand) -> CommandResult: + var carriers: Variant = command.payload.get(&"carriers", command.payload.get("carriers", null)) + if not carriers is Dictionary: + return CommandResult.failed(CommandResult.Code.INVALID_PAYLOAD, command.command_id, "carriers must be a Dictionary.") + var manual_requests: Array[LogisticsRequest] = [] + for request: LogisticsRequest in _sorted_logistics_requests(): + if not String(request.request_id).begins_with(SupplyLogisticsBridge.REQUEST_PREFIX): + manual_requests.append(request) + var assignments := logistics_dispatcher.dispatch(manual_requests, carriers, _runtime_event_minute()) + return CommandResult.succeeded(command.command_id, { + &"assignments": assignments, + &"report": logistics_dispatcher.last_report.duplicate(true), + }) + +func _execute_advance_logistics_request(command: SimulationCommand) -> CommandResult: + var request_id := StringName(command.payload.get(&"request_id", command.payload.get("request_id", ""))) + if command.command_id > 0 and String(request_id).begins_with(SupplyLogisticsBridge.REQUEST_PREFIX): + return CommandResult.failed(CommandResult.Code.VALIDATION_FAILED, command.command_id, "Supply deliveries advance only after the porter arrives.", {&"reason": &"SUPPLY_DRIVER_REQUIRED"}) + if not logistics_requests.has(request_id): + return CommandResult.failed(CommandResult.Code.NOT_FOUND, command.command_id, "Logistics request was not found.") + var request: LogisticsRequest = logistics_requests[request_id] + var request_before := request.create_snapshot() + var inventory_before_command := inventory_service.create_snapshot() + var dispatcher_before := logistics_dispatcher.create_snapshot() + match request.state: + LogisticsRequest.State.ASSIGNED: + if not request.transition_to(LogisticsRequest.State.PICKING_UP, "", _runtime_event_minute()): + return CommandResult.failed(CommandResult.Code.TRANSACTION_APPLY_FAILED, command.command_id, "Logistics pickup could not begin.") + LogisticsRequest.State.PICKING_UP: + var carrier_id := request.assigned_carrier_id + var pickup := inventory_service.transfer_reserved( + request.reservation_ids, + StringName("cargo:%s" % carrier_id), + StringName("logistics.pickup.%s" % request_id), + _runtime_event_minute() + ) + if not bool(pickup.get(&"ok", false)): + request.mark_blocked(String(pickup.get(&"code", &"PICKUP_FAILED")), _runtime_event_minute()) + var released := logistics_dispatcher.release_assignment_for_request( + request_id, carrier_id, request.total_weight + ) + if not released: + inventory_service.restore_snapshot(inventory_before_command) + logistics_dispatcher.restore_snapshot(dispatcher_before) + logistics_requests[request_id] = LogisticsRequest.from_snapshot(request_before) + return CommandResult.failed( + CommandResult.Code.TRANSACTION_APPLY_FAILED, + command.command_id, + "Logistics pickup failure could not release request ownership." + ) + return _command_result_from_dictionary(command.command_id, pickup, "Logistics pickup failed.") + request.transition_to(LogisticsRequest.State.DELIVERING, "", _runtime_event_minute()) + LogisticsRequest.State.DELIVERING: + var carrier_id := request.assigned_carrier_id + var cargo_id := StringName("cargo:%s" % carrier_id) + var inventory_before := inventory_service.create_snapshot() + for item_id: StringName in _sorted_name_keys(request.item_requirements): + var delivery := inventory_service.transfer_available( + cargo_id, + request.destination_inventory_id, + item_id, + int(request.item_requirements[item_id]), + StringName("logistics.delivery.%s.%s" % [request_id, item_id]), + _runtime_event_minute() + ) + if not bool(delivery.get(&"ok", false)): + inventory_service.restore_snapshot(inventory_before) + request.mark_blocked(String(delivery.get(&"code", &"DELIVERY_FAILED")), _runtime_event_minute()) + var released := logistics_dispatcher.release_assignment_for_request( + request_id, carrier_id, request.total_weight + ) + if not released: + inventory_service.restore_snapshot(inventory_before_command) + logistics_dispatcher.restore_snapshot(dispatcher_before) + logistics_requests[request_id] = LogisticsRequest.from_snapshot(request_before) + return CommandResult.failed( + CommandResult.Code.TRANSACTION_APPLY_FAILED, + command.command_id, + "Logistics delivery failure could not release request ownership." + ) + return _command_result_from_dictionary(command.command_id, delivery, "Logistics delivery failed.") + request.transition_to(LogisticsRequest.State.COMPLETED, "", _runtime_event_minute()) + var released := logistics_dispatcher.release_assignment_for_request( + request_id, carrier_id, request.total_weight + ) + if not released: + inventory_service.restore_snapshot(inventory_before_command) + logistics_dispatcher.restore_snapshot(dispatcher_before) + logistics_requests[request_id] = LogisticsRequest.from_snapshot(request_before) + return CommandResult.failed( + CommandResult.Code.TRANSACTION_APPLY_FAILED, + command.command_id, + "Completed Logistics delivery could not release request ownership." + ) + _: + return CommandResult.failed(CommandResult.Code.VALIDATION_FAILED, command.command_id, "Logistics request cannot advance from its current state.") + return CommandResult.succeeded(command.command_id, { + &"request_id": request_id, + &"state": LogisticsRequest.State.keys()[request.state], + }) + +func _execute_register_guest_wallet(command: SimulationCommand) -> CommandResult: + var guest_id := StringName(command.payload.get(&"guest_id", command.payload.get("guest_id", ""))) + var amount := int(command.payload.get(&"gold", command.payload.get("gold", 0))) + if guest_id.is_empty() or amount < 0: + return CommandResult.failed(CommandResult.Code.INVALID_PAYLOAD, command.command_id, "A guest id and non-negative Gold are required.") + var account_id := _guest_wallet_account(guest_id) + if economy_service.has_account(account_id): + return CommandResult.failed(CommandResult.Code.DUPLICATE_COMMAND_ID, command.command_id, "Guest wallet already exists.") + var before := economy_service.create_snapshot() + if not economy_service.register_account(account_id): + return CommandResult.failed(CommandResult.Code.TRANSACTION_APPLY_FAILED, command.command_id, "Guest wallet account could not be registered.") + if amount > 0: + var initialized := economy_service.initialize_account( + account_id, + amount, + _runtime_event_minute(), + StringName("guest.wallet.%s" % guest_id) + ) + if not initialized.is_success(): + economy_service.restore_snapshot(before) + return initialized + return CommandResult.succeeded(command.command_id, {&"account_id": account_id, &"balance": amount}) + +func _execute_spawn_guest(command: SimulationCommand) -> CommandResult: + if _operations_enabled and operations_coordinator != null: + if operations_coordinator.is_terminal() \ + or operations_coordinator.is_external_quarantined(): + return CommandResult.failed( + CommandResult.Code.VALIDATION_FAILED, + command.command_id, + "Operations currently block new Guest arrivals." + ) + if guest_service.guest_count() >= int( + operations_coordinator.get_config().get(&"population_cap", 0) + ): + return CommandResult.failed( + CommandResult.Code.VALIDATION_FAILED, + command.command_id, + "Operations Guest population capacity is full." + ) + var guest_id := StringName(command.payload.get(&"guest_id", command.payload.get("guest_id", ""))) + var id_before := id_generator.create_snapshot() + if guest_id.is_empty(): + guest_id = id_generator.next_id(&"guest") + if String(guest_id).begins_with("guest-auto-") \ + and not (_operations_transaction_active \ + and operations_coordinator != null \ + and command.actor_id == &"operations_coordinator"): + id_generator.restore_snapshot(id_before) + return CommandResult.failed( + CommandResult.Code.VALIDATION_FAILED, + command.command_id, + "guest-auto IDs are reserved for the OperationsCoordinator." + ) + var guest_type_id := StringName(command.payload.get(&"guest_type_id", command.payload.get("guest_type_id", "traveler"))) + var definition := guest_service.get_definition(guest_type_id) + if definition == null: + id_generator.restore_snapshot(id_before) + return CommandResult.failed(CommandResult.Code.NOT_FOUND, command.command_id, "Guest definition was not found.") + if guest_service.has_guest(guest_id): + id_generator.restore_snapshot(id_before) + return CommandResult.failed(CommandResult.Code.DUPLICATE_COMMAND_ID, command.command_id, "Guest already exists.") + var raw_options: Variant = command.payload.get(&"options", command.payload.get("options", {})) + var options: Dictionary = raw_options.duplicate(true) if raw_options is Dictionary else {} + var initial_cell_value: Variant = _parse_world_cell( + options.get(&"current_cell", options.get("current_cell", Vector3i.ZERO)) + ) + if initial_cell_value == null: + id_generator.restore_snapshot(id_before) + return CommandResult.failed( + CommandResult.Code.INVALID_PAYLOAD, + command.command_id, + "Guest current_cell must be Vector3i or [x, y, layer]." + ) + var initial_cell: Vector3i = initial_cell_value + var initial_traffic := path_service.get_cell_traffic(initial_cell) + var path_actor_id := _guest_path_actor_id(guest_id) + if initial_traffic.is_empty() or bool(initial_traffic.get(&"temporary_block", false)) \ + or path_service.has_actor_projection(path_actor_id): + id_generator.restore_snapshot(id_before) + return CommandResult.failed( + CommandResult.Code.VALIDATION_FAILED, + command.command_id, + "Guest current_cell must be an unblocked walkable cell with a free Path actor id." + ) + options[&"current_cell"] = initial_cell + var requested_gold := int(command.payload.get( + &"gold", + command.payload.get("gold", options.get(&"wallet_gold", options.get("wallet_gold", definition.wealth_min))) + )) + if requested_gold < 0: + id_generator.restore_snapshot(id_before) + return CommandResult.failed(CommandResult.Code.INVALID_PAYLOAD, command.command_id, "Guest Gold cannot be negative.") + var defer_spawn_notification := _operations_transaction_active \ + and operations_coordinator != null \ + and command.actor_id == &"operations_coordinator" + var account_id := _guest_wallet_account(guest_id) + var economy_before := economy_service.create_snapshot() + var guest_before := guest_service.create_snapshot() + var path_before := path_service.create_snapshot() + var movement_credits_before := _movement_credits.duplicate(true) + var events_before := domain_events.create_snapshot() + var progression_before := progression_service.create_snapshot() + if economy_service.has_account(account_id): + requested_gold = economy_service.get_balance(account_id) + else: + if not economy_service.register_account(account_id): + id_generator.restore_snapshot(id_before) + return CommandResult.failed(CommandResult.Code.TRANSACTION_APPLY_FAILED, command.command_id, "Guest wallet account could not be registered.") + if requested_gold > 0: + var initialized := economy_service.initialize_account( + account_id, + requested_gold, + _runtime_event_minute(), + StringName("guest.wallet.%s" % guest_id), + not defer_spawn_notification + ) + if not initialized.is_success(): + economy_service.restore_snapshot(economy_before) + id_generator.restore_snapshot(id_before) + initialized.command_id = command.command_id + return initialized + if defer_spawn_notification \ + and not _stage_operations_economy_notification(initialized): + economy_service.restore_snapshot(economy_before) + id_generator.restore_snapshot(id_before) + return CommandResult.failed( + CommandResult.Code.TRANSACTION_APPLY_FAILED, + command.command_id, + "Guest wallet notification could not be staged." + ) + options[&"wallet_gold"] = requested_gold + var guest := guest_service.spawn_guest( + guest_id, + guest_type_id, + _runtime_event_minute(), + options, + not defer_spawn_notification + ) + if guest != null and defer_spawn_notification: + # Runtime's Path projection and DomainEvent are part of the external + # transaction, while the observable GuestService signal is held until the + # Coordinator minute and DomainEvent batch have both committed. + _on_guest_spawned(guest) + if guest == null or not path_service.has_actor_projection(path_actor_id) \ + or path_service.get_actor_projected_cell(path_actor_id) != guest.current_cell \ + or not _movement_credits.has(path_actor_id): + economy_service.restore_snapshot(economy_before) + guest_service.restore_snapshot(guest_before) + path_service.restore_snapshot(path_before) + _movement_credits = movement_credits_before + domain_events.restore_snapshot(events_before) + id_generator.restore_snapshot(id_before) + return CommandResult.failed( + CommandResult.Code.TRANSACTION_APPLY_FAILED, + command.command_id, + guest_service.last_error if guest == null else "Guest Path projection failed." + ) + var next_guest_lifetime := int(progression_service.metrics[&"guest_lifetime"]) + 1 + if not progression_service.update_metrics({&"guest_lifetime": next_guest_lifetime}): + economy_service.restore_snapshot(economy_before) + guest_service.restore_snapshot(guest_before) + path_service.restore_snapshot(path_before) + _movement_credits = movement_credits_before + domain_events.restore_snapshot(events_before) + id_generator.restore_snapshot(id_before) + progression_service.restore_snapshot(progression_before) + return CommandResult.failed( + CommandResult.Code.TRANSACTION_APPLY_FAILED, + command.command_id, + "Progression lifetime Guest count could not be updated atomically." + ) + if defer_spawn_notification: + _operations_queued_guest_spawn_notifications.append(guest_id) + return CommandResult.succeeded(command.command_id, { + &"guest_id": guest_id, + &"guest_type_id": guest_type_id, + &"wallet_account": account_id, + &"wallet_gold": requested_gold, + &"guest_lifetime": next_guest_lifetime, + }) + +func _execute_transition_guest(command: SimulationCommand) -> CommandResult: + var guest_id := StringName(command.payload.get(&"guest_id", command.payload.get("guest_id", ""))) + var state_index := _guest_state_index(command.payload.get(&"state", command.payload.get("state", -1))) + if guest_id.is_empty() or state_index < 0: + return CommandResult.failed(CommandResult.Code.INVALID_PAYLOAD, command.command_id, "Guest id and valid state are required.") + var guest := guest_service.get_guest(guest_id) + if guest == null: + return CommandResult.failed(CommandResult.Code.NOT_FOUND, command.command_id, "Guest was not found.") + if not guest.queue_request_id.is_empty() or not guest.current_service_id.is_empty(): + return CommandResult.failed( + CommandResult.Code.TRANSACTION_APPLY_FAILED, + command.command_id, + "A queued or serviced Guest must transition through the owning Facility transaction." + ) + var result := guest_service.transition_guest( + guest_id, + state_index, + _runtime_event_minute(), + StringName(command.payload.get(&"reason", command.payload.get("reason", "PLAYER_COMMAND"))) + ) + return _command_result_from_dictionary(command.command_id, result, "Guest transition failed.") + +func _execute_decide_guest(command: SimulationCommand) -> CommandResult: + var guest_id := StringName(command.payload.get(&"guest_id", command.payload.get("guest_id", ""))) + var guest := guest_service.get_guest(guest_id) + if guest == null: + return CommandResult.failed(CommandResult.Code.NOT_FOUND, command.command_id, "Guest was not found.") + var wallet_account := _guest_wallet_account(guest_id) + if economy_service.has_account(wallet_account): + guest.wallet_gold = economy_service.get_balance(wallet_account) + var raw_candidates: Variant = command.payload.get(&"candidates", command.payload.get("candidates", [])) + var candidates: Array = raw_candidates.duplicate(true) if raw_candidates is Array else [] + if candidates.is_empty(): + candidates = _project_guest_candidates(guest_id) + var raw_context: Variant = command.payload.get(&"context", command.payload.get("context", {})) + var context: Dictionary = raw_context.duplicate(true) if raw_context is Dictionary else {} + context[&"absolute_minute"] = _runtime_event_minute() + var result := _decide_guest_with_log(guest_id, candidates, _runtime_event_minute(), context) + return _command_result_from_dictionary(command.command_id, result, "Guest decision failed.") + +func _execute_register_staff(command: SimulationCommand) -> CommandResult: + var staff_id := StringName(command.payload.get(&"staff_id", command.payload.get("staff_id", ""))) + var role_id := StringName(command.payload.get(&"role_id", command.payload.get("role_id", ""))) + var raw_options: Variant = command.payload.get(&"options", command.payload.get("options", {})) + if staff_id.is_empty() or role_id.is_empty() or not raw_options is Dictionary: + return CommandResult.failed(CommandResult.Code.INVALID_PAYLOAD, command.command_id, "Staff id, role id and options are required.") + if _is_mvp_staff_candidate_id(staff_id) or mvp_content_catalog.get_unique_character_ids().has(staff_id): + return CommandResult.failed( + CommandResult.Code.VALIDATION_FAILED, + command.command_id, + "Catalog identities must be created through their validated recruitment flow." + ) + var options: Dictionary = raw_options.duplicate(true) + var initial_cell_value: Variant = _parse_world_cell( + options.get(&"current_cell", options.get("current_cell", Vector3i.ZERO)) + ) + if initial_cell_value == null: + return CommandResult.failed( + CommandResult.Code.INVALID_PAYLOAD, + command.command_id, + "Staff current_cell must be Vector3i or [x, y, layer]." + ) + var initial_cell: Vector3i = initial_cell_value + var initial_traffic := path_service.get_cell_traffic(initial_cell) + var path_actor_id := _staff_path_actor_id(staff_id) + if initial_traffic.is_empty() or bool(initial_traffic.get(&"temporary_block", false)) \ + or path_service.has_actor_projection(path_actor_id): + return CommandResult.failed( + CommandResult.Code.VALIDATION_FAILED, + command.command_id, + "Staff current_cell must be an unblocked walkable cell with a free Path actor id." + ) + options[&"current_cell"] = initial_cell + var staff_before := staff_service.create_snapshot() + var path_before := path_service.create_snapshot() + var credits_before := _movement_credits.duplicate(true) + var events_before := domain_events.create_snapshot() + var ids_before := id_generator.create_snapshot() + options[&"employment_effective_minute"] = _runtime_event_minute() + var result := staff_service.create_staff(staff_id, role_id, options) + if result.is_success() and ( + not path_service.has_actor_projection(path_actor_id) + or path_service.get_actor_projected_cell(path_actor_id) != initial_cell + or not _movement_credits.has(path_actor_id) + ): + staff_service.restore_snapshot(staff_before) + path_service.restore_snapshot(path_before) + _movement_credits = credits_before + domain_events.restore_snapshot(events_before) + id_generator.restore_snapshot(ids_before) + result = CommandResult.failed( + CommandResult.Code.TRANSACTION_APPLY_FAILED, + command.command_id, + "Staff Path projection failed." + ) + result.command_id = command.command_id + return result + + +# Original 6.1 plus the approved Anri staff-screen choice. Other unique +# characters still have no approved joining timing. Zero signing cost and both +# canonical wage fields must be actual integers; payload aliases cannot qualify. +func _is_approved_free_unique_hire(candidate_id: StringName, canonical: Dictionary) -> bool: + if candidate_id != &"heroine_anri" or not mvp_content_catalog.is_valid(): + return false + var staff_id_value: Variant = canonical.get(&"staff_id", null) + var role_value: Variant = canonical.get(&"role_id", null) + var options_value: Variant = canonical.get(&"options", null) + if typeof(role_value) not in [TYPE_STRING, TYPE_STRING_NAME] or StringName(role_value) != &"innkeeper": + return false + if typeof(staff_id_value) not in [TYPE_STRING, TYPE_STRING_NAME] \ + or StringName(staff_id_value) != candidate_id or not options_value is Dictionary: + return false + var options: Dictionary = options_value + var employment_value: Variant = options.get(&"employment_kind", null) + if typeof(employment_value) not in [TYPE_STRING, TYPE_STRING_NAME] \ + or StringName(employment_value) != &"UNIQUE" \ + or typeof(canonical.get(&"adult_verified", null)) != TYPE_BOOL \ + or not bool(canonical[&"adult_verified"]): + return false + for value: Variant in [canonical.get(&"signing_cost", null), canonical.get(&"daily_wage", null), options.get(&"daily_wage", null)]: + if typeof(value) != TYPE_INT or int(value) != 0: + return false + return true + + +func _execute_hire_staff_candidate(command: SimulationCommand) -> CommandResult: + # Ordinary contracts retain the investment gate and its existing error + # priority. Only Anri has approved free recruitment; classify that exception + # from validated canonical data, never caller-supplied costs or employment. + var operations_gate := _operations_construction_investment_gate(command.command_id) + var raw_candidate_id: Variant = command.payload.get( + &"candidate_id", command.payload.get("candidate_id", null) + ) + if operations_gate != null and ( + typeof(raw_candidate_id) not in [TYPE_STRING, TYPE_STRING_NAME] + or StringName(raw_candidate_id) != &"heroine_anri" + ): + return operations_gate + if typeof(raw_candidate_id) not in [TYPE_STRING, TYPE_STRING_NAME]: + return CommandResult.failed( + CommandResult.Code.INVALID_PAYLOAD, + command.command_id, + "A catalog Staff candidate id is required." + ) + var candidate_id := StringName(raw_candidate_id) + var canonical := mvp_content_catalog.staff_hire_payload(candidate_id) + if canonical.is_empty(): + return CommandResult.failed( + CommandResult.Code.NOT_FOUND, + command.command_id, + "Staff candidate was not found in the validated MVP catalog." + ) + if not bool(canonical.get(&"adult_verified", false)): + return CommandResult.failed( + CommandResult.Code.VALIDATION_FAILED, + command.command_id, + "Staff candidate does not have verified adult eligibility." + ) + if operations_gate != null: + var reason := StringName(operations_gate.details.get(&"reason", &"")) + var can_join_without_investment := _is_approved_free_unique_hire(candidate_id, canonical) \ + and reason in [&"ARREARS_REMAIN", &"UNPAID_EXPENSES"] \ + and not _operations_should_block_tick() + if not can_join_without_investment: + return operations_gate + if _hired_staff_candidate_ids.has(candidate_id) or staff_service.has_staff(candidate_id): + return CommandResult.failed( + CommandResult.Code.DUPLICATE_COMMAND_ID, + command.command_id, + "Staff candidate has already been hired." + ) + var initial_cell_value: Variant = _parse_world_cell( + command.payload.get(&"current_cell", command.payload.get("current_cell", Vector3i.ZERO)) + ) + if initial_cell_value == null: + return CommandResult.failed( + CommandResult.Code.INVALID_PAYLOAD, + command.command_id, + "Staff current_cell must be Vector3i or [x, y, layer]." + ) + var initial_cell: Vector3i = initial_cell_value + var path_actor_id := _staff_path_actor_id(candidate_id) + var traffic := path_service.get_cell_traffic(initial_cell) + if traffic.is_empty() or bool(traffic.get(&"temporary_block", false)) \ + or path_service.has_actor_projection(path_actor_id): + return CommandResult.failed( + CommandResult.Code.VALIDATION_FAILED, + command.command_id, + "Staff current_cell must be an unblocked walkable cell with a free Path actor id." + ) + var assigned_facility_id := StringName(command.payload.get( + &"facility_id", command.payload.get("facility_id", "") + )) + if not assigned_facility_id.is_empty() \ + and not facility_service.has_instance(assigned_facility_id): + return CommandResult.failed( + CommandResult.Code.NOT_FOUND, + command.command_id, + "Assigned Staff facility was not found." + ) + + var before := { + &"economy": economy_service.create_snapshot(), + &"facility_upkeep": facility_upkeep.create_snapshot(), + &"upkeep_settlement": upkeep_settlement.create_snapshot(), + &"game_state": _state.create_snapshot(), + &"staff": staff_service.create_snapshot(), + &"path": path_service.create_snapshot(), + &"elevators": elevators.create_snapshot(), + &"legacy_stairs": legacy_stairs.create_snapshot(), + &"movement_credits": _movement_credits.duplicate(true), + &"domain_events": domain_events.create_snapshot(), + &"id_generator": id_generator.create_snapshot(), + &"hired_ids": _hired_staff_candidate_ids.duplicate(true), + &"staffing_projection_enabled": _staffing_projection_enabled, + } + var signing_cost := int(canonical[&"signing_cost"]) + if signing_cost > 0: + var payment := economy_service.transfer( + PLAYER_CASH_ACCOUNT, + OPERATING_EXPENSE_ACCOUNT, + signing_cost, + _runtime_event_minute(), + &"STAFF_SIGNING_COST", + candidate_id, + StringName("staff_signing_%s" % candidate_id), + &"staff.signing_cost" + ) + if not payment.is_success(): + payment.command_id = command.command_id + return payment + + var options: Dictionary = (canonical[&"options"] as Dictionary).duplicate(true) + options[&"current_cell"] = initial_cell + options[&"assigned_facility_id"] = assigned_facility_id + options[&"employment_effective_minute"] = _runtime_event_minute() + var staff_result := staff_service.create_staff( + candidate_id, + StringName(canonical[&"role_id"]), + options + ) + if not staff_result.is_success(): + _rollback_staff_candidate_hire(before) + staff_result.command_id = command.command_id + return staff_result + var preferred_shift := StringName(canonical[&"preferred_shift"]) + var shift_day := _candidate_shift_start_day(preferred_shift, _runtime_event_minute()) + var shift_result: CommandResult + if preferred_shift in [&"day", &"night"]: + shift_result = staff_service.create_standard_shift( + candidate_id, + preferred_shift, + shift_day, + assigned_facility_id + ) + else: + shift_result = staff_service.create_flex_shift( + candidate_id, + shift_day, + ShiftInstance.DAY_START, + ShiftInstance.DAY_END, + assigned_facility_id + ) + var shift_id := StringName(shift_result.details.get(&"shift_id", &"")) + var hired_staff := staff_service.get_staff(candidate_id) + var hired_shift := staff_service.get_shift(shift_id) + if not shift_result.is_success() or hired_staff == null or hired_shift == null \ + or hired_staff.daily_wage != int(canonical[&"daily_wage"]) \ + or hired_staff.get_employment_kind_name() != StringName(canonical[&"options"][&"employment_kind"]) \ + or not path_service.has_actor_projection(path_actor_id) \ + or path_service.get_actor_projected_cell(path_actor_id) != initial_cell \ + or not _movement_credits.has(path_actor_id): + _rollback_staff_candidate_hire(before) + return CommandResult.failed( + CommandResult.Code.TRANSACTION_APPLY_FAILED, + command.command_id, + "Staff hire could not commit its employee, Shift, finance, and Path state atomically." + ) + _hired_staff_candidate_ids[candidate_id] = true + _publish_actor_event( + &"STAFF_CANDIDATE_HIRED", + &"simulation_runtime", + [candidate_id, shift_id], + { + &"candidate_id": candidate_id, + &"role_id": StringName(canonical[&"role_id"]), + &"daily_wage": int(canonical[&"daily_wage"]), + &"employment_kind": hired_staff.get_employment_kind_name(), + &"signing_cost": signing_cost, + &"shift_id": shift_id, + &"preferred_shift": preferred_shift, + } + ) + return CommandResult.succeeded(command.command_id, { + &"candidate_id": candidate_id, + &"staff_id": candidate_id, + &"role_id": StringName(canonical[&"role_id"]), + &"daily_wage": int(canonical[&"daily_wage"]), + &"employment_kind": hired_staff.get_employment_kind_name(), + &"signing_cost": signing_cost, + &"shift_id": shift_id, + &"player_cash": economy_service.get_balance(PLAYER_CASH_ACCOUNT), + }) + +func _execute_create_staff_shift(command: SimulationCommand) -> CommandResult: + var staff_id := StringName(command.payload.get(&"staff_id", command.payload.get("staff_id", ""))) + var shift_id := StringName(command.payload.get(&"shift_id", command.payload.get("shift_id", ""))) + var start_day := int(command.payload.get(&"start_day", command.payload.get("start_day", _runtime_event_minute() / 1_440))) + var facility_id := StringName(command.payload.get(&"facility_id", command.payload.get("facility_id", ""))) + var template_id := StringName(command.payload.get(&"template_id", command.payload.get("template_id", "FLEX"))) + var result: CommandResult + if String(template_id).to_upper() in ["DAY", "NIGHT"]: + result = staff_service.create_standard_shift(staff_id, template_id, start_day, facility_id, shift_id) + else: + var start_minute := int(command.payload.get(&"start_minute_of_day", command.payload.get("start_minute_of_day", posmod(_runtime_event_minute(), 1_440)))) + var end_minute := int(command.payload.get(&"end_minute_of_day", command.payload.get("end_minute_of_day", posmod(start_minute + 480, 1_440)))) + result = staff_service.create_flex_shift(staff_id, start_day, start_minute, end_minute, facility_id, shift_id) + result.command_id = command.command_id + return result + + +func _candidate_shift_start_day(preferred_shift: StringName, absolute_minute: int) -> int: + var day := absolute_minute / ShiftInstance.MINUTES_PER_DAY + var minute_of_day := posmod(absolute_minute, ShiftInstance.MINUTES_PER_DAY) + # A NIGHT record starts at 18:00 of its named day and remains meaningful even + # when hired after that start. DAY/FLEX records move to tomorrow once today's + # daytime window has ended. + if preferred_shift != &"night" and minute_of_day >= ShiftInstance.DAY_END: + day += 1 + return day + + +func _rollback_staff_candidate_hire(snapshot: Dictionary) -> bool: + var restored := economy_service.restore_snapshot(snapshot[&"economy"]) + restored = _state.restore_snapshot(snapshot[&"game_state"]) and restored + restored = staff_service.restore_snapshot(snapshot[&"staff"]) and restored + restored = path_service.restore_snapshot(snapshot[&"path"]) and restored + restored = elevators.restore_snapshot(snapshot[&"elevators"]) and restored + restored = legacy_stairs.restore_snapshot(snapshot[&"legacy_stairs"]) and restored + _movement_credits = (snapshot[&"movement_credits"] as Dictionary).duplicate(true) + restored = domain_events.restore_snapshot(snapshot[&"domain_events"]) and restored + restored = id_generator.restore_snapshot(snapshot[&"id_generator"]) and restored + _hired_staff_candidate_ids = (snapshot[&"hired_ids"] as Dictionary).duplicate(true) + _staffing_projection_enabled = bool(snapshot[&"staffing_projection_enabled"]) + return restored and _validate_runtime_path_crosslinks( + path_service, + guest_service, + staff_service, + _movement_credits, + _runtime_event_minute() + ) + +func _staff_role_matches_facility(role_id: StringName, definition: FacilityDefinition) -> bool: + if definition.required_roles.is_empty() or role_id in definition.required_roles: + return true + # Existing public-bath data uses a legacy job ID; the paid roster exposes + # the canonical attendant. The other aliases likewise retain saved job IDs. + if definition.id == &"public_bath" and role_id == &"attendant" and &"bath_attendant" in definition.required_roles: + return true + if definition.id == &"massage_room" and role_id == &"masseur" and &"therapist" in definition.required_roles: + return true + return definition.id == &"security_station" and role_id == &"guard" and &"security" in definition.required_roles + +func _execute_set_staff_work_assignment(command: SimulationCommand) -> CommandResult: + for key: StringName in [&"staff_id", &"facility_id", &"template_id"]: + if typeof(command.payload.get(key, null)) not in [TYPE_STRING, TYPE_STRING_NAME]: + return _staff_assignment_failure(command.command_id, &"INVALID_PAYLOAD", "Staff, facility, and shift IDs must be strings.") + var staff_id := StringName(command.payload[&"staff_id"]) + var facility_id := StringName(command.payload[&"facility_id"]) + var template_id := StringName(command.payload[&"template_id"]) + var staff := staff_service.get_staff(staff_id) + if staff == null: + return _staff_assignment_failure(command.command_id, &"STAFF_NOT_FOUND", "Staff was not found.") + if template_id not in [&"DAY", &"NIGHT"]: + return _staff_assignment_failure(command.command_id, &"INVALID_TEMPLATE", "Choose DAY or NIGHT.") + var block_reason := _staff_assignment_block_reason(staff) + if not block_reason.is_empty(): + var messages := { + &"DEFENSE_LOCKED": "Staff is participating in a defense operation.", + &"EVENT_LOCKED": "Staff is participating in an event.", + &"STAFF_BUSY": "Finish the current task before changing assignment.", + &"STAFF_UNAVAILABLE": "Staff cannot change assignment in the current state.", + } + return _staff_assignment_failure(command.command_id, block_reason, messages[block_reason]) + if not facility_id.is_empty(): + var facility := facility_service.get_instance(facility_id) + if facility == null: + return _staff_assignment_failure(command.command_id, &"FACILITY_NOT_FOUND", "Facility was not found.") + var definition := definition_registry.get_facility(facility.definition_id) + if definition == null or definition.staff_max <= 0: + return _staff_assignment_failure(command.command_id, &"FACILITY_UNSTAFFED", "This facility does not accept Staff assignments.") + if not _staff_role_matches_facility(staff.role_id, definition) and not _is_anri_manager(staff): + return _staff_assignment_failure(command.command_id, &"ROLE_MISMATCH", "Staff role does not match this facility.") + if _staff_assignment_count(facility_id, template_id, staff_id) >= definition.staff_max: + return _staff_assignment_failure(command.command_id, &"FACILITY_FULL", "This shift has reached the facility Staff limit.") + if not _staff_can_reach_facility(staff, facility): + return _staff_assignment_failure(command.command_id, &"NO_ROUTE", "Staff cannot reach this facility through an accessible route.") + var result := staff_service.set_work_assignment(staff_id, facility_id, template_id, _runtime_event_minute()) + result.command_id = command.command_id + if result.is_success(): + var actor_id := _staff_path_actor_id(staff_id) + var trip := elevators.get_actor_trip(actor_id) + if result.details.get(&"reason", &"") == &"ASSIGNMENT_UPDATED" and trip.get(&"state", &"") == PathService.ELEVATOR_REQUEST_QUEUED: + elevators.cancel_actor(actor_id) + _staffing_projection_enabled = true + _sync_facility_staffing(_runtime_event_minute()) + if result.details.get(&"reason", &"") == &"ASSIGNMENT_UPDATED": + _publish_actor_event(&"STAFF_WORK_ASSIGNMENT_CHANGED", &"staff_service", [staff_id], result.details) + return result + + +## Shared by the command and presentation; always derived from live state. +func _staff_assignment_block_reason(staff: StaffRuntime) -> StringName: + if not _defense_lock_owner(staff.staff_id, &"DEFENDER").is_empty(): + return &"DEFENSE_LOCKED" + if event_service.get_actor_locks().has(staff.staff_id): + return &"EVENT_LOCKED" + if not staff.current_task_id.is_empty(): + return &"STAFF_BUSY" + if not staff.can_take_shift() or staff.state not in [StaffRuntime.State.OFF_DUTY, StaffRuntime.State.COMMUTING, StaffRuntime.State.AVAILABLE]: + return &"STAFF_UNAVAILABLE" + return &"" + + +func _staff_assignment_failure(command_id: int, reason: StringName, message: String) -> CommandResult: + return CommandResult.failed(CommandResult.Code.VALIDATION_FAILED, command_id, message, {&"reason": reason}) + + +func _staff_assignment_count(facility_id: StringName, template_id: StringName, excluded_staff: StringName) -> int: + var counted: Dictionary = {} + for row: Dictionary in staff_service.get_work_assignments(): + var staff_id := StringName(row[&"staff_id"]) + if staff_id != excluded_staff and StringName(row[&"facility_id"]) == facility_id \ + and StringName(row[&"template_id"]) == template_id: + counted[staff_id] = true + var interval := StaffService._work_interval(template_id, _runtime_event_minute()) + for shift: ShiftInstance in staff_service.get_shifts(): + if shift.staff_id != excluded_staff and not shift.cancelled \ + and shift.assigned_facility_id == facility_id \ + and shift.end_at > maxi(interval.x, _runtime_event_minute()) and shift.start_at < interval.y: + counted[shift.staff_id] = true + return counted.size() + + +func _staff_can_reach_facility(staff: StaffRuntime, facility: FacilityInstance) -> bool: + var occupied: Array[Vector3i] = facility.occupied_cells.duplicate() + if occupied.is_empty(): + occupied.append(facility.origin) + var candidates: Dictionary = {} + for cell: Vector3i in occupied: + for offset: Vector3i in [Vector3i.ZERO, Vector3i.LEFT, Vector3i.RIGHT]: + var candidate := cell + offset + if path_service.has_cell(candidate): + candidates[candidate] = true + for candidate: Vector3i in candidates: + if not path_service.find_path(staff.current_cell, candidate, [WorldService.LAYER_FRONT, WorldService.LAYER_BACK], [&"staff"]).is_empty(): + return true + return false + + +func _execute_set_staff_schedule_override(command: SimulationCommand) -> CommandResult: + var staff_id := StringName(command.payload.get(&"staff_id", command.payload.get("staff_id", ""))) + var start_day := int(command.payload.get(&"start_day", command.payload.get("start_day", -1))) + var patch_value: Variant = command.payload.get(&"patch", command.payload.get("patch", null)) + if not patch_value is Dictionary: + return CommandResult.failed(CommandResult.Code.INVALID_PAYLOAD, command.command_id, "Schedule override patch must be a Dictionary.") + var result := staff_service.set_schedule_override(staff_id, start_day, patch_value) + result.command_id = command.command_id + return result + +func _execute_create_staff_task(command: SimulationCommand) -> CommandResult: + var task_type := StringName(command.payload.get(&"task_type", command.payload.get("task_type", ""))) + var priority := int(command.payload.get(&"priority", command.payload.get("priority", 50))) + var task_id := StringName(command.payload.get(&"task_id", command.payload.get("task_id", ""))) + var raw_options: Variant = command.payload.get(&"options", command.payload.get("options", {})) + if task_type == CleaningBridge.TASK_TYPE: + return CommandResult.failed(CommandResult.Code.INVALID_PAYLOAD, command.command_id, "Cleaning jobs are scheduled from actual facility dirt.") + if task_type.is_empty() or not raw_options is Dictionary: + return CommandResult.failed(CommandResult.Code.INVALID_PAYLOAD, command.command_id, "Task type and options are required.") + var result := staff_service.create_task(task_type, priority, _runtime_event_minute(), raw_options, task_id) + result.command_id = command.command_id + return result + +func _execute_begin_staff_task(command: SimulationCommand) -> CommandResult: + var task_id := StringName(command.payload.get(&"task_id", command.payload.get("task_id", ""))) + var result := staff_service.begin_task(task_id, _runtime_event_minute()) + result.command_id = command.command_id + return result + +func _execute_progress_staff_task(command: SimulationCommand) -> CommandResult: + var task_id := StringName(command.payload.get(&"task_id", command.payload.get("task_id", ""))) + var task := staff_service.get_task(task_id) + if task != null and task.task_type == CleaningBridge.TASK_TYPE: + return CommandResult.failed(CommandResult.Code.INVALID_PAYLOAD, command.command_id, "Cleaning work advances only at its physical destination with game time.") + var minutes := int(command.payload.get(&"minutes", command.payload.get("minutes", 1))) + var result := staff_service.progress_task(task_id, minutes, _runtime_event_minute()) + result.command_id = command.command_id + return result + +func _execute_create_relationship(command: SimulationCommand) -> CommandResult: + var actor_a_id := StringName(command.payload.get(&"actor_a_id", command.payload.get("actor_a_id", ""))) + var actor_b_id := StringName(command.payload.get(&"actor_b_id", command.payload.get("actor_b_id", ""))) + if not _event_actor_exists(actor_a_id) or not _event_actor_exists(actor_b_id): + return CommandResult.failed(CommandResult.Code.NOT_FOUND, command.command_id, "Both relationship actors must exist.") + var relationship_id := StringName(command.payload.get(&"relationship_id", command.payload.get("relationship_id", ""))) + var id_before := id_generator.create_snapshot() + if relationship_id.is_empty(): + relationship_id = id_generator.next_id(&"relationship") + var raw_options: Variant = command.payload.get(&"options", command.payload.get("options", {})) + var options: Dictionary = raw_options.duplicate(true) if raw_options is Dictionary else {} + var result := relationship_service.create_relationship(relationship_id, actor_a_id, actor_b_id, options) + if not result.is_success(): + id_generator.restore_snapshot(id_before) + result.command_id = command.command_id + return result + +func _execute_change_relationship(command: SimulationCommand) -> CommandResult: + var relationship_id := StringName(command.payload.get(&"relationship_id", command.payload.get("relationship_id", ""))) + var metric_id := StringName(command.payload.get(&"metric_id", command.payload.get("metric_id", ""))) + var delta := int(command.payload.get(&"delta", command.payload.get("delta", 0))) + var raw_options: Variant = command.payload.get(&"options", command.payload.get("options", {})) + var options: Dictionary = raw_options.duplicate(true) if raw_options is Dictionary else {} + var result := relationship_service.apply_value_change( + relationship_id, metric_id, delta, _runtime_event_minute(), options + ) + result.command_id = command.command_id + return result + +func _execute_attack_relationship_shield(command: SimulationCommand) -> CommandResult: + var relationship_id := StringName(command.payload.get(&"relationship_id", command.payload.get("relationship_id", ""))) + var action_id := StringName(command.payload.get(&"action_id", command.payload.get("action_id", ""))) + var raw_terms: Variant = command.payload.get(&"terms", command.payload.get("terms", {})) + if not raw_terms is Dictionary: + return CommandResult.failed(CommandResult.Code.INVALID_PAYLOAD, command.command_id, "Shield attack terms must be a Dictionary.") + var result := relationship_service.apply_shield_attack( + relationship_id, action_id, _runtime_event_minute(), raw_terms + ) + result.command_id = command.command_id + return result + +func _execute_request_route_lock(command: SimulationCommand) -> CommandResult: + var relationship_id := StringName(command.payload.get(&"relationship_id", command.payload.get("relationship_id", ""))) + var route_id := StringName(command.payload.get(&"route_id", command.payload.get("route_id", ""))) + var lock_index := _route_lock_index(command.payload.get(&"route_lock", command.payload.get("route_lock", -1))) + if lock_index < 0: + return CommandResult.failed(CommandResult.Code.INVALID_PAYLOAD, command.command_id, "A valid Route Lock state is required.") + var result := relationship_service.request_route_lock( + relationship_id, + lock_index, + route_id, + bool(command.payload.get(&"irreversible_event", command.payload.get("irreversible_event", false))) + ) + result.command_id = command.command_id + return result + +func _execute_confirm_route_lock(command: SimulationCommand) -> CommandResult: + var relationship_id := StringName(command.payload.get(&"relationship_id", command.payload.get("relationship_id", ""))) + var route_id := StringName(command.payload.get(&"route_id", command.payload.get("route_id", ""))) + var result := relationship_service.confirm_route_lock(relationship_id, route_id) + result.command_id = command.command_id + return result + +func _execute_register_event_definition(command: SimulationCommand) -> CommandResult: + var raw_definition: Variant = command.payload.get(&"definition", command.payload.get("definition", null)) + var definition := EventDefinition.from_variant(raw_definition) + if definition == null: + return CommandResult.failed(CommandResult.Code.INVALID_PAYLOAD, command.command_id, "Event definition is malformed.") + var result := event_service.register_definition(definition) + result.command_id = command.command_id + return result + +func _execute_enqueue_event(command: SimulationCommand) -> CommandResult: + var event_id := StringName(command.payload.get(&"event_id", command.payload.get("event_id", ""))) + var actor_ids := _name_array(command.payload.get(&"actor_ids", command.payload.get("actor_ids", []))) + var raw_options: Variant = command.payload.get(&"options", command.payload.get("options", {})) + var options: Dictionary = raw_options.duplicate(true) if raw_options is Dictionary else {} + var result := event_service.enqueue_candidate(event_id, actor_ids, _runtime_event_minute(), options) + result.command_id = command.command_id + return result + +func _execute_reserve_event_choice_cost(command: SimulationCommand) -> CommandResult: + var instance_id := StringName(command.payload.get(&"event_instance_id", command.payload.get("event_instance_id", ""))) + var choice_id := StringName(command.payload.get(&"choice_id", command.payload.get("choice_id", ""))) + var result := event_service.reserve_choice_cost(instance_id, choice_id, _runtime_event_minute()) + result.command_id = command.command_id + return result + +func _execute_submit_event_choice(command: SimulationCommand) -> CommandResult: + var instance_id := StringName(command.payload.get(&"event_instance_id", command.payload.get("event_instance_id", ""))) + var choice_instance_id := StringName(command.payload.get(&"choice_instance_id", command.payload.get("choice_instance_id", ""))) + var choice_id := StringName(command.payload.get(&"choice_id", command.payload.get("choice_id", ""))) + var result := event_service.submit_choice( + instance_id, choice_instance_id, choice_id, _runtime_event_minute() + ) + if result.is_success() and event_service.get_foreground_instance() == null: + _clock.set_paused(false) + result.command_id = command.command_id + return result + +func _execute_report_authority_incident(command: SimulationCommand) -> CommandResult: + var incident_id := StringName(command.payload.get(&"incident_id", command.payload.get("incident_id", ""))) + var incident_type := StringName(command.payload.get(&"incident_type", command.payload.get("incident_type", ""))) + var detector_id := StringName(command.payload.get(&"detector_id", command.payload.get("detector_id", command.actor_id))) + var raw_options: Variant = command.payload.get(&"options", command.payload.get("options", {})) + if not raw_options is Dictionary: + return CommandResult.failed(CommandResult.Code.INVALID_PAYLOAD, command.command_id, "Incident options must be a Dictionary.") + var id_before := id_generator.create_snapshot() + if incident_id.is_empty(): + incident_id = id_generator.next_id(&"incident") + var result := authority_service.create_incident( + incident_id, + incident_type, + _runtime_event_minute(), + detector_id, + (raw_options as Dictionary).duplicate(true) + ) + if not result.is_success(): + id_generator.restore_snapshot(id_before) + result.command_id = command.command_id + return result + +func _execute_register_authority_evidence(command: SimulationCommand) -> CommandResult: + var evidence_id := StringName(command.payload.get(&"evidence_id", command.payload.get("evidence_id", ""))) + var source := StringName(command.payload.get(&"source", command.payload.get("source", ""))) + var severity := int(command.payload.get(&"severity", command.payload.get("severity", -1))) + var raw_options: Variant = command.payload.get(&"options", command.payload.get("options", {})) + if not raw_options is Dictionary: + return CommandResult.failed(CommandResult.Code.INVALID_PAYLOAD, command.command_id, "Evidence options must be a Dictionary.") + var options: Dictionary = (raw_options as Dictionary).duplicate(true) + if not options.has(&"created_at") and not options.has("created_at"): + options[&"created_at"] = _runtime_event_minute() + var id_before := id_generator.create_snapshot() + if evidence_id.is_empty(): + evidence_id = id_generator.next_id(&"evidence") + var result := authority_service.create_evidence(evidence_id, source, severity, options) + if not result.is_success(): + id_generator.restore_snapshot(id_before) + result.command_id = command.command_id + return result + +func _execute_discover_authority_evidence(command: SimulationCommand) -> CommandResult: + var evidence_id := StringName(command.payload.get(&"evidence_id", command.payload.get("evidence_id", ""))) + var inspector_id := StringName(command.payload.get(&"inspector_id", command.payload.get("inspector_id", command.actor_id))) + var parsed_cell: Variant = _parse_world_cell(command.payload.get(&"inspector_cell", command.payload.get("inspector_cell", null))) + var raw_context: Variant = command.payload.get(&"context", command.payload.get("context", {})) + if parsed_cell == null or not raw_context is Dictionary: + return CommandResult.failed(CommandResult.Code.INVALID_PAYLOAD, command.command_id, "Evidence discovery requires inspector_cell and Dictionary context.") + var result := authority_service.attempt_discover_evidence( + evidence_id, + inspector_id, + parsed_cell, + _runtime_event_minute(), + (raw_context as Dictionary).duplicate(true) + ) + result.command_id = command.command_id + return result + +func _execute_evaluate_authority_inspection(command: SimulationCommand) -> CommandResult: + var raw_terms: Variant = command.payload.get(&"terms", command.payload.get("terms", {})) + if not raw_terms is Dictionary: + return CommandResult.failed(CommandResult.Code.INVALID_PAYLOAD, command.command_id, "Inspection evaluation terms must be a Dictionary.") + var result := authority_service.evaluate_inspection( + _runtime_event_minute(), + (raw_terms as Dictionary).duplicate(true) + ) + if result.is_success() and not result.is_replay: + _publish_authority_evaluation(result.details) + result.command_id = command.command_id + return result + +func _execute_schedule_authority_inspection(command: SimulationCommand) -> CommandResult: + var raw_options: Variant = command.payload.get(&"options", command.payload.get("options", {})) + if not raw_options is Dictionary: + return CommandResult.failed(CommandResult.Code.INVALID_PAYLOAD, command.command_id, "Inspection options must be a Dictionary.") + var scheduled_at := int(command.payload.get(&"scheduled_at", command.payload.get("scheduled_at", _runtime_event_minute()))) + var result := authority_service.schedule_inspection( + scheduled_at, + (raw_options as Dictionary).duplicate(true) + ) + result.command_id = command.command_id + return result + +func _execute_start_authority_inspection(command: SimulationCommand) -> CommandResult: + var inspection_id := StringName(command.payload.get(&"inspection_id", command.payload.get("inspection_id", ""))) + var result := authority_service.start_inspection(inspection_id, _runtime_event_minute()) + result.command_id = command.command_id + return result + +func _execute_arrive_authority_inspection(command: SimulationCommand) -> CommandResult: + var inspection_id := StringName(command.payload.get(&"inspection_id", command.payload.get("inspection_id", ""))) + var result := authority_service.mark_inspector_arrived(inspection_id, _runtime_event_minute()) + result.command_id = command.command_id + return result + +func _execute_plan_authority_inspection(command: SimulationCommand) -> CommandResult: + var inspection_id := StringName(command.payload.get(&"inspection_id", command.payload.get("inspection_id", ""))) + var raw_candidates: Variant = command.payload.get(&"candidates", command.payload.get("candidates", null)) + if not raw_candidates is Array: + return CommandResult.failed(CommandResult.Code.INVALID_PAYLOAD, command.command_id, "Inspection patrol candidates must be an Array.") + var time_budget := int(command.payload.get(&"time_budget_minutes", command.payload.get("time_budget_minutes", 120))) + var result := authority_service.plan_patrol( + inspection_id, + raw_candidates, + _runtime_event_minute(), + time_budget + ) + result.command_id = command.command_id + return result + +func _execute_advance_authority_inspection(command: SimulationCommand) -> CommandResult: + var inspection_id := StringName(command.payload.get(&"inspection_id", command.payload.get("inspection_id", ""))) + var raw_context: Variant = command.payload.get(&"context", command.payload.get("context", {})) + if not raw_context is Dictionary: + return CommandResult.failed(CommandResult.Code.INVALID_PAYLOAD, command.command_id, "Inspection path context must be a Dictionary.") + var result := authority_service.advance_patrol( + inspection_id, + _runtime_event_minute(), + (raw_context as Dictionary).duplicate(true) + ) + if result.is_success() and result.details.has(&"reachable"): + var event_type := &"INSPECTION_WAYPOINT_REACHED" if bool(result.details[&"reachable"]) else &"INSPECTION_PATH_BLOCKED" + _publish_actor_event( + event_type, + &"authority_service", + [inspection_id, StringName(result.details.get(&"waypoint_id", ""))], + result.details.duplicate(true) + ) + result.command_id = command.command_id + return result + +func _execute_inspect_authority_waypoint(command: SimulationCommand) -> CommandResult: + var inspection_id := StringName(command.payload.get(&"inspection_id", command.payload.get("inspection_id", ""))) + var evidence_ids := _name_array(command.payload.get(&"evidence_ids", command.payload.get("evidence_ids", []))) + var raw_context: Variant = command.payload.get(&"context", command.payload.get("context", {})) + if not raw_context is Dictionary: + return CommandResult.failed(CommandResult.Code.INVALID_PAYLOAD, command.command_id, "Inspection evidence context must be a Dictionary.") + var result := authority_service.inspect_current_waypoint( + inspection_id, + evidence_ids, + _runtime_event_minute(), + (raw_context as Dictionary).duplicate(true) + ) + if result.is_success(): + _publish_actor_event( + &"INSPECTION_WAYPOINT_INSPECTED", + &"authority_service", + [inspection_id], + result.details.duplicate(true) + ) + result.command_id = command.command_id + return result + +func _execute_resolve_authority_inspection(command: SimulationCommand) -> CommandResult: + var inspection_id := StringName(command.payload.get(&"inspection_id", command.payload.get("inspection_id", ""))) + var resolution_id := StringName(command.payload.get(&"resolution_id", command.payload.get("resolution_id", ""))) + var raw_options: Variant = command.payload.get(&"options", command.payload.get("options", {})) + if not raw_options is Dictionary: + return CommandResult.failed(CommandResult.Code.INVALID_PAYLOAD, command.command_id, "Inspection resolution options must be a Dictionary.") + var id_before := id_generator.create_snapshot() + if resolution_id.is_empty(): + resolution_id = id_generator.next_id(&"resolution") + var result := authority_service.resolve_inspection( + inspection_id, + resolution_id, + _runtime_event_minute(), + (raw_options as Dictionary).duplicate(true) + ) + if not result.is_success(): + id_generator.restore_snapshot(id_before) + result.command_id = command.command_id + return result + +func _execute_depart_authority_inspection(command: SimulationCommand) -> CommandResult: + var inspection_id := StringName(command.payload.get(&"inspection_id", command.payload.get("inspection_id", ""))) + var result := authority_service.depart_inspection(inspection_id, _runtime_event_minute()) + result.command_id = command.command_id + return result + +## Existing Defense commands remain authoritative. This report only explains +## why a live employee or target can be registered; no balance data is invented. +func get_defense_management_report(selected_raid_id: StringName = &"") -> Dictionary: + var raids: Array[Dictionary] = defense_service.get_management_rows() + var selected: Dictionary = {} + for row: Dictionary in raids: + if row[&"raid_id"] == selected_raid_id: + selected = row + break + if selected.is_empty() and not raids.is_empty(): + selected = raids[0] + for row: Dictionary in raids: + if int(row[&"state"]) not in DefenseService.TERMINAL_STATES: + selected = row + break + var raid_id := StringName(selected.get(&"raid_id", &"")) + var detail := defense_service.get_management_detail(raid_id) + var defenders: Array = detail.get(&"defenders", []) + var assets: Array = detail.get(&"assets", []) + var defender_owners: Dictionary = {} + var asset_owners: Dictionary = {} + for row: Dictionary in raids: + if int(row[&"state"]) in DefenseService.TERMINAL_STATES: continue + for actor_id: StringName in row[&"defender_ids"]: defender_owners[actor_id] = row[&"raid_id"] + for asset_id: StringName in row[&"asset_ids"]: asset_owners[asset_id] = row[&"raid_id"] + var preparing := int(selected.get(&"state", -1)) == DefenseService.RaidState.PREPARING + var staff_rows: Array[Dictionary] = [] + for staff: StaffRuntime in staff_service.get_staff_members(): + var entry := mvp_content_catalog.get_recruitment_entry(staff.staff_id) + var registered := false + for actor: Dictionary in defenders: + registered = registered or actor[&"actor_id"] == staff.staff_id + var reason := "" + if not preparing: reason = "準備中だけ参加者を追加できます。" + elif registered: reason = "この防衛戦に参加済みです。" + elif defender_owners.has(staff.staff_id) and defender_owners[staff.staff_id] != raid_id: reason = "別の防衛戦に参加しています。" + elif not staff.can_take_shift(): reason = "現在の体調や状態では参加できません。" + elif not staff.current_task_id.is_empty(): reason = "担当中の業務が完了するまで参加できません。" + staff_rows.append({ + &"staff_id": staff.staff_id, &"display_name": String(entry.get(&"display_name", staff.staff_id)), + &"combat": int(staff.skills.get(&"combat", 0)), &"health": staff.health, + &"fatigue": staff.fatigue, &"registered": registered, + &"can_register": reason.is_empty(), &"reason": reason, + }) + var objective_rows: Array[Dictionary] = [] + for objective_id: StringName in selected.get(&"objective_ids", []): + var facility := facility_service.get_instance(objective_id) + var definition: Variant = definition_registry.get_facility(facility.definition_id) if facility != null else null + var registered := false + for asset: Dictionary in assets: + registered = registered or asset[&"asset_id"] == objective_id + var reason := "" + if not preparing: reason = "準備中だけ防衛対象を登録できます。" + elif registered: reason = "登録済みです。" + elif facility == null: reason = "指定された施設が見つかりません。" + elif facility.durability <= 0: reason = "破壊された施設は登録できません。" + elif not facility.queue_entries.is_empty() or not facility.active_services.is_empty(): reason = "利用中または待機中の顧客がいます。" + elif asset_owners.has(objective_id) and asset_owners[objective_id] != raid_id: reason = "別の防衛戦の対象です。" + objective_rows.append({ + &"facility_id": objective_id, &"display_name": String(definition.display_name) if definition != null else String(objective_id), + &"durability": facility.durability if facility != null else 0, + &"registered": registered, &"can_register": reason.is_empty(), &"reason": reason, + }) + return {&"raids": raids, &"selected": selected, &"staff": staff_rows, + &"objectives": objective_rows, &"defenders": defenders, &"assets": assets, + &"can_control": not (_operations_enabled and operations_coordinator != null and operations_coordinator.is_terminal())} + + +func _execute_create_defense_raid(command: SimulationCommand) -> CommandResult: + var raw_definition: Variant = command.payload.get(&"definition", command.payload.get("definition", null)) + if not raw_definition is Dictionary: + return CommandResult.failed(CommandResult.Code.INVALID_PAYLOAD, command.command_id, "Defense Raid definition must be a Dictionary.") + var event_source_value: Variant = raw_definition.get(&"event_source", raw_definition.get("event_source", {})) + if not event_source_value is Dictionary or not event_source_value.is_empty(): + return CommandResult.failed(CommandResult.Code.INVALID_PAYLOAD, command.command_id, "Event threat provenance is reserved to the configured Event boundary.") + var raid_id := StringName(command.payload.get(&"raid_id", command.payload.get("raid_id", command.target_id))) + var id_before := id_generator.create_snapshot() + if raid_id.is_empty(): + raid_id = id_generator.next_id(&"defense_raid") + var definition: Dictionary = (raw_definition as Dictionary).duplicate(true) + if not definition.has(&"source_id") and not definition.has("source_id"): + definition[&"source_id"] = command.actor_id + var result := defense_service.create_raid(raid_id, _defense_operation_minute(), definition) + if not result.is_success(): + id_generator.restore_snapshot(id_before) + result.command_id = command.command_id + result.target_id = raid_id + return result + +func _execute_register_defense_combatant(command: SimulationCommand) -> CommandResult: + var raid_id := StringName(command.payload.get(&"raid_id", command.payload.get("raid_id", command.target_id))) + var actor_id := StringName(command.payload.get(&"actor_id", command.payload.get("actor_id", command.payload.get(&"staff_id", command.payload.get("staff_id", ""))))) + var side := StringName(String(command.payload.get(&"side", command.payload.get("side", ""))).to_upper()) + var combat := int(command.payload.get(&"combat", command.payload.get("combat", 0))) + var raw_options: Variant = command.payload.get(&"options", command.payload.get("options", {})) + if raid_id.is_empty() or actor_id.is_empty() or not raw_options is Dictionary: + return CommandResult.failed(CommandResult.Code.INVALID_PAYLOAD, command.command_id, "Defense combatant requires raid_id, actor_id and Dictionary options.") + if not _validate_live_defense_crosslinks(): + return CommandResult.failed(CommandResult.Code.VALIDATION_FAILED, command.command_id, "Defense cross-domain links are invalid before combatant registration.") + var options: Dictionary = (raw_options as Dictionary).duplicate(true) + var max_health := int(command.payload.get(&"max_health", command.payload.get("max_health", 100))) + if side == &"DEFENDER": + var staff := staff_service.get_staff(actor_id) + if staff == null: + return CommandResult.failed(CommandResult.Code.NOT_FOUND, command.command_id, "Defense defender must reference existing Staff.") + if not staff.can_take_shift() or not staff.current_task_id.is_empty(): + return CommandResult.failed(CommandResult.Code.VALIDATION_FAILED, command.command_id, "Defense defender must be healthy enough to participate and free of Staff tasks.") + var lock_owner: StringName = _defense_lock_owner(actor_id, &"DEFENDER", raid_id) + if not lock_owner.is_empty(): + return CommandResult.failed(CommandResult.Code.VALIDATION_FAILED, command.command_id, "Staff is already assigned to another non-terminal Raid.", {&"raid_id": lock_owner}) + if max_health != StaffRuntime.MAX_PSYCHOLOGY_VALUE \ + or ((options.has(&"health") or options.has("health")) and int(options.get(&"health", options.get("health", -1))) != staff.health): + return CommandResult.failed(CommandResult.Code.VALIDATION_FAILED, command.command_id, "Defender health must use the current Staff health on a 100-point baseline.") + max_health = StaffRuntime.MAX_PSYCHOLOGY_VALUE + options[&"health"] = staff.health + options[&"fatigue"] = staff.fatigue + var staff_before: Dictionary = staff_service.create_snapshot() + staff.transition_to(StaffRuntime.State.UNAVAILABLE, &"DEFENSE_ASSIGNED", _defense_operation_minute()) + var defender_result := defense_service.register_combatant( + raid_id, actor_id, side, combat, max_health, options + ) + if not defender_result.is_success(): + staff_service.restore_snapshot(staff_before) + defender_result.command_id = command.command_id + defender_result.target_id = raid_id + return defender_result + elif side != &"ATTACKER": + return CommandResult.failed(CommandResult.Code.INVALID_PAYLOAD, command.command_id, "Defense combatant side must be DEFENDER or ATTACKER.") + var result := defense_service.register_combatant( + raid_id, actor_id, side, combat, max_health, options + ) + result.command_id = command.command_id + result.target_id = raid_id + return result + +func _execute_register_defense_asset(command: SimulationCommand) -> CommandResult: + var raid_id := StringName(command.payload.get(&"raid_id", command.payload.get("raid_id", command.target_id))) + var asset_id := StringName(command.payload.get(&"asset_id", command.payload.get("asset_id", command.payload.get(&"facility_id", command.payload.get("facility_id", ""))))) + var kind := StringName(String(command.payload.get(&"kind", command.payload.get("kind", ""))).to_upper()) + var defense := int(command.payload.get(&"defense", command.payload.get("defense", 0))) + var raw_options: Variant = command.payload.get(&"options", command.payload.get("options", {})) + if raid_id.is_empty() or asset_id.is_empty() or not raw_options is Dictionary: + return CommandResult.failed(CommandResult.Code.INVALID_PAYLOAD, command.command_id, "Defense asset requires raid_id, facility asset_id and Dictionary options.") + if not _validate_live_defense_crosslinks(): + return CommandResult.failed(CommandResult.Code.VALIDATION_FAILED, command.command_id, "Defense cross-domain links are invalid before asset registration.") + var facility := facility_service.get_instance(asset_id) + if facility == null: + return CommandResult.failed(CommandResult.Code.NOT_FOUND, command.command_id, "Defense asset must reference an existing Facility.") + if not facility.queue_entries.is_empty() or not facility.active_services.is_empty(): + return CommandResult.failed(CommandResult.Code.VALIDATION_FAILED, command.command_id, "A busy Facility cannot become a Defense asset.") + var lock_owner: StringName = _defense_lock_owner(asset_id, &"ASSET", raid_id) + if not lock_owner.is_empty(): + return CommandResult.failed(CommandResult.Code.VALIDATION_FAILED, command.command_id, "Facility is already assigned to another non-terminal Raid.", {&"raid_id": lock_owner}) + var supplied_maximum := int(command.payload.get(&"max_integrity", command.payload.get("max_integrity", FacilityInstance.DEFAULT_DURABILITY))) + var options: Dictionary = (raw_options as Dictionary).duplicate(true) + if supplied_maximum != FacilityInstance.DEFAULT_DURABILITY \ + or ((options.has(&"integrity") or options.has("integrity")) and int(options.get(&"integrity", options.get("integrity", -1))) != facility.durability): + return CommandResult.failed(CommandResult.Code.VALIDATION_FAILED, command.command_id, "Defense asset integrity must use the current Facility durability on a 10000-point baseline.") + options[&"integrity"] = facility.durability + var result := defense_service.register_asset( + raid_id, + asset_id, + kind, + FacilityInstance.DEFAULT_DURABILITY, + defense, + options + ) + result.command_id = command.command_id + result.target_id = raid_id + return result + +func _execute_start_defense_raid(command: SimulationCommand) -> CommandResult: + var raid_id := StringName(command.payload.get(&"raid_id", command.payload.get("raid_id", command.target_id))) + if not _validate_live_defense_crosslinks(): + return CommandResult.failed(CommandResult.Code.VALIDATION_FAILED, command.command_id, "Defense cross-domain links are invalid before Raid start.") + var result := defense_service.start_raid(raid_id, _defense_operation_minute()) + result.command_id = command.command_id + result.target_id = raid_id + return result + +func _execute_set_defense_retreat_policy(command: SimulationCommand) -> CommandResult: + var raid_id := StringName(command.payload.get(&"raid_id", command.payload.get("raid_id", command.target_id))) + var policy := StringName(String(command.payload.get(&"policy", command.payload.get("policy", ""))).to_upper()) + var threshold := int(command.payload.get(&"threshold", command.payload.get("threshold", -1))) + if not _validate_live_defense_crosslinks(): + return CommandResult.failed(CommandResult.Code.VALIDATION_FAILED, command.command_id, "Defense cross-domain links are invalid before retreat-policy update.") + var result := defense_service.set_retreat_policy(raid_id, policy, threshold) + result.command_id = command.command_id + result.target_id = raid_id + return result + +func _execute_queue_defense_skill(command: SimulationCommand) -> CommandResult: + var raid_id := StringName(command.payload.get(&"raid_id", command.payload.get("raid_id", command.target_id))) + var action_id := StringName(command.payload.get(&"action_id", command.payload.get("action_id", ""))) + var actor_id := StringName(command.payload.get(&"actor_id", command.payload.get("actor_id", command.actor_id))) + var kind := StringName(String(command.payload.get(&"kind", command.payload.get("kind", ""))).to_upper()) + var raw_options: Variant = command.payload.get(&"options", command.payload.get("options", {})) + if not raw_options is Dictionary: + return CommandResult.failed(CommandResult.Code.INVALID_PAYLOAD, command.command_id, "Defense skill options must be a Dictionary.") + if not _validate_live_defense_crosslinks(): + return CommandResult.failed(CommandResult.Code.VALIDATION_FAILED, command.command_id, "Defense cross-domain links are invalid before skill queueing.") + var id_before := id_generator.create_snapshot() + if action_id.is_empty(): + action_id = id_generator.next_id(&"defense_skill_action") + var result := defense_service.queue_active_skill( + raid_id, action_id, actor_id, kind, (raw_options as Dictionary).duplicate(true) + ) + if not result.is_success(): + id_generator.restore_snapshot(id_before) + result.command_id = command.command_id + result.target_id = raid_id + return result + +func _execute_resolve_defense_round(command: SimulationCommand) -> CommandResult: + var raid_id := StringName(command.payload.get(&"raid_id", command.payload.get("raid_id", command.target_id))) + var resolution_id := StringName(command.payload.get(&"resolution_id", command.payload.get("resolution_id", ""))) + var result := _resolve_defense_round_internal(raid_id, resolution_id, _defense_operation_minute()) + result.command_id = command.command_id + result.target_id = raid_id + return result + + +## Both the explicit command and ordinary clock progression use the same +## transaction, generated-ID namespace and cross-domain callback boundaries. +func _resolve_defense_round_internal( + raid_id: StringName, + resolution_id: StringName, + absolute_minute: int +) -> CommandResult: + _prepare_defense_round_attempts(absolute_minute) + var already_attempted := _defense_round_attempted_raids.has(raid_id) + _defense_round_attempted_raids[raid_id] = true + var previous_command_minute := _active_command_minute + _active_command_minute = absolute_minute + if not _validate_live_defense_crosslinks(): + _active_command_minute = previous_command_minute + return CommandResult.failed(CommandResult.Code.VALIDATION_FAILED, 0, "Defense cross-domain links are invalid before round resolution.") + var id_before := id_generator.create_snapshot() + if resolution_id.is_empty(): + # Existing explicit IDs can occupy this namespace without advancing the + # allocator. Skip those IDs instead of replaying history as a new attack. + while id_generator.get_counter(&"defense_round_resolution") < StableIdGenerator.MAX_COUNTER: + resolution_id = id_generator.next_id(&"defense_round_resolution") + if not defense_service.has_resolution(raid_id, resolution_id): + break + if resolution_id.is_empty() or defense_service.has_resolution(raid_id, resolution_id): + id_generator.restore_snapshot(id_before) + _active_command_minute = previous_command_minute + var exhausted := CommandResult.failed(CommandResult.Code.VALIDATION_FAILED, 0, "Defense round ID namespace is exhausted.") + exhausted.target_id = raid_id + return exhausted + var result := defense_service.resolve_next_round(raid_id, resolution_id, absolute_minute) + # Reading an old canonical result is not a new round attempt. Restore only + # the guard introduced by this replay; an earlier real failure in this same + # minute must still prevent automatic progression from hiding that failure. + if result.is_replay and not already_attempted: + _defense_round_attempted_raids.erase(raid_id) + if not result.is_success(): + id_generator.restore_snapshot(id_before) + _active_command_minute = previous_command_minute + result.target_id = raid_id + return result + + +func _prepare_defense_round_attempts(absolute_minute: int) -> void: + if _defense_round_attempt_minute == absolute_minute: + return + _defense_round_attempt_minute = absolute_minute + _defense_round_attempted_raids.clear() + _defense_automatic_failed_minute = -1 + + +func _reset_defense_round_attempts() -> void: + _defense_round_attempt_minute = -1 + _defense_round_attempted_raids.clear() + _defense_automatic_failed_minute = -1 + + +func _execute_begin_next_defense_wave(command: SimulationCommand) -> CommandResult: + var raid_id := StringName(command.payload.get(&"raid_id", command.payload.get("raid_id", command.target_id))) + if not _validate_live_defense_crosslinks(): + return CommandResult.failed(CommandResult.Code.VALIDATION_FAILED, command.command_id, "Defense cross-domain links are invalid before the next Wave.") + var result := defense_service.begin_next_wave(raid_id, _defense_operation_minute()) + result.command_id = command.command_id + result.target_id = raid_id + return result + +func _execute_retreat_defenders(command: SimulationCommand) -> CommandResult: + var raid_id := StringName(command.payload.get(&"raid_id", command.payload.get("raid_id", command.target_id))) + var resolution_id := StringName(command.payload.get(&"resolution_id", command.payload.get("resolution_id", ""))) + var reason := StringName(command.payload.get(&"reason", command.payload.get("reason", &"PLAYER_RETREAT"))) + if not _validate_live_defense_crosslinks(): + return CommandResult.failed(CommandResult.Code.VALIDATION_FAILED, command.command_id, "Defense cross-domain links are invalid before defender retreat.") + var id_before := id_generator.create_snapshot() + if resolution_id.is_empty(): + resolution_id = id_generator.next_id(&"defense_retreat_resolution") + var result := defense_service.retreat_defenders(raid_id, resolution_id, _defense_operation_minute(), reason) + if not result.is_success(): + id_generator.restore_snapshot(id_before) + result.command_id = command.command_id + result.target_id = raid_id + return result + +func _execute_cancel_defense_raid(command: SimulationCommand) -> CommandResult: + var raid_id := StringName(command.payload.get(&"raid_id", command.payload.get("raid_id", command.target_id))) + var resolution_id := StringName(command.payload.get(&"resolution_id", command.payload.get("resolution_id", ""))) + var reason := StringName(command.payload.get(&"reason", command.payload.get("reason", &"CANCELLED"))) + if not _validate_live_defense_crosslinks(): + return CommandResult.failed(CommandResult.Code.VALIDATION_FAILED, command.command_id, "Defense cross-domain links are invalid before Raid cancellation.") + var id_before := id_generator.create_snapshot() + if resolution_id.is_empty(): + resolution_id = id_generator.next_id(&"defense_cancel_resolution") + var result := defense_service.cancel_raid(raid_id, _defense_operation_minute(), reason, resolution_id) + if not result.is_success(): + id_generator.restore_snapshot(id_before) + result.command_id = command.command_id + result.target_id = raid_id + return result + +func _execute_create_service_request(command: SimulationCommand) -> CommandResult: + var facility_id := StringName(command.payload.get(&"facility_id", command.payload.get("facility_id", ""))) + var instance := facility_service.get_instance(facility_id) + if instance == null: + return CommandResult.failed(CommandResult.Code.NOT_FOUND, command.command_id, "Facility was not found.") + var defense_owner: StringName = _defense_lock_owner(facility_id, &"ASSET") + if not defense_owner.is_empty(): + return CommandResult.failed( + CommandResult.Code.VALIDATION_FAILED, + command.command_id, + "A Facility assigned to a non-terminal Defense Raid cannot accept service requests.", + {&"raid_id": defense_owner} + ) + var guest_ids := _name_array(command.payload.get(&"guest_ids", command.payload.get("guest_ids", []))) + if guest_ids.is_empty(): + return CommandResult.failed(CommandResult.Code.INVALID_PAYLOAD, command.command_id, "At least one guest id is required.") + for guest_id: StringName in guest_ids: + if not economy_service.has_account(_guest_wallet_account(guest_id)): + return CommandResult.failed(CommandResult.Code.NOT_FOUND, command.command_id, "Every guest needs a registered wallet.") + var raw_options: Variant = command.payload.get(&"options", command.payload.get("options", {})) + if not raw_options is Dictionary: + return CommandResult.failed(CommandResult.Code.INVALID_PAYLOAD, command.command_id, "Service request options must be a Dictionary.") + var options: Dictionary = raw_options.duplicate(true) + var quoted_price := maxi( + int(options.get(&"quoted_price", options.get("quoted_price", get_facility_business_price(instance)))), + 0 + ) + # Freeze the same price used by candidate selection and the business panel. + options[&"quoted_price"] = quoted_price + if not instance.operator_open or not instance.is_within_opening_hours(_runtime_event_minute()): + return CommandResult.failed(CommandResult.Code.VALIDATION_FAILED, command.command_id, "This facility is closed to new service requests.", {&"reason": &"FACILITY_CLOSED"}) + if instance.requires_operating_kitchen() and not bool(facility_service.get_required_kitchen_report(facility_id).get(&"operating", false)): + return CommandResult.failed(CommandResult.Code.VALIDATION_FAILED, command.command_id, "This dining facility requires an operating BACK kitchen.", {&"reason": &"FACILITY_CLOSED", &"dependency_reason": facility_service.get_required_kitchen_report(facility_id).get(&"reason", &"")}) + var payer_account := _guest_wallet_account(guest_ids[0]) + var committed_price := _outstanding_service_commitment(guest_ids[0]) + if not economy_service.account_allows_negative(payer_account) \ + and economy_service.get_balance(payer_account) < committed_price + quoted_price: + return CommandResult.failed( + CommandResult.Code.VALIDATION_FAILED, + command.command_id, + "The guest cannot fund this service together with existing commitments.", + { + &"available_gold": economy_service.get_balance(payer_account), + &"committed_gold": committed_price, + &"quoted_price": quoted_price, + } + ) + + var inventory_before := inventory_service.create_snapshot() + var facility_before := facility_service.create_snapshot() + var guest_before := guest_service.create_snapshot() + var events_before := domain_events.create_snapshot() + var id_before := id_generator.create_snapshot() + var request_id := StringName(command.payload.get(&"request_id", command.payload.get("request_id", ""))) + if request_id.is_empty(): + request_id = id_generator.next_id(&"service_request") + var definition := definition_registry.get_facility(instance.definition_id) + if definition == null: + id_generator.restore_snapshot(id_before) + return CommandResult.failed(CommandResult.Code.NOT_FOUND, command.command_id, "Facility definition was not found.") + var reservation_ids: Array[StringName] = [] + for item_id: StringName in _sorted_name_keys(definition.inventory_inputs): + var reserve_result := inventory_service.reserve_item( + instance.local_inventory_id, + item_id, + int(definition.inventory_inputs[item_id]), + request_id, + &"facility_service", + _runtime_event_minute(), + _runtime_event_minute() + 240, + StringName("service.reserve.%s.%s" % [request_id, item_id]) + ) + if not bool(reserve_result.get(&"ok", false)): + inventory_service.restore_snapshot(inventory_before) + id_generator.restore_snapshot(id_before) + return _command_result_from_dictionary(command.command_id, reserve_result, "Facility input could not be reserved.") + for reservation_id: StringName in reserve_result.get(&"reservation_ids", []): + reservation_ids.append(reservation_id) + options[&"reserved_inventory_ids"] = reservation_ids + var request := facility_service.create_request( + request_id, + facility_id, + guest_ids, + StringName(command.payload.get(&"service_type", command.payload.get("service_type", "default"))), + _runtime_event_minute(), + options + ) + var enqueue_result: CommandResult + var runtime_guests: Array[StringName] = [] + for guest_id: StringName in guest_ids: + if guest_service.has_guest(guest_id): + runtime_guests.append(guest_id) + if runtime_guests.is_empty(): + enqueue_result = facility_service.enqueue_request(request, _runtime_event_minute(), not (_operations_enabled and _active_command_minute == _clock.absolute_minute + 1)) + else: + var first_guest := runtime_guests[0] + var queue_result := guest_service.enter_queue( + first_guest, + facility_id, + request_id, + _runtime_event_minute(), + int(options.get(&"initial_patience", options.get("initial_patience", -1))), + func(_guest_id: StringName, _facility_id: StringName, _request_id: StringName) -> CommandResult: + return facility_service.enqueue_request(request, _runtime_event_minute(), not (_operations_enabled and _active_command_minute == _clock.absolute_minute + 1)) + ) + if bool(queue_result.get(&"ok", false)): + for guest_index in range(1, runtime_guests.size()): + var joined := guest_service.enter_queue( + runtime_guests[guest_index], facility_id, request_id, _runtime_event_minute(), + int(options.get(&"initial_patience", options.get("initial_patience", -1))) + ) + if not bool(joined.get(&"ok", false)): + queue_result = joined + break + enqueue_result = CommandResult.succeeded(0, { + &"expected_wait": request.expected_wait, + }) if bool(queue_result.get(&"ok", false)) else CommandResult.failed( + CommandResult.Code.VALIDATION_FAILED, 0, + String(queue_result.get(&"message", "Guest could not enter the facility queue.")), + queue_result + ) + if not enqueue_result.is_success(): + inventory_service.restore_snapshot(inventory_before) + facility_service.restore_snapshot(facility_before) + guest_service.restore_snapshot(guest_before) + domain_events.restore_snapshot(events_before) + id_generator.restore_snapshot(id_before) + enqueue_result.command_id = command.command_id + return enqueue_result + return CommandResult.succeeded(command.command_id, { + &"request_id": request_id, + &"reservation_ids": reservation_ids, + &"expected_wait": enqueue_result.details.get(&"expected_wait", 0), + }) + +func _execute_set_facility_conditions(command: SimulationCommand) -> CommandResult: + var facility_id := StringName(command.payload.get(&"facility_id", command.payload.get("facility_id", ""))) + var conditions: Variant = command.payload.get(&"conditions", command.payload.get("conditions", null)) + if not conditions is Dictionary: + return CommandResult.failed(CommandResult.Code.INVALID_PAYLOAD, command.command_id, "conditions must be a Dictionary.") + var defense_owner: StringName = _defense_lock_owner(facility_id, &"ASSET") + if not defense_owner.is_empty() and (conditions.has(&"durability") or conditions.has("durability")): + return CommandResult.failed( + CommandResult.Code.VALIDATION_FAILED, + command.command_id, + "Defense-owned Facility durability can only change through its Raid.", + {&"raid_id": defense_owner} + ) + var result := facility_service.set_operational_status(facility_id, conditions, _runtime_event_minute()) + result.command_id = command.command_id + return result + +func _execute_set_facility_policy(command: SimulationCommand) -> CommandResult: + var facility_id := StringName(command.payload.get(&"facility_id", command.payload.get("facility_id", ""))) + var patch: Variant = command.payload.get(&"patch", command.payload.get("patch", null)) + if not patch is Dictionary: + return CommandResult.failed(CommandResult.Code.INVALID_PAYLOAD, command.command_id, "patch must be a Dictionary.") + var effective_at := int(command.payload.get(&"effective_at", command.payload.get("effective_at", -1))) + var result := facility_service.set_policy(facility_id, patch, effective_at) + result.command_id = command.command_id + return result + +func _execute_leave_facility_queue(command: SimulationCommand) -> CommandResult: + var facility_id := StringName(command.payload.get(&"facility_id", command.payload.get("facility_id", ""))) + var request_id := StringName(command.payload.get(&"request_id", command.payload.get("request_id", ""))) + var request := facility_service.get_request(request_id) + if request == null: + return CommandResult.failed(CommandResult.Code.NOT_FOUND, command.command_id, "Queued service request was not found.") + var inventory_before := inventory_service.create_snapshot() + var facility_before := facility_service.create_snapshot() + var guest_before := guest_service.create_snapshot() + var events_before := domain_events.create_snapshot() + var id_before := id_generator.create_snapshot() + var external_leave := func(_guest_id: StringName = &"", _facility_id: StringName = &"", _request_id: StringName = &"") -> Dictionary: + for reservation_id: StringName in request.reserved_inventory_ids: + var released := inventory_service.release_reservation( + reservation_id, + StringName("service.release.%s.%s" % [request_id, reservation_id]), + _runtime_event_minute() + ) + if not bool(released.get(&"ok", false)): + return released + var leave_result := facility_service.leave_queue(facility_id, request_id) + return {&"ok": leave_result.is_success(), &"message": leave_result.message} + var runtime_guests: Array[StringName] = [] + for guest_id: StringName in request.guest_ids: + if guest_service.has_guest(guest_id): + runtime_guests.append(guest_id) + var successful := true + var failure_message := "Facility queue removal failed." + if runtime_guests.is_empty(): + var external_result: Dictionary = external_leave.call() + successful = bool(external_result.get(&"ok", false)) + failure_message = String(external_result.get(&"message", failure_message)) + else: + for guest_index in range(runtime_guests.size()): + var queued_guest := guest_service.get_guest(runtime_guests[guest_index]) + var abandon := guest_service.tick_queue_patience( + queued_guest.guest_id, + _runtime_event_minute(), + -queued_guest.patience_remaining, + external_leave if guest_index == 0 else Callable() + ) + if not bool(abandon.get(&"ok", false)): + successful = false + failure_message = String(abandon.get(&"message", failure_message)) + break + if not successful: + inventory_service.restore_snapshot(inventory_before) + facility_service.restore_snapshot(facility_before) + guest_service.restore_snapshot(guest_before) + domain_events.restore_snapshot(events_before) + id_generator.restore_snapshot(id_before) + return CommandResult.failed(CommandResult.Code.TRANSACTION_APPLY_FAILED, command.command_id, failure_message) + return CommandResult.succeeded(command.command_id, {&"request_id": request_id, &"reason": &"GUEST_LEFT"}) + +func _execute_build_facility(command: SimulationCommand) -> CommandResult: + var operations_gate := _operations_construction_investment_gate(command.command_id) + if operations_gate != null: + return operations_gate + var definition_id := StringName(command.payload.get(&"definition_id", command.payload.get("definition_id", ""))) + var definition := definition_registry.get_facility(definition_id) + if definition == null: + return CommandResult.failed(CommandResult.Code.NOT_FOUND, command.command_id, "Facility definition was not found.") + if _facility_definition_is_unsupported_vertical_proxy(definition): + return CommandResult.failed( + CommandResult.Code.VALIDATION_FAILED, + command.command_id, + "Vertical infrastructure topology is not supported.", + { + &"reason": FacilityInfrastructureProjector.REASON_UNSUPPORTED_VERTICAL_TOPOLOGY, + &"definition_id": definition_id, + &"unchanged": true, + } + ) + var origin_value: Variant = _parse_world_cell(command.payload.get(&"origin", command.payload.get("origin", null))) + if origin_value == null: + return CommandResult.failed(CommandResult.Code.INVALID_PAYLOAD, command.command_id, "origin must be Vector3i or [x, y, layer].") + var origin: Vector3i = origin_value + if definition.id in [&"guest_stairs", &"service_stairs"] and not path_service.has_cell(origin + Vector3i(0, -1, 0)): + return _staff_assignment_failure(command.command_id, &"STAIR_LANDING_MISSING", "先に階段の一つ下の階に通路を作ってください。") + for shaft: Dictionary in elevators.get_report()[&"shafts"]: + if _elevator_touches_module(shaft, definition, origin) and _elevator_shaft_busy(shaft): + return _staff_assignment_failure(command.command_id, &"ELEVATOR_BUSY", "Wait until the shaft is idle before extending it.") + var rotation := int(command.payload.get(&"rotation", command.payload.get("rotation", 0))) + if rotation not in definition.allowed_rotations: + return CommandResult.failed(CommandResult.Code.INVALID_PAYLOAD, command.command_id, "The selected rotation is not allowed.") + var result := construction_service.start_construction( + definition, + origin, + rotation, + definition.build_minutes, + _runtime_event_minute() + ) + result.command_id = command.command_id + return result + +func _execute_cancel_construction(command: SimulationCommand) -> CommandResult: + var job_id := StringName(command.payload.get(&"job_id", command.payload.get("job_id", ""))) + if job_id.is_empty(): + return CommandResult.failed(CommandResult.Code.INVALID_PAYLOAD, command.command_id, "job_id is required.") + var result := construction_service.cancel_job(job_id, _runtime_event_minute()) + result.command_id = command.command_id + return result + + +func _execute_start_excavation(command: SimulationCommand) -> CommandResult: + var operations_gate := _operations_construction_investment_gate(command.command_id) + if operations_gate != null: + return operations_gate + if progression_service.current_rank < 2: + return CommandResult.failed( + CommandResult.Code.VALIDATION_FAILED, + command.command_id, + "Excavation requires World Tree Rank 2.", + {&"required_rank": 2, &"current_rank": progression_service.current_rank} + ) + var parsed_cell: Variant = _parse_world_cell( + command.payload.get(&"cell", command.payload.get("cell", null)) + ) + if parsed_cell == null: + return CommandResult.failed( + CommandResult.Code.INVALID_PAYLOAD, + command.command_id, + "cell must be Vector3i or [x, y, layer]." + ) + var result := excavation_service.start_excavation(parsed_cell, _runtime_event_minute()) + result.command_id = command.command_id + return result + + +func _execute_pause_excavation(command: SimulationCommand) -> CommandResult: + var job_id := StringName(command.payload.get(&"job_id", command.payload.get("job_id", ""))) + if job_id.is_empty(): + return CommandResult.failed( + CommandResult.Code.INVALID_PAYLOAD, + command.command_id, + "job_id is required." + ) + var result := excavation_service.pause_job(job_id) + result.command_id = command.command_id + return result + + +func _execute_resume_excavation(command: SimulationCommand) -> CommandResult: + var job_id := StringName(command.payload.get(&"job_id", command.payload.get("job_id", ""))) + if job_id.is_empty(): + return CommandResult.failed( + CommandResult.Code.INVALID_PAYLOAD, + command.command_id, + "job_id is required." + ) + var result := excavation_service.resume_job(job_id) + result.command_id = command.command_id + return result + +func _execute_demolish_facility(command: SimulationCommand) -> CommandResult: + var instance_id := StringName(command.payload.get(&"instance_id", command.payload.get("instance_id", ""))) + if instance_id.is_empty(): + return CommandResult.failed(CommandResult.Code.INVALID_PAYLOAD, command.command_id, "instance_id is required.") + for shaft: Dictionary in elevators.get_report()[&"shafts"]: + if instance_id in shaft[&"module_ids"] and _elevator_shaft_busy(shaft): + return _staff_assignment_failure(command.command_id, &"ELEVATOR_BUSY", "Wait until passengers have left before demolition.") + var lost_landing_cells := _demolition_lost_route_cells(instance_id) + for actor: Dictionary in path_service.create_snapshot()[&"actor_cells"]: + if actor[&"cell"] in lost_landing_cells: + return _staff_assignment_failure(command.command_id, &"FACILITY_OCCUPIED", "この通路・階段に人がいます。移動し終わってから撤去してください。") + var stair_dependents := _stair_demolition_dependents(instance_id) + if not stair_dependents.is_empty(): + return _staff_assignment_failure(command.command_id, &"STAIR_LANDING_IN_USE", "この通路につながる上の階段を先に撤去するか、建設を中止してください。") + var defense_owner: StringName = _defense_lock_owner(instance_id, &"ASSET") + if not defense_owner.is_empty(): + return CommandResult.failed( + CommandResult.Code.VALIDATION_FAILED, + command.command_id, + "A Facility assigned to a non-terminal Defense Raid cannot be demolished.", + {&"raid_id": defense_owner} + ) + var supply_guard: Dictionary = supply_management.validate_demolition(self, instance_id) + if not bool(supply_guard.get(&"ok", false)): + return _staff_assignment_failure(command.command_id, StringName(supply_guard.get(&"reason", &"SUPPLY_IN_USE")), "Facility still owns supply stock, a production plan, or an active delivery.") + for assignment: Dictionary in staff_service.get_work_assignments(): + if StringName(assignment[&"facility_id"]) == instance_id: + return _staff_assignment_failure(command.command_id, &"STAFF_ASSIGNED", "Unassign scheduled Staff before demolishing this facility.") + var operational_instance := facility_service.get_instance(instance_id) + if operational_instance != null \ + and (not operational_instance.queue_entries.is_empty() or not operational_instance.active_services.is_empty()): + return CommandResult.failed(CommandResult.Code.VALIDATION_FAILED, command.command_id, "A busy facility cannot be demolished.") + if facility_infrastructure_projector.has_projection(instance_id): + var boundary_before := _capture_infrastructure_demolition_state() + if boundary_before.is_empty(): + return CommandResult.failed( + CommandResult.Code.TRANSACTION_CLOSED, + command.command_id, + "Infrastructure demolition state could not be captured." + ) + var unprojected := facility_infrastructure_projector.unproject_facility( + StringName("runtime.demolish:%s" % instance_id), + instance_id + ) + if not unprojected.is_success(): + if not _infrastructure_demolition_state_matches(boundary_before): + _restore_infrastructure_demolition_state(boundary_before) + var rejection_details := unprojected.details.duplicate(true) + rejection_details[&"reason"] = &"INFRASTRUCTURE_UNPROJECT_FAILED" + rejection_details[&"instance_id"] = instance_id + return CommandResult.failed( + unprojected.code, + command.command_id, + unprojected.message, + rejection_details + ) + var demolished := construction_service.demolish_facility(instance_id) + demolished.command_id = command.command_id + var transport_ok := false + if demolished.is_success(): + transport_ok = bool(_synchronize_elevators(_runtime_event_minute()).get(&"ok", false)) and _prune_legacy_stairs_and_movement() + if not transport_ok or not demolished.is_success() \ + or not _validate_runtime_path_crosslinks(path_service, guest_service, staff_service, _movement_credits, _runtime_event_minute()) \ + or facility_infrastructure_projector.has_projection(instance_id) \ + or not construction_service.get_facility(instance_id).is_empty() \ + or not world_service.get_entity_cells(instance_id).is_empty() \ + or not _validate_infrastructure_crosslinks( + world_service, + path_service, + construction_service, + construction_resource_state, + excavation_service, + spatial_subslot_service, + route_subslot_path_adapter, + facility_infrastructure_projector + ): + var rolled_back := _restore_infrastructure_demolition_state(boundary_before) + return CommandResult.failed( + CommandResult.Code.TRANSACTION_APPLY_FAILED, + command.command_id, + "Infrastructure demolition failed and was rolled back.", + { + &"instance_id": instance_id, + &"rolled_back": rolled_back, + &"cause": demolished.to_dictionary(), + } + ) + return demolished + var result := construction_service.demolish_facility(instance_id) + result.command_id = command.command_id + if result.is_success(): + kitchen_production.remove_plan(instance_id) + auto_purchase_plans.remove_warehouse(instance_id) + return result + + +func _capture_infrastructure_demolition_state() -> Dictionary: + return { + &"game_state": _state.create_snapshot(), + &"economy": economy_service.create_snapshot(), + &"facility_upkeep": facility_upkeep.create_snapshot(), + &"upkeep_settlement": upkeep_settlement.create_snapshot(), + &"world": world_service.create_snapshot(), + &"path": path_service.create_snapshot(), + &"elevators": elevators.create_snapshot(), + &"legacy_stairs": legacy_stairs.create_snapshot(), + &"construction": construction_service.create_snapshot(), + &"excavation": excavation_service.create_snapshot(), + &"spatial_subslots": spatial_subslot_service.create_snapshot(), + &"route_projection": route_subslot_path_adapter.create_snapshot(), + &"facility_infrastructure": facility_infrastructure_projector.create_snapshot(), + &"facilities": facility_service.create_snapshot(), + &"facility_cleanliness": facility_cleanliness.create_snapshot(), + &"mana": mana_network_service.create_snapshot(), + &"movement_credits": _movement_credits.duplicate(true), + &"domain_events": domain_events.create_snapshot(), + &"id_generator": id_generator.create_snapshot(), + } + + +func _restore_infrastructure_demolition_state(snapshot: Dictionary) -> bool: + for key: StringName in [ + &"game_state", &"economy", &"facility_upkeep", &"upkeep_settlement", + &"world", &"path", &"elevators", &"legacy_stairs", &"construction", + &"excavation", &"spatial_subslots", &"route_projection", + &"facility_infrastructure", &"facilities", &"facility_cleanliness", &"mana", &"movement_credits", + &"domain_events", &"id_generator", + ]: + if not snapshot.get(key, null) is Dictionary: + return false + var restored: bool = bool(_state.restore_snapshot(snapshot[&"game_state"])) + restored = economy_service.restore_snapshot(snapshot[&"economy"]) and restored + restored = facility_upkeep.restore_snapshot(snapshot[&"facility_upkeep"]) and restored + restored = upkeep_settlement.restore_snapshot(snapshot[&"upkeep_settlement"], economy_service) and restored + restored = world_service.restore_snapshot(snapshot[&"world"]) and restored + restored = spatial_subslot_service.restore_snapshot(snapshot[&"spatial_subslots"]) and restored + var route_snapshot: Dictionary = snapshot[&"route_projection"] + var projected_walkable: Variant = route_snapshot.get(&"projected_walkable_cells", null) + if not projected_walkable is Array: + return false + restored = path_service.rebuild_graph(projected_walkable) and restored + restored = path_service.restore_snapshot(snapshot[&"path"]) and restored + restored = elevators.restore_snapshot(snapshot[&"elevators"]) and restored + restored = legacy_stairs.restore_snapshot(snapshot[&"legacy_stairs"]) and restored + restored = route_subslot_path_adapter.restore_snapshot(route_snapshot) and restored + restored = facility_infrastructure_projector.restore_snapshot( + snapshot[&"facility_infrastructure"] + ) and restored + restored = construction_service.restore_snapshot(snapshot[&"construction"]) and restored + restored = excavation_service.restore_snapshot(snapshot[&"excavation"]) and restored + restored = facility_service.restore_snapshot(snapshot[&"facilities"]) and restored + restored = facility_cleanliness.restore_snapshot(snapshot.get(&"facility_cleanliness", {})) and restored + restored = mana_network_service.restore_snapshot(snapshot[&"mana"]) and restored + _movement_credits = (snapshot[&"movement_credits"] as Dictionary).duplicate(true) + restored = domain_events.restore_snapshot(snapshot[&"domain_events"]) and restored + restored = id_generator.restore_snapshot(snapshot[&"id_generator"]) and restored + return restored and _infrastructure_demolition_state_matches(snapshot) \ + and _validate_infrastructure_crosslinks( + world_service, + path_service, + construction_service, + construction_resource_state, + excavation_service, + spatial_subslot_service, + route_subslot_path_adapter, + facility_infrastructure_projector + ) + + +func _infrastructure_demolition_state_matches(snapshot: Dictionary) -> bool: + return _strict_variants_equal(_state.create_snapshot(), snapshot.get(&"game_state", {})) \ + and _strict_variants_equal(economy_service.create_snapshot(), snapshot.get(&"economy", {})) \ + and _strict_variants_equal(facility_upkeep.create_snapshot(), snapshot.get(&"facility_upkeep", {})) \ + and _strict_variants_equal(upkeep_settlement.create_snapshot(), snapshot.get(&"upkeep_settlement", {})) \ + and _strict_variants_equal(world_service.create_snapshot(), snapshot.get(&"world", {})) \ + and _strict_variants_equal(path_service.create_snapshot(), snapshot.get(&"path", {})) \ + and _strict_variants_equal(elevators.create_snapshot(), snapshot.get(&"elevators", {})) \ + and _strict_variants_equal(legacy_stairs.create_snapshot(), snapshot.get(&"legacy_stairs", {})) \ + and _strict_variants_equal(construction_service.create_snapshot(), snapshot.get(&"construction", {})) \ + and _strict_variants_equal(excavation_service.create_snapshot(), snapshot.get(&"excavation", {})) \ + and _strict_variants_equal(spatial_subslot_service.create_snapshot(), snapshot.get(&"spatial_subslots", {})) \ + and _strict_variants_equal(route_subslot_path_adapter.create_snapshot(), snapshot.get(&"route_projection", {})) \ + and _strict_variants_equal(facility_infrastructure_projector.create_snapshot(), snapshot.get(&"facility_infrastructure", {})) \ + and _strict_variants_equal(facility_service.create_snapshot(), snapshot.get(&"facilities", {})) \ + and _strict_variants_equal(facility_cleanliness.create_snapshot(), snapshot.get(&"facility_cleanliness", {})) \ + and _strict_variants_equal(mana_network_service.create_snapshot(), snapshot.get(&"mana", {})) \ + and _strict_variants_equal(_movement_credits, snapshot.get(&"movement_credits", {})) \ + and _strict_variants_equal(domain_events.create_snapshot(), snapshot.get(&"domain_events", {})) \ + and _strict_variants_equal(id_generator.create_snapshot(), snapshot.get(&"id_generator", {})) + +func _execute_set_world_tree_rank(command: SimulationCommand) -> CommandResult: + var operations_gate := _operations_construction_investment_gate(command.command_id) + if operations_gate != null: + return operations_gate + var rank := int(command.payload.get(&"rank", command.payload.get("rank", 0))) + var request_id := StringName(command.payload.get( + &"request_id", + command.payload.get("request_id", "rank_up_%d" % rank) + )) + var progression_before := progression_service.create_snapshot() + if not _sync_progression_live_metrics(): + progression_service.restore_snapshot(progression_before) + return CommandResult.failed( + CommandResult.Code.TRANSACTION_APPLY_FAILED, + command.command_id, + "Live progression facts could not be synchronized." + ) + var external_before := _capture_rank_external_state() + var result := progression_service.request_rank_up( + request_id, + rank, + func(_current_rank: int, _target_rank: int) -> Dictionary: + return {&"ok": true, &"snapshot": external_before.duplicate(true)}, + func(target_rank: int, expected_bounds: Dictionary) -> Dictionary: + return {&"ok": _apply_rank_external_state(target_rank, expected_bounds)}, + func(snapshot: Dictionary) -> Dictionary: + return {&"ok": _rollback_rank_external_state(snapshot)}, + ) + result.command_id = command.command_id + if not result.is_success(): + progression_service.restore_snapshot(progression_before) + return result + if result.is_replay: + # Progression replays are read-only. The live-fact synchronization above is + # deliberately rolled back so a resent command reproduces the first result + # without silently changing any metric. + progression_service.restore_snapshot(progression_before) + return result + var event := SimulationDomainEvent.new( + id_generator.next_id(&"domain_event"), + &"WORLD_TREE_RANK_ADVANCED", + _runtime_event_minute(), + &"progression_service", + [StringName("world_tree_rank_%d" % rank)], + result.details.duplicate(true), + command.command_id + ) + if not _publish_domain_event(event): + _rollback_rank_external_state(external_before) + progression_service.restore_snapshot(progression_before) + return CommandResult.failed( + CommandResult.Code.TRANSACTION_APPLY_FAILED, + command.command_id, + "World Tree promotion event could not be published; the promotion was rolled back." + ) + return result + + +func _execute_update_progression_metrics(command: SimulationCommand) -> CommandResult: + var patch: Variant = command.payload.get(&"metrics", command.payload.get("metrics", null)) + if not patch is Dictionary: + return CommandResult.failed( + CommandResult.Code.INVALID_PAYLOAD, + command.command_id, + "Progression metrics must be a Dictionary." + ) + if patch.has(&"guest_lifetime") or patch.has("guest_lifetime"): + var requested_value: Variant = patch.get( + &"guest_lifetime", patch.get("guest_lifetime", null) + ) + var guest_snapshot := guest_service.create_snapshot() + var records_value: Variant = guest_snapshot.get(&"persistent_records", null) + var derived: int = guest_service.guest_count() \ + + (records_value.size() if records_value is Array else -1) + if typeof(requested_value) != TYPE_INT or derived < 0 \ + or int(requested_value) != derived: + return CommandResult.failed( + CommandResult.Code.VALIDATION_FAILED, + command.command_id, + "guest_lifetime is an authoritative Guest aggregate fact." + ) + if not progression_service.update_metrics(patch): + return CommandResult.failed( + CommandResult.Code.VALIDATION_FAILED, + command.command_id, + progression_service.last_error + ) + return CommandResult.succeeded(command.command_id, { + &"metrics": progression_service.metrics.duplicate(true), + }) + + +func _execute_complete_progression_quest(command: SimulationCommand) -> CommandResult: + var raw_quest_id: Variant = command.payload.get(&"quest_id", command.payload.get("quest_id", null)) + if typeof(raw_quest_id) not in [TYPE_STRING, TYPE_STRING_NAME]: + return CommandResult.failed( + CommandResult.Code.INVALID_PAYLOAD, + command.command_id, + "A progression quest id is required." + ) + var quest_id := StringName(raw_quest_id) + if not progression_service.complete_quest(quest_id): + return CommandResult.failed( + CommandResult.Code.VALIDATION_FAILED, + command.command_id, + progression_service.last_error + ) + return CommandResult.succeeded(command.command_id, {&"quest_id": quest_id}) + + +func _execute_acknowledge_operations_pause(command: SimulationCommand) -> CommandResult: + var acknowledged := acknowledge_operations_pause() + if not bool(acknowledged.get(&"ok", false)): + return CommandResult.failed( + CommandResult.Code.VALIDATION_FAILED, + command.command_id, + String(acknowledged.get(&"message", "Operations acknowledgement failed.")), + acknowledged.get(&"details", {}) + ) + return CommandResult.succeeded(command.command_id, acknowledged) + + +func _operations_construction_investment_gate(command_id: int) -> CommandResult: + if not _operations_enabled or operations_coordinator == null: + return null + var gate := operations_coordinator.can_start_construction_or_investment() + if bool(gate.get(&"allowed", false)) and _operations_total_arrears() > 0: + gate = {&"allowed": false, &"reason": &"UNPAID_EXPENSES", &"arrears_total": _operations_total_arrears()} + if bool(gate.get(&"allowed", false)): + return null + return CommandResult.failed( + CommandResult.Code.VALIDATION_FAILED, + command_id, + "Operations currently block construction and investment.", + gate + ) + +func _advance_construction(absolute_minute: int) -> void: + if _operations_tick_blocked: + return + var preflight := _preflight_proxy_completions(absolute_minute) + if not bool(preflight.get(&"ok", false)): + _latch_infrastructure_failure( + StringName(preflight.get(&"code", &"INFRASTRUCTURE_PREFLIGHT_FAILED")), + absolute_minute, + String(preflight.get(&"reason", "Infrastructure completion preflight failed.")), + preflight.get(&"details", {}) + ) + return + # The expensive composed rollback token is only needed when Construction can + # cross the completion boundary this minute. Idle ticks and ordinary progress + # retain ConstructionService's direct job_changed notification semantics. + if not _construction_completion_is_due_this_tick(): + var fast_results: Array[CommandResult] = construction_service.advance_one_minute( + absolute_minute + ) + for fast_result: CommandResult in fast_results: + if fast_result == null or not fast_result.is_success(): + _latch_infrastructure_failure( + &"CONSTRUCTION_ADVANCE_FAILED", + absolute_minute, + ( + fast_result.message + if fast_result != null else "Construction returned an invalid result." + ), + ( + fast_result.to_dictionary() + if fast_result != null else {} + ) + ) + return + excavation_service.advance_one_minute(absolute_minute) + if _infrastructure_failure_latched: + return + if not _validate_infrastructure_crosslinks( + world_service, + path_service, + construction_service, + construction_resource_state, + excavation_service, + spatial_subslot_service, + route_subslot_path_adapter, + facility_infrastructure_projector + ): + _latch_infrastructure_failure( + &"INFRASTRUCTURE_POSTCONDITION_FAILED", + absolute_minute, + "Construction and Excavation ended the minute with invalid infrastructure cross-links." + ) + return + var boundary_before := _capture_infrastructure_demolition_state() + if boundary_before.is_empty(): + _latch_infrastructure_failure( + &"INFRASTRUCTURE_CAPTURE_FAILED", + absolute_minute, + "The Construction completion boundary could not be captured." + ) + return + var previous_facility_ids: Dictionary = {} + for instance: Dictionary in construction_service.get_facilities(): + previous_facility_ids[StringName(instance.get(&"instance_id", &""))] = true + + _construction_completion_transaction_active = true + _construction_queued_domain_events.clear() + var progress_results: Array[CommandResult] = construction_service.advance_one_minute( + absolute_minute, {}, true + ) + var apply_failure: Dictionary = {} + for progress_result: CommandResult in progress_results: + if progress_result == null or not progress_result.is_success(): + apply_failure = { + &"code": &"CONSTRUCTION_ADVANCE_FAILED", + &"reason": ( + progress_result.message + if progress_result != null else "Construction returned an invalid result." + ), + &"details": ( + progress_result.to_dictionary() + if progress_result != null else {} + ), + } + break + if apply_failure.is_empty(): + for instance: Dictionary in construction_service.get_facilities(): + var instance_id := StringName(instance.get(&"instance_id", &"")) + if previous_facility_ids.has(instance_id): + continue + var applied := _apply_completed_facility_internal(instance, absolute_minute) + if not bool(applied.get(&"ok", false)): + apply_failure = applied + break + if apply_failure.is_empty() and not _construction_completion_crosslinks_are_valid( + absolute_minute + ): + apply_failure = { + &"code": &"CONSTRUCTION_COMPLETION_CROSSLINK_FAILED", + &"reason": "Construction completion produced invalid Runtime cross-links.", + &"details": {}, + } + if not apply_failure.is_empty(): + _fail_construction_completion_boundary( + boundary_before, + StringName(apply_failure.get(&"code", &"CONSTRUCTION_COMPLETION_FAILED")), + absolute_minute, + String(apply_failure.get(&"reason", "Construction completion failed.")), + apply_failure.get(&"details", {}) + ) + return + + # DomainEventStream validates and commits the whole batch before notifying + # subscribers. Construction's own raw signals are released only after the + # authoritative Runtime state and event batch have both committed. + var queued_events: Array[SimulationDomainEvent] = [] + for event: SimulationDomainEvent in _construction_queued_domain_events: + queued_events.append(event.copy()) + _construction_completion_transaction_active = false + _construction_queued_domain_events.clear() + if not domain_events.publish_batch(queued_events): + _fail_construction_completion_boundary( + boundary_before, + &"CONSTRUCTION_EVENT_COMMIT_FAILED", + absolute_minute, + "Construction completion DomainEvents could not be committed." + ) + return + _construction_committing_notifications = true + var notifications_committed := construction_service.commit_deferred_notifications() + _construction_committing_notifications = false + if not notifications_committed: + # Service-created deferred rows are prevalidated by ConstructionService, + # so this branch represents an unrecoverable notification boundary fault. + _latch_infrastructure_failure( + &"CONSTRUCTION_NOTIFICATION_COMMIT_FAILED", + absolute_minute, + "Committed Construction state could not release its deferred notifications.", + {&"rolled_back": false} + ) + return + + # Construction is authoritative first. Excavation then advances to the same + # absolute minute, preventing an excavation completion signal from escaping + # when the Construction boundary must be rolled back. + excavation_service.advance_one_minute(absolute_minute) + if _infrastructure_failure_latched: + return + if not _validate_infrastructure_crosslinks( + world_service, + path_service, + construction_service, + construction_resource_state, + excavation_service, + spatial_subslot_service, + route_subslot_path_adapter, + facility_infrastructure_projector + ): + _latch_infrastructure_failure( + &"INFRASTRUCTURE_POSTCONDITION_FAILED", + absolute_minute, + "Construction and Excavation ended the minute with invalid infrastructure cross-links." + ) + + +func _construction_completion_is_due_this_tick() -> bool: + for job: ConstructionJob in construction_service.get_jobs(): + if job == null or job.is_terminal(): + continue + if job.work_units_done + ConstructionJob.NORMAL_WORK_RATE \ + >= job.build_minutes * ConstructionJob.MAX_PROGRESS: + return true + return false + + +func _apply_completed_facility_internal(instance: Dictionary, absolute_minute: int) -> Dictionary: + var instance_id := StringName(instance.get(&"instance_id", &"")) + var definition_id := StringName(instance.get(&"definition_id", &"")) + var definition := definition_registry.get_facility(definition_id) + if instance_id.is_empty() or definition == null: + return { + &"ok": false, + &"code": &"INVALID_COMPLETED_FACILITY", + &"reason": "Construction produced an invalid completed Facility.", + &"details": {&"instance_id": instance_id, &"definition_id": definition_id}, + } + if _facility_definition_is_schema_proxy(definition): + if not _facility_definition_is_supported_infrastructure_proxy(definition): + return { + &"ok": false, + &"code": &"UNSUPPORTED_VERTICAL_TOPOLOGY", + &"reason": "An unsupported vertical proxy reached Construction completion.", + &"details": {&"instance_id": instance_id, &"definition_id": definition.id}, + } + if not facility_infrastructure_projector.has_projection(instance_id): + var projection_request_id := StringName( + "runtime.complete:%s" % String(instance.get(&"source_job_id", instance_id)) + ) + var projected := facility_infrastructure_projector.project_completed_facility( + projection_request_id, definition, instance + ) + if not projected.is_success(): + return { + &"ok": false, + &"code": &"FACILITY_INFRASTRUCTURE_PROJECTION_FAILED", + &"reason": projected.message, + &"details": projected.details.duplicate(true), + } + if not _publish_domain_event(SimulationDomainEvent.new( + id_generator.next_id(&"domain_event"), + &"FACILITY_INFRASTRUCTURE_PROJECTED", + absolute_minute, + &"facility_infrastructure_projector", + [instance_id], + projected.details.duplicate(true), + 0 + )): + return { + &"ok": false, + &"code": &"FACILITY_INFRASTRUCTURE_EVENT_FAILED", + &"reason": "The infrastructure projection event could not be staged.", + &"details": {&"instance_id": instance_id}, + } + else: + if not facility_service.has_instance(instance_id): + _register_facility_mana_consumer(instance) + _register_operational_facility(instance) + if not facility_service.has_instance(instance_id) \ + or not economy_service.has_account(_facility_revenue_account(instance_id)): + return { + &"ok": false, + &"code": &"OPERATIONAL_FACILITY_REGISTRATION_FAILED", + &"reason": "The completed Facility could not be registered operationally.", + &"details": {&"instance_id": instance_id}, + } + if definition.maintenance_mana > 0 \ + and mana_network_service.get_node(instance_id).is_empty(): + return { + &"ok": false, + &"code": &"FACILITY_MANA_REGISTRATION_FAILED", + &"reason": "The completed Facility Mana consumer could not be registered.", + &"details": {&"instance_id": instance_id}, + } + if definition.id in [&"guest_elevator", &"service_elevator"]: + _register_facility_mana_consumer(instance) + var transport := _synchronize_elevators(absolute_minute) + if not bool(transport.get(&"ok", false)): + return {&"ok": false, &"code": &"ELEVATOR_SYNC_FAILED", &"reason": elevators.last_error, &"details": transport} + if not _prune_legacy_stairs_and_movement(): + return {&"ok": false, &"code": &"LEGACY_STAIR_PRUNE_FAILED", &"reason": "Stair topology could not be updated."} + if not _publish_domain_event(SimulationDomainEvent.new( + id_generator.next_id(&"domain_event"), + &"FACILITY_COMPLETED", + absolute_minute, + &"construction_service", + [instance_id], + instance.duplicate(true), + 0 + )): + return { + &"ok": false, + &"code": &"FACILITY_COMPLETION_EVENT_FAILED", + &"reason": "The Facility completion event could not be staged.", + &"details": {&"instance_id": instance_id}, + } + return {&"ok": true} + + +func _construction_completion_crosslinks_are_valid(absolute_minute: int) -> bool: + return construction_service.current_minute == absolute_minute \ + and bool(mana_network_service.validate_invariants().get(&"valid", false)) \ + and _validate_construction_facility_crosslinks( + world_service, + construction_service, + facility_service, + facility_infrastructure_projector + ) \ + and _validate_infrastructure_crosslinks( + world_service, + path_service, + construction_service, + construction_resource_state, + excavation_service, + spatial_subslot_service, + route_subslot_path_adapter, + facility_infrastructure_projector, + false + ) + + +func _fail_construction_completion_boundary( + boundary_before: Dictionary, + code: StringName, + absolute_minute: int, + reason: String, + details: Dictionary = {} +) -> void: + _construction_completion_transaction_active = false + _construction_committing_notifications = false + _construction_queued_domain_events.clear() + var notifications_discarded := construction_service.discard_deferred_notifications() + var rolled_back := notifications_discarded \ + and _restore_infrastructure_demolition_state(boundary_before) + var failure_details := details.duplicate(true) + failure_details[&"notifications_discarded"] = notifications_discarded + failure_details[&"rolled_back"] = rolled_back + _latch_infrastructure_failure( + code if rolled_back else &"INFRASTRUCTURE_COMPLETION_ROLLBACK_FAILED", + absolute_minute, + reason, + failure_details + ) + + +func _preflight_proxy_completions(absolute_minute: int) -> Dictionary: + if construction_service == null or facility_infrastructure_projector == null \ + or route_subslot_path_adapter == null or spatial_subslot_service == null: + return { + &"ok": false, + &"code": &"INFRASTRUCTURE_UNAVAILABLE", + &"reason": "Infrastructure services are unavailable.", + } + var due_proxy_ids: Array[StringName] = [] + for job: ConstructionJob in construction_service.get_jobs(): + if job == null or job.is_terminal(): + continue + var definition := definition_registry.get_facility(job.definition_id) + if definition == null: + return { + &"ok": false, + &"code": &"MISSING_FACILITY_DEFINITION", + &"reason": "A Construction job references a missing Facility definition.", + &"details": {&"job_id": job.job_id, &"definition_id": job.definition_id}, + } + if not _facility_definition_is_supported_infrastructure_proxy(definition): + continue + if job.work_units_done + ConstructionJob.NORMAL_WORK_RATE \ + >= job.build_minutes * ConstructionJob.MAX_PROGRESS: + due_proxy_ids.append(job.job_id) + if due_proxy_ids.is_empty(): + return {&"ok": true} + if route_subslot_path_adapter.is_quarantined() \ + or route_subslot_path_adapter.is_projection_stale() \ + or facility_infrastructure_projector.is_quarantined() \ + or facility_infrastructure_projector.is_projection_stale() \ + or not bool(spatial_subslot_service.validate_invariants().get(&"ok", false)) \ + or not bool(route_subslot_path_adapter.validate_invariants().get(&"ok", false)) \ + or not bool(facility_infrastructure_projector.validate_invariants().get(&"ok", false)): + return { + &"ok": false, + &"code": &"INFRASTRUCTURE_DEPENDENCY_STALE", + &"reason": "Infrastructure dependencies are stale or quarantined.", + &"details": {&"job_ids": due_proxy_ids.duplicate()}, + } + var projector_snapshot := facility_infrastructure_projector.create_snapshot() + if int((projector_snapshot.get(&"projections", []) as Array).size()) \ + > FacilityInfrastructureProjector.MAX_PROJECTIONS - due_proxy_ids.size() \ + or int((projector_snapshot.get(&"processed_requests", []) as Array).size()) \ + > FacilityInfrastructureProjector.MAX_PROCESSED_REQUESTS - due_proxy_ids.size(): + return { + &"ok": false, + &"code": &"INFRASTRUCTURE_CAPACITY_EXHAUSTED", + &"reason": "Infrastructure projection capacity is exhausted.", + &"details": {&"job_ids": due_proxy_ids.duplicate()}, + } + return {&"ok": true} + + +func _latch_infrastructure_failure( + code: StringName, + absolute_minute: int, + reason: String, + details: Dictionary = {} +) -> void: + _infrastructure_failure_latched = true + _infrastructure_failure_report = { + &"code": code, + &"minute": absolute_minute, + &"reason": reason, + &"details": details.duplicate(true), + } + _operations_tick_blocked = true + if _clock != null: + _clock.set_paused(true) + + +func get_infrastructure_report() -> Dictionary: + return { + &"failure_latched": _infrastructure_failure_latched, + &"failure_report": _infrastructure_failure_report.duplicate(true), + &"excavation": ( + excavation_service.validate_invariants().duplicate(true) + if excavation_service != null else {&"ok": false} + ), + &"spatial_subslots": ( + spatial_subslot_service.validate_invariants().duplicate(true) + if spatial_subslot_service != null else {&"ok": false} + ), + &"route_projection": ( + route_subslot_path_adapter.get_projection_status().duplicate(true) + if route_subslot_path_adapter != null else {&"configured": false} + ), + &"facility_infrastructure": ( + facility_infrastructure_projector.get_status().duplicate(true) + if facility_infrastructure_projector != null else {&"configured": false} + ), + }.duplicate(true) + +func _advance_inventory_and_logistics(absolute_minute: int) -> void: + if _operations_tick_blocked: + return + inventory_service.current_minute = absolute_minute + inventory_service.process_expired(absolute_minute, &"inventory.waste") + if int(_state.get_resource(&"mana")) > 0: + supply_management.advance_auto_purchases(self, absolute_minute) + supply_management.ensure_deliveries(self, absolute_minute) + supply_logistics.tick(absolute_minute) + +func _advance_facilities(absolute_minute: int) -> void: + if _operations_tick_blocked: + return + facility_service.advance_one_minute(absolute_minute) + kitchen_production.advance_one_minute(absolute_minute) + +func _advance_guest_vitals(absolute_minute: int) -> void: + if _operations_tick_blocked: + return + var minute_of_day := posmod(absolute_minute, 1_440) + guest_service.advance_one_minute(absolute_minute, { + &"default": { + &"unstimulated": true, + &"solo_guest": true, + &"is_night": minute_of_day < 360 or minute_of_day >= 1_080, + &"intimacy_day": 3, + &"intimacy_night": 7, + } + }) + +func _advance_guest_queues(absolute_minute: int) -> void: + if _operations_tick_blocked: + return + _release_closed_business_queues(absolute_minute) + var guests_by_request: Dictionary = {} + for guest: GuestRuntime in guest_service.get_guests(): + if guest.state != GuestRuntime.State.QUEUING: + continue + if not guests_by_request.has(guest.queue_request_id): + guests_by_request[guest.queue_request_id] = [] + (guests_by_request[guest.queue_request_id] as Array).append(guest) + for request_id: StringName in _sorted_name_keys(guests_by_request): + var grouped_guests: Array = guests_by_request[request_id] + var queued_facility := facility_service.get_instance(grouped_guests[0].target_facility_id) + if queued_facility != null and (not queued_facility.operator_open or not queued_facility.is_within_opening_hours(absolute_minute)): + continue # A deferred closure must not spend patience while retrying. + var abandon_group := false + for guest: GuestRuntime in grouped_guests: + if guest.patience_remaining <= 1: + abandon_group = true + break + if not abandon_group: + for guest: GuestRuntime in grouped_guests: + guest_service.tick_queue_patience(guest.guest_id, absolute_minute) + continue + var inventory_before := inventory_service.create_snapshot() + var facility_before := facility_service.create_snapshot() + var guest_before := guest_service.create_snapshot() + var events_before := domain_events.create_snapshot() + var id_before := id_generator.create_snapshot() + var group_succeeded := true + for guest_index in range(grouped_guests.size()): + var guest: GuestRuntime = grouped_guests[guest_index] + var abandoned := guest_service.tick_queue_patience( + guest.guest_id, + absolute_minute, + -guest.patience_remaining, + Callable(self, "_leave_guest_queue_boundary") if guest_index == 0 else Callable() + ) + if not bool(abandoned.get(&"ok", false)): + group_succeeded = false + break + if not group_succeeded: + inventory_service.restore_snapshot(inventory_before) + facility_service.restore_snapshot(facility_before) + guest_service.restore_snapshot(guest_before) + domain_events.restore_snapshot(events_before) + id_generator.restore_snapshot(id_before) + +func _advance_authority_incidents(absolute_minute: int) -> void: + if _operations_tick_blocked: + return + if _operations_morning_already_processed(absolute_minute): + return + var advanced := authority_service.advance_time(absolute_minute) + if not advanced.is_success(): + _clock.set_paused(true) + _publish_actor_event(&"AUTHORITY_ADVANCE_FAILED", &"authority_service", [], { + &"message": advanced.message, + &"details": advanced.details.duplicate(true), + }) + return + if posmod(absolute_minute, 1_440) != AuthorityService.INSPECTION_EVALUATION_MINUTE: + return + var evaluated := authority_service.evaluate_inspection(absolute_minute) + if not evaluated.is_success(): + _clock.set_paused(true) + _publish_actor_event(&"AUTHORITY_INSPECTION_EVALUATION_FAILED", &"authority_service", [], { + &"message": evaluated.message, + &"details": evaluated.details.duplicate(true), + }) + return + if not evaluated.is_replay: + _publish_authority_evaluation(evaluated.details) + +func _advance_defense(absolute_minute: int) -> void: + if _operations_tick_blocked: + return + _prepare_defense_round_attempts(absolute_minute) + # A deferred operations suffix must not reattempt the same failed automatic + # batch. Resume on a later game minute, after the player resumes the clock. + if _defense_automatic_failed_minute == absolute_minute: + return + var advanced := defense_service.advance_time(absolute_minute) + if not advanced.is_success(): + _defense_automatic_failed_minute = absolute_minute + _clock.set_paused(true) + _publish_actor_event(&"DEFENSE_ADVANCE_FAILED", &"defense_service", [], { + &"message": advanced.message, + &"details": advanced.details.duplicate(true), + }) + return + # One attempt per due raid, in stable raid-ID order. No catch-up while-loop, + # invented deadline, offline replay, PREPARING start or BETWEEN_WAVES advance. + for due: Dictionary in defense_service.get_due_rounds(absolute_minute): + if _defense_automatic_failed_minute == absolute_minute: + return + var raid_id := StringName(due[&"raid_id"]) + if _defense_round_attempted_raids.has(raid_id): + continue + var resolved := _resolve_defense_round_internal(raid_id, &"", absolute_minute) + if not resolved.is_success(): + _defense_automatic_failed_minute = absolute_minute + _clock.set_paused(true) + _publish_actor_event(&"DEFENSE_AUTOMATIC_ROUND_FAILED", &"defense_service", [raid_id], { + &"round_number": due[&"round_number"], + &"earliest_minute": due[&"earliest_minute"], + &"message": resolved.message, + &"details": resolved.details.duplicate(true), + }) + return + + +func _advance_relationship_events(absolute_minute: int) -> void: + if _operations_tick_blocked: + return + if _operations_morning_already_processed(absolute_minute): + return + event_service.process_expired(absolute_minute) + event_service.audit_actor_availability(absolute_minute) + _enqueue_pending_relationship_candidates() + if event_service.get_foreground_instance() == null: + var started := event_service.start_next_event( + absolute_minute, + _build_event_context(absolute_minute) + ) + if started.is_success() and bool(started.details.get(&"waiting_for_choice", false)): + _clock.set_paused(true) + if event_service.pause_requested: + _clock.set_paused(true) + + +func _operations_morning_already_processed(absolute_minute: int) -> bool: + if not _operations_enabled or operations_coordinator == null \ + or posmod(absolute_minute, SimulationKernel.MINUTES_PER_DAY) \ + != OPERATIONS_START_MINUTE: + return false + var day := absolute_minute / SimulationKernel.MINUTES_PER_DAY + 1 + var report := operations_coordinator.get_morning_report(day) + return not report.is_empty() and int(report.get(&"minute", -1)) == absolute_minute + +func _enqueue_pending_relationship_candidates() -> void: + for candidate: Dictionary in relationship_service.get_threshold_candidates(): + _on_relationship_threshold_candidate(candidate) + +func _build_event_context(absolute_minute: int) -> Dictionary: + return { + &"absolute_minute": absolute_minute, + &"day": absolute_minute / 1_440 + 1, + &"minute_of_day": posmod(absolute_minute, 1_440), + &"player": { + &"gold": economy_service.get_balance(PLAYER_CASH_ACCOUNT), + &"world_tree_rank": int(_state.world_tree_rank), + }, + } + +func _advance_staff_ai(absolute_minute: int) -> void: + if _operations_tick_blocked: + return + _prune_expired_staff_elevator_waits(absolute_minute) + _advance_elevators(absolute_minute) + _advance_staff_rescue(absolute_minute) + staff_service.advance_one_minute(absolute_minute) + _prune_expired_staff_elevator_waits(absolute_minute) + staff_commute.tick(absolute_minute) + _sync_facility_staffing(absolute_minute) + cleaning_bridge.tick(absolute_minute) + for task: StaffTask in staff_service.get_tasks(): + if task.state == StaffTask.State.RESERVED: + if task.state_changed_at < absolute_minute: + task.begin_moving(task.state_changed_at) + elif task.state == StaffTask.State.WORKING and task.task_type == CleaningBridge.TASK_TYPE: + # Only cleaning is deferred from StaffService to these attendance and + # dependency checks. Other tasks already progressed once this minute. + if task.state_changed_at >= absolute_minute or not cleaning_bridge.task_can_work(task, staff_service.get_staff(task.assigned_staff_id)): + continue + var work_units := CleaningBridge.work_units_per_minute(staff_service.get_staff(task.assigned_staff_id)) + staff_service.progress_task(task.task_id, work_units, absolute_minute) + for staff: StaffRuntime in staff_service.get_staff_members(): + if absolute_minute % 30 != 0: + continue + if staff.state == StaffRuntime.State.WORKING or (staff.state == StaffRuntime.State.AVAILABLE and staff_commute.is_present(staff, facility_service.get_instance(staff.assigned_facility_id))): + staff.add_fatigue(1) + if _operations_enabled: + staff.health = maxi(staff.health - 1, 0) + elif staff.state == StaffRuntime.State.OFF_DUTY: + var resting := get_staff_rest_report(staff.staff_id) + if not _operations_enabled or resting.get(&"resting", false): + staff.add_fatigue(-1) + if _operations_enabled and staff.health > 0: + staff.health = mini(staff.health + 1, 100) + _advance_staff_rescue(absolute_minute) + _sync_facility_staffing(absolute_minute) + _advance_guest_ai_decisions(absolute_minute) + +## Record the actual completed choice, not a fresh prediction made by the UI. +func _decide_guest_with_log(guest_id: StringName, candidates: Array, minute: int, context: Dictionary) -> Dictionary: + var observation := guest_service.get_observation(guest_id) + var result: Dictionary + if get_script().resource_path == "res://autoload/simulation_runtime.gd" and guest_service.get_script().resource_path == "res://scripts/guest/guest_service.gd" and ClassDB.class_exists(&"TheTreeNumericCore"): + var core = ClassDB.instantiate(&"TheTreeNumericCore") + if core != null and core.has_method(&"guest_candidate_batch"): + result = guest_service.decide_with_native_candidates(core, guest_id, candidates, minute, context) + else: + result = guest_service.decide(guest_id, candidates, minute, context) + else: + result = guest_service.decide(guest_id, candidates, minute, context) + if not bool(result.get(&"ok", false)): + return result + var candidate: Dictionary = result.get(&"candidate", {}) + var rejections := guest_service.get_last_candidate_rejections(guest_id) + var reason_counts: Dictionary = {} + for rejection: Dictionary in rejections: + var reason := String(rejection.get(&"reason", "")) + reason_counts[reason] = int(reason_counts.get(reason, 0)) + 1 + var payload := { + &"rejection_counts": reason_counts, + &"rejection_examples": rejections.slice(0, 8), + &"rejection_count": rejections.size(), + &"action": result[&"action"], + &"needs": observation.get(&"needs", {}).duplicate(true), + &"primary_needs": observation.get(&"primary_needs", []).duplicate(), + &"facility_id": candidate.get(&"facility_id", &""), + &"need_priority": candidate.get(&"need_priority", 0), + &"utility": candidate.get(&"utility", 0), + &"travel_minutes": candidate.get(&"travel_minutes", 0), + &"wait_minutes": candidate.get(&"utility_terms", {}).get(&"wait_minutes", 0), + &"estimated_total_gold": maxi(int(candidate.get(&"estimated_total_gold", candidate.get(&"price_gold", candidate.get(&"quoted_price", 0)))), 0), + } + _publish_actor_event(&"GUEST_DECISION_RECORDED", &"guest_service", [guest_id], payload) + return result + + +func get_guest_decision_record(guest_id: StringName) -> Dictionary: + var events := domain_events.recent_events() + for index in range(events.size() - 1, -1, -1): + var event: SimulationDomainEvent = events[index] + if event.event_type == &"GUEST_DECISION_RECORDED" and guest_id in event.entity_ids: + var report := event.payload.duplicate(true) + report[&"absolute_minute"] = event.absolute_minute + return report + return {} + + +func _advance_guest_ai_decisions(absolute_minute: int) -> void: + for guest: GuestRuntime in guest_service.get_guests(): + match guest.state: + GuestRuntime.State.SPAWNING: + guest_service.transition_guest(guest.guest_id, GuestRuntime.State.ENTERING, absolute_minute, &"ENTRY_STARTED") + GuestRuntime.State.ENTERING: + guest_service.transition_guest(guest.guest_id, GuestRuntime.State.DECIDING, absolute_minute, &"ENTRY_COMPLETED") + GuestRuntime.State.DECIDING: + if guest.planned_exit_at <= absolute_minute: + guest_service.begin_leaving(guest.guest_id, absolute_minute, &"PLANNED_EXIT") + continue + guest.wallet_gold = economy_service.get_balance(_guest_wallet_account(guest.guest_id)) + _decide_guest_with_log( + guest.guest_id, + _project_guest_candidates(guest.guest_id), + absolute_minute, + {&"absolute_minute": absolute_minute} + ) + GuestRuntime.State.MOVING: + pass + GuestRuntime.State.REPLANNING, GuestRuntime.State.ABANDONING, GuestRuntime.State.INTERRUPTED: + guest_service.transition_guest(guest.guest_id, GuestRuntime.State.DECIDING, absolute_minute, &"REPLAN_AFTER_INTERRUPTION") + GuestRuntime.State.LEAVING: + if guest.state_entered_at < absolute_minute: + guest_service.despawn_guest(guest.guest_id, absolute_minute) + _advance_runtime_path_traffic(absolute_minute) + + +func _advance_runtime_path_traffic(absolute_minute: int) -> void: + var actors: Array[Dictionary] = [] + for guest: GuestRuntime in guest_service.get_guests(): + actors.append({ + &"actor_id": _guest_path_actor_id(guest.guest_id), + &"kind": &"guest", + &"entity_id": guest.guest_id, + }) + for staff: StaffRuntime in staff_service.get_staff_members(): + actors.append({ + &"actor_id": _staff_path_actor_id(staff.staff_id), + &"kind": &"staff", + &"entity_id": staff.staff_id, + }) + actors.sort_custom(func(left: Dictionary, right: Dictionary) -> bool: + return String(left[&"actor_id"]) < String(right[&"actor_id"]) + ) + for entry: Dictionary in actors: + var actor_id: StringName = entry[&"actor_id"] + if not elevators.get_actor_trip(actor_id).is_empty(): + _record_expected_actor_wait(actor_id, path_service.get_actor_projected_cell(actor_id), absolute_minute) + continue + var kind: StringName = entry[&"kind"] + var entity_id: StringName = entry[&"entity_id"] + if kind == &"guest": + var guest := guest_service.get_guest(entity_id) + if guest == null: + continue + if not _path_projection_matches(actor_id, guest.current_cell): + _report_path_integration_failure(actor_id, entity_id, kind, guest.current_cell) + continue + if guest.state == GuestRuntime.State.MOVING \ + and guest.state_entered_at < absolute_minute: + _advance_guest_path_travel(guest, actor_id, absolute_minute) + else: + _record_expected_actor_wait(actor_id, guest.current_cell, absolute_minute) + continue + var staff := staff_service.get_staff(entity_id) + if staff == null: + continue + if not _path_projection_matches(actor_id, staff.current_cell): + _report_path_integration_failure(actor_id, entity_id, kind, staff.current_cell) + continue + if staff.state == StaffRuntime.State.COMMUTING and staff_service.requires_attendance(staff.staff_id): + # The commute driver already consumed this tick, including fractional + # walking credit under congestion. Do not reset it as an idle task. + continue + if _operations_enabled and staff.state == StaffRuntime.State.OFF_DUTY and staff.current_task_id.is_empty(): + var rest := get_staff_rest_report(staff.staff_id) + if rest.get(&"reachable", false): + advance_staff_commute(staff.staff_id, rest[&"cell"], absolute_minute) + else: + _record_expected_actor_wait(actor_id, staff.current_cell, absolute_minute) + continue + var task := staff_service.get_task(staff.current_task_id) + if task != null and task.state == StaffTask.State.MOVING \ + and task.assigned_staff_id == staff.staff_id \ + and supply_logistics.can_move_task(task.task_id): + _advance_staff_task_travel(task, actor_id, absolute_minute) + else: + _record_expected_actor_wait(actor_id, staff.current_cell, absolute_minute) + + +func _path_projection_matches(actor_id: StringName, cell: Vector3i) -> bool: + return path_service.has_actor_projection(actor_id) \ + and path_service.get_actor_projected_cell(actor_id) == cell \ + and _movement_credits.has(actor_id) + + +func _report_path_integration_failure( + actor_id: StringName, + entity_id: StringName, + kind: StringName, + cell: Vector3i +) -> void: + _clock.set_paused(true) + _publish_actor_event(&"PATH_ACTOR_CROSSLINK_FAILED", &"simulation_runtime", [actor_id, entity_id], { + &"actor_kind": kind, + &"actor_cell": cell, + &"projected_cell": path_service.get_actor_projected_cell(actor_id), + &"has_projection": path_service.has_actor_projection(actor_id), + &"has_movement_credit": _movement_credits.has(actor_id), + }) + + +func _record_expected_actor_wait( + actor_id: StringName, + cell: Vector3i, + absolute_minute: int +) -> void: + # Intentional queue, service, decision, and idle waits are valid progress. Reset + # the stuck baseline so a later movement does not inherit intentional waiting. + path_service.clear_actor_progress(actor_id) + var recorded := path_service.record_actor_progress(actor_id, cell, absolute_minute) + if not bool(recorded.get(&"ok", false)): + _report_path_integration_failure(actor_id, actor_id, &"unknown", cell) + return + _movement_credits[actor_id] = {&"credit": 0, &"updated_at": absolute_minute} + + +func _record_blocked_actor_wait( + actor_id: StringName, + entity_id: StringName, + kind: StringName, + cell: Vector3i, + absolute_minute: int, + credit: int +) -> void: + var recorded := path_service.record_actor_progress(actor_id, cell, absolute_minute) + if not bool(recorded.get(&"ok", false)): + _report_path_integration_failure(actor_id, entity_id, kind, cell) + return + _movement_credits[actor_id] = { + &"credit": clampi(credit, 0, _cell_movement_credit_limit(path_service, cell) - 1), + &"updated_at": absolute_minute, + } + if int(recorded.get(&"minutes_without_progress", 0)) >= PathService.STUCK_THRESHOLD_MINUTES: + _recover_stuck_actor(actor_id, entity_id, kind, cell, absolute_minute) + + +func _movement_credit_after_one_minute( + actor_id: StringName, + cell: Vector3i, + absolute_minute: int +) -> int: + var current: Dictionary = _movement_credits.get(actor_id, { + &"credit": 0, + &"updated_at": absolute_minute - 1, + }) + var carried := int(current.get(&"credit", 0)) + if int(current.get(&"updated_at", -1)) >= absolute_minute: + return carried + var speed_credit := clampi( + roundi(path_service.get_speed_multiplier(cell) * MOVEMENT_CREDIT_SCALE), + MIN_MOVEMENT_CREDIT_PER_MINUTE, + MOVEMENT_CREDIT_SCALE + ) + return carried + speed_credit + + +func _advance_guest_path_travel( + guest: GuestRuntime, + actor_id: StringName, + absolute_minute: int +) -> void: + var target_cell := _path_handle_target(guest.path_handle, guest.current_cell) + if guest.current_cell == target_cell: + _record_expected_actor_wait(actor_id, guest.current_cell, absolute_minute) + _queue_guest_at_target(guest, absolute_minute) + return + var path := path_service.find_path(guest.current_cell, target_cell) + if path.size() <= 1: + _record_blocked_actor_wait( + actor_id, guest.guest_id, &"guest", guest.current_cell, absolute_minute, 0 + ) + return + if _request_elevator_step(actor_id, path[0], path[1], absolute_minute, []): + return + var required_credit := _step_movement_credit(path_service, path[0], path[1], []) + var accumulated := _movement_credit_after_one_minute( + actor_id, guest.current_cell, absolute_minute + ) + accumulated = mini(accumulated, required_credit + MOVEMENT_CREDIT_SCALE - 1) + if accumulated < required_credit: + _record_blocked_actor_wait( + actor_id, + guest.guest_id, + &"guest", + guest.current_cell, + absolute_minute, + accumulated + ) + return + var previous_cell := guest.current_cell + var previous_progress_cell := guest.last_progress_cell + var previous_progress_at := guest.last_progress_at + var previous_failure_count := guest.path_failure_count + var previous_recovery_kind := guest.recovery_kind + if not guest_service.record_path_success(guest.guest_id, path[1], absolute_minute) \ + or not path_service.project_actor_position(actor_id, path[1], absolute_minute): + guest.current_cell = previous_cell + guest.last_progress_cell = previous_progress_cell + guest.last_progress_at = previous_progress_at + guest.path_failure_count = previous_failure_count + guest.recovery_kind = previous_recovery_kind + _report_path_integration_failure(actor_id, guest.guest_id, &"guest", previous_cell) + return + var remaining_credit := accumulated - required_credit + _movement_credits[actor_id] = { + &"credit": remaining_credit, + &"updated_at": absolute_minute, + } + _publish_actor_event(&"GUEST_MOVED", &"path_service", [guest.guest_id, actor_id], { + &"from_cell": previous_cell, + &"cell": guest.current_cell, + &"target_cell": target_cell, + &"movement_credit": remaining_credit, + }) + if guest.current_cell == target_cell: + _queue_guest_at_target(guest, absolute_minute) + +func _queue_guest_at_target(guest: GuestRuntime, absolute_minute: int) -> void: + var operation_key := StringName( + "guest.auto_queue.%s.%d" % [guest.guest_id, guest.state_entered_at] + ) + var result := submit_command( + &"create_service_request", + { + &"facility_id": guest.target_facility_id, + &"guest_ids": [guest.guest_id], + &"service_type": &"default", + }, + guest.guest_id, + guest.target_facility_id, + 0, + absolute_minute, + operation_key + ) + if not result.is_success() and guest.state == GuestRuntime.State.MOVING: + guest_service.transition_guest( + guest.guest_id, + GuestRuntime.State.DECIDING, + absolute_minute, + &"QUEUE_REQUEST_FAILED" + ) + +func _guest_recovery_targets(current_cell: Vector3i) -> Array: + return [{ + &"kind": &"emergency_exit", + &"cell": current_cell, + &"path_handle": {&"recovery": true, &"target_cell": current_cell}, + }] + +func _guest_recovery_path_exists( + from_cell: Vector3i, + to_cell: Vector3i, + _guest_id: StringName, + _target: Dictionary +) -> bool: + return not path_service.find_path(from_cell, to_cell).is_empty() + +func _advance_staff_task_travel( + task: StaffTask, + actor_id: StringName, + absolute_minute: int +) -> void: + var staff := staff_service.get_staff(task.assigned_staff_id) + if staff == null or staff.current_task_id != task.task_id: + return + if task.task_type == CleaningBridge.TASK_TYPE and not cleaning_bridge.prepare_route(task, staff): + _record_expected_actor_wait(actor_id, staff.current_cell, absolute_minute) + return + if staff.current_cell == task.target_cell: + _record_expected_actor_wait(actor_id, staff.current_cell, absolute_minute) + staff_service.begin_task(task.task_id, absolute_minute) + return + var path := path_service.find_path( + staff.current_cell, + task.target_cell, + [WorldService.LAYER_FRONT, WorldService.LAYER_BACK], + [&"staff"] + ) + if task.task_type in [&"rescue_pickup", &"rescue_transport"]: + path = path_service.find_backyard_path(staff.current_cell, task.target_cell, true) + if path.size() <= 1: + if task.task_type in [&"rescue_pickup", &"rescue_transport"]: + _record_expected_actor_wait(actor_id, staff.current_cell, absolute_minute) + return + _record_blocked_actor_wait( + actor_id, staff.staff_id, &"staff", staff.current_cell, absolute_minute, 0 + ) + return + if task.task_type == &"rescue_transport" and elevators.owns_connector(path_service.get_step_connector(path[0], path[1], [&"staff"])): + var patient_actor := _staff_path_actor_id(task.target_entity_id) + var connector := path_service.get_step_connector(path[0], path[1], [&"staff"]) + var pair := {} + if not _elevator_extension_pending(elevators.get_shaft_for_connector(connector)): + pair = elevators.request_pair(actor_id, patient_actor, path[0], path[1], absolute_minute, [&"staff"]) + if pair.get(&"ok", false) and not pair.get(&"replay", false): + _publish_actor_event(&"ELEVATOR_RESCUE_REQUESTED", &"elevators", [actor_id, patient_actor], pair[&"trip"]) + _record_expected_actor_wait(actor_id, staff.current_cell, absolute_minute) + return + if _request_elevator_step(actor_id, path[0], path[1], absolute_minute, [&"staff"]): + return + var required_credit := _step_movement_credit(path_service, path[0], path[1], [&"staff"]) + var accumulated := _movement_credit_after_one_minute( + actor_id, staff.current_cell, absolute_minute + ) + accumulated = mini(accumulated, required_credit + MOVEMENT_CREDIT_SCALE - 1) + if accumulated < required_credit: + _record_blocked_actor_wait( + actor_id, + staff.staff_id, + &"staff", + staff.current_cell, + absolute_minute, + accumulated + ) + return + var previous_cell := staff.current_cell + staff.current_cell = path[1] + if not path_service.project_actor_position(actor_id, staff.current_cell, absolute_minute): + staff.current_cell = previous_cell + _report_path_integration_failure(actor_id, staff.staff_id, &"staff", previous_cell) + return + if task.task_type == &"rescue_transport": + var patient := staff_service.get_staff(task.target_entity_id) + if patient == null or not path_service.project_actor_position(_staff_path_actor_id(patient.staff_id), staff.current_cell, absolute_minute): + staff.current_cell = previous_cell + path_service.project_actor_position(actor_id, previous_cell, absolute_minute) + return + patient.current_cell = staff.current_cell + _record_expected_actor_wait(_staff_path_actor_id(patient.staff_id), patient.current_cell, absolute_minute) + var remaining_credit := accumulated - required_credit + _movement_credits[actor_id] = { + &"credit": remaining_credit, + &"updated_at": absolute_minute, + } + _publish_actor_event(&"STAFF_MOVED", &"staff_service", [staff.staff_id, task.task_id], { + &"path_actor_id": actor_id, + &"from_cell": previous_cell, + &"cell": staff.current_cell, + &"target_cell": task.target_cell, + &"movement_credit": remaining_credit, + }) + if staff.current_cell == task.target_cell: + staff_service.begin_task(task.task_id, absolute_minute) + + +func _recover_stuck_actor( + actor_id: StringName, + entity_id: StringName, + kind: StringName, + cell: Vector3i, + absolute_minute: int +) -> void: + var path_before := path_service.create_snapshot() + var guests_before := guest_service.create_snapshot() + var staff_before := staff_service.create_snapshot() + var inventory_before := inventory_service.create_snapshot() + var events_before := domain_events.create_snapshot() + var ids_before := id_generator.create_snapshot() + var credits_before := _movement_credits.duplicate(true) + var report := path_service.get_actor_stuck_report( + actor_id, + absolute_minute, + [WorldService.LAYER_FRONT, WorldService.LAYER_BACK] + ) + if not bool(report.get(&"ok", false)) or not bool(report.get(&"should_recover", false)): + return + var action := &"NONE" + var action_ok := false + if kind == &"guest": + var guest := guest_service.get_guest(entity_id) + if guest != null: + var recovery_targets: Array = [] + var recovery_value: Variant = report.get(&"recovery", {}) + if recovery_value is Dictionary and bool(recovery_value.get(&"found", false)): + var recovery_cell: Vector3i = recovery_value[&"cell"] + recovery_targets.append({ + &"kind": &"guidance", + &"cell": recovery_cell, + &"path_handle": { + &"recovery": true, + &"target_cell": recovery_cell, + }, + }) + var recovery_result: Dictionary = {} + for _attempt in range(GuestService.PATH_FAILURE_LIMIT): + recovery_result = guest_service.record_path_failure( + entity_id, + absolute_minute, + recovery_targets, + Callable(self, "_guest_recovery_path_exists") + ) + if not bool(recovery_result.get(&"ok", false)) \ + or bool(recovery_result.get(&"recovery_started", false)): + break + action_ok = bool(recovery_result.get(&"ok", false)) \ + and bool(recovery_result.get(&"recovery_started", false)) + action = &"GUEST_SAFE_RECOVERY" if action_ok else &"GUEST_RECOVERY_FAILED" + elif kind == &"staff": + var staff := staff_service.get_staff(entity_id) + if staff != null and not staff.current_task_id.is_empty(): + if supply_logistics.owns_task(staff.current_task_id): + var recovery: Dictionary = supply_logistics.handle_path_stuck(staff.current_task_id, absolute_minute) + action_ok = bool(recovery.get(&"ok", false)) + action = StringName(recovery.get(&"action", &"SUPPLY_RECOVERY_FAILED")) + else: + var cancelled := staff_service.cancel_task(staff.current_task_id, &"PATH_STUCK", absolute_minute) + action_ok = cancelled.is_success() + action = &"STAFF_TASK_CANCELLED" if action_ok else &"STAFF_TASK_CANCEL_FAILED" + if not action_ok: + path_service.restore_snapshot(path_before) + guest_service.restore_snapshot(guests_before) + staff_service.restore_snapshot(staff_before) + inventory_service.restore_snapshot(inventory_before) + domain_events.restore_snapshot(events_before) + id_generator.restore_snapshot(ids_before) + _movement_credits = credits_before + return + _movement_credits[actor_id] = {&"credit": 0, &"updated_at": absolute_minute} + _publish_actor_event(&"PATH_ACTOR_STUCK", &"path_service", [actor_id, entity_id], { + &"actor_kind": kind, + &"cell": cell, + &"minutes_without_progress": int(report[&"minutes_without_progress"]), + &"threshold_minutes": int(report[&"threshold_minutes"]), + &"recovery": (report[&"recovery"] as Dictionary).duplicate(true), + &"action": action, + &"implicit_move_applied": false, + }) + +func _leave_guest_queue_boundary( + _guest_id: StringName, + facility_id: StringName, + request_id: StringName +) -> Dictionary: + var request := facility_service.get_request(request_id) + if request == null: + return {&"ok": true, &"orphan_repaired": true} + var inventory_before := inventory_service.create_snapshot() + for reservation_id: StringName in request.reserved_inventory_ids: + var released := inventory_service.release_reservation( + reservation_id, + StringName("guest.abandon.%s.%s" % [request_id, reservation_id]), + _clock.absolute_minute + ) + if not bool(released.get(&"ok", false)): + inventory_service.restore_snapshot(inventory_before) + return released + var leave_result := facility_service.leave_queue(facility_id, request_id, &"PATIENCE_EXHAUSTED") + if not leave_result.is_success(): + inventory_service.restore_snapshot(inventory_before) + return {&"ok": false, &"message": leave_result.message} + return {&"ok": true} + +func _reserve_staff_task_items(task: StaffTask, absolute_minute: int) -> Dictionary: + var inventory_id := &"staff.supplies" + var facility := facility_service.get_instance(task.target_entity_id) + if facility != null: + inventory_id = facility.local_inventory_id + var before := inventory_service.create_snapshot() + var reservation_ids: Array[StringName] = [] + for item_id: StringName in _sorted_name_keys(task.required_items): + var result := inventory_service.reserve_item( + inventory_id, + item_id, + int(task.required_items[item_id]), + task.task_id, + &"staff_task", + absolute_minute, + absolute_minute + 480, + StringName("staff.task.reserve.%s.%s" % [task.task_id, item_id]) + ) + if not bool(result.get(&"ok", false)): + inventory_service.restore_snapshot(before) + return { + &"ok": false, + &"reason": StringName(result.get(&"code", &"ITEM_RESERVATION_FAILED")), + } + for reservation_id: StringName in result.get(&"reservation_ids", []): + reservation_ids.append(reservation_id) + return {&"ok": true, &"reservation_ids": reservation_ids} + +func _release_staff_task_items( + reservation_ids: Array[StringName], + task: StaffTask, + absolute_minute: int +) -> Dictionary: + var before := inventory_service.create_snapshot() + for reservation_id: StringName in reservation_ids: + var result := inventory_service.release_reservation( + reservation_id, + StringName("staff.task.release.%s.%s" % [task.task_id, reservation_id]), + absolute_minute + ) + if not bool(result.get(&"ok", false)): + inventory_service.restore_snapshot(before) + return result + return {&"ok": true} + +func _estimate_staff_task_travel(staff: StaffRuntime, task: StaffTask, _absolute_minute: int) -> int: + if task.task_type == CleaningBridge.TASK_TYPE and not cleaning_bridge.can_dispatch(staff): return -1 + if task.task_type in [&"rescue_pickup", &"rescue_transport"]: + if not elevators.get_actor_trip(_staff_path_actor_id(staff.staff_id)).is_empty(): + return -1 + var route := path_service.find_backyard_path(staff.current_cell, task.target_cell, true) + return _estimated_actor_path_minutes(route, [&"staff"]) if not route.is_empty() else -1 + if staff.current_cell == task.target_cell: + return 0 + var path := path_service.find_path( + staff.current_cell, + task.target_cell, + [WorldService.LAYER_FRONT, WorldService.LAYER_BACK], + [&"staff"] + ) + if not path.is_empty(): + return _estimated_actor_path_minutes(path, [&"staff"]) + return -1 + +func _sync_facility_staffing(absolute_minute: int) -> void: + if not _staffing_projection_enabled: + return + var staff_members := staff_service.get_staff_members() + for facility: FacilityInstance in facility_service.get_instances(): + var active_ids: Array[StringName] = [] + for staff: StaffRuntime in staff_members: + if (staff_commute.is_present(staff, facility) or CleaningBridge.is_deployed(staff, facility, staff_service)) and not _is_anri_manager(staff): + active_ids.append(staff.staff_id) + facility_service.set_operational_status( + facility.instance_id, + {&"active_staff_ids": active_ids}, + absolute_minute + ) + +func _project_guest_candidates(guest_id: StringName) -> Array: + # Stateless native pipeline; missing old bindings/custom/observer domains + # retain the exact original projection below. + if get_script().resource_path == "res://autoload/simulation_runtime.gd" and ClassDB.class_exists(&"TheTreeNumericCore"): + var core = ClassDB.instantiate(&"TheTreeNumericCore") + if core != null and core.has_method(&"project_guest_candidates"): + var selected: Dictionary = core.project_guest_candidates(self, guest_id, FacilityInstance.State.OPEN, FacilityInstance.State.DEGRADED, MOVEMENT_CREDIT_SCALE, STAIR_MOVEMENT_CREDIT, PathService.ORTHOGONAL_MOVE_COST, Callable(Time, "get_ticks_usec")) + if bool(selected.get(&"supported", false)): + return selected[&"candidates"] + return _project_guest_candidates_without_native(guest_id) + + +func _project_guest_candidates_without_native(guest_id: StringName) -> Array: + var guest := guest_service.get_guest(guest_id) + var candidates: Array = [] + if guest == null: + return candidates + var query_goals: Array = [] + for instance: FacilityInstance in facility_service.get_instances(): + query_goals.append_array(_facility_access_candidates(instance)) + path_service.begin_guest_path_query_batch(guest.current_cell, query_goals) + for facility: FacilityInstance in facility_service.get_instances(): + var definition := definition_registry.get_facility(facility.definition_id) + if definition == null or &"staff_only" in definition.tags: + continue + if &"entrance" in definition.tags and _guest_has_checked_in(guest): + continue + var has_inputs := true + for item_id: StringName in _sorted_name_keys(definition.inventory_inputs): + if inventory_service.available_quantity(facility.local_inventory_id, item_id) < int(definition.inventory_inputs[item_id]): + has_inputs = false + break + if not has_inputs: + # Preserve the existing exclusion without doing unnecessary path queries, + # but let the decision evidence explain why this facility was unavailable. + candidates.append({&"facility_id": facility.instance_id, &"inputs_available": false}) + continue + var access := _find_facility_access_path(guest.current_cell, facility) + var path: Array = access.get(&"path", []) + var need_relief := _need_relief_for_facility_tags(definition.tags) + candidates.append({ + &"facility_id": facility.instance_id, + &"facility_tags": definition.tags.duplicate(), + &"is_open": facility.state in [FacilityInstance.State.OPEN, FacilityInstance.State.DEGRADED], + &"reachable": bool(access.get(&"reachable", false)), + &"travel_minutes": _estimated_actor_path_minutes(path, []), + &"expected_wait": facility.queue_entries.size() * facility.base_service_minutes / maxi(facility.capacity, 1), + &"estimated_total_gold": get_facility_business_price(facility), + &"expected_quality": clampi(facility.base_quality, 0, 100), + &"need_relief": need_relief, + &"risk_level": 0, + &"illegal_level": 0, + &"group_fit": mini(facility.capacity, 4) * 5, + &"service_category": _service_category_for_tags(definition.tags), + &"path_handle": { + &"target_cell": access.get(&"target_cell", guest.current_cell), + &"graph_revision": path_service.graph_revision, + }, + }) + path_service.end_guest_path_query_batch() + return candidates + +func _facility_access_candidates(facility: FacilityInstance) -> Array: + var candidate_cells: Dictionary = {} + var occupied: Array[Vector3i] = facility.occupied_cells.duplicate() + if occupied.is_empty(): + occupied.append(facility.origin) + for cell: Vector3i in occupied: + for direction: Vector3i in [Vector3i.LEFT, Vector3i.RIGHT]: + var candidate := Vector3i(cell.x + direction.x, cell.y + direction.y, cell.z) + if path_service.has_cell(candidate): + candidate_cells[candidate] = true + if path_service.has_cell(facility.origin): + candidate_cells[facility.origin] = true + var ordered: Array = candidate_cells.keys() + ordered.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 ordered + + +# External synchronous observers can mutate World after each old point query. +# Their presence (including the nested budget signal) retains that exact loop. +func _guest_access_batch_can_select() -> bool: + if get_script().resource_path != "res://autoload/simulation_runtime.gd" or path_service.get_script().resource_path != "res://scripts/world/path_service.gd": return false + if _clock == null or _clock.performance_budget == null: return false + var budget = _clock.performance_budget + if budget.get_script().resource_path != "res://scripts/core/performance_budget.gd" or not budget.get_signal_connection_list(&"budget_exceeded").is_empty(): return false + var listeners := path_service.get_signal_connection_list(&"path_query_measured") + return listeners.size() == 1 and listeners[0]["callable"] == Callable(self, "_on_path_query_measured") and int(listeners[0]["flags"]) == 0 + + +func _find_facility_access_path(start: Vector3i, facility: FacilityInstance) -> Dictionary: + var ordered := _facility_access_candidates(facility) + if _guest_access_batch_can_select(): + var selected := path_service.query_nearest_guest_access(start, ordered, facility.origin) + if bool(selected.get(&"supported", false)): + selected.erase(&"supported") + return selected + var best_path: Array[Vector3i] = [] + var best_cell := facility.origin + for candidate: Vector3i in ordered: + var path := path_service.find_path(start, candidate) + if path.is_empty(): + continue + if best_path.is_empty() or path_service.calculate_guest_batch_path_cost(start, candidate, path) < path_service.calculate_guest_batch_path_cost(start, best_cell, best_path): + best_path.assign(path) + best_cell = candidate + return {&"reachable": not best_path.is_empty(), &"path": best_path, &"target_cell": best_cell} + +func _need_relief_for_facility_tags(tags: Array[StringName]) -> Dictionary: + var relief := { + &"hunger": 0, &"sleep": 0, &"hygiene": 0, + &"medical": 0, &"leisure": 0, &"intimacy": 0, + } + if &"dining" in tags: + relief[&"hunger"] = 80 + relief[&"leisure"] = 10 + if &"lodging" in tags: + relief[&"sleep"] = 90 + relief[&"hygiene"] = maxi(int(relief[&"hygiene"]), 10) + if &"social" in tags or &"leisure" in tags: + relief[&"leisure"] = maxi(int(relief[&"leisure"]), 40) + if &"hygiene" in tags or &"bath" in tags: + relief[&"hygiene"] = maxi(int(relief[&"hygiene"]), 80) + if &"medical" in tags: + relief[&"medical"] = 90 + elif &"wellness" in tags: + relief[&"medical"] = maxi(int(relief[&"medical"]), 20) + if &"adult_only" in tags or &"restricted_service" in tags: + relief[&"intimacy"] = 80 + return relief + +func _service_category_for_tags(tags: Array[StringName]) -> StringName: + if &"dining" in tags: + return &"food" + if &"lodging" in tags: + return &"lodging" + if &"social" in tags: + return &"leisure" + return &"service" + +func _path_handle_target(path_handle: Variant, fallback: Vector3i) -> Vector3i: + if path_handle is Dictionary: + var value: Variant = path_handle.get(&"target_cell", path_handle.get("target_cell", fallback)) + var parsed: Variant = _parse_world_cell(value) + if parsed != null: + return parsed + return fallback + +func _event_actor_exists(actor_id: StringName) -> bool: + if actor_id == &"player": + return true + return guest_service.has_guest(actor_id) or staff_service.has_staff(actor_id) + +func _event_entity_exists(entity_id: Variant) -> bool: + var normalized := StringName(entity_id) + return _event_actor_exists(normalized) \ + or facility_service.has_instance(normalized) \ + or relationship_service.has_relationship(normalized) \ + or event_service.get_instance(normalized) != null + +func _event_path_exists(value: Variant) -> bool: + if not value is Dictionary: + return false + var from_value: Variant = _parse_world_cell(value.get(&"from", value.get("from", null))) + var to_value: Variant = _parse_world_cell(value.get(&"to", value.get("to", null))) + if from_value == null or to_value == null: + return false + return not path_service.find_path(from_value, to_value).is_empty() + +func _roll_authority_inspection(_day_index: int, _chance: int, _terms: Dictionary) -> int: + var stream := get_rng_stream(&"authority") + return stream.randi_range(0, 99) if stream != null else 99 + +func _authority_line_of_sight( + from_cell: Vector3i, + to_cell: Vector3i, + _context: Dictionary +) -> bool: + if from_cell.z != to_cell.z \ + or not world_service.is_in_bounds(from_cell) \ + or not world_service.is_in_bounds(to_cell): + return false + if from_cell == to_cell: + return true + var x := from_cell.x + var y := from_cell.y + var delta_x := absi(to_cell.x - from_cell.x) + var delta_y := absi(to_cell.y - from_cell.y) + var step_x := 1 if from_cell.x < to_cell.x else -1 + var step_y := 1 if from_cell.y < to_cell.y else -1 + var error := delta_x - delta_y + while x != to_cell.x or y != to_cell.y: + var doubled_error := error * 2 + if doubled_error > -delta_y: + error -= delta_y + x += step_x + if doubled_error < delta_x: + error += delta_x + y += step_y + var current := Vector3i(x, y, from_cell.z) + if current == to_cell: + return true + if not world_service.is_walkable(current) \ + or not world_service.get_occupant(current).is_empty(): + return false + return true + +func _authority_path_query( + from_cell: Vector3i, + to_cell: Vector3i, + context: Dictionary +) -> Dictionary: + var connector_access := _name_array( + context.get(&"connector_access", context.get("connector_access", [])) + ) + var path := path_service.find_path( + from_cell, + to_cell, + [WorldService.LAYER_FRONT, WorldService.LAYER_BACK], + connector_access + ) + if path.is_empty(): + return { + &"reachable": false, + &"reason": StringName(path_service.last_error if not path_service.last_error.is_empty() else "PATH_UNREACHABLE"), + &"travel_minutes": 0, + } + return { + &"reachable": true, + &"reason": &"OK", + &"travel_minutes": maxi(path.size() - 1, 0), + &"path": path, + } + +func _apply_authority_resolution_boundary( + payload: Dictionary, + phase: StringName, + rollback_token: Variant +) -> Dictionary: + if phase == &"rollback": + if not rollback_token is Dictionary \ + or not rollback_token.get(&"economy", null) is Dictionary \ + or not rollback_token.get(&"game_state", null) is Dictionary \ + or not rollback_token.get(&"facilities", null) is Dictionary \ + or not rollback_token.get(&"domain_events", null) is Dictionary \ + or not rollback_token.get(&"id_generator", null) is Dictionary: + return {&"ok": false, &"error": "Authority rollback token is malformed."} + var economy_ok: bool = economy_service.restore_snapshot(rollback_token[&"economy"]) + var facilities_ok: bool = facility_service.restore_snapshot(rollback_token[&"facilities"]) + facilities_ok = facility_cleanliness.restore_snapshot(rollback_token.get(&"facility_cleanliness", {})) and facilities_ok + var events_ok: bool = domain_events.restore_snapshot(rollback_token[&"domain_events"]) + var ids_ok: bool = id_generator.restore_snapshot(rollback_token[&"id_generator"]) + var state_ok: bool = _state.restore_snapshot(rollback_token[&"game_state"]) + return { + &"ok": economy_ok and state_ok and facilities_ok and events_ok and ids_ok, + &"error": "Authority fine rollback failed." if not economy_ok or not state_ok or not facilities_ok or not events_ok or not ids_ok else "", + } + var resolution_id := StringName(payload.get(&"resolution_id", payload.get("resolution_id", ""))) + var inspection_id := StringName(payload.get(&"inspection_id", payload.get("inspection_id", ""))) + var fine_amount := int(payload.get(&"fine_amount", payload.get("fine_amount", -1))) + var absolute_minute := int(payload.get(&"absolute_minute", payload.get("absolute_minute", -1))) + var idempotency_key := _authority_resolution_idempotency_key(resolution_id) + if resolution_id.is_empty() or inspection_id.is_empty() or fine_amount < 0 \ + or absolute_minute != _runtime_event_minute() \ + or not economy_service.has_account(PLAYER_CASH_ACCOUNT) \ + or not economy_service.has_account(AUTHORITY_FINE_ACCOUNT): + return {&"ok": false, &"error": "Authority resolution payload is invalid."} + if phase == &"validate": + var existing_transaction := economy_service.get_transaction_id_for_idempotency_key(idempotency_key) + if not existing_transaction.is_empty(): + if not _authority_fine_transaction_matches( + economy_service, existing_transaction, inspection_id, fine_amount + ): + return {&"ok": false, &"error": "Authority fine idempotency key conflicts with its ledger transaction."} + return {&"ok": true, &"token": { + &"economy": economy_service.create_snapshot(), + &"facility_upkeep": facility_upkeep.create_snapshot(), + &"upkeep_settlement": upkeep_settlement.create_snapshot(), + &"game_state": _state.create_snapshot(), + &"facilities": facility_service.create_snapshot(), + &"facility_cleanliness": facility_cleanliness.create_snapshot(), + &"domain_events": domain_events.create_snapshot(), + &"id_generator": id_generator.create_snapshot(), + &"already_applied": true, + &"transaction_id": existing_transaction, + }} + if fine_amount > 0 \ + and not economy_service.account_allows_negative(PLAYER_CASH_ACCOUNT) \ + and economy_service.get_balance(PLAYER_CASH_ACCOUNT) < fine_amount: + return {&"ok": false, &"error": "PLAYER_CASH cannot fund the Authority fine."} + return {&"ok": true, &"token": { + &"economy": economy_service.create_snapshot(), + &"facility_upkeep": facility_upkeep.create_snapshot(), + &"upkeep_settlement": upkeep_settlement.create_snapshot(), + &"game_state": _state.create_snapshot(), + &"facilities": facility_service.create_snapshot(), + &"facility_cleanliness": facility_cleanliness.create_snapshot(), + &"domain_events": domain_events.create_snapshot(), + &"id_generator": id_generator.create_snapshot(), + &"already_applied": false, + }} + if phase != &"apply" or not rollback_token is Dictionary: + return {&"ok": false, &"token": rollback_token, &"error": "Unknown Authority resolution phase or token."} + if StringName(payload.get(&"result", &"")) == &"TEMPORARY_CLOSURE": + var targets: Dictionary = {} + for raw_id: Variant in payload.get(&"evidence_ids", []): + var evidence := authority_service.get_evidence(StringName(raw_id)) + if evidence != null and evidence.is_discovered() and not evidence.is_expired(absolute_minute) and facility_service.has_instance(evidence.facility_id): + targets[evidence.facility_id] = true + for target_id: StringName in _sorted_name_keys(targets): + var stopped := facility_service.set_operational_status(target_id, {&"suspension_reason": &"RAIDED"}, absolute_minute) + if not stopped.is_success(): + return {&"ok": false, &"token": rollback_token, &"error": stopped.message} + if bool(rollback_token.get(&"already_applied", false)) or fine_amount == 0: + return { + &"ok": true, + &"transaction_id": StringName(rollback_token.get(&"transaction_id", &"")), + } + var posted := economy_service.transfer( + PLAYER_CASH_ACCOUNT, + AUTHORITY_FINE_ACCOUNT, + fine_amount, + absolute_minute, + &"AUTHORITY_FINE", + inspection_id, + idempotency_key, + &"authority.inspection.fine" + ) + if not posted.is_success(): + return { + &"ok": false, + &"token": rollback_token, + &"error": posted.message, + } + return { + &"ok": true, + &"transaction_id": posted.details.get(&"transaction_id", &""), + } + +func _authority_resolution_idempotency_key(resolution_id: StringName) -> StringName: + return StringName("authority.resolution.%s" % resolution_id) + +func _authority_fine_transaction_matches( + economy: EconomyService, + transaction_id: StringName, + inspection_id: StringName, + fine_amount: int +) -> bool: + if fine_amount == 0: + return transaction_id.is_empty() + var entries := economy.get_transaction_entries(transaction_id) + if entries.size() != 1: + return false + var entry: LedgerEntry = entries[0] + return entry.debit_account == AUTHORITY_FINE_ACCOUNT \ + and entry.credit_account == PLAYER_CASH_ACCOUNT \ + and entry.amount == fine_amount \ + and entry.category == &"AUTHORITY_FINE" \ + and entry.source_entity_id == inspection_id + +func _roll_defense_round(_key: StringName, _context: Dictionary) -> int: + var stream := get_rng_stream(&"defense") + return stream.randi_range(0, 100) if stream != null else 50 + +func _apply_defense_rng_boundary( + _context: Dictionary, + phase: StringName, + token: Variant +) -> Dictionary: + var stream := get_rng_stream(&"defense") + if stream == null: + return {&"ok": false, &"error": "Defense RNG stream is unavailable."} + if phase == &"snapshot": + return {&"ok": true, &"token": stream.snapshot()} + if phase == &"commit": + return {&"ok": true, &"token": token} + if phase == &"rollback": + if not token is Dictionary or not stream.restore(token): + return {&"ok": false, &"error": "Defense RNG rollback token is invalid."} + return {&"ok": true} + return {&"ok": false, &"error": "Unknown Defense RNG boundary phase."} + +func _apply_defense_skill_boundary( + payload: Dictionary, + phase: StringName, + rollback_token: Variant +) -> Dictionary: + if phase == &"rollback": + return _restore_defense_skill_token(rollback_token) + var cost_report := _parse_defense_skill_cost(payload) + if not bool(cost_report[&"ok"]): + return cost_report + var cost: Dictionary = cost_report[&"cost"] + var raid_id := StringName(payload.get(&"raid_id", payload.get("raid_id", ""))) + var action_id := StringName(payload.get(&"action_id", payload.get("action_id", ""))) + if raid_id.is_empty() or action_id.is_empty(): + return {&"ok": false, &"error": "Defense skill boundary IDs are invalid."} + if phase == &"validate": + var token := { + &"economy": economy_service.create_snapshot(), + &"facility_upkeep": facility_upkeep.create_snapshot(), + &"upkeep_settlement": upkeep_settlement.create_snapshot(), + &"game_state": _state.create_snapshot(), + &"inventory": inventory_service.create_snapshot(), + &"domain_events": domain_events.create_snapshot(), + &"id_generator": id_generator.create_snapshot(), + &"cost": cost.duplicate(true), + &"reservation_ids": [], + } + var gold := int(cost[&"gold"]) + var mana := int(cost[&"mana"]) + if economy_service.get_balance(PLAYER_CASH_ACCOUNT) < gold \ + or int(_state.get_resource(&"mana")) < mana: + return {&"ok": false, &"error": "Defense skill resources are insufficient."} + var ledger_key := _defense_skill_ledger_key(raid_id, action_id) + if gold > 0 and not economy_service.get_transaction_id_for_idempotency_key(ledger_key).is_empty(): + return {&"ok": false, &"error": "Defense skill ledger key already exists without an applied skill."} + var reservation_ids: Array[StringName] = [] + var items: Array = cost[&"items"] + for item_index: int in range(items.size()): + var item: Dictionary = items[item_index] + var reserved := inventory_service.reserve_item( + StringName(item[&"inventory_id"]), + StringName(item[&"item_id"]), + int(item[&"quantity"]), + action_id, + &"defense_skill", + _defense_operation_minute(), + -1, + StringName("defense.skill.reserve.%s.%s.%d" % [raid_id, action_id, item_index]) + ) + if not bool(reserved.get(&"ok", false)): + token[&"reservation_ids"] = reservation_ids + return { + &"ok": false, + &"token": token, + &"error": "Defense skill item reservation failed.", + &"details": reserved, + } + for reservation_value: Variant in reserved.get(&"reservation_ids", []): + reservation_ids.append(StringName(reservation_value)) + token[&"reservation_ids"] = reservation_ids + return {&"ok": true, &"token": token} + if phase != &"apply" or not _defense_skill_token_is_valid(rollback_token): + return {&"ok": false, &"token": rollback_token, &"error": "Defense skill apply token is invalid."} + var token: Dictionary = rollback_token + var mana := int(cost[&"mana"]) + if mana > 0 and not bool(_state.apply_resource_deltas({&"mana": -mana})): + return {&"ok": false, &"token": token, &"error": "Defense skill Mana application failed."} + var gold := int(cost[&"gold"]) + if gold > 0: + var posted := economy_service.transfer( + PLAYER_CASH_ACCOUNT, + OPERATING_EXPENSE_ACCOUNT, + gold, + _defense_operation_minute(), + &"DEFENSE_SKILL_COST", + action_id, + _defense_skill_ledger_key(raid_id, action_id), + &"defense.skill.cost" + ) + if not posted.is_success(): + return {&"ok": false, &"token": token, &"error": posted.message} + var reservation_ids: Array[StringName] = [] + for reservation_value: Variant in token[&"reservation_ids"]: + reservation_ids.append(StringName(reservation_value)) + if not reservation_ids.is_empty(): + var consumed := inventory_service.consume_reservations( + reservation_ids, + StringName("defense.skill.consume.%s.%s" % [raid_id, action_id]), + _defense_operation_minute() + ) + if not bool(consumed.get(&"ok", false)): + return {&"ok": false, &"token": token, &"error": "Defense skill item consumption failed.", &"details": consumed} + return {&"ok": true, &"token": token} + +func _parse_defense_skill_cost(payload: Dictionary) -> Dictionary: + var metadata_value: Variant = payload.get(&"metadata", payload.get("metadata", {})) + if not metadata_value is Dictionary: + return {&"ok": false, &"error": "Defense skill metadata must be a Dictionary."} + var metadata: Dictionary = metadata_value + var cost_value: Variant = metadata.get(&"cost", metadata.get("cost", {})) + if not cost_value is Dictionary: + return {&"ok": false, &"error": "Defense skill metadata.cost must be a Dictionary."} + var cost_data: Dictionary = cost_value + var gold_value: Variant = cost_data.get(&"gold", cost_data.get("gold", 0)) + var mana_value: Variant = cost_data.get(&"mana", cost_data.get("mana", 0)) + if not gold_value is int or not mana_value is int: + return {&"ok": false, &"error": "Defense skill Gold and Mana costs must be integers."} + var gold := int(gold_value) + var mana := int(mana_value) + if gold < 0 or mana < 0: + return {&"ok": false, &"error": "Defense skill costs cannot be negative."} + var items_value: Variant = cost_data.get(&"items", cost_data.get("items", [])) + if not items_value is Array: + return {&"ok": false, &"error": "Defense skill item costs must be an Array."} + var items: Array[Dictionary] = [] + for raw_item: Variant in items_value: + if not raw_item is Dictionary: + return {&"ok": false, &"error": "Defense skill item cost is malformed."} + var inventory_value: Variant = raw_item.get(&"inventory_id", raw_item.get("inventory_id", "")) + var item_value: Variant = raw_item.get(&"item_id", raw_item.get("item_id", "")) + var quantity_value: Variant = raw_item.get(&"quantity", raw_item.get("quantity", 0)) + if not (inventory_value is String or inventory_value is StringName) \ + or not (item_value is String or item_value is StringName) \ + or not quantity_value is int: + return {&"ok": false, &"error": "Defense skill item cost types are invalid."} + var inventory_id := StringName(inventory_value) + var item_id := StringName(item_value) + var quantity := int(quantity_value) + if inventory_id.is_empty() or item_id.is_empty() or quantity <= 0: + return {&"ok": false, &"error": "Defense skill item cost requires inventory_id, item_id and positive quantity."} + items.append({&"inventory_id": inventory_id, &"item_id": item_id, &"quantity": quantity}) + items.sort_custom(func(left: Dictionary, right: Dictionary) -> bool: + var left_key := "%s:%s" % [left[&"inventory_id"], left[&"item_id"]] + var right_key := "%s:%s" % [right[&"inventory_id"], right[&"item_id"]] + return left_key < right_key + ) + return {&"ok": true, &"cost": {&"gold": gold, &"mana": mana, &"items": items}} + +func _defense_skill_ledger_key(raid_id: StringName, action_id: StringName) -> StringName: + return StringName("defense.skill.%s.%s" % [raid_id, action_id]) + +func _defense_skill_token_is_valid(token: Variant) -> bool: + return token is Dictionary \ + and token.get(&"economy", null) is Dictionary \ + and token.get(&"game_state", null) is Dictionary \ + and token.get(&"inventory", null) is Dictionary \ + and token.get(&"domain_events", null) is Dictionary \ + and token.get(&"id_generator", null) is Dictionary \ + and token.get(&"reservation_ids", null) is Array + +func _restore_defense_skill_token(token: Variant) -> Dictionary: + if not _defense_skill_token_is_valid(token): + return {&"ok": false, &"error": "Defense skill rollback token is malformed."} + var economy_ok: bool = economy_service.restore_snapshot(token[&"economy"]) + var state_ok: bool = _state.restore_snapshot(token[&"game_state"]) + var inventory_ok: bool = inventory_service.restore_snapshot(token[&"inventory"]) + var events_ok: bool = domain_events.restore_snapshot(token[&"domain_events"]) + var ids_ok: bool = id_generator.restore_snapshot(token[&"id_generator"]) + return { + &"ok": economy_ok and state_ok and inventory_ok and events_ok and ids_ok, + &"error": "Defense skill rollback failed." if ( + not economy_ok or not state_ok or not inventory_ok or not events_ok or not ids_ok + ) else "", + } + +func _apply_defense_outcome_boundary( + payload: Dictionary, + phase: StringName, + rollback_token: Variant +) -> Dictionary: + if phase == &"rollback": + return _restore_defense_outcome_token(rollback_token) + var validation := _validate_defense_outcome_payload(payload) + if not bool(validation[&"ok"]): + return validation + if phase == &"validate": + return {&"ok": true, &"token": { + &"staff": staff_service.create_snapshot(), + &"facilities": facility_service.create_snapshot(), + &"facility_cleanliness": facility_cleanliness.create_snapshot(), + &"domain_events": domain_events.create_snapshot(), + &"id_generator": id_generator.create_snapshot(), + }} + if phase != &"apply" or not _defense_outcome_token_is_valid(rollback_token): + return {&"ok": false, &"token": rollback_token, &"error": "Defense outcome apply token is invalid."} + var token: Dictionary = rollback_token + var combatant_results: Array = payload[&"combatant_results"] + for result_value: Variant in combatant_results: + var result: Dictionary = result_value + if StringName(result[&"side"]) != &"DEFENDER": + continue + var staff := staff_service.get_staff(StringName(result[&"actor_id"])) + staff.health = int(result[&"health"]) + staff.fatigue = int(result[&"fatigue"]) + var defense_state := StringName(result[&"state"]) + if defense_state == &"ACTIVE": + staff.transition_to(StaffRuntime.State.AVAILABLE, &"DEFENSE_RELEASED", _defense_operation_minute()) + elif defense_state == &"INCAPACITATED": + staff.transition_to(StaffRuntime.State.INCAPACITATED, &"DEFENSE_INCAPACITATED", _defense_operation_minute()) + elif defense_state == &"RETREATED": + staff.transition_to(StaffRuntime.State.FLEEING, &"DEFENSE_RETREATED", _defense_operation_minute()) + var asset_results: Array = payload[&"asset_results"] + for result_value: Variant in asset_results: + var result: Dictionary = result_value + var updated := facility_service.set_operational_status( + StringName(result[&"asset_id"]), + {&"durability": int(result[&"integrity"])}, + _defense_operation_minute() + ) + if not updated.is_success(): + return {&"ok": false, &"token": token, &"error": updated.message} + var staff_report := staff_service.validate_invariants() + if not bool(staff_report.get(&"ok", false)): + return {&"ok": false, &"token": token, &"error": "Defense outcome violates Staff invariants.", &"details": staff_report} + return {&"ok": true, &"token": token} + +func _validate_defense_outcome_payload(payload: Dictionary) -> Dictionary: + var raid_id := StringName(payload.get(&"raid_id", payload.get("raid_id", ""))) + var resolution_id := StringName(payload.get(&"resolution_id", payload.get("resolution_id", ""))) + var operation_kind := StringName(payload.get(&"operation_kind", payload.get("operation_kind", ""))) + var absolute_minute := int(payload.get(&"absolute_minute", payload.get("absolute_minute", -1))) + var combatants_value: Variant = payload.get(&"combatant_results", payload.get("combatant_results", null)) + var assets_value: Variant = payload.get(&"asset_results", payload.get("asset_results", null)) + var raid := defense_service.get_raid(raid_id) + if raid_id.is_empty() or resolution_id.is_empty() \ + or operation_kind not in DefenseService.VALID_OPERATION_KINDS \ + or absolute_minute != _defense_operation_minute() \ + or not combatants_value is Array or not assets_value is Array \ + or raid.is_empty(): + return {&"ok": false, &"error": "Defense outcome payload root is invalid."} + var expected_combatants: Dictionary = raid[&"combatants"] + var expected_assets: Dictionary = raid[&"assets"] + if combatants_value.size() != expected_combatants.size() \ + or assets_value.size() != expected_assets.size(): + return {&"ok": false, &"error": "Defense outcome does not cover the complete Raid roster."} + var seen_combatants: Dictionary = {} + for value: Variant in combatants_value: + if not value is Dictionary: + return {&"ok": false, &"error": "Defense combatant outcome is malformed."} + var actor_id := StringName(value.get(&"actor_id", value.get("actor_id", ""))) + var side := StringName(value.get(&"side", value.get("side", ""))) + var state := StringName(value.get(&"state", value.get("state", ""))) + var health := int(value.get(&"health", value.get("health", -1))) + var maximum := int(value.get(&"max_health", value.get("max_health", -1))) + var fatigue := int(value.get(&"fatigue", value.get("fatigue", -1))) + if actor_id.is_empty() or seen_combatants.has(actor_id) or not expected_combatants.has(actor_id) \ + or side not in [&"DEFENDER", &"ATTACKER"] \ + or state not in [&"ACTIVE", &"INCAPACITATED", &"RETREATED"] \ + or health < 0 or health > maximum or fatigue < 0 or fatigue > 100 \ + or (health == 0 and state == &"ACTIVE"): + return {&"ok": false, &"error": "Defense combatant outcome values are invalid."} + var expected_combatant: Dictionary = expected_combatants[actor_id] + if side != StringName(expected_combatant[&"side"]) \ + or maximum != int(expected_combatant[&"max_health"]): + return {&"ok": false, &"error": "Defense combatant outcome differs from the Raid roster."} + seen_combatants[actor_id] = true + if side == &"DEFENDER" and (maximum != StaffRuntime.MAX_PSYCHOLOGY_VALUE or not staff_service.has_staff(actor_id)): + return {&"ok": false, &"error": "Defense outcome references invalid Staff."} + var seen_assets: Dictionary = {} + for value: Variant in assets_value: + if not value is Dictionary: + return {&"ok": false, &"error": "Defense asset outcome is malformed."} + var asset_id := StringName(value.get(&"asset_id", value.get("asset_id", ""))) + var integrity := int(value.get(&"integrity", value.get("integrity", -1))) + var maximum := int(value.get(&"max_integrity", value.get("max_integrity", -1))) + if asset_id.is_empty() or seen_assets.has(asset_id) or not expected_assets.has(asset_id) \ + or maximum != FacilityInstance.DEFAULT_DURABILITY \ + or integrity < 0 or integrity > maximum \ + or not facility_service.has_instance(asset_id): + return {&"ok": false, &"error": "Defense outcome references invalid Facility data."} + var expected_asset: Dictionary = expected_assets[asset_id] + if maximum != int(expected_asset[&"max_integrity"]): + return {&"ok": false, &"error": "Defense asset outcome differs from the Raid roster."} + seen_assets[asset_id] = true + return {&"ok": true} + +func _defense_outcome_token_is_valid(token: Variant) -> bool: + return token is Dictionary \ + and token.get(&"staff", null) is Dictionary \ + and token.get(&"facilities", null) is Dictionary \ + and token.get(&"domain_events", null) is Dictionary \ + and token.get(&"id_generator", null) is Dictionary + +func _restore_defense_outcome_token(token: Variant) -> Dictionary: + if not _defense_outcome_token_is_valid(token): + return {&"ok": false, &"error": "Defense outcome rollback token is malformed."} + var staff_ok: bool = staff_service.restore_snapshot(token[&"staff"]) + var facilities_ok: bool = facility_service.restore_snapshot(token[&"facilities"]) + facilities_ok = facility_cleanliness.restore_snapshot(token.get(&"facility_cleanliness", {})) and facilities_ok + var events_ok: bool = domain_events.restore_snapshot(token[&"domain_events"]) + var ids_ok: bool = id_generator.restore_snapshot(token[&"id_generator"]) + return { + &"ok": staff_ok and facilities_ok and events_ok and ids_ok, + &"error": "Defense outcome rollback failed." if not staff_ok or not facilities_ok or not events_ok or not ids_ok else "", + } + +func _execute_event_action_boundary( + action: EventAction, + phase: StringName, + context: Dictionary, + rollback_token: Variant +) -> Dictionary: + if action.type not in [ + EventAction.Type.ADD_LEDGER_ENTRY, + EventAction.Type.ADD_OR_REMOVE_ITEM, + EventAction.Type.CREATE_TASK, + EventAction.Type.CREATE_DEFENSE_RAID, + EventAction.Type.SET_FACILITY_STATE, + EventAction.Type.TELEPORT_ENTITY_FOR_RECOVERY, + ]: + return {&"not_handled": true} + if phase == &"rollback": + return _restore_event_action_token(rollback_token) + var validation := _build_event_defense_request(action, context) if action.type == EventAction.Type.CREATE_DEFENSE_RAID else _validate_event_action(action) + if not bool(validation.get(&"ok", false)) or phase == &"validate": + return validation + var token := _capture_event_action_token() + if action.type == EventAction.Type.CREATE_DEFENSE_RAID: + token[&"defense"] = defense_service.create_snapshot() + var result := _apply_event_action(action, context) + if not bool(result.get(&"ok", false)): + var rollback := _restore_event_action_token(token) + if not bool(rollback.get(&"ok", false)): + result[&"fatal"] = true + result[&"rollback"] = rollback + result[&"error"] = "Event Action failed and rollback failed: %s; %s" % [result.get(&"error", ""), rollback.get(&"error", "")] + return result + result[&"token"] = token + return result + +func _validate_event_action(action: EventAction) -> Dictionary: + match action.type: + EventAction.Type.ADD_LEDGER_ENTRY: + var from_account := StringName(action.payload.get(&"from_account", action.payload.get("from_account", PLAYER_CASH_ACCOUNT))) + var to_account := StringName(action.payload.get(&"to_account", action.payload.get("to_account", OPERATING_EXPENSE_ACCOUNT))) + var amount := int(action.payload.get(&"amount", action.payload.get("amount", 0))) + if amount <= 0 or not economy_service.has_account(from_account) or not economy_service.has_account(to_account): + return {&"ok": false, &"error": "Ledger Action has invalid accounts or amount."} + if not economy_service.account_allows_negative(from_account) and economy_service.get_balance(from_account) < amount: + return {&"ok": false, &"error": "Ledger Action source cannot fund the amount."} + EventAction.Type.ADD_OR_REMOVE_ITEM: + var inventory_id := StringName(action.payload.get(&"inventory_id", action.payload.get("inventory_id", ""))) + var item_id := StringName(action.payload.get(&"item_id", action.payload.get("item_id", ""))) + var delta := int(action.payload.get(&"quantity_delta", action.payload.get("quantity_delta", 0))) + if inventory_id.is_empty() or item_id.is_empty() or delta == 0: + return {&"ok": false, &"error": "Inventory Action requires inventory, item and non-zero delta."} + if delta < 0 and inventory_service.available_quantity(inventory_id, item_id) < -delta: + return {&"ok": false, &"error": "Inventory Action stock is unavailable."} + EventAction.Type.CREATE_TASK: + if StringName(action.payload.get(&"task_type", action.payload.get("task_type", ""))).is_empty(): + return {&"ok": false, &"error": "CREATE_TASK requires task_type."} + EventAction.Type.SET_FACILITY_STATE: + var facility_id := StringName(action.payload.get(&"facility_id", action.payload.get("facility_id", ""))) + var conditions_value: Variant = action.payload.get(&"conditions", action.payload.get("conditions", null)) + if not facility_service.has_instance(facility_id) or not conditions_value is Dictionary: + return {&"ok": false, &"error": "Facility Action target or conditions are invalid."} + var defense_owner: StringName = _defense_lock_owner(facility_id, &"ASSET") + if not defense_owner.is_empty() \ + and (conditions_value.has(&"durability") or conditions_value.has("durability")): + return {&"ok": false, &"error": "Event Action cannot mutate Defense-owned Facility durability."} + EventAction.Type.TELEPORT_ENTITY_FOR_RECOVERY: + var entity_id := StringName(action.payload.get(&"entity_id", action.payload.get("entity_id", ""))) + var cell_value: Variant = _parse_world_cell( + action.payload.get(&"cell", action.payload.get("cell", null)) + ) + if not _event_actor_exists(entity_id) or cell_value == null: + return {&"ok": false, &"error": "Recovery teleport target is invalid."} + var target_cell: Vector3i = cell_value + var traffic := path_service.get_cell_traffic(target_cell) + var actor_id := ( + _guest_path_actor_id(entity_id) + if guest_service.has_guest(entity_id) + else _staff_path_actor_id(entity_id) + ) + if traffic.is_empty() or bool(traffic.get(&"temporary_block", false)) \ + or not path_service.has_actor_projection(actor_id): + return {&"ok": false, &"error": "Recovery teleport requires a projected actor and an unblocked walkable target."} + return {&"ok": true} + +func _apply_event_action(action: EventAction, context: Dictionary) -> Dictionary: + var instance_id := StringName(context.get(&"event_instance_id", "")) + match action.type: + EventAction.Type.CREATE_DEFENSE_RAID: + return _apply_event_defense_raid(action, context) + EventAction.Type.ADD_LEDGER_ENTRY: + var from_account := StringName(action.payload.get(&"from_account", action.payload.get("from_account", PLAYER_CASH_ACCOUNT))) + var to_account := StringName(action.payload.get(&"to_account", action.payload.get("to_account", OPERATING_EXPENSE_ACCOUNT))) + var amount := int(action.payload.get(&"amount", action.payload.get("amount", 0))) + var result := economy_service.transfer( + from_account, to_account, amount, _runtime_event_minute(), + &"EVENT_ACTION", instance_id, + StringName("event.action.%s.%s" % [instance_id, action.action_id]) + ) + return {&"ok": result.is_success(), &"error": result.message} + EventAction.Type.ADD_OR_REMOVE_ITEM: + var inventory_id := StringName(action.payload.get(&"inventory_id", action.payload.get("inventory_id", ""))) + var item_id := StringName(action.payload.get(&"item_id", action.payload.get("item_id", ""))) + var delta := int(action.payload.get(&"quantity_delta", action.payload.get("quantity_delta", 0))) + if delta > 0: + var added := inventory_service.add_stack(inventory_id, item_id, delta, 100, -1, _runtime_event_minute()) + return {&"ok": bool(added.get(&"ok", false)), &"error": added.get(&"message", "")} + var reserved := inventory_service.reserve_item( + inventory_id, item_id, -delta, instance_id, &"event_action", + _runtime_event_minute(), _runtime_event_minute() + 1, + StringName("event.item.reserve.%s.%s" % [instance_id, action.action_id]) + ) + if not bool(reserved.get(&"ok", false)): + return {&"ok": false, &"error": reserved.get(&"message", "Item reservation failed.")} + var consumed := inventory_service.consume_reservations( + _name_array(reserved.get(&"reservation_ids", [])), + StringName("event.item.consume.%s.%s" % [instance_id, action.action_id]), + _runtime_event_minute() + ) + return {&"ok": bool(consumed.get(&"ok", false)), &"error": consumed.get(&"message", "")} + EventAction.Type.CREATE_TASK: + var raw_options: Variant = action.payload.get(&"options", action.payload.get("options", {})) + var options: Dictionary = raw_options.duplicate(true) if raw_options is Dictionary else {} + var created := staff_service.create_task( + StringName(action.payload.get(&"task_type", action.payload.get("task_type", ""))), + int(action.payload.get(&"priority", action.payload.get("priority", 50))), + _runtime_event_minute(), + options, + StringName(action.payload.get(&"task_id", action.payload.get("task_id", ""))) + ) + return {&"ok": created.is_success(), &"error": created.message} + EventAction.Type.SET_FACILITY_STATE: + var updated := facility_service.set_operational_status( + StringName(action.payload.get(&"facility_id", action.payload.get("facility_id", ""))), + action.payload.get(&"conditions", action.payload.get("conditions", {})), + _runtime_event_minute() + ) + return {&"ok": updated.is_success(), &"error": updated.message} + EventAction.Type.TELEPORT_ENTITY_FOR_RECOVERY: + var entity_id := StringName(action.payload.get(&"entity_id", action.payload.get("entity_id", ""))) + var cell: Vector3i = _parse_world_cell(action.payload.get(&"cell", action.payload.get("cell", null))) + var guest := guest_service.get_guest(entity_id) + var actor_id: StringName + var moved := false + if guest != null: + actor_id = _guest_path_actor_id(entity_id) + moved = guest_service.record_path_success( + entity_id, cell, _runtime_event_minute() + ) + else: + var staff := staff_service.get_staff(entity_id) + actor_id = _staff_path_actor_id(entity_id) + staff.current_cell = cell + moved = true + if not moved or not path_service.project_actor_position( + actor_id, cell, _runtime_event_minute() + ): + return {&"ok": false, &"error": "Recovery teleport could not commit its Path projection."} + _movement_credits[actor_id] = { + &"credit": 0, + &"updated_at": _runtime_event_minute(), + } + _publish_actor_event(&"ENTITY_RECOVERY_TELEPORTED", &"event_service", [entity_id, actor_id], { + &"cell": cell, + &"path_actor_id": actor_id, + }) + return {&"ok": true} + return {&"ok": false, &"error": "Unsupported Event Action."} + +func _capture_event_action_token() -> Dictionary: + return { + &"economy": economy_service.create_snapshot(), + &"facility_upkeep": facility_upkeep.create_snapshot(), + &"upkeep_settlement": upkeep_settlement.create_snapshot(), + &"game_state": _state.create_snapshot(), + &"inventory": inventory_service.create_snapshot(), + &"staff": staff_service.create_snapshot(), + &"facilities": facility_service.create_snapshot(), + &"facility_cleanliness": facility_cleanliness.create_snapshot(), + &"guests": guest_service.create_snapshot(), + &"path": path_service.create_snapshot(), + &"elevators": elevators.create_snapshot(), + &"legacy_stairs": legacy_stairs.create_snapshot(), + &"movement_credits": _movement_credits.duplicate(true), + &"domain_events": domain_events.create_snapshot(), + &"id_generator": id_generator.create_snapshot(), + } + +func _restore_event_action_token(token: Variant) -> Dictionary: + if not token is Dictionary: + return {&"ok": false, &"error": "Event rollback token is malformed."} + # Every participant must be asked to restore even if an earlier participant + # reports an error; short-circuiting here would leave a half-rolled-back world. + var economy_ok: bool = economy_service.restore_snapshot(token.get(&"economy", {})) + var state_ok: bool = _state.restore_snapshot(token.get(&"game_state", {})) + var inventory_ok: bool = inventory_service.restore_snapshot(token.get(&"inventory", {})) + var staff_ok: bool = staff_service.restore_snapshot(token.get(&"staff", {})) + var facilities_ok: bool = facility_service.restore_snapshot(token.get(&"facilities", {})) + facilities_ok = facility_cleanliness.restore_snapshot(token.get(&"facility_cleanliness", {})) and facilities_ok + var guests_ok: bool = guest_service.restore_snapshot(token.get(&"guests", {})) + var path_ok: bool = path_service.restore_snapshot(token.get(&"path", {})) + path_ok = elevators.restore_snapshot(token.get(&"elevators", {})) and path_ok + path_ok = legacy_stairs.restore_snapshot(token.get(&"legacy_stairs", {})) and path_ok + var credits_value: Variant = token.get(&"movement_credits", null) + var credits_ok := credits_value is Dictionary + if credits_ok: + _movement_credits = (credits_value as Dictionary).duplicate(true) + var defense_ok := true + if token.has(&"defense"): + # This creates PREPARING incidents only; rollback must not reset another + # raid's pre-existing same-tick automatic/manual attempt guard. + defense_ok = token[&"defense"] is Dictionary and defense_service.restore_snapshot(token[&"defense"]) + var events_ok: bool = domain_events.restore_snapshot(token.get(&"domain_events", {})) + var ids_ok: bool = id_generator.restore_snapshot(token.get(&"id_generator", {})) + var ok: bool = economy_ok and state_ok and inventory_ok and staff_ok \ + and facilities_ok and guests_ok and path_ok and credits_ok and defense_ok and events_ok and ids_ok + return {&"ok": ok, &"error": "Event Action rollback failed." if not ok else ""} + +func _reserve_event_cost(cost: Dictionary, context: Dictionary) -> Dictionary: + var normalized: Dictionary = {} + for raw_resource: Variant in cost: + var resource_id := StringName(raw_resource) + var amount := int(cost[raw_resource]) + if resource_id.is_empty() or amount < 0: + return {&"ok": false, &"error": "Event Choice cost is invalid."} + normalized[resource_id] = amount + var economy_before := economy_service.create_snapshot() + var state_before: Dictionary = _state.create_snapshot() + var ids_before := id_generator.create_snapshot() + var instance_id := StringName(context.get(&"event_instance_id", "")) + var reservation_id: StringName = &"" + for amount: int in normalized.values(): + if amount > 0: + if id_generator.get_counter(&"event_cost") >= StableIdGenerator.MAX_COUNTER: + return {&"ok": false, &"error": "Event cost reservation IDs are exhausted."} + reservation_id = id_generator.next_id(&"event_cost") + break + var escrow_account := StringName("EVENT_ESCROW:%s" % instance_id) + var reservation := {&"event_instance_id": instance_id, &"cost": normalized, &"escrow_account": escrow_account} + if not reservation_id.is_empty(): + reservation[&"reservation_id"] = reservation_id + var gold := int(normalized.get(&"gold", 0)) + if gold > 0: + if not economy_service.has_account(escrow_account): + economy_service.register_account(escrow_account) + var transfer := economy_service.transfer( + PLAYER_CASH_ACCOUNT, escrow_account, gold, _runtime_event_minute(), + &"EVENT_COST_RESERVED", instance_id, + _event_cost_ledger_key(reservation, &"reserve") + ) + if not transfer.is_success(): + economy_service.restore_snapshot(economy_before) + _state.restore_snapshot(state_before) + id_generator.restore_snapshot(ids_before) + return {&"ok": false, &"error": transfer.message} + var other_costs := normalized.duplicate(true) + other_costs.erase(&"gold") + if not other_costs.is_empty() and not _state.spend(other_costs): + economy_service.restore_snapshot(economy_before) + _state.restore_snapshot(state_before) + id_generator.restore_snapshot(ids_before) + return {&"ok": false, &"error": "Event Choice resources are unavailable."} + return {&"ok": true, &"token": reservation} + +func _release_event_cost(token: Variant, _context: Dictionary) -> Dictionary: + if token is Dictionary and token.is_empty(): + return {&"ok": true} + if not token is Dictionary or not token.get(&"cost", null) is Dictionary: + return {&"ok": false, &"error": "Event Cost release token is malformed."} + var cost: Dictionary = token[&"cost"] + var instance_id := StringName(token.get(&"event_instance_id", "")) + var escrow_account := StringName(token.get(&"escrow_account", "")) + var gold := int(cost.get(&"gold", 0)) + if gold > 0: + var released := economy_service.transfer( + escrow_account, PLAYER_CASH_ACCOUNT, gold, _runtime_event_minute(), + &"EVENT_COST_RELEASED", instance_id, + _event_cost_ledger_key(token, &"release") + ) + if not released.is_success(): + return {&"ok": false, &"error": released.message} + for raw_resource: Variant in cost: + var resource_id := StringName(raw_resource) + if resource_id != &"gold": + _state.add_resource(resource_id, int(cost[raw_resource])) + return {&"ok": true} + +func _commit_event_cost(token: Variant, _context: Dictionary) -> Dictionary: + if token is Dictionary and token.is_empty(): + return {&"ok": true} + if not token is Dictionary or not token.get(&"cost", null) is Dictionary: + return {&"ok": false, &"error": "Event Cost commit token is malformed."} + var cost: Dictionary = token[&"cost"] + var gold := int(cost.get(&"gold", 0)) + if gold <= 0: + return {&"ok": true} + var instance_id := StringName(token.get(&"event_instance_id", "")) + var committed := economy_service.transfer( + StringName(token.get(&"escrow_account", "")), OPERATING_EXPENSE_ACCOUNT, + gold, _runtime_event_minute(), &"EVENT_COST_COMMITTED", instance_id, + _event_cost_ledger_key(token, &"commit") + ) + return {&"ok": committed.is_success(), &"error": committed.message} + +func _event_cost_ledger_key(token: Dictionary, operation: StringName) -> StringName: + var reservation_id := StringName(token.get(&"reservation_id", "")) + if not reservation_id.is_empty(): + return StringName("event.reserved_cost.%s.%s" % [operation, reservation_id]) + # Existing pending save tokens keep the original ledger operation identity. + return StringName("event.cost.%s.%s" % [operation, token.get(&"event_instance_id", "")]) + +func _on_relationship_threshold_candidate(candidate: Dictionary) -> void: + var candidate_id := StringName(candidate.get(&"candidate_id", "")) + if candidate_id.is_empty() or event_service.get_instance(candidate_id) != null: + return + event_service.enqueue_candidate( + &"relationship.threshold", + candidate.get(&"actor_ids", []), + int(candidate.get(&"created_at", _runtime_event_minute())), + { + &"event_instance_id": candidate_id, + &"deduplication_key": StringName(candidate.get(&"deduplication_key", candidate_id)), + &"context": {&"relationship_candidate": candidate.duplicate(true)}, + } + ) + +func _route_lock_index(value: Variant) -> int: + if value is int: + return value if value >= 0 and value < RelationshipState.RouteLock.keys().size() else -1 + return RelationshipState.RouteLock.keys().find(String(value).to_upper()) + + +func _validate_construction_facility_crosslinks( + world: WorldService, + construction: ConstructionService, + facilities: FacilityService, + projector: FacilityInfrastructureProjector = null +) -> bool: + for construction_facility: Dictionary in construction.get_facilities(): + var instance_id := StringName(construction_facility.get(&"instance_id", &"")) + var definition_id := StringName(construction_facility.get(&"definition_id", &"")) + var definition := definition_registry.get_facility(definition_id) + if instance_id.is_empty() or definition == null: + return false + if _facility_definition_is_schema_proxy(definition): + if facilities.has_instance(instance_id): + return false + if _facility_definition_is_supported_infrastructure_proxy(definition): + if projector == null or not projector.has_projection(instance_id): + return false + elif projector != null and projector.has_projection(instance_id): + return false + for cell: Vector3i in construction_facility.get(&"occupied_cells", []): + if world.get_occupant(cell) != instance_id \ + or world.get_occupant_kind(cell) != &"facility": + return false + continue + var operational := facilities.get_instance(instance_id) + if operational == null or (projector != null and projector.has_projection(instance_id)): + return false + if operational.definition_id != StringName( + construction_facility.get(&"definition_id", &"") + ) or operational.origin != construction_facility.get(&"origin", Vector3i.ZERO): + return false + var expected_cells: Dictionary = {} + for cell: Vector3i in construction_facility.get(&"occupied_cells", []): + expected_cells[cell] = true + var actual_cells: Dictionary = {} + for cell: Vector3i in operational.occupied_cells: + actual_cells[cell] = true + if expected_cells != actual_cells: + return false + for cell: Vector3i in expected_cells: + if world.get_occupant(cell) != instance_id \ + or world.get_occupant_kind(cell) != &"facility": + return false + return true + + +func _facility_definition_is_schema_proxy(definition: Variant) -> bool: + return definition is FacilityDefinition and &"schema_facility_proxy" in definition.tags + + +func _facility_definition_is_supported_infrastructure_proxy(definition: Variant) -> bool: + return _facility_definition_is_schema_proxy(definition) \ + and definition.id in FacilityInfrastructureProjector.SUPPORTED_KINDS + + +func _facility_definition_is_unsupported_vertical_proxy(definition: Variant) -> bool: + return _facility_definition_is_schema_proxy(definition) \ + and definition.id in FacilityInfrastructureProjector.UNSUPPORTED_VERTICAL_KINDS + + +func _migrate_legacy_infrastructure_proxies( + construction: ConstructionService, + facilities: FacilityService, + projector: FacilityInfrastructureProjector +) -> bool: + if construction == null or facilities == null or projector == null: + return false + for instance: Dictionary in construction.get_facilities(): + var instance_id := StringName(instance.get(&"instance_id", &"")) + var definition := definition_registry.get_facility( + StringName(instance.get(&"definition_id", &"")) + ) + if instance_id.is_empty() or definition == null: + return false + if not _facility_definition_is_schema_proxy(definition): + continue + if facilities.has_instance(instance_id) \ + and not facilities.unregister_instance(instance_id).is_success(): + return false + if _facility_definition_is_unsupported_vertical_proxy(definition): + continue + if not _facility_definition_is_supported_infrastructure_proxy(definition): + return false + var projected := projector.project_completed_facility( + StringName("runtime.migrate.v6:%s" % instance_id), + definition, + instance + ) + if not projected.is_success(): + return false + return true + + +func _validate_infrastructure_crosslinks( + world: WorldService, + path: PathService, + construction: ConstructionService, + _resource_state: Variant, + excavation: ExcavationService, + spatial: SpatialSubslotService, + adapter: RouteSubslotPathAdapter, + projector: FacilityInfrastructureProjector, + require_minute_alignment := true +) -> bool: + if world == null or path == null or construction == null or excavation == null \ + or spatial == null or adapter == null or projector == null: + return false + # An active projector validates its bound Spatial and RouteAdapter services + # itself. Reuse that composed check only for these exact dependencies. + # Quarantine intentionally suppresses its live dependency check, so retain + # the independent validations on that path (and for a different binding). + var composed_dependencies := projector.is_bound_to(adapter, spatial, path) and not projector.is_quarantined() + if not adapter.is_bound_to(spatial, path) \ + or (not composed_dependencies and ( + not bool(spatial.validate_invariants().get(&"ok", false)) + or not bool(adapter.validate_invariants().get(&"ok", false)))) \ + or not bool(projector.validate_invariants().get(&"ok", false)) \ + or not bool(excavation.validate_invariants().get(&"ok", false)) \ + or (require_minute_alignment \ + and excavation.current_minute != construction.current_minute) \ + or not _same_vector3i_set( + adapter.get_base_walkable_cells(false), world.get_walkable_cells(false) + ) \ + or not _same_vector3i_set( + adapter.get_projected_route_cells(), spatial.get_route_cells() + ) \ + or not path.matches_passable_cells(adapter.get_projected_walkable_cells()): + return false + var expected_projection_ids: Dictionary = {} + for instance: Dictionary in construction.get_facilities(): + var instance_id := StringName(instance.get(&"instance_id", &"")) + var definition_id := StringName(instance.get(&"definition_id", &"")) + var definition := definition_registry.get_facility(definition_id) + if instance_id.is_empty() or definition == null: + return false + if not _facility_definition_is_schema_proxy(definition): + if projector.has_projection(instance_id): + return false + continue + if _facility_definition_is_unsupported_vertical_proxy(definition): + if projector.has_projection(instance_id): + return false + continue + if not _facility_definition_is_supported_infrastructure_proxy(definition): + return false + expected_projection_ids[instance_id] = true + var projection := projector.get_projection(instance_id) + if projection.is_empty() \ + or StringName(projection.get(&"definition_id", &"")) != definition_id \ + or StringName(projection.get(&"source_job_id", &"")) \ + != StringName(instance.get(&"source_job_id", &"")) \ + or projection.get(&"origin", null) != instance.get(&"origin", null) \ + or int(projection.get(&"rotation", -1)) != int(instance.get(&"rotation", -2)) \ + or int(projection.get(&"layer", -1)) != int(instance.get(&"layer", -2)) \ + or not _same_vector3i_set( + projection.get(&"cells", []), instance.get(&"occupied_cells", []) + ): + return false + var actual_projection_ids: Dictionary = {} + for instance_id: StringName in projector.get_projection_ids(): + if actual_projection_ids.has(instance_id): + return false + actual_projection_ids[instance_id] = true + return actual_projection_ids == expected_projection_ids + + +func _same_vector3i_set(first: Variant, second: Variant) -> bool: + if not first is Array or not second is Array or first.size() != second.size(): + return false + var expected: Dictionary = {} + for value: Variant in first: + if not value is Vector3i or expected.has(value): + return false + expected[value] = true + for value: Variant in second: + if not value is Vector3i or not expected.has(value): + return false + expected.erase(value) + return expected.is_empty() + + +func _validate_runtime_crosslinks( + facilities: FacilityService, + guests: GuestService, + economy: EconomyService, + inventory: InventoryService, + staff: StaffService, + absolute_minute: int, + staffing_projection_enabled: bool +) -> bool: + if absolute_minute < 0 \ + or facilities.current_minute != absolute_minute \ + or guests.current_minute != absolute_minute \ + or inventory.current_minute != absolute_minute \ + or staff.current_minute != absolute_minute: + return false + for assignment: Dictionary in staff.get_work_assignments(): + var facility_id := StringName(assignment[&"facility_id"]) + if facility_id.is_empty(): + continue + var assigned_facility := facilities.get_instance(facility_id) + if assigned_facility == null: + return false + var assigned_staff := staff.get_staff(StringName(assignment[&"staff_id"])) + var definition := definition_registry.get_facility(assigned_facility.definition_id) + if assigned_staff == null or definition == null or definition.staff_max <= 0 \ + or (not _staff_role_matches_facility(assigned_staff.role_id, definition) and not _is_anri_manager(assigned_staff)): + return false + var queued_links: Dictionary = {} + var service_links: Dictionary = {} + for facility: FacilityInstance in facilities.get_instances(): + if staffing_projection_enabled: + for staff_id: StringName in facility.active_staff_ids: + var active_staff := staff.get_staff(staff_id) + if active_staff == null \ + or active_staff.assigned_facility_id != facility.instance_id \ + or (active_staff.state != StaffRuntime.State.AVAILABLE and not CleaningBridge.is_deployed(active_staff, facility, staff)) \ + or _is_anri_manager(active_staff): + return false + for request: ServiceRequest in facilities.get_queue(facility.instance_id): + if not _reservation_references_are_valid( + request.reserved_inventory_ids, request.request_id, inventory + ): + return false + for guest_id: StringName in request.guest_ids: + if not guests.has_guest(guest_id): + continue + var guest := guests.get_guest(guest_id) + if guest.state != GuestRuntime.State.QUEUING \ + or guest.queue_request_id != request.request_id \ + or guest.target_facility_id != facility.instance_id: + return false + queued_links[guest_id] = request.request_id + for service: ServiceInstance in facilities.get_active_services(facility.instance_id): + if not _reservation_references_are_valid( + service.inventory_reservations, service.request_id, inventory + ): + return false + for guest_id: StringName in service.guest_ids: + if not guests.has_guest(guest_id): + continue + var guest := guests.get_guest(guest_id) + if guest.state != GuestRuntime.State.RECEIVING_SERVICE \ + or guest.current_service_id != service.service_id \ + or guest.target_facility_id != facility.instance_id: + return false + service_links[guest_id] = service.service_id + for guest: GuestRuntime in guests.get_guests(): + if not guest.queue_request_id.is_empty() \ + and queued_links.get(guest.guest_id, &"") != guest.queue_request_id: + return false + if not guest.current_service_id.is_empty() \ + and service_links.get(guest.guest_id, &"") != guest.current_service_id: + return false + var account_id := _guest_wallet_account(guest.guest_id) + if not economy.has_account(account_id) \ + or economy.get_balance(account_id) != guest.wallet_gold: + return false + for task: StaffTask in staff.get_tasks(): + if not _reservation_references_are_valid( + task.reservation_ids, task.task_id, inventory + ): + return false + return true + +func _validate_authority_economy_crosslinks( + authority: AuthorityService, + economy: EconomyService, + absolute_minute: int +) -> bool: + if authority == null or authority.current_minute != absolute_minute \ + or not bool(authority.validate_invariants().get(&"ok", false)) \ + or not economy.has_account(AUTHORITY_FINE_ACCOUNT): + return false + var authority_snapshot := authority.create_snapshot() + var raw_resolutions: Variant = authority_snapshot.get(&"applied_resolutions", null) + if not raw_resolutions is Array: + return false + var expected_fine_balance := 0 + for raw_resolution: Variant in raw_resolutions: + if not raw_resolution is Dictionary \ + or not raw_resolution.get(&"payload", null) is Dictionary: + return false + var resolution_id := StringName(raw_resolution.get(&"resolution_id", "")) + var payload: Dictionary = raw_resolution[&"payload"] + var inspection_id := StringName(payload.get(&"inspection_id", "")) + var fine_amount := int(payload.get(&"fine_amount", -1)) + if resolution_id.is_empty() or inspection_id.is_empty() or fine_amount < 0: + return false + expected_fine_balance += fine_amount + if fine_amount == 0: + continue + var transaction_id := economy.get_transaction_id_for_idempotency_key( + _authority_resolution_idempotency_key(resolution_id) + ) + if transaction_id.is_empty() or not _authority_fine_transaction_matches( + economy, + transaction_id, + inspection_id, + fine_amount + ): + return false + return economy.get_balance(AUTHORITY_FINE_ACCOUNT) == expected_fine_balance + +func _validate_live_defense_crosslinks() -> bool: + return _validate_defense_crosslinks( + defense_service, + staff_service, + facility_service, + _defense_operation_minute() + ) + +func _validate_defense_crosslinks( + defense: DefenseService, + staff: StaffService, + facilities: FacilityService, + absolute_minute: int +) -> bool: + if defense == null or staff == null or facilities == null \ + or absolute_minute < 0 or defense.current_minute != absolute_minute \ + or not bool(defense.validate_invariants().get(&"ok", false)): + return false + var defender_owners: Dictionary = {} + var asset_owners: Dictionary = {} + for raid_id: StringName in defense.get_raid_ids(): + var raid := defense.get_raid(raid_id) + if raid.is_empty(): + return false + var state := int(raid.get(&"state", -1)) + var terminal := state in DefenseService.TERMINAL_STATES + var preparing := state == DefenseService.RaidState.PREPARING + var combatants_value: Variant = raid.get(&"combatants", null) + var assets_value: Variant = raid.get(&"assets", null) + if not combatants_value is Dictionary or not assets_value is Dictionary: + return false + var combatants: Dictionary = combatants_value + for actor_id: StringName in _sorted_name_keys(combatants): + var combatant_value: Variant = combatants[actor_id] + if not combatant_value is Dictionary: + return false + var combatant: Dictionary = combatant_value + if StringName(combatant.get(&"side", &"")) != &"DEFENDER": + continue + if not terminal: + if defender_owners.has(actor_id): + return false + defender_owners[actor_id] = raid_id + if int(combatant.get(&"max_health", -1)) != StaffRuntime.MAX_PSYCHOLOGY_VALUE \ + or int(combatant.get(&"initial_health", -1)) != StaffRuntime.MAX_PSYCHOLOGY_VALUE: + return false + # A terminal Raid is historical. Its reconciled Staff may later heal, + # work elsewhere, leave the roster, or enter another Raid; requiring the + # old outcome to remain a permanent live mirror would make future saves + # unrestorable. Capacity baselines remain self-contained above. + if terminal: + continue + var linked_staff := staff.get_staff(actor_id) + if linked_staff == null or linked_staff.state != StaffRuntime.State.UNAVAILABLE \ + or not linked_staff.current_task_id.is_empty(): + return false + if preparing and ( + int(combatant.get(&"health", -1)) != linked_staff.health + or int(combatant.get(&"fatigue", -1)) != linked_staff.fatigue + ): + return false + var assets: Dictionary = assets_value + for asset_id: StringName in _sorted_name_keys(assets): + var asset_value: Variant = assets[asset_id] + if not asset_value is Dictionary: + return false + var asset: Dictionary = asset_value + if not terminal: + if asset_owners.has(asset_id): + return false + asset_owners[asset_id] = raid_id + if int(asset.get(&"max_integrity", -1)) != FacilityInstance.DEFAULT_DURABILITY \ + or int(asset.get(&"initial_integrity", -1)) != FacilityInstance.DEFAULT_DURABILITY: + return false + # Terminal assets are historical for the same reason as terminal Staff: + # the live Facility can subsequently be repaired, reused, or demolished. + if terminal: + continue + var facility := facilities.get_instance(asset_id) + if facility == null: + return false + if preparing and int(asset.get(&"integrity", -1)) != facility.durability: + return false + return true + +func _defense_lock_owner( + external_id: StringName, + lock_kind: StringName, + excluded_raid_id: StringName = &"" +) -> StringName: + if defense_service == null or external_id.is_empty() \ + or lock_kind not in [&"DEFENDER", &"ASSET"]: + return &"" + for raid_id: StringName in defense_service.get_raid_ids(): + if raid_id == excluded_raid_id: + continue + var raid := defense_service.get_raid(raid_id) + if raid.is_empty() or int(raid.get(&"state", -1)) in DefenseService.TERMINAL_STATES: + continue + if lock_kind == &"DEFENDER": + var combatants_value: Variant = raid.get(&"combatants", {}) + if not combatants_value is Dictionary or not combatants_value.has(external_id): + continue + var combatant_value: Variant = combatants_value[external_id] + if combatant_value is Dictionary \ + and StringName(combatant_value.get(&"side", &"")) == &"DEFENDER": + return raid_id + else: + var assets_value: Variant = raid.get(&"assets", {}) + if assets_value is Dictionary and assets_value.has(external_id): + return raid_id + return &"" + +func _reservation_references_are_valid( + reservation_ids: Array[StringName], + requester_id: StringName, + inventory: InventoryService +) -> bool: + for reservation_id: StringName in reservation_ids: + var reservation := inventory.get_reservation(reservation_id) + if reservation == null or reservation.requester_id != requester_id: + return false + return true + +func _advance_mana(absolute_minute: int) -> void: + if _operations_tick_blocked: + return + var infrastructure_preflight := _preflight_proxy_completions(absolute_minute) + if not bool(infrastructure_preflight.get(&"ok", false)): + _latch_infrastructure_failure( + StringName(infrastructure_preflight.get( + &"code", &"INFRASTRUCTURE_PREFLIGHT_FAILED" + )), + absolute_minute, + String(infrastructure_preflight.get( + &"reason", "Infrastructure completion preflight failed." + )), + infrastructure_preflight.get(&"details", {}) + ) + return + var report := mana_network_service.advance_one_minute(absolute_minute) + # Finite reserve is an additional gate, even between network evaluations and + # for zero-demand facilities. The network remains the local supply authority. + preload("res://scripts/mana/mana_power_projection.gd").apply( + facility_service, mana_network_service, int(_state.get_resource(&"mana")), absolute_minute + ) + for change: Dictionary in report.get(&"state_changes", []): + var consumer_id := StringName(change.get(&"consumer_id", "")) + _publish_domain_event(SimulationDomainEvent.new( + id_generator.next_id(&"domain_event"), + &"MANA_STATE_CHANGED", + absolute_minute, + &"mana_network_service", + [consumer_id], + change, + 0 + )) + +func _on_facility_completed(instance: Dictionary) -> void: + # The staged Runtime boundary has already projected/registered this Facility + # and committed its DomainEvents before Construction releases raw signals. + if _construction_committing_notifications: + return + var instance_id := StringName(instance.get(&"instance_id", &"")) + var definition := definition_registry.get_facility( + StringName(instance.get(&"definition_id", &"")) + ) + if instance_id.is_empty() or definition == null: + _latch_infrastructure_failure( + &"INVALID_COMPLETED_FACILITY", + _runtime_event_minute(), + "Construction emitted an invalid completed Facility.", + {&"instance_id": instance_id} + ) + return + if _facility_definition_is_schema_proxy(definition): + if not _facility_definition_is_supported_infrastructure_proxy(definition): + _latch_infrastructure_failure( + &"UNSUPPORTED_VERTICAL_TOPOLOGY", + _runtime_event_minute(), + "An unsupported vertical proxy reached Construction completion.", + {&"instance_id": instance_id, &"definition_id": definition.id} + ) + return + var projection_request_id := StringName( + "runtime.complete:%s" % String(instance.get(&"source_job_id", instance_id)) + ) + var projected := facility_infrastructure_projector.project_completed_facility( + projection_request_id, + definition, + instance + ) + if not projected.is_success(): + _latch_infrastructure_failure( + &"FACILITY_INFRASTRUCTURE_PROJECTION_FAILED", + _runtime_event_minute(), + projected.message, + projected.details + ) + return + _publish_domain_event(SimulationDomainEvent.new( + id_generator.next_id(&"domain_event"), + &"FACILITY_INFRASTRUCTURE_PROJECTED", + _runtime_event_minute(), + &"facility_infrastructure_projector", + [instance_id], + projected.details.duplicate(true), + 0 + )) + else: + _register_facility_mana_consumer(instance) + _register_operational_facility(instance) + _publish_domain_event(SimulationDomainEvent.new( + id_generator.next_id(&"domain_event"), + &"FACILITY_COMPLETED", + _runtime_event_minute(), + &"construction_service", + [instance_id], + instance, + 0 + )) + + +func _on_excavation_completed(report: Dictionary) -> void: + var job_id := StringName(report.get(&"job_id", &"")) + if job_id.is_empty(): + return + var synchronized := route_subslot_path_adapter.synchronize_projection( + StringName("runtime.excavation:%s" % job_id), + world_service.get_walkable_cells() + ) + if not synchronized.is_success(): + _latch_infrastructure_failure( + &"EXCAVATION_ROUTE_SYNCHRONIZATION_FAILED", + int(report.get(&"completed_at", _runtime_event_minute())), + synchronized.message, + synchronized.details + ) + return + _publish_domain_event(SimulationDomainEvent.new( + id_generator.next_id(&"domain_event"), + &"EXCAVATION_COMPLETED", + int(report.get(&"completed_at", _runtime_event_minute())), + &"excavation_service", + [job_id], + report.duplicate(true), + 0 + )) + +func _on_facility_demolished(instance_id: StringName, refund: Dictionary) -> void: + if not mana_network_service.get_node(instance_id).is_empty(): + mana_network_service.unregister_node(instance_id) + if facility_service.has_instance(instance_id): + facility_service.unregister_instance(instance_id) + _publish_domain_event(SimulationDomainEvent.new( + id_generator.next_id(&"domain_event"), + &"FACILITY_DEMOLISHED", + _runtime_event_minute(), + &"construction_service", + [instance_id], + {&"refund": refund}, + 0 + )) + +func _on_ledger_transaction_posted(transaction_id: StringName, _entries: Array) -> void: + if _operations_flushing_economy_notifications.has(transaction_id): + return + if _state != null: + _state.set_resource(&"gold", economy_service.get_balance(PLAYER_CASH_ACCOUNT)) + + +func get_mvp_content_report() -> Dictionary: + return mvp_content_report.duplicate(true) + + +func get_mvp_guest_definition_ids() -> Array[StringName]: + var result: Array[StringName] = [] + for definition: GuestDefinition in mvp_content_catalog.build_all_guest_definitions(): + result.append(definition.guest_type_id) + return result + + +func get_mvp_staff_candidate_ids() -> Array[StringName]: + return mvp_content_catalog.get_staff_candidate_ids() + + +func get_mvp_staff_candidate_payload(candidate_id: StringName) -> Dictionary: + return mvp_content_catalog.staff_hire_payload(candidate_id).duplicate(true) + + +## Read-only presentation data for the hiring screen. The validated catalog +## remains authoritative and the UI never receives a mutable reference to it. +func get_mvp_staff_candidate_summaries() -> Array[Dictionary]: + var summaries: Array[Dictionary] = [] + for candidate_id: StringName in mvp_content_catalog.get_recruitable_staff_ids(): + var entry := mvp_content_catalog.get_recruitment_entry(candidate_id) + var payload := mvp_content_catalog.staff_hire_payload(candidate_id) + if entry.is_empty() or payload.is_empty(): + continue + var options_value: Variant = payload.get(&"options", {}) + var options: Dictionary = options_value if options_value is Dictionary else {} + var skills_value: Variant = options.get(&"skills", {}) + var skills: Dictionary = skills_value if skills_value is Dictionary else {} + summaries.append({ + &"candidate_id": candidate_id, + &"display_name": String(entry.get(&"display_name", candidate_id)), + &"age": int(payload.get(&"age", 0)), + &"role_id": StringName(payload.get(&"role_id", &"")), + &"daily_wage": int(payload.get(&"daily_wage", 0)), + &"signing_cost": int(payload.get(&"signing_cost", 0)), + &"preferred_shift": StringName(payload.get(&"preferred_shift", &"any")), + &"skills": skills.duplicate(true), + &"hired": _hired_staff_candidate_ids.has(candidate_id), + &"unique": candidate_id == &"heroine_anri", + }) + return summaries + + +## Defensive presentation snapshot for the Staff management panel. Planned +## assignment remains visible at night/off-duty even when the active link clears. +func get_staff_management_report() -> Dictionary: + var rows: Array[Dictionary] = [] + for staff: StaffRuntime in staff_service.get_staff_members(): + var entry := mvp_content_catalog.get_recruitment_entry(staff.staff_id) + var plan := staff_service.get_work_assignment(staff.staff_id) + var shift := staff_service.get_shift(staff.shift_id) + var template_id: StringName = &"DAY" + var effective_from := _runtime_event_minute() + var next_start_at := -1 + var assigned_facility_id := staff.assigned_facility_id + if not plan.is_empty(): + template_id = StringName(plan[&"template_id"]) + assigned_facility_id = StringName(plan[&"facility_id"]) + effective_from = int(plan[&"effective_from"]) + next_start_at = int(plan[&"next_start_at"]) + elif shift != null: + template_id = shift.template_id + effective_from = shift.start_at + else: + template_id = &"NIGHT" if StringName(entry.get(&"preferred_shift", &"day")) == &"night" else &"DAY" + var block_reason := _staff_assignment_block_reason(staff) + rows.append({ + &"staff_id": staff.staff_id, &"display_name": String(entry.get(&"display_name", staff.staff_id)), + &"role_id": staff.role_id, &"state": staff.state, &"state_name": StaffRuntime.State.keys()[staff.state], + &"assigned_facility_id": assigned_facility_id, &"active_facility_id": staff.assigned_facility_id, + &"template_id": template_id, &"recurring": not plan.is_empty() and not assigned_facility_id.is_empty(), + &"effective_from": effective_from, &"next_start_at": next_start_at, + &"daily_wage": staff.get_effective_daily_wage(), &"health": staff.health, &"fatigue": staff.fatigue, &"unpaid_wages": staff.unpaid_wages, + &"current_task_id": staff.current_task_id, &"rescue_status": _staff_rescue_status(staff.staff_id), + &"rest_status": get_staff_rest_report(staff.staff_id).get(&"label", ""), + &"can_edit": block_reason.is_empty(), + &"assignment_block_reason": block_reason, + &"unique_contract": _staff_unique_contract_report(staff), + }) + var facilities: Array[Dictionary] = [] + for facility: FacilityInstance in facility_service.get_instances(): + var definition := definition_registry.get_facility(facility.definition_id) + if definition == null or definition.staff_max <= 0: + continue + facilities.append({ + &"facility_id": facility.instance_id, &"display_name": definition.display_name, + &"definition_id": facility.definition_id, &"layer": facility.origin.z, &"origin": facility.origin, + &"required_roles": definition.required_roles.duplicate(), &"staff_min": definition.staff_min, &"staff_max": definition.staff_max, + }) + return {&"staff": rows, &"facilities": facilities} + + +## Original 6.1 / 6.2 presentation, derived from validated canonical content +## and the same planned placement as pricing. This report owns all containers; +## it does not add a hire, consent, growth policy, save field or tick-side effect. +func _staff_unique_contract_report(staff: StaffRuntime) -> Dictionary: + if staff == null or staff.employment_kind != StaffRuntime.EmploymentKind.UNIQUE or not mvp_content_catalog.is_valid(): + return {} + var definition := mvp_content_catalog.get_entry(&"unique_characters", staff.staff_id) + if definition.is_empty() or StringName(definition.get(&"role_id", &"")) != staff.role_id: + return {} + var passive_effects: Array[Dictionary] = [] + var contract := { + &"canonical_id": staff.staff_id, + &"free_employment": true, + &"free_upkeep": staff.get_effective_daily_wage() == 0, + &"permanent_removal_forbidden": true, + &"passive_effects": passive_effects, + } + # Only Anri's explicitly specified effect is presented here. Neither kind + # heroine nor a zero wage grants this effect to another employee. + if not _is_anri_manager(staff): + return contract + var effects: Dictionary = definition.get(&"mechanical_effects", {}) + if not effects.has(&"assigned_floor_revenue_multiplier_bp"): + return contract + var plan := staff_service.get_work_assignment(staff.staff_id) + var facility_id := StringName(plan.get(&"facility_id", &"")) + var facility := facility_service.get_instance(facility_id) + var effect := { + &"effect_id": &"assigned_floor_revenue_multiplier_bp", + &"display_name": "看板娘の微笑", + &"multiplier_bp": int(effects[&"assigned_floor_revenue_multiplier_bp"]), + &"scope": &"ASSIGNED_FLOOR_ALL_LAYERS", + &"applied": facility != null, + &"assigned_facility_id": facility_id, + } + if facility != null: + effect[&"floor_y"] = facility.origin.y + passive_effects.append(effect) + return contract + + +func get_hired_staff_candidate_ids() -> Array[StringName]: + return _sorted_name_keys(_hired_staff_candidate_ids) + + +func get_live_progression_report() -> Dictionary: + # Project current cash/facility facts without changing saved progression. + var projected := ProgressionService.new() + var source := progression_service.create_snapshot() + if not projected.restore_snapshot(source): + return {&"snapshot":source, &"evaluation":{&"eligible":false,&"requirements":[],&"reason":&"UNAVAILABLE"}} + var synchronized := projected.update_metrics({ + &"asset_gold":economy_service.get_balance(PLAYER_CASH_ACCOUNT), + &"active_above_ground_facilities":_count_active_facilities(facility_service,false), + &"active_underground_facilities":_count_active_facilities(facility_service,true), + }) + var evaluation := projected.evaluate_rank() + if not synchronized: + evaluation[&"eligible"] = false + evaluation[&"reason"] = &"UNAVAILABLE" + var gate := _operations_construction_investment_gate(0) + if gate != null: + evaluation[&"eligible"] = false + evaluation[&"reason"] = &"OPERATIONS_BLOCKED" + evaluation[&"block_reason"] = gate.details.get(&"reason", &"") + return {&"snapshot":projected.create_snapshot(), &"evaluation":evaluation} + + +func get_progression_report(target_rank := -1) -> Dictionary: + return { + &"snapshot": progression_service.create_snapshot(), + &"evaluation": progression_service.evaluate_rank(target_rank).duplicate(true), + } + + +func _saved_content_manifest_matches(catalog: MvpContentCatalog, data: Dictionary) -> bool: + var expected := _create_mvp_content_manifest(catalog) + var saved: Variant = data.get(&"catalog_manifest", null) + if _strict_variants_equal(expected, saved): + return true + # The sole v1-to-v2 catalog addition is the chapel cleric. Require the + # complete old manifest, not an arbitrary subset of current content. + expected[&"manifest_version"] = 1 + expected[&"staff_candidate_ids"].erase("staff_uma") + if not _strict_variants_equal(expected, saved): + return false + var hired: Variant = data.get(&"hired_staff_candidate_ids", null) + var staff_section: Variant = data.get(&"staff", null) + if not hired is Array or "staff_uma" in hired or not staff_section is Dictionary: + return false + var staff_rows: Variant = staff_section.get(&"staff", null) + if not staff_rows is Array: + return false + for staff: Variant in staff_rows: + if not staff is Dictionary or String(staff.get(&"staff_id", "")) == "staff_uma": + return false + return true + + +func _clear_mvp_content_manifest_cache() -> void: + _mvp_manifest_cache_catalog = null + _mvp_manifest_cache_generation = -1 + _mvp_manifest_cache = {} + + +func _create_mvp_content_manifest(catalog: MvpContentCatalog) -> Dictionary: + # Restore validates a temporary catalog through the full original builder. + # A rejected restore must not replace or invalidate the live Runtime cache. + if catalog != mvp_content_catalog: + return _build_mvp_content_manifest(catalog) + var generation := catalog.get_manifest_generation() + if _mvp_manifest_cache_catalog == catalog \ + and _mvp_manifest_cache_generation == generation: + return _mvp_manifest_cache.duplicate(true) + var manifest := _build_mvp_content_manifest(catalog) + _mvp_manifest_cache_catalog = catalog + _mvp_manifest_cache_generation = generation + _mvp_manifest_cache = manifest + # Legacy v1 matching edits its returned manifest, including a nested array. + # The private template must stay owned even on the first cache fill. + return _mvp_manifest_cache.duplicate(true) + + +func _build_mvp_content_manifest(catalog: MvpContentCatalog) -> Dictionary: + var guest_definition_ids: Array[String] = [] + for definition: GuestDefinition in catalog.build_all_guest_definitions(): + guest_definition_ids.append(String(definition.guest_type_id)) + return { + &"manifest_version": MVP_CONTENT_MANIFEST_VERSION, + &"schema_version": MvpContentCatalog.SCHEMA_VERSION, + &"guest_class_ids": _names_to_strings_for_manifest(catalog.get_guest_class_ids()), + &"species_ids": _names_to_strings_for_manifest(catalog.get_species_ids()), + &"staff_candidate_ids": _names_to_strings_for_manifest(catalog.get_staff_candidate_ids()), + &"unique_character_ids": _names_to_strings_for_manifest(catalog.get_unique_character_ids()), + &"guest_definition_ids": guest_definition_ids, + } + + +func _names_to_strings_for_manifest(values: Array[StringName]) -> Array[String]: + var result: Array[String] = [] + for value: StringName in values: + result.append(String(value)) + return result + + +func _catalog_definitions_are_registered(service: GuestService) -> bool: + if not bool(mvp_content_report.get(&"valid", false)): + return false + return _saved_catalog_guest_definitions_match( + service.create_snapshot(), + mvp_content_catalog + ) + + +func _saved_catalog_guest_definitions_match( + guest_snapshot: Dictionary, + catalog: MvpContentCatalog +) -> bool: + var raw_definitions: Variant = guest_snapshot.get(&"definitions", null) + if not raw_definitions is Array: + return false + var saved_by_id: Dictionary = {} + for raw_definition: Variant in raw_definitions: + if not raw_definition is Dictionary: + return false + var raw_id: Variant = raw_definition.get(&"guest_type_id", null) + if typeof(raw_id) not in [TYPE_STRING, TYPE_STRING_NAME]: + return false + var definition_id := StringName(raw_id) + if definition_id.is_empty() or saved_by_id.has(definition_id): + return false + saved_by_id[definition_id] = raw_definition + var expected_definitions := catalog.build_all_guest_definitions() + if expected_definitions.size() != 32: + return false + for definition: GuestDefinition in expected_definitions: + if not saved_by_id.has(definition.guest_type_id) \ + or not _strict_variants_equal( + definition.create_snapshot(), + saved_by_id[definition.guest_type_id] + ): + return false + return true + + +func _register_or_validate_legacy_catalog_definitions( + service: GuestService, + catalog: MvpContentCatalog +) -> bool: + var definitions := catalog.build_all_guest_definitions() + if definitions.size() != 32: + return false + for definition: GuestDefinition in definitions: + var existing := service.get_definition(definition.guest_type_id) + if existing == null: + if not service.register_definition(definition): + return false + elif not _strict_variants_equal( + existing.create_snapshot(), + definition.create_snapshot() + ): + return false + return true + + +func _serialize_hired_staff_candidate_ids() -> Array[String]: + var result: Array[String] = [] + for candidate_id: StringName in _sorted_name_keys(_hired_staff_candidate_ids): + result.append(String(candidate_id)) + return result + + +func _is_mvp_staff_candidate_id(staff_id: StringName) -> bool: + return mvp_content_catalog.get_staff_candidate_ids().has(staff_id) + + +func _deserialize_hired_staff_candidate_ids( + raw_ids: Array, + catalog: MvpContentCatalog +) -> Variant: + var allowed: Dictionary = {} + for candidate_id: StringName in catalog.get_recruitable_staff_ids(): + allowed[candidate_id] = true + var result: Dictionary = {} + var previous := "" + for raw_id: Variant in raw_ids: + if typeof(raw_id) not in [TYPE_STRING, TYPE_STRING_NAME]: + return null + var candidate_id := StringName(raw_id) + var candidate_text := String(candidate_id) + if not allowed.has(candidate_id) or result.has(candidate_id) \ + or (not previous.is_empty() and candidate_text <= previous): + return null + result[candidate_id] = true + previous = candidate_text + return result + + +func _validate_catalog_staff_crosslinks( + staff: StaffService, + hired_ids: Dictionary, + catalog: MvpContentCatalog +) -> bool: + if hired_ids.has(&"heroine_anri") and not staff.has_staff(&"heroine_anri"): + return false + var candidate_ids: Dictionary = {} + for candidate_id: StringName in catalog.get_recruitable_staff_ids(): + candidate_ids[candidate_id] = true + for member: StaffRuntime in staff.get_staff_members(): + if not candidate_ids.has(member.staff_id): + continue + if not hired_ids.has(member.staff_id): + return false + var canonical := catalog.staff_hire_payload(member.staff_id) + if canonical.is_empty() \ + or member.role_id != StringName(canonical[&"role_id"]) \ + or member.daily_wage != int(canonical[&"daily_wage"]) \ + or member.get_employment_kind_name() != StringName(canonical[&"options"][&"employment_kind"]) \ + or not _strict_variants_equal( + member.permitted_task_tags, + (canonical[&"options"] as Dictionary)[&"permitted_task_tags"] + ) \ + or not _strict_variants_equal( + member.skills, + (canonical[&"options"] as Dictionary)[&"skills"] + ): + return false + return true + + +func _derive_legacy_hired_staff_candidate_ids( + staff: StaffService, + staff_snapshot: Dictionary, + catalog: MvpContentCatalog +) -> Dictionary: + var allowed: Dictionary = {} + for candidate_id: StringName in catalog.get_recruitable_staff_ids(): + allowed[candidate_id] = true + var result: Dictionary = {} + for member: StaffRuntime in staff.get_staff_members(): + if allowed.has(member.staff_id): + result[member.staff_id] = true + var raw_departed: Variant = staff_snapshot.get(&"departed_payroll", []) + if raw_departed is Array: + for raw_record: Variant in raw_departed: + if raw_record is Dictionary: + var candidate_id := StringName(raw_record.get(&"staff_id", "")) + if allowed.has(candidate_id): + result[candidate_id] = true + return result + + +func _strict_variants_equal(expected: Variant, actual: Variant) -> bool: + if typeof(expected) != typeof(actual): + return false + if expected is Dictionary: + var expected_dictionary := expected as Dictionary + var actual_dictionary := actual as Dictionary + if expected_dictionary.size() != actual_dictionary.size(): + return false + var unmatched_actual_keys: Array = actual_dictionary.keys() + for expected_key: Variant in expected_dictionary: + var matched_index := -1 + for index in range(unmatched_actual_keys.size()): + var actual_key: Variant = unmatched_actual_keys[index] + if typeof(actual_key) == typeof(expected_key) and actual_key == expected_key: + matched_index = index + break + if matched_index < 0: + return false + var matched_key: Variant = unmatched_actual_keys[matched_index] + if not _strict_variants_equal( + expected_dictionary[expected_key], + actual_dictionary[matched_key] + ): + return false + unmatched_actual_keys.remove_at(matched_index) + return unmatched_actual_keys.is_empty() + if expected is Array: + var expected_array := expected as Array + var actual_array := actual as Array + if expected_array.size() != actual_array.size(): + return false + for index in range(expected_array.size()): + if not _strict_variants_equal(expected_array[index], actual_array[index]): + return false + return true + return expected == actual + + +func _operations_snapshot_comparison_value(value: Variant) -> Variant: + if typeof(value) == TYPE_STRING_NAME: + return String(value) + if value is Array: + var normalized_array: Array = [] + for member: Variant in value: + normalized_array.append(_operations_snapshot_comparison_value(member)) + return normalized_array + if value is Dictionary: + var normalized_dictionary: Dictionary = {} + for raw_key: Variant in value: + var normalized_key: Variant = raw_key + if typeof(raw_key) == TYPE_STRING_NAME: + normalized_key = String(raw_key) + if normalized_dictionary.has(normalized_key): + # A String/StringName alias collision is not a canonical snapshot. + return null + normalized_dictionary[normalized_key] = _operations_snapshot_comparison_value( + value[raw_key] + ) + return normalized_dictionary + return value + + +func get_actor_movement_credit(actor_id: StringName) -> Dictionary: + if not _movement_credits.has(actor_id): + return {} + return (_movement_credits[actor_id] as Dictionary).duplicate(true) + + +func _guest_path_actor_id(guest_id: StringName) -> StringName: + return StringName("guest:%s" % guest_id) + + +func _staff_path_actor_id(staff_id: StringName) -> StringName: + return StringName("staff:%s" % staff_id) + + +func _expected_runtime_actor_cells( + guests: GuestService, + staff: StaffService +) -> Dictionary: + var expected: Dictionary = {} + for guest: GuestRuntime in guests.get_guests(): + var guest_actor_id := _guest_path_actor_id(guest.guest_id) + if guest_actor_id.is_empty() or expected.has(guest_actor_id): + return {} + expected[guest_actor_id] = guest.current_cell + for member: StaffRuntime in staff.get_staff_members(): + var staff_actor_id := _staff_path_actor_id(member.staff_id) + if staff_actor_id.is_empty() or expected.has(staff_actor_id): + return {} + expected[staff_actor_id] = member.current_cell + return expected + + +func _serialize_movement_credits() -> Array[Dictionary]: + var saved: Array[Dictionary] = [] + for actor_id: StringName in _sorted_name_keys(_movement_credits): + var entry: Dictionary = _movement_credits[actor_id] + saved.append({ + &"actor_id": String(actor_id), + &"credit": int(entry[&"credit"]), + &"updated_at": int(entry[&"updated_at"]), + }) + return saved + + +func _deserialize_movement_credits(raw_entries: Array, absolute_minute: int) -> Variant: + var restored: Dictionary = {} + for raw_entry: Variant in raw_entries: + if not raw_entry is Dictionary: + return null + var actor_id_value: Variant = raw_entry.get(&"actor_id", null) + var credit_value: Variant = raw_entry.get(&"credit", null) + var updated_at_value: Variant = raw_entry.get(&"updated_at", null) + if not (actor_id_value is String or actor_id_value is StringName) \ + or not credit_value is int or not updated_at_value is int: + return null + var actor_id := StringName(actor_id_value) + var credit := int(credit_value) + var updated_at := int(updated_at_value) + if actor_id.is_empty() or restored.has(actor_id) \ + or credit < 0 or credit >= STAIR_MOVEMENT_CREDIT \ + or updated_at < 0 or updated_at > absolute_minute: + return null + restored[actor_id] = { + &"credit": credit, + &"updated_at": updated_at, + } + return restored + + +func _rebuild_legacy_path_actor_projection( + path: PathService, + guests: GuestService, + staff: StaffService, + absolute_minute: int +) -> Variant: + var expected := _expected_runtime_actor_cells(guests, staff) + var entries: Array[Dictionary] = [] + var progress_entries: Array[Dictionary] = [] + var expected_density: Dictionary = {} + var rebuilt: Dictionary = {} + for actor_id: StringName in _sorted_name_keys(expected): + var cell: Vector3i = expected[actor_id] + if not path.has_cell(cell): + return null + entries.append({&"actor_id": actor_id, &"cell": cell}) + progress_entries.append({ + &"actor_id": actor_id, + &"last_cell": cell, + &"last_progress_at": absolute_minute, + &"last_observed_at": absolute_minute, + &"stuck_reported_at": -1, + }) + expected_density[cell] = int(expected_density.get(cell, 0)) + 1 + rebuilt[actor_id] = {&"credit": 0, &"updated_at": absolute_minute} + var migrated_path := path.create_snapshot() + migrated_path[&"actor_cells"] = entries + migrated_path[&"actor_progress"] = progress_entries + migrated_path[&"traffic_revision"] = int(migrated_path[&"traffic_revision"]) + 1 + for traffic: Dictionary in migrated_path[&"cell_traffic"]: + traffic[&"current_density"] = int(expected_density.get(traffic[&"cell"], 0)) + if not path.restore_snapshot(migrated_path): + return null + return rebuilt + + +func _validate_runtime_path_crosslinks( + path: PathService, + guests: GuestService, + staff: StaffService, + credits: Dictionary, + absolute_minute: int +) -> bool: + if path == null or guests == null or staff == null or absolute_minute < 0: + return false + var expected := _expected_runtime_actor_cells(guests, staff) + if expected.size() != guests.guest_count() + staff.get_staff_members().size() \ + or credits.size() != expected.size(): + return false + var path_snapshot := path.create_snapshot() + var raw_actor_cells: Variant = path_snapshot.get(&"actor_cells", null) + var raw_progress: Variant = path_snapshot.get(&"actor_progress", null) + var raw_traffic: Variant = path_snapshot.get(&"cell_traffic", null) + if not raw_actor_cells is Array or not raw_progress is Array or not raw_traffic is Array: + return false + var projected: Dictionary = {} + for raw_actor: Variant in raw_actor_cells: + if not raw_actor is Dictionary: + return false + var actor_id := StringName(raw_actor.get(&"actor_id", &"")) + var cell: Variant = raw_actor.get(&"cell", null) + if actor_id.is_empty() or not cell is Vector3i or projected.has(actor_id): + return false + projected[actor_id] = cell + if projected.size() != expected.size(): + return false + var progress_by_actor: Dictionary = {} + for raw_entry: Variant in raw_progress: + if not raw_entry is Dictionary: + return false + var actor_id := StringName(raw_entry.get(&"actor_id", &"")) + if actor_id.is_empty() or progress_by_actor.has(actor_id): + return false + progress_by_actor[actor_id] = raw_entry + if progress_by_actor.size() != expected.size(): + return false + var expected_density: Dictionary = {} + for actor_id: StringName in _sorted_name_keys(expected): + var cell: Vector3i = expected[actor_id] + if projected.get(actor_id, null) != cell or not credits.has(actor_id): + return false + var credit_entry: Variant = credits[actor_id] + var progress_entry: Variant = progress_by_actor.get(actor_id, null) + if not credit_entry is Dictionary or not progress_entry is Dictionary: + return false + var credit_value: Variant = credit_entry.get(&"credit", null) + var credit_updated_value: Variant = credit_entry.get(&"updated_at", null) + var last_progress_value: Variant = progress_entry.get(&"last_progress_at", null) + var last_observed_value: Variant = progress_entry.get(&"last_observed_at", null) + var stuck_reported_value: Variant = progress_entry.get(&"stuck_reported_at", null) + if not credit_value is int or not credit_updated_value is int \ + or not last_progress_value is int or not last_observed_value is int \ + or not stuck_reported_value is int: + return false + var credit := int(credit_value) + var updated_at := int(credit_updated_value) + var last_progress_at := int(last_progress_value) + var last_observed_at := int(last_observed_value) + var stuck_reported_at := int(stuck_reported_value) + if credit < 0 or credit >= _cell_movement_credit_limit(path, cell) \ + or updated_at < 0 or updated_at > absolute_minute \ + or progress_entry.get(&"last_cell", null) != cell \ + or last_progress_at < 0 or last_progress_at > last_observed_at \ + or last_observed_at != updated_at \ + or last_observed_at > absolute_minute \ + or (stuck_reported_at != -1 and ( + stuck_reported_at < last_progress_at or stuck_reported_at > last_observed_at + )): + return false + if not path.is_actor_in_transit(actor_id): + expected_density[cell] = int(expected_density.get(cell, 0)) + 1 + for raw_cell: Variant in raw_traffic: + if not raw_cell is Dictionary: + return false + var cell: Variant = raw_cell.get(&"cell", null) + var density_value: Variant = raw_cell.get(&"current_density", null) + if not cell is Vector3i or not density_value is int \ + or int(density_value) != int(expected_density.get(cell, 0)): + return false + return true + + +func _path_snapshot_cells_match_world( + path_snapshot: Dictionary, + walkable_cells: Array[Vector3i] +) -> bool: + var schema_value: Variant = path_snapshot.get( + &"schema_version", + path_snapshot.get("schema_version", null) + ) + if not schema_value is int or int(schema_value) not in [PathService.ELEVATOR_INDEX_SNAPSHOT_VERSION, PathService.SHAFT_SNAPSHOT_VERSION, PathService.SNAPSHOT_VERSION]: + return false + var raw_traffic: Variant = path_snapshot.get( + &"cell_traffic", + path_snapshot.get("cell_traffic", null) + ) + if not raw_traffic is Array or raw_traffic.size() != walkable_cells.size(): + return false + var expected_cells: Dictionary = {} + for cell: Vector3i in walkable_cells: + if expected_cells.has(cell): + return false + expected_cells[cell] = true + var actual_cells: Dictionary = {} + for raw_entry: Variant in raw_traffic: + if not raw_entry is Dictionary: + return false + var cell_value: Variant = raw_entry.get(&"cell", raw_entry.get("cell", null)) + if not cell_value is Vector3i \ + or actual_cells.has(cell_value) \ + or not expected_cells.has(cell_value): + return false + actual_cells[cell_value] = true + return actual_cells.size() == expected_cells.size() + + +func _serialize_logistics_requests() -> Array[Dictionary]: + var saved: Array[Dictionary] = [] + for request_id: StringName in _sorted_name_keys(logistics_requests): + saved.append((logistics_requests[request_id] as LogisticsRequest).create_snapshot()) + return saved + + +func _derive_legacy_logistics_ownership( + dispatcher: LogisticsDispatcher, + requests: Dictionary, + absolute_minute: int +) -> Dictionary: + var latest_history: Dictionary = {} + for assignment: Dictionary in dispatcher.get_assignment_history(): + var request_id := StringName(assignment.get(&"request_id", &"")) + var assigned_at := int(assignment.get(&"assigned_at", -1)) + if request_id.is_empty() or assigned_at < 0 or assigned_at > absolute_minute: + return {} + latest_history[request_id] = assignment + var expected: Dictionary = {} + for request_id: StringName in _sorted_name_keys(requests): + var request: LogisticsRequest = requests[request_id] + if request.state not in [ + LogisticsRequest.State.ASSIGNED, + LogisticsRequest.State.PICKING_UP, + LogisticsRequest.State.DELIVERING, + ]: + continue + if request.assigned_carrier_id.is_empty() or request.total_weight <= 0 \ + or not latest_history.has(request_id): + return {} + var latest: Dictionary = latest_history[request_id] + if StringName(latest.get(&"carrier_id", &"")) != request.assigned_carrier_id \ + or int(latest.get(&"weight", -1)) != request.total_weight: + return {} + expected[request_id] = { + &"carrier_id": request.assigned_carrier_id, + &"weight": request.total_weight, + &"assigned_at": int(latest[&"assigned_at"]), + } + return expected + + +func _validate_logistics_dispatcher_crosslinks( + dispatcher: LogisticsDispatcher, + requests: Dictionary, + absolute_minute: int +) -> bool: + if dispatcher == null or absolute_minute < 0: + return false + var ownership := dispatcher.get_assignment_ownership() + var expected_ids: Dictionary = {} + for request_id: StringName in _sorted_name_keys(requests): + var request: LogisticsRequest = requests[request_id] + if request.created_at < 0 or request.state_changed_at < request.created_at \ + or request.state_changed_at > absolute_minute: + return false + var active := request.state in [ + LogisticsRequest.State.ASSIGNED, + LogisticsRequest.State.PICKING_UP, + LogisticsRequest.State.DELIVERING, + ] + if not active: + if not request.assigned_carrier_id.is_empty(): + return false + continue + if request.assigned_carrier_id.is_empty() or request.total_weight <= 0: + return false + expected_ids[request_id] = true + var entry: Variant = ownership.get(request_id, null) + if not entry is Dictionary \ + or StringName(entry.get(&"carrier_id", &"")) != request.assigned_carrier_id \ + or int(entry.get(&"weight", -1)) != request.total_weight: + return false + var assigned_at := int(entry.get(&"assigned_at", -1)) + if assigned_at < request.created_at or assigned_at > request.state_changed_at: + return false + if ownership.size() != expected_ids.size(): + return false + for request_id: StringName in _sorted_name_keys(ownership): + if not expected_ids.has(request_id): + return false + var latest_history: Dictionary = {} + var history_fingerprints: Dictionary = {} + for assignment: Dictionary in dispatcher.get_assignment_history(): + var request_id := StringName(assignment.get(&"request_id", &"")) + if request_id.is_empty() or not requests.has(request_id): + return false + var request: LogisticsRequest = requests[request_id] + var carrier_id := StringName(assignment.get(&"carrier_id", &"")) + var weight := int(assignment.get(&"weight", -1)) + var assigned_at := int(assignment.get(&"assigned_at", -1)) + var fingerprint := "%s|%s|%d|%d" % [request_id, carrier_id, weight, assigned_at] + if carrier_id.is_empty() or weight != request.total_weight \ + or assigned_at < request.created_at or assigned_at > absolute_minute \ + or history_fingerprints.has(fingerprint): + return false + history_fingerprints[fingerprint] = true + latest_history[request_id] = assignment + for request_id: StringName in _sorted_name_keys(ownership): + if not latest_history.has(request_id): + return false + var entry: Dictionary = ownership[request_id] + var latest: Dictionary = latest_history[request_id] + if StringName(latest[&"carrier_id"]) != StringName(entry[&"carrier_id"]) \ + or int(latest[&"weight"]) != int(entry[&"weight"]) \ + or int(latest[&"assigned_at"]) != int(entry[&"assigned_at"]): + return false + return true + +func _serialize_event_definitions() -> Array[Dictionary]: + var saved: Array[Dictionary] = [] + for definition: EventDefinition in event_service.get_definitions(): + saved.append(definition.create_snapshot()) + return saved + +func _deserialize_logistics_requests(raw_requests: Array) -> Variant: + var restored: Dictionary = {} + for raw_request: Variant in raw_requests: + if not raw_request is Dictionary: + return null + var request := LogisticsRequest.from_snapshot(raw_request) + if request == null or restored.has(request.request_id): + return null + restored[request.request_id] = request + return restored + +func _sorted_logistics_requests() -> Array: + var requests: Array = [] + for request_id: StringName in _sorted_name_keys(logistics_requests): + requests.append(logistics_requests[request_id]) + return requests + +func _sorted_name_keys(source: Dictionary) -> Array[StringName]: + var keys: Array[StringName] = [] + for raw_key: Variant in source: + keys.append(StringName(raw_key)) + keys.sort_custom(func(first: StringName, second: StringName) -> bool: + return String(first) < String(second) + ) + return keys + +func _name_array(source: Variant) -> Array[StringName]: + var result: Array[StringName] = [] + if not source is Array: + return result + for value: Variant in source: + result.append(StringName(value)) + return result + +func _guest_wallet_account(guest_id: StringName) -> StringName: + return StringName("GUEST_WALLET:%s" % guest_id) + +func _outstanding_service_commitment(guest_id: StringName) -> int: + var total := 0 + var counted_requests: Dictionary = {} + for facility: FacilityInstance in facility_service.get_instances(): + for request: ServiceRequest in facility_service.get_queue(facility.instance_id): + if not request.guest_ids.is_empty() and request.guest_ids[0] == guest_id: + total += request.quoted_price + counted_requests[request.request_id] = true + for service: ServiceInstance in facility_service.get_active_services(facility.instance_id): + if counted_requests.has(service.request_id): + continue + var request := facility_service.get_request(service.request_id) + if request != null and not request.guest_ids.is_empty() \ + and request.guest_ids[0] == guest_id: + total += request.quoted_price + counted_requests[request.request_id] = true + return total + +func _facility_revenue_account(facility_id: StringName) -> StringName: + return StringName("FACILITY_REVENUE:%s" % facility_id) + +func _command_result_from_dictionary( + command_id: int, + result: Dictionary, + fallback_message: String +) -> CommandResult: + if bool(result.get(&"ok", result.get("ok", false))): + return CommandResult.succeeded(command_id, result.duplicate(true)) + return CommandResult.failed( + CommandResult.Code.VALIDATION_FAILED, + command_id, + String(result.get(&"message", result.get("message", fallback_message))), + result.duplicate(true) + ) + +func _register_facility_mana_consumer(instance: Dictionary) -> void: + var instance_id := StringName(instance.get(&"instance_id", &"")) + var definition_id := StringName(instance.get(&"definition_id", &"")) + var definition := definition_registry.get_facility(definition_id) + if instance_id.is_empty() or definition == null or definition.maintenance_mana <= 0: + return + var priority := ManaNetworkService.ConsumerPriority.NORMAL + if &"entrance" in definition.tags or &"medical" in definition.tags: + priority = ManaNetworkService.ConsumerPriority.ESSENTIAL + if mana_network_service.register_consumer(instance_id, definition.maintenance_mana, priority): + mana_network_service.connect_nodes(instance_id, STARTER_MANA_BUS) + +func _register_operational_facility(construction_instance: Dictionary) -> void: + var instance_id := StringName(construction_instance.get(&"instance_id", &"")) + var definition_id := StringName(construction_instance.get(&"definition_id", &"")) + var definition := definition_registry.get_facility(definition_id) + if instance_id.is_empty() or definition == null or facility_service.has_instance(instance_id): + return + economy_service.register_account(_facility_revenue_account(instance_id)) + facility_service.create_instance({ + &"instance_id": instance_id, + &"definition_id": definition_id, + &"origin": construction_instance.get(&"origin", Vector3i.ZERO), + &"rotation": construction_instance.get(&"rotation", 0), + &"occupied_cells": construction_instance.get(&"occupied_cells", []), + &"local_inventory_id": StringName("facility.inventory:%s" % instance_id), + &"power_available": int(_state.get_resource(&"mana")) > 0, + &"customer_capacity": definition.customer_capacity, + &"service_attribute": definition.service_attribute, + &"capacity": maxi(definition.capacity, 1), + &"queue_capacity": definition.queue_capacity, + &"base_service_minutes": definition.base_service_minutes, + &"base_quality": definition.base_quality, + &"base_price": definition.base_price, + &"staff_min": definition.staff_min, + &"staff_max": definition.staff_max, + &"created_at": int(construction_instance.get(&"completed_at", _clock.absolute_minute)), + }) + +func _validate_service_start(request: ServiceRequest, _facility: FacilityInstance, absolute_minute: int) -> Dictionary: + if int(_state.get_resource(&"mana")) == 0: + return {&"ok": false, &"message": "The tower has no stored Mana."} + for guest_id: StringName in request.guest_ids: + var guest := guest_service.get_guest(guest_id) + if guest == null: + continue + if guest.state != GuestRuntime.State.QUEUING \ + or guest.queue_request_id != request.request_id \ + or guest.target_facility_id != request.facility_id: + return {&"ok": false, &"message": "Guest and Facility queue ownership do not match."} + if guest.transition_tick == absolute_minute \ + and guest.transitions_this_tick >= GuestService.MAX_TRANSITIONS_PER_TICK: + return {&"ok": false, &"message": "Guest transition budget is exhausted for this minute."} + for reservation_id: StringName in request.reserved_inventory_ids: + var reservation := inventory_service.get_reservation(reservation_id) + if reservation == null or not reservation.is_active() or reservation.is_expired(absolute_minute) \ + or inventory_service.is_stack_expired(reservation.stack_id, absolute_minute): + return {&"ok": false, &"message": "A required inventory reservation is unavailable or expired."} + var payer_account := _guest_wallet_account(request.guest_ids[0]) + if not economy_service.has_account(payer_account) \ + or economy_service.get_balance(payer_account) < request.quoted_price: + return {&"ok": false, &"message": "The guest cannot pay the quoted service price."} + return {&"ok": true} + +func _complete_service_transaction( + service: ServiceInstance, + request: ServiceRequest, + facility: FacilityInstance, + absolute_minute: int +) -> Variant: + # A sale is only observable after its inventory, rewards and cleanliness + # effects commit. Rollback removes provisional receipts before scope exit. + economy_service.begin_notification_scope() + var result: Variant = _apply_service_transaction(service, request, facility, absolute_minute) + if result is bool and result: + _state.set_resource(&"gold", economy_service.get_balance(PLAYER_CASH_ACCOUNT)) + economy_service.end_notification_scope() + return result + + +func _apply_service_transaction( + service: ServiceInstance, + request: ServiceRequest, + facility: FacilityInstance, + absolute_minute: int +) -> Variant: + for guest_id: StringName in service.guest_ids: + var guest := guest_service.get_guest(guest_id) + if guest == null: + continue + if guest.state != GuestRuntime.State.RECEIVING_SERVICE \ + or guest.current_service_id != service.service_id \ + or guest.target_facility_id != facility.instance_id: + return CommandResult.failed( + CommandResult.Code.TRANSACTION_APPLY_FAILED, 0, + "Guest and Facility service ownership do not match." + ) + if guest.transition_tick == absolute_minute \ + and guest.transitions_this_tick >= GuestService.MAX_TRANSITIONS_PER_TICK: + return CommandResult.failed( + CommandResult.Code.TRANSACTION_APPLY_FAILED, 0, + "Guest transition budget is exhausted for this minute." + ) + var inventory_before := inventory_service.create_snapshot() + var economy_before := economy_service.create_snapshot() + var state_before: Dictionary = _state.create_snapshot() + if not service.inventory_reservations.is_empty(): + var consumed := inventory_service.consume_reservations( + service.inventory_reservations, + StringName("service.consume.%s" % service.service_id), + absolute_minute + ) + if not bool(consumed.get(&"ok", false)): + return CommandResult.failed(CommandResult.Code.TRANSACTION_APPLY_FAILED, 0, "Reserved service inventory could not be consumed.", consumed) + + if service.price_snapshot > 0: + var payer_account := _guest_wallet_account(request.guest_ids[0]) + var revenue_account := _facility_revenue_account(facility.instance_id) + if not economy_service.has_account(payer_account) or not economy_service.has_account(revenue_account): + inventory_service.restore_snapshot(inventory_before) + return CommandResult.failed(CommandResult.Code.NOT_FOUND, 0, "A service settlement account is missing.") + var sale_entries: Array = [ + LedgerEntry.new().configure( + &"", absolute_minute, revenue_account, payer_account, + service.price_snapshot, &"SERVICE_SALE", facility.instance_id + ), + LedgerEntry.new().configure( + &"", absolute_minute, PLAYER_CASH_ACCOUNT, revenue_account, + service.price_snapshot, &"SERVICE_CASH_SWEEP", facility.instance_id + ), + ] + var payment := economy_service.post( + sale_entries, + StringName("service.payment.%s" % service.service_id) + ) + if not payment.is_success(): + inventory_service.restore_snapshot(inventory_before) + economy_service.restore_snapshot(economy_before) + _state.restore_snapshot(state_before) + return payment + + var definition := definition_registry.get_facility(facility.definition_id) + if definition != null: + for item_id: StringName in _sorted_name_keys(definition.inventory_outputs): + var output := inventory_service.add_stack( + facility.local_inventory_id, + item_id, + int(definition.inventory_outputs[item_id]), + service.quality_snapshot, + -1, + absolute_minute + ) + if not bool(output.get(&"ok", false)): + inventory_service.restore_snapshot(inventory_before) + economy_service.restore_snapshot(economy_before) + _state.restore_snapshot(state_before) + return CommandResult.failed(CommandResult.Code.TRANSACTION_APPLY_FAILED, 0, "Facility outputs could not be committed.", output) + var progression_before: Dictionary = {} + if definition != null and definition.faith_per_service > 0: + progression_before = progression_service.create_snapshot() + var next_faith := int(progression_service.metrics[&"faith_lifetime"]) + definition.faith_per_service + if not progression_service.update_metrics({&"faith_lifetime":next_faith}) or not _state.apply_resource_deltas({&"faith":definition.faith_per_service}): + inventory_service.restore_snapshot(inventory_before) + economy_service.restore_snapshot(economy_before) + _state.restore_snapshot(state_before) + progression_service.restore_snapshot(progression_before) + return CommandResult.failed(CommandResult.Code.TRANSACTION_APPLY_FAILED, 0, "Service Faith reward could not be committed.") + if definition != null and definition.cleanliness_decay_per_service > 0: + if not facility_cleanliness.record_use(facility.instance_id, service.service_id, definition.cleanliness_decay_per_service): + inventory_service.restore_snapshot(inventory_before) + economy_service.restore_snapshot(economy_before) + _state.restore_snapshot(state_before) + if not progression_before.is_empty(): progression_service.restore_snapshot(progression_before) + return CommandResult.failed(CommandResult.Code.TRANSACTION_APPLY_FAILED, 0, "Facility cleanliness could not be committed.") + return true + +func _on_facility_state_changed(instance_id: StringName, old_state: int, new_state: int, reasons: Array[StringName]) -> void: + _publish_domain_event(SimulationDomainEvent.new( + id_generator.next_id(&"domain_event"), &"FACILITY_STATE_CHANGED", _runtime_event_minute(), + &"facility_service", [instance_id], + {&"old_state": old_state, &"state": new_state, &"reasons": reasons}, 0 + )) + +func _on_service_started(service: ServiceInstance) -> void: + var effective_minute := _runtime_event_minute() + for guest_id: StringName in service.guest_ids: + var guest := guest_service.get_guest(guest_id) + if guest == null: + continue + var wallet_account := _guest_wallet_account(guest_id) + if economy_service.has_account(wallet_account): + guest.wallet_gold = economy_service.get_balance(wallet_account) + var synchronized := guest_service.begin_service( + guest_id, service.facility_id, service.service_id, effective_minute + ) + if not bool(synchronized.get(&"ok", false)): + _publish_actor_event( + &"GUEST_SERVICE_SYNC_FAILED", &"simulation_runtime", + [guest_id, service.service_id], synchronized + ) + _publish_domain_event(SimulationDomainEvent.new( + id_generator.next_id(&"domain_event"), &"SERVICE_STARTED", effective_minute, + &"facility_service", [service.facility_id, service.service_id], service.create_snapshot(), 0 + )) + +func _on_service_interrupted(service: ServiceInstance, reason: StringName) -> void: + _publish_domain_event(SimulationDomainEvent.new( + id_generator.next_id(&"domain_event"), &"SERVICE_INTERRUPTED", _runtime_event_minute(), + &"facility_service", [service.facility_id, service.service_id], + {&"reason": reason, &"service": service.create_snapshot()}, 0 + )) + +func _on_service_completed(service: ServiceInstance) -> void: + var effective_minute := _runtime_event_minute() + for guest_id: StringName in service.guest_ids: + var guest := guest_service.get_guest(guest_id) + if guest == null: + continue + var wallet_account := _guest_wallet_account(guest_id) + if economy_service.has_account(wallet_account): + guest.wallet_gold = economy_service.get_balance(wallet_account) + var synchronized := guest_service.complete_service( + guest_id, + service.service_id, + effective_minute, + _service_satisfaction_sample(service, guest), + _service_need_effects(service.facility_id) + ) + if not bool(synchronized.get(&"ok", false)): + _publish_actor_event( + &"GUEST_SERVICE_SYNC_FAILED", &"simulation_runtime", + [guest_id, service.service_id], synchronized + ) + _publish_domain_event(SimulationDomainEvent.new( + id_generator.next_id(&"domain_event"), &"SERVICE_COMPLETED", effective_minute, + &"facility_service", [service.facility_id, service.service_id], service.create_snapshot(), 0 + )) + +func _on_service_failed(service: ServiceInstance, reason: StringName) -> void: + var effective_minute := _runtime_event_minute() + for reservation_id: StringName in service.inventory_reservations: + inventory_service.release_reservation( + reservation_id, + StringName("service.failed.%s.%s" % [service.service_id, reservation_id]), + effective_minute + ) + for guest_id: StringName in service.guest_ids: + var guest := guest_service.get_guest(guest_id) + if guest != null and guest.current_service_id == service.service_id: + guest_service.interrupt_service( + guest_id, service.service_id, effective_minute + ) + _publish_domain_event(SimulationDomainEvent.new( + id_generator.next_id(&"domain_event"), &"SERVICE_FAILED", effective_minute, + &"facility_service", [service.facility_id, service.service_id], + {&"reason": reason, &"service": service.create_snapshot()}, 0 + )) + +func _on_guest_spawned(guest: GuestRuntime) -> void: + if _operations_flushing_guest_spawn_notifications.has(guest.guest_id): + return + var actor_id := _guest_path_actor_id(guest.guest_id) + var projected := path_service.actor_enter_cell( + actor_id, + guest.current_cell, + _runtime_event_minute() + ) + if projected: + _movement_credits[actor_id] = { + &"credit": 0, + &"updated_at": _runtime_event_minute(), + } + var payload := guest.create_snapshot() + payload[&"path_actor_id"] = actor_id + payload[&"path_projected"] = projected + payload[&"path_error"] = path_service.last_error if not projected else "" + _publish_actor_event(&"GUEST_SPAWNED", &"guest_service", [guest.guest_id], payload) + +func _on_guest_state_changed( + guest_id: StringName, + old_state: int, + new_state: int, + reason: StringName +) -> void: + _publish_actor_event(&"GUEST_STATE_CHANGED", &"guest_service", [guest_id], { + &"old_state": old_state, + &"state": new_state, + &"reason": reason, + }) + +func _on_guest_queue_abandoned( + guest_id: StringName, + facility_id: StringName, + request_id: StringName +) -> void: + _publish_actor_event( + &"GUEST_QUEUE_ABANDONED", &"guest_service", + [guest_id, facility_id, request_id], {} + ) + +func _on_guest_recovery_started( + guest_id: StringName, + recovery_kind: StringName, + target_cell: Vector3i +) -> void: + _publish_actor_event(&"GUEST_RECOVERY_STARTED", &"guest_service", [guest_id], { + &"recovery_kind": recovery_kind, + &"target_cell": target_cell, + }) + +func _on_guest_despawned(guest_id: StringName, persistent_record: Dictionary) -> void: + var actor_id := _guest_path_actor_id(guest_id) + elevators.cancel_actor(actor_id) + var path_removed := path_service.despawn_actor(actor_id) + _movement_credits.erase(actor_id) + _synchronize_operations_population() + var payload := persistent_record.duplicate(true) + payload[&"path_actor_id"] = actor_id + payload[&"path_projection_removed"] = path_removed + _publish_actor_event(&"GUEST_DESPAWNED", &"guest_service", [guest_id], payload) + +func _on_staff_registered(staff: StaffRuntime) -> void: + _staffing_projection_enabled = true + var actor_id := _staff_path_actor_id(staff.staff_id) + var projected := path_service.actor_enter_cell( + actor_id, + staff.current_cell, + _runtime_event_minute() + ) + if projected: + _movement_credits[actor_id] = { + &"credit": 0, + &"updated_at": _runtime_event_minute(), + } + var payload := staff.create_snapshot() + payload[&"path_actor_id"] = actor_id + payload[&"path_projected"] = projected + payload[&"path_error"] = path_service.last_error if not projected else "" + _publish_actor_event(&"STAFF_REGISTERED", &"staff_service", [staff.staff_id], payload) + +func _on_staff_removed(staff_id: StringName, reason: StringName) -> void: + var actor_id := _staff_path_actor_id(staff_id) + elevators.cancel_actor(actor_id) + var path_removed := path_service.despawn_actor(actor_id) + _movement_credits.erase(actor_id) + _publish_actor_event(&"STAFF_REMOVED", &"staff_service", [staff_id], { + &"reason": reason, + &"path_actor_id": actor_id, + &"path_projection_removed": path_removed, + }) + +func _on_shift_registered(shift: ShiftInstance) -> void: + _publish_actor_event( + &"STAFF_SHIFT_REGISTERED", &"staff_service", + [shift.staff_id, shift.shift_instance_id], shift.create_snapshot() + ) + +func _on_shift_state_changed(staff_id: StringName, shift_id: StringName, state: int) -> void: + _publish_actor_event( + &"STAFF_SHIFT_STATE_CHANGED", &"staff_service", + [staff_id, shift_id], {&"state": state} + ) + +func _on_staff_task_registered(task: StaffTask) -> void: + _publish_actor_event(&"STAFF_TASK_REGISTERED", &"staff_service", [task.task_id], task.create_snapshot()) + +func _on_staff_task_assigned(task_id: StringName, staff_id: StringName) -> void: + var task := staff_service.get_task(task_id) + if task != null and task.state == StaffTask.State.RESERVED: + task.begin_moving(_runtime_event_minute()) + _publish_actor_event(&"STAFF_TASK_ASSIGNED", &"staff_service", [task_id, staff_id], {}) + +func _on_staff_task_reopened(task_id: StringName, reason: StringName) -> void: + _publish_actor_event(&"STAFF_TASK_REOPENED", &"staff_service", [task_id], {&"reason": reason}) + +func _on_staff_task_completed(task_id: StringName, staff_id: StringName) -> void: + var task := staff_service.get_task(task_id) + if task != null and task.task_type == CleaningBridge.TASK_TYPE: + if not cleaning_bridge.completed(task, staff_service.get_staff(staff_id)): + _latch_infrastructure_failure(&"CLEANING_COMPLETION_FAILED", _runtime_event_minute(), "Cleaning task effect could not be committed.") + if task != null and task.task_type == &"rescue_transport": + var patient := staff_service.get_staff(task.target_entity_id) + var clinic := facility_service.get_instance(task.rescue_facility_id) + if patient != null and clinic != null and patient.current_cell == task.target_cell and clinic.operation_state == FacilityInstance.OperationState.OPERATING: + patient.health = 100 + patient.fatigue = 0 + patient.transition_to(StaffRuntime.State.OFF_DUTY, &"RESCUE_TREATED", _runtime_event_minute()) + _publish_actor_event(&"STAFF_RESCUED", &"staff_service", [patient.staff_id, staff_id, clinic.instance_id], {}) + _publish_actor_event(&"STAFF_TASK_COMPLETED", &"staff_service", [task_id, staff_id], {}) + +func _on_authority_heat_changed( + old_value: int, + new_value: int, + source_id: StringName +) -> void: + var entity_ids: Array[StringName] = [] + if not source_id.is_empty(): + entity_ids.append(source_id) + _publish_actor_event(&"AUTHORITY_HEAT_CHANGED", &"authority_service", entity_ids, { + &"old_value": old_value, + &"new_value": new_value, + &"source_id": source_id, + }) + +func _on_authority_incident_created(incident: AuthorityIncident) -> void: + _publish_actor_event( + &"INCIDENT_CREATED", + &"authority_service", + [incident.incident_id], + incident.create_snapshot() + ) + +func _on_authority_incident_detector_added( + incident_id: StringName, + detector_id: StringName +) -> void: + var incident := authority_service.get_incident(incident_id) + _publish_actor_event( + &"INCIDENT_DETECTOR_ADDED", + &"authority_service", + [incident_id, detector_id], + {&"incident": incident.create_snapshot() if incident != null else {}} + ) + +func _on_authority_evidence_registered(evidence: AuthorityEvidence) -> void: + _publish_actor_event( + &"AUTHORITY_EVIDENCE_REGISTERED", + &"authority_service", + [evidence.evidence_id, evidence.incident_id, evidence.facility_id], + evidence.create_snapshot() + ) + +func _on_authority_evidence_discovered( + evidence: AuthorityEvidence, + method: StringName +) -> void: + _publish_actor_event( + &"AUTHORITY_EVIDENCE_DISCOVERED", + &"authority_service", + [evidence.evidence_id, evidence.incident_id, evidence.facility_id], + { + &"method": method, + &"evidence": evidence.create_snapshot(), + &"authority_evidence_points": authority_service.get_authority_state().evidence_points, + } + ) + +func _on_authority_inspection_scheduled(inspection: AuthorityInspection) -> void: + _publish_actor_event( + &"INSPECTION_SCHEDULED", + &"authority_service", + [inspection.inspection_id, inspection.inspector_id], + inspection.create_snapshot() + ) + +func _on_authority_inspection_state_changed( + inspection_id: StringName, + old_state: int, + new_state: int, + reason: StringName +) -> void: + var inspection := authority_service.get_inspection(inspection_id) + var entity_ids: Array[StringName] = [inspection_id] + if inspection != null: + entity_ids.append(inspection.inspector_id) + var payload := { + &"old_state": StringName(AuthorityInspection.State.keys()[old_state]), + &"new_state": StringName(AuthorityInspection.State.keys()[new_state]), + &"reason": reason, + } + _publish_actor_event( + &"INSPECTION_STATE_CHANGED", + &"authority_service", + entity_ids, + payload + ) + if new_state == AuthorityInspection.State.ARRIVING: + _publish_actor_event( + &"INSPECTION_STARTED", + &"authority_service", + entity_ids, + payload + ) + +func _on_authority_resolution_applied( + inspection_id: StringName, + resolution: Dictionary +) -> void: + _publish_actor_event( + &"INSPECTION_RESOLVED", + &"authority_service", + [inspection_id, StringName(resolution.get(&"resolution_id", ""))], + resolution.duplicate(true) + ) + +func _on_defense_raid_created(raid_id: StringName) -> void: + _publish_actor_event( + &"DEFENSE_RAID_CREATED", + &"defense_service", + [raid_id], + defense_service.get_raid(raid_id) + ) + +func _on_defense_raid_state_changed( + raid_id: StringName, + old_state: int, + new_state: int, + reason: StringName +) -> void: + _publish_actor_event( + &"DEFENSE_RAID_STATE_CHANGED", + &"defense_service", + [raid_id], + { + &"old_state": StringName(DefenseService.RaidState.keys()[old_state]), + &"new_state": StringName(DefenseService.RaidState.keys()[new_state]), + &"reason": reason, + } + ) + +func _on_defense_active_skill_applied( + raid_id: StringName, + action_id: StringName, + round_number: int +) -> void: + _publish_actor_event( + &"DEFENSE_SKILL_APPLIED", + &"defense_service", + [raid_id, action_id], + {&"round_number": round_number} + ) + +func _on_defense_round_resolved(raid_id: StringName, resolution: Dictionary) -> void: + var entity_ids: Array[StringName] = [raid_id] + var resolution_id := StringName(resolution.get(&"resolution_id", "")) + var wave_id := StringName(resolution.get(&"wave_id", "")) + if not resolution_id.is_empty(): + entity_ids.append(resolution_id) + if not wave_id.is_empty(): + entity_ids.append(wave_id) + _publish_actor_event( + &"DEFENSE_ROUND_RESOLVED", + &"defense_service", + entity_ids, + resolution.duplicate(true) + ) + +func _on_defense_raid_resolved(raid_id: StringName, outcome: Dictionary) -> void: + var entity_ids: Array[StringName] = [raid_id] + var resolution_id := StringName(outcome.get(&"resolution_id", "")) + if not resolution_id.is_empty(): + entity_ids.append(resolution_id) + _publish_actor_event( + &"DEFENSE_RAID_RESOLVED", + &"defense_service", + entity_ids, + outcome.duplicate(true) + ) + +func _publish_authority_evaluation(details: Dictionary) -> void: + var entity_ids: Array[StringName] = [] + var inspection_id := StringName(details.get(&"inspection_id", "")) + if not inspection_id.is_empty(): + entity_ids.append(inspection_id) + _publish_actor_event( + &"AUTHORITY_INSPECTION_EVALUATED", + &"authority_service", + entity_ids, + details.duplicate(true) + ) + +func _on_relationship_registered(state: RelationshipState) -> void: + _publish_actor_event( + &"RELATIONSHIP_REGISTERED", + &"relationship_service", + [state.actor_a_id, state.actor_b_id, state.relationship_id], + state.create_snapshot() + ) + +func _on_relationship_value_changed( + relationship_id: StringName, + metric_id: StringName, + old_value: int, + new_value: int +) -> void: + var state := relationship_service.get_relationship(relationship_id) + var actor_ids: Array[StringName] = [relationship_id] + if state != null: + actor_ids = [state.actor_a_id, state.actor_b_id, relationship_id] + _publish_actor_event(&"RELATIONSHIP_VALUE_CHANGED", &"relationship_service", actor_ids, { + &"metric_id": metric_id, + &"old_value": old_value, + &"new_value": new_value, + }) + +func _on_relationship_shield_changed( + relationship_id: StringName, + old_value: int, + new_value: int +) -> void: + var state := relationship_service.get_relationship(relationship_id) + var actor_ids: Array[StringName] = [relationship_id] + if state != null: + actor_ids = [state.actor_a_id, state.actor_b_id, relationship_id] + _publish_actor_event(&"RELATIONSHIP_SHIELD_CHANGED", &"relationship_service", actor_ids, { + &"old_value": old_value, + &"new_value": new_value, + }) + +func _on_relationship_route_lock_changed( + relationship_id: StringName, + old_lock: int, + new_lock: int, + route_id: StringName +) -> void: + var state := relationship_service.get_relationship(relationship_id) + var actor_ids: Array[StringName] = [relationship_id] + if state != null: + actor_ids = [state.actor_a_id, state.actor_b_id, relationship_id] + _publish_actor_event(&"RELATIONSHIP_ROUTE_LOCK_CHANGED", &"relationship_service", actor_ids, { + &"old_lock": old_lock, + &"new_lock": new_lock, + &"route_id": route_id, + }) + +func _on_event_effect_transaction_started() -> void: + economy_service.begin_notification_scope() + + +func _on_event_effect_transaction_finished() -> void: + # A nested action batch ends before the enclosing choice cost commits. + # Keep the authoritative mirror current without releasing observer receipts. + if economy_service.notification_scope_depth() == 1: + _state.set_resource(&"gold", economy_service.get_balance(PLAYER_CASH_ACCOUNT)) + economy_service.end_notification_scope() + + +func _on_event_enqueued(instance: EventInstance) -> void: + _publish_actor_event( + &"EVENT_ENQUEUED", &"event_service", instance.actor_ids, + instance.create_snapshot() + ) + +func _on_event_started(instance: EventInstance) -> void: + _publish_actor_event( + &"EVENT_STARTED", &"event_service", instance.actor_ids, + instance.create_snapshot() + ) + +func _on_event_finished(instance: EventInstance) -> void: + _resolve_relationship_threshold_event(instance) + _publish_actor_event( + &"EVENT_FINISHED", &"event_service", instance.actor_ids, + instance.create_snapshot() + ) + +func _on_event_cancelled(instance: EventInstance, reason: StringName) -> void: + _consume_relationship_candidate_from_event(instance) + # Placement rejection occurs before an Event starts and never owns a pause. + if reason != &"PLACEMENT_PROTECTION" and event_service.get_foreground_instance() == null and not event_service.pause_requested: + _clock.set_paused(false) + _publish_actor_event(&"EVENT_CANCELLED", &"event_service", instance.actor_ids, { + &"reason": reason, + &"event": instance.create_snapshot(), + }) + +func _on_event_choice_resolved( + instance_id: StringName, + choice_id: StringName, + result: Dictionary +) -> void: + var instance := event_service.get_instance(instance_id) + var actor_ids: Array[StringName] = [] + if instance != null: + actor_ids = instance.actor_ids.duplicate() + _publish_actor_event(&"EVENT_CHOICE_RESOLVED", &"event_service", actor_ids, { + &"event_instance_id": instance_id, + &"choice_id": choice_id, + &"result": result.duplicate(true), + }) + +func _resolve_relationship_threshold_event(instance: EventInstance) -> void: + if instance.source_event_id != &"relationship.threshold": + return + var raw_candidate: Variant = instance.context.get(&"relationship_candidate", null) + if not raw_candidate is Dictionary: + return + var candidate: Dictionary = raw_candidate + if instance.selected_choice_id == &"acknowledge": + var relationship_id := StringName(candidate.get(&"relationship_id", "")) + var metric_id := StringName(candidate.get(&"metric_id", "")) + var state := relationship_service.get_relationship(relationship_id) + if state != null and RelationshipState.METRIC_IDS.has(metric_id): + var threshold := int(candidate.get(&"threshold", state.get_metric(metric_id))) + var delta := maxi(threshold - state.get_metric(metric_id), 0) + if delta > 0: + relationship_service.apply_value_change( + relationship_id, + metric_id, + delta, + _runtime_event_minute(), + {&"important": true, &"allow_threshold_crossing": true} + ) + _consume_relationship_candidate_from_event(instance) + +func _consume_relationship_candidate_from_event(instance: EventInstance) -> void: + var raw_candidate: Variant = instance.context.get(&"relationship_candidate", null) + if not raw_candidate is Dictionary: + return + var candidate_id := StringName(raw_candidate.get(&"candidate_id", "")) + if not candidate_id.is_empty(): + relationship_service.consume_threshold_candidate(candidate_id) + +func _publish_actor_event( + event_type: StringName, + source_system: StringName, + entity_ids: Array[StringName], + payload: Dictionary +) -> void: + _publish_domain_event(SimulationDomainEvent.new( + id_generator.next_id(&"domain_event"), + event_type, + _runtime_event_minute(), + source_system, + entity_ids, + payload, + 0 + )) + +func _service_need_effects(facility_id: StringName) -> Dictionary: + var effects := { + &"hunger": 0, &"sleep": 0, &"hygiene": 0, + &"medical": 0, &"leisure": 0, &"intimacy": 0, + } + var facility := facility_service.get_instance(facility_id) + if facility == null: + return effects + var definition := definition_registry.get_facility(facility.definition_id) + if definition == null: + return effects + if &"dining" in definition.tags: + effects[&"hunger"] = -70 + effects[&"leisure"] = -10 + if &"lodging" in definition.tags: + effects[&"sleep"] = -80 + effects[&"hygiene"] = mini(int(effects[&"hygiene"]), -10) + if &"social" in definition.tags or &"leisure" in definition.tags: + effects[&"leisure"] = mini(int(effects[&"leisure"]), -40) + if &"hygiene" in definition.tags or &"bath" in definition.tags: + effects[&"hygiene"] = -75 + if &"medical" in definition.tags: + effects[&"medical"] = -85 + elif &"wellness" in definition.tags: + effects[&"medical"] = -20 + if &"adult_only" in definition.tags or &"restricted_service" in definition.tags: + effects[&"intimacy"] = -75 + return effects + +func _parse_world_cell(value: Variant) -> Variant: + if value is Vector3i: + return value + if value is Array and value.size() == 3: + return Vector3i(int(value[0]), int(value[1]), int(value[2])) + return null + +func _guest_state_index(value: Variant) -> int: + if value is int: + return value if value >= 0 and value < GuestRuntime.State.size() else -1 + var normalized := String(value).to_upper() + return GuestRuntime.State.keys().find(normalized) + +func _publish_snapshot(_absolute_minute: int) -> void: + latest_snapshot = create_snapshot() + _latest_digest_dirty = true + if not snapshot_published.get_connections().is_empty(): + snapshot_published.emit(latest_snapshot, latest_digest) + state_updated.emit(latest_snapshot) + + +func _execute_purchase_supplies(command: SimulationCommand) -> CommandResult: + return supply_management.purchase(self, command) + + +func _execute_set_supply_auto_purchase(command: SimulationCommand) -> CommandResult: + return supply_management.set_auto_purchase(self, command) + + +func _execute_set_kitchen_production(command: SimulationCommand) -> CommandResult: + return supply_management.set_production(self, command) + + +func get_supply_management_report() -> Dictionary: + return supply_management.get_report(self) + + +func preview_supply_purchase(warehouse_id: StringName, item_id: StringName, quantity: int) -> Dictionary: + return supply_management.preview_purchase(self, warehouse_id, item_id, quantity) + + +# Existing saves retain each facility's stored base rate; reception never quotes +# a new direct sale. Already queued/in-service requests retain their old quote. +func _is_anri_manager(staff: StaffRuntime) -> bool: + return staff.staff_id == &"heroine_anri" and staff.employment_kind == StaffRuntime.EmploymentKind.UNIQUE and staff.role_id == &"innkeeper" + + +func get_floor_price_multiplier(floor_y: int) -> int: + var anri := staff_service.get_staff(&"heroine_anri") + if anri == null or not _is_anri_manager(anri): return 10000 + var assignment := staff_service.get_work_assignment(anri.staff_id) + var facility := facility_service.get_instance(StringName(assignment.get(&"facility_id", &""))) + if facility == null or facility.origin.y != floor_y: return 10000 + var definition := mvp_content_catalog.get_entry(&"unique_characters", anri.staff_id) + return int(definition.get(&"mechanical_effects", {}).get(&"assigned_floor_revenue_multiplier_bp", 10000)) + + +## Original 6.2 / 6.4: Amelia's assigned chapel receives triple donations. +## This is a placement passive; attendance does not remove it. Existing service +## quotes remain frozen by ServiceRequest/ServiceInstance as for Anri. +func get_donation_multiplier(facility: FacilityInstance) -> int: + if facility.definition_id != &"chapel": return 1 + var amelia := staff_service.get_staff(&"heroine_amelia") + if amelia == null or amelia.employment_kind != StaffRuntime.EmploymentKind.UNIQUE or amelia.role_id != &"cleric": return 1 + var assignment := staff_service.get_work_assignment(amelia.staff_id) + return 3 if StringName(assignment.get(&"facility_id", &"")) == facility.instance_id else 1 + + +func get_facility_business_price(facility: FacilityInstance) -> int: + var donation := FacilityPricing.quote(_business_base_price(facility), facility.policy_current) * get_donation_multiplier(facility) + return FacilityPricing.apply_multiplier(donation, get_floor_price_multiplier(facility.origin.y)) + + +## Owned presentation data; accepted request/service quotations stay frozen. +## Matching-surcharge charging remains pending until its inputs are specified. +func get_facility_service_pricing_readmodel(facility: FacilityInstance) -> Dictionary: + return FacilityPricing.pending_service_readmodel( + get_facility_business_price(facility), facility.service_attribute + ) + + +func _business_base_price(facility: FacilityInstance) -> int: + return 0 if facility.definition_id == &"reception" else facility.base_price + + +func get_business_management_report() -> Dictionary: + var minute: int = _clock.absolute_minute + var start := (minute / SimulationKernel.MINUTES_PER_DAY) * SimulationKernel.MINUTES_PER_DAY + var totals: Dictionary = {} + for entry: LedgerEntry in economy_service.get_entries(): + if entry.absolute_minute < start or entry.absolute_minute > minute: + continue + if not totals.has(entry.source_entity_id): + totals[entry.source_entity_id] = {&"revenue": 0, &"expenses": 0} + if entry.category == &"SERVICE_SALE": + totals[entry.source_entity_id][&"revenue"] += entry.amount + elif entry.category in [&"OPERATING_EXPENSE", &"STAFF_SIGNING_COST", &"EVENT_ACTION", &"EVENT_COST_COMMITTED", &"DEFENSE_SKILL_COST", &"AUTHORITY_FINE", &"REFUND"]: + totals[entry.source_entity_id][&"expenses"] += entry.amount + var rows: Array[Dictionary] = [] + var kitchens: Array[Dictionary] = [] + for facility: FacilityInstance in facility_service.get_instances(): + var definition := definition_registry.get_facility(facility.definition_id) + if definition == null: + continue + if facility.definition_id == &"kitchen" and facility.origin.z == WorldService.LAYER_BACK: + kitchens.append({&"facility_id": facility.instance_id, &"display_name": definition.display_name, &"origin": Vector2i(facility.origin.x, facility.origin.y), &"layer": facility.origin.z, &"operation_state": StringName(FacilityInstance.OperationState.keys()[facility.operation_state])}) + var amounts: Dictionary = totals.get(facility.instance_id, {}) + var base := _business_base_price(facility) + var service_pricing := get_facility_service_pricing_readmodel(facility) + var queued_customers: Dictionary = {} + for request: ServiceRequest in facility_service.get_queue(facility.instance_id): + for guest_id: StringName in request.guest_ids: + queued_customers[guest_id] = true + rows.append({ + &"facility_id": facility.instance_id, + &"definition_id": facility.definition_id, + &"category": definition.category, + &"display_name": definition.display_name, + &"origin": Vector2i(facility.origin.x, facility.origin.y), + &"layer": facility.origin.z, + &"state": StringName(FacilityInstance.State.keys()[facility.state]), + &"kitchen_dependency": facility_service.get_required_kitchen_report(facility.instance_id), + &"business_open": facility.operator_open, + &"price_mode": StringName(facility.policy_current.get(&"price_mode", &"standard")), + &"base_price": base, + &"price": service_pricing[&"normal_price"], + &"service_pricing": service_pricing, + &"floor_price_multiplier_bp": get_floor_price_multiplier(facility.origin.y), + &"donation_multiplier": get_donation_multiplier(facility), + &"faith_per_service": definition.faith_per_service, + &"can_price": base > 0, + &"queued_count": facility.queue_entries.size(), + &"active_count": facility.active_services.size(), + &"queued_customer_count": queued_customers.size(), + &"active_customer_count": facility.customer_count(), + &"customer_capacity": facility.customer_capacity, + &"cleanliness": facility_cleanliness.get_facility(facility.instance_id).get("current", -1), + &"cleaning_status": cleaning_bridge.status(facility.instance_id), + &"today_revenue": int(amounts.get(&"revenue", 0)), + &"today_expenses": int(amounts.get(&"expenses", 0)), + }) + return {&"minute": minute, &"modes": FacilityPricing.modes(), &"facilities": rows, &"kitchens": kitchens} + + +func _guest_has_checked_in(guest: GuestRuntime) -> bool: + for visited_id: StringName in guest.visited_facility_ids: + var visited := facility_service.get_instance(visited_id) + if visited != null and visited.definition_id == &"reception": + return true + return false + + +func _service_satisfaction_sample(service: ServiceInstance, _guest: GuestRuntime) -> int: + # P1 uses a common expectation of 50 until archetypes have an explicit + # expected-quality field. Quality sensitivity is a separate concept. + var expectation := 50 + return clampi(service.quality_snapshot + FacilityPricing.satisfaction_modifier( + service.policy_snapshot, service.quality_snapshot, expectation + ), 0, 100) + + +func _release_closed_business_queues(absolute_minute: int) -> void: + for facility: FacilityInstance in facility_service.get_instances(): + if facility.operator_open and facility.is_within_opening_hours(absolute_minute): + continue + for request: ServiceRequest in facility_service.get_queue(facility.instance_id): + _release_closed_business_request(facility, request, absolute_minute) + for guest: GuestRuntime in guest_service.get_guests(): + if guest.state != GuestRuntime.State.MOVING: + continue + var facility := facility_service.get_instance(guest.target_facility_id) + if facility == null or (facility.operator_open and facility.is_within_opening_hours(absolute_minute)): + continue + var transition := guest_service.transition_guest(guest.guest_id, GuestRuntime.State.REPLANNING, absolute_minute, &"FACILITY_CLOSED") + if bool(transition.get(&"ok", false)): + guest.target_facility_id = &"" + guest.path_handle = null + + +func _release_closed_business_request(facility: FacilityInstance, request: ServiceRequest, minute: int) -> void: + # Validate the entire group before any queue/state signals become observable. + for guest_id: StringName in request.guest_ids: + var guest := guest_service.get_guest(guest_id) + if guest == null: + continue + if guest.state != GuestRuntime.State.QUEUING or guest.queue_request_id != request.request_id or guest.target_facility_id != facility.instance_id: + return + if guest.transition_tick == minute and guest.transitions_this_tick >= GuestService.MAX_TRANSITIONS_PER_TICK: + return + # A group releases its one reservation and queue atomically. A transition-budget + # failure retries next minute, without taking patience or dropping any stock. + var before_inventory := inventory_service.create_snapshot() + var before_facility := facility_service.create_snapshot() + var before_guest := guest_service.create_snapshot() + var before_events := domain_events.create_snapshot() + var before_ids := id_generator.create_snapshot() + var released := false + var release := func(_guest_id: StringName = &"", _facility_id: StringName = &"", _request_id: StringName = &"") -> Dictionary: + for reservation_id: StringName in request.reserved_inventory_ids: + var result := inventory_service.release_reservation(reservation_id, StringName("business.close.%s.%s" % [request.request_id, reservation_id]), minute) + if not bool(result.get(&"ok", false)): + return result + var result := facility_service.leave_queue(facility.instance_id, request.request_id, &"FACILITY_CLOSED") + return {&"ok": result.is_success()} + var ok := true + for guest_id: StringName in request.guest_ids: + if not guest_service.has_guest(guest_id): + continue + var result: Dictionary = guest_service.release_queue_for_replanning(guest_id, minute, release if not released else Callable()) + if not bool(result.get(&"ok", false)): + ok = false + break + released = true + if ok and not released: + ok = bool(release.call().get(&"ok", false)) + if not ok: + inventory_service.restore_snapshot(before_inventory) + facility_service.restore_snapshot(before_facility) + guest_service.restore_snapshot(before_guest) + domain_events.restore_snapshot(before_events) + id_generator.restore_snapshot(before_ids) + + +## Commute and elevator movement share the authoritative Path projection and +## fixed-point walking credit used by errands and guests. +func advance_staff_commute(staff_id: StringName, target_cell: Vector3i, minute: int, reason: StringName = &"") -> Dictionary: + var staff := staff_service.get_staff(staff_id) + if staff == null: + return {&"ok": false, &"reason": &"STAFF_NOT_FOUND"} + var actor_id := _staff_path_actor_id(staff_id) + if not _path_projection_matches(actor_id, staff.current_cell): + _report_path_integration_failure(actor_id, staff_id, &"staff", staff.current_cell) + return {&"ok": false} + if not reason.is_empty() or staff.current_cell == target_cell or not elevators.get_actor_trip(actor_id).is_empty(): + _record_expected_actor_wait(actor_id, staff.current_cell, minute) + return {&"ok": true, &"moved": false, &"reason": reason} + var path := path_service.find_path(staff.current_cell, target_cell, + [WorldService.LAYER_FRONT, WorldService.LAYER_BACK], [&"staff"]) + if path.size() < 2: + _record_expected_actor_wait(actor_id, staff.current_cell, minute) + return {&"ok": true, &"moved": false, &"reason": &"COMMUTE_ROUTE_BLOCKED"} + if _request_elevator_step(actor_id, path[0], path[1], minute, [&"staff"]): + return {&"ok": true, &"moved": false, &"reason": &"ELEVATOR_WAIT"} + var required_credit := _step_movement_credit(path_service, path[0], path[1], [&"staff"]) + var credit := _movement_credit_after_one_minute(actor_id, staff.current_cell, minute) + credit = mini(credit, required_credit + MOVEMENT_CREDIT_SCALE - 1) + if credit < required_credit: + _record_expected_actor_wait(actor_id, staff.current_cell, minute) + _movement_credits[actor_id] = {&"credit": credit, &"updated_at": minute} + return {&"ok": true, &"moved": false} + var previous := staff.current_cell + if not path_service.project_actor_position(actor_id, path[1], minute): + _report_path_integration_failure(actor_id, staff_id, &"staff", previous) + return {&"ok": false} + staff.current_cell = path[1] + _movement_credits[actor_id] = {&"credit": credit - required_credit, &"updated_at": minute} + _publish_actor_event(&"STAFF_MOVED", &"staff_commute", [staff_id, actor_id], { + &"path_actor_id": actor_id, &"from_cell": previous, &"cell": staff.current_cell, + &"target_cell": target_cell, &"reason": &"COMMUTE"}) + return {&"ok": true, &"moved": true, &"cell": staff.current_cell} + + +func _request_elevator_step(actor_id: StringName, from: Vector3i, to: Vector3i, + minute: int, access: Array[StringName]) -> bool: + var connector_id := path_service.get_step_connector(from, to, access) + if not elevators.owns_connector(connector_id): + return false + var shaft_id := elevators.get_shaft_for_connector(connector_id) + if not _elevator_extension_pending(shaft_id): + var result := elevators.request_trip(actor_id, from, to, minute, access) + if bool(result.get(&"ok", false)) and not bool(result.get(&"replay", false)): + _publish_actor_event(&"ELEVATOR_TRIP_REQUESTED", &"elevators", [actor_id, shaft_id], result[&"trip"]) + # Waiting for a cabin or a construction extension is intentional, never a + # walk across floors and never a stuck-actor timeout. + _record_expected_actor_wait(actor_id, from, minute) + return true + + +func _advance_elevators(minute: int) -> void: + var queued_before: Dictionary = {} + for trip: Dictionary in elevators.get_report()[&"trips"]: + if trip[&"state"] == PathService.ELEVATOR_REQUEST_QUEUED: + queued_before[trip[&"request_id"]] = true + for shaft: Dictionary in elevators.get_report()[&"shafts"]: + var powered := int(_state.get_resource(&"mana")) > 0 + for module_id: StringName in shaft[&"module_ids"]: + var state := mana_network_service.get_consumer_state(module_id) + if state not in [ManaNetworkService.ConsumerState.ACTIVE, ManaNetworkService.ConsumerState.DEGRADED]: + powered = false + _apply_elevator_arrivals(elevators.set_powered(shaft[&"shaft_id"], powered, minute), minute) + _apply_elevator_arrivals(elevators.advance_one_minute(minute), minute) + for trip: Dictionary in elevators.get_report()[&"trips"]: + if trip[&"state"] == PathService.ELEVATOR_REQUEST_BOARDED and queued_before.has(trip[&"request_id"]): + var payload := trip.duplicate(true) + payload[&"boarded_at"] = minute + payload[&"wait_minutes"] = minute - int(trip[&"enqueued_at"]) + _publish_actor_event(&"ELEVATOR_BOARDED", &"elevators", [trip[&"actor_id"], trip[&"shaft_id"]], payload) + + +func _apply_elevator_arrivals(arrivals: Array[Dictionary], minute: int) -> void: + for arrival: Dictionary in arrivals: + var actor_id := StringName(arrival[&"actor_id"]) + if not path_service.has_actor_projection(actor_id): + continue + var previous: Vector3i = path_service.get_actor_projected_cell(actor_id) + var cell: Vector3i = arrival[&"cell"] + var entity_id := StringName(String(actor_id).trim_prefix("guest:").trim_prefix("staff:")) + var guest := guest_service.get_guest(entity_id) if String(actor_id).begins_with("guest:") else null + var staff := staff_service.get_staff(entity_id) if String(actor_id).begins_with("staff:") else null + if guest == null and staff == null: + _report_path_integration_failure(actor_id, entity_id, &"elevator", previous) + continue + if not path_service.project_actor_position(actor_id, cell, minute): + _report_path_integration_failure(actor_id, entity_id, &"elevator", previous) + continue + if guest != null: + guest_service.record_path_success(guest.guest_id, cell, minute) + else: + staff.current_cell = cell + _movement_credits[actor_id] = {&"credit": 0, &"updated_at": minute} + _publish_actor_event(&"ELEVATOR_ARRIVED", &"elevators", [actor_id, entity_id], arrival) + + +func _execute_set_elevator_operation(command: SimulationCommand) -> CommandResult: + var shaft_id := StringName(command.payload.get(&"shaft_id", &"")) + var open_value: Variant = command.payload.get(&"operator_open", null) + if typeof(open_value) != TYPE_BOOL: + return CommandResult.failed(CommandResult.Code.INVALID_PAYLOAD, command.command_id, "operator_open must be a bool.") + var found := false + for shaft: Dictionary in elevators.get_report()[&"shafts"]: + if shaft[&"shaft_id"] == shaft_id: found = true + if not found: + return CommandResult.failed(CommandResult.Code.NOT_FOUND, command.command_id, "Elevator shaft was not found.") + _apply_elevator_arrivals(elevators.set_operator_open(shaft_id, open_value, _runtime_event_minute()), _runtime_event_minute()) + _publish_actor_event(&"ELEVATOR_OPERATION_CHANGED", &"elevators", [shaft_id], {&"operator_open": open_value}) + return CommandResult.succeeded(command.command_id, {&"shaft_id": shaft_id, &"operator_open": open_value}) + + +func _synchronize_elevators(minute: int) -> Dictionary: + return elevators.synchronize(ElevatorService.configurations_from_facilities( + construction_service.get_facilities(), definition_registry), minute) + + +func _elevator_shaft_busy(shaft: Dictionary) -> bool: + return shaft.get(&"phase", &"IDLE") != &"IDLE" or int(shaft.get(&"waiting_count", 0)) > 0 \ + or int(shaft.get(&"passenger_count", 0)) > 0 + + +func _elevator_touches_module(shaft: Dictionary, definition: FacilityDefinition, origin: Vector3i) -> bool: + if definition == null or definition.id not in [&"guest_elevator", &"service_elevator"]: + return false + var stops: Array = shaft[&"stops"] + var bottom: Vector3i = stops.front() + var top: Vector3i = stops.back() + return origin.x == bottom.x and origin.z == bottom.z and definition.footprint.x == int(shaft[&"width"]) \ + and origin.y <= top.y + 1 and origin.y + definition.footprint.y >= bottom.y + + +func _elevator_extension_pending(shaft_id: StringName) -> bool: + for shaft: Dictionary in elevators.get_report()[&"shafts"]: + if shaft[&"shaft_id"] != shaft_id: continue + for job: ConstructionJob in construction_service.get_jobs(): + if job.state in [ConstructionJob.State.COMPLETED, ConstructionJob.State.CANCELLED]: continue + if _elevator_touches_module(shaft, definition_registry.get_facility(job.definition_id), job.origin): + return true + return false + + +func _validate_elevator_crosslinks(service: ElevatorService, paths: PathService, + construction: ConstructionService, guests: GuestService, staff: StaffService, minute: int) -> bool: + if service.current_minute != minute: return false + var expected := ElevatorService.configurations_from_facilities(construction.get_facilities(), definition_registry) + var saved_configs: Array = [] + for car: Dictionary in service.create_snapshot()[&"shafts"]: + saved_configs.append(car[&"config"]) + # Construction uses numeric spatial order while snapshots use lexical IDs. + # Compare canonical shaft identity order (x=-2/10, floors=2/10 otherwise differ). + var by_id := func(a: Dictionary, b: Dictionary) -> bool: + return String(a[&"shaft_id"]) < String(b[&"shaft_id"]) + expected.sort_custom(by_id) + saved_configs.sort_custom(by_id) + if expected != saved_configs: return false + for trip: Dictionary in service.get_report()[&"trips"]: + var actor_id := StringName(trip[&"actor_id"]) + var entity_id := StringName(String(actor_id).trim_prefix("guest:").trim_prefix("staff:")) + var guest := guests.get_guest(entity_id) if String(actor_id).begins_with("guest:") else null + var worker := staff.get_staff(entity_id) if String(actor_id).begins_with("staff:") else null + if guest == null and worker == null: return false + if guest != null and (guest.current_cell != trip[&"from"] or trip[&"from"].z != WorldService.LAYER_FRONT): return false + if worker != null and worker.current_cell != trip[&"from"]: return false + if not paths.has_actor_projection(actor_id) or paths.get_actor_projected_cell(actor_id) != trip[&"from"]: return false + return true + + +func is_staff_in_transit(staff_id: StringName) -> bool: + return not elevators.get_actor_trip(_staff_path_actor_id(staff_id)).is_empty() + + +func _estimated_actor_path_minutes(path: Array, access: Array[StringName]) -> int: + for index: int in range(1, path.size()): + var connector_id := path_service.get_step_connector(path[index - 1], path[index], access) + if elevators.owns_connector(connector_id) \ + or _step_movement_credit_for_connector(path_service, path[index - 1], path[index], connector_id) > MOVEMENT_CREDIT_SCALE: + var cost: int + if access.is_empty() and path_service.get_script().resource_path == "res://scripts/world/path_service.gd" \ + and path[0] is Vector3i and path[path.size() - 1] is Vector3i: + cost = path_service.calculate_guest_batch_path_cost(path[0], path[path.size() - 1], path) + else: + cost = path_service.calculate_path_cost(path, access) + return ceili(float(cost) / PathService.ORTHOGONAL_MOVE_COST) + return maxi(path.size() - 1, 0) + + +func _prune_expired_staff_elevator_waits(minute: int) -> void: + for trip: Dictionary in elevators.get_report()[&"trips"]: + if trip.has(&"companion_id"): continue + var actor_id := StringName(trip[&"actor_id"]) + if trip[&"state"] != PathService.ELEVATOR_REQUEST_QUEUED or not String(actor_id).begins_with("staff:"): + continue + var staff := staff_service.get_staff(StringName(String(actor_id).trim_prefix("staff:"))) + if staff == null: + elevators.cancel_actor(actor_id) + continue + if staff.state == StaffRuntime.State.OFF_DUTY and get_staff_rest_report(staff.staff_id).get(&"reachable", false): + continue + if not staff_service.requires_attendance(staff.staff_id): + continue + var task := staff_service.get_task(staff.current_task_id) + if task != null and task.state == StaffTask.State.MOVING and task.assigned_staff_id == staff.staff_id: + continue + var shift := staff_service.get_shift(staff.shift_id) + if not staff.can_take_shift() or shift == null or (not shift.contains_minute(minute) and not shift.is_commute_window(minute)): + elevators.cancel_actor(actor_id) + _publish_actor_event(&"ELEVATOR_COMMUTE_CANCELLED", &"staff_commute", [staff.staff_id, actor_id], {&"reason": &"SHIFT_ENDED_OR_UNAVAILABLE"}) + + +## Stairs carry half-minute progress between ticks. Doors keep their established +## one-step walking behavior, and elevator time is owned by the cabin service. +static func _step_movement_credit(path: PathService, from: Vector3i, to: Vector3i, + access: Array[StringName]) -> int: + return _step_movement_credit_for_connector(path,from,to,path.get_step_connector(from,to,access)) + + +static func _step_movement_credit_for_connector(path: PathService, from: Vector3i, to: Vector3i, connector_id: StringName) -> int: + if String(connector_id).begins_with("infra.connector:") and from.x == to.x \ + and from.z == to.z and absi(from.y - to.y) == 1: + var connector := path.get_connector(connector_id) + if connector.get(&"movement_type", &"") == PathService.MOVEMENT_CONNECTOR \ + and int(connector.get(&"cost", 0)) == 150: + return STAIR_MOVEMENT_CREDIT + return MOVEMENT_CREDIT_SCALE + + +static func _cell_movement_credit_limit(path: PathService, cell: Vector3i) -> int: + # Temporary blockades must not invalidate progress saved on a staircase. + # Inspect structural edges here instead of querying currently usable routes. + for edge: Dictionary in path.get_neighbors(cell): + var next: Vector3i = edge[&"to"] + if String(edge[&"connector_id"]).begins_with("infra.connector:") \ + and edge[&"movement_type"] == PathService.MOVEMENT_CONNECTOR \ + and int(edge[&"base_cost"]) == 150 and cell.x == next.x \ + and cell.z == next.z and absi(cell.y - next.y) == 1: + return STAIR_MOVEMENT_CREDIT + return MOVEMENT_CREDIT_SCALE + + +func _prune_legacy_stairs_and_movement() -> bool: + if not legacy_stairs.prune_invalid(): + return false + # A removed staircase discards only progress that can no longer be used. + for actor_id: StringName in _movement_credits: + if path_service.has_actor_projection(actor_id): + var cell: Vector3i = path_service.get_actor_projected_cell(actor_id) + _movement_credits[actor_id][&"credit"] = mini(int(_movement_credits[actor_id][&"credit"]), + _cell_movement_credit_limit(path_service, cell) - 1) + return true + + +func get_elevator_wait_statistics(shaft_id: StringName) -> Dictionary: + var samples: Array[int] = [] + var oldest_wait := 0 + var minute := _runtime_event_minute() + for event: SimulationDomainEvent in domain_events.recent_events(): + if event.event_type != &"ELEVATOR_BOARDED" or event.absolute_minute < minute - 60 \ + or event.absolute_minute > minute or event.payload.get(&"shaft_id", &"") != shaft_id: + continue + var wait: int = int(event.payload.get(&"wait_minutes", -1)) + if wait >= 0: + samples.append(wait) + for trip: Dictionary in elevators.get_report()[&"trips"]: + if trip[&"shaft_id"] == shaft_id and trip[&"state"] == PathService.ELEVATOR_REQUEST_QUEUED: + oldest_wait = maxi(oldest_wait, minute - int(trip[&"enqueued_at"])) + samples.sort() + var total := 0 + for wait: int in samples: + total += wait + return {&"sample_count": samples.size(), &"window_minutes": 60, + &"event_history_limit": DomainEventStream.RECENT_EVENT_LIMIT, + &"mean_wait_minutes": float(total) / samples.size() if not samples.is_empty() else 0.0, + &"p95_wait_minutes": samples[ceili(samples.size() * 0.95) - 1] if not samples.is_empty() else 0, + &"oldest_wait_minutes": oldest_wait} + + +func _demolition_lost_route_cells(instance_id: StringName) -> Array[Vector3i]: + var lost_cells: Array[Vector3i] = [] + var projection := facility_infrastructure_projector.get_projection(instance_id) + var base := route_subslot_path_adapter.get_base_walkable_cells() + for cell: Vector3i in projection.get(&"cells", []): + if cell not in base: + lost_cells.append(cell) + return lost_cells + + +func _stair_demolition_dependents(instance_id: StringName) -> Array[StringName]: + var lost_cells := _demolition_lost_route_cells(instance_id) + var dependents := facility_infrastructure_projector.get_stair_dependents(lost_cells) + dependents.erase(instance_id) + # A staircase under construction already relies on its lower landing too. + for job: ConstructionJob in construction_service.get_jobs(): + if not job.is_terminal() and job.definition_id in [&"guest_stairs", &"service_stairs"] \ + and job.origin + Vector3i(0, -1, 0) in lost_cells: + dependents.append(job.job_id) + return dependents + + +## Use the same entrance and horizontal access cells as ordinary Guest AI. +## Facility access_available is an operating override, not a topology cache. +func is_facility_reachable(facility_id: StringName) -> bool: + var facility := facility_service.get_instance(facility_id) + var entrance: Variant = _operations_entrance_cell() + if facility == null or entrance == null or not facility.access_available: + return false + # Read-only UI queries need existence, not the cheapest destination. + # Share Guest AI's candidates and path rules without caching topology state. + for candidate: Vector3i in _facility_access_candidates(facility): + if not path_service.find_path(entrance, candidate).is_empty(): + return true + return false + + +func get_facility_access_report(facility_id: StringName) -> Dictionary: + var facility := facility_service.get_instance(facility_id) + var entrance: Variant = _operations_entrance_cell() + if facility == null or entrance == null or not facility.access_available: + return {&"reachable": false, &"path": []} + return _find_facility_access_path(entrance, facility) + + +func _staff_is_being_carried(patient_id: StringName) -> bool: + for task: StaffTask in staff_service.get_tasks(): + if task.task_type == &"rescue_transport" and not task.is_terminal() and not task.assigned_staff_id.is_empty() and task.target_entity_id == patient_id: + return true + return false + +func _rescue_destination(patient_cell: Vector3i) -> Dictionary: + var best := {} + var distance := 2147483647 + for clinic: FacilityInstance in facility_service.get_instances(): + if clinic.definition_id != &"infirmary" or clinic.operation_state != FacilityInstance.OperationState.OPERATING: + continue + var cells := {clinic.origin: true} + for cell: Vector3i in clinic.occupied_cells: + for delta in [Vector3i.LEFT, Vector3i.RIGHT, Vector3i.UP, Vector3i.DOWN]: + cells[cell + delta] = true + var ordered := cells.keys() + ordered.sort_custom(func(a: Vector3i, b: Vector3i): return a.y < b.y if a.y != b.y else a.x < b.x) + for cell: Vector3i in ordered: + if cell.z != WorldService.LAYER_BACK: + continue + var route := path_service.find_backyard_path(patient_cell, cell, true) + if not route.is_empty() and route.size() < distance: + distance = route.size() + best = {&"facility_id": clinic.instance_id, &"cell": cell} + return best + +func _handle_rescue_arrival(task: StaffTask, carrier: StaffRuntime, minute: int) -> bool: + if task.task_type == CleaningBridge.TASK_TYPE: + return carrier.current_cell == task.target_cell and cleaning_bridge.task_can_work(task, carrier) + if task.task_type not in [&"rescue_pickup", &"rescue_transport"]: + return true + var patient := staff_service.get_staff(task.target_entity_id) + if patient == null or patient.state != StaffRuntime.State.INCAPACITATED or carrier.current_cell != task.target_cell: + return false + if task.task_type == &"rescue_transport": + var clinic := facility_service.get_instance(task.rescue_facility_id) + return clinic != null and clinic.operation_state == FacilityInstance.OperationState.OPERATING and patient.current_cell == carrier.current_cell + if patient.current_cell != carrier.current_cell: + return false + var destination := _rescue_destination(patient.current_cell) + if destination.is_empty(): + return false + task.task_type = &"rescue_transport" + task.rescue_facility_id = destination[&"facility_id"] + task.target_cell = destination[&"cell"] + task.state = StaffTask.State.MOVING + task.state_changed_at = minute + _publish_actor_event(&"STAFF_RESCUE_PICKED_UP", &"staff_service", [patient.staff_id, carrier.staff_id], {&"facility_id": task.rescue_facility_id}) + return false + +func _advance_staff_rescue(minute: int) -> void: + for task: StaffTask in staff_service.get_tasks(): + if task.task_type not in [&"rescue_pickup", &"rescue_transport"] or task.is_terminal(): + continue + var patient := staff_service.get_staff(task.target_entity_id) + var carrier := staff_service.get_staff(task.assigned_staff_id) + var invalid := patient == null or patient.state != StaffRuntime.State.INCAPACITATED + if not task.assigned_staff_id.is_empty(): + invalid = invalid or carrier == null or not carrier.can_take_shift() or carrier.current_task_id != task.task_id + if task.task_type == &"rescue_transport": + var clinic := facility_service.get_instance(task.rescue_facility_id) + invalid = invalid or carrier == null or clinic == null or clinic.operation_state != FacilityInstance.OperationState.OPERATING + if patient != null and carrier != null: + invalid = invalid or patient.current_cell != carrier.current_cell + if invalid: + if carrier != null and elevators.get_actor_trip(_staff_path_actor_id(carrier.staff_id)).get(&"state", &"") == PathService.ELEVATOR_REQUEST_BOARDED: + continue # Keep the pair aboard until a real or emergency landing. + staff_service.cancel_task(task.task_id, &"RESCUE_INTERRUPTED", minute) + if patient != null: elevators.cancel_actor(_staff_path_actor_id(patient.staff_id)) + if carrier != null: elevators.cancel_actor(_staff_path_actor_id(carrier.staff_id)) + for patient: StaffRuntime in staff_service.get_staff_members(): + if not _defense_lock_owner(patient.staff_id, &"DEFENDER").is_empty(): + continue + if patient.health > 0 and patient.state != StaffRuntime.State.INCAPACITATED: + continue + if not patient.current_task_id.is_empty(): + staff_service.cancel_task(patient.current_task_id, &"STAFF_COLLAPSED", minute) + if patient.state != StaffRuntime.State.INCAPACITATED or patient.state_reason.is_empty(): + patient.transition_to(StaffRuntime.State.INCAPACITATED, &"STAFF_COLLAPSED", minute) + _publish_actor_event(&"STAFF_COLLAPSED", &"staff_service", [patient.staff_id], {&"cell": patient.current_cell}) + if patient.current_cell.z == WorldService.LAYER_FRONT and patient.state_reason != &"COLLAPSE_WITNESSED": + for guest: GuestRuntime in guest_service.get_guests(): + if _authority_line_of_sight(guest.current_cell, patient.current_cell, {}): + if progression_service.update_metrics({&"reputation": maxi(int(progression_service.metrics[&"reputation"]) - 1, 0)}): + patient.state_reason = &"COLLAPSE_WITNESSED" + _publish_actor_event(&"STAFF_COLLAPSE_WITNESSED", &"staff_service", [patient.staff_id, guest.guest_id], {}) + break + var pending := false + for task: StaffTask in staff_service.get_tasks(): + if task.task_type in [&"rescue_pickup", &"rescue_transport"] and task.target_entity_id == patient.staff_id and not task.is_terminal(): + pending = true + if not elevators.get_actor_trip(_staff_path_actor_id(patient.staff_id)).is_empty(): + continue + if pending or _rescue_destination(patient.current_cell).is_empty(): + continue + staff_service.create_task(&"rescue_pickup", 95, minute, { + &"target_entity_id": patient.staff_id, &"target_cell": patient.current_cell, + &"required_role_tags": [&"medic"], &"task_tags": [&"rescue"], + &"estimated_minutes": 30, &"interrupt_level": 0, &"emergency": true, + }) + + +func get_staff_rescue_report() -> Dictionary: + var report := {&"waiting": 0, &"transporting": 0, &"treating": 0} + for patient: StaffRuntime in staff_service.get_staff_members(): + if patient.state != StaffRuntime.State.INCAPACITATED: + continue + var category: StringName = &"waiting" + for task: StaffTask in staff_service.get_tasks(): + if task.target_entity_id == patient.staff_id and task.task_type == &"rescue_transport" and not task.is_terminal(): + category = &"treating" if task.state == StaffTask.State.WORKING else &"transporting" + report[category] += 1 + return report + + +func _staff_rescue_status(staff_id: StringName) -> String: + var staff := staff_service.get_staff(staff_id) + if staff == null or staff.state != StaffRuntime.State.INCAPACITATED: + return "" + for task: StaffTask in staff_service.get_tasks(): + if task.target_entity_id != staff_id or task.is_terminal(): continue + if task.task_type == &"rescue_transport": + return "治療中" if task.state == StaffTask.State.WORKING else "搬送中" + if task.task_type == &"rescue_pickup" and not task.assigned_staff_id.is_empty(): + return "救護班が向かっています" + return "救護待ち(医療職・医務室・裏経路を確認)" + + +## Rest destinations derive from saved facilities and position, with no hidden timer. +func get_staff_rest_report(staff_id: StringName) -> Dictionary: + var staff := staff_service.get_staff(staff_id) + if staff == null or staff.state != StaffRuntime.State.OFF_DUTY or staff.health <= 0 or not staff.current_task_id.is_empty(): return {} + if not _defense_lock_owner(staff_id, &"DEFENDER").is_empty() or event_service.get_actor_locks().has(staff_id): return {} + var best := {&"reachable": false, &"resting": false, &"label": "休憩先なし(稼働中の待機室・宿舎と経路を確認)"} + var best_cost := 2147483647 + for room: FacilityInstance in facility_service.get_instances(): + if room.definition_id not in [&"staff_waiting_room", &"staff_dormitory"] or room.operation_state != FacilityInstance.OperationState.OPERATING: continue + var route := staff_commute.find_facility_access_path(staff.current_cell, room) + if not route.get(&"reachable", false): continue + var cost := path_service.calculate_path_cost(route[&"path"], [&"staff"]) + if cost >= best_cost: continue + best_cost = cost + var resting := StaffCommuteService.is_at_facility(staff, room) and not is_staff_in_transit(staff_id) + best = {&"reachable": true, &"resting": resting, &"cell": route[&"cell"], &"facility_id": room.instance_id, + &"label": ("宿舎で休憩中" if room.definition_id == &"staff_dormitory" else "待機室で休憩中") if resting else "休憩先へ移動中"} + return best + + +func _advance_weather(absolute_minute: int) -> void: + weather.advance_to(absolute_minute) + + +func get_weather_report() -> Dictionary: + return {&"kind": weather.get_kind(), &"minute": weather.get_minute()} + + +## Read-only migration preview; applying dimensions remains a separate transaction. +func preview_facility_dimension_migration(instance_id: StringName) -> Dictionary: + var instance := construction_service.get_facility(instance_id) + if instance.is_empty(): + return {&"geometry_ok": false, &"ready_to_apply": false, &"errors": [&"COMPLETED_FACILITY_REQUIRED"]} + var weights := {} + for built: Dictionary in construction_service.get_facilities(): + var definition := definition_registry.get_facility(built[&"definition_id"]) + if definition == null: + return {&"geometry_ok": false, &"ready_to_apply": false, &"errors": [&"MISSING_FACILITY_DEFINITION"]} + weights[built[&"instance_id"]] = definition.weight_per_cell + var preview: Dictionary = preload("res://scripts/construction/facility_resize_preview.gd").inspect(world_service, instance, weights[instance_id], weights) + if preview.has(&"remaining_gates"): + preview[&"remaining_gates"].erase(&"DEFINITION_WEIGHT_VALIDATION") + preview[&"definition_weights"] = weights + if preview.get(&"geometry_ok", false): + preview[&"construction_candidate"] = preload("res://scripts/construction/facility_resize_preview.gd").prepare_construction_candidate(world_service, construction_service, instance_id, weights[instance_id], weights) + preview[&"actors_on_removed_cells"] = [] + for guest: GuestRuntime in guest_service.get_guests(): + if guest.current_cell in preview.get(&"removed", []): + preview[&"actors_on_removed_cells"].append({&"kind": &"guest", &"id": guest.guest_id, &"cell": guest.current_cell}) + for worker: StaffRuntime in staff_service.get_staff_members(): + if worker.current_cell in preview.get(&"removed", []): + preview[&"actors_on_removed_cells"].append({&"kind": &"staff", &"id": worker.staff_id, &"cell": worker.current_cell}) + preview[&"construction_jobs"] = [] + for job: ConstructionJob in construction_service.get_jobs(): + preview[&"construction_jobs"].append({&"job_id": job.job_id, &"definition_id": job.definition_id, &"state": job.state}) + return preview + +## Assemble and validate a snapshot in an isolated Runtime, including access +## preservation. Disk backup, actor relocation and atomic commit remain separate. +func prepare_facility_dimension_snapshot(instance_id: StringName) -> Dictionary: + if construction_service.get_facility(instance_id).is_empty() and construction_service.get_job(instance_id) != null: + return _prepare_construction_dimension_snapshot(instance_id) + var preview := preview_facility_dimension_migration(instance_id) + var result := {&"ok": false, &"ready_to_apply": false, &"errors": [], &"preview": preview} + if not preview.get(&"construction_candidate", {}).get(&"ok", false): + result[&"errors"].append(&"CONSTRUCTION_CANDIDATE_REQUIRED") + return result + if not preview[&"actors_on_removed_cells"].is_empty(): + result[&"errors"].append(&"ACTOR_RELOCATION_REQUIRED") + return result + var candidate := create_snapshot() + var data: Dictionary = candidate[&"systems"][&"runtime"] + data[&"world"] = preview[&"construction_candidate"][&"world"].duplicate(true) + data[&"construction"] = preview[&"construction_candidate"][&"construction"].duplicate(true) + var found := false + for instance: Dictionary in data[&"facilities"][&"instances"]: + if StringName(instance[&"instance_id"]) != instance_id: continue + found = true + var cells: Array = [] + for cell: Vector3i in preview[&"target_cells"]: cells.append([cell.x, cell.y, cell.z]) + instance[&"occupied_cells"] = cells + if not found: + result[&"errors"].append(&"OPERATIONAL_FACILITY_REQUIRED") + return result + var isolated: Variant = get_script().new() + var candidate_clock: Variant = load("res://autoload/game_clock.gd").new() + var candidate_state: Variant = load("res://autoload/game_state.gd").new() + candidate_clock.reset(1, 6, 0) + candidate_state.reset() + isolated.initialize(candidate_clock, candidate_state, world_seed) + # The immutable definition resources must match the source registry, + # including any explicitly registered expansion definitions. + isolated.definition_registry = definition_registry + var restored: bool = isolated.restore_snapshot(candidate) + if restored: + result[&"routes"] = preload("res://scripts/construction/facility_resize_routes.gd").inspect(self, isolated, instance_id) + if result[&"routes"][&"errors"].is_empty() and not result[&"routes"][&"guest_replans"].is_empty(): + result[&"retargeting"] = preload("res://scripts/construction/facility_resize_routes.gd").retarget_guests(isolated, result[&"routes"]) + if result[&"retargeting"][&"ok"]: + candidate = isolated.create_snapshot() + restored = isolated.restore_snapshot(candidate) + if restored: + result[&"routes"] = preload("res://scripts/construction/facility_resize_routes.gd").inspect(self, isolated, instance_id) + isolated.free() + candidate_clock.free() + candidate_state.free() + if not restored: + result[&"errors"].append(&"RUNTIME_CANDIDATE_REJECTED") + return result + if not result[&"routes"][&"ok"]: + result[&"errors"].append(&"ROUTE_MIGRATION_REQUIRED") + return result + result[&"ok"] = true + result[&"snapshot"] = candidate + return result + +## Explicit disk migration. Source Runtime stays untouched; the formal package +## is replaced only after isolated domain/route validation and verified backup. +func migrate_facility_dimensions_at_path(save_path: String, instance_id: StringName = &"", expected_source_digest: String = "") -> Dictionary: + var loaded := SaveService.load_snapshot(save_path) + if not loaded[&"ok"]: return loaded + if not expected_source_digest.is_empty() and SimulationDigest.compute(loaded[&"snapshot"]) != expected_source_digest: + return {&"ok": false, &"code": &"MIGRATION_SOURCE_CHANGED"} + var isolated: Variant = get_script().new() + var migration_clock: Variant = load("res://autoload/game_clock.gd").new() + var migration_state: Variant = load("res://autoload/game_state.gd").new() + migration_clock.reset(1, 6, 0); migration_state.reset() + isolated.initialize(migration_clock, migration_state, world_seed) + isolated.definition_registry = definition_registry + var prepared := {&"ok": false, &"errors": [&"SOURCE_RUNTIME_REJECTED"]} + if isolated.restore_snapshot(loaded[&"snapshot"]): + if instance_id.is_empty(): + prepared = isolated.prepare_all_facility_dimension_snapshots() + else: + prepared = isolated.prepare_facility_dimension_snapshot(instance_id) + isolated.free(); migration_clock.free(); migration_state.free() + if not prepared[&"ok"]: + return {&"ok": false, &"code": &"DIMENSION_MIGRATION_REJECTED", &"details": prepared} + return SaveService.replace_snapshot_with_backup(save_path, loaded[&"snapshot"], prepared[&"snapshot"]) + +## Runs on an isolated migration Runtime; caller commits only the final snapshot. +func prepare_all_facility_dimension_snapshots() -> Dictionary: + var original := create_snapshot() + var targets: Array[StringName] = [] + var shapes: Dictionary = preload("res://scripts/construction/facility_resize_preview.gd").DIMENSIONS + for job: ConstructionJob in construction_service.get_jobs(): + if job.is_terminal() or not shapes.has(job.definition_id): continue + var size: Vector2i = shapes[job.definition_id][&"new"] + if job.rotation in [90,270]: size = Vector2i(size.y,size.x) + if not preload("res://scripts/construction/facility_resize_preview.gd")._same_cells(job.reserved_cells, world_service.cells_for_footprint(job.origin,size)): + targets.append(job.job_id) + for facility: Dictionary in construction_service.get_facilities(): + if not shapes.has(facility[&"definition_id"]): continue + var size: Vector2i = shapes[facility[&"definition_id"]][&"new"] + if facility[&"rotation"] in [90,270]: size = Vector2i(size.y,size.x) + if not preload("res://scripts/construction/facility_resize_preview.gd")._same_cells(facility[&"occupied_cells"], world_service.cells_for_footprint(facility[&"origin"],size)): + targets.append(facility[&"instance_id"]) + for id: StringName in targets: + var prepared := prepare_facility_dimension_snapshot(id) + if not prepared[&"ok"] or not restore_snapshot(prepared[&"snapshot"]): + var rolled_back := restore_snapshot(original) + return {&"ok": false, &"errors": [&"BATCH_DIMENSION_REJECTED"], &"facility_id": id, &"details": prepared, &"rolled_back": rolled_back} + return {&"ok": true, &"snapshot": create_snapshot(), &"facility_ids": targets} + +func _prepare_construction_dimension_snapshot(job_id: StringName) -> Dictionary: + var weights := {} + for facility: Dictionary in construction_service.get_facilities(): + var definition := definition_registry.get_facility(facility[&"definition_id"]) + if definition == null: return {&"ok": false, &"errors": [&"MISSING_FACILITY_DEFINITION"]} + weights[facility[&"instance_id"]] = definition.weight_per_cell + var job := construction_service.get_job(job_id) + var definition := definition_registry.get_facility(job.definition_id) + if job.is_terminal() or definition == null: + return {&"ok": false, &"errors": [&"ACTIVE_JOB_REQUIRED"]} + weights[job_id] = definition.weight_per_cell + var prepared: Dictionary = preload("res://scripts/construction/facility_resize_preview.gd").prepare_construction_candidate(world_service, construction_service, job_id, definition.weight_per_cell, weights) + if not prepared[&"ok"]: return prepared + var candidate := create_snapshot() + candidate[&"systems"][&"runtime"][&"world"] = prepared[&"world"] + candidate[&"systems"][&"runtime"][&"construction"] = prepared[&"construction"] + var isolated: Variant = get_script().new() + var candidate_clock: Variant = load("res://autoload/game_clock.gd").new() + var candidate_state: Variant = load("res://autoload/game_state.gd").new() + candidate_clock.reset(1,6,0); candidate_state.reset() + isolated.initialize(candidate_clock, candidate_state, world_seed) + isolated.definition_registry = definition_registry + var valid: bool = isolated.restore_snapshot(candidate) + isolated.free(); candidate_clock.free(); candidate_state.free() + if not valid: return {&"ok": false, &"errors": [&"RUNTIME_CANDIDATE_REJECTED"]} + return {&"ok": true, &"ready_to_apply": false, &"snapshot": candidate} + +## Read-only saved-game preflight used by the load menu before confirmation. +func preview_facility_dimensions_at_path(save_path: String) -> Dictionary: + var loaded := SaveService.inspect_snapshot(save_path) + if not loaded[&"ok"]: return loaded + var isolated: Variant = get_script().new() + var preview_clock: Variant = load("res://autoload/game_clock.gd").new() + var preview_state: Variant = load("res://autoload/game_state.gd").new() + preview_clock.reset(1,6,0); preview_state.reset() + isolated.initialize(preview_clock,preview_state,world_seed) + isolated.definition_registry=definition_registry + var result := {&"ok": false, &"code": &"SOURCE_RUNTIME_REJECTED"} + if isolated.restore_snapshot(loaded[&"snapshot"]): + result=isolated.prepare_all_facility_dimension_snapshots() + result[&"source_digest"]=SimulationDigest.compute(loaded[&"snapshot"]) + result[&"changes"]=[] + if result[&"ok"]: + for id: StringName in result[&"facility_ids"]: + var facility: Dictionary=isolated.construction_service.get_facility(id) + var job: ConstructionJob=isolated.construction_service.get_job(id) + var definition_id: StringName=facility.get(&"definition_id",&"") if job==null else job.definition_id + var definition: FacilityDefinition=definition_registry.get_facility(definition_id) + result[&"changes"].append({&"id":id,&"name":definition.display_name,&"size":definition.footprint,&"building":job!=null}) + result.erase(&"snapshot") + isolated.free();preview_clock.free();preview_state.free() + return result + + +func _on_cleanliness_facility_registered(facility: FacilityInstance) -> void: + var values := cleaning_policy.create_snapshot() + if facility_cleanliness.get_facility(facility.instance_id).is_empty(): + facility_cleanliness.register_facility(facility.instance_id, values["initial_cleanliness"], values["maximum_cleanliness"]) + + +func _on_cleanliness_facility_unregistered(facility_id: StringName) -> void: + for task: StaffTask in staff_service.get_tasks(): + if task.task_type == CleaningBridge.TASK_TYPE and task.target_entity_id == facility_id and not task.is_terminal(): + staff_service.cancel_task(task.task_id, &"CLEANING_TARGET_REMOVED", _runtime_event_minute()) + if not facility_cleanliness.get_facility(facility_id).is_empty(): + facility_cleanliness.unregister_facility(facility_id) + _sync_facility_staffing(_runtime_event_minute()) + + +func _validate_cleanliness_crosslinks(cleanliness: RefCounted, facilities: FacilityService, staff: StaffService, path: PathService) -> bool: + var snapshot: Dictionary = cleanliness.create_snapshot() + var instances := facilities.get_instances() + var completed_cleanings: Dictionary = {} + if snapshot["facilities"].size() != instances.size(): return false + for facility: FacilityInstance in instances: + var row: Dictionary = cleanliness.get_facility(facility.instance_id) + if row.is_empty() or row["maximum"] != 100 or row["initial"] != 100: return false + for receipt: Dictionary in snapshot["receipts"]: + if receipt["kind"] == "clean": + var task := staff.get_task(StringName(receipt["source_id"])) + if task == null or task.task_type != CleaningBridge.TASK_TYPE or task.state != StaffTask.State.COMPLETED or task.target_entity_id != StringName(receipt["facility_id"]) or receipt["amount"] != cleaning_policy.create_snapshot()["recovery_per_completed_task"]: return false + if task.progress_minutes != task.estimated_minutes: return false + completed_cleanings[task.task_id] = true + continue + if receipt["kind"] != "use": return false + var service := facilities.get_service(StringName(receipt["source_id"])) + if service == null or service.state != ServiceInstance.State.COMPLETED or service.facility_id != StringName(receipt["facility_id"]): return false + var facility := facilities.get_instance(service.facility_id) + var definition := definition_registry.get_facility(facility.definition_id) + if definition == null or receipt["amount"] != definition.cleanliness_decay_per_service: return false + var pending: Dictionary = {} + for task: StaffTask in staff.get_tasks(): + if task.task_type != CleaningBridge.TASK_TYPE: continue + if task.estimated_minutes != cleaning_policy.create_snapshot()["work_minutes"] or task.required_role_tags != [&"cleaner"]: return false + if task.state == StaffTask.State.COMPLETED and facilities.has_instance(task.target_entity_id) and not completed_cleanings.has(task.task_id): return false + if task.is_terminal(): continue + var facility := facilities.get_instance(task.target_entity_id) + if facility == null or task.target_cell not in CleaningBridge.access_cells(facility, path, false) or pending.has(task.target_entity_id): return false + pending[task.target_entity_id] = true + if not task.assigned_staff_id.is_empty(): + var worker := staff.get_staff(task.assigned_staff_id) + if not CleaningBridge.is_deployed(worker, facilities.get_instance(worker.assigned_facility_id) if worker != null else null, staff): return false + return true + +func _automatic_staff_work_units(task_id: StringName) -> int: + var task := staff_service.get_task(task_id) + # Cleaning is progressed after attendance, dependency and cancellation checks. + return 0 if task != null and task.task_type == CleaningBridge.TASK_TYPE else 1 + + +## This is a configured event sink, not a thief/riot/loan predicate. Producers +## must supply a cause identity and a complete, approved threat template. +func _build_event_defense_request(action: EventAction, context: Dictionary) -> Dictionary: + var instance_value: Variant = context.get(&"event_instance_id", context.get("event_instance_id", null)) + var source_value: Variant = context.get(&"threat_source_id", context.get("threat_source_id", null)) + var template_value: Variant = action.payload.get(&"template_id", action.payload.get("template_id", null)) + var kind_value: Variant = action.payload.get(&"source_kind", action.payload.get("source_kind", null)) + for raw: Variant in [instance_value, source_value, template_value, kind_value]: + if typeof(raw) not in [TYPE_STRING, TYPE_STRING_NAME] or String(raw).is_empty(): + return {&"ok": false, &"error": "Configured threat requires event, source, template and source-kind identities."} + var source_kind := String(kind_value).to_upper() + if source_kind not in ["THIEF", "BANKRUPTCY_RIOT", "BANK_DELINQUENCY", "AUTHORITY_ESCALATION"]: + return {&"ok": false, &"error": "Configured threat source kind is unknown."} + var identity := JSON.stringify([String(instance_value), String(action.action_id)], "", true, true) + var raid_id := StringName("event_defense_raid." + identity.sha256_text()) + var prepared := _prepare_event_threat_template(action, StringName(source_value), raid_id) + if not bool(prepared.get(&"ok", false)): + return prepared + var definition: Dictionary = prepared[&"definition"] + var digest := String(prepared[&"digest"]) + var event_source := { + "event_instance_id": String(instance_value), "event_action_id": String(action.action_id), + "template_id": String(template_value), "template_digest": digest, + "source_kind": source_kind, + } + definition[&"event_source"] = event_source + var existing := defense_service.get_raid(raid_id) + if not existing.is_empty(): + if existing.get(&"event_source", {}) != event_source or not _event_threat_template_matches_raid(existing, prepared[&"canonical_raid"]): + return {&"ok": false, &"error": "Event threat identity conflicts with an already-created incident."} + return {&"ok": true, &"raid_id": raid_id, &"definition": definition, &"is_replay": true} + for objective_id: StringName in prepared[&"objective_ids"]: + var objective := facility_service.get_instance(objective_id) + if objective == null or objective.durability <= 0: + return {&"ok": false, &"error": "Configured threat objective is unavailable."} + if not _defense_lock_owner(objective_id, &"ASSET", raid_id).is_empty(): + return {&"ok": false, &"error": "Configured threat objective already belongs to another active incident."} + return {&"ok": true, &"raid_id": raid_id, &"definition": definition, &"is_replay": false} + + +func _prepare_event_threat_template(action: EventAction, source_id: StringName, raid_id: StringName) -> Dictionary: + var raw_definition: Variant = action.payload.get(&"definition", action.payload.get("definition", null)) + if not raw_definition is Dictionary: + return {&"ok": false, &"error": "Configured threat requires its complete definition."} + var definition: Dictionary = raw_definition.duplicate(true) + # Do not silently turn service defaults into newly approved enemy values. + for field: StringName in [&"round_minutes", &"max_duration_minutes", &"defender_retreat_threshold", &"terrain_bonus", &"command_bonus", &"surprise_penalty", &"multiple_entry_penalty"]: + if typeof(definition.get(field, definition.get(String(field), null))) != TYPE_INT: + return {&"ok": false, &"error": "Configured threat requires an explicit integer: %s." % field} + var retreat_value: Variant = definition.get(&"retreat_policy", definition.get("retreat_policy", null)) + if typeof(retreat_value) not in [TYPE_STRING, TYPE_STRING_NAME]: + return {&"ok": false, &"error": "Configured threat requires an explicit retreat policy."} + for field: StringName in [&"entry_points", &"objective_ids"]: + var raw_ids: Variant = definition.get(field, definition.get(String(field), null)) + if not raw_ids is Array or raw_ids.is_empty(): + return {&"ok": false, &"error": "Configured threat requires explicit %s." % field} + for raw_id: Variant in raw_ids: + if typeof(raw_id) not in [TYPE_STRING, TYPE_STRING_NAME] or String(raw_id).is_empty(): + return {&"ok": false, &"error": "Configured threat entry/objective IDs must be nonempty names."} + var raw_waves: Variant = definition.get(&"waves", definition.get("waves", null)) + if not raw_waves is Array or raw_waves.is_empty(): + return {&"ok": false, &"error": "Configured threat requires explicit waves."} + for raw_wave: Variant in raw_waves: + if not raw_wave is Dictionary: + return {&"ok": false, &"error": "Configured threat wave must be a dictionary."} + for field: StringName in [&"threat_strength", &"max_rounds", &"retreat_threshold"]: + if typeof(raw_wave.get(field, raw_wave.get(String(field), null))) != TYPE_INT: + return {&"ok": false, &"error": "Configured threat wave requires an explicit integer: %s." % field} + definition[&"source_id"] = source_id + # An isolated aggregate validates and canonicalizes without touching live + # clocks, RNG, IDs, facilities, events or the queue. Time zero is only the + # canonical probe's timestamp, never the actual incident creation time. + definition.erase(&"event_source") + definition.erase("event_source") + var probe := DefenseService.new() + var valid := probe.create_raid(raid_id, 0, definition) + if not valid.is_success(): + return {&"ok": false, &"error": valid.message, &"details": valid.details} + var canonical: Dictionary = (probe.create_snapshot()[&"raids"] as Array)[0] + var digest := JSON.stringify(canonical, "", true, true).sha256_text() + var canonical_raid := probe.get_raid(raid_id) + return {&"ok": true, &"definition": definition, &"digest": digest, &"objective_ids": canonical_raid[&"objective_ids"], &"canonical_raid": canonical_raid} + + +func _apply_event_defense_raid(action: EventAction, context: Dictionary) -> Dictionary: + var request := _build_event_defense_request(action, context) + if not bool(request.get(&"ok", false)): + return request + if bool(request.get(&"is_replay", false)): + return {&"ok": true, &"raid_id": request[&"raid_id"], &"is_replay": true} + var advanced := defense_service.advance_time(_defense_operation_minute()) + if not advanced.is_success(): + return {&"ok": false, &"error": advanced.message} + var created := defense_service.create_raid(request[&"raid_id"], _defense_operation_minute(), request[&"definition"]) + if not created.is_success(): + return {&"ok": false, &"error": created.message} + if not _validate_live_defense_crosslinks(): + return {&"ok": false, &"error": "Configured event threat violates live Defense crosslinks."} + return {&"ok": true, &"raid_id": request[&"raid_id"], &"is_replay": false} + + +## Historical provenance is checked against the saved event definition and +## cause context, without requiring a departed guest or repaid loan to exist. +func _validate_event_defense_crosslinks(events: EventService, defense: DefenseService) -> bool: + for raid_id: StringName in defense.get_raid_ids(): + var raid := defense.get_raid(raid_id) + var source: Dictionary = raid.get(&"event_source", {}) + if source.is_empty(): + continue + var instance_id := StringName(source["event_instance_id"]) + var action_id := StringName(source["event_action_id"]) + var identity := JSON.stringify([String(instance_id), String(action_id)], "", true, true) + if raid_id != StringName("event_defense_raid." + identity.sha256_text()): + return false + var instance := events.get_instance(instance_id) + if instance == null: + return false + var source_value: Variant = instance.context.get(&"threat_source_id", instance.context.get("threat_source_id", null)) + if typeof(source_value) not in [TYPE_STRING, TYPE_STRING_NAME] or StringName(source_value) != raid[&"source_id"]: + return false + var definition := events.get_definition(instance.resolved_event_id) + if definition == null: + return false + var actions: Array[EventAction] = [] + for collection: Array in [definition.automatic_actions, definition.fallback_actions, definition.failure_actions]: + actions.append_array(collection) + for choice: EventChoice in definition.choices: + actions.append_array(choice.actions) + var matches := false + for action: EventAction in actions: + if action.action_id != action_id or action.type != EventAction.Type.CREATE_DEFENSE_RAID: + continue + if String(action.payload.get(&"template_id", action.payload.get("template_id", ""))) != source["template_id"] or String(action.payload.get(&"source_kind", action.payload.get("source_kind", ""))).to_upper() != source["source_kind"]: + continue + var prepared := _prepare_event_threat_template(action, StringName(source_value), raid_id) + if bool(prepared.get(&"ok", false)) and String(prepared[&"digest"]) == source["template_digest"] and _event_threat_template_matches_raid(raid, prepared[&"canonical_raid"]): + matches = true + break + if not matches: + return false + return true + + +## Compare the immutable creation configuration to its original template. +## Combat progress, damage, roster, timestamps and player retreat-policy edits +## remain mutable and must not be compared to a newly-created probe. +func _event_threat_template_matches_raid(raid: Dictionary, canonical: Dictionary) -> bool: + for field: StringName in [&"raid_id", &"source_id", &"max_duration_minutes", &"round_minutes", &"terrain_bonus", &"command_bonus", &"surprise_penalty", &"multiple_entry_penalty", &"entry_points", &"objective_ids"]: + if not _strict_variants_equal(raid.get(field, null), canonical.get(field, null)): + return false + var waves: Array = raid.get(&"waves", []) + var template_waves: Array = canonical.get(&"waves", []) + if waves.size() != template_waves.size(): + return false + for index: int in range(waves.size()): + var wave: Dictionary = waves[index] + var template_wave: Dictionary = template_waves[index] + for field: StringName in [&"wave_id", &"threat_strength", &"retreat_threshold", &"max_rounds", &"entry_points", &"objective_ids"]: + if not _strict_variants_equal(wave.get(field, null), template_wave.get(field, null)): + return false + return true diff --git a/docs/plans/original-completion-2026-10-02/checkpoints/native-hot-loop-r325-r329/023-guest_candidate_batch.inc b/docs/plans/original-completion-2026-10-02/checkpoints/native-hot-loop-r325-r329/023-guest_candidate_batch.inc new file mode 100644 index 0000000..29d7fff --- /dev/null +++ b/docs/plans/original-completion-2026-10-02/checkpoints/native-hot-loop-r325-r329/023-guest_candidate_batch.inc @@ -0,0 +1,395 @@ +private: + // This scope owns names only for one call. It retains no guest, candidate, + // callback, result, cache or saved simulation state across calls. + struct GuestBatchKey { + const StringName name; + const String text; + explicit GuestBatchKey(const char *s):name(s,false),text(name) {} + }; + // Immutable key objects belong to one public batch call only. No guest, + // dictionary, callback, cached value or name survives that call. + struct GuestBatchNames { + const GuestBatchKey k_empty{""}; + const GuestBatchKey k_ADULT_NOT_VERIFIED{"ADULT_NOT_VERIFIED"}; + const GuestBatchKey k_CANNOT_PAY{"CANNOT_PAY"}; + const GuestBatchKey k_CONTENT_FILTERED{"CONTENT_FILTERED"}; + const GuestBatchKey k_ETA_AFTER_EXIT{"ETA_AFTER_EXIT"}; + const GuestBatchKey k_FACILITY_CLOSED{"FACILITY_CLOSED"}; + const GuestBatchKey k_FORBIDDEN_GUEST_TAG{"FORBIDDEN_GUEST_TAG"}; + const GuestBatchKey k_GUEST_TYPE_NOT_ALLOWED{"GUEST_TYPE_NOT_ALLOWED"}; + const GuestBatchKey k_ILLEGALITY_REFUSED{"ILLEGALITY_REFUSED"}; + const GuestBatchKey k_INVALID_NEED_MAP{"INVALID_NEED_MAP"}; + const GuestBatchKey k_MISSING_INPUTS{"MISSING_INPUTS"}; + const GuestBatchKey k_MISSING_PERMISSION{"MISSING_PERMISSION"}; + const GuestBatchKey k_QUALITY_TOO_LOW{"QUALITY_TOO_LOW"}; + const GuestBatchKey k_RISK_REFUSED{"RISK_REFUSED"}; + const GuestBatchKey k_UNREACHABLE{"UNREACHABLE"}; + const GuestBatchKey k_WAIT_HARD_LIMIT{"WAIT_HARD_LIMIT"}; + const GuestBatchKey k_absolute_minute{"absolute_minute"}; + const GuestBatchKey k_accepted{"accepted"}; + const GuestBatchKey k_addiction_by_tag{"addiction_by_tag"}; + const GuestBatchKey k_adult{"adult"}; + const GuestBatchKey k_adult_content_enabled{"adult_content_enabled"}; + const GuestBatchKey k_adult_only{"adult_only"}; + const GuestBatchKey k_adult_verified{"adult_verified"}; + const GuestBatchKey k_allow_credit{"allow_credit"}; + const GuestBatchKey k_allowed_guest_types{"allowed_guest_types"}; + const GuestBatchKey k_credit_allowed{"credit_allowed"}; + const GuestBatchKey k_credit_limit_gold{"credit_limit_gold"}; + const GuestBatchKey k_current_minute{"current_minute"}; + const GuestBatchKey k_debt_gold{"debt_gold"}; + const GuestBatchKey k_discretionary{"discretionary"}; + const GuestBatchKey k_entertainment{"entertainment"}; + const GuestBatchKey k_estimated_total_gold{"estimated_total_gold"}; + const GuestBatchKey k_exit_reason{"exit_reason"}; + const GuestBatchKey k_exit_threshold{"exit_threshold"}; + const GuestBatchKey k_expected_quality{"expected_quality"}; + const GuestBatchKey k_expected_wait{"expected_wait"}; + const GuestBatchKey k_facility_id{"facility_id"}; + const GuestBatchKey k_facility_tags{"facility_tags"}; + const GuestBatchKey k_forbidden_guest_tags{"forbidden_guest_tags"}; + const GuestBatchKey k_frugal{"frugal"}; + const GuestBatchKey k_gambling{"gambling"}; + const GuestBatchKey k_get_definition{"get_definition"}; + const GuestBatchKey k_get_guest{"get_guest"}; + const GuestBatchKey k_group_fit{"group_fit"}; + const GuestBatchKey k_guest_type_id{"guest_type_id"}; + const GuestBatchKey k_hunger{"hunger"}; + const GuestBatchKey k_hygiene{"hygiene"}; + const GuestBatchKey k_illegal_level{"illegal_level"}; + const GuestBatchKey k_inputs_available{"inputs_available"}; + const GuestBatchKey k_instance_id{"instance_id"}; + const GuestBatchKey k_intimacy{"intimacy"}; + const GuestBatchKey k_is_open{"is_open"}; + const GuestBatchKey k_legal_permission_override{"legal_permission_override"}; + const GuestBatchKey k_legal_sensitivity{"legal_sensitivity"}; + const GuestBatchKey k_leisure{"leisure"}; + const GuestBatchKey k_medical{"medical"}; + const GuestBatchKey k_minimum_quality{"minimum_quality"}; + const GuestBatchKey k_need_effects{"need_effects"}; + const GuestBatchKey k_need_priority{"need_priority"}; + const GuestBatchKey k_need_relief{"need_relief"}; + const GuestBatchKey k_needs{"needs"}; + const GuestBatchKey k_novelty{"novelty"}; + const GuestBatchKey k_ok{"ok"}; + const GuestBatchKey k_open{"open"}; + const GuestBatchKey k_open_at_arrival{"open_at_arrival"}; + const GuestBatchKey k_ordered{"ordered"}; + const GuestBatchKey k_planned_exit_at{"planned_exit_at"}; + const GuestBatchKey k_preference{"preference"}; + const GuestBatchKey k_preference_weights{"preference_weights"}; + const GuestBatchKey k_price_burden{"price_burden"}; + const GuestBatchKey k_price_gold{"price_gold"}; + const GuestBatchKey k_quality{"quality"}; + const GuestBatchKey k_quoted_price{"quoted_price"}; + const GuestBatchKey k_ranked{"ranked"}; + const GuestBatchKey k_reachable{"reachable"}; + const GuestBatchKey k_reason{"reason"}; + const GuestBatchKey k_rejected{"rejected"}; + const GuestBatchKey k_repetition_penalty{"repetition_penalty"}; + const GuestBatchKey k_required_guest_tags{"required_guest_tags"}; + const GuestBatchKey k_requires_adult_verified{"requires_adult_verified"}; + const GuestBatchKey k_respect_survival_reserve{"respect_survival_reserve"}; + const GuestBatchKey k_risk{"risk"}; + const GuestBatchKey k_risk_level{"risk_level"}; + const GuestBatchKey k_risk_sensitivity{"risk_sensitivity"}; + const GuestBatchKey k_risk_tolerance{"risk_tolerance"}; + const GuestBatchKey k_service_category{"service_category"}; + const GuestBatchKey k_sleep{"sleep"}; + const GuestBatchKey k_spendthrift{"spendthrift"}; + const GuestBatchKey k_supported{"supported"}; + const GuestBatchKey k_tags{"tags"}; + const GuestBatchKey k_tourism{"tourism"}; + const GuestBatchKey k_travel_minutes{"travel_minutes"}; + const GuestBatchKey k_urgency{"urgency"}; + const GuestBatchKey k_utility{"utility"}; + const GuestBatchKey k_utility_terms{"utility_terms"}; + const GuestBatchKey k_visited_facility_ids{"visited_facility_ids"}; + const GuestBatchKey k_wait_hard_limit{"wait_hard_limit"}; + const GuestBatchKey k_wait_minutes{"wait_minutes"}; + const GuestBatchKey k_wallet_gold{"wallet_gold"}; + Variant value(const Dictionary &d,const GuestBatchKey &k,const Variant &fallback=Variant()) { + if(d.has(k.name)) return d[k.name]; + return d.has(k.text) ? d[k.text] : fallback; + } + bool has(const Dictionary &d,const GuestBatchKey &k) { return d.has(k.name) || d.has(k.text); } + int64_t integer(const Dictionary &d,const GuestBatchKey &k,int64_t fallback=0) { return value(d,k,fallback); } + bool flag(const Dictionary &d,const GuestBatchKey &k,bool fallback=false) { + const Variant v=value(d,k,fallback); + return v.get_type()==Variant::INT ? int64_t(v)!=0 : bool(v); + } + }; + struct GuestBatchRow { + Dictionary candidate,relief; + StringName id; + String id_text; + bool need_map_valid=true; + int64_t priority=0,utility=0,urgency=0,travel=0; + }; + struct GuestBatchFacilityOrder { + bool operator()(const GuestBatchRow &a,const GuestBatchRow &b) const { return a.id_text < b.id_text; } + }; + struct GuestBatchRankOrder { + bool operator()(const GuestBatchRow &a,const GuestBatchRow &b) const { + if(a.priority!=b.priority) return a.priority>b.priority; + if(a.utility!=b.utility) return a.utility>b.utility; + if(a.urgency!=b.urgency) return a.urgency>b.urgency; + if(a.travel!=b.travel) return a.travel(std::max(x,low),high); + } + static bool guest_name_value(const Variant &v) { + return v.get_type()==Variant::STRING || v.get_type()==Variant::STRING_NAME; + } + static bool guest_names_supported(const Variant &v) { + // The original name helper ignores non-arrays, but converting unusual + // array elements is deliberately left to the original GDScript path. + if(v.get_type()!=Variant::ARRAY) return true; + const Array a=v; + for(int64_t i=0;i ordered; + for(int64_t i=0;i sorter; + if(!ordered.empty()) sorter.sort(ordered.data(),ordered.size()); + out.clear();for(const String &s:ordered) out.append(StringName(s)); + return out; + } + static bool guest_normalize_need(GuestBatchNames &n,const Variant &v,Dictionary &out) { + if(v.get_type()!=Variant::DICTIONARY) return false; + const Dictionary source=v; + if(source.size()>6) return false; + const Array keys=source.keys(); + for(int64_t i=0;iname) {valid=true;break;} + if(!valid || out.has(k) || source[keys[i]].get_type()!=Variant::INT) return false; + out[k]=source[keys[i]]; + } + return true; + } + static bool guest_candidate_supported(GuestBatchNames &n,const Dictionary &d) { + // Adding canonical/result StringName keys to a typed dictionary can + // invoke conversion/error semantics; that entire domain stays original. + if(d.is_typed()) return false; + for(const GuestBatchKey *s: {&n.k_travel_minutes,&n.k_wait_minutes,&n.k_expected_wait,&n.k_wait_hard_limit,&n.k_risk_level,&n.k_risk,&n.k_illegal_level,&n.k_expected_quality,&n.k_quality,&n.k_minimum_quality,&n.k_estimated_total_gold,&n.k_price_gold,&n.k_quoted_price,&n.k_need_priority,&n.k_preference,&n.k_novelty,&n.k_group_fit,&n.k_price_burden,&n.k_risk_sensitivity,&n.k_repetition_penalty,&n.k_urgency}) + if(n.has(d,*s) && n.value(d,*s).get_type()!=Variant::INT) return false; + for(const GuestBatchKey *s: {&n.k_inputs_available,&n.k_adult_only,&n.k_requires_adult_verified,&n.k_is_open,&n.k_open,&n.k_open_at_arrival,&n.k_reachable,&n.k_credit_allowed,&n.k_allow_credit,&n.k_discretionary,&n.k_respect_survival_reserve,&n.k_legal_permission_override,&n.k_ok}) { + if(!n.has(d,*s)) continue; + const Variant::Type t=n.value(d,*s).get_type(); + if(t!=Variant::BOOL && t!=Variant::INT) return false; + } + for(const GuestBatchKey *s: {&n.k_facility_id,&n.k_instance_id,&n.k_service_category}) if(n.has(d,*s) && !guest_name_value(n.value(d,*s))) return false; + for(const GuestBatchKey *s: {&n.k_required_guest_tags,&n.k_forbidden_guest_tags,&n.k_allowed_guest_types,&n.k_facility_tags,&n.k_tags}) if(!guest_names_supported(n.value(d,*s,Array()))) return false; + return true; + } + static int64_t guest_price(GuestBatchNames &n,const Dictionary &c) { + return std::max(int64_t(n.value(c,n.k_estimated_total_gold,n.value(c,n.k_price_gold,n.value(c,n.k_quoted_price,int64_t(0))))),0); + } + static bool guest_discretionary(GuestBatchNames &n,const Dictionary &c) { + if(n.flag(c,n.k_discretionary)) return true; + const StringName category=n.value(c,n.k_service_category,n.k_empty.name); + for(const GuestBatchKey *s: {&n.k_leisure,&n.k_entertainment,&n.k_gambling,&n.k_intimacy,&n.k_tourism}) if(category==s->name) return true; + const Array tags=guest_names(n.value(c,n.k_facility_tags,n.value(c,n.k_tags,Array()))); + for(const GuestBatchKey *s: {&n.k_leisure,&n.k_entertainment,&n.k_gambling,&n.k_adult,&n.k_intimacy}) if(tags.has(s->name)) return true; + return false; + } + static StringName guest_rejection(GuestBatchNames &n,const GuestBatchRow &row, + const Dictionary &context,const StringName &type,const Array &tags,bool adult, + int64_t planned_exit,int64_t decision_minute,int64_t cash,int64_t credit, + int64_t reserve,int64_t tolerance,int64_t legal,int64_t minimum) { + const Dictionary &c=row.candidate; + if(!row.need_map_valid) return n.k_INVALID_NEED_MAP.name; + if(!n.flag(c,n.k_inputs_available,true)) return n.k_MISSING_INPUTS.name; + if(n.flag(c,n.k_adult_only,n.flag(c,n.k_requires_adult_verified))) { + if(!adult) return n.k_ADULT_NOT_VERIFIED.name; + if(!n.flag(context,n.k_adult_content_enabled,true)) return n.k_CONTENT_FILTERED.name; + } + const Array required=guest_names(n.value(c,n.k_required_guest_tags,Array())); + for(int64_t i=0;i(n.integer(c,n.k_travel_minutes),0); + if(decision_minute>=0 && the_tree_native::signed_add(decision_minute,travel)>planned_exit) return n.k_ETA_AFTER_EXIT.name; + const int64_t price=guest_price(n,c); + const int64_t available=n.flag(c,n.k_respect_survival_reserve,guest_discretionary(n,c)) ? std::max(guest_sub(cash,reserve),0) : cash; + if(price>available && (!n.flag(c,n.k_credit_allowed,n.flag(c,n.k_allow_credit)) || guest_sub(price,available)>credit)) return n.k_CANNOT_PAY.name; + const int64_t wait=std::max(int64_t(n.value(c,n.k_wait_minutes,n.value(c,n.k_expected_wait,int64_t(0)))),0); + const int64_t hard=std::max(int64_t(n.value(c,n.k_wait_hard_limit,n.value(context,n.k_wait_hard_limit,int64_t(120)))),0); + if(wait>=hard) return n.k_WAIT_HARD_LIMIT.name; + const int64_t risk=guest_clamp(int64_t(n.value(c,n.k_risk_level,n.value(c,n.k_risk,int64_t(0)))),0,100); + if(risk>tolerance) return n.k_RISK_REFUSED.name; + if(guest_clamp(n.integer(c,n.k_illegal_level),0,100)>guest_sub(100,legal) && !n.flag(c,n.k_legal_permission_override)) return n.k_ILLEGALITY_REFUSED.name; + const int64_t quality=guest_clamp(int64_t(n.value(c,n.k_expected_quality,n.value(c,n.k_quality,int64_t(0)))),0,100); + if(quality(int64_t(raw_relief),0); + else if(raw_relief.get_type()==Variant::DICTIONARY) { + for(const GuestBatchKey *s: {&n.k_hunger,&n.k_sleep,&n.k_hygiene,&n.k_medical,&n.k_leisure,&n.k_intimacy}) { + const int64_t effect=row.relief.get(s->name,int64_t(0)); + const int64_t need=needs[s->name]; + relief=std::max(relief,signed_product(std::max(effect,0),need)/100); + if(effect>0) { + default_urgency=std::max(default_urgency,need); + if(s->name==n.k_hunger.name || s->name==n.k_sleep.name || s->name==n.k_leisure.name || s->name==n.k_intimacy.name) priority=std::max(priority,need); + } + } + } + int64_t default_preference=0; + const Array facility_tags=guest_names(n.value(c,n.k_facility_tags,n.value(c,n.k_tags,Array()))); + for(int64_t i=0;i(the_tree_native::signed_add(cash,credit),1); + const int64_t numerator=guest_sub(the_tree_native::signed_add(signed_product(guest_price(n,c),100),power),1); + const int64_t burden=n.integer(c,n.k_price_burden,guest_clamp(numerator/power,0,100)); + const int64_t travel=std::max(n.integer(c,n.k_travel_minutes),0); + const int64_t wait=std::max(int64_t(n.value(c,n.k_wait_minutes,n.value(c,n.k_expected_wait,int64_t(0)))),0); + const int64_t risk=std::max(int64_t(n.value(c,n.k_risk,n.value(c,n.k_risk_level,int64_t(0)))),0); + const int64_t sensitivity=std::max(n.integer(c,n.k_risk_sensitivity,the_tree_native::signed_add(guest_sub(100,tolerance),24)/25),0); + const int64_t repetition=n.integer(c,n.k_repetition_penalty,seen?10:0); + int64_t utility=signed_product(relief,8); + utility=the_tree_native::signed_add(utility,signed_product(preference,5)); + utility=the_tree_native::signed_add(utility,signed_product(quality,3)); + utility=the_tree_native::signed_add(utility,signed_product(novelty,2)); + utility=the_tree_native::signed_add(utility,signed_product(group,2)); + utility=guest_sub(utility,signed_product(burden,5)); + utility=guest_sub(utility,signed_product(travel,2)); + utility=guest_sub(utility,signed_product(wait,3)); + utility=guest_sub(utility,signed_product(risk,sensitivity)); + utility=guest_sub(utility,repetition); + Dictionary result=c.duplicate(true); + result[n.k_facility_id.name]=row.id;result[n.k_need_priority.name]=priority; + result[n.k_utility.name]=utility;result[n.k_urgency.name]=n.integer(c,n.k_urgency,default_urgency);result[n.k_travel_minutes.name]=travel; + Dictionary terms; + terms[n.k_need_relief.name]=relief;terms[n.k_preference.name]=preference;terms[n.k_expected_quality.name]=quality; + terms[n.k_novelty.name]=novelty;terms[n.k_group_fit.name]=group;terms[n.k_price_burden.name]=burden; + terms[n.k_travel_minutes.name]=travel;terms[n.k_wait_minutes.name]=wait;terms[n.k_risk.name]=risk; + terms[n.k_risk_sensitivity.name]=sensitivity;terms[n.k_repetition_penalty.name]=repetition; + result[n.k_utility_terms.name]=terms; + row.candidate=result;row.priority=priority;row.utility=utility;row.urgency=result[n.k_urgency.name];row.travel=travel; + } +public: + Dictionary guest_candidate_batch(Object *owner,const StringName &guest_id,const Array &facilities,const Dictionary &context) { + // No owner/input mutation occurs in the entire implementation. A failed + // new standard-container allocation cannot escape the native boundary + // or publish a partially built result. Engine-internal allocation failure + // is not claimed to be covered by these standard C++ exception catches. + Dictionary unsupported;unsupported[StringName("supported",false)]=false; + try { return guest_candidate_batch_impl(owner,guest_id,facilities,context); } + catch(const std::bad_alloc &) { return unsupported; } + catch(const std::length_error &) { return unsupported; } + } +private: + Dictionary guest_candidate_batch_impl(Object *owner,const StringName &guest_id,const Array &facilities,const Dictionary &context) { + GuestBatchNames n;const ProjectionNames p; + Dictionary unsupported;unsupported[n.k_supported.name]=false; + if(!builtin(p,owner,"res://scripts/guest/guest_service.gd")) return unsupported; + // No caller callback is invoked during preflight or computation. Callback + // contexts use original extraction/evaluation/decision in their entirety. + const Array context_keys=context.keys(); + for(int64_t i=0;icall(n.k_get_guest.name,guest_id); + if(!builtin(p,guest,"res://scripts/guest/guest_runtime.gd")) return unsupported; + const Variant typev=guest->get(n.k_guest_type_id.name); + if(!guest_name_value(typev)) return unsupported; + const StringName type=typev; + Object *definition=owner->call(n.k_get_definition.name,type); + if(!builtin(p,definition,"res://scripts/guest/guest_definition.gd")) return unsupported; + for(const GuestBatchKey *s: {&n.k_wallet_gold,&n.k_credit_limit_gold,&n.k_debt_gold,&n.k_planned_exit_at}) if(guest->get(s->name).get_type()!=Variant::INT) return unsupported; + for(const GuestBatchKey *s: {&n.k_risk_tolerance,&n.k_legal_sensitivity,&n.k_minimum_quality}) if(definition->get(s->name).get_type()!=Variant::INT) return unsupported; + if(definition->get(n.k_adult_verified.name).get_type()!=Variant::BOOL || owner->get(n.k_current_minute.name).get_type()!=Variant::INT) return unsupported; + if(guest->get(n.k_needs.name).get_type()!=Variant::DICTIONARY || guest->get(n.k_addiction_by_tag.name).get_type()!=Variant::DICTIONARY || guest->get(n.k_visited_facility_ids.name).get_type()!=Variant::ARRAY || definition->get(n.k_tags.name).get_type()!=Variant::ARRAY || definition->get(n.k_preference_weights.name).get_type()!=Variant::DICTIONARY) return unsupported; + const Dictionary needs=guest->get(n.k_needs.name),addiction=guest->get(n.k_addiction_by_tag.name),weights=definition->get(n.k_preference_weights.name); + const Array visited=guest->get(n.k_visited_facility_ids.name),tags=definition->get(n.k_tags.name); + if(!guest_names_supported(visited) || !guest_names_supported(tags)) return unsupported; + for(const GuestBatchKey *s: {&n.k_hunger,&n.k_sleep,&n.k_hygiene,&n.k_medical,&n.k_leisure,&n.k_intimacy}) if(!needs.has(s->name) || needs[s->name].get_type()!=Variant::INT) return unsupported; + const Array addiction_values=addiction.values(),weight_values=weights.values(); + int64_t max_addiction=0; + for(int64_t i=0;i(max_addiction,int64_t(addiction_values[i]));} + for(int64_t i=0;i ordered; + for(int64_t i=0;iname==n.k_need_relief.name && v.get_type()==Variant::INT) continue; + Dictionary normalized; + if(!guest_normalize_need(n,v,normalized)) {row.need_map_valid=false;break;} + if(s->name==n.k_need_relief.name) row.relief=normalized; + } + ordered.push_back(row); + } + SortArray facility_sort; + if(!ordered.empty()) facility_sort.sort(ordered.data(),ordered.size()); + const int64_t cash=guest->get(n.k_wallet_gold.name); + const int64_t credit=std::max(guest_sub(guest->get(n.k_credit_limit_gold.name),guest->get(n.k_debt_gold.name)),0); + int64_t reserve=0; + if(max_addiction<75) reserve=tags.has(n.k_spendthrift.name) ? cash/10 : (tags.has(n.k_frugal.name) ? cash/2 : signed_product(cash,3)/10); + const int64_t tolerance=definition->get(n.k_risk_tolerance.name); + const int64_t minute=n.integer(context,n.k_absolute_minute,owner->get(n.k_current_minute.name)); + TypedArray raw_ordered,accepted,rejected,ranked; + std::vector rank_rows; + for(GuestBatchRow &row:ordered) { + raw_ordered.append(row.candidate); + const StringName reason=guest_rejection(n,row,context,type,tags,definition->get(n.k_adult_verified.name),guest->get(n.k_planned_exit_at.name),minute,cash,credit,reserve,tolerance,definition->get(n.k_legal_sensitivity.name),definition->get(n.k_minimum_quality.name)); + if(!reason.is_empty()) {Dictionary failure;failure[n.k_facility_id.name]=row.id_text;failure[n.k_reason.name]=String(reason);rejected.append(failure);continue;} + accepted.append(row.candidate); + guest_evaluate(n,row,needs,weights,visited,cash,credit,tolerance); + if(n.flag(row.candidate,n.k_ok,true)) rank_rows.push_back(row); + } + SortArray rank_sort; + if(!rank_rows.empty()) rank_sort.sort(rank_rows.data(),rank_rows.size()); + for(const GuestBatchRow &row:rank_rows) ranked.append(row.candidate); + Dictionary result;result[n.k_supported.name]=true;result[n.k_ordered.name]=raw_ordered; + result[n.k_accepted.name]=accepted;result[n.k_rejected.name]=rejected;result[n.k_ranked.name]=ranked; + return result; + } + +public: diff --git a/docs/plans/original-completion-2026-10-02/checkpoints/native-hot-loop-r325-r329/024-tree_core.hpp b/docs/plans/original-completion-2026-10-02/checkpoints/native-hot-loop-r325-r329/024-tree_core.hpp new file mode 100644 index 0000000..ea7ebd9 --- /dev/null +++ b/docs/plans/original-completion-2026-10-02/checkpoints/native-hot-loop-r325-r329/024-tree_core.hpp @@ -0,0 +1,78 @@ +#pragma once +// Standalone numeric prototype; not connected to Godot or production. +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +namespace the_tree_native { +using I = std::int64_t; +struct Edge { I neighbor, cost; }; +struct Result { + std::string status = "usable"; + std::vector costs, parents, discovered, parent_discovered, settled; +}; +I signed_add(I a, I b) { + const auto sum = static_cast(a) + static_cast(b); + if (sum <= static_cast(std::numeric_limits::max())) return static_cast(sum); + return std::numeric_limits::min() + static_cast(sum - (std::uint64_t(1) << 63)); +} +template +Result search(I n, I start, const std::vector& goals, EdgeProvider&& edge_provider) { + Result r; + if (start < 0 || start >= n) { r.status = "unusable"; return r; } + r.costs.assign(n, std::numeric_limits::max()); + r.parents.assign(n, -1); + std::vector finalized(n, 0), pending(n, 0); + bool unknown_goal = false; + I remaining = 0; + for (I goal : goals) { + if (goal < 0 || goal >= n) { unknown_goal = true; continue; } + if (!pending[goal]) { pending[goal] = 1; ++remaining; } + } + r.costs[start] = 0; r.discovered.push_back(start); + // Positive edges cannot enqueue another node at this entry cost. + // Lexicographic (cost,index) ordering preserves the old sorted cost buckets. + using Entry=std::pair; + std::priority_queue,std::greater> frontier; + frontier.emplace(0,start); + while (!frontier.empty()) { + const auto entry=frontier.top();frontier.pop(); + const I entry_cost=entry.first,current=entry.second; + { + if (finalized[current] || entry_cost != r.costs[current]) continue; + finalized[current] = 1; + r.settled.push_back(current); + if (pending[current]) { pending[current] = 0; --remaining; } + if (!remaining && !unknown_goal) return r; + for (const Edge& edge : edge_provider(current)) { + if (edge.cost <= 0) { r.status = "unusable"; return r; } + const I neighbor = edge.neighbor; + if (neighbor < 0 || neighbor >= n) { r.status = "unusable"; return r; } + const I cost = signed_add(r.costs[current], edge.cost); + if (cost < r.costs[current]) { r.status = "wrapped"; return r; } + const I known = r.costs[neighbor]; + const bool better_parent = cost == known && r.parents[neighbor] >= 0 && current < r.parents[neighbor]; + if (cost > known || (cost == known && !better_parent)) continue; + if (r.parents[neighbor] < 0) { + r.parent_discovered.push_back(neighbor); + if (neighbor != start) r.discovered.push_back(neighbor); + } + r.parents[neighbor] = current; + r.costs[neighbor] = cost; + if (finalized[neighbor]) r.settled.erase(std::remove(r.settled.begin(), r.settled.end(), neighbor), r.settled.end()); + finalized[neighbor] = 0; + frontier.emplace(cost,neighbor); + } + } + } + return r; +} +} // namespace the_tree_native diff --git a/docs/plans/original-completion-2026-10-02/checkpoints/native-hot-loop-r325-r329/025-tree_numeric_core.cpp b/docs/plans/original-completion-2026-10-02/checkpoints/native-hot-loop-r325-r329/025-tree_numeric_core.cpp new file mode 100644 index 0000000..8a16025 --- /dev/null +++ b/docs/plans/original-completion-2026-10-02/checkpoints/native-hot-loop-r325-r329/025-tree_numeric_core.cpp @@ -0,0 +1,862 @@ +// Isolated, uncompiled bridge prototype. No production registration or integration yet. +#include "tree_core.hpp" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +using namespace godot; +using the_tree_native::I; +using the_tree_native::Edge; + +class TheTreeNumericCore : public RefCounted { + GDCLASS(TheTreeNumericCore, RefCounted) +protected: + static void _bind_methods() { + ClassDB::bind_method(D_METHOD("performance_record_sample", "owner", "metric_id", "duration_us"), &TheTreeNumericCore::performance_record_sample); + ClassDB::bind_method(D_METHOD("guest_candidate_batch", "owner", "guest_id", "facilities", "context"), &TheTreeNumericCore::guest_candidate_batch); + ClassDB::bind_method(D_METHOD("guest_tree_edge_rows", "path", "current", "layers", "access"), &TheTreeNumericCore::guest_tree_edge_rows); + ClassDB::bind_method(D_METHOD("search", "cells", "indices", "start_index", "goals", "edge_provider"), &TheTreeNumericCore::search); + ClassDB::bind_method(D_METHOD("search_cached", "cells", "indices", "start_index", "goals", "edge_provider"), &TheTreeNumericCore::search_cached); + ClassDB::bind_method(D_METHOD("clear_edge_cache"), &TheTreeNumericCore::clear_edge_cache); + ClassDB::bind_method(D_METHOD("nearest_guest_access", "path", "start", "goals", "fallback_target", "layers", "access", "ticks"), &TheTreeNumericCore::nearest_guest_access); + ClassDB::bind_method(D_METHOD("project_guest_candidates", "runtime", "guest_id", "open_state", "degraded_state", "movement_scale", "stair_credit", "orthogonal_cost", "ticks"), &TheTreeNumericCore::project_guest_candidates); + } +private: + // Cache belongs to this instance and one caller-owned graph workspace. + // No reuse after cells, indices, filters or edge values change: the caller + // must clear it or release this core. The original search never uses it. + std::map> cached_edges; + // Per-public-call immutable names. No static/global lifetime, cached + // object values, script dispatch result or profile; recursive calls own + // separate scopes. Destruction precedes leaving the engine call. + struct ProjectionNames { + const StringName k_ACCESS_LAYER_DENIED{"ACCESS_LAYER_DENIED",false}; + const StringName k_CONNECTOR{"CONNECTOR",false}; + const StringName k_GOAL_NOT_PASSABLE{"GOAL_NOT_PASSABLE",false}; + const StringName k_GOAL_TEMPORARILY_BLOCKED{"GOAL_TEMPORARILY_BLOCKED",false}; + const StringName k_GOAL_UNREACHABLE{"GOAL_UNREACHABLE",false}; + const StringName k_OK{"OK",false}; + const StringName k_START_NOT_PASSABLE{"START_NOT_PASSABLE",false}; + const StringName k_START_TEMPORARILY_BLOCKED{"START_TEMPORARILY_BLOCKED",false}; + const StringName k__build_guest_query_tree_compact{"_build_guest_query_tree_compact",false}; + const StringName k__cache_key{"_cache_key",false}; + const StringName k__cell_traffic{"_cell_traffic",false}; + const StringName k__copy_path{"_copy_path",false}; + const StringName k__diagnose_no_route_reason{"_diagnose_no_route_reason",false}; + const StringName k__find_facility_access_path{"_find_facility_access_path",false}; + const StringName k__find_path_internal{"_find_path_internal",false}; + const StringName k__guest_access_batch_can_select{"_guest_access_batch_can_select",false}; + const StringName k__guest_batch_has_permitted_layer_crossing{"_guest_batch_has_permitted_layer_crossing",false}; + const StringName k__guest_has_checked_in{"_guest_has_checked_in",false}; + const StringName k__guest_step_connector_memo{"_guest_step_connector_memo",false}; + const StringName k__guest_tree_arithmetic_wrapped{"_guest_tree_arithmetic_wrapped",false}; + const StringName k__guest_tree_reconstruct_path{"_guest_tree_reconstruct_path",false}; + const StringName k__need_relief_for_facility_tags{"_need_relief_for_facility_tags",false}; + const StringName k__passable_cells{"_passable_cells",false}; + const StringName k__path_cache{"_path_cache",false}; + const StringName k__query_batch{"_query_batch",false}; + const StringName k__route_failure{"_route_failure",false}; + const StringName k__service_category_for_tags{"_service_category_for_tags",false}; + const StringName k__sorted_name_keys{"_sorted_name_keys",false}; + const StringName k_arithmetic_wrapped{"arithmetic_wrapped",false}; + const StringName k_available_quantity{"available_quantity",false}; + const StringName k_base_quality{"base_quality",false}; + const StringName k_base_service_minutes{"base_service_minutes",false}; + const StringName k_begin_guest_path_query_batch{"begin_guest_path_query_batch",false}; + const StringName k_cache_misses{"cache_misses",false}; + const StringName k_calculate_guest_batch_path_cost{"calculate_guest_batch_path_cost",false}; + const StringName k_candidates{"candidates",false}; + const StringName k_capacity{"capacity",false}; + const StringName k_compact{"compact",false}; + const StringName k_cost{"cost",false}; + const StringName k_cost_values{"cost_values",false}; + const StringName k_costs{"costs",false}; + const StringName k_current_cell{"current_cell",false}; + const StringName k_definition_id{"definition_id",false}; + const StringName k_definition_registry{"definition_registry",false}; + const StringName k_elevators{"elevators",false}; + const StringName k_end_guest_path_query_batch{"end_guest_path_query_batch",false}; + const StringName k_entrance{"entrance",false}; + const StringName k_estimated_total_gold{"estimated_total_gold",false}; + const StringName k_expected_quality{"expected_quality",false}; + const StringName k_expected_wait{"expected_wait",false}; + const StringName k_facility_id{"facility_id",false}; + const StringName k_facility_service{"facility_service",false}; + const StringName k_facility_tags{"facility_tags",false}; + const StringName k_get_connector{"get_connector",false}; + const StringName k_get_facility{"get_facility",false}; + const StringName k_get_facility_business_price{"get_facility_business_price",false}; + const StringName k_get_guest{"get_guest",false}; + const StringName k_get_instances{"get_instances",false}; + const StringName k_get_step_connector{"get_step_connector",false}; + const StringName k_graph_revision{"graph_revision",false}; + const StringName k_group_fit{"group_fit",false}; + const StringName k_guest_service{"guest_service",false}; + const StringName k_illegal_level{"illegal_level",false}; + const StringName k_indices{"indices",false}; + const StringName k_inputs_available{"inputs_available",false}; + const StringName k_instance_id{"instance_id",false}; + const StringName k_inventory_inputs{"inventory_inputs",false}; + const StringName k_inventory_service{"inventory_service",false}; + const StringName k_is_open{"is_open",false}; + const StringName k_last_error{"last_error",false}; + const StringName k_last_no_route_reason{"last_no_route_reason",false}; + const StringName k_last_path_cost{"last_path_cost",false}; + const StringName k_local_inventory_id{"local_inventory_id",false}; + const StringName k_movement_type{"movement_type",false}; + const StringName k_need_relief{"need_relief",false}; + const StringName k_occupied_cells{"occupied_cells",false}; + const StringName k_origin{"origin",false}; + const StringName k_owns_connector{"owns_connector",false}; + const StringName k_path{"path",false}; + const StringName k_path_handle{"path_handle",false}; + const StringName k_path_query_measured{"path_query_measured",false}; + const StringName k_path_service{"path_service",false}; + const StringName k_queue_entries{"queue_entries",false}; + const StringName k_reachable{"reachable",false}; + const StringName k_ready{"ready",false}; + const StringName k_resource_path{"resource_path",false}; + const StringName k_risk_level{"risk_level",false}; + const StringName k_service_category{"service_category",false}; + const StringName k_settled{"settled",false}; + const StringName k_staff_only{"staff_only",false}; + const StringName k_start{"start",false}; + const StringName k_state{"state",false}; + const StringName k_supported{"supported",false}; + const StringName k_tags{"tags",false}; + const StringName k_tagproj_hunger{"hunger",false}; + const StringName k_tagproj_sleep{"sleep",false}; + const StringName k_tagproj_hygiene{"hygiene",false}; + const StringName k_tagproj_medical{"medical",false}; + const StringName k_tagproj_leisure{"leisure",false}; + const StringName k_tagproj_intimacy{"intimacy",false}; + const StringName k_tagproj_dining{"dining",false}; + const StringName k_tagproj_lodging{"lodging",false}; + const StringName k_tagproj_social{"social",false}; + const StringName k_tagproj_bath{"bath",false}; + const StringName k_tagproj_wellness{"wellness",false}; + const StringName k_tagproj_adult_only{"adult_only",false}; + const StringName k_tagproj_restricted_service{"restricted_service",false}; + const StringName k_tagproj_food{"food",false}; + const StringName k_tagproj_service{"service",false}; + const StringName k_target_cell{"target_cell",false}; + const StringName k_targets{"targets",false}; + const StringName k_temporary_block{"temporary_block",false}; + const StringName k_travel_minutes{"travel_minutes",false}; + const StringName k_tree{"tree",false}; + const StringName k_usable{"usable",false}; + }; +public: + // Whole builtin integer record operation. No retained sample values or time + // suppression. Unsupported data returns before any caller-owned mutation. + Dictionary performance_record_sample(Object *owner, const StringName &metric, int64_t duration) { + const StringName supported("supported",false), report_key("report",false); + Dictionary unsupported; unsupported[supported]=false; + Object *script=owner ? owner->get_script() : nullptr; + if (!script || String(script->get(StringName("resource_path",false))) != "res://scripts/core/performance_budget.gd" || metric==StringName() || duration<0) return unsupported; + const StringName samples_key("_samples",false), ordered_key("_ordered_samples",false), bytes_key("_ordered_sample_bytes",false), integer_key("_integer_window_bytes",false), totals_key("_totals",false), counts_key("_sample_counts",false), budgets_key("_budgets",false); + const Variant sv=owner->get(samples_key), ov=owner->get(ordered_key), bv=owner->get(bytes_key), iv=owner->get(integer_key), tv=owner->get(totals_key), cv=owner->get(counts_key), uv=owner->get(budgets_key), capacityv=owner->get(StringName("sample_capacity",false)); + if (sv.get_type()!=Variant::DICTIONARY || ov.get_type()!=Variant::DICTIONARY || bv.get_type()!=Variant::DICTIONARY || iv.get_type()!=Variant::DICTIONARY || tv.get_type()!=Variant::DICTIONARY || cv.get_type()!=Variant::DICTIONARY || uv.get_type()!=Variant::DICTIONARY || capacityv.get_type()!=Variant::INT) return unsupported; + Dictionary samples=sv, orders=ov, rawbytes=bv, integers=iv, totals=tv, counts=cv, budgets=uv; + if (samples.is_read_only() || orders.is_read_only() || rawbytes.is_read_only() || integers.is_read_only() || totals.is_read_only() || counts.is_read_only()) return unsupported; + const std::array maps{samples,orders,rawbytes,integers,totals,counts,budgets}; + for (size_t i=0;i ordered; + Variant savedbytes=rawbytes.get(metric,PackedByteArray()); + bool reuse=existing && orders.has(metric) && savedbytes.get_type()==Variant::PACKED_BYTE_ARRAY && PackedByteArray(savedbytes)==before; + if (reuse) { + Variant orderv=orders[metric]; + if (orderv.get_type()!=Variant::ARRAY) return unsupported; + Array prior=orderv; + if (prior.is_read_only() || !prior.is_typed() || prior.get_typed_builtin()!=Variant::INT || prior.size()!=values.size()) return unsupported; + ordered=prior; + } else { + for (int64_t i=0;iint64_t(capacityv)) { + Variant removed=values.pop_front(); + ordered.remove_at(ordered.bsearch(removed)); + } + PackedByteArray after=UtilityFunctions::var_to_bytes(values); + rawbytes[metric]=after; integers[metric]=after; + const int64_t lifetime_total=the_tree_native::signed_add(int64_t(totalv),duration); + const int64_t lifetime_count=the_tree_native::signed_add(int64_t(countv),1); + totals[metric]=lifetime_total; counts[metric]=lifetime_count; + const int64_t average=lifetime_count>0 ? lifetime_total/lifetime_count : 0; + const int64_t maximum=ordered.is_empty() ? 0 : std::max(0,int64_t(ordered[ordered.size()-1])); + int64_t p95=0; + if (!ordered.is_empty()) { + const int64_t index=std::max(the_tree_native::signed_add(the_tree_native::signed_add(signed_product(95,ordered.size()),99)/100,-1),0); + p95=ordered[std::min(index,ordered.size()-1)]; + } + const int64_t aggregation=aggregationv, limit=limitv; + // Original default match is P95, including unknown aggregation values. + const int64_t measured=aggregation==0 ? average : (aggregation==2 ? maximum : p95); + Dictionary report; + report[StringName("metric_id",false)]=metric; + report[StringName("window_sample_count",false)]=values.size(); + report[StringName("lifetime_sample_count",false)]=lifetime_count; + report[StringName("average_us",false)]=average; + report[StringName("p95_us",false)]=p95; + report[StringName("maximum_us",false)]=maximum; + report[aggregation_key]=aggregation; + report[StringName("measured_us",false)]=measured; + report[limit_key]=limit; + report[StringName("budget_exceeded",false)]=limit>0 && measured>limit; + Dictionary result; result[supported]=true; result[report_key]=report; return result; + } +#include "guest_candidate_batch.inc" + void clear_edge_cache() { cached_edges.clear(); } + Dictionary search(const Array &cells, const Dictionary &indices, I start_index, + const Dictionary &goals, const Callable &edge_provider) { + return search_impl(cells, indices, start_index, goals, edge_provider, false); + } + Dictionary search_cached(const Array &cells, const Dictionary &indices, I start_index, + const Dictionary &goals, const Callable &edge_provider) { + return search_impl(cells, indices, start_index, goals, edge_provider, true); + } + // One ordered projection, with no new persistent ownership/cache. Only + // builtin pure getters and the already guarded sole budget observer qualify. + Dictionary project_guest_candidates(Object *runtime, const StringName &guest_id, + int64_t open_state, int64_t degraded_state, int64_t movement_scale, + int64_t stair_credit, int64_t orthogonal_cost, const Callable &ticks) { + const ProjectionNames n; + Dictionary unsupported; + unsupported[n.k_supported] = false; + if (!builtin(n,runtime, "res://autoload/simulation_runtime.gd") || !ticks.is_valid()) return unsupported; + Object *path = runtime->get(n.k_path_service); + Object *elevators = runtime->get(n.k_elevators); + Object *facilities = runtime->get(n.k_facility_service); + Object *definitions = runtime->get(n.k_definition_registry); + Object *inventory = runtime->get(n.k_inventory_service); + Object *guests = runtime->get(n.k_guest_service); + if (!builtin(n,path,"res://scripts/world/path_service.gd") || + !builtin(n,elevators,"res://scripts/transport/elevator_service.gd") || + !builtin(n,facilities,"res://scripts/facility/facility_service.gd") || + !builtin(n,definitions,"res://scripts/core/definition_registry.gd") || + !builtin(n,inventory,"res://scripts/inventory/inventory_service.gd") || + !builtin(n,guests,"res://scripts/guest/guest_service.gd") || + !static_cast(runtime->call(n.k__guest_access_batch_can_select))) return unsupported; + Object *guest = guests->call(n.k_get_guest,guest_id); + Array candidates; + if (!guest) { + Dictionary result; result[n.k_supported] = true; + result[n.k_candidates] = candidates; return result; + } + if (!builtin(n,guest,"res://scripts/guest/guest_runtime.gd")) return unsupported; + const Array initial = facilities->call(n.k_get_instances); + // Preflight is read-only: default registry/get_instances have no public + // error/counter effects. Custom resources retain the original whole loop. + for (int64_t i=0;icall(n.k_get_facility,facility->get(n.k_definition_id)); + if (definition && !builtin(n,definition,"res://scripts/data/facility_definition.gd")) return unsupported; + } + Array goals; + for (int64_t i=0;iget(n.k_current_cell); + path->call(n.k_begin_guest_path_query_batch,start,goals); + const Array ordered = facilities->call(n.k_get_instances); + TypedArray layers; layers.push_back(0); layers.push_back(1); + TypedArray access_ids; + for (int64_t i=0;icall(n.k_get_facility,facility->get(n.k_definition_id)); + if (!definition) continue; + const Array tags = definition->get(n.k_tags); + if (tags.has(n.k_staff_only)) continue; + if (tags.has(n.k_entrance) && static_cast(runtime->call(n.k__guest_has_checked_in,guest))) continue; + bool has_inputs = true; + const Dictionary inputs = definition->get(n.k_inventory_inputs); + const Array items = runtime->call(n.k__sorted_name_keys,inputs); + for (int64_t j=0;jcall(n.k_available_quantity,facility->get(n.k_local_inventory_id),items[j]); + if (quantity < static_cast(inputs[items[j]])) {has_inputs=false;break;} + } + if (!has_inputs) { + Dictionary row;row[n.k_facility_id]=facility->get(n.k_instance_id); + row[n.k_inputs_available]=false;candidates.push_back(row);continue; + } + const Array destinations = facility_access_cells(n,path,facility); + Dictionary route = nearest_guest_access_impl(n,path,start,destinations,facility->get(n.k_origin),layers,access_ids,ticks); + if (static_cast(route.get(n.k_supported,false))) route.erase(n.k_supported); + else route = Dictionary(runtime->call(n.k__find_facility_access_path,start,facility)); + const Array walk = route.get(n.k_path,Array()); + const bool plain_tag_relief = builtin(n,runtime,"res://autoload/simulation_runtime.gd") && + tags.is_typed() && tags.get_typed_builtin()==Variant::STRING_NAME; + const Dictionary relief = plain_tag_relief ? builtin_tag_relief(n,tags) : + Dictionary(runtime->call(n.k__need_relief_for_facility_tags,tags)); + Dictionary row; + row[n.k_facility_id]=facility->get(n.k_instance_id); + row[n.k_facility_tags]=tags.duplicate(); + const int64_t state = facility->get(n.k_state); + row[n.k_is_open]=(state==open_state || state==degraded_state); + row[n.k_reachable]=route.get(n.k_reachable,false); + row[n.k_travel_minutes]=guest_path_minutes(n,path,elevators,walk,movement_scale,stair_credit,orthogonal_cost); + const Array queue = facility->get(n.k_queue_entries); + const int64_t capacity = facility->get(n.k_capacity); + row[n.k_expected_wait]=signed_product(queue.size(),facility->get(n.k_base_service_minutes))/std::max(capacity,1); + row[n.k_estimated_total_gold]=runtime->call(n.k_get_facility_business_price,facility); + const int64_t quality = facility->get(n.k_base_quality); + row[n.k_expected_quality]=std::max(0,std::min(quality,100)); + row[n.k_need_relief]=relief; + row[n.k_risk_level]=int64_t(0);row[n.k_illegal_level]=int64_t(0); + row[n.k_group_fit]=signed_product(std::min(capacity,4),5); + // Recheck at the original call position: preceding price/observer + // callbacks must retain a replaced/custom runtime's original call. + const bool plain_tag_category = builtin(n,runtime,"res://autoload/simulation_runtime.gd") && + tags.is_typed() && tags.get_typed_builtin()==Variant::STRING_NAME; + row[n.k_service_category]=plain_tag_category ? Variant(builtin_tag_category(n,tags)) : + runtime->call(n.k__service_category_for_tags,tags); + Dictionary handle;handle[n.k_target_cell]=route.get(n.k_target_cell,guest->get(n.k_current_cell)); + handle[n.k_graph_revision]=path->get(n.k_graph_revision); + row[n.k_path_handle]=handle;candidates.push_back(row); + } + path->call(n.k_end_guest_path_query_batch); + Dictionary result;result[n.k_supported]=true;result[n.k_candidates]=candidates;return result; + } +private: + // Direct translation of the two pure builtin tag methods. Fresh result, + // original insertion/branch order, no retained definition/tag memo. + static Dictionary builtin_tag_relief(const ProjectionNames &n,const Array &tags) { + Dictionary relief; + relief[n.k_tagproj_hunger]=int64_t(0);relief[n.k_tagproj_sleep]=int64_t(0); + relief[n.k_tagproj_hygiene]=int64_t(0);relief[n.k_tagproj_medical]=int64_t(0); + relief[n.k_tagproj_leisure]=int64_t(0);relief[n.k_tagproj_intimacy]=int64_t(0); + if(tags.has(n.k_tagproj_dining)) {relief[n.k_tagproj_hunger]=int64_t(80);relief[n.k_tagproj_leisure]=int64_t(10);} + if(tags.has(n.k_tagproj_lodging)) {relief[n.k_tagproj_sleep]=int64_t(90);relief[n.k_tagproj_hygiene]=std::max(int64_t(relief[n.k_tagproj_hygiene]),10);} + if(tags.has(n.k_tagproj_social)||tags.has(n.k_tagproj_leisure)) relief[n.k_tagproj_leisure]=std::max(int64_t(relief[n.k_tagproj_leisure]),40); + if(tags.has(n.k_tagproj_hygiene)||tags.has(n.k_tagproj_bath)) relief[n.k_tagproj_hygiene]=std::max(int64_t(relief[n.k_tagproj_hygiene]),80); + if(tags.has(n.k_tagproj_medical)) relief[n.k_tagproj_medical]=int64_t(90); + else if(tags.has(n.k_tagproj_wellness)) relief[n.k_tagproj_medical]=std::max(int64_t(relief[n.k_tagproj_medical]),20); + if(tags.has(n.k_tagproj_adult_only)||tags.has(n.k_tagproj_restricted_service)) relief[n.k_tagproj_intimacy]=int64_t(80); + return relief; + } + static StringName builtin_tag_category(const ProjectionNames &n,const Array &tags) { + if(tags.has(n.k_tagproj_dining))return n.k_tagproj_food; + if(tags.has(n.k_tagproj_lodging))return n.k_tagproj_lodging; + if(tags.has(n.k_tagproj_social))return n.k_tagproj_leisure; + return n.k_tagproj_service; + } + static bool builtin(const ProjectionNames &n,Object *object,const char *resource) { + if (!object) return false;Object *script=object->get_script(); + return script && String(script->get(n.k_resource_path))==resource; + } + // Exact builtin cache-key spelling. No normalized/reordered profile and no + // persistent cache; String lengths count Unicode codepoints as GDScript does. + static String cache_key_suffix(const ProjectionNames &n,Object *path, const TypedArray &layers, + const TypedArray &access) { + String layer_text, access_text; + for (int64_t i=0; i(layers[i])); + } + for (int64_t i=0; i(access[i])); + access_text += String::num_int64(value.length()) + ":" + value; + } + return String("|L:") + layer_text + "|A:" + access_text + "|R:" + + String::num_int64(static_cast(path->get(n.k_graph_revision))); + } + static String cache_key_with_suffix(const Vector3i &start, const Vector3i &goal, + const String &suffix) { + return String::num_int64(start.x) + "," + String::num_int64(start.y) + "," + + String::num_int64(start.z) + ">" + String::num_int64(goal.x) + "," + + String::num_int64(goal.y) + "," + String::num_int64(goal.z) + suffix; + } + static int64_t signed_product(int64_t a,int64_t b) { + const uint64_t product=static_cast(a)*static_cast(b); + if (product<=static_cast(std::numeric_limits::max())) return static_cast(product); + return std::numeric_limits::min()+static_cast(product-(uint64_t(1)<<63)); + } + static Array facility_access_cells(const ProjectionNames &n,Object *path,Object *facility) { + const Dictionary passable=path->get(n.k__passable_cells); + Array occupied=Array(facility->get(n.k_occupied_cells)).duplicate(); + if (occupied.is_empty()) occupied.push_back(facility->get(n.k_origin)); + Dictionary unique; + for (int64_t i=0;iget(n.k_origin);if(passable.has(origin)) unique[origin]=true; + const Array keys=unique.keys();std::vector sorted;sorted.reserve(keys.size()); + for (int64_t i=0;iget(n.k__guest_step_connector_memo); + // Route-local only. Reuse an ownership result only after an actual + // BOOL return and a typed cached step; no script callback occurs on a + // repeated typed memo hit. A miss/conversion/custom script keeps calls. + const bool builtin_pair = builtin(n,path,"res://scripts/world/path_service.gd") && + builtin(n,elevators,"res://scripts/transport/elevator_service.gd"); + bool ownership_valid = false; + bool ownership_value = false; + StringName ownership_connector; + for(int64_t i=1;i access;connector=path->call(n.k_get_step_connector,from,to,access);} + bool special; + if (builtin_pair && typed_memo_hit && ownership_valid && connector == ownership_connector) { + special = ownership_value; + } else { + const Variant owned = elevators->call(n.k_owns_connector,connector); + special = static_cast(owned); + ownership_valid = builtin_pair && typed_memo_hit && owned.get_type() == Variant::BOOL; + ownership_value = special; + ownership_connector = connector; + } + if(!special && String(connector).begins_with("infra.connector:") && from.x==to.x && from.z==to.z && + std::abs(int64_t(from.y)-int64_t(to.y))==1) { + const Dictionary detail=path->call(n.k_get_connector,connector); + special=detail.get(n.k_movement_type,StringName())==Variant(n.k_CONNECTOR) && + static_cast(detail.get(n.k_cost,int64_t(0)))==150 && stair_credit>movement_scale; + } + if(special) { + const int64_t cost=path->call(n.k_calculate_guest_batch_path_cost,route[0],route[route.size()-1],route); + return static_cast(std::ceil(static_cast(cost)/static_cast(orthogonal_cost))); + } + } + return std::max(route.size()-1,0); + } +public: + + // Stateless translation of the builtin nearest access query. The caller + // supplies the existing typed default profile and the real Time callable. + // Public cache publication, failure diagnostics and fallback stay ordered. + Dictionary nearest_guest_access(Object *path, const Vector3i &start, + const Array &goals, const Vector3i &fallback_target, + const TypedArray &layers, const TypedArray &access, + const Callable &ticks) { + const ProjectionNames n; + return nearest_guest_access_impl(n,path,start,goals,fallback_target,layers,access,ticks); + } +private: + Dictionary nearest_guest_access_impl(const ProjectionNames &n,Object *path, const Vector3i &start, + const Array &goals, const Vector3i &fallback_target, + const TypedArray &layers, const TypedArray &access, + const Callable &ticks) { + Dictionary unsupported; + unsupported[n.k_supported] = false; + if (!path || !ticks.is_valid() || goals.is_empty()) return unsupported; + Object *script = path->get_script(); + if (!script || String(script->get(n.k_resource_path)) != "res://scripts/world/path_service.gd") return unsupported; + Dictionary batch = path->get(n.k__query_batch); + if (batch.is_empty() || start != static_cast(batch[n.k_start])) return unsupported; + const Dictionary passable = path->get(n.k__passable_cells); + if (!passable.has(start) || blocked(n,path, start) || !layers.has(start.z)) return unsupported; + const Dictionary targets = batch[n.k_targets]; + Dictionary cache = path->get(n.k__path_cache); + // Exact builtin validation loop has no callback before ticks.call(). + // Build the unchanged profile/revision suffix lazily at the first key. + String key_suffix; + bool key_suffix_ready = false; + for (int64_t j = 0; j < goals.size(); ++j) { + if (goals[j].get_type() != Variant::VECTOR3I) return unsupported; + const Vector3i cell = goals[j]; + if (!targets.has(cell)) { + const bool searchable = passable.has(cell) && layers.has(cell.z) && !blocked(n,path, cell); + if (searchable && (start.z == cell.z || static_cast(path->call(n.k__guest_batch_has_permitted_layer_crossing, start, cell, layers, access)))) return unsupported; + } + if (!key_suffix_ready) { + key_suffix = cache_key_suffix(n,path, layers, access); + key_suffix_ready = true; + } + const String key = cache_key_with_suffix(start, cell, key_suffix); + if (cache.has(key)) return unsupported; + } + const int64_t started = ticks.call(); + if (!static_cast(batch[n.k_ready])) { + batch[n.k_tree] = path->call(n.k__build_guest_query_tree_compact, start, targets, layers, access); + batch[n.k_arithmetic_wrapped] = path->get(n.k__guest_tree_arithmetic_wrapped); + batch[n.k_ready] = true; + } + const Dictionary tree = batch[n.k_tree]; + if (!static_cast(tree.get(n.k_usable, false)) || static_cast(batch.get(n.k_arithmetic_wrapped, false))) return unsupported; + Vector3i best_cell = fallback_target; + bool found = false; + int64_t best_cost = 0; + const Dictionary settled = tree[n.k_settled]; + bool cost_view_ready=false,compact_cost_view=false; + Dictionary cost_indices,cost_dictionary; + PackedInt64Array cost_values; + for (int64_t j = 0; j < goals.size(); ++j) { + const Vector3i cell = goals[j]; + if (!targets.has(cell) || !settled.has(cell)) continue; + // No callback occurs between these original numeric cost reads. + // Initialize lazily at the first eligible goal, and retain nothing + // past this call or reuse after the subsequent route callbacks. + if (!cost_view_ready) { + compact_cost_view=tree.get(n.k_compact,false); + if(compact_cost_view) {cost_indices=tree[n.k_indices];cost_values=tree[n.k_cost_values];} + else cost_dictionary=tree[n.k_costs]; + cost_view_ready=true; + } + int64_t cost=0; + if(compact_cost_view) { + if(!cost_indices.has(cell)) return unsupported; + const int64_t index=cost_indices[cell]; + if(index<0 || index>=cost_values.size()) return unsupported; + cost=cost_values[index]; + } else { + if(!cost_dictionary.has(cell)) return unsupported; + cost=cost_dictionary[cell]; + } + if (!found || cost < best_cost) { + found = true; + best_cost = cost; + best_cell = cell; + } + } + TypedArray best_path; + if (found) { + best_path = resolve_ready_guest_point(n,path, start, best_cell, tree, layers, access); + if (best_path.is_empty()) return unsupported; + } + if (!goals.is_empty() && (!found || goals[goals.size() - 1] != Variant(best_cell))) { + resolve_ready_guest_point(n,path, start, goals[goals.size() - 1], tree, layers, access); + } + const int64_t elapsed = the_tree_native::signed_add(static_cast(ticks.call()), the_tree_native::signed_add(~started, 1)); + path->emit_signal(n.k_path_query_measured, std::max(elapsed, 0)); + Dictionary result; + result[n.k_supported] = true; + result[n.k_reachable] = found; + result[n.k_path] = best_path; + result[n.k_target_cell] = best_cell; + return result; + } +private: + static bool blocked(const ProjectionNames &n,Object *path, const Vector3i &cell) { + const Dictionary traffic_map = path->get(n.k__cell_traffic); + const Dictionary traffic = traffic_map.get(cell, Dictionary()); + return !traffic.is_empty() && static_cast(traffic[n.k_temporary_block]); + } + static bool tree_has_cost(const ProjectionNames &n,const Dictionary &tree, const Vector3i &cell) { + if (!static_cast(tree.get(n.k_compact, false))) { + const Dictionary values = tree[n.k_costs]; + return values.has(cell); + } + const Dictionary indices = tree[n.k_indices]; + if (!indices.has(cell)) return false; + const int64_t index = indices[cell]; + const PackedInt64Array values = tree[n.k_cost_values]; + return index >= 0 && index < values.size(); + } + static int64_t tree_cost(const ProjectionNames &n,const Dictionary &tree, const Vector3i &cell) { + if (!static_cast(tree.get(n.k_compact, false))) { + const Dictionary values = tree[n.k_costs]; + return static_cast(values[cell]); + } + const Dictionary indices = tree[n.k_indices]; + const PackedInt64Array values = tree[n.k_cost_values]; + return values[static_cast(indices[cell])]; + } + static TypedArray resolve_ready_guest_point(const ProjectionNames &n,Object *path, + const Vector3i &start, const Vector3i &goal, const Dictionary &tree, + const TypedArray &layers, const TypedArray &access) { + const Dictionary passable = path->get(n.k__passable_cells); + if (!passable.has(start)) return path->call(n.k__route_failure, n.k_START_NOT_PASSABLE); + if (!passable.has(goal)) return path->call(n.k__route_failure, n.k_GOAL_NOT_PASSABLE); + if (!layers.has(start.z) || !layers.has(goal.z)) return path->call(n.k__route_failure, n.k_ACCESS_LAYER_DENIED); + if (blocked(n,path, start)) return path->call(n.k__route_failure, n.k_START_TEMPORARILY_BLOCKED); + if (blocked(n,path, goal)) return path->call(n.k__route_failure, n.k_GOAL_TEMPORARILY_BLOCKED); + // ticks/tree callbacks may have changed the script or revision. Recheck + // at this original call position and never reuse the pre-tick suffix. + const String key = builtin(n,path,"res://scripts/world/path_service.gd") ? + cache_key_with_suffix(start, goal, cache_key_suffix(n,path, layers, access)) : + String(path->call(n.k__cache_key, start, goal, layers, access)); + Dictionary cache = path->get(n.k__path_cache); + const Dictionary batch = path->get(n.k__query_batch); + const Dictionary targets = batch[n.k_targets]; + if (cache.has(key) || !targets.has(goal)) return path->call(n.k__find_path_internal, start, goal); + path->set(n.k_cache_misses, the_tree_native::signed_add(static_cast(path->get(n.k_cache_misses)), 1)); + if (start.z != goal.z && !static_cast(path->call(n.k__guest_batch_has_permitted_layer_crossing, start, goal, layers, access))) return path->call(n.k__route_failure, n.k_GOAL_UNREACHABLE); + TypedArray route; + const Dictionary settled = tree[n.k_settled]; + if (start == goal) route.push_back(start); + else if (settled.has(goal)) route = Array(path->call(n.k__guest_tree_reconstruct_path, tree, goal)); + if (route.is_empty()) return path->call(n.k__route_failure, path->call(n.k__diagnose_no_route_reason, start, goal, layers, access)); + cache[key] = path->call(n.k__copy_path, route); + path->set(n.k_last_no_route_reason, n.k_OK); + path->set(n.k_last_path_cost, tree_cost(n,tree, goal)); + path->set(n.k_last_error, String("")); + return route; + } +public: +private: + // One existing builtin provider operation, with no new value cache. Only + // connector-free WALK rows qualify; unsupported cases call the old provider. + // All work stays private until the original final shared[current] assignment. + struct WalkEdgeNames { + const StringName resource_path{"resource_path",false}, constants_method{"get_script_constant_map",false}; + const StringName front{"FRONT",false}, back{"BACK",false}, walk{"MOVEMENT_WALK",false}; + const StringName base{"ORTHOGONAL_MOVE_COST",false}, congestion{"CONGESTION_COST_PER_EXCESS",false}; + const StringName workspace{"_guest_tree_workspace",false}, edges{"edges",false}, indices{"indices",false}; + const StringName adjacency{"_adjacency",false}, traffic{"_cell_traffic",false}, vertical{"_vertical_walk_requires_connector",false}; + const StringName to{"to",false}, connector{"connector_id",false}, access{"access_id",false}, movement{"movement_type",false}; + const StringName base_cost{"base_cost",false}, cost{"cost",false}, temporary{"temporary_block",false}; + const StringName dynamic{"dynamic_cost",false}, density{"current_density",false}, capacity{"base_capacity",false}; + const StringName provider{"_guest_tree_shared_edges",false}; + }; + struct WalkEdgeContext { + Object *path=nullptr; + int64_t front=0, back=0, base=0, congestion=0; + StringName walk; + }; + static bool walk_edge_context(const WalkEdgeNames &n,Object *path,const Array &layers, + const Array &access,WalkEdgeContext &context) { + if (!path || layers.size()!=2 || !access.is_empty() || + layers[0].get_type()!=Variant::INT || layers[1].get_type()!=Variant::INT) return false; + Object *script=path->get_script(); + if (!script || String(script->get(n.resource_path))!="res://scripts/world/path_service.gd" || + !script->has_method(n.constants_method)) return false; + const Variant value=script->call(n.constants_method); + if (value.get_type()!=Variant::DICTIONARY) return false; + const Dictionary constants=value; + for (const StringName &key : {n.front,n.back,n.base,n.congestion}) + if (constants.get(key,Variant()).get_type()!=Variant::INT) return false; + if (constants.get(n.walk,Variant()).get_type()!=Variant::STRING_NAME || + int64_t(layers[0])!=int64_t(constants[n.front]) || + int64_t(layers[1])!=int64_t(constants[n.back])) return false; + context.path=path;context.front=constants[n.front];context.back=constants[n.back]; + context.base=constants[n.base];context.congestion=constants[n.congestion];context.walk=constants[n.walk]; + return true; + } + static bool builtin_walk_edge_rows(const WalkEdgeNames &n,const WalkEdgeContext &context, + const Vector3i ¤t,Array &rows) { + if (!context.path) return false; + const Variant workspace_value=context.path->get(n.workspace); + if (workspace_value.get_type()!=Variant::DICTIONARY) return false; + const Dictionary workspace=workspace_value; + const Variant shared_value=workspace.get(n.edges,Variant()); + if (shared_value.get_type()!=Variant::DICTIONARY) return false; + Dictionary shared=shared_value; + // Original cached-row branch precedes readonly and all other state reads. + if (shared.has(current)) { + const Variant cached=shared[current]; + if (cached.get_type()!=Variant::ARRAY) return false; + rows=cached;return true; + } + if (shared.is_read_only()) return false; + const Variant indices_value=workspace.get(n.indices,Variant()), adjacency_value=context.path->get(n.adjacency); + const Variant traffic_value=context.path->get(n.traffic), vertical_value=context.path->get(n.vertical); + if (indices_value.get_type()!=Variant::DICTIONARY || adjacency_value.get_type()!=Variant::DICTIONARY || + traffic_value.get_type()!=Variant::DICTIONARY || vertical_value.get_type()!=Variant::BOOL) return false; + const Dictionary indices=indices_value, adjacency=adjacency_value, traffic=traffic_value; + const Variant raw_value=adjacency.get(current,Array()); + if (raw_value.get_type()!=Variant::ARRAY) return false; + const Array raw=raw_value;Array result; + for (int64_t i=0;i0?excess:0,context.congestion)); + } + const int64_t cost=the_tree_native::signed_add(int64_t(base),dynamic); + Array pair;pair.push_back(neighbor);pair.push_back(cost);result.push_back(pair); + // Same short circuit and first invalid prefix as the original method. + if (cost<=0 || !indices.has(neighbor)) break; + } + shared[current]=result;rows=result;return true; + } +public: + Dictionary guest_tree_edge_rows(Object *path,const Vector3i ¤t,const Array &layers,const Array &access) { + const WalkEdgeNames names;WalkEdgeContext context;Array rows; + Dictionary result;const StringName supported{"supported",false}, row_key{"rows",false}; + result[supported]=false; + if (!walk_edge_context(names,path,layers,access,context) || + !builtin_walk_edge_rows(names,context,current,rows)) return result; + result[supported]=true;result[row_key]=rows;return result; + } +private: + Dictionary search_impl(const Array &cells, const Dictionary &indices, I start_index, + const Dictionary &goals, const Callable &edge_provider, bool use_edge_cache) { + Dictionary output; + output[StringName("status")] = String("unusable"); + if (start_index < 0 || start_index >= cells.size() || !edge_provider.is_valid()) return output; + try { + std::vector goal_indices; + const Array goal_keys = goals.keys(); + for (I j = 0; j < goal_keys.size(); ++j) + goal_indices.push_back(static_cast(indices.get(goal_keys[j], I(-1)))); + Array arguments; + arguments.resize(1); + const WalkEdgeNames walk_names;WalkEdgeContext walk_context; + if (edge_provider.get_method()==walk_names.provider && edge_provider.get_argument_count()==1 && + edge_provider.get_bound_arguments_count()==2 && edge_provider.get_unbound_arguments_count()==0) { + const Array bound=edge_provider.get_bound_arguments(); + if (bound.size()==2 && bound[0].get_type()==Variant::ARRAY && bound[1].get_type()==Variant::ARRAY) + walk_edge_context(walk_names,edge_provider.get_object(),bound[0],bound[1],walk_context); + } + // Call the original bound Godot edge method only when the search expands a node. + // Preserve its lazy cache population, filter ordering and first invalid row. + auto original_edges = [&](I current) { + arguments[0] = cells[current]; + Array fused;bool fused_supported=false; + if (cells[current].get_type()==Variant::VECTOR3I) { + const Vector3i cell=cells[current]; + fused_supported=builtin_walk_edge_rows(walk_names,walk_context,cell,fused); + } + const Variant value = fused_supported ? Variant(fused) : edge_provider.callv(arguments); + if (value.get_type() != Variant::ARRAY) return std::vector{{-1, 0}}; + const Array rows = value; + std::vector result; + result.reserve(rows.size()); + for (I j = 0; j < rows.size(); ++j) { + if (rows[j].get_type() != Variant::ARRAY) { result.push_back({-1, 0}); break; } + const Array row = rows[j]; + if (row.size() != 2 || row[1].get_type() != Variant::INT) { + result.push_back({-1, 0}); break; + } + const I cost = static_cast(row[1]); + // Nonpositive cost is rejected before requesting the neighbor index. + if (cost <= 0) { result.push_back({-1, cost}); break; } + const I neighbor = static_cast(indices.get(row[0], I(-1))); + result.push_back({neighbor, cost}); + if (neighbor < 0) break; + } + return result; + }; + // Borrow cached rows only during the pure numeric edge loop. + // No engine callback runs while that borrowed vector is traversed. + std::vector converted; + auto edges = [&](I current) -> const std::vector& { + if (use_edge_cache) { + const auto found = cached_edges.find(current); + if (found != cached_edges.end()) return found->second; + } + converted = original_edges(current); + // Preserve the returned conversion even if a reentrant provider + // populated this cache key first. Never substitute its row here. + if (use_edge_cache) cached_edges.emplace(current, converted); + return converted; + }; + const auto result = the_tree_native::search(cells.size(), start_index, goal_indices, edges); + output[StringName("status")] = String(result.status.c_str()); + if (result.status != "usable") return output; + PackedInt64Array costs, parents; + if (costs.resize(result.costs.size()) != OK || parents.resize(result.parents.size()) != OK) { + output[StringName("status")] = String("unusable"); + return output; + } + // Newly resized, private packed arrays own their writable storage. + // A provider may shrink caller cells. Preserve the old written + // prefix and resize-created zero tail, not the whole native vector. + const I written_count=cells.size(); + if(written_count<=static_cast(result.costs.size())) { + std::copy_n(result.costs.begin(),written_count,costs.ptrw()); + std::copy_n(result.parents.begin(),written_count,parents.ptrw()); + } else { + // Keep the original unsupported-growth path unchanged. + for (I j=0;j discovered, parent_discovered; + for (I j : result.discovered) discovered.push_back(j); + for (I j : result.parent_discovered) parent_discovered.push_back(j); + Dictionary settled; + for (I j : result.settled) settled[cells[j]] = true; + output[StringName("cost_values")] = costs; + output[StringName("parent_indices")] = parents; + output[StringName("discovered")] = discovered; + output[StringName("parent_discovered")] = parent_discovered; + output[StringName("settled")] = settled; + return output; + } catch (const std::exception &) { + output = Dictionary(); + output[StringName("status")] = String("unusable"); + return output; + } + } +}; + +void initialize_tree_numeric_core(ModuleInitializationLevel level) { + if (level == MODULE_INITIALIZATION_LEVEL_SCENE) GDREGISTER_CLASS(TheTreeNumericCore); +} +void uninitialize_tree_numeric_core(ModuleInitializationLevel) {} + +extern "C" { +GDExtensionBool GDE_EXPORT tree_numeric_core_init(GDExtensionInterfaceGetProcAddress get_proc, + GDExtensionClassLibraryPtr library, GDExtensionInitialization *initialization) { + GDExtensionBinding::InitObject init(get_proc, library, initialization); + init.register_initializer(initialize_tree_numeric_core); + init.register_terminator(uninitialize_tree_numeric_core); + init.set_minimum_library_initialization_level(MODULE_INITIALIZATION_LEVEL_SCENE); + return init.init(); +} +} diff --git a/docs/plans/original-completion-2026-10-02/checkpoints/native-hot-loop-r325-r329/026-p23-02-native-hot-loop-local-actual-independent-review-r329.json.gz b/docs/plans/original-completion-2026-10-02/checkpoints/native-hot-loop-r325-r329/026-p23-02-native-hot-loop-local-actual-independent-review-r329.json.gz new file mode 100644 index 0000000..7031a59 Binary files /dev/null and b/docs/plans/original-completion-2026-10-02/checkpoints/native-hot-loop-r325-r329/026-p23-02-native-hot-loop-local-actual-independent-review-r329.json.gz differ diff --git a/docs/plans/original-completion-2026-10-02/checkpoints/native-hot-loop-r325-r329/027-p23-02-native-hot-loop-local-preexecution-independent-review-r329.json.gz b/docs/plans/original-completion-2026-10-02/checkpoints/native-hot-loop-r325-r329/027-p23-02-native-hot-loop-local-preexecution-independent-review-r329.json.gz new file mode 100644 index 0000000..32e3456 Binary files /dev/null and b/docs/plans/original-completion-2026-10-02/checkpoints/native-hot-loop-r325-r329/027-p23-02-native-hot-loop-local-preexecution-independent-review-r329.json.gz differ diff --git a/docs/plans/original-completion-2026-10-02/checkpoints/native-hot-loop-r325-r329/028-actual-root-verification.json.gz b/docs/plans/original-completion-2026-10-02/checkpoints/native-hot-loop-r325-r329/028-actual-root-verification.json.gz new file mode 100644 index 0000000..197e576 Binary files /dev/null and b/docs/plans/original-completion-2026-10-02/checkpoints/native-hot-loop-r325-r329/028-actual-root-verification.json.gz differ diff --git a/docs/plans/original-completion-2026-10-02/checkpoints/native-hot-loop-r325-r329/029-candidate-guarded.gd b/docs/plans/original-completion-2026-10-02/checkpoints/native-hot-loop-r325-r329/029-candidate-guarded.gd new file mode 100644 index 0000000..3f25994 --- /dev/null +++ b/docs/plans/original-completion-2026-10-02/checkpoints/native-hot-loop-r325-r329/029-candidate-guarded.gd @@ -0,0 +1,11 @@ +extends "/Users/korizatou114/Projects/\u3068\u308a\u306b\u304f\u5de5\u623f\u30b2\u30fc\u30e0\u5236\u4f5c\u90e8/artifacts/original-completion-2026-10-02/p23-02-native-hot-loop-local-r329/local.gd" +const Guard := preload("/Users/korizatou114/Projects/\u3068\u308a\u306b\u304f\u5de5\u623f\u30b2\u30fc\u30e0\u5236\u4f5c\u90e8/artifacts/original-completion-2026-10-02/p23-02-native-cache-public-mutations-r144/profile_guard.gd") +func _initialize() -> void: + if not Guard.activate(): quit(97); return + super._initialize() +func _run() -> void: + var loaded := GDExtensionManager.load_extension("/Users/korizatou114/Projects/\u3068\u308a\u306b\u304f\u5de5\u623f\u30b2\u30fc\u30e0\u5236\u4f5c\u90e8/artifacts/original-completion-2026-10-02/p23-02-native-hot-loop-local-r329/candidate.gdextension") + if loaded != GDExtensionManager.LOAD_STATUS_OK or not ClassDB.class_exists(&"TheTreeNumericCore"): + push_error("exact native Guest batch extension failed to load"); quit(96); return + print("NATIVE_ADAPTER_LIBRARY_REGISTERED") + super._run() diff --git a/docs/plans/original-completion-2026-10-02/checkpoints/native-hot-loop-r325-r329/030-candidate.gdextension b/docs/plans/original-completion-2026-10-02/checkpoints/native-hot-loop-r325-r329/030-candidate.gdextension new file mode 100644 index 0000000..d46e2a8 --- /dev/null +++ b/docs/plans/original-completion-2026-10-02/checkpoints/native-hot-loop-r325-r329/030-candidate.gdextension @@ -0,0 +1,7 @@ +[configuration] +entry_symbol="tree_numeric_core_init" +compatibility_minimum="4.7.2" +reloadable=false + +[libraries] +macos.arm64="/private/tmp/TheTree-hot-loop-r329-41b6a86c36104f96acdf24f428667ade/candidate.dylib" diff --git a/docs/plans/original-completion-2026-10-02/checkpoints/native-hot-loop-r325-r329/031-driver-before-prepath-repair.py b/docs/plans/original-completion-2026-10-02/checkpoints/native-hot-loop-r325-r329/031-driver-before-prepath-repair.py new file mode 100644 index 0000000..a6d1373 --- /dev/null +++ b/docs/plans/original-completion-2026-10-02/checkpoints/native-hot-loop-r325-r329/031-driver-before-prepath-repair.py @@ -0,0 +1,64 @@ +# coding: utf-8 +"""One owned Guest batch ABI/local/affected-S3 execution; no full regression or release acceptance.""" +from pathlib import Path +import hashlib,importlib.util,json,os,shutil,sys,uuid +sys.dont_write_bytecode=True +R=Path(__file__).resolve().parents[2];A=R/'artifacts/original-completion-2026-10-02';O=A/'p23-02-native-hot-loop-local-r329';D=R/'docs/plans/original-completion-2026-10-02' +NAMES=['IMPLEMENTATION_PLAN.md','PROGRESS_INTEGRATION.md','work_packages.json','progress_snapshot.json'] +def mod(name,p): + s=importlib.util.spec_from_file_location(name,p);m=importlib.util.module_from_spec(s);s.loader.exec_module(m);return m +def main(): + r=mod('guest_local_runner',R/'tools/run_all_tests.py');outer=mod('guest_local_owned',r.SUPPORT/'owned_outer.py');plan=r.read(O/'input.json');fixed=plan['pins'] + assert not (O/'execution.json').exists() + stage=Path(plan['stage']);parent=Path(plan['parent']);public=(stage/'project.godot').read_bytes();private=Path(fixed['private_library']['path']).parent + source=r.source_refs(R);player=r.default_player(R);docs={n:r.sha(D/n) for n in NAMES} + assert source==plan['source_before'] and player==plan['default_before'] and docs==plan['doc4_before'] + def protect(): + assert r.source_refs(R)==source and r.default_player(R)==player and {n:r.sha(D/n) for n in NAMES}==docs + assert r.inventory(parent,stage=True)==plan['parent_stage_inventory'] and r.inventory(stage,stage=True)==plan['expected_stage'] + assert r.inventory(r.SUPPORT)==plan['support_inventory'] and r.inventory(private)==plan['private_library_inventory'] + for k,q in fixed.items(): + p=Path(q['path']);assert p.stat().st_size==q['bytes'] and r.sha(p)==q['sha256'],k + def growth():return sum(p.stat().st_size for p in O.rglob('*') if p.is_file() and p.relative_to(O).parts[0]!='stage') + protect();host=r.read(fixed['host_review']['path']);assert host['host_build_accepted'] and not host['blockers'] + prepath=A/'p23-02-native-guest-bulk-local-preexecution-independent-review-r315.json';pre=r.read(prepath);assert pre['execution_allowed'] and not pre['blockers'] + for group in ['evidence_sha256','canonical_doc4_context_pins']: + for q in pre[group].values(): + p=Path(q['path']);assert p.stat().st_size==q['bytes'] and r.sha(p)==q['sha256'] + root=r.read(O/'preexecution-root-verification.json');assert root['passed'] and root['review_sha256']==r.sha(prepath) + phases=[];samples=[];error=None;cleanup=[];cleanup_errors=[];report=None + try: + protect();assert not outer.native.helper['live_engines']() and shutil.disk_usage(A).free>=plan['capacity_floor_bytes'] and growth()<=plan['maximum_measurement_growth_bytes'] + output=O/'local-measurement';output.mkdir();ident=uuid.uuid4().hex;startup='TheTree-PCK-tests-'+ident+'-guest-startup';profile='TheTree-PCK-tests-'+ident+'-guest-runtime' + assert not r.profile_path(startup).exists() and not r.profile_path(profile).exists() + env=r.fresh_environment(os.environ);env.update(THE_TREE_PACK_PROFILE_NAME=profile,THE_TREE_PACK_PROFILE_EXPECTED=str(r.profile_path(profile)),THE_TREE_PACK_PROFILE_REPORT=str(O/'local-profile.json'),THE_TREE_RUNTIME_MONITOR_OUTPUT=str(output)) + (stage/'project.godot').write_bytes(r.private_project(public,startup));phase=O/'local' + argv=[fixed['engine']['path'],'--headless','--path',str(stage),'--script',fixed['starter']['path'],'--verbose','--log-file',str(phase/'engine-native.log')] + result,capture=outer.run_phase(argv,phase,90,env,now_fn=r.now,dump_fn=r.write,sha_fn=r.sha,live_engines_fn=outer.native.helper['live_engines']) + phases.append(dict(name='local',result=result,capture=capture));r.write(O/'local-phase.json',phases[0]) + report=r.read(output/'report.json') if (output/'report.json').exists() else None + if report:samples.append(dict(name='local',report=report)) + assert result is not None and result['exit_code']==0 and not result['error_lines'] and not result['timed_out'] and capture['passed'] + logs=''.join((phase/n).read_text() for n in ['stdout.log','stderr.log','engine-native.log']) + assert 'Orphan StringName:' not in logs and 'unclaimed string names' not in logs + assert report and report['passed'] and report['failures']==0 and report['pid']==result['pid'] + count=plan['expected_case_counts'];assert report['pure_case_count']==count['pure'] and report['decision_case_count']==count['decision'] and report['custom_fallback_case_count']==count['custom_fallback'] and report['original_s3_invocations']==count['original_s3_invocations'] + stdout=(phase/'stdout.log').read_text();assert stdout.count('Guest bulk local: %d checks, 0 failures'%report['checks'])==1 and stdout.count('NATIVE_ADAPTER_LIBRARY_REGISTERED')==1 + fixture=output/'fixtures.varbytes';assert fixture.stat().st_size==report['fixture_bytes'] and r.sha(fixture)==report['fixture_sha256'] + protocol=r.read(output/'protocol-report.json');assert protocol['passed'] and not protocol['failures'] and len(protocol['cases'])==50 and len(protocol['cached_cases'])==50 and protocol['checks']==report['protocol_checks'] + assert fixture.read_bytes()==Path(plan['original_Guest_fixture']['path']).read_bytes(), 'Unchanged full Guest53 original fixture bytes' + r.profile_gate(r.profile_path(profile),O/'local-profile.json',profile) + assert growth()<=plan['maximum_measurement_growth_bytes'] + (stage/'project.godot').write_bytes(public);protect() + except BaseException as caught:error=type(caught).__name__+': '+str(caught) + finally: + cleanup,cleanup_errors=outer.finish_captured(outer.OWNED) + if all(q['engine_ended'] is True for q in cleanup):(stage/'project.godot').write_bytes(public) + else:cleanup_errors.append('Restoration refused while actual owned child remains live') + guard_errors=[] + try:protect() + except BaseException as caught:guard_errors.append(type(caught).__name__+': '+str(caught)) + passed=error is None and not guard_errors and not cleanup_errors and len(phases)==1 and len(samples)==1 + r.write(O/'execution.json',dict(passed=passed,error=error,guard_errors=guard_errors,phases=phases,samples=samples,cleanup=cleanup,cleanup_errors=cleanup_errors,input_source_default_doc4_parent_support_private_preserved=not guard_errors,compiler_processes=0,full_regression_runs=0,candidate_selected=False,candidate_adopted=False,unit_accepted=False,performance_accepted=False,normal_OS_accepted=False,Windows_accepted=False,limitations=['Finite Guest53 and18 affected original S3 methods plus frozen50 protocols/shrink/ownership/reentrant collision on same new bridge. Literal original Guest source oracle removes class_name line only. No Runtime cold646→647 whole state, ordinary OS/reentry domain, arbitrary conversion or memory failure, final full regression, performance budget/P95/D10, Windows or adoption/unit acceptance.'])) + print(json.dumps(dict(passed=passed,error=error,guard_errors=guard_errors)),flush=True);return 0 if passed else 1 +if __name__=='__main__':raise SystemExit(main()) diff --git a/docs/plans/original-completion-2026-10-02/checkpoints/native-hot-loop-r325-r329/032-execution.json.gz b/docs/plans/original-completion-2026-10-02/checkpoints/native-hot-loop-r325-r329/032-execution.json.gz new file mode 100644 index 0000000..3ccbc45 Binary files /dev/null and b/docs/plans/original-completion-2026-10-02/checkpoints/native-hot-loop-r325-r329/032-execution.json.gz differ diff --git a/docs/plans/original-completion-2026-10-02/checkpoints/native-hot-loop-r325-r329/033-input-before-prepath-repair.json.gz b/docs/plans/original-completion-2026-10-02/checkpoints/native-hot-loop-r325-r329/033-input-before-prepath-repair.json.gz new file mode 100644 index 0000000..cca399e Binary files /dev/null and b/docs/plans/original-completion-2026-10-02/checkpoints/native-hot-loop-r325-r329/033-input-before-prepath-repair.json.gz differ diff --git a/docs/plans/original-completion-2026-10-02/checkpoints/native-hot-loop-r325-r329/034-input.json.gz b/docs/plans/original-completion-2026-10-02/checkpoints/native-hot-loop-r325-r329/034-input.json.gz new file mode 100644 index 0000000..8364061 Binary files /dev/null and b/docs/plans/original-completion-2026-10-02/checkpoints/native-hot-loop-r325-r329/034-input.json.gz differ diff --git a/docs/plans/original-completion-2026-10-02/checkpoints/native-hot-loop-r325-r329/035-adapter-owned-cleanup.json b/docs/plans/original-completion-2026-10-02/checkpoints/native-hot-loop-r325-r329/035-adapter-owned-cleanup.json new file mode 100644 index 0000000..f44709a --- /dev/null +++ b/docs/plans/original-completion-2026-10-02/checkpoints/native-hot-loop-r325-r329/035-adapter-owned-cleanup.json @@ -0,0 +1,18 @@ +{ + "owned_child_created": true, + "engine_ended": true, + "actual_process_exit_code": 0, + "attempts": [ + { + "attempt": 1, + "kill_attempted": false, + "wait_timeout_seconds": 5, + "wait_completed": true, + "errors": [], + "poll_before": 0, + "wait_returned_code": 0, + "poll_after": 0 + } + ], + "errors": [] +} diff --git a/docs/plans/original-completion-2026-10-02/checkpoints/native-hot-loop-r325-r329/036-ended.json b/docs/plans/original-completion-2026-10-02/checkpoints/native-hot-loop-r325-r329/036-ended.json new file mode 100644 index 0000000..d4103e4 --- /dev/null +++ b/docs/plans/original-completion-2026-10-02/checkpoints/native-hot-loop-r325-r329/036-ended.json @@ -0,0 +1,38 @@ +{ + "scope": "isolated diagnostic owned child", + "event": "ENGINE_ENDED", + "actual_pid": 36884, + "owner_pid": 36862, + "command": [ + "/Applications/Godot.app/Contents/MacOS/Godot", + "--headless", + "--path", + "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/artifacts/original-completion-2026-10-02/p23-02-native-guest-bulk-local-r315/stage", + "--script", + "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/artifacts/original-completion-2026-10-02/p23-02-native-hot-loop-local-r329/candidate-guarded.gd", + "--verbose", + "--log-file", + "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/artifacts/original-completion-2026-10-02/p23-02-native-hot-loop-local-r329/local/engine-native.log" + ], + "monotonic_ns": 4535962000, + "exit_code": 0, + "actual_process_exit_code": 0, + "engine_ended": true, + "timed_out": false, + "lifecycle_error": null, + "start_receipt_written": true, + "pid": 36884, + "argv": [ + "/Applications/Godot.app/Contents/MacOS/Godot", + "--headless", + "--path", + "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/artifacts/original-completion-2026-10-02/p23-02-native-guest-bulk-local-r315/stage", + "--script", + "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/artifacts/original-completion-2026-10-02/p23-02-native-hot-loop-local-r329/candidate-guarded.gd", + "--verbose", + "--log-file", + "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/artifacts/original-completion-2026-10-02/p23-02-native-hot-loop-local-r329/local/engine-native.log" + ], + "ended_utc": "2026-10-12T03:57:44.034797+00:00", + "signal_scope": "tracked Popen child only; no external group signal" +} diff --git a/docs/plans/original-completion-2026-10-02/checkpoints/native-hot-loop-r325-r329/037-engine-native.log.gz b/docs/plans/original-completion-2026-10-02/checkpoints/native-hot-loop-r325-r329/037-engine-native.log.gz new file mode 100644 index 0000000..36857bf Binary files /dev/null and b/docs/plans/original-completion-2026-10-02/checkpoints/native-hot-loop-r325-r329/037-engine-native.log.gz differ diff --git a/docs/plans/original-completion-2026-10-02/checkpoints/native-hot-loop-r325-r329/038-outer-owned-finally.json b/docs/plans/original-completion-2026-10-02/checkpoints/native-hot-loop-r325-r329/038-outer-owned-finally.json new file mode 100644 index 0000000..74dee8e --- /dev/null +++ b/docs/plans/original-completion-2026-10-02/checkpoints/native-hot-loop-r325-r329/038-outer-owned-finally.json @@ -0,0 +1,39 @@ +{ + "actual_captured_pids": [ + 36884 + ], + "cleanup": [ + { + "pid": 36884, + "cleanup": { + "owned_child_created": true, + "engine_ended": true, + "actual_process_exit_code": 0, + "attempts": [ + { + "attempt": 1, + "kill_attempted": false, + "wait_timeout_seconds": 5, + "wait_completed": true, + "errors": [], + "poll_before": 0, + "wait_returned_code": 0, + "poll_after": 0 + } + ], + "errors": [] + }, + "outer_cleanup_errors": [], + "engine_ended": true, + "outer_maximum_helper_invocations": 3, + "helper_maximum_waits": 3, + "per_wait_timeout_seconds": 5, + "foreign_process_or_group_signals": 0 + } + ], + "adapter_exception": null, + "all_captured_ended": true, + "outer_cleanup_errors": [], + "foreign_process_or_group_signals": 0, + "all_cleanup_before_outer_receipt": true +} diff --git a/docs/plans/original-completion-2026-10-02/checkpoints/native-hot-loop-r325-r329/039-process.json b/docs/plans/original-completion-2026-10-02/checkpoints/native-hot-loop-r325-r329/039-process.json new file mode 100644 index 0000000..5e60936 --- /dev/null +++ b/docs/plans/original-completion-2026-10-02/checkpoints/native-hot-loop-r325-r329/039-process.json @@ -0,0 +1,140 @@ +{ + "argv": [ + "/Applications/Godot.app/Contents/MacOS/Godot", + "--headless", + "--path", + "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/artifacts/original-completion-2026-10-02/p23-02-native-guest-bulk-local-r315/stage", + "--script", + "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/artifacts/original-completion-2026-10-02/p23-02-native-hot-loop-local-r329/candidate-guarded.gd", + "--verbose", + "--log-file", + "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/artifacts/original-completion-2026-10-02/p23-02-native-hot-loop-local-r329/local/engine-native.log" + ], + "pid": 36884, + "started_utc": "2026-10-12T03:57:42.177687+00:00", + "ended_utc": "2026-10-12T03:57:44.040127+00:00", + "wall_seconds": 1.86218475, + "exit_code": 0, + "helper_exit_code": 0, + "timed_out": false, + "stdout_sha256": "0d80fd887d3ae60d97ac91ec360387c6dd4aa06b0417cc000f2567e5b401735f", + "stderr_sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "error_lines": [], + "owned_popen_ended": true, + "old_helper_engine_ended": true, + "start_receipt_written": true, + "end_receipt_written": true, + "lifecycle_error": null, + "old_helper_lifecycle_error": null, + "actual_start_binding": { + "scope": "isolated diagnostic owned child", + "event": "ENGINE_STARTED", + "actual_pid": 36884, + "owner_pid": 36862, + "command": [ + "/Applications/Godot.app/Contents/MacOS/Godot", + "--headless", + "--path", + "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/artifacts/original-completion-2026-10-02/p23-02-native-guest-bulk-local-r315/stage", + "--script", + "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/artifacts/original-completion-2026-10-02/p23-02-native-hot-loop-local-r329/candidate-guarded.gd", + "--verbose", + "--log-file", + "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/artifacts/original-completion-2026-10-02/p23-02-native-hot-loop-local-r329/local/engine-native.log" + ], + "monotonic_ns": 2683606125, + "pid": 36884, + "argv": [ + "/Applications/Godot.app/Contents/MacOS/Godot", + "--headless", + "--path", + "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/artifacts/original-completion-2026-10-02/p23-02-native-guest-bulk-local-r315/stage", + "--script", + "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/artifacts/original-completion-2026-10-02/p23-02-native-hot-loop-local-r329/candidate-guarded.gd", + "--verbose", + "--log-file", + "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/artifacts/original-completion-2026-10-02/p23-02-native-hot-loop-local-r329/local/engine-native.log" + ], + "started_utc": "2026-10-12T03:57:42.177687+00:00", + "current_process_identity": { + "pid": 36884, + "ppid": 36862, + "pgid": 36884, + "args": "/Applications/Godot.app/Contents/MacOS/Godot --headless --path /Users/korizatou114/Projects/とりにく工房ゲーム制作部/artifacts/original-completion-2026-10-02/p23-02-native-guest-bulk-local-r315/stage --script /Users/korizatou114/Projects/とりにく工房ゲーム制作部/artifacts/original-completion-2026-10-02/p23-02-native-hot-loop-local-r329/candidate-guarded.gd --verbose --log-file /Users/korizatou114/Projects/とりにく工房ゲーム制作部/artifacts/original-completion-2026-10-02/p23-02-native-hot-loop-local-r329/local/engine-native.log" + }, + "live_identity_exact": true, + "tracked_child_ended_before_initial_probe": false, + "signal_scope": "tracked Popen child only; no external group signal" + }, + "actual_end_binding": { + "scope": "isolated diagnostic owned child", + "event": "ENGINE_ENDED", + "actual_pid": 36884, + "owner_pid": 36862, + "command": [ + "/Applications/Godot.app/Contents/MacOS/Godot", + "--headless", + "--path", + "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/artifacts/original-completion-2026-10-02/p23-02-native-guest-bulk-local-r315/stage", + "--script", + "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/artifacts/original-completion-2026-10-02/p23-02-native-hot-loop-local-r329/candidate-guarded.gd", + "--verbose", + "--log-file", + "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/artifacts/original-completion-2026-10-02/p23-02-native-hot-loop-local-r329/local/engine-native.log" + ], + "monotonic_ns": 4535962000, + "exit_code": 0, + "actual_process_exit_code": 0, + "engine_ended": true, + "timed_out": false, + "lifecycle_error": null, + "start_receipt_written": true, + "pid": 36884, + "argv": [ + "/Applications/Godot.app/Contents/MacOS/Godot", + "--headless", + "--path", + "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/artifacts/original-completion-2026-10-02/p23-02-native-guest-bulk-local-r315/stage", + "--script", + "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/artifacts/original-completion-2026-10-02/p23-02-native-hot-loop-local-r329/candidate-guarded.gd", + "--verbose", + "--log-file", + "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/artifacts/original-completion-2026-10-02/p23-02-native-hot-loop-local-r329/local/engine-native.log" + ], + "ended_utc": "2026-10-12T03:57:44.034797+00:00", + "signal_scope": "tracked Popen child only; no external group signal" + }, + "strict_native_diagnostics": { + "errors": [], + "banner_count": 1, + "known_unicode_diagnostic_count": 0, + "unrecognized_unicode_diagnostic_count": 0, + "exit_leak_diagnostic_lines": [] + }, + "external_process_or_group_signals": 0, + "owned_child_process_helper_sha256": "c007e712c281573254faa1233c2ac92e100ab87393256a6dc096b35283e43037", + "adapter_owned_cleanup": { + "owned_child_created": true, + "engine_ended": true, + "actual_process_exit_code": 0, + "attempts": [ + { + "attempt": 1, + "kill_attempted": false, + "wait_timeout_seconds": 5, + "wait_completed": true, + "errors": [], + "poll_before": 0, + "wait_returned_code": 0, + "poll_after": 0 + } + ], + "errors": [] + }, + "adapter_error": null, + "old_helper_bytes_preserved": true, + "helper_wait_None_maps_to_actual_timeout_seconds": 5, + "explicit_helper_wait_timeouts_preserved": true, + "additional_cleanup_wait_timeout_seconds": 5, + "additional_cleanup_max_attempts": 3 +} diff --git a/docs/plans/original-completion-2026-10-02/checkpoints/native-hot-loop-r325-r329/040-started.json b/docs/plans/original-completion-2026-10-02/checkpoints/native-hot-loop-r325-r329/040-started.json new file mode 100644 index 0000000..abdaa18 --- /dev/null +++ b/docs/plans/original-completion-2026-10-02/checkpoints/native-hot-loop-r325-r329/040-started.json @@ -0,0 +1,40 @@ +{ + "scope": "isolated diagnostic owned child", + "event": "ENGINE_STARTED", + "actual_pid": 36884, + "owner_pid": 36862, + "command": [ + "/Applications/Godot.app/Contents/MacOS/Godot", + "--headless", + "--path", + "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/artifacts/original-completion-2026-10-02/p23-02-native-guest-bulk-local-r315/stage", + "--script", + "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/artifacts/original-completion-2026-10-02/p23-02-native-hot-loop-local-r329/candidate-guarded.gd", + "--verbose", + "--log-file", + "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/artifacts/original-completion-2026-10-02/p23-02-native-hot-loop-local-r329/local/engine-native.log" + ], + "monotonic_ns": 2683606125, + "pid": 36884, + "argv": [ + "/Applications/Godot.app/Contents/MacOS/Godot", + "--headless", + "--path", + "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/artifacts/original-completion-2026-10-02/p23-02-native-guest-bulk-local-r315/stage", + "--script", + "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/artifacts/original-completion-2026-10-02/p23-02-native-hot-loop-local-r329/candidate-guarded.gd", + "--verbose", + "--log-file", + "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/artifacts/original-completion-2026-10-02/p23-02-native-hot-loop-local-r329/local/engine-native.log" + ], + "started_utc": "2026-10-12T03:57:42.177687+00:00", + "current_process_identity": { + "pid": 36884, + "ppid": 36862, + "pgid": 36884, + "args": "/Applications/Godot.app/Contents/MacOS/Godot --headless --path /Users/korizatou114/Projects/とりにく工房ゲーム制作部/artifacts/original-completion-2026-10-02/p23-02-native-guest-bulk-local-r315/stage --script /Users/korizatou114/Projects/とりにく工房ゲーム制作部/artifacts/original-completion-2026-10-02/p23-02-native-hot-loop-local-r329/candidate-guarded.gd --verbose --log-file /Users/korizatou114/Projects/とりにく工房ゲーム制作部/artifacts/original-completion-2026-10-02/p23-02-native-hot-loop-local-r329/local/engine-native.log" + }, + "live_identity_exact": true, + "tracked_child_ended_before_initial_probe": false, + "signal_scope": "tracked Popen child only; no external group signal" +} diff --git a/docs/plans/original-completion-2026-10-02/checkpoints/native-hot-loop-r325-r329/041-stderr.log b/docs/plans/original-completion-2026-10-02/checkpoints/native-hot-loop-r325-r329/041-stderr.log new file mode 100644 index 0000000..e69de29 diff --git a/docs/plans/original-completion-2026-10-02/checkpoints/native-hot-loop-r325-r329/042-stdout.log.gz b/docs/plans/original-completion-2026-10-02/checkpoints/native-hot-loop-r325-r329/042-stdout.log.gz new file mode 100644 index 0000000..c3dbf28 Binary files /dev/null and b/docs/plans/original-completion-2026-10-02/checkpoints/native-hot-loop-r325-r329/042-stdout.log.gz differ diff --git a/docs/plans/original-completion-2026-10-02/checkpoints/native-hot-loop-r325-r329/043-fixtures.varbytes.gz b/docs/plans/original-completion-2026-10-02/checkpoints/native-hot-loop-r325-r329/043-fixtures.varbytes.gz new file mode 100644 index 0000000..c9dc755 Binary files /dev/null and b/docs/plans/original-completion-2026-10-02/checkpoints/native-hot-loop-r325-r329/043-fixtures.varbytes.gz differ diff --git a/docs/plans/original-completion-2026-10-02/checkpoints/native-hot-loop-r325-r329/044-protocol-report.json.gz b/docs/plans/original-completion-2026-10-02/checkpoints/native-hot-loop-r325-r329/044-protocol-report.json.gz new file mode 100644 index 0000000..08c7640 Binary files /dev/null and b/docs/plans/original-completion-2026-10-02/checkpoints/native-hot-loop-r325-r329/044-protocol-report.json.gz differ diff --git a/docs/plans/original-completion-2026-10-02/checkpoints/native-hot-loop-r325-r329/045-report.json b/docs/plans/original-completion-2026-10-02/checkpoints/native-hot-loop-r325-r329/045-report.json new file mode 100644 index 0000000..7a231c1 --- /dev/null +++ b/docs/plans/original-completion-2026-10-02/checkpoints/native-hot-loop-r325-r329/045-report.json @@ -0,0 +1,16 @@ +{ + "checks": 7415, + "custom_fallback_case_count": 7, + "decision_case_count": 4, + "failures": 0, + "fixture_bytes": 606268, + "fixture_sha256": "6ec6e879a2de02b9d4081295b33e53e2060f31829e1fb674168a414d8b3e5c2e", + "original_s3_invocations": 18, + "passed": true, + "performance_accepted": false, + "pid": 36884, + "protocol_cases": 50, + "protocol_checks": 6722, + "pure_case_count": 42, + "unit_accepted": false +} diff --git a/docs/plans/original-completion-2026-10-02/checkpoints/native-hot-loop-r325-r329/046-local-phase.json b/docs/plans/original-completion-2026-10-02/checkpoints/native-hot-loop-r325-r329/046-local-phase.json new file mode 100644 index 0000000..fa80ed4 --- /dev/null +++ b/docs/plans/original-completion-2026-10-02/checkpoints/native-hot-loop-r325-r329/046-local-phase.json @@ -0,0 +1,185 @@ +{ + "name": "local", + "result": { + "argv": [ + "/Applications/Godot.app/Contents/MacOS/Godot", + "--headless", + "--path", + "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/artifacts/original-completion-2026-10-02/p23-02-native-guest-bulk-local-r315/stage", + "--script", + "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/artifacts/original-completion-2026-10-02/p23-02-native-hot-loop-local-r329/candidate-guarded.gd", + "--verbose", + "--log-file", + "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/artifacts/original-completion-2026-10-02/p23-02-native-hot-loop-local-r329/local/engine-native.log" + ], + "pid": 36884, + "started_utc": "2026-10-12T03:57:42.177687+00:00", + "ended_utc": "2026-10-12T03:57:44.040127+00:00", + "wall_seconds": 1.86218475, + "exit_code": 0, + "helper_exit_code": 0, + "timed_out": false, + "stdout_sha256": "0d80fd887d3ae60d97ac91ec360387c6dd4aa06b0417cc000f2567e5b401735f", + "stderr_sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "error_lines": [], + "owned_popen_ended": true, + "old_helper_engine_ended": true, + "start_receipt_written": true, + "end_receipt_written": true, + "lifecycle_error": null, + "old_helper_lifecycle_error": null, + "actual_start_binding": { + "scope": "isolated diagnostic owned child", + "event": "ENGINE_STARTED", + "actual_pid": 36884, + "owner_pid": 36862, + "command": [ + "/Applications/Godot.app/Contents/MacOS/Godot", + "--headless", + "--path", + "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/artifacts/original-completion-2026-10-02/p23-02-native-guest-bulk-local-r315/stage", + "--script", + "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/artifacts/original-completion-2026-10-02/p23-02-native-hot-loop-local-r329/candidate-guarded.gd", + "--verbose", + "--log-file", + "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/artifacts/original-completion-2026-10-02/p23-02-native-hot-loop-local-r329/local/engine-native.log" + ], + "monotonic_ns": 2683606125, + "pid": 36884, + "argv": [ + "/Applications/Godot.app/Contents/MacOS/Godot", + "--headless", + "--path", + "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/artifacts/original-completion-2026-10-02/p23-02-native-guest-bulk-local-r315/stage", + "--script", + "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/artifacts/original-completion-2026-10-02/p23-02-native-hot-loop-local-r329/candidate-guarded.gd", + "--verbose", + "--log-file", + "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/artifacts/original-completion-2026-10-02/p23-02-native-hot-loop-local-r329/local/engine-native.log" + ], + "started_utc": "2026-10-12T03:57:42.177687+00:00", + "current_process_identity": { + "pid": 36884, + "ppid": 36862, + "pgid": 36884, + "args": "/Applications/Godot.app/Contents/MacOS/Godot --headless --path /Users/korizatou114/Projects/とりにく工房ゲーム制作部/artifacts/original-completion-2026-10-02/p23-02-native-guest-bulk-local-r315/stage --script /Users/korizatou114/Projects/とりにく工房ゲーム制作部/artifacts/original-completion-2026-10-02/p23-02-native-hot-loop-local-r329/candidate-guarded.gd --verbose --log-file /Users/korizatou114/Projects/とりにく工房ゲーム制作部/artifacts/original-completion-2026-10-02/p23-02-native-hot-loop-local-r329/local/engine-native.log" + }, + "live_identity_exact": true, + "tracked_child_ended_before_initial_probe": false, + "signal_scope": "tracked Popen child only; no external group signal" + }, + "actual_end_binding": { + "scope": "isolated diagnostic owned child", + "event": "ENGINE_ENDED", + "actual_pid": 36884, + "owner_pid": 36862, + "command": [ + "/Applications/Godot.app/Contents/MacOS/Godot", + "--headless", + "--path", + "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/artifacts/original-completion-2026-10-02/p23-02-native-guest-bulk-local-r315/stage", + "--script", + "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/artifacts/original-completion-2026-10-02/p23-02-native-hot-loop-local-r329/candidate-guarded.gd", + "--verbose", + "--log-file", + "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/artifacts/original-completion-2026-10-02/p23-02-native-hot-loop-local-r329/local/engine-native.log" + ], + "monotonic_ns": 4535962000, + "exit_code": 0, + "actual_process_exit_code": 0, + "engine_ended": true, + "timed_out": false, + "lifecycle_error": null, + "start_receipt_written": true, + "pid": 36884, + "argv": [ + "/Applications/Godot.app/Contents/MacOS/Godot", + "--headless", + "--path", + "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/artifacts/original-completion-2026-10-02/p23-02-native-guest-bulk-local-r315/stage", + "--script", + "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/artifacts/original-completion-2026-10-02/p23-02-native-hot-loop-local-r329/candidate-guarded.gd", + "--verbose", + "--log-file", + "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/artifacts/original-completion-2026-10-02/p23-02-native-hot-loop-local-r329/local/engine-native.log" + ], + "ended_utc": "2026-10-12T03:57:44.034797+00:00", + "signal_scope": "tracked Popen child only; no external group signal" + }, + "strict_native_diagnostics": { + "errors": [], + "banner_count": 1, + "known_unicode_diagnostic_count": 0, + "unrecognized_unicode_diagnostic_count": 0, + "exit_leak_diagnostic_lines": [] + }, + "external_process_or_group_signals": 0, + "owned_child_process_helper_sha256": "c007e712c281573254faa1233c2ac92e100ab87393256a6dc096b35283e43037", + "adapter_owned_cleanup": { + "owned_child_created": true, + "engine_ended": true, + "actual_process_exit_code": 0, + "attempts": [ + { + "attempt": 1, + "kill_attempted": false, + "wait_timeout_seconds": 5, + "wait_completed": true, + "errors": [], + "poll_before": 0, + "wait_returned_code": 0, + "poll_after": 0 + } + ], + "errors": [] + }, + "adapter_error": null, + "old_helper_bytes_preserved": true, + "helper_wait_None_maps_to_actual_timeout_seconds": 5, + "explicit_helper_wait_timeouts_preserved": true, + "additional_cleanup_wait_timeout_seconds": 5, + "additional_cleanup_max_attempts": 3 + }, + "capture": { + "actual_captured_pids": [ + 36884 + ], + "cleanup": [ + { + "pid": 36884, + "cleanup": { + "owned_child_created": true, + "engine_ended": true, + "actual_process_exit_code": 0, + "attempts": [ + { + "attempt": 1, + "kill_attempted": false, + "wait_timeout_seconds": 5, + "wait_completed": true, + "errors": [], + "poll_before": 0, + "wait_returned_code": 0, + "poll_after": 0 + } + ], + "errors": [] + }, + "outer_cleanup_errors": [], + "engine_ended": true, + "outer_maximum_helper_invocations": 3, + "helper_maximum_waits": 3, + "per_wait_timeout_seconds": 5, + "foreign_process_or_group_signals": 0 + } + ], + "adapter_exception": null, + "all_captured_ended": true, + "outer_cleanup_errors": [], + "foreign_process_or_group_signals": 0, + "all_cleanup_before_outer_receipt": true, + "outer_receipt_written": true, + "outer_receipt_error": null, + "passed": true + } +} diff --git a/docs/plans/original-completion-2026-10-02/checkpoints/native-hot-loop-r325-r329/047-local-profile.json b/docs/plans/original-completion-2026-10-02/checkpoints/native-hot-loop-r325-r329/047-local-profile.json new file mode 100644 index 0000000..b0dff9f --- /dev/null +++ b/docs/plans/original-completion-2026-10-02/checkpoints/native-hot-loop-r325-r329/047-local-profile.json @@ -0,0 +1,20 @@ +{ + "actual_user_data_dir": "/Users/korizatou114/Library/Application Support/TheTree-PCK-tests-8457ef5c03f34d3e873fd1f43b7716fd-guest-runtime", + "before": "/Users/korizatou114/Library/Application Support/TheTree-PCK-tests-8457ef5c03f34d3e873fd1f43b7716fd-guest-startup", + "conditions": [ + true, + true, + true, + true, + true + ], + "conditions_checked": 5, + "custom_user_dir_name": "TheTree-PCK-tests-8457ef5c03f34d3e873fd1f43b7716fd-guest-runtime", + "expected_user_data_dir": "/Users/korizatou114/Library/Application Support/TheTree-PCK-tests-8457ef5c03f34d3e873fd1f43b7716fd-guest-runtime", + "fresh_before_parent_initialize": true, + "globalized_probe": "/Users/korizatou114/Library/Application Support/TheTree-PCK-tests-8457ef5c03f34d3e873fd1f43b7716fd-guest-runtime/guard-probe", + "profile_writes_before_guard": 0, + "project_settings_persisted": false, + "runtime_settings_only": true, + "verified": true +} diff --git a/docs/plans/original-completion-2026-10-02/checkpoints/native-hot-loop-r325-r329/048-local.gd b/docs/plans/original-completion-2026-10-02/checkpoints/native-hot-loop-r325-r329/048-local.gd new file mode 100644 index 0000000..cb727c8 --- /dev/null +++ b/docs/plans/original-completion-2026-10-02/checkpoints/native-hot-loop-r325-r329/048-local.gd @@ -0,0 +1,285 @@ +extends "res://tests/s3/test_guest_service.gd" + +const OriginalGuestScript = preload("/Users/korizatou114/Projects/\u3068\u308a\u306b\u304f\u5de5\u623f\u30b2\u30fc\u30e0\u5236\u4f5c\u90e8/artifacts/original-completion-2026-10-02/p23-02-native-guest-bulk-original-oracle-r315.gd") + +class CustomGuest: + extends GuestRuntime +class CustomDefinition: + extends GuestDefinition +class CustomService: + extends GuestService +class FakeCore: + extends RefCounted + var calls := 0 + func guest_candidate_batch(_owner, _id, _facilities, _context) -> Dictionary: + calls += 1 + return {&"supported": false} + +const ProtocolHelper = preload("/Users/korizatou114/Projects/\u3068\u308a\u306b\u304f\u5de5\u623f\u30b2\u30fc\u30e0\u5236\u4f5c\u90e8/artifacts/original-completion-2026-10-02/p23-02-native-hot-loop-local-r329/protocol_helper.gd") + +var _bulk_records: Array = [] +var _bulk_core: Object +var _bulk_cases := 0 + +func _bulk_state(service) -> Array: + var order: Array[StringName] = service._sorted_guest_ids_cache.duplicate() + var dirty: bool = service._guest_order_dirty + return [service.create_snapshot(), service.last_error, service._decision_rejections.duplicate(true), service._diagnostics.duplicate(true), order, dirty, service._pending_spawn_notifications.duplicate(true), service._external_mutation_blocked, service.current_minute] + +func _bulk_prime(service) -> void: + service.get_guests() + +func _bulk_original(service): + var oracle = OriginalGuestScript.new() + _expect(oracle.restore_snapshot(service.create_snapshot()), "literal original Guest independent state restored") + _bulk_prime(oracle) + return oracle + +func _bulk_array_shape(rows: Array) -> Array: + var shapes: Array = [rows.is_typed(), rows.get_typed_builtin(), rows.is_read_only()] + for row: Dictionary in rows: + shapes.append([row.is_typed(), row.is_read_only()]) + return shapes + +func _bulk_service() -> GuestService: + var service := _service_with_definitions() + service.spawn_guest(&"bulk", &"adventurer", 0, { + &"wallet_gold": 500, &"planned_exit_at": 1000, + &"state": GuestRuntimeType.State.DECIDING, + &"needs": {&"hunger": 80, &"sleep": 30, &"hygiene": 40, &"medical": 20, &"leisure": 60, &"intimacy": 50}, + }) + _bulk_prime(service) + return service + +func _bulk_compare(label: String, rows: Array, patch: Dictionary = {}, context: Dictionary = {}) -> void: + _bulk_cases += 1 + var service := _bulk_service() + var oracle = _bulk_original(service) + var guest := service.get_guest(&"bulk") + for key: Variant in patch: + guest.set(key, patch[key]) + oracle.get_guest(&"bulk").set(key, patch[key]) + service.last_error = "retained-before-pure-call" + var before := var_to_bytes(_bulk_state(service)) + var input_before := var_to_bytes(rows) + var batch: Dictionary = _bulk_core.call(&"guest_candidate_batch", service, &"bulk", rows, context) + _expect(bool(batch.get(&"supported", false)), label + " supported fixed pure input") + _expect(var_to_bytes(_bulk_state(service)) == before, label + " pure call preserves full Guest snapshot/errors/rejections") + _expect(var_to_bytes(rows) == input_before, label + " input ownership preserved") + if not bool(batch.get(&"supported", false)): return + var accepted: Array[Dictionary] = oracle.extract_candidates(&"bulk", rows, context) + var rejected: Array[Dictionary] = oracle.get_last_candidate_rejections(&"bulk") + var ranked: Array[Dictionary] = oracle.rank_candidates(&"bulk", accepted) + _expect(_bulk_array_shape(batch[&"accepted"]) == _bulk_array_shape(accepted), label + " accepted type/readonly shape original equality") + _expect(_bulk_array_shape(batch[&"ranked"]) == _bulk_array_shape(ranked), label + " ranked type/readonly shape original equality") + _expect(var_to_bytes(batch[&"accepted"]) == var_to_bytes(accepted), label + " entire ordered typed accepted original equality") + _expect(var_to_bytes(batch[&"rejected"]) == var_to_bytes(rejected), label + " entire ordered typed rejection original equality") + _expect(var_to_bytes(batch[&"ranked"]) == var_to_bytes(ranked), label + " entire ordered typed scoring/ranking original equality") + _bulk_records.append({&"case": label, &"input": rows.duplicate(true), &"context": context.duplicate(true), &"patch": patch.duplicate(true), &"actual": batch.duplicate(true), &"accepted_original": accepted, &"rejected_original": rejected, &"ranked_original": ranked}) + if not (batch[&"ranked"] as Array).is_empty(): + var accepted_before := var_to_bytes(batch[&"accepted"]) + var actual: Dictionary = batch[&"ranked"][0] + actual[&"ownership_probe"] = true + if actual.get(&"path_handle") is Dictionary: actual[&"path_handle"][&"ownership_probe"] = true + _expect(var_to_bytes(rows) == input_before, label + " ranked mutation is isolated from caller input") + _expect(var_to_bytes(batch[&"accepted"]) == accepted_before, label + " ranked deep copy is isolated from accepted candidates") + +func _test_bulk_candidates() -> void: + _bulk_core = ClassDB.instantiate(&"TheTreeNumericCore") + _expect(_bulk_core != null and _bulk_core.has_method(&"guest_candidate_batch"), "exact new batch binding exists") + if _bulk_core == null or not _bulk_core.has_method(&"guest_candidate_batch"): return + var base := _candidate(&"a", {&"need_relief": {&"hunger": 80, &"leisure": 20}, &"facility_tags": [&"meal", &"leisure", &"meal"], &"path_handle": {&"path": [Vector3i.ZERO, Vector3i(1, 0, 0)], &"target_cell": Vector3i(1, 0, 0)}}) + _bulk_compare("ordinary", [base]) + _bulk_compare("empty", []) + _bulk_compare("skip-nondictionary-and-empty-id", [null, 4, "ignored", {}, {&"facility_id": &""}, base]) + var duplicates: Array = [] + for i in range(64): + var row := base.duplicate(true) + row[&"facility_id"] = StringName("b" if i % 3 == 0 else "a") + row[&"probe_index"] = i + duplicates.append(row) + _bulk_compare("equal-sort-keys64", duplicates) + var branches := [ + {&"need_relief": {&"rest": 1}, &"inputs_available": false}, + {&"need_effects": {&"hunger": 1.0}}, + {&"need_relief": null}, {&"need_relief": true}, + {&"inputs_available": false, &"adult_only": true}, + {&"adult_only": true}, {&"required_guest_tags": [&"missing"]}, + {&"forbidden_guest_tags": [&"general"]}, {&"allowed_guest_types": [&"other"]}, + {&"is_open": false, &"reachable": false}, {&"reachable": false}, + {&"travel_minutes": 1001}, {&"price_gold": 10000}, + {&"wait_minutes": 120}, {&"risk_level": 100}, {&"illegal_level": 100}, + {&"expected_quality": 0}, {&"is_open": false, &"open_at_arrival": true}, + {&"credit_allowed": true, &"price_gold": 550}, + {&"discretionary": true, &"price_gold": 400}, + {&"respect_survival_reserve": false, &"discretionary": true, &"price_gold": 400}, + {&"risk": 90, &"risk_level": 0, &"risk_sensitivity": 3}, + {&"legal_permission_override": true, &"illegal_level": 100}, + {&"need_relief": -100, &"travel_minutes": -2, &"wait_minutes": -3}, + {&"ok": false}, {&"ok": 0}, + {&"preference": 9223372036854775807, &"group_fit": -9223372036854775808, &"repetition_penalty": -9223372036854775808}, + {&"price_gold": 9223372036854775807, &"credit_allowed": true}, + {&"risk": 9223372036854775807, &"risk_level": 0, &"risk_sensitivity": 9223372036854775807}, + {&"need_relief": {"hunger": 9223372036854775807, "medical": -9223372036854775808}}, + {&"novelty": -10, &"urgency": 77, &"price_burden": 100}, + ] + for i in range(branches.size()): + var row := base.duplicate(true) + for key: Variant in branches[i]: row[key] = branches[i][key] + _bulk_compare("branch-%02d" % i, [row, base]) + var string_keys := {} + for key: Variant in base: string_keys[String(key)] = base[key] + _bulk_compare("string-keys", [string_keys]) + _bulk_compare("visited-defaults", [base], {&"visited_facility_ids": [&"a"]}) + _bulk_compare("addiction-removes-reserve", [_candidate(&"a", {&"discretionary": true, &"price_gold": 500})], {&"addiction_by_tag": {&"gambling": 75}}) + _bulk_compare("wallet-wrap", [base], {&"wallet_gold": 9223372036854775807, &"credit_limit_gold": 9223372036854775807, &"debt_gold": -9223372036854775808}) + _bulk_compare("context-gates", [base], {}, {&"absolute_minute": 999, &"wait_hard_limit": 1}) + var readonly := base.duplicate(true) + readonly.make_read_only() + var readonly_rows: Array = [readonly] + readonly_rows.make_read_only() + _bulk_compare("readonly-input", readonly_rows) + var rank_rows: Array = [ + _candidate(&"priority", {&"need_relief": {&"hunger": 1}, &"novelty": 0}), + _candidate(&"medical", {&"need_relief": {&"medical": 100}, &"preference": 100}), + _candidate(&"utility", {&"need_relief": {&"hunger": 1}, &"novelty": 10}), + _candidate(&"urgency-a", {&"need_relief": {&"hunger": 1}, &"urgency": 90}), + _candidate(&"urgency-b", {&"need_relief": {&"hunger": 1}, &"urgency": 10}), + _candidate(&"travel-a", {&"need_relief": {&"hunger": 1}, &"travel_minutes": 1, &"group_fit": 1}), + _candidate(&"travel-b", {&"need_relief": {&"hunger": 1}, &"travel_minutes": 2, &"group_fit": 2}), + ] + _bulk_compare("all-rank-tuple-components", rank_rows) + _test_bulk_fallback_and_decision(base) + _test_bulk_custom_and_typed(base) + +func _test_bulk_fallback_and_decision(base: Dictionary) -> void: + for label in ["ordinary", "float-conversion", "reach-callback", "reserve-callback"]: + var actual := _bulk_service() + var oracle = _bulk_original(actual) + var a_signals: Array = [] + var b_signals: Array = [] + actual.guest_state_changed.connect(func(id: StringName, old: int, new_state: int, reason: StringName) -> void: a_signals.append([id, old, new_state, reason])) + oracle.guest_state_changed.connect(func(id: StringName, old: int, new_state: int, reason: StringName) -> void: b_signals.append([id, old, new_state, reason])) + var a_calls: Array = [] + var b_calls: Array = [] + var a_context := {&"absolute_minute": 10} + var b_context := {&"absolute_minute": 10} + if label == "reach-callback": + a_context[&"reachability_callback"] = func(_guest: GuestRuntime, candidate: Dictionary, _context: Dictionary) -> bool: + a_calls.append(candidate[&"facility_id"]); candidate[&"callback_probe"] = true; return true + b_context[&"reachability_callback"] = func(_guest: GuestRuntime, candidate: Dictionary, _context: Dictionary) -> bool: + b_calls.append(candidate[&"facility_id"]); candidate[&"callback_probe"] = true; return true + if label == "reserve-callback": + a_context[&"reserve_callback"] = func(_guest_id: StringName, facility_id: StringName, candidate: Dictionary) -> bool: + a_calls.append(facility_id); candidate[&"callback_probe"] = true; return true + b_context[&"reserve_callback"] = func(_guest_id: StringName, facility_id: StringName, candidate: Dictionary) -> bool: + b_calls.append(facility_id); candidate[&"callback_probe"] = true; return true + var row := base.duplicate(true) + if label == "float-conversion": row[&"travel_minutes"] = 1.75 + var input_before := var_to_bytes([row]) + var state_before := var_to_bytes(_bulk_state(actual)) + var batch: Dictionary = _bulk_core.call(&"guest_candidate_batch", actual, &"bulk", [row], a_context) + _expect(bool(batch[&"supported"]) == (label == "ordinary"), label + " supported or whole original fallback domain") + _expect(var_to_bytes(_bulk_state(actual)) == state_before and a_calls.is_empty(), label + " probe causes no observable state/callback effects") + var got := actual.decide_with_native_candidates(_bulk_core, &"bulk", [row], 10, a_context) + var expected: Dictionary = oracle.decide(&"bulk", [row], 10, b_context) + _expect(var_to_bytes(got) == var_to_bytes(expected), label + " entire typed decision result matches original") + _expect(var_to_bytes(_bulk_state(actual)) == var_to_bytes(_bulk_state(oracle)), label + " entire Guest state/errors/rejection matches original") + _expect(a_calls == b_calls, label + " callback count/order preserves original single dispatch") + _expect(a_signals == b_signals, label + " state notification content/order original equality") + _expect(var_to_bytes([row]) == input_before, label + " decision input ownership preserved") + _bulk_records.append({&"decision_case": label, &"actual_result": got, &"original_result": expected, &"actual_state": _bulk_state(actual), &"original_state": _bulk_state(oracle), &"actual_calls": a_calls, &"original_calls": b_calls, &"actual_signals": a_signals, &"original_signals": b_signals}) + +func _test_bulk_custom_and_typed(base: Dictionary) -> void: + for label in ["typed-dictionary", "custom-guest", "custom-definition", "custom-service", "fake-core", "unknown-guest", "missing-definition"]: + var actual = _bulk_service() + var oracle = _bulk_original(actual) + var row: Dictionary = base.duplicate(true) + if label == "typed-dictionary": + var typed: Dictionary[StringName, Variant] = {} + for key: Variant in row: typed[StringName(key)] = row[key] + row = typed + if label == "custom-guest": + var custom := CustomGuest.new() + custom.configure(&"bulk", &"adventurer", actual.get_guest(&"bulk").create_snapshot()) + actual._guests[&"bulk"] = custom + if label == "custom-definition": + var custom := CustomDefinition.new() + custom.configure(&"adventurer", actual.get_definition(&"adventurer").create_snapshot()) + actual._definitions[&"adventurer"] = custom + if label == "custom-service": + var custom := CustomService.new() + _expect(custom.restore_snapshot(actual.create_snapshot()), label + " custom owner independent restore") + actual = custom + _bulk_prime(actual) + if label == "unknown-guest": + actual._guests.clear(); actual._guest_order_dirty = true + oracle._guests.clear(); oracle._guest_order_dirty = true + if label == "missing-definition": + actual._definitions.erase(&"adventurer") + oracle._definitions.erase(&"adventurer") + _bulk_prime(actual); _bulk_prime(oracle) + var before := var_to_bytes(_bulk_state(actual)) + var input_before := var_to_bytes([row]) + var selected_core: Object = FakeCore.new() if label == "fake-core" else _bulk_core + var probe: Dictionary = _bulk_core.call(&"guest_candidate_batch", actual, &"bulk", [row], {}) + _expect(bool(probe[&"supported"]) == (label == "fake-core"), label + " unsupported domain gate") + _expect(var_to_bytes(_bulk_state(actual)) == before, label + " unsupported returns before error/state changes") + var got: Dictionary = actual.decide_with_native_candidates(selected_core, &"bulk", [row], 10, {}) + var expected: Dictionary = oracle.decide(&"bulk", [row], 10, {}) + _expect(var_to_bytes(got) == var_to_bytes(expected), label + " literal original decision equality") + _expect(var_to_bytes(_bulk_state(actual)) == var_to_bytes(_bulk_state(oracle)), label + " literal original state/error/rejection equality") + _expect(var_to_bytes([row]) == input_before, label + " original fallback input ownership") + if label == "fake-core": _expect(int(selected_core.get("calls")) == 0, "non-native core does not receive public wrapper call") + _bulk_records.append({&"fallback_case": label, &"actual_result": got, &"original_result": expected, &"actual_state": _bulk_state(actual), &"original_state": _bulk_state(oracle)}) + _bulk_core = null + +func _bulk_sha256(bytes: PackedByteArray) -> String: + var digest := HashingContext.new() + if digest.start(HashingContext.HASH_SHA256) != OK: return "" + if digest.update(bytes) != OK: return "" + return digest.finish().hex_encode() + +func _run() -> void: + _test_definition_and_runtime_contract() + _test_need_updates_and_single_clamp() + _test_need_context_and_adult_gate() + _test_shared_need_context_fast_paths() + _test_batched_need_updates_match_individual_api() + _test_candidate_filtering() + _test_integer_utility_and_stable_tie_break() + _test_primary_need_priority() + _test_state_machine_and_loop_guard() + _test_atomic_queue_abandonment() + _test_service_interruption_is_idempotent() + _test_service_need_effect_schema() + _test_path_failure_safe_recovery() + _test_exit_persistence_boundary() + _test_overdue_departure_snapshot() + _test_snapshot_round_trip_and_atomic_rejection() + _test_v1_need_migration_and_schema_rejection() + _test_deterministic_decision_snapshot() + _test_bulk_candidates() + var output := OS.get_environment("THE_TREE_RUNTIME_MONITOR_OUTPUT") + var protocol = ProtocolHelper.new() + var protocol_core: Object = ClassDB.instantiate(&"TheTreeNumericCore") + var protocol_report: Dictionary = protocol.run_frozen(protocol_core,"/Users/korizatou114/Projects/\u3068\u308a\u306b\u304f\u5de5\u623f\u30b2\u30fc\u30e0\u5236\u4f5c\u90e8/artifacts/original-completion-2026-10-02/p23-02-native-bridge-Godot-protocol-preparation-r73/pinned-inputs.json") + var core_ref: WeakRef = weakref(protocol_core) + protocol_core = null + _expect(core_ref.get_ref() == null, "combined protocol core with populated cache releases") + _checks += int(protocol_report["checks"]) + _failures += protocol_report["failures"].size() + var protocol_file := FileAccess.open(output.path_join("protocol-report.json"),FileAccess.WRITE) + if protocol_file == null: quit(98); return + protocol_file.store_string(JSON.stringify(protocol_report,"\t")+"\n");protocol_file.close() + protocol = null + var bytes := var_to_bytes(_bulk_records) + var file := FileAccess.open(output.path_join("fixtures.varbytes"), FileAccess.WRITE) + if file == null: quit(98); return + file.store_buffer(bytes); file.close() + var report := {"passed": _failures == 0, "checks": _checks, "failures": _failures, "pure_case_count": _bulk_cases, "decision_case_count": 4, "custom_fallback_case_count": 7, "original_s3_invocations": 18, "protocol_cases": 50, "protocol_checks": int(protocol_report["checks"]), "pid": OS.get_process_id(), "fixture_bytes": bytes.size(), "fixture_sha256": _bulk_sha256(bytes), "performance_accepted": false, "unit_accepted": false} + file = FileAccess.open(output.path_join("report.json"), FileAccess.WRITE) + if file == null: quit(98); return + file.store_string(JSON.stringify(report, "\t") + "\n"); file.close() + print("Guest bulk local: %d checks, %d failures" % [_checks, _failures]) + quit(0 if _failures == 0 else 1) diff --git a/docs/plans/original-completion-2026-10-02/checkpoints/native-hot-loop-r325-r329/049-preexecution-root-verification.json.gz b/docs/plans/original-completion-2026-10-02/checkpoints/native-hot-loop-r325-r329/049-preexecution-root-verification.json.gz new file mode 100644 index 0000000..cd21d65 Binary files /dev/null and b/docs/plans/original-completion-2026-10-02/checkpoints/native-hot-loop-r325-r329/049-preexecution-root-verification.json.gz differ diff --git a/docs/plans/original-completion-2026-10-02/checkpoints/native-hot-loop-r325-r329/050-protocol_helper.gd b/docs/plans/original-completion-2026-10-02/checkpoints/native-hot-loop-r325-r329/050-protocol_helper.gd new file mode 100644 index 0000000..71cba2b --- /dev/null +++ b/docs/plans/original-completion-2026-10-02/checkpoints/native-hot-loop-r325-r329/050-protocol_helper.gd @@ -0,0 +1,292 @@ +extends RefCounted +# Isolated protocol/marshalling QA only. No production PathService or performance claim. + +class EdgeProvider extends RefCounted: + var rows: Array = [] + var calls: Array[int] = [] + func edges(cell: Vector3i) -> Array: + calls.append(cell.x) + return rows[cell.x] + +class ShrinkingProvider extends RefCounted: + var cells: Array[Vector3i] = [] + var calls: Array[Vector3i] = [] + func edges(cell: Vector3i) -> Array: + calls.append(cell) + cells.resize(2) + return [[Vector3i(1, 0, 0), 7]] + +var failures: Array[String] = [] +var checks: int = 0 +var cases: Array = [] +var cached_cases: Array = [] + + +func _check(condition: bool, label: String) -> void: + checks += 1 + if not condition: + failures.append(label) + +func _ints(text: String) -> Array[int]: + var result: Array[int] = [] + for token in text.replace("\n", " ").split(" ", false): + _check(token.is_valid_int() and str(token.to_int()) == token, "canonical signed integer " + token) + result.append(token.to_int()) + return result + +func _line(values) -> String: + var text: PackedStringArray = [] + for value in values: + text.append(str(value)) + return " ".join(text) + "\n" + +func _serialized(output: Dictionary) -> String: + var status = output.get(&"status", "missing") + _check(typeof(status) == TYPE_STRING, "status String") + var text: String = str(status) + "\n" + for key in output: + _check(typeof(key) == TYPE_STRING_NAME, "output StringName key") + if status != "usable": + _check(output.size() == 1, "unusable/wrapped status only") + return text + _check(output.size() == 6, "usable six output fields") + _check(typeof(output.get(&"cost_values")) == TYPE_PACKED_INT64_ARRAY, "cost PackedInt64Array") + _check(typeof(output.get(&"parent_indices")) == TYPE_PACKED_INT64_ARRAY, "parents PackedInt64Array") + for name in [&"discovered", &"parent_discovered"]: + var values = output.get(name) + _check(typeof(values) == TYPE_ARRAY and values.is_typed() and values.get_typed_builtin() == TYPE_INT, "typed Array[int] " + str(name)) + for name in [&"cost_values", &"parent_indices", &"discovered", &"parent_discovered"]: + text += _line(output.get(name, [])) + var settled: Dictionary = output.get(&"settled", {}) + var indices: Array[int] = [] + for cell in settled: + _check(typeof(cell) == TYPE_VECTOR3I and typeof(settled[cell]) == TYPE_BOOL and settled[cell], "settled Vector3i -> true") + indices.append(cell.x) + text += _line(indices) + return text + +func _case(core, row: Dictionary) -> void: + var tokens := _ints(FileAccess.get_file_as_string(row["input_path"])) + var count: int = tokens[0] + var start: int = tokens[1] + var goal_count: int = tokens[2] + var cursor: int = 3 + var cells: Array[Vector3i] = [] + var indices: Dictionary = {} + for i in count: + var cell := Vector3i(i, 0, 0) + cells.append(cell) + indices[cell] = i + var goals: Dictionary = {} + for i in goal_count: + goals[Vector3i(tokens[cursor], 0, 0)] = true + cursor += 1 + var provider := EdgeProvider.new() + for i in count: + var edge_count: int = tokens[cursor] + cursor += 1 + var edges: Array = [] + for j in edge_count: + edges.append([Vector3i(tokens[cursor], 0, 0), tokens[cursor + 1]]) + cursor += 2 + provider.rows.append(edges) + _check(cursor == tokens.size(), "complete input consumption") + var before := var_to_bytes([cells, indices, goals, provider.rows]) + var output = core.call("search", cells, indices, start, goals, Callable(provider, "edges")) + _check(typeof(output) == TYPE_DICTIONARY, "result Dictionary") + var actual := _serialized(output) + var expected := FileAccess.get_file_as_string(row["expected_path"]) + _check(actual == expected, "whole protocol bytes: " + str(row["group"]) + " / " + str(row["case"])) + _check(var_to_bytes([cells, indices, goals, provider.rows]) == before, "caller input ownership") + cases.append({"group": row["group"], "case": row["case"], "actual": actual, "expected_sha256": row["expected_sha256"], "equal": actual == expected, "provider_calls": provider.calls.duplicate()}) + # Separate cached API: one immutable caller graph until explicit clear. + core.call("clear_edge_cache") + provider.calls.clear() + var cold: Dictionary = core.call("search_cached", cells, indices, start, goals, Callable(provider, "edges")) + var cold_bytes := var_to_bytes(cold) + var cold_calls := provider.calls.duplicate() + _check(_serialized(cold) == expected and cold_bytes == var_to_bytes(output), "cached cold full typed output equals original") + provider.calls.clear() + var warm: Dictionary = core.call("search_cached", cells, indices, start, goals, Callable(provider, "edges")) + _check(_serialized(warm) == expected and var_to_bytes(warm) == cold_bytes, "cached warm full typed output equals original") + var warm_calls := provider.calls.duplicate() + _check(provider.calls.is_empty(), "warm search makes no already-cached provider calls") + # The original API remains uncached even on an instance with a warm cache. + provider.calls.clear() + var uncached: Dictionary = core.call("search", cells, indices, start, goals, Callable(provider, "edges")) + _check(var_to_bytes(uncached) == cold_bytes and provider.calls == cold_calls, "original API ignores warm native cache") + if cold.get(&"status") == "usable": + cold[&"cost_values"][0] = 12345 + cold[&"parent_indices"][0] = 12345 + cold[&"discovered"].clear() + cold[&"parent_discovered"].clear() + cold[&"settled"].clear() + var owned: Dictionary = core.call("search_cached", cells, indices, start, goals, Callable(provider, "edges")) + _check(var_to_bytes(owned) == cold_bytes, "external result mutations do not alter cached edge vectors or fresh output") + core.call("clear_edge_cache") + provider.calls.clear() + var cleared: Dictionary = core.call("search_cached", cells, indices, start, goals, Callable(provider, "edges")) + _check(var_to_bytes(cleared) == cold_bytes and provider.calls == cold_calls, "clear repeats original lazy calls and full output") + _check(var_to_bytes([cells, indices, goals, provider.rows]) == before, "cached APIs preserve all caller inputs") + cached_cases.append({"group":row["group"], "case":row["case"], "cold":_serialized(cleared), "warm":_serialized(warm), "expected_sha256":row["expected_sha256"], "cold_calls":cold_calls, "warm_calls":warm_calls, "equal":var_to_bytes(cleared)==cold_bytes and var_to_bytes(warm)==cold_bytes}) + var provider_weak: WeakRef = weakref(provider) + provider = null + _check(provider_weak.get_ref() == null, "native cache does not retain the provider Callable") + output = null + cold = {}; warm = {}; uncached = {}; cleared = {} + +func _prefix_wrap(core, malformed) -> void: + var cells: Array[Vector3i] = [Vector3i.ZERO, Vector3i(1, 0, 0), Vector3i(2, 0, 0)] + var indices: Dictionary = {cells[0]: 0, cells[1]: 1, cells[2]: 2} + var provider := EdgeProvider.new() + provider.rows = [[[cells[1], 9223372036854775802]], [[cells[2], 10], malformed], []] + var goals: Dictionary = {cells[2]: true} + var before := var_to_bytes([cells, indices, goals, provider.rows]) + var output = core.call("search", cells, indices, 0, goals, Callable(provider, "edges")) + _check(_serialized(output) == "wrapped\n", "wrap before later malformed row") + _check(var_to_bytes([cells, indices, goals, provider.rows]) == before, "prefix-wrap caller ownership") + provider = null + output = null + +func _shrink_transfer(core) -> void: + var cells: Array[Vector3i] = [Vector3i.ZERO, Vector3i(1, 0, 0), Vector3i(2, 0, 0)] + var indices: Dictionary = {cells[0]: 0, cells[1]: 1, cells[2]: 2} + var goals: Dictionary = {cells[1]: true} + var fixed_before := var_to_bytes([indices, goals]) + var provider := ShrinkingProvider.new() + provider.cells = cells + var output = core.call("search", cells, indices, 0, goals, Callable(provider, "edges")) + # Old bridge transfers only the post-search prefix: the allocated third slots stay zero. + var expected := "usable\n0 7 0\n-1 0 0\n0 1\n1\n0 1\n" + _check(_serialized(output) == expected, "provider shrink preserves original transferred prefix and zero tail") + _check(cells.size() == 2 and provider.calls == [Vector3i.ZERO], "single lazy callback shrinks shared cells") + _check(var_to_bytes([indices, goals]) == fixed_before, "shrink fixture maps unchanged") + var costs: PackedInt64Array = output[&"cost_values"].duplicate() + var parents: PackedInt64Array = output[&"parent_indices"].duplicate() + costs[0] = 12345 + parents[0] = 12345 + _check(_serialized(output) == expected, "PackedInt64Array explicit duplicates do not mutate retained result") + var shared_costs: PackedInt64Array = output[&"cost_values"] + var shared_parents: PackedInt64Array = output[&"parent_indices"] + shared_costs[0] = 12345 + shared_parents[0] = 12345 + _check(_serialized(output) == "usable\n12345 7 0\n12345 0 0\n0 1\n1\n0 1\n", "PackedInt64Array aliases retain documented shared mutation") + # A new search allocates new outputs; external result mutations cannot poison it. + output[&"cost_values"] = PackedInt64Array([12345]) + output[&"parent_indices"] = PackedInt64Array([12345]) + cells.append(Vector3i(2, 0, 0)) + provider.calls.clear() + var repeated = core.call("search", cells, indices, 0, goals, Callable(provider, "edges")) + _check(_serialized(repeated) == expected, "fresh result ownership after caller mutation") + _check(provider.calls == [Vector3i.ZERO] and cells.size() == 2, "repeat shrink lazy callback") + provider = null + output = null + repeated = null + +func _cached_goals_and_reset(core) -> void: + core.call("clear_edge_cache") + var cells: Array[Vector3i] = [Vector3i.ZERO, Vector3i(1, 0, 0), Vector3i(2, 0, 0)] + var indices: Dictionary = {cells[0]:0, cells[1]:1, cells[2]:2} + var provider := EdgeProvider.new() + provider.rows = [[[cells[1],7],[cells[2],20]], [[cells[2],3]], []] + var goals: Dictionary = {cells[1]:true} + var first: Dictionary = core.call("search_cached", cells, indices, 0, goals, Callable(provider,"edges")) + _check(first[&"status"] == "usable" and first[&"cost_values"][1] == 7 and provider.calls == [0], "first goal stops before requesting goal edges or unseen node") + provider.calls.clear(); goals = {cells[2]:true} + var later: Dictionary = core.call("search_cached", cells, indices, 0, goals, Callable(provider,"edges")) + _check(provider.calls == [1], "changed goals lazily request only the newly expanded node") + var oracle: Dictionary = core.call("search", cells, indices, 0, goals, Callable(provider,"edges")) + _check(var_to_bytes(later)==var_to_bytes(oracle), "changed goals full typed equivalence") + provider.calls.clear() + var shifted: Dictionary = core.call("search_cached", cells, indices, 1, goals, Callable(provider,"edges")) + _check(provider.calls.is_empty(), "changed start reuses previously expanded node only") + oracle = core.call("search", cells, indices, 1, goals, Callable(provider,"edges")) + _check(var_to_bytes(shifted)==var_to_bytes(oracle), "changed start full typed equivalence") + # Graph mutation obeys the explicit API contract: clear before reuse. + provider.rows[0][0][1] = 2 + core.call("clear_edge_cache"); provider.calls.clear() + var changed: Dictionary = core.call("search_cached", cells, indices, 0, goals, Callable(provider,"edges")) + _check(provider.calls == [0,1], "clear after edge mutation repopulates exact lazy nodes") + oracle = core.call("search", cells, indices, 0, goals, Callable(provider,"edges")) + _check(var_to_bytes(changed)==var_to_bytes(oracle) and changed[&"cost_values"][2]==5, "mutated graph after clear has complete correct output") + core.call("clear_edge_cache") + provider = null + +func _cached_prefix_wrap(core, malformed) -> void: + core.call("clear_edge_cache") + var cells: Array[Vector3i] = [Vector3i.ZERO,Vector3i(1,0,0),Vector3i(2,0,0)] + var indices: Dictionary = {cells[0]:0,cells[1]:1,cells[2]:2} + var goals: Dictionary = {cells[2]:true} + var provider := EdgeProvider.new() + provider.rows = [[[cells[1],9223372036854775802]], [[cells[2],10],malformed], []] + var before := var_to_bytes([cells,indices,goals,provider.rows]) + var cold: Dictionary = core.call("search_cached",cells,indices,0,goals,Callable(provider,"edges")) + _check(_serialized(cold)=="wrapped\n" and provider.calls==[0,1], "cached prefix real wrap before later malformed row") + provider.calls.clear() + var warm: Dictionary = core.call("search_cached",cells,indices,0,goals,Callable(provider,"edges")) + _check(var_to_bytes(warm)==var_to_bytes(cold) and provider.calls.is_empty(), "cached invalid-prefix wrap repeat unchanged") + _check(var_to_bytes([cells,indices,goals,provider.rows])==before, "cached prefix all caller inputs unchanged") + core.call("clear_edge_cache"); provider = null + +func _warm_for_teardown(core) -> void: + core.call("clear_edge_cache") + var cells: Array[Vector3i] = [Vector3i.ZERO,Vector3i(1,0,0)] + var indices: Dictionary = {cells[0]:0,cells[1]:1} + var goals: Dictionary = {cells[1]:true} + var provider := EdgeProvider.new() + provider.rows = [[[cells[1],7]], []] + var output: Dictionary = core.call("search_cached",cells,indices,0,goals,Callable(provider,"edges")) + _check(output[&"status"]=="usable" and output[&"cost_values"][1]==7 and provider.calls==[0],"populate native cache before actual core teardown") + var provider_weak: WeakRef = weakref(provider) + provider = null + _check(provider_weak.get_ref()==null,"warm native cache retains no Callable or caller objects") + +class ReentrantProvider extends RefCounted: + var core: Object + var cells: Array[Vector3i] + var indices: Dictionary + var goals: Dictionary + var entered := false + var inner_result: Dictionary + func edges(cell: Vector3i) -> Array: + if cell.x != 0: return [] + if not entered: + entered = true + core.call("clear_edge_cache") + var inner := EdgeProvider.new() + inner.rows = [[[cells[1],9]], []] + inner_result = core.call("search_cached",cells,indices,0,goals,Callable(inner,"edges")) + return [[cells[1],7]] + +func _reentrant_collision(core) -> Dictionary: + core.call("clear_edge_cache") + var cells: Array[Vector3i] = [Vector3i.ZERO,Vector3i(1,0,0)] + var indices: Dictionary = {cells[0]:0,cells[1]:1} + var goals: Dictionary = {cells[1]:true} + var provider := ReentrantProvider.new() + provider.core=core;provider.cells=cells;provider.indices=indices;provider.goals=goals + var outer: Dictionary = core.call("search_cached",cells,indices,0,goals,Callable(provider,"edges")) + _check(outer[&"status"] == "usable" and outer[&"cost_values"][1] == 7, "reentrant collision returns original converted outer row") + _check(provider.inner_result[&"status"] == "usable" and provider.inner_result[&"cost_values"][1] == 9, "inner populated same cache key before outer insertion") + var warm: Dictionary = core.call("search_cached",cells,indices,0,goals,Callable(provider,"edges")) + _check(warm[&"status"] == "usable" and warm[&"cost_values"][1] == 9, "warm preserves inner first cache publication") + var result := {"outer":outer,"inner":provider.inner_result,"warm":warm} + var ref: WeakRef = weakref(provider) + provider = null + _check(ref.get_ref() == null, "borrowed native cache retains no reentrant provider") + core.call("clear_edge_cache") + return result + +func run_frozen(core, manifest_path: String) -> Dictionary: + var manifest: Dictionary = JSON.parse_string(FileAccess.get_file_as_string(manifest_path)) + _check(manifest["cases"].size() == 50, "same frozen fifty-input scope") + for row: Dictionary in manifest["cases"]: _case(core,row) + _prefix_wrap(core,true) + _prefix_wrap(core,[Vector3i(2,0,0),false]) + _shrink_transfer(core) + _cached_goals_and_reset(core) + _cached_prefix_wrap(core,true) + _cached_prefix_wrap(core,[Vector3i(2,0,0),false]) + var collision := _reentrant_collision(core) + _warm_for_teardown(core) + return {"passed":failures.is_empty(),"checks":checks,"failures":failures,"cases":cases,"cached_cases":cached_cases,"reentrant_collision":collision,"unit_accepted":false,"performance_accepted":false} diff --git a/docs/plans/original-completion-2026-10-02/checkpoints/native-hot-loop-r325-r329/051-p23-02-native-hot-loop-local-r329.py b/docs/plans/original-completion-2026-10-02/checkpoints/native-hot-loop-r325-r329/051-p23-02-native-hot-loop-local-r329.py new file mode 100644 index 0000000..08abfe8 --- /dev/null +++ b/docs/plans/original-completion-2026-10-02/checkpoints/native-hot-loop-r325-r329/051-p23-02-native-hot-loop-local-r329.py @@ -0,0 +1,64 @@ +# coding: utf-8 +"""One owned Guest batch ABI/local/affected-S3 execution; no full regression or release acceptance.""" +from pathlib import Path +import hashlib,importlib.util,json,os,shutil,sys,uuid +sys.dont_write_bytecode=True +R=Path(__file__).resolve().parents[2];A=R/'artifacts/original-completion-2026-10-02';O=A/'p23-02-native-hot-loop-local-r329';D=R/'docs/plans/original-completion-2026-10-02' +NAMES=['IMPLEMENTATION_PLAN.md','PROGRESS_INTEGRATION.md','work_packages.json','progress_snapshot.json'] +def mod(name,p): + s=importlib.util.spec_from_file_location(name,p);m=importlib.util.module_from_spec(s);s.loader.exec_module(m);return m +def main(): + r=mod('guest_local_runner',R/'tools/run_all_tests.py');outer=mod('guest_local_owned',r.SUPPORT/'owned_outer.py');plan=r.read(O/'input.json');fixed=plan['pins'] + assert not (O/'execution.json').exists() + stage=Path(plan['stage']);parent=Path(plan['parent']);public=(stage/'project.godot').read_bytes();private=Path(fixed['private_library']['path']).parent + source=r.source_refs(R);player=r.default_player(R);docs={n:r.sha(D/n) for n in NAMES} + assert source==plan['source_before'] and player==plan['default_before'] and docs==plan['doc4_before'] + def protect(): + assert r.source_refs(R)==source and r.default_player(R)==player and {n:r.sha(D/n) for n in NAMES}==docs + assert r.inventory(parent,stage=True)==plan['parent_stage_inventory'] and r.inventory(stage,stage=True)==plan['expected_stage'] + assert r.inventory(r.SUPPORT)==plan['support_inventory'] and r.inventory(private)==plan['private_library_inventory'] + for k,q in fixed.items(): + p=Path(q['path']);assert p.stat().st_size==q['bytes'] and r.sha(p)==q['sha256'],k + def growth():return sum(p.stat().st_size for p in O.rglob('*') if p.is_file() and p.relative_to(O).parts[0]!='stage') + protect();host=r.read(fixed['host_review']['path']);assert host['host_build_accepted'] and not host['blockers'] + prepath=A/'p23-02-native-hot-loop-local-preexecution-independent-review-r329.json';pre=r.read(prepath);assert pre['execution_allowed'] and not pre['blockers'] + for group in ['evidence_sha256','canonical_doc4_context_pins']: + for q in pre[group].values(): + p=Path(q['path']);assert p.stat().st_size==q['bytes'] and r.sha(p)==q['sha256'] + root=r.read(O/'preexecution-root-verification.json');assert root['passed'] and root['review_sha256']==r.sha(prepath) + phases=[];samples=[];error=None;cleanup=[];cleanup_errors=[];report=None + try: + protect();assert not outer.native.helper['live_engines']() and shutil.disk_usage(A).free>=plan['capacity_floor_bytes'] and growth()<=plan['maximum_measurement_growth_bytes'] + output=O/'local-measurement';output.mkdir();ident=uuid.uuid4().hex;startup='TheTree-PCK-tests-'+ident+'-guest-startup';profile='TheTree-PCK-tests-'+ident+'-guest-runtime' + assert not r.profile_path(startup).exists() and not r.profile_path(profile).exists() + env=r.fresh_environment(os.environ);env.update(THE_TREE_PACK_PROFILE_NAME=profile,THE_TREE_PACK_PROFILE_EXPECTED=str(r.profile_path(profile)),THE_TREE_PACK_PROFILE_REPORT=str(O/'local-profile.json'),THE_TREE_RUNTIME_MONITOR_OUTPUT=str(output)) + (stage/'project.godot').write_bytes(r.private_project(public,startup));phase=O/'local' + argv=[fixed['engine']['path'],'--headless','--path',str(stage),'--script',fixed['starter']['path'],'--verbose','--log-file',str(phase/'engine-native.log')] + result,capture=outer.run_phase(argv,phase,90,env,now_fn=r.now,dump_fn=r.write,sha_fn=r.sha,live_engines_fn=outer.native.helper['live_engines']) + phases.append(dict(name='local',result=result,capture=capture));r.write(O/'local-phase.json',phases[0]) + report=r.read(output/'report.json') if (output/'report.json').exists() else None + if report:samples.append(dict(name='local',report=report)) + assert result is not None and result['exit_code']==0 and not result['error_lines'] and not result['timed_out'] and capture['passed'] + logs=''.join((phase/n).read_text() for n in ['stdout.log','stderr.log','engine-native.log']) + assert 'Orphan StringName:' not in logs and 'unclaimed string names' not in logs + assert report and report['passed'] and report['failures']==0 and report['pid']==result['pid'] + count=plan['expected_case_counts'];assert report['pure_case_count']==count['pure'] and report['decision_case_count']==count['decision'] and report['custom_fallback_case_count']==count['custom_fallback'] and report['original_s3_invocations']==count['original_s3_invocations'] + stdout=(phase/'stdout.log').read_text();assert stdout.count('Guest bulk local: %d checks, 0 failures'%report['checks'])==1 and stdout.count('NATIVE_ADAPTER_LIBRARY_REGISTERED')==1 + fixture=output/'fixtures.varbytes';assert fixture.stat().st_size==report['fixture_bytes'] and r.sha(fixture)==report['fixture_sha256'] + protocol=r.read(output/'protocol-report.json');assert protocol['passed'] and not protocol['failures'] and len(protocol['cases'])==50 and len(protocol['cached_cases'])==50 and protocol['checks']==report['protocol_checks'] + assert fixture.read_bytes()==Path(plan['original_Guest_fixture']['path']).read_bytes(), 'Unchanged full Guest53 original fixture bytes' + r.profile_gate(r.profile_path(profile),O/'local-profile.json',profile) + assert growth()<=plan['maximum_measurement_growth_bytes'] + (stage/'project.godot').write_bytes(public);protect() + except BaseException as caught:error=type(caught).__name__+': '+str(caught) + finally: + cleanup,cleanup_errors=outer.finish_captured(outer.OWNED) + if all(q['engine_ended'] is True for q in cleanup):(stage/'project.godot').write_bytes(public) + else:cleanup_errors.append('Restoration refused while actual owned child remains live') + guard_errors=[] + try:protect() + except BaseException as caught:guard_errors.append(type(caught).__name__+': '+str(caught)) + passed=error is None and not guard_errors and not cleanup_errors and len(phases)==1 and len(samples)==1 + r.write(O/'execution.json',dict(passed=passed,error=error,guard_errors=guard_errors,phases=phases,samples=samples,cleanup=cleanup,cleanup_errors=cleanup_errors,input_source_default_doc4_parent_support_private_preserved=not guard_errors,compiler_processes=0,full_regression_runs=0,candidate_selected=False,candidate_adopted=False,unit_accepted=False,performance_accepted=False,normal_OS_accepted=False,Windows_accepted=False,limitations=['Finite Guest53 and18 affected original S3 methods plus frozen50 protocols/shrink/ownership/reentrant collision on same new bridge. Literal original Guest source oracle removes class_name line only. No Runtime cold646→647 whole state, ordinary OS/reentry domain, arbitrary conversion or memory failure, final full regression, performance budget/P95/D10, Windows or adoption/unit acceptance.'])) + print(json.dumps(dict(passed=passed,error=error,guard_errors=guard_errors)),flush=True);return 0 if passed else 1 +if __name__=='__main__':raise SystemExit(main()) diff --git a/docs/plans/original-completion-2026-10-02/checkpoints/native-hot-loop-r325-r329/052-corpus.tsv.gz b/docs/plans/original-completion-2026-10-02/checkpoints/native-hot-loop-r325-r329/052-corpus.tsv.gz new file mode 100644 index 0000000..75f3e56 Binary files /dev/null and b/docs/plans/original-completion-2026-10-02/checkpoints/native-hot-loop-r325-r329/052-corpus.tsv.gz differ diff --git a/docs/plans/original-completion-2026-10-02/checkpoints/native-hot-loop-r325-r329/053-execution.json b/docs/plans/original-completion-2026-10-02/checkpoints/native-hot-loop-r325-r329/053-execution.json new file mode 100644 index 0000000..3baac9a --- /dev/null +++ b/docs/plans/original-completion-2026-10-02/checkpoints/native-hot-loop-r325-r329/053-execution.json @@ -0,0 +1,40 @@ +{ + "passed": true, + "error": null, + "phases": [ + { + "pid": 34107, + "argv": [ + "/Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/bin/clang++", + "-std=c++17", + "-O2", + "-o", + "/Users/korizatou114/Projects/\u3068\u308a\u306b\u304f\u5de5\u623f\u30b2\u30fc\u30e0\u5236\u4f5c\u90e8/artifacts/original-completion-2026-10-02/p23-02-native-hot-loop-numeric-r328/oracle", + "/Users/korizatou114/Projects/\u3068\u308a\u306b\u304f\u5de5\u623f\u30b2\u30fc\u30e0\u5236\u4f5c\u90e8/artifacts/original-completion-2026-10-02/p23-02-native-hot-loop-numeric-r328/oracle.cpp" + ], + "exit_code": 0, + "seconds": 1.136918583, + "ended": true, + "foreign_signals": 0 + }, + { + "pid": 34122, + "argv": [ + "/Users/korizatou114/Projects/\u3068\u308a\u306b\u304f\u5de5\u623f\u30b2\u30fc\u30e0\u5236\u4f5c\u90e8/artifacts/original-completion-2026-10-02/p23-02-native-hot-loop-numeric-r328/oracle", + "/Users/korizatou114/Projects/\u3068\u308a\u306b\u304f\u5de5\u623f\u30b2\u30fc\u30e0\u5236\u4f5c\u90e8/artifacts/original-completion-2026-10-02/p23-02-native-hot-loop-numeric-r328/corpus.tsv" + ], + "exit_code": 0, + "seconds": 0.557632916, + "ended": true, + "foreign_signals": 0 + } + ], + "all_inputs_preserved": true, + "corpus": { + "path": "/Users/korizatou114/Projects/\u3068\u308a\u306b\u304f\u5de5\u623f\u30b2\u30fc\u30e0\u5236\u4f5c\u90e8/artifacts/original-completion-2026-10-02/p23-02-native-hot-loop-numeric-r328/corpus.tsv", + "bytes": 4556458, + "sha256": "51036394c19bd2c3212a80fb67f7406e6f0d049ec018910d36a8d3856a0b270c" + }, + "unit_accepted": false, + "performance_accepted": false +} diff --git a/docs/plans/original-completion-2026-10-02/checkpoints/native-hot-loop-r325-r329/054-input.json b/docs/plans/original-completion-2026-10-02/checkpoints/native-hot-loop-r325-r329/054-input.json new file mode 100644 index 0000000..f617cb6 --- /dev/null +++ b/docs/plans/original-completion-2026-10-02/checkpoints/native-hot-loop-r325-r329/054-input.json @@ -0,0 +1,48 @@ +{ + "pins": { + "/Users/korizatou114/Projects/\u3068\u308a\u306b\u304f\u5de5\u623f\u30b2\u30fc\u30e0\u5236\u4f5c\u90e8/artifacts/original-completion-2026-10-02/p23-02-native-guest-bulk-candidate-r313-r02/source/tree_core.hpp": { + "path": "/Users/korizatou114/Projects/\u3068\u308a\u306b\u304f\u5de5\u623f\u30b2\u30fc\u30e0\u5236\u4f5c\u90e8/artifacts/original-completion-2026-10-02/p23-02-native-guest-bulk-candidate-r313-r02/source/tree_core.hpp", + "bytes": 3299, + "sha256": "be46852fe55aeee4e18996b4d87222662b031fd1b946977c81092f4d0707e480" + }, + "/Users/korizatou114/Projects/\u3068\u308a\u306b\u304f\u5de5\u623f\u30b2\u30fc\u30e0\u5236\u4f5c\u90e8/artifacts/original-completion-2026-10-02/p23-02-native-hot-loop-candidate-r326/source/tree_core.hpp": { + "path": "/Users/korizatou114/Projects/\u3068\u308a\u306b\u304f\u5de5\u623f\u30b2\u30fc\u30e0\u5236\u4f5c\u90e8/artifacts/original-completion-2026-10-02/p23-02-native-hot-loop-candidate-r326/source/tree_core.hpp", + "bytes": 3467, + "sha256": "5f67d85bfc91d5c7830aabd8089f4bca05883045d671d4e683d7c532cf3a34a1" + }, + "/Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/bin/clang++": { + "path": "/Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/bin/clang++", + "bytes": 141373024, + "sha256": "7def90dd8829726686213a747fc5bff1583df933dae5edc55d755479e0bfe00a" + }, + "/private/tmp/thetree-numeric-oracle-r328.py": { + "path": "/private/tmp/thetree-numeric-oracle-r328.py", + "bytes": 5646, + "sha256": "21675754454c5d8fca8b35145217f32762243e39908e67bf2a1bdb783028c94c" + }, + "/Users/korizatou114/Projects/\u3068\u308a\u306b\u304f\u5de5\u623f\u30b2\u30fc\u30e0\u5236\u4f5c\u90e8/artifacts/original-completion-2026-10-02/p23-02-native-hot-loop-numeric-r328/oracle.cpp": { + "path": "/Users/korizatou114/Projects/\u3068\u308a\u306b\u304f\u5de5\u623f\u30b2\u30fc\u30e0\u5236\u4f5c\u90e8/artifacts/original-completion-2026-10-02/p23-02-native-hot-loop-numeric-r328/oracle.cpp", + "bytes": 3291, + "sha256": "5d786e3c3be56c87c4311f80f5e9d64411f0875bb00f97ca778d191dbffe8b66" + } + }, + "argv": [ + [ + "/Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/bin/clang++", + "-std=c++17", + "-O2", + "-o", + "/Users/korizatou114/Projects/\u3068\u308a\u306b\u304f\u5de5\u623f\u30b2\u30fc\u30e0\u5236\u4f5c\u90e8/artifacts/original-completion-2026-10-02/p23-02-native-hot-loop-numeric-r328/oracle", + "/Users/korizatou114/Projects/\u3068\u308a\u306b\u304f\u5de5\u623f\u30b2\u30fc\u30e0\u5236\u4f5c\u90e8/artifacts/original-completion-2026-10-02/p23-02-native-hot-loop-numeric-r328/oracle.cpp" + ], + [ + "/Users/korizatou114/Projects/\u3068\u308a\u306b\u304f\u5de5\u623f\u30b2\u30fc\u30e0\u5236\u4f5c\u90e8/artifacts/original-completion-2026-10-02/p23-02-native-hot-loop-numeric-r328/oracle", + "/Users/korizatou114/Projects/\u3068\u308a\u306b\u304f\u5de5\u623f\u30b2\u30fc\u30e0\u5236\u4f5c\u90e8/artifacts/original-completion-2026-10-02/p23-02-native-hot-loop-numeric-r328/corpus.tsv" + ] + ], + "case_count": 16388, + "timeout_seconds": 30, + "growth_cap_bytes": 16777216, + "Godot_runs": 0, + "unit_accepted": false +} diff --git a/docs/plans/original-completion-2026-10-02/checkpoints/native-hot-loop-r325-r329/055-oracle.gz b/docs/plans/original-completion-2026-10-02/checkpoints/native-hot-loop-r325-r329/055-oracle.gz new file mode 100644 index 0000000..afb7ea0 Binary files /dev/null and b/docs/plans/original-completion-2026-10-02/checkpoints/native-hot-loop-r325-r329/055-oracle.gz differ diff --git a/docs/plans/original-completion-2026-10-02/checkpoints/native-hot-loop-r325-r329/056-oracle.cpp b/docs/plans/original-completion-2026-10-02/checkpoints/native-hot-loop-r325-r329/056-oracle.cpp new file mode 100644 index 0000000..8aa9f4f --- /dev/null +++ b/docs/plans/original-completion-2026-10-02/checkpoints/native-hot-loop-r325-r329/056-oracle.cpp @@ -0,0 +1,54 @@ +#include +#include +#include +#define the_tree_native original_numeric +#include "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/artifacts/original-completion-2026-10-02/p23-02-native-guest-bulk-candidate-r313-r02/source/tree_core.hpp" +#undef the_tree_native +#define the_tree_native candidate_numeric +#include "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/artifacts/original-completion-2026-10-02/p23-02-native-hot-loop-candidate-r326/source/tree_core.hpp" +#undef the_tree_native +using I=std::int64_t; +struct E {I neighbor,cost;}; +std::string vec(const std::vector& a) {std::ostringstream out;for(auto x:a)out< std::string result(const T &r,const std::vector& calls) { + return r.status+"|"+vec(r.costs)+"|"+vec(r.parents)+"|"+vec(r.discovered)+"|"+vec(r.parent_discovered)+"|"+vec(r.settled)+"|"+vec(calls); +} +int main(int argc,char **argv) { + if(argc!=2)return 98; std::ofstream corpus(argv[1]);if(!corpus)return 98; + std::uint64_t cases=0;bool passed=true; + auto run=[&](I n,I start,const std::vector& goals,const std::vector>& graph) { + std::vector old_calls,new_calls; + auto old_provider=[&](I current) {old_calls.push_back(current);std::vector r;for(auto e:graph[current])r.push_back({e.neighbor,e.cost});return r;}; + auto new_provider=[&](I current) {new_calls.push_back(current);std::vector r;for(auto e:graph[current])r.push_back({e.neighbor,e.cost});return r;}; + auto old=original_numeric::search(n,start,goals,old_provider); + auto now=candidate_numeric::search(n,start,goals,new_provider); + const auto a=result(old,old_calls),b=result(now,new_calls);passed=passed && a==b; + corpus<> g(3);for(I a=0;a<3;++a)for(I b=0;b<3;++b)if(mask&(I(1)<<(a*3+b)))g[a].push_back({b,1}); + for(I start=0;start<3;++start)for(I target=0;target<8;++target) {std::vector goals;for(I i=0;i<3;++i)if(target&(I(1)<> g(n); + for(I i=0;i::max(); + g[i].push_back({node,cost}); + } + std::vector goals;for(I i=0,count=rng()%8;i::max(); + run(3,0,{2},{{{1,maximum}},{{2,1}}, {}}); + run(4,0,{3,3,99},{{{2,1},{1,1},{1,1}},{{3,2}},{{3,2}}, {}}); + run(0,-1,{},{});run(1,1,{0},{{}}); + corpus.close();if(!corpus)return 98; + std::cout<<"NUMERIC_DIFFERENTIAL "<=528*1048576;O.mkdir() +def pin(p):return dict(path=str(p),bytes=p.stat().st_size,sha256=hashlib.sha256(p.read_bytes()).hexdigest()) +old=json.loads((A/'p23-02-native-bridge-build-r76/input.json').read_text());compiler=Path(old['compiler_pins']['clang++']['path']);assert pin(compiler)==old['compiler_pins']['clang++'] +cpp='''#include +#include +#include +#define the_tree_native original_numeric +#include "'''+str(P)+'''" +#undef the_tree_native +#define the_tree_native candidate_numeric +#include "'''+str(C)+'''" +#undef the_tree_native +using I=std::int64_t; +struct E {I neighbor,cost;}; +std::string vec(const std::vector& a) {std::ostringstream out;for(auto x:a)out< std::string result(const T &r,const std::vector& calls) { + return r.status+"|"+vec(r.costs)+"|"+vec(r.parents)+"|"+vec(r.discovered)+"|"+vec(r.parent_discovered)+"|"+vec(r.settled)+"|"+vec(calls); +} +int main(int argc,char **argv) { + if(argc!=2)return 98; std::ofstream corpus(argv[1]);if(!corpus)return 98; + std::uint64_t cases=0;bool passed=true; + auto run=[&](I n,I start,const std::vector& goals,const std::vector>& graph) { + std::vector old_calls,new_calls; + auto old_provider=[&](I current) {old_calls.push_back(current);std::vector r;for(auto e:graph[current])r.push_back({e.neighbor,e.cost});return r;}; + auto new_provider=[&](I current) {new_calls.push_back(current);std::vector r;for(auto e:graph[current])r.push_back({e.neighbor,e.cost});return r;}; + auto old=original_numeric::search(n,start,goals,old_provider); + auto now=candidate_numeric::search(n,start,goals,new_provider); + const auto a=result(old,old_calls),b=result(now,new_calls);passed=passed && a==b; + corpus<> g(3);for(I a=0;a<3;++a)for(I b=0;b<3;++b)if(mask&(I(1)<<(a*3+b)))g[a].push_back({b,1}); + for(I start=0;start<3;++start)for(I target=0;target<8;++target) {std::vector goals;for(I i=0;i<3;++i)if(target&(I(1)<> g(n); + for(I i=0;i::max(); + g[i].push_back({node,cost}); + } + std::vector goals;for(I i=0,count=rng()%8;i::max(); + run(3,0,{2},{{{1,maximum}},{{2,1}}, {}}); + run(4,0,{3,3,99},{{{2,1},{1,1},{1,1}},{{3,2}},{{3,2}}, {}}); + run(0,-1,{},{});run(1,1,{0},{{}}); + corpus.close();if(!corpus)return 98; + std::cout<<"NUMERIC_DIFFERENTIAL "<=528*1048576;start=time.monotonic() + process=subprocess.Popen(argv,stdout=subprocess.PIPE,stderr=subprocess.PIPE,start_new_session=True) + try:out,err=process.communicate(timeout=30) + except BaseException: + if process.poll() is None:process.terminate() + try:process.wait(timeout=3) + except subprocess.TimeoutExpired:process.kill();process.wait(timeout=3) + raise + (O/('phase%d.stdout'%i)).write_bytes(out);(O/('phase%d.stderr'%i)).write_bytes(err) + rows.append(dict(pid=process.pid,argv=argv,exit_code=process.returncode,seconds=time.monotonic()-start,ended=process.poll() is not None,foreign_signals=0)) + assert process.returncode==0 and not err + assert sum(p.stat().st_size for p in O.rglob('*') if p.is_file())<=16*1048576 + assert (O/'phase1.stdout').read_text()=='NUMERIC_DIFFERENTIAL 16388 PASS\n' + for path,p in protected.items():assert pin(Path(path))==p +except BaseException as ex:error=repr(ex) +(O/'execution.json').write_text(json.dumps(dict(passed=error is None,error=error,phases=rows,all_inputs_preserved=all(pin(Path(k))==v for k,v in protected.items()),corpus=pin(O/'corpus.tsv') if (O/'corpus.tsv').exists() else None,unit_accepted=False,performance_accepted=False),indent=2)+'\n') +print(json.dumps(dict(passed=error is None,error=error,phases=rows,corpus_bytes=(O/'corpus.tsv').stat().st_size if (O/'corpus.tsv').exists() else None))) diff --git a/docs/plans/original-completion-2026-10-02/checkpoints/native-hot-loop-r325-r329/065-thetree-plan-local-r329.py b/docs/plans/original-completion-2026-10-02/checkpoints/native-hot-loop-r325-r329/065-thetree-plan-local-r329.py new file mode 100644 index 0000000..ab93107 --- /dev/null +++ b/docs/plans/original-completion-2026-10-02/checkpoints/native-hot-loop-r325-r329/065-thetree-plan-local-r329.py @@ -0,0 +1,24 @@ +# coding: utf-8 +from pathlib import Path +import importlib.util,json,shutil,uuid +R=Path('/Users/korizatou114/Projects/とりにく工房ゲーム制作部');A=R/'artifacts/original-completion-2026-10-02';O=A/'p23-02-native-hot-loop-local-r329';C=A/'p23-02-native-hot-loop-candidate-r326';B=A/'p23-02-native-hot-loop-build-r327' +s=importlib.util.spec_from_file_location('prepare329',R/'tools/run_all_tests.py');r=importlib.util.module_from_spec(s);s.loader.exec_module(r) +def pin(p):return dict(path=str(p),bytes=p.stat().st_size,sha256=r.sha(p)) +assert r.read(B/'execution.json')['passed'] and shutil.disk_usage(A).free>=528*1048576 +review=A/'p23-02-native-hot-loop-build-actual-independent-review-r327.json';assert r.read(review)['host_build_accepted'] +old=r.read(A/'p23-02-native-guest-bulk-local-r315/input.json');stage=A/'p23-02-native-guest-bulk-local-r315/stage';parent=Path(old['parent']);assert r.inventory(stage,stage=True)==old['expected_stage'];assert r.inventory(parent,stage=True)==old['parent_stage_inventory'] +private=Path('/private/tmp')/('TheTree-hot-loop-r329-'+uuid.uuid4().hex);private.mkdir();lib=private/'candidate.dylib' +with lib.open('xb') as f:f.write((B/'libtree_numeric_core.macos.template_debug.dylib').read_bytes()) +assert lib.read_bytes()==(B/'libtree_numeric_core.macos.template_debug.dylib').read_bytes() +with (O/'candidate.gdextension').open('x') as f:f.write('[configuration]\nentry_symbol="tree_numeric_core_init"\ncompatibility_minimum="4.7.2"\nreloadable=false\n\n[libraries]\nmacos.arm64="'+str(lib)+'"\n') +fixed=old['pins'].copy() +updates=dict(host_review=review,host_root=A/'p23-02-native-hot-loop-build-root-verification-r327.json',library=B/'libtree_numeric_core.macos.template_debug.dylib',private_library=lib,config=O/'candidate.gdextension',qa=O/'local.gd',candidate=C/'input.json',candidate_guest=C/'scripts/guest_service.gd',candidate_runtime=C/'scripts/simulation_runtime.gd',candidate_cpp=C/'source/tree_numeric_core.cpp',candidate_include=C/'source/guest_candidate_batch.inc',contract=A/'p23-02-native-hot-loop-work-contract-r325.json',starter=O/'candidate-guarded.gd',runner=A/'p23-02-native-hot-loop-local-r329.py',prepare=Path(__file__),protocol_helper=O/'protocol_helper.gd',protocol_original=A/'p23-02-native-lazy-edge-cache-protocol-r143.gd',protocol_manifest=A/'p23-02-native-bridge-Godot-protocol-preparation-r73/pinned-inputs.json',numeric_execution=A/'p23-02-native-hot-loop-numeric-r328/execution.json',header=C/'source/tree_core.hpp') +for k,p in updates.items():fixed[k]=pin(p) +# Pin all 50 frozen full inputs and expected outputs, once; never copy this corpus. +manifest=r.read(updates['protocol_manifest']) +for i,row in enumerate(manifest['cases']): + for kind in ['input','expected']: + p=Path(row[kind+'_path']);q=pin(p);assert q['sha256']==row[kind+'_sha256'];fixed['protocol_%d_%s'%(i,kind)]=q +v=old.copy();v.update(pins=fixed,stage=str(stage),source_before=r.source_refs(R),default_before=r.default_player(R),doc4_before={n:r.sha(R/'docs/plans/original-completion-2026-10-02'/n) for n in old['doc4_before']},support_inventory=r.inventory(r.SUPPORT),private_library_inventory=r.inventory(private),original_Guest_fixture=pin(A/'p23-02-native-guest-bulk-local-r315/local-measurement/fixtures.varbytes'),additional_protocol_cases=50,stage_clones=0,expected_duration_seconds=[2,30],capacity_floor_bytes=528*1048576) +with (O/'input.json').open('x') as f:json.dump(v,f,ensure_ascii=False,indent=2);f.write('\n') +print(json.dumps(dict(plan=str(O/'input.json'),pins=len(fixed),clone_count=0,free=shutil.disk_usage(A).free))) diff --git a/docs/plans/original-completion-2026-10-02/checkpoints/native-hot-loop-r325-r329/066-thetree-prepare-local-r329.py b/docs/plans/original-completion-2026-10-02/checkpoints/native-hot-loop-r325-r329/066-thetree-prepare-local-r329.py new file mode 100644 index 0000000..c5c8f3c --- /dev/null +++ b/docs/plans/original-completion-2026-10-02/checkpoints/native-hot-loop-r325-r329/066-thetree-prepare-local-r329.py @@ -0,0 +1,88 @@ +# coding: utf-8 +from pathlib import Path +import json,re +R=Path('/Users/korizatou114/Projects/とりにく工房ゲーム制作部');A=R/'artifacts/original-completion-2026-10-02';O=A/'p23-02-native-hot-loop-local-r329';assert not O.exists();O.mkdir() +def write(p,s): + with p.open('x') as f:f.write(s) +old=A/'p23-02-native-lazy-edge-cache-protocol-r143.gd';s=old.read_text();s=s.replace('extends SceneTree','extends RefCounted',1) +s=re.sub(r'func _initialize\(\) -> void:\n.*?(?=\nfunc )','',s,count=1,flags=re.S) +s=s[:s.index('func _run() -> void:')] +s+='''class ReentrantProvider extends RefCounted: + var core: Object + var cells: Array[Vector3i] + var indices: Dictionary + var goals: Dictionary + var entered := false + var inner_result: Dictionary + func edges(cell: Vector3i) -> Array: + if cell.x != 0: return [] + if not entered: + entered = true + core.call("clear_edge_cache") + var inner := EdgeProvider.new() + inner.rows = [[[cells[1],9]], []] + inner_result = core.call("search_cached",cells,indices,0,goals,Callable(inner,"edges")) + return [[cells[1],7]] + +func _reentrant_collision(core) -> Dictionary: + core.call("clear_edge_cache") + var cells: Array[Vector3i] = [Vector3i.ZERO,Vector3i(1,0,0)] + var indices: Dictionary = {cells[0]:0,cells[1]:1} + var goals: Dictionary = {cells[1]:true} + var provider := ReentrantProvider.new() + provider.core=core;provider.cells=cells;provider.indices=indices;provider.goals=goals + var outer: Dictionary = core.call("search_cached",cells,indices,0,goals,Callable(provider,"edges")) + _check(outer[&"status"] == "usable" and outer[&"cost_values"][1] == 7, "reentrant collision returns original converted outer row") + _check(provider.inner_result[&"status"] == "usable" and provider.inner_result[&"cost_values"][1] == 9, "inner populated same cache key before outer insertion") + var warm: Dictionary = core.call("search_cached",cells,indices,0,goals,Callable(provider,"edges")) + _check(warm[&"status"] == "usable" and warm[&"cost_values"][1] == 9, "warm preserves inner first cache publication") + var result := {"outer":outer,"inner":provider.inner_result,"warm":warm} + var ref: WeakRef = weakref(provider) + provider = null + _check(ref.get_ref() == null, "borrowed native cache retains no reentrant provider") + core.call("clear_edge_cache") + return result + +func run_frozen(core, manifest_path: String) -> Dictionary: + var manifest: Dictionary = JSON.parse_string(FileAccess.get_file_as_string(manifest_path)) + _check(manifest["cases"].size() == 50, "same frozen fifty-input scope") + for row: Dictionary in manifest["cases"]: _case(core,row) + _prefix_wrap(core,true) + _prefix_wrap(core,[Vector3i(2,0,0),false]) + _shrink_transfer(core) + _cached_goals_and_reset(core) + _cached_prefix_wrap(core,true) + _cached_prefix_wrap(core,[Vector3i(2,0,0),false]) + var collision := _reentrant_collision(core) + _warm_for_teardown(core) + return {"passed":failures.is_empty(),"checks":checks,"failures":failures,"cases":cases,"cached_cases":cached_cases,"reentrant_collision":collision,"unit_accepted":false,"performance_accepted":false} +''' +write(O/'protocol_helper.gd',s) +qa=(A/'p23-02-native-guest-bulk-local-r315.gd').read_text() +qa=qa.replace('var _bulk_records: Array = []','const ProtocolHelper = preload('+json.dumps(str(O/'protocol_helper.gd'))+')\n\nvar _bulk_records: Array = []',1) +a='\t_test_bulk_candidates()\n\tvar output := OS.get_environment("THE_TREE_RUNTIME_MONITOR_OUTPUT")';b='''\t_test_bulk_candidates() + var output := OS.get_environment("THE_TREE_RUNTIME_MONITOR_OUTPUT") + var protocol = ProtocolHelper.new() + var protocol_core: Object = ClassDB.instantiate(&"TheTreeNumericCore") + var protocol_report: Dictionary = protocol.run_frozen(protocol_core,'''+json.dumps(str(A/'p23-02-native-bridge-Godot-protocol-preparation-r73/pinned-inputs.json'))+''') + var core_ref: WeakRef = weakref(protocol_core) + protocol_core = null + _expect(core_ref.get_ref() == null, "combined protocol core with populated cache releases") + _checks += int(protocol_report["checks"]) + _failures += protocol_report["failures"].size() + var protocol_file := FileAccess.open(output.path_join("protocol-report.json"),FileAccess.WRITE) + if protocol_file == null: quit(98); return + protocol_file.store_string(JSON.stringify(protocol_report,"\\t")+"\\n");protocol_file.close() + protocol = null''';assert a in qa;qa=qa.replace(a,b,1) +qa=qa.replace('"original_s3_invocations": 18,','"original_s3_invocations": 18, "protocol_cases": 50, "protocol_checks": int(protocol_report["checks"]),') +write(O/'local.gd',qa) +guard=(A/'p23-02-native-guest-bulk-local-r315/candidate-guarded.gd').read_text() +guard=guard.replace('p23-02-native-guest-bulk-local-r315.gd','p23-02-native-hot-loop-local-r329/local.gd').replace('p23-02-native-guest-bulk-local-r315/candidate.gdextension','p23-02-native-hot-loop-local-r329/candidate.gdextension') +write(O/'candidate-guarded.gd',guard) +runner=(A/'p23-02-native-guest-bulk-local-r315.py').read_text().replace('p23-02-native-guest-bulk-local-r315','p23-02-native-hot-loop-local-r329') +a=" fixture=output/'fixtures.varbytes';assert fixture.stat().st_size==report['fixture_bytes'] and r.sha(fixture)==report['fixture_sha256']" +b=a+"\n protocol=r.read(output/'protocol-report.json');assert protocol['passed'] and not protocol['failures'] and len(protocol['cases'])==50 and len(protocol['cached_cases'])==50 and protocol['checks']==report['protocol_checks']\n assert fixture.read_bytes()==Path(plan['original_Guest_fixture']['path']).read_bytes(), 'Unchanged full Guest53 original fixture bytes'" +assert a in runner;runner=runner.replace(a,b,1) +runner=runner.replace('Finite standalone Guest candidate ABI/local and18 affected original S3 methods only.','Finite Guest53 and18 affected original S3 methods plus frozen50 protocols/shrink/ownership/reentrant collision on same new bridge.') +write(A/'p23-02-native-hot-loop-local-r329.py',runner) +print('Prepared single-engine combined local/protocol QA. Engine0.') diff --git a/docs/plans/original-completion-2026-10-02/checkpoints/native-hot-loop-r325-r329/067-thetree-prepare-r326.py.gz b/docs/plans/original-completion-2026-10-02/checkpoints/native-hot-loop-r325-r329/067-thetree-prepare-r326.py.gz new file mode 100644 index 0000000..6fbfa8d Binary files /dev/null and b/docs/plans/original-completion-2026-10-02/checkpoints/native-hot-loop-r325-r329/067-thetree-prepare-r326.py.gz differ diff --git a/docs/plans/original-completion-2026-10-02/checkpoints/native-hot-loop-r325-r329/README.md b/docs/plans/original-completion-2026-10-02/checkpoints/native-hot-loop-r325-r329/README.md new file mode 100644 index 0000000..586263a --- /dev/null +++ b/docs/plans/original-completion-2026-10-02/checkpoints/native-hot-loop-r325-r329/README.md @@ -0,0 +1,3 @@ +# Native hot loop candidate r325–r329 + +The manifest preserves source, build, numeric corpus, local evidence, review receipts and root verification. Compressed copies were decompressed and compared against every original byte. Originals remain in the artifact area. Files are checkpoints, not a production deployment. Candidate r326/library327 remains unselected and unadopted. Whole-state comparison, final regression and unit acceptance are pending. See the current execution note for scope. diff --git a/docs/plans/original-completion-2026-10-02/checkpoints/native-hot-loop-r325-r329/manifest.json b/docs/plans/original-completion-2026-10-02/checkpoints/native-hot-loop-r325-r329/manifest.json new file mode 100644 index 0000000..1883612 --- /dev/null +++ b/docs/plans/original-completion-2026-10-02/checkpoints/native-hot-loop-r325-r329/manifest.json @@ -0,0 +1,1027 @@ +{ + "plan_id": "P23-02", + "candidate": "r326/library327", + "scope": "Finite local and numeric evidence; whole comparison pending; unselected and unadopted", + "copies": [ + { + "original": { + "path": "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/artifacts/original-completion-2026-10-02/p23-02-native-hot-loop-build-actual-independent-review-r327.json", + "bytes": 35660, + "sha256": "538e5e0e08eef15a3207c22c2f21eb812ef7b549f7e0797ffc33a89ca935f9c8" + }, + "stored": { + "path": "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/docs/plans/original-completion-2026-10-02/checkpoints/native-hot-loop-r325-r329/000-p23-02-native-hot-loop-build-actual-independent-review-r327.json.gz", + "bytes": 5773, + "sha256": "cd75345961ca3c077d9ac8feba3f733484b76fd67ba7a0c40df5ca6cfd71f314" + }, + "gzip": true, + "raw_bytes": 35660, + "raw_sha256": "538e5e0e08eef15a3207c22c2f21eb812ef7b549f7e0797ffc33a89ca935f9c8" + }, + { + "original": { + "path": "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/artifacts/original-completion-2026-10-02/p23-02-native-hot-loop-build-preexecution-independent-review-r327.json", + "bytes": 27596, + "sha256": "531567cb26853a119012d01482335d04cd00b7f589df16e8c936b9dcd3227914" + }, + "stored": { + "path": "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/docs/plans/original-completion-2026-10-02/checkpoints/native-hot-loop-r325-r329/001-p23-02-native-hot-loop-build-preexecution-independent-review-r327.json.gz", + "bytes": 5193, + "sha256": "8edc082feeac551314a66c7b391389dd74916bb0f88d8dada35ceac232f2261a" + }, + "gzip": true, + "raw_bytes": 27596, + "raw_sha256": "531567cb26853a119012d01482335d04cd00b7f589df16e8c936b9dcd3227914" + }, + { + "original": { + "path": "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/artifacts/original-completion-2026-10-02/p23-02-native-hot-loop-build-preexecution-root-verification-r327.json", + "bytes": 16724, + "sha256": "1e03d6d0ff7b36de010be5dbc54757c63728bce43c239ad4d6f4cae07057a551" + }, + "stored": { + "path": "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/docs/plans/original-completion-2026-10-02/checkpoints/native-hot-loop-r325-r329/002-p23-02-native-hot-loop-build-preexecution-root-verification-r327.json.gz", + "bytes": 3818, + "sha256": "16f88a2c5713eda8ebe1557160632570011b842cfbf2ac16a0378618a740065e" + }, + "gzip": true, + "raw_bytes": 16724, + "raw_sha256": "1e03d6d0ff7b36de010be5dbc54757c63728bce43c239ad4d6f4cae07057a551" + }, + { + "original": { + "path": "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/artifacts/original-completion-2026-10-02/p23-02-native-hot-loop-build-preview-r327.json", + "bytes": 2911, + "sha256": "951b8c7fc6d76bde01126d31058039a914e4f1f768a11b839b94496dce6fa882" + }, + "stored": { + "path": "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/docs/plans/original-completion-2026-10-02/checkpoints/native-hot-loop-r325-r329/003-p23-02-native-hot-loop-build-preview-r327.json", + "bytes": 2911, + "sha256": "951b8c7fc6d76bde01126d31058039a914e4f1f768a11b839b94496dce6fa882" + }, + "gzip": false, + "raw_bytes": 2911, + "raw_sha256": "951b8c7fc6d76bde01126d31058039a914e4f1f768a11b839b94496dce6fa882" + }, + { + "original": { + "path": "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/artifacts/original-completion-2026-10-02/p23-02-native-hot-loop-build-r327/compile-062/ended.json", + "bytes": 1836, + "sha256": "7a359215f2274f31c0e00e9771a645bf2592228ec2953e2956cd199d0f3343b9" + }, + "stored": { + "path": "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/docs/plans/original-completion-2026-10-02/checkpoints/native-hot-loop-r325-r329/004-ended.json", + "bytes": 1836, + "sha256": "7a359215f2274f31c0e00e9771a645bf2592228ec2953e2956cd199d0f3343b9" + }, + "gzip": false, + "raw_bytes": 1836, + "raw_sha256": "7a359215f2274f31c0e00e9771a645bf2592228ec2953e2956cd199d0f3343b9" + }, + { + "original": { + "path": "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/artifacts/original-completion-2026-10-02/p23-02-native-hot-loop-build-r327/compile-062/started.json", + "bytes": 2926, + "sha256": "66d6bfd1a8b32476d4b58a9e3ecab176d135bcb118acb3e3afa1c803964c04d8" + }, + "stored": { + "path": "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/docs/plans/original-completion-2026-10-02/checkpoints/native-hot-loop-r325-r329/005-started.json", + "bytes": 2926, + "sha256": "66d6bfd1a8b32476d4b58a9e3ecab176d135bcb118acb3e3afa1c803964c04d8" + }, + "gzip": false, + "raw_bytes": 2926, + "raw_sha256": "66d6bfd1a8b32476d4b58a9e3ecab176d135bcb118acb3e3afa1c803964c04d8" + }, + { + "original": { + "path": "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/artifacts/original-completion-2026-10-02/p23-02-native-hot-loop-build-r327/compile-062/stderr.log", + "bytes": 0, + "sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + }, + "stored": { + "path": "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/docs/plans/original-completion-2026-10-02/checkpoints/native-hot-loop-r325-r329/006-stderr.log", + "bytes": 0, + "sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + }, + "gzip": false, + "raw_bytes": 0, + "raw_sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + }, + { + "original": { + "path": "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/artifacts/original-completion-2026-10-02/p23-02-native-hot-loop-build-r327/compile-062/stdout.log", + "bytes": 0, + "sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + }, + "stored": { + "path": "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/docs/plans/original-completion-2026-10-02/checkpoints/native-hot-loop-r325-r329/007-stdout.log", + "bytes": 0, + "sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + }, + "gzip": false, + "raw_bytes": 0, + "raw_sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + }, + { + "original": { + "path": "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/artifacts/original-completion-2026-10-02/p23-02-native-hot-loop-build-r327/compile-062-phase.json", + "bytes": 621, + "sha256": "16eb2c00f6de1c474cba136713c6d2b66be99aee094e4c23c7058c0ed33b940e" + }, + "stored": { + "path": "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/docs/plans/original-completion-2026-10-02/checkpoints/native-hot-loop-r325-r329/008-compile-062-phase.json", + "bytes": 621, + "sha256": "16eb2c00f6de1c474cba136713c6d2b66be99aee094e4c23c7058c0ed33b940e" + }, + "gzip": false, + "raw_bytes": 621, + "raw_sha256": "16eb2c00f6de1c474cba136713c6d2b66be99aee094e4c23c7058c0ed33b940e" + }, + { + "original": { + "path": "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/artifacts/original-completion-2026-10-02/p23-02-native-hot-loop-build-r327/execution.json", + "bytes": 7069, + "sha256": "b7343875926c9c8f2811eeead25537e826ea2c449f84892d8c5bef95d768d66d" + }, + "stored": { + "path": "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/docs/plans/original-completion-2026-10-02/checkpoints/native-hot-loop-r325-r329/009-execution.json", + "bytes": 7069, + "sha256": "b7343875926c9c8f2811eeead25537e826ea2c449f84892d8c5bef95d768d66d" + }, + "gzip": false, + "raw_bytes": 7069, + "raw_sha256": "b7343875926c9c8f2811eeead25537e826ea2c449f84892d8c5bef95d768d66d" + }, + { + "original": { + "path": "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/artifacts/original-completion-2026-10-02/p23-02-native-hot-loop-build-r327/input.json", + "bytes": 658598, + "sha256": "df8280ec808ad50a6c1c2e5d02d3371ff203b08f6b383079018793f8834cb450" + }, + "stored": { + "path": "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/docs/plans/original-completion-2026-10-02/checkpoints/native-hot-loop-r325-r329/010-input.json.gz", + "bytes": 137961, + "sha256": "c9810db4df668724ebd36d285db97f853271849253308b16a8e4a5545b34d31a" + }, + "gzip": true, + "raw_bytes": 658598, + "raw_sha256": "df8280ec808ad50a6c1c2e5d02d3371ff203b08f6b383079018793f8834cb450" + }, + { + "original": { + "path": "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/artifacts/original-completion-2026-10-02/p23-02-native-hot-loop-build-r327/libtree_numeric_core.macos.template_debug.dylib", + "bytes": 417184, + "sha256": "64608dccb3441594603fa5561fad3e2e90b055f32e4823c4864d2486a298602c" + }, + "stored": { + "path": "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/docs/plans/original-completion-2026-10-02/checkpoints/native-hot-loop-r325-r329/011-libtree_numeric_core.macos.template_debug.dylib.gz", + "bytes": 153480, + "sha256": "4f99da375141435b649a0dbf12625f45b3fdf56b74f61f84ded013fceaae03b3" + }, + "gzip": true, + "raw_bytes": 417184, + "raw_sha256": "64608dccb3441594603fa5561fad3e2e90b055f32e4823c4864d2486a298602c" + }, + { + "original": { + "path": "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/artifacts/original-completion-2026-10-02/p23-02-native-hot-loop-build-r327/link/ended.json", + "bytes": 1334, + "sha256": "3f101696a2982d2e6ca3a80ea956fefccd057eec7e48949918140d1c07c06b5f" + }, + "stored": { + "path": "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/docs/plans/original-completion-2026-10-02/checkpoints/native-hot-loop-r325-r329/012-ended.json", + "bytes": 1334, + "sha256": "3f101696a2982d2e6ca3a80ea956fefccd057eec7e48949918140d1c07c06b5f" + }, + "gzip": false, + "raw_bytes": 1334, + "raw_sha256": "3f101696a2982d2e6ca3a80ea956fefccd057eec7e48949918140d1c07c06b5f" + }, + { + "original": { + "path": "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/artifacts/original-completion-2026-10-02/p23-02-native-hot-loop-build-r327/link/started.json", + "bytes": 2018, + "sha256": "95b8659fec4d142f76cc8fbf50863e3caaa087cea4890fa9333816eeca34900a" + }, + "stored": { + "path": "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/docs/plans/original-completion-2026-10-02/checkpoints/native-hot-loop-r325-r329/013-started.json", + "bytes": 2018, + "sha256": "95b8659fec4d142f76cc8fbf50863e3caaa087cea4890fa9333816eeca34900a" + }, + "gzip": false, + "raw_bytes": 2018, + "raw_sha256": "95b8659fec4d142f76cc8fbf50863e3caaa087cea4890fa9333816eeca34900a" + }, + { + "original": { + "path": "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/artifacts/original-completion-2026-10-02/p23-02-native-hot-loop-build-r327/link/stderr.log", + "bytes": 0, + "sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + }, + "stored": { + "path": "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/docs/plans/original-completion-2026-10-02/checkpoints/native-hot-loop-r325-r329/014-stderr.log", + "bytes": 0, + "sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + }, + "gzip": false, + "raw_bytes": 0, + "raw_sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + }, + { + "original": { + "path": "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/artifacts/original-completion-2026-10-02/p23-02-native-hot-loop-build-r327/link/stdout.log", + "bytes": 0, + "sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + }, + "stored": { + "path": "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/docs/plans/original-completion-2026-10-02/checkpoints/native-hot-loop-r325-r329/015-stdout.log", + "bytes": 0, + "sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + }, + "gzip": false, + "raw_bytes": 0, + "raw_sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + }, + { + "original": { + "path": "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/artifacts/original-completion-2026-10-02/p23-02-native-hot-loop-build-r327/link-phase.json", + "bytes": 604, + "sha256": "deb61213f6da65f7ae457f708761a7974773503ac5b0935a1d315fbbd7ee9714" + }, + "stored": { + "path": "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/docs/plans/original-completion-2026-10-02/checkpoints/native-hot-loop-r325-r329/016-link-phase.json", + "bytes": 604, + "sha256": "deb61213f6da65f7ae457f708761a7974773503ac5b0935a1d315fbbd7ee9714" + }, + "gzip": false, + "raw_bytes": 604, + "raw_sha256": "deb61213f6da65f7ae457f708761a7974773503ac5b0935a1d315fbbd7ee9714" + }, + { + "original": { + "path": "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/artifacts/original-completion-2026-10-02/p23-02-native-hot-loop-build-r327/objects/062-tree_numeric_core.os", + "bytes": 368776, + "sha256": "4ac5a21e47a4441374131310c44474497b5fb5b28f6c6e75a5ed17243616687d" + }, + "stored": { + "path": "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/docs/plans/original-completion-2026-10-02/checkpoints/native-hot-loop-r325-r329/017-062-tree_numeric_core.os.gz", + "bytes": 118411, + "sha256": "34140e075125f4134eb80bf9a62d2c75c3f0ebe5b564e02e5937dff1d724cd0e" + }, + "gzip": true, + "raw_bytes": 368776, + "raw_sha256": "4ac5a21e47a4441374131310c44474497b5fb5b28f6c6e75a5ed17243616687d" + }, + { + "original": { + "path": "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/artifacts/original-completion-2026-10-02/p23-02-native-hot-loop-build-r327.py", + "bytes": 16335, + "sha256": "6e4d1d423e54038236b3b2c87f3ee48b807c161a156734087128dc9e7d3b8fa7" + }, + "stored": { + "path": "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/docs/plans/original-completion-2026-10-02/checkpoints/native-hot-loop-r325-r329/018-p23-02-native-hot-loop-build-r327.py.gz", + "bytes": 5091, + "sha256": "6e12d689be11ff7ec2a269c4bf05146f77c48875fbea5db387a6ad2b5d64eed5" + }, + "gzip": true, + "raw_bytes": 16335, + "raw_sha256": "6e4d1d423e54038236b3b2c87f3ee48b807c161a156734087128dc9e7d3b8fa7" + }, + { + "original": { + "path": "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/artifacts/original-completion-2026-10-02/p23-02-native-hot-loop-build-root-verification-r327.json", + "bytes": 21497, + "sha256": "7a601c165a342d043a5b2c4f1a7c6c54bf82419ad41894e5d6c32c171064af6a" + }, + "stored": { + "path": "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/docs/plans/original-completion-2026-10-02/checkpoints/native-hot-loop-r325-r329/019-p23-02-native-hot-loop-build-root-verification-r327.json.gz", + "bytes": 4534, + "sha256": "2fafe48ec3f2cfbccd35a26e78c1370d68f31961599f527d5ac5b72c0971690f" + }, + "gzip": true, + "raw_bytes": 21497, + "raw_sha256": "7a601c165a342d043a5b2c4f1a7c6c54bf82419ad41894e5d6c32c171064af6a" + }, + { + "original": { + "path": "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/artifacts/original-completion-2026-10-02/p23-02-native-hot-loop-candidate-r326/input.json", + "bytes": 3185, + "sha256": "27141ccb37a741cc468bf57cf6e5da2250b729a2fa21bbda5fa1b7f15df7b254" + }, + "stored": { + "path": "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/docs/plans/original-completion-2026-10-02/checkpoints/native-hot-loop-r325-r329/020-input.json", + "bytes": 3185, + "sha256": "27141ccb37a741cc468bf57cf6e5da2250b729a2fa21bbda5fa1b7f15df7b254" + }, + "gzip": false, + "raw_bytes": 3185, + "raw_sha256": "27141ccb37a741cc468bf57cf6e5da2250b729a2fa21bbda5fa1b7f15df7b254" + }, + { + "original": { + "path": "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/artifacts/original-completion-2026-10-02/p23-02-native-hot-loop-candidate-r326/scripts/guest_service.gd", + "bytes": 62505, + "sha256": "77ce52ebeed6a89f3e397de01572b17ea3e3cb246f7daca9145a1a192389969e" + }, + "stored": { + "path": "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/docs/plans/original-completion-2026-10-02/checkpoints/native-hot-loop-r325-r329/021-guest_service.gd", + "bytes": 62505, + "sha256": "77ce52ebeed6a89f3e397de01572b17ea3e3cb246f7daca9145a1a192389969e" + }, + "gzip": false, + "raw_bytes": 62505, + "raw_sha256": "77ce52ebeed6a89f3e397de01572b17ea3e3cb246f7daca9145a1a192389969e" + }, + { + "original": { + "path": "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/artifacts/original-completion-2026-10-02/p23-02-native-hot-loop-candidate-r326/scripts/simulation_runtime.gd", + "bytes": 612068, + "sha256": "7eb24adbf91fe53cde14954dda12abd5efb05130de7d356d8df281ee99065e86" + }, + "stored": { + "path": "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/docs/plans/original-completion-2026-10-02/checkpoints/native-hot-loop-r325-r329/022-simulation_runtime.gd", + "bytes": 612068, + "sha256": "7eb24adbf91fe53cde14954dda12abd5efb05130de7d356d8df281ee99065e86" + }, + "gzip": false, + "raw_bytes": 612068, + "raw_sha256": "7eb24adbf91fe53cde14954dda12abd5efb05130de7d356d8df281ee99065e86" + }, + { + "original": { + "path": "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/artifacts/original-completion-2026-10-02/p23-02-native-hot-loop-candidate-r326/source/guest_candidate_batch.inc", + "bytes": 28222, + "sha256": "3623a7ca03b96118c927662fc5a95c1f73e20251d5ef4b04ec519603c3c05ba9" + }, + "stored": { + "path": "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/docs/plans/original-completion-2026-10-02/checkpoints/native-hot-loop-r325-r329/023-guest_candidate_batch.inc", + "bytes": 28222, + "sha256": "3623a7ca03b96118c927662fc5a95c1f73e20251d5ef4b04ec519603c3c05ba9" + }, + "gzip": false, + "raw_bytes": 28222, + "raw_sha256": "3623a7ca03b96118c927662fc5a95c1f73e20251d5ef4b04ec519603c3c05ba9" + }, + { + "original": { + "path": "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/artifacts/original-completion-2026-10-02/p23-02-native-hot-loop-candidate-r326/source/tree_core.hpp", + "bytes": 3467, + "sha256": "5f67d85bfc91d5c7830aabd8089f4bca05883045d671d4e683d7c532cf3a34a1" + }, + "stored": { + "path": "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/docs/plans/original-completion-2026-10-02/checkpoints/native-hot-loop-r325-r329/024-tree_core.hpp", + "bytes": 3467, + "sha256": "5f67d85bfc91d5c7830aabd8089f4bca05883045d671d4e683d7c532cf3a34a1" + }, + "gzip": false, + "raw_bytes": 3467, + "raw_sha256": "5f67d85bfc91d5c7830aabd8089f4bca05883045d671d4e683d7c532cf3a34a1" + }, + { + "original": { + "path": "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/artifacts/original-completion-2026-10-02/p23-02-native-hot-loop-candidate-r326/source/tree_numeric_core.cpp", + "bytes": 56138, + "sha256": "a2434ef2fbc9624aa3b62746cebde2cacdce78712d4d866774df54fbd291bee3" + }, + "stored": { + "path": "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/docs/plans/original-completion-2026-10-02/checkpoints/native-hot-loop-r325-r329/025-tree_numeric_core.cpp", + "bytes": 56138, + "sha256": "a2434ef2fbc9624aa3b62746cebde2cacdce78712d4d866774df54fbd291bee3" + }, + "gzip": false, + "raw_bytes": 56138, + "raw_sha256": "a2434ef2fbc9624aa3b62746cebde2cacdce78712d4d866774df54fbd291bee3" + }, + { + "original": { + "path": "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/artifacts/original-completion-2026-10-02/p23-02-native-hot-loop-local-actual-independent-review-r329.json", + "bytes": 88603, + "sha256": "56fab0fb4c8b9218dcfdf22b914f12843dad0b8fbadcfd96d81f0ef8ea8fa895" + }, + "stored": { + "path": "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/docs/plans/original-completion-2026-10-02/checkpoints/native-hot-loop-r325-r329/026-p23-02-native-hot-loop-local-actual-independent-review-r329.json.gz", + "bytes": 8877, + "sha256": "b8cb022743a13e6ffac99e823d635c3659e50dcdc9cb2a750e8bb2eac7ef9582" + }, + "gzip": true, + "raw_bytes": 88603, + "raw_sha256": "56fab0fb4c8b9218dcfdf22b914f12843dad0b8fbadcfd96d81f0ef8ea8fa895" + }, + { + "original": { + "path": "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/artifacts/original-completion-2026-10-02/p23-02-native-hot-loop-local-preexecution-independent-review-r329.json", + "bytes": 75701, + "sha256": "97170206d613635be3e234270fbd9e83de7044674bb8d3305d31eceb676a0e6e" + }, + "stored": { + "path": "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/docs/plans/original-completion-2026-10-02/checkpoints/native-hot-loop-r325-r329/027-p23-02-native-hot-loop-local-preexecution-independent-review-r329.json.gz", + "bytes": 7215, + "sha256": "f15899fecc4ac8d59d62fce026320c12095398c3e64548b3e086a5f10a17a369" + }, + "gzip": true, + "raw_bytes": 75701, + "raw_sha256": "97170206d613635be3e234270fbd9e83de7044674bb8d3305d31eceb676a0e6e" + }, + { + "original": { + "path": "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/artifacts/original-completion-2026-10-02/p23-02-native-hot-loop-local-r329/actual-root-verification.json", + "bytes": 53507, + "sha256": "3be337d874c4e6f88bd0a1f4a7a58d46bbe0d3df3e76666e6606653b27d8d4bc" + }, + "stored": { + "path": "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/docs/plans/original-completion-2026-10-02/checkpoints/native-hot-loop-r325-r329/028-actual-root-verification.json.gz", + "bytes": 6582, + "sha256": "53760d41d593e40f8e0b042e992742e6280278bd0710c6b68d4cbed3b753a4b3" + }, + "gzip": true, + "raw_bytes": 53507, + "raw_sha256": "3be337d874c4e6f88bd0a1f4a7a58d46bbe0d3df3e76666e6606653b27d8d4bc" + }, + { + "original": { + "path": "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/artifacts/original-completion-2026-10-02/p23-02-native-hot-loop-local-r329/candidate-guarded.gd", + "bytes": 1030, + "sha256": "2cf049a15dfa1f81f6b3b1d2e46c99207254a4d08320c7da7c5fb719a9d8986c" + }, + "stored": { + "path": "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/docs/plans/original-completion-2026-10-02/checkpoints/native-hot-loop-r325-r329/029-candidate-guarded.gd", + "bytes": 1030, + "sha256": "2cf049a15dfa1f81f6b3b1d2e46c99207254a4d08320c7da7c5fb719a9d8986c" + }, + "gzip": false, + "raw_bytes": 1030, + "raw_sha256": "2cf049a15dfa1f81f6b3b1d2e46c99207254a4d08320c7da7c5fb719a9d8986c" + }, + { + "original": { + "path": "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/artifacts/original-completion-2026-10-02/p23-02-native-hot-loop-local-r329/candidate.gdextension", + "bytes": 212, + "sha256": "8e9b3929b86c066f5ced41e669c3da1ff326763100df38495b814116dc1e759b" + }, + "stored": { + "path": "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/docs/plans/original-completion-2026-10-02/checkpoints/native-hot-loop-r325-r329/030-candidate.gdextension", + "bytes": 212, + "sha256": "8e9b3929b86c066f5ced41e669c3da1ff326763100df38495b814116dc1e759b" + }, + "gzip": false, + "raw_bytes": 212, + "raw_sha256": "8e9b3929b86c066f5ced41e669c3da1ff326763100df38495b814116dc1e759b" + }, + { + "original": { + "path": "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/artifacts/original-completion-2026-10-02/p23-02-native-hot-loop-local-r329/driver-before-prepath-repair.py", + "bytes": 6898, + "sha256": "fb1ec87b317eb506e482ad4833b5439b7c8b095438440b5cb9dea1247d4a1c2f" + }, + "stored": { + "path": "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/docs/plans/original-completion-2026-10-02/checkpoints/native-hot-loop-r325-r329/031-driver-before-prepath-repair.py", + "bytes": 6898, + "sha256": "fb1ec87b317eb506e482ad4833b5439b7c8b095438440b5cb9dea1247d4a1c2f" + }, + "gzip": false, + "raw_bytes": 6898, + "raw_sha256": "fb1ec87b317eb506e482ad4833b5439b7c8b095438440b5cb9dea1247d4a1c2f" + }, + { + "original": { + "path": "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/artifacts/original-completion-2026-10-02/p23-02-native-hot-loop-local-r329/execution.json", + "bytes": 10931, + "sha256": "2762ca36c104c65afe92f093ce79aee04b7a6f3cf229dbc4399984268c9bff72" + }, + "stored": { + "path": "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/docs/plans/original-completion-2026-10-02/checkpoints/native-hot-loop-r325-r329/032-execution.json.gz", + "bytes": 2205, + "sha256": "91a227c2def9efe6b3b61bc88e43669b8b7fa804bfb18a433f5ab033367c81f8" + }, + "gzip": true, + "raw_bytes": 10931, + "raw_sha256": "2762ca36c104c65afe92f093ce79aee04b7a6f3cf229dbc4399984268c9bff72" + }, + { + "original": { + "path": "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/artifacts/original-completion-2026-10-02/p23-02-native-hot-loop-local-r329/input-before-prepath-repair.json", + "bytes": 1007217, + "sha256": "3077d4f4ea4cc48110073bad204a601d6c2822d4074528b195838e80c2b6b8ec" + }, + "stored": { + "path": "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/docs/plans/original-completion-2026-10-02/checkpoints/native-hot-loop-r325-r329/033-input-before-prepath-repair.json.gz", + "bytes": 230512, + "sha256": "3cfa0bd25847e50171fc1d155af826540c61319bba1352d112ca876e339eaea3" + }, + "gzip": true, + "raw_bytes": 1007217, + "raw_sha256": "3077d4f4ea4cc48110073bad204a601d6c2822d4074528b195838e80c2b6b8ec" + }, + { + "original": { + "path": "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/artifacts/original-completion-2026-10-02/p23-02-native-hot-loop-local-r329/input.json", + "bytes": 1008002, + "sha256": "46ed33e78a3c98855bed0ab570cea045fd8b786305787436d12ce81317c0c099" + }, + "stored": { + "path": "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/docs/plans/original-completion-2026-10-02/checkpoints/native-hot-loop-r325-r329/034-input.json.gz", + "bytes": 230748, + "sha256": "b71945eb8aa284a952c3e08e2a3c31683cae651b4546fba3717f9a76c6b419be" + }, + "gzip": true, + "raw_bytes": 1008002, + "raw_sha256": "46ed33e78a3c98855bed0ab570cea045fd8b786305787436d12ce81317c0c099" + }, + { + "original": { + "path": "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/artifacts/original-completion-2026-10-02/p23-02-native-hot-loop-local-r329/local/adapter-owned-cleanup.json", + "bytes": 351, + "sha256": "c1f3d289b362cc61b9be169a3994be27df39f35e6221881b4a9678ce3c72ba54" + }, + "stored": { + "path": "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/docs/plans/original-completion-2026-10-02/checkpoints/native-hot-loop-r325-r329/035-adapter-owned-cleanup.json", + "bytes": 351, + "sha256": "c1f3d289b362cc61b9be169a3994be27df39f35e6221881b4a9678ce3c72ba54" + }, + "gzip": false, + "raw_bytes": 351, + "raw_sha256": "c1f3d289b362cc61b9be169a3994be27df39f35e6221881b4a9678ce3c72ba54" + }, + { + "original": { + "path": "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/artifacts/original-completion-2026-10-02/p23-02-native-hot-loop-local-r329/local/ended.json", + "bytes": 1746, + "sha256": "dd938bec0af0d85db308c70ee60b328e9af5831dfefbd7c0b8f89561423ac981" + }, + "stored": { + "path": "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/docs/plans/original-completion-2026-10-02/checkpoints/native-hot-loop-r325-r329/036-ended.json", + "bytes": 1746, + "sha256": "dd938bec0af0d85db308c70ee60b328e9af5831dfefbd7c0b8f89561423ac981" + }, + "gzip": false, + "raw_bytes": 1746, + "raw_sha256": "dd938bec0af0d85db308c70ee60b328e9af5831dfefbd7c0b8f89561423ac981" + }, + { + "original": { + "path": "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/artifacts/original-completion-2026-10-02/p23-02-native-hot-loop-local-r329/local/engine-native.log", + "bytes": 50284, + "sha256": "38f33e0c67420fae498c9011a11c7a0a086003b6ccb0aaac2695adf275764bfb" + }, + "stored": { + "path": "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/docs/plans/original-completion-2026-10-02/checkpoints/native-hot-loop-r325-r329/037-engine-native.log.gz", + "bytes": 6581, + "sha256": "552012c3d07f6696c655c97ece0645b2269bacebf4c3cc854d0db0f55c6fb167" + }, + "gzip": true, + "raw_bytes": 50284, + "raw_sha256": "38f33e0c67420fae498c9011a11c7a0a086003b6ccb0aaac2695adf275764bfb" + }, + { + "original": { + "path": "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/artifacts/original-completion-2026-10-02/p23-02-native-hot-loop-local-r329/local/outer-owned-finally.json", + "bytes": 964, + "sha256": "561ba831e60b2f4a2452921e47f4142670bbb8079e1190492bab49abb5761800" + }, + "stored": { + "path": "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/docs/plans/original-completion-2026-10-02/checkpoints/native-hot-loop-r325-r329/038-outer-owned-finally.json", + "bytes": 964, + "sha256": "561ba831e60b2f4a2452921e47f4142670bbb8079e1190492bab49abb5761800" + }, + "gzip": false, + "raw_bytes": 964, + "raw_sha256": "561ba831e60b2f4a2452921e47f4142670bbb8079e1190492bab49abb5761800" + }, + { + "original": { + "path": "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/artifacts/original-completion-2026-10-02/p23-02-native-hot-loop-local-r329/local/process.json", + "bytes": 6582, + "sha256": "f2b7d1c85dd2c424c7e8788fc33b02241a5b238a76f714643aaaafd4dffd009f" + }, + "stored": { + "path": "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/docs/plans/original-completion-2026-10-02/checkpoints/native-hot-loop-r325-r329/039-process.json", + "bytes": 6582, + "sha256": "f2b7d1c85dd2c424c7e8788fc33b02241a5b238a76f714643aaaafd4dffd009f" + }, + "gzip": false, + "raw_bytes": 6582, + "raw_sha256": "f2b7d1c85dd2c424c7e8788fc33b02241a5b238a76f714643aaaafd4dffd009f" + }, + { + "original": { + "path": "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/artifacts/original-completion-2026-10-02/p23-02-native-hot-loop-local-r329/local/started.json", + "bytes": 2353, + "sha256": "5cf80945887a0480217a70844007e5ffc93e6170567dce2916506513179c9a47" + }, + "stored": { + "path": "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/docs/plans/original-completion-2026-10-02/checkpoints/native-hot-loop-r325-r329/040-started.json", + "bytes": 2353, + "sha256": "5cf80945887a0480217a70844007e5ffc93e6170567dce2916506513179c9a47" + }, + "gzip": false, + "raw_bytes": 2353, + "raw_sha256": "5cf80945887a0480217a70844007e5ffc93e6170567dce2916506513179c9a47" + }, + { + "original": { + "path": "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/artifacts/original-completion-2026-10-02/p23-02-native-hot-loop-local-r329/local/stderr.log", + "bytes": 0, + "sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + }, + "stored": { + "path": "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/docs/plans/original-completion-2026-10-02/checkpoints/native-hot-loop-r325-r329/041-stderr.log", + "bytes": 0, + "sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + }, + "gzip": false, + "raw_bytes": 0, + "raw_sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + }, + { + "original": { + "path": "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/artifacts/original-completion-2026-10-02/p23-02-native-hot-loop-local-r329/local/stdout.log", + "bytes": 50434, + "sha256": "0d80fd887d3ae60d97ac91ec360387c6dd4aa06b0417cc000f2567e5b401735f" + }, + "stored": { + "path": "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/docs/plans/original-completion-2026-10-02/checkpoints/native-hot-loop-r325-r329/042-stdout.log.gz", + "bytes": 6669, + "sha256": "4685420d1d3c251e281eb19a098c1936b13bd3be16eed159b6fbd26cbe9b0e39" + }, + "gzip": true, + "raw_bytes": 50434, + "raw_sha256": "0d80fd887d3ae60d97ac91ec360387c6dd4aa06b0417cc000f2567e5b401735f" + }, + { + "original": { + "path": "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/artifacts/original-completion-2026-10-02/p23-02-native-hot-loop-local-r329/local-measurement/fixtures.varbytes", + "bytes": 606268, + "sha256": "6ec6e879a2de02b9d4081295b33e53e2060f31829e1fb674168a414d8b3e5c2e" + }, + "stored": { + "path": "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/docs/plans/original-completion-2026-10-02/checkpoints/native-hot-loop-r325-r329/043-fixtures.varbytes.gz", + "bytes": 9748, + "sha256": "be91546209959f47557bb71ed4e371b67f794f2ad6bc6519781638acd27882ae" + }, + "gzip": true, + "raw_bytes": 606268, + "raw_sha256": "6ec6e879a2de02b9d4081295b33e53e2060f31829e1fb674168a414d8b3e5c2e" + }, + { + "original": { + "path": "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/artifacts/original-completion-2026-10-02/p23-02-native-hot-loop-local-r329/local-measurement/protocol-report.json", + "bytes": 47951, + "sha256": "ce2f9c68597f2b927346e3c6ea0dec0e7c77e9682e321423bfc966e6c0d3fa0f" + }, + "stored": { + "path": "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/docs/plans/original-completion-2026-10-02/checkpoints/native-hot-loop-r325-r329/044-protocol-report.json.gz", + "bytes": 2507, + "sha256": "f2137a0f131b89a27513bbe551b242c6eb19e282218056fdb775d4774c1a7a33" + }, + "gzip": true, + "raw_bytes": 47951, + "raw_sha256": "ce2f9c68597f2b927346e3c6ea0dec0e7c77e9682e321423bfc966e6c0d3fa0f" + }, + { + "original": { + "path": "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/artifacts/original-completion-2026-10-02/p23-02-native-hot-loop-local-r329/local-measurement/report.json", + "bytes": 404, + "sha256": "6ecfcd3b913feba90c841f8beb4c232a2b5e92dd4380b493e17c7936c6312a40" + }, + "stored": { + "path": "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/docs/plans/original-completion-2026-10-02/checkpoints/native-hot-loop-r325-r329/045-report.json", + "bytes": 404, + "sha256": "6ecfcd3b913feba90c841f8beb4c232a2b5e92dd4380b493e17c7936c6312a40" + }, + "gzip": false, + "raw_bytes": 404, + "raw_sha256": "6ecfcd3b913feba90c841f8beb4c232a2b5e92dd4380b493e17c7936c6312a40" + }, + { + "original": { + "path": "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/artifacts/original-completion-2026-10-02/p23-02-native-hot-loop-local-r329/local-phase.json", + "bytes": 8037, + "sha256": "2bb84c884270d087d8acfbe1845bf8bcacccbac5cf3f6b7923cc3fa4fcbc53cc" + }, + "stored": { + "path": "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/docs/plans/original-completion-2026-10-02/checkpoints/native-hot-loop-r325-r329/046-local-phase.json", + "bytes": 8037, + "sha256": "2bb84c884270d087d8acfbe1845bf8bcacccbac5cf3f6b7923cc3fa4fcbc53cc" + }, + "gzip": false, + "raw_bytes": 8037, + "raw_sha256": "2bb84c884270d087d8acfbe1845bf8bcacccbac5cf3f6b7923cc3fa4fcbc53cc" + }, + { + "original": { + "path": "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/artifacts/original-completion-2026-10-02/p23-02-native-hot-loop-local-r329/local-profile.json", + "bytes": 907, + "sha256": "e1def3043186b68cdcb2e154b6f44da34eacc352f05f0b619ff8bc2565df6c56" + }, + "stored": { + "path": "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/docs/plans/original-completion-2026-10-02/checkpoints/native-hot-loop-r325-r329/047-local-profile.json", + "bytes": 907, + "sha256": "e1def3043186b68cdcb2e154b6f44da34eacc352f05f0b619ff8bc2565df6c56" + }, + "gzip": false, + "raw_bytes": 907, + "raw_sha256": "e1def3043186b68cdcb2e154b6f44da34eacc352f05f0b619ff8bc2565df6c56" + }, + { + "original": { + "path": "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/artifacts/original-completion-2026-10-02/p23-02-native-hot-loop-local-r329/local.gd", + "bytes": 17804, + "sha256": "1008bfcd3db6e86b3b8e2e4bec8331be62d158610516877a3e4438c0d996821a" + }, + "stored": { + "path": "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/docs/plans/original-completion-2026-10-02/checkpoints/native-hot-loop-r325-r329/048-local.gd", + "bytes": 17804, + "sha256": "1008bfcd3db6e86b3b8e2e4bec8331be62d158610516877a3e4438c0d996821a" + }, + "gzip": false, + "raw_bytes": 17804, + "raw_sha256": "1008bfcd3db6e86b3b8e2e4bec8331be62d158610516877a3e4438c0d996821a" + }, + { + "original": { + "path": "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/artifacts/original-completion-2026-10-02/p23-02-native-hot-loop-local-r329/preexecution-root-verification.json", + "bytes": 45677, + "sha256": "ba7aba3067fd05e656d059499ab055e3000c17867f3ab641069d379d0d00834c" + }, + "stored": { + "path": "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/docs/plans/original-completion-2026-10-02/checkpoints/native-hot-loop-r325-r329/049-preexecution-root-verification.json.gz", + "bytes": 5171, + "sha256": "a63e264269708cd9f67fe17cacf5e9f7f7e174a9b52d84cf9cebdf21cd80c86b" + }, + "gzip": true, + "raw_bytes": 45677, + "raw_sha256": "ba7aba3067fd05e656d059499ab055e3000c17867f3ab641069d379d0d00834c" + }, + { + "original": { + "path": "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/artifacts/original-completion-2026-10-02/p23-02-native-hot-loop-local-r329/protocol_helper.gd", + "bytes": 15690, + "sha256": "198a16aa04d63bc46ad6ccadaaca8074b8d881db14148fa06b20dc51b2e8569b" + }, + "stored": { + "path": "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/docs/plans/original-completion-2026-10-02/checkpoints/native-hot-loop-r325-r329/050-protocol_helper.gd", + "bytes": 15690, + "sha256": "198a16aa04d63bc46ad6ccadaaca8074b8d881db14148fa06b20dc51b2e8569b" + }, + "gzip": false, + "raw_bytes": 15690, + "raw_sha256": "198a16aa04d63bc46ad6ccadaaca8074b8d881db14148fa06b20dc51b2e8569b" + }, + { + "original": { + "path": "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/artifacts/original-completion-2026-10-02/p23-02-native-hot-loop-local-r329.py", + "bytes": 6896, + "sha256": "30ef2a891eaddad5d4a2240730a03a06458e71affb138b4c7719e59ee2967317" + }, + "stored": { + "path": "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/docs/plans/original-completion-2026-10-02/checkpoints/native-hot-loop-r325-r329/051-p23-02-native-hot-loop-local-r329.py", + "bytes": 6896, + "sha256": "30ef2a891eaddad5d4a2240730a03a06458e71affb138b4c7719e59ee2967317" + }, + "gzip": false, + "raw_bytes": 6896, + "raw_sha256": "30ef2a891eaddad5d4a2240730a03a06458e71affb138b4c7719e59ee2967317" + }, + { + "original": { + "path": "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/artifacts/original-completion-2026-10-02/p23-02-native-hot-loop-numeric-r328/corpus.tsv", + "bytes": 4556458, + "sha256": "51036394c19bd2c3212a80fb67f7406e6f0d049ec018910d36a8d3856a0b270c" + }, + "stored": { + "path": "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/docs/plans/original-completion-2026-10-02/checkpoints/native-hot-loop-r325-r329/052-corpus.tsv.gz", + "bytes": 452756, + "sha256": "efc84fee8f38e4a1c11a0732159062da97da9fc73e26fb304babc4737ae8b15a" + }, + "gzip": true, + "raw_bytes": 4556458, + "raw_sha256": "51036394c19bd2c3212a80fb67f7406e6f0d049ec018910d36a8d3856a0b270c" + }, + { + "original": { + "path": "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/artifacts/original-completion-2026-10-02/p23-02-native-hot-loop-numeric-r328/execution.json", + "bytes": 1741, + "sha256": "aa28610e43ff59d7b1daae8151de5500f7c95d0565fc6f721ccccfc9d9d0ad65" + }, + "stored": { + "path": "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/docs/plans/original-completion-2026-10-02/checkpoints/native-hot-loop-r325-r329/053-execution.json", + "bytes": 1741, + "sha256": "aa28610e43ff59d7b1daae8151de5500f7c95d0565fc6f721ccccfc9d9d0ad65" + }, + "gzip": false, + "raw_bytes": 1741, + "raw_sha256": "aa28610e43ff59d7b1daae8151de5500f7c95d0565fc6f721ccccfc9d9d0ad65" + }, + { + "original": { + "path": "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/artifacts/original-completion-2026-10-02/p23-02-native-hot-loop-numeric-r328/input.json", + "bytes": 3289, + "sha256": "70e8a4e422ae229087454320fac2f5641cbfa24cee393a34cb79e1e14b7e392b" + }, + "stored": { + "path": "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/docs/plans/original-completion-2026-10-02/checkpoints/native-hot-loop-r325-r329/054-input.json", + "bytes": 3289, + "sha256": "70e8a4e422ae229087454320fac2f5641cbfa24cee393a34cb79e1e14b7e392b" + }, + "gzip": false, + "raw_bytes": 3289, + "raw_sha256": "70e8a4e422ae229087454320fac2f5641cbfa24cee393a34cb79e1e14b7e392b" + }, + { + "original": { + "path": "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/artifacts/original-completion-2026-10-02/p23-02-native-hot-loop-numeric-r328/oracle", + "bytes": 59928, + "sha256": "f45dd0a7cdf79c632d29a276fbcc726c71a79d036009db72226fa182a2fd1043" + }, + "stored": { + "path": "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/docs/plans/original-completion-2026-10-02/checkpoints/native-hot-loop-r325-r329/055-oracle.gz", + "bytes": 16483, + "sha256": "a12db6181cea2b152647c9400e904daf2f77bbb8e4f08b22618668f600f8dc75" + }, + "gzip": true, + "raw_bytes": 59928, + "raw_sha256": "f45dd0a7cdf79c632d29a276fbcc726c71a79d036009db72226fa182a2fd1043" + }, + { + "original": { + "path": "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/artifacts/original-completion-2026-10-02/p23-02-native-hot-loop-numeric-r328/oracle.cpp", + "bytes": 3291, + "sha256": "5d786e3c3be56c87c4311f80f5e9d64411f0875bb00f97ca778d191dbffe8b66" + }, + "stored": { + "path": "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/docs/plans/original-completion-2026-10-02/checkpoints/native-hot-loop-r325-r329/056-oracle.cpp", + "bytes": 3291, + "sha256": "5d786e3c3be56c87c4311f80f5e9d64411f0875bb00f97ca778d191dbffe8b66" + }, + "gzip": false, + "raw_bytes": 3291, + "raw_sha256": "5d786e3c3be56c87c4311f80f5e9d64411f0875bb00f97ca778d191dbffe8b66" + }, + { + "original": { + "path": "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/artifacts/original-completion-2026-10-02/p23-02-native-hot-loop-numeric-r328/phase0.stderr", + "bytes": 0, + "sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + }, + "stored": { + "path": "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/docs/plans/original-completion-2026-10-02/checkpoints/native-hot-loop-r325-r329/057-phase0.stderr", + "bytes": 0, + "sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + }, + "gzip": false, + "raw_bytes": 0, + "raw_sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + }, + { + "original": { + "path": "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/artifacts/original-completion-2026-10-02/p23-02-native-hot-loop-numeric-r328/phase0.stdout", + "bytes": 0, + "sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + }, + "stored": { + "path": "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/docs/plans/original-completion-2026-10-02/checkpoints/native-hot-loop-r325-r329/058-phase0.stdout", + "bytes": 0, + "sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + }, + "gzip": false, + "raw_bytes": 0, + "raw_sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + }, + { + "original": { + "path": "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/artifacts/original-completion-2026-10-02/p23-02-native-hot-loop-numeric-r328/phase1.stderr", + "bytes": 0, + "sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + }, + "stored": { + "path": "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/docs/plans/original-completion-2026-10-02/checkpoints/native-hot-loop-r325-r329/059-phase1.stderr", + "bytes": 0, + "sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + }, + "gzip": false, + "raw_bytes": 0, + "raw_sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + }, + { + "original": { + "path": "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/artifacts/original-completion-2026-10-02/p23-02-native-hot-loop-numeric-r328/phase1.stdout", + "bytes": 32, + "sha256": "ae8d713cb96e313f625a63ce0d86216be75e7f4063a8ec2ce66abd8da181ec91" + }, + "stored": { + "path": "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/docs/plans/original-completion-2026-10-02/checkpoints/native-hot-loop-r325-r329/060-phase1.stdout", + "bytes": 32, + "sha256": "ae8d713cb96e313f625a63ce0d86216be75e7f4063a8ec2ce66abd8da181ec91" + }, + "gzip": false, + "raw_bytes": 32, + "raw_sha256": "ae8d713cb96e313f625a63ce0d86216be75e7f4063a8ec2ce66abd8da181ec91" + }, + { + "original": { + "path": "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/artifacts/original-completion-2026-10-02/p23-02-native-hot-loop-static-independent-review-r326.json", + "bytes": 12687, + "sha256": "717f8aa9a1b12425b79f7dbbb81b4e0526de32ddd836114525b53d4fd7bc05e1" + }, + "stored": { + "path": "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/docs/plans/original-completion-2026-10-02/checkpoints/native-hot-loop-r325-r329/061-p23-02-native-hot-loop-static-independent-review-r326.json.gz", + "bytes": 3058, + "sha256": "2febf674f923e6dbd3b9e68134bc9a6fc4f32717a165b5071aa13f5653f7cf10" + }, + "gzip": true, + "raw_bytes": 12687, + "raw_sha256": "717f8aa9a1b12425b79f7dbbb81b4e0526de32ddd836114525b53d4fd7bc05e1" + }, + { + "original": { + "path": "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/artifacts/original-completion-2026-10-02/p23-02-native-hot-loop-static-root-verification-r326.json", + "bytes": 6482, + "sha256": "4b3e08a9b9982f818c2eea96e0d4d6b927d32c9ddf76a4338cb71810d81486d6" + }, + "stored": { + "path": "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/docs/plans/original-completion-2026-10-02/checkpoints/native-hot-loop-r325-r329/062-p23-02-native-hot-loop-static-root-verification-r326.json", + "bytes": 6482, + "sha256": "4b3e08a9b9982f818c2eea96e0d4d6b927d32c9ddf76a4338cb71810d81486d6" + }, + "gzip": false, + "raw_bytes": 6482, + "raw_sha256": "4b3e08a9b9982f818c2eea96e0d4d6b927d32c9ddf76a4338cb71810d81486d6" + }, + { + "original": { + "path": "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/artifacts/original-completion-2026-10-02/p23-02-native-hot-loop-work-contract-r325.json", + "bytes": 2313, + "sha256": "bbb488bbb0229c31711982867375f41487ec7f5c9343eb6eb5903b8321e05602" + }, + "stored": { + "path": "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/docs/plans/original-completion-2026-10-02/checkpoints/native-hot-loop-r325-r329/063-p23-02-native-hot-loop-work-contract-r325.json", + "bytes": 2313, + "sha256": "bbb488bbb0229c31711982867375f41487ec7f5c9343eb6eb5903b8321e05602" + }, + "gzip": false, + "raw_bytes": 2313, + "raw_sha256": "bbb488bbb0229c31711982867375f41487ec7f5c9343eb6eb5903b8321e05602" + }, + { + "original": { + "path": "/private/tmp/thetree-numeric-oracle-r328.py", + "bytes": 5646, + "sha256": "21675754454c5d8fca8b35145217f32762243e39908e67bf2a1bdb783028c94c" + }, + "stored": { + "path": "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/docs/plans/original-completion-2026-10-02/checkpoints/native-hot-loop-r325-r329/064-thetree-numeric-oracle-r328.py", + "bytes": 5646, + "sha256": "21675754454c5d8fca8b35145217f32762243e39908e67bf2a1bdb783028c94c" + }, + "gzip": false, + "raw_bytes": 5646, + "raw_sha256": "21675754454c5d8fca8b35145217f32762243e39908e67bf2a1bdb783028c94c" + }, + { + "original": { + "path": "/private/tmp/thetree-plan-local-r329.py", + "bytes": 3582, + "sha256": "c44d13cdd092043bef03930d279aa40429f7eba0b8c64731e2139a8ade302633" + }, + "stored": { + "path": "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/docs/plans/original-completion-2026-10-02/checkpoints/native-hot-loop-r325-r329/065-thetree-plan-local-r329.py", + "bytes": 3582, + "sha256": "c44d13cdd092043bef03930d279aa40429f7eba0b8c64731e2139a8ade302633" + }, + "gzip": false, + "raw_bytes": 3582, + "raw_sha256": "c44d13cdd092043bef03930d279aa40429f7eba0b8c64731e2139a8ade302633" + }, + { + "original": { + "path": "/private/tmp/thetree-prepare-local-r329.py", + "bytes": 5951, + "sha256": "cd26cab2abf22c6f0f5eb70dc5de5fd2df1df473bf65c1c0efa93657bd16e86f" + }, + "stored": { + "path": "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/docs/plans/original-completion-2026-10-02/checkpoints/native-hot-loop-r325-r329/066-thetree-prepare-local-r329.py", + "bytes": 5951, + "sha256": "cd26cab2abf22c6f0f5eb70dc5de5fd2df1df473bf65c1c0efa93657bd16e86f" + }, + "gzip": false, + "raw_bytes": 5951, + "raw_sha256": "cd26cab2abf22c6f0f5eb70dc5de5fd2df1df473bf65c1c0efa93657bd16e86f" + }, + { + "original": { + "path": "/private/tmp/thetree-prepare-r326.py", + "bytes": 11548, + "sha256": "b75b738a0f4a785445b86e6d6ab99d6908337158b359c03f2cba049f18f7045a" + }, + "stored": { + "path": "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/docs/plans/original-completion-2026-10-02/checkpoints/native-hot-loop-r325-r329/067-thetree-prepare-r326.py.gz", + "bytes": 4144, + "sha256": "850ba11eb3861c73454dd85cf040e0d572d0e4d24edd9a0e93ec0d1a9d05bca1" + }, + "gzip": true, + "raw_bytes": 11548, + "raw_sha256": "b75b738a0f4a785445b86e6d6ab99d6908337158b359c03f2cba049f18f7045a" + } + ] +} diff --git a/docs/plans/original-completion-2026-10-02/execution/CURRENT_NATIVE_HOT_LOOP_R325_R329.md b/docs/plans/original-completion-2026-10-02/execution/CURRENT_NATIVE_HOT_LOOP_R325_R329.md new file mode 100644 index 0000000..b83bc95 --- /dev/null +++ b/docs/plans/original-completion-2026-10-02/execution/CURRENT_NATIVE_HOT_LOOP_R325_R329.md @@ -0,0 +1,17 @@ +# P23-02 native修正候補 r325–r329 + +ユーザーの現在成果をコミット・pushする指示に従い、r326/library327のソース、ビルド、局所証拠と独立レビューを保存する。制作r32、選定済み実装親r274/library275は変更していない。新候補は未選定・未採用であり、正式完了追加0。30パッケージ/88成果単位、部分点なし、正式9/着手19/予定60を維持する。 + +修正はnumeric frontierのheap化、cached edge参照、PackedInt64Array書込、nearest goal読取、性能記録の固定長配列、Guest99キーの呼出内保持にまとめた。callback順・算術・保存値・公開出力を保つ範囲で実装した。生成初期版から縮小入力互換性を修正した最終CPPはSHA256 a2434ef2fbc9624aa3b62746cebde2cacdce78712d4d866774df54fbd291bee3。Guest/Runtimeは親r313-r02とbyte不変。 + +r327はbridge compile1/link1、SDK compile0で正常終了し、libraryは417,184 bytes/SHA256 64608dccb3441594603fa5561fad3e2e90b055f32e4823c4864d2486a298602c。r328の16,388入力で原版・候補の7出力fieldsが全文一致した。実statusはusable13,549/unusable2,839/wrapped0であり、numeric corpus単独のwrap到達を認定しない。 + +r329はPID36884正常終了、7,415チェック/失敗0。Guest53記録606,268 bytesが原315と全文一致し、その固定rawに対応する既存codec証拠を再利用した。既存50protocolの原/cold/warm/clear、縮小入力、解放、同じcacheへの再入inner9/outer7/warm9を確認した。実wrap-prefixはこの局所protocol検査が支える。試験件数を成果単位の完了数に換算しない。 + +静的r326、build実r327、局所実r329は読取専用の独立レビューで有限scopeのみ支持された。r329 receiptは88,603 bytes/SHA256 56fab0fb4c8b9218dcfdf22b914f12843dad0b8fbadcfd96d81f0ef8ea8fa895、blockers=[]。rootがdirect161/current D4 4/archive4の169pinsを再照合し全一致した。ビルド、stage、source/default、canonical D4保持の証拠を保存している。 + +次に閉じる1件はP23-02、依存P08/P22。全受入条件・対象版はcurrent r320 dispositionと前記CURRENT_NATIVE_DIAGNOSTIC_R321_R323.mdを継承する。C01〜C09/X1〜X8の17条件は未受入。C08/X8だけWindows実機block。Ctrl+ホイールと左ボタン保持ドラッグはユーザー指示で延期し、未達・非免除のまま残す。 + +次の必要な検証は同じ保存入力によるS6とwhole647の比較(準備中r330、未実行)。代表負荷性能、安定利益、50ms/P95/D10、通常OS操作、小画面、x3、同一Windows、正確な最終版の必須全回帰・独立受入は未達。未確定の環境・数値は設定しない。今回の有限証拠を全状態一致・性能改善・正式単位受入へ拡張しない。 + +保存入口はcheckpoints/native-hot-loop-r325-r329/manifest.json。ソースと全証拠を内容保持圧縮し、全保存コピーの復元byte一致を検証した。既存成果や未実行検証を削除せず、今回のpushのために追加回帰や別の実装を起動していない。 diff --git a/docs/plans/original-completion-2026-10-02/execution/native-hot-loop-r331-review/p23-02-native-hot-loop-checkpoint-independent-review-r331.json.gz b/docs/plans/original-completion-2026-10-02/execution/native-hot-loop-r331-review/p23-02-native-hot-loop-checkpoint-independent-review-r331.json.gz new file mode 100644 index 0000000..1512496 Binary files /dev/null and b/docs/plans/original-completion-2026-10-02/execution/native-hot-loop-r331-review/p23-02-native-hot-loop-checkpoint-independent-review-r331.json.gz differ diff --git a/docs/plans/original-completion-2026-10-02/execution/native-hot-loop-r331-review/p23-02-native-hot-loop-checkpoint-root-verification-r331.json.gz b/docs/plans/original-completion-2026-10-02/execution/native-hot-loop-r331-review/p23-02-native-hot-loop-checkpoint-root-verification-r331.json.gz new file mode 100644 index 0000000..038a47e Binary files /dev/null and b/docs/plans/original-completion-2026-10-02/execution/native-hot-loop-r331-review/p23-02-native-hot-loop-checkpoint-root-verification-r331.json.gz differ