diff --git a/docs/plans/original-completion-2026-10-02/checkpoints/guest-need-normalization-r295-r298/README.md b/docs/plans/original-completion-2026-10-02/checkpoints/guest-need-normalization-r295-r298/README.md new file mode 100644 index 0000000..d57f2fd --- /dev/null +++ b/docs/plans/original-completion-2026-10-02/checkpoints/guest-need-normalization-r295-r298/README.md @@ -0,0 +1,11 @@ +# P23-02 Guest Need normalization r295–r298 checkpoint + +レビュー済みの未採用候補と有限検証を保存する。r295 は Need map の正規化を共有する候補で、r296 の実 Godot 局所検証は 1619/0。224 個の Need 境界・キー型・readonly の組合せと追加 7 入力、custom definition callback 2 ケースを検証した。233 行の結果 fixture は原結果と byte 完全一致を独立レビューで確認した。結果の所有、公開状態、入力保持も固定された局所条件で確認している。 + +r296 の単一 Need helper 2000 回の有限な測定は原 28,461 µs、候補 41,815 µs。代表セーブ、6 Need、whole、安定した性能差の証拠ではない。この観測で速度利益を示しておらず、r295 は未採用。 + +r298 は同版 Godot 一次ソースに基づき、r295 の共有 guard の全走査を除いた候補。exact service/guest/definition script と Dictionary guard、原 validation、custom fallback、計算・呼出順序を保持する。静的独立レビューのみ支持され、r296 の実行証拠は r298 に適用しない。r298 の実行・whole・性能・採用の判断は未了。r299 は準備中・未実行であり、本 checkpoint に実行証拠として含めない。 + +現在の実装親 r274/library275 を保持する。正式完了の追加は 0。30 パッケージ / 88 成果単位、正式完了 9 / 着手 19 / 予定 60 の定義を維持する。P23-02 の原 17 条件は未受入。必要な最終全回帰、性能予算、通常 OS 入力、同一 Windows 配布版実機などは残件。手動 Ctrl 保持 wheel と左保持 drag は延期し、免除しない。Windows C08/X8 のみをブロック扱いとする。 + +manifest.json は原ファイルと保存コピーのサイズ・SHA256 を固定する。大きな JSON は gzip の復元内容を完全一致で検証した。原参照の絶対パスとレビュー時の r294 台帳文脈を保持する。SDK・engine・stage・全実行証拠を備えた単独実行パッケージではなく、完全な証拠はローカル artifacts に残す。親 checkpoints/.gdignore により保存 GDScript を本体の resource 走査から除外する。 diff --git a/docs/plans/original-completion-2026-10-02/checkpoints/guest-need-normalization-r295-r298/manifest.json b/docs/plans/original-completion-2026-10-02/checkpoints/guest-need-normalization-r295-r298/manifest.json new file mode 100644 index 0000000..9c71979 --- /dev/null +++ b/docs/plans/original-completion-2026-10-02/checkpoints/guest-need-normalization-r295-r298/manifest.json @@ -0,0 +1,197 @@ +{ + "schema": 1, + "unit": "P23-02", + "status": "unadopted_checkpoint", + "formal_completion_added": 0, + "entries": [ + { + "source": "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/artifacts/original-completion-2026-10-02/p23-02-guest-need-normalization-candidate-r295/input.json", + "checkpoint": "docs/plans/original-completion-2026-10-02/checkpoints/guest-need-normalization-r295-r298/p23-02-guest-need-normalization-candidate-r295/input.json", + "bytes": 4366, + "sha256": "f592b9895d9253be9e7f829cd88888fe166e77e00959570f7d4a7f0c3c169489", + "stored_bytes": 4366, + "stored_sha256": "f592b9895d9253be9e7f829cd88888fe166e77e00959570f7d4a7f0c3c169489", + "encoding": "identity" + }, + { + "source": "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/artifacts/original-completion-2026-10-02/p23-02-guest-need-normalization-candidate-r295/structural-check.json", + "checkpoint": "docs/plans/original-completion-2026-10-02/checkpoints/guest-need-normalization-r295-r298/p23-02-guest-need-normalization-candidate-r295/structural-check.json", + "bytes": 211, + "sha256": "20b5510966fce15f43e4682a367666b1e0b1bcfd28042462b10960af0c79a280", + "stored_bytes": 211, + "stored_sha256": "20b5510966fce15f43e4682a367666b1e0b1bcfd28042462b10960af0c79a280", + "encoding": "identity" + }, + { + "source": "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/artifacts/original-completion-2026-10-02/p23-02-guest-need-normalization-candidate-r295/source/guest_service.gd", + "checkpoint": "docs/plans/original-completion-2026-10-02/checkpoints/guest-need-normalization-r295-r298/p23-02-guest-need-normalization-candidate-r295/source/guest_service.gd", + "bytes": 64563, + "sha256": "2794f20630168d902e6a7fb55a0b5aacfbbcfcccd51d336f0724f03ee35ea249", + "stored_bytes": 64563, + "stored_sha256": "2794f20630168d902e6a7fb55a0b5aacfbbcfcccd51d336f0724f03ee35ea249", + "encoding": "identity" + }, + { + "source": "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/artifacts/original-completion-2026-10-02/p23-02-guest-need-normalization-static-independent-review-r295.json", + "checkpoint": "docs/plans/original-completion-2026-10-02/checkpoints/guest-need-normalization-r295-r298/p23-02-guest-need-normalization-static-independent-review-r295.json", + "bytes": 9244, + "sha256": "0954f512e6065c26ffc7d06c087fe973379a48c9cebef6752f19ec52d24f59d8", + "stored_bytes": 9244, + "stored_sha256": "0954f512e6065c26ffc7d06c087fe973379a48c9cebef6752f19ec52d24f59d8", + "encoding": "identity" + }, + { + "source": "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/artifacts/original-completion-2026-10-02/p23-02-guest-need-normalization-static-root-verification-r295.json", + "checkpoint": "docs/plans/original-completion-2026-10-02/checkpoints/guest-need-normalization-r295-r298/p23-02-guest-need-normalization-static-root-verification-r295.json", + "bytes": 3906, + "sha256": "6b015060efcf7a38773c5495f389fe987ec3eb31867e05721620aa32dd1ea9bd", + "stored_bytes": 3906, + "stored_sha256": "6b015060efcf7a38773c5495f389fe987ec3eb31867e05721620aa32dd1ea9bd", + "encoding": "identity" + }, + { + "source": "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/artifacts/original-completion-2026-10-02/p23-02-guest-need-normalization-local-r296.py", + "checkpoint": "docs/plans/original-completion-2026-10-02/checkpoints/guest-need-normalization-r295-r298/p23-02-guest-need-normalization-local-r296.py", + "bytes": 5232, + "sha256": "b9059debc1eb3aea9497d5abea646238edec5c73291d170737c29ce9026803eb", + "stored_bytes": 5232, + "stored_sha256": "b9059debc1eb3aea9497d5abea646238edec5c73291d170737c29ce9026803eb", + "encoding": "identity" + }, + { + "source": "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/artifacts/original-completion-2026-10-02/p23-02-guest-need-normalization-local-r296/input.json", + "checkpoint": "docs/plans/original-completion-2026-10-02/checkpoints/guest-need-normalization-r295-r298/p23-02-guest-need-normalization-local-r296/input.json.gz", + "bytes": 968619, + "sha256": "3bba39c1ff62fb59a7b844515072dc322728295c5d7680aef4055af5d10494ee", + "stored_bytes": 227315, + "stored_sha256": "fa574f2b3e464a5bc8929b6ddfe423fb0a80c796f1aef9502b2aa9c9a564754c", + "encoding": "gzip" + }, + { + "source": "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/artifacts/original-completion-2026-10-02/p23-02-guest-need-normalization-local-r296/local.gd", + "checkpoint": "docs/plans/original-completion-2026-10-02/checkpoints/guest-need-normalization-r295-r298/p23-02-guest-need-normalization-local-r296/local.gd", + "bytes": 7048, + "sha256": "bd9cb24e28bc97c0e8f906b6d2b0d60c14964fdaabbac3ac6561334e434ff97f", + "stored_bytes": 7048, + "stored_sha256": "bd9cb24e28bc97c0e8f906b6d2b0d60c14964fdaabbac3ac6561334e434ff97f", + "encoding": "identity" + }, + { + "source": "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/artifacts/original-completion-2026-10-02/p23-02-guest-need-normalization-local-r296/guarded.gd", + "checkpoint": "docs/plans/original-completion-2026-10-02/checkpoints/guest-need-normalization-r295-r298/p23-02-guest-need-normalization-local-r296/guarded.gd", + "bytes": 525, + "sha256": "5db64f0527aee0c74d0dff52103aefeec112824faa82137ab78d546979655658", + "stored_bytes": 525, + "stored_sha256": "5db64f0527aee0c74d0dff52103aefeec112824faa82137ab78d546979655658", + "encoding": "identity" + }, + { + "source": "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/artifacts/original-completion-2026-10-02/p23-02-guest-need-normalization-local-r296/custom_definition.gd", + "checkpoint": "docs/plans/original-completion-2026-10-02/checkpoints/guest-need-normalization-r295-r298/p23-02-guest-need-normalization-local-r296/custom_definition.gd", + "bytes": 281, + "sha256": "d29c18196c89452a0ba6db8ec17d0ac2e75be2b6092533c734288230bc44f0c7", + "stored_bytes": 281, + "stored_sha256": "d29c18196c89452a0ba6db8ec17d0ac2e75be2b6092533c734288230bc44f0c7", + "encoding": "identity" + }, + { + "source": "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/artifacts/original-completion-2026-10-02/p23-02-guest-need-normalization-local-r296/original_guest_service.gd", + "checkpoint": "docs/plans/original-completion-2026-10-02/checkpoints/guest-need-normalization-r295-r298/p23-02-guest-need-normalization-local-r296/original_guest_service.gd", + "bytes": 61426, + "sha256": "a70362ac6326266573a873319146d50a540829b751ad4c07746937fef85f5193", + "stored_bytes": 61426, + "stored_sha256": "a70362ac6326266573a873319146d50a540829b751ad4c07746937fef85f5193", + "encoding": "identity" + }, + { + "source": "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/artifacts/original-completion-2026-10-02/p23-02-guest-need-normalization-local-r296/execution.json", + "checkpoint": "docs/plans/original-completion-2026-10-02/checkpoints/guest-need-normalization-r295-r298/p23-02-guest-need-normalization-local-r296/execution.json", + "bytes": 10788, + "sha256": "d01a859ce75a18084cbec7a081c906e267d1e8c7a42bce55e2539dd3a7ede8c3", + "stored_bytes": 10788, + "stored_sha256": "d01a859ce75a18084cbec7a081c906e267d1e8c7a42bce55e2539dd3a7ede8c3", + "encoding": "identity" + }, + { + "source": "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/artifacts/original-completion-2026-10-02/p23-02-guest-need-normalization-local-r296/actual-root-verification.json", + "checkpoint": "docs/plans/original-completion-2026-10-02/checkpoints/guest-need-normalization-r295-r298/p23-02-guest-need-normalization-local-r296/actual-root-verification.json", + "bytes": 10454, + "sha256": "6d9dc235ac63d6629e4375e5b75516f522f47d4d491b40cda790da14fa65f4fe", + "stored_bytes": 10454, + "stored_sha256": "6d9dc235ac63d6629e4375e5b75516f522f47d4d491b40cda790da14fa65f4fe", + "encoding": "identity" + }, + { + "source": "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/artifacts/original-completion-2026-10-02/p23-02-guest-need-normalization-local-preexecution-independent-review-r296.json", + "checkpoint": "docs/plans/original-completion-2026-10-02/checkpoints/guest-need-normalization-r295-r298/p23-02-guest-need-normalization-local-preexecution-independent-review-r296.json", + "bytes": 11638, + "sha256": "dc2662c7f9b92de16e9d4d97daf86d3a2eb537f2587cc76946d77e7021907071", + "stored_bytes": 11638, + "stored_sha256": "dc2662c7f9b92de16e9d4d97daf86d3a2eb537f2587cc76946d77e7021907071", + "encoding": "identity" + }, + { + "source": "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/artifacts/original-completion-2026-10-02/p23-02-guest-need-normalization-local-actual-independent-review-r296.json", + "checkpoint": "docs/plans/original-completion-2026-10-02/checkpoints/guest-need-normalization-r295-r298/p23-02-guest-need-normalization-local-actual-independent-review-r296.json", + "bytes": 19007, + "sha256": "ce06bfe9156ed8e1c9ec53519d6cc52b26c963ca0e8b4a263ff70d1a1abf9a23", + "stored_bytes": 19007, + "stored_sha256": "ce06bfe9156ed8e1c9ec53519d6cc52b26c963ca0e8b4a263ff70d1a1abf9a23", + "encoding": "identity" + }, + { + "source": "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/artifacts/original-completion-2026-10-02/p23-02-guest-need-normalization-candidate-r298/input.json", + "checkpoint": "docs/plans/original-completion-2026-10-02/checkpoints/guest-need-normalization-r295-r298/p23-02-guest-need-normalization-candidate-r298/input.json", + "bytes": 11472, + "sha256": "3e2eaa147e365a74145224d8995bade5b44528d59b5508dc159458555081ed99", + "stored_bytes": 11472, + "stored_sha256": "3e2eaa147e365a74145224d8995bade5b44528d59b5508dc159458555081ed99", + "encoding": "identity" + }, + { + "source": "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/artifacts/original-completion-2026-10-02/p23-02-guest-need-normalization-candidate-r298/source/guest_service.gd", + "checkpoint": "docs/plans/original-completion-2026-10-02/checkpoints/guest-need-normalization-r295-r298/p23-02-guest-need-normalization-candidate-r298/source/guest_service.gd", + "bytes": 63639, + "sha256": "5895d5da6488c767685b109ef01f9e2b57f41272d49cc9e3c415865769adc11d", + "stored_bytes": 63639, + "stored_sha256": "5895d5da6488c767685b109ef01f9e2b57f41272d49cc9e3c415865769adc11d", + "encoding": "identity" + }, + { + "source": "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/artifacts/original-completion-2026-10-02/p23-02-guest-need-normalization-static-independent-review-r298.json", + "checkpoint": "docs/plans/original-completion-2026-10-02/checkpoints/guest-need-normalization-r295-r298/p23-02-guest-need-normalization-static-independent-review-r298.json", + "bytes": 17223, + "sha256": "940dd5f51b55942f07ef7ad091ec557af4d0d7bb07d48c08b6a98bde570f32b2", + "stored_bytes": 17223, + "stored_sha256": "940dd5f51b55942f07ef7ad091ec557af4d0d7bb07d48c08b6a98bde570f32b2", + "encoding": "identity" + }, + { + "source": "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/artifacts/original-completion-2026-10-02/p23-02-guest-need-normalization-static-root-verification-r298.json", + "checkpoint": "docs/plans/original-completion-2026-10-02/checkpoints/guest-need-normalization-r295-r298/p23-02-guest-need-normalization-static-root-verification-r298.json", + "bytes": 9067, + "sha256": "e6f83f624358270199fb6a0d3236db0e20da614443514cba04d66995a671de42", + "stored_bytes": 9067, + "stored_sha256": "e6f83f624358270199fb6a0d3236db0e20da614443514cba04d66995a671de42", + "encoding": "identity" + }, + { + "source": "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/artifacts/original-completion-2026-10-02/p23-02-guest-need-normalization-local-r296/measurement/report.json", + "checkpoint": "docs/plans/original-completion-2026-10-02/checkpoints/guest-need-normalization-r295-r298/p23-02-guest-need-normalization-local-r296/measurement/report.json", + "bytes": 1128, + "sha256": "fcf08d05ca7109beed8e0c4fc75f422baab72a515467e09debf58c812fb0689d", + "stored_bytes": 1128, + "stored_sha256": "fcf08d05ca7109beed8e0c4fc75f422baab72a515467e09debf58c812fb0689d", + "encoding": "identity" + }, + { + "source": "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/artifacts/original-completion-2026-10-02/p23-02-guest-need-normalization-local-r296/measurement/evaluation-fixtures.varbytes.zst", + "checkpoint": "docs/plans/original-completion-2026-10-02/checkpoints/guest-need-normalization-r295-r298/p23-02-guest-need-normalization-local-r296/measurement/evaluation-fixtures.varbytes.zst", + "bytes": 3540, + "sha256": "c21dad1a71f30a2c8d4cab558bbfb4543f2daca4487b1fbdbcbb1b1a97b9143d", + "stored_bytes": 3540, + "stored_sha256": "c21dad1a71f30a2c8d4cab558bbfb4543f2daca4487b1fbdbcbb1b1a97b9143d", + "encoding": "identity" + } + ] +} diff --git a/docs/plans/original-completion-2026-10-02/checkpoints/guest-need-normalization-r295-r298/p23-02-guest-need-normalization-candidate-r295/input.json b/docs/plans/original-completion-2026-10-02/checkpoints/guest-need-normalization-r295-r298/p23-02-guest-need-normalization-candidate-r295/input.json new file mode 100644 index 0000000..90d69c2 --- /dev/null +++ b/docs/plans/original-completion-2026-10-02/checkpoints/guest-need-normalization-r295-r298/p23-02-guest-need-normalization-candidate-r295/input.json @@ -0,0 +1,59 @@ +{ + "scope": "Source-only guarded reuse of one normalized Need map within exact builtin evaluate_candidate; no production/stage edit or execution", + "parent": "r274/library275", + "pins": { + "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/artifacts/original-completion-2026-10-02/p23-02-native-reconstruction-local-r209/stage/scripts/guest/guest_service.gd": { + "path": "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/artifacts/original-completion-2026-10-02/p23-02-native-reconstruction-local-r209/stage/scripts/guest/guest_service.gd", + "bytes": 61414, + "sha256": "56880410e9a00816f9a95d30821982955532de8dd976980934e12016a9f2b21e" + }, + "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/artifacts/original-completion-2026-10-02/p23-02-native-pure-tag-projection-candidate-r274/input.json": { + "path": "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/artifacts/original-completion-2026-10-02/p23-02-native-pure-tag-projection-candidate-r274/input.json", + "bytes": 4511, + "sha256": "6bd46c589bc57ab5afad1d7195f7293ec450aafeed055e394a3a880744666ca3" + }, + "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/artifacts/original-completion-2026-10-02/p23-02-native-pure-tag-projection-candidate-r274/source/tree_numeric_core.cpp": { + "path": "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/artifacts/original-completion-2026-10-02/p23-02-native-pure-tag-projection-candidate-r274/source/tree_numeric_core.cpp", + "bytes": 54002, + "sha256": "ce20a90adb8a410dc1b6cbf607b7bfa8bf21eaef158c5c6bfc7a9bbcc703d2b2" + }, + "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/artifacts/original-completion-2026-10-02/p23-02-native-integer-window-codec-impact-whole-r293-r04/execution.json": { + "path": "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/artifacts/original-completion-2026-10-02/p23-02-native-integer-window-codec-impact-whole-r293-r04/execution.json", + "bytes": 33451, + "sha256": "44d6c26b797a99e7729d1eb77e725330b2b95bd643876eb41cd11237f2488848" + }, + "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/artifacts/original-completion-2026-10-02/p23-02-current-residual-diagnostic-actual-independent-review-r238.json": { + "path": "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/artifacts/original-completion-2026-10-02/p23-02-current-residual-diagnostic-actual-independent-review-r238.json", + "bytes": 36734, + "sha256": "e41634755f116006c91ad251b48eb8b71286a53a6a687d6f371366f3140cc66d" + }, + "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/scripts/guest/guest_definition.gd": { + "path": "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/scripts/guest/guest_definition.gd", + "bytes": 9374, + "sha256": "082aa3f33b9cb17daf10a2cf8a1dece7b79dd80c2ae21e1a424d1b2ceb52b918" + }, + "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/scripts/guest/guest_runtime.gd": { + "path": "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/scripts/guest/guest_runtime.gd", + "bytes": 14267, + "sha256": "57ba99e5d977b5302987f4359a19056f1ab09c8fb7e4762ee8d79cbb9316741a" + }, + "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/artifacts/original-completion-2026-10-02/p23-02-guest-need-normalization-candidate-r295/source/guest_service.gd": { + "path": "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/artifacts/original-completion-2026-10-02/p23-02-guest-need-normalization-candidate-r295/source/guest_service.gd", + "bytes": 64563, + "sha256": "2794f20630168d902e6a7fb55a0b5aacfbbcfcccd51d336f0724f03ee35ea249" + } + }, + "original_evaluation_validation_and_order_preserved": true, + "weights_thresholds_prices_rng_tick_save_changed": false, + "compiler_runs": 0, + "engine_runs": 0, + "candidate_adopted": false, + "unit_accepted": false, + "required_next": [ + "independent static review", + "parse and exact builtin/fallback helper/callback boundary equivalence", + "full raw ranked candidate and guest decision/Runtime equality", + "impact and whole same-final-version validation", + "performance gain qualification before adoption" + ] +} diff --git a/docs/plans/original-completion-2026-10-02/checkpoints/guest-need-normalization-r295-r298/p23-02-guest-need-normalization-candidate-r295/source/guest_service.gd b/docs/plans/original-completion-2026-10-02/checkpoints/guest-need-normalization-r295-r298/p23-02-guest-need-normalization-candidate-r295/source/guest_service.gd new file mode 100644 index 0000000..b87f284 --- /dev/null +++ b/docs/plans/original-completion-2026-10-02/checkpoints/guest-need-normalization-r295-r298/p23-02-guest-need-normalization-candidate-r295/source/guest_service.gd @@ -0,0 +1,1752 @@ +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.") + # Shared only for exact builtin pure evaluation. Custom providers and + # conversions retain the original helper calls and their observation order. + var shared_need_map: Variant = null + if _can_share_candidate_need_map(guest, definition, candidate): + shared_need_map = _normalized_partial_need_map(_value(candidate, &"need_relief", {})) + var need_relief := _need_relief(guest, candidate) if shared_need_map == null else _need_relief_from_normalized(guest, shared_need_map) + 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) if shared_need_map == null else _candidate_urgency_from_normalized(guest, shared_need_map))) + 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) if shared_need_map == null else _primary_need_priority_from_normalized(guest, shared_need_map) + 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) + 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} + + +# No retained cache and no observation is moved across a user callback. All +# expressions evaluated between the three original helper positions are pure +# builtin conversions under this guard. Extra candidate fields stay untouched. +func _can_share_candidate_need_map(guest: GuestRuntime, definition: GuestDefinition, candidate: Dictionary) -> bool: + if get_script() != load("res://scripts/guest/guest_service.gd") \ + or guest.get_script() != GuestRuntimeType \ + or definition.get_script() != GuestDefinitionType: + return false + if not _value(candidate, &"need_relief", null) is Dictionary: + return false + for need: StringName in NEED_IDS: + if not guest.needs.get(need, null) is int: + return false + for field: StringName in [&"preference", &"expected_quality", &"quality", &"novelty", &"group_fit", &"price_burden", &"travel_minutes", &"wait_minutes", &"expected_wait", &"risk", &"risk_level", &"risk_sensitivity", &"repetition_penalty", &"urgency", &"estimated_total_gold", &"price_gold", &"quoted_price"]: + if _has_value(candidate, field) and not _value(candidate, field, null) is int: + return false + for field: StringName in [&"facility_id", &"instance_id"]: + if _has_value(candidate, field): + var value: Variant = _value(candidate, field, null) + if not value is String and not value is StringName: + return false + for field: StringName in [&"facility_tags", &"tags"]: + if _has_value(candidate, field): + var value: Variant = _value(candidate, field, null) + if not value is Array: + return false + for tag: Variant in value: + if not tag is String and not tag is StringName: + return false + for value: Variant in definition.preference_weights.values(): + if not value is int: + return false + return true + + +func _need_relief_from_normalized(guest: GuestRuntime, normalized: Dictionary) -> int: + 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_from_normalized(guest: GuestRuntime, normalized: Dictionary) -> int: + 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_from_normalized(guest: GuestRuntime, normalized: Dictionary) -> int: + 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 diff --git a/docs/plans/original-completion-2026-10-02/checkpoints/guest-need-normalization-r295-r298/p23-02-guest-need-normalization-candidate-r295/structural-check.json b/docs/plans/original-completion-2026-10-02/checkpoints/guest-need-normalization-r295-r298/p23-02-guest-need-normalization-candidate-r295/structural-check.json new file mode 100644 index 0000000..aa23677 --- /dev/null +++ b/docs/plans/original-completion-2026-10-02/checkpoints/guest-need-normalization-r295-r298/p23-02-guest-need-normalization-candidate-r295/structural-check.json @@ -0,0 +1,8 @@ +{ + "inverse_original_byteexact": true, + "three_pure_loop_bodies_byteexact": true, + "parser_executed": false, + "runtime_equivalence_accepted": false, + "production_stage_edits": 0, + "unit_accepted": false +} diff --git a/docs/plans/original-completion-2026-10-02/checkpoints/guest-need-normalization-r295-r298/p23-02-guest-need-normalization-candidate-r298/input.json b/docs/plans/original-completion-2026-10-02/checkpoints/guest-need-normalization-r295-r298/p23-02-guest-need-normalization-candidate-r298/input.json new file mode 100644 index 0000000..bed87b9 --- /dev/null +++ b/docs/plans/original-completion-2026-10-02/checkpoints/guest-need-normalization-r295-r298/p23-02-guest-need-normalization-candidate-r298/input.json @@ -0,0 +1,133 @@ +{ + "pins": { + "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/artifacts/original-completion-2026-10-02/p23-02-guest-need-normalization-candidate-r295/source/guest_service.gd": { + "path": "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/artifacts/original-completion-2026-10-02/p23-02-guest-need-normalization-candidate-r295/source/guest_service.gd", + "bytes": 64563, + "sha256": "2794f20630168d902e6a7fb55a0b5aacfbbcfcccd51d336f0724f03ee35ea249" + }, + "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/artifacts/original-completion-2026-10-02/p23-02-guest-need-normalization-candidate-r298/source/guest_service.gd": { + "path": "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/artifacts/original-completion-2026-10-02/p23-02-guest-need-normalization-candidate-r298/source/guest_service.gd", + "bytes": 63639, + "sha256": "5895d5da6488c767685b109ef01f9e2b57f41272d49cc9e3c415865769adc11d" + }, + "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/artifacts/original-completion-2026-10-02/p23-02-guest-need-normalization-static-independent-review-r295.json": { + "path": "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/artifacts/original-completion-2026-10-02/p23-02-guest-need-normalization-static-independent-review-r295.json", + "bytes": 9244, + "sha256": "0954f512e6065c26ffc7d06c087fe973379a48c9cebef6752f19ec52d24f59d8" + }, + "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/artifacts/original-completion-2026-10-02/p23-02-guest-need-normalization-local-actual-independent-review-r296.json": { + "path": "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/artifacts/original-completion-2026-10-02/p23-02-guest-need-normalization-local-actual-independent-review-r296.json", + "bytes": 19007, + "sha256": "ce06bfe9156ed8e1c9ec53519d6cc52b26c963ca0e8b4a263ff70d1a1abf9a23" + }, + "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/artifacts/original-completion-2026-10-02/p23-02-guest-need-normalization-local-r296/actual-root-verification.json": { + "path": "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/artifacts/original-completion-2026-10-02/p23-02-guest-need-normalization-local-r296/actual-root-verification.json", + "bytes": 10454, + "sha256": "6d9dc235ac63d6629e4375e5b75516f522f47d4d491b40cda790da14fa65f4fe" + }, + "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/artifacts/original-completion-2026-10-02/p23-02-guest-need-normalization-local-r296/execution.json": { + "path": "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/artifacts/original-completion-2026-10-02/p23-02-guest-need-normalization-local-r296/execution.json", + "bytes": 10788, + "sha256": "d01a859ce75a18084cbec7a081c906e267d1e8c7a42bce55e2539dd3a7ede8c3" + }, + "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/artifacts/original-completion-2026-10-02/p23-02-native-pure-tag-projection-candidate-r274/input.json": { + "path": "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/artifacts/original-completion-2026-10-02/p23-02-native-pure-tag-projection-candidate-r274/input.json", + "bytes": 4511, + "sha256": "6bd46c589bc57ab5afad1d7195f7293ec450aafeed055e394a3a880744666ca3" + }, + "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/scripts/guest/guest_definition.gd": { + "path": "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/scripts/guest/guest_definition.gd", + "bytes": 9374, + "sha256": "082aa3f33b9cb17daf10a2cf8a1dece7b79dd80c2ae21e1a424d1b2ceb52b918" + }, + "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/scripts/guest/guest_runtime.gd": { + "path": "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/scripts/guest/guest_runtime.gd", + "bytes": 14267, + "sha256": "57ba99e5d977b5302987f4359a19056f1ab09c8fb7e4762ee8d79cbb9316741a" + }, + "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/artifacts/original-completion-2026-10-02/p23-02-guest-need-conversion-source-r297/variant.cpp": { + "path": "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/artifacts/original-completion-2026-10-02/p23-02-guest-need-conversion-source-r297/variant.cpp", + "bytes": 94629, + "sha256": "4af0a620aa59d2376d1b35ab201f26edd352f056c908b970890ad9a3449bb5c0" + }, + "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/artifacts/original-completion-2026-10-02/p23-02-guest-need-conversion-source-r297/variant.cpp.source.json": { + "path": "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/artifacts/original-completion-2026-10-02/p23-02-guest-need-conversion-source-r297/variant.cpp.source.json", + "bytes": 278, + "sha256": "dfce2e51d892ac01b198055551cc899c06aa1b9962f20bca687dbb1c31e2bd9d" + }, + "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/artifacts/original-completion-2026-10-02/p23-02-guest-need-conversion-source-r297/variant.h": { + "path": "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/artifacts/original-completion-2026-10-02/p23-02-guest-need-conversion-source-r297/variant.h", + "bytes": 34956, + "sha256": "3d7d7c8bdc02d4ac73d06b16f487a9ba0f9a188c854ab0befc57b98f807523b2" + }, + "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/artifacts/original-completion-2026-10-02/p23-02-guest-need-conversion-source-r297/variant.h.source.json": { + "path": "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/artifacts/original-completion-2026-10-02/p23-02-guest-need-conversion-source-r297/variant.h.source.json", + "bytes": 276, + "sha256": "42d0b1ef9b9f5329029e4250cc97533bd800ccfea2fa18d6586c21f7973009af" + }, + "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/artifacts/original-completion-2026-10-02/p23-02-guest-need-conversion-source-r297/variant_construct.cpp": { + "path": "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/artifacts/original-completion-2026-10-02/p23-02-guest-need-conversion-source-r297/variant_construct.cpp", + "bytes": 19030, + "sha256": "51e8ebbbffb9c65959aa4c86f7dd1a21ed121ef82706996893743de077c82645" + }, + "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/artifacts/original-completion-2026-10-02/p23-02-guest-need-conversion-source-r297/variant_construct.cpp.source.json": { + "path": "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/artifacts/original-completion-2026-10-02/p23-02-guest-need-conversion-source-r297/variant_construct.cpp.source.json", + "bytes": 288, + "sha256": "7aed774b87af26bf775abeaa63a22df93cb1552398cc52ca3230800b1abdd555" + }, + "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/artifacts/original-completion-2026-10-02/p23-02-guest-need-conversion-source-r297/variant_construct.h": { + "path": "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/artifacts/original-completion-2026-10-02/p23-02-guest-need-conversion-source-r297/variant_construct.h", + "bytes": 25252, + "sha256": "e6aecba36f8ff90af382427fb6be8b69e161583cffba7751b2c2ffdfa5e632c9" + }, + "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/artifacts/original-completion-2026-10-02/p23-02-guest-need-conversion-source-r297/variant_construct.h.source.json": { + "path": "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/artifacts/original-completion-2026-10-02/p23-02-guest-need-conversion-source-r297/variant_construct.h.source.json", + "bytes": 286, + "sha256": "e7c85dca32c621dd842706fe0851e74a63e288848b4055ca895d657059bcc312" + }, + "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/artifacts/original-completion-2026-10-02/p23-02-guest-need-conversion-source-r297/dictionary.cpp": { + "path": "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/artifacts/original-completion-2026-10-02/p23-02-guest-need-conversion-source-r297/dictionary.cpp", + "bytes": 27721, + "sha256": "13eb030c2e59ac0e7060852949bf5b7a62624e31474b781d89e72515410daa7d" + }, + "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/artifacts/original-completion-2026-10-02/p23-02-guest-need-conversion-source-r297/dictionary.cpp.source.json": { + "path": "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/artifacts/original-completion-2026-10-02/p23-02-guest-need-conversion-source-r297/dictionary.cpp.source.json", + "bytes": 281, + "sha256": "032aaf8bb193500745770c940088a2223c1f3f6bb9f8edf6300693bb3e26fc7b" + }, + "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/artifacts/original-completion-2026-10-02/p23-02-guest-need-conversion-source-r297/variant_setget.cpp": { + "path": "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/artifacts/original-completion-2026-10-02/p23-02-guest-need-conversion-source-r297/variant_setget.cpp", + "bytes": 62857, + "sha256": "e3f73e1df096bedd4856e29227b302f879d0520c66718467338556b589f685c9" + }, + "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/artifacts/original-completion-2026-10-02/p23-02-guest-need-conversion-source-r297/variant_setget.cpp.source.json": { + "path": "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/artifacts/original-completion-2026-10-02/p23-02-guest-need-conversion-source-r297/variant_setget.cpp.source.json", + "bytes": 276, + "sha256": "f3002c603869280c99e5efc5f1e923258726c48c404571af34ba6f3bdc009cd2" + } + }, + "scope": "Only replace r295 sharing guard, retains original pure computation/return order. Exact builtin service/guest/definition guards unchanged; no retained cache.", + "parent": "r274/library275 native plus unadopted295 GDScript refinement", + "removed_guards_basis": [ + "Variant::_to_int and int constructors use scalar conversions; no Object/custom callback invoked", + "StringName constructor registrations and strict conversion selection do not invoke custom Object stringify", + "Dictionary::duplicate(true) uses RESOURCE_DEEP_DUPLICATE_NONE; nested Objects/Resources are returned unchanged by recursive_duplicate", + "Exact service/guest/definition checks retain custom properties/preference/helper dynamic fallback", + "Original Need map validation remains before sharing; shared map contains only validated INT scalar values" + ], + "r296_scope": "Accepted local231+2/1619 only, initial guard single-Need 2000calls 28461->41815us not performance gain or representative tick. Static298 and modified local/whole verification required; old result does not validate expanded guard.", + "inverse_parent_byteexact": true, + "source_edits": 0, + "stage_edits": 0, + "engine_runs": 0, + "compiler_runs": 0, + "unit_accepted": false, + "candidate_adopted": false, + "unverified": [ + "independent static callback/conversion/duplication proof", + "actual parse and expanded-guard local equivalence", + "new same-template helper observations including full-six Need map without extrapolating budget", + "original impact and whole same-version equality", + "full final regression and all17 acceptance conditions" + ] +} diff --git a/docs/plans/original-completion-2026-10-02/checkpoints/guest-need-normalization-r295-r298/p23-02-guest-need-normalization-candidate-r298/source/guest_service.gd b/docs/plans/original-completion-2026-10-02/checkpoints/guest-need-normalization-r295-r298/p23-02-guest-need-normalization-candidate-r298/source/guest_service.gd new file mode 100644 index 0000000..74f4ef5 --- /dev/null +++ b/docs/plans/original-completion-2026-10-02/checkpoints/guest-need-normalization-r295-r298/p23-02-guest-need-normalization-candidate-r298/source/guest_service.gd @@ -0,0 +1,1730 @@ +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.") + # Shared only for exact builtin pure evaluation. Custom providers and + # conversions retain the original helper calls and their observation order. + var shared_need_map: Variant = null + if _can_share_candidate_need_map(guest, definition, candidate): + shared_need_map = _normalized_partial_need_map(_value(candidate, &"need_relief", {})) + var need_relief := _need_relief(guest, candidate) if shared_need_map == null else _need_relief_from_normalized(guest, shared_need_map) + 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) if shared_need_map == null else _candidate_urgency_from_normalized(guest, shared_need_map))) + 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) if shared_need_map == null else _primary_need_priority_from_normalized(guest, shared_need_map) + 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) + 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} + + +# No retained cache and no observation is moved across a user callback. All +# expressions evaluated between the three original helper positions are pure +# builtin conversions under this guard. Extra candidate fields stay untouched. +func _can_share_candidate_need_map(guest: GuestRuntime, definition: GuestDefinition, candidate: Dictionary) -> bool: + # Same-engine constructor/duplicate source shows no user callback for these + # builtin conversions or nested Dictionary.duplicate(true). Identity guards + # preserve custom service, guest properties, and preference implementations. + return get_script() == load("res://scripts/guest/guest_service.gd") \ + and guest.get_script() == GuestRuntimeType \ + and definition.get_script() == GuestDefinitionType \ + and _value(candidate, &"need_relief", null) is Dictionary + + +func _need_relief_from_normalized(guest: GuestRuntime, normalized: Dictionary) -> int: + 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_from_normalized(guest: GuestRuntime, normalized: Dictionary) -> int: + 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_from_normalized(guest: GuestRuntime, normalized: Dictionary) -> int: + 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 diff --git a/docs/plans/original-completion-2026-10-02/checkpoints/guest-need-normalization-r295-r298/p23-02-guest-need-normalization-local-actual-independent-review-r296.json b/docs/plans/original-completion-2026-10-02/checkpoints/guest-need-normalization-r295-r298/p23-02-guest-need-normalization-local-actual-independent-review-r296.json new file mode 100644 index 0000000..5591d0f --- /dev/null +++ b/docs/plans/original-completion-2026-10-02/checkpoints/guest-need-normalization-r295-r298/p23-02-guest-need-normalization-local-actual-independent-review-r296.json @@ -0,0 +1,249 @@ +{ + "review_id": "p23-02-guest-need-normalization-local-actual-independent-review-r296", + "finite_local_equivalence_accepted": true, + "finite_callback_fallback_equivalence_accepted": true, + "finite_exit_observation_accepted": true, + "blockers": [], + "evidence_sha256": { + "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/artifacts/original-completion-2026-10-02/p23-02-native-reconstruction-local-r209/stage/scripts/guest/guest_service.gd": { + "path": "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/artifacts/original-completion-2026-10-02/p23-02-native-reconstruction-local-r209/stage/scripts/guest/guest_service.gd", + "bytes": 61414, + "sha256": "56880410e9a00816f9a95d30821982955532de8dd976980934e12016a9f2b21e" + }, + "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/artifacts/original-completion-2026-10-02/p23-02-guest-need-normalization-candidate-r295/source/guest_service.gd": { + "path": "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/artifacts/original-completion-2026-10-02/p23-02-guest-need-normalization-candidate-r295/source/guest_service.gd", + "bytes": 64563, + "sha256": "2794f20630168d902e6a7fb55a0b5aacfbbcfcccd51d336f0724f03ee35ea249" + }, + "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/artifacts/original-completion-2026-10-02/p23-02-guest-need-normalization-local-r296/original_guest_service.gd": { + "path": "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/artifacts/original-completion-2026-10-02/p23-02-guest-need-normalization-local-r296/original_guest_service.gd", + "bytes": 61426, + "sha256": "a70362ac6326266573a873319146d50a540829b751ad4c07746937fef85f5193" + }, + "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/artifacts/original-completion-2026-10-02/p23-02-guest-need-normalization-local-r296/custom_definition.gd": { + "path": "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/artifacts/original-completion-2026-10-02/p23-02-guest-need-normalization-local-r296/custom_definition.gd", + "bytes": 281, + "sha256": "d29c18196c89452a0ba6db8ec17d0ac2e75be2b6092533c734288230bc44f0c7" + }, + "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/artifacts/original-completion-2026-10-02/p23-02-guest-need-normalization-local-r296/local.gd": { + "path": "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/artifacts/original-completion-2026-10-02/p23-02-guest-need-normalization-local-r296/local.gd", + "bytes": 7048, + "sha256": "bd9cb24e28bc97c0e8f906b6d2b0d60c14964fdaabbac3ac6561334e434ff97f" + }, + "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/artifacts/original-completion-2026-10-02/p23-02-guest-need-normalization-local-r296/guarded.gd": { + "path": "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/artifacts/original-completion-2026-10-02/p23-02-guest-need-normalization-local-r296/guarded.gd", + "bytes": 525, + "sha256": "5db64f0527aee0c74d0dff52103aefeec112824faa82137ab78d546979655658" + }, + "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/tools/run_all_tests.py": { + "path": "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/tools/run_all_tests.py", + "bytes": 26296, + "sha256": "320ca668732b4ff72fc4449998af2327d4e5c527b06896740806aaf0487282b4" + }, + "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/tools/regression/owned_outer.py": { + "path": "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/tools/regression/owned_outer.py", + "bytes": 4177, + "sha256": "c2e3483b76db1a3ff940f23f88ea0083fc78736e874ebcdb385d766dde8866e7" + }, + "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/artifacts/original-completion-2026-10-02/p23-02-guest-need-normalization-static-independent-review-r295.json": { + "path": "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/artifacts/original-completion-2026-10-02/p23-02-guest-need-normalization-static-independent-review-r295.json", + "bytes": 9244, + "sha256": "0954f512e6065c26ffc7d06c087fe973379a48c9cebef6752f19ec52d24f59d8" + }, + "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/artifacts/original-completion-2026-10-02/p23-02-guest-need-normalization-candidate-r295/input.json": { + "path": "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/artifacts/original-completion-2026-10-02/p23-02-guest-need-normalization-candidate-r295/input.json", + "bytes": 4366, + "sha256": "f592b9895d9253be9e7f829cd88888fe166e77e00959570f7d4a7f0c3c169489" + }, + "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/artifacts/original-completion-2026-10-02/p23-02-build-preview-monitor-compressed-r06.gd": { + "path": "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/artifacts/original-completion-2026-10-02/p23-02-build-preview-monitor-compressed-r06.gd", + "bytes": 17582, + "sha256": "92d5e1956f4e613e12555d756de4f71fef03678cde159ac68fac3188ce3f336e" + }, + "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/artifacts/original-completion-2026-10-02/p23-02-native-cache-public-mutations-r144/profile_guard.gd": { + "path": "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/artifacts/original-completion-2026-10-02/p23-02-native-cache-public-mutations-r144/profile_guard.gd", + "bytes": 2276, + "sha256": "597696c705cc8f0e79a35265ca3ef8df55f7fc3c4e0e5a2ce9a4e20e29e29a6f" + }, + "/Applications/Godot.app/Contents/MacOS/Godot": { + "path": "/Applications/Godot.app/Contents/MacOS/Godot", + "bytes": 351312912, + "sha256": "c7cccbf8fb143e34e02fd6521e09be2c2b974f0d5db080b19071c9c570718ccf" + }, + "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/artifacts/original-completion-2026-10-02/p23-02-guest-need-normalization-local-r296.py": { + "path": "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/artifacts/original-completion-2026-10-02/p23-02-guest-need-normalization-local-r296.py", + "bytes": 5232, + "sha256": "b9059debc1eb3aea9497d5abea646238edec5c73291d170737c29ce9026803eb" + }, + "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/artifacts/original-completion-2026-10-02/p23-02-guest-need-normalization-local-preexecution-independent-review-r296.json": { + "path": "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/artifacts/original-completion-2026-10-02/p23-02-guest-need-normalization-local-preexecution-independent-review-r296.json", + "bytes": 11638, + "sha256": "dc2662c7f9b92de16e9d4d97daf86d3a2eb537f2587cc76946d77e7021907071" + }, + "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/artifacts/original-completion-2026-10-02/p23-02-guest-need-normalization-local-r296/input.json": { + "path": "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/artifacts/original-completion-2026-10-02/p23-02-guest-need-normalization-local-r296/input.json", + "bytes": 968619, + "sha256": "3bba39c1ff62fb59a7b844515072dc322728295c5d7680aef4055af5d10494ee" + }, + "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/artifacts/original-completion-2026-10-02/p23-02-guest-need-normalization-local-r296/preexecution-root-verification.json": { + "path": "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/artifacts/original-completion-2026-10-02/p23-02-guest-need-normalization-local-r296/preexecution-root-verification.json", + "bytes": 5516, + "sha256": "9877abcde2f6fa06aeaa81a8a8d6b8a5b06bb7db8012bbe321f9cf8e106a8f12" + }, + "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/artifacts/original-completion-2026-10-02/p23-02-guest-need-normalization-local-r296/execution.json": { + "path": "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/artifacts/original-completion-2026-10-02/p23-02-guest-need-normalization-local-r296/execution.json", + "bytes": 10788, + "sha256": "d01a859ce75a18084cbec7a081c906e267d1e8c7a42bce55e2539dd3a7ede8c3" + }, + "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/artifacts/original-completion-2026-10-02/p23-02-guest-need-normalization-local-r296/profile.json": { + "path": "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/artifacts/original-completion-2026-10-02/p23-02-guest-need-normalization-local-r296/profile.json", + "bytes": 907, + "sha256": "f52b9cc1f3642a8b150c97a40f2cf3cd9a80c793cba5a6693811369294a50a64" + }, + "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/artifacts/original-completion-2026-10-02/p23-02-guest-need-normalization-local-r296/phase.json": { + "path": "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/artifacts/original-completion-2026-10-02/p23-02-guest-need-normalization-local-r296/phase.json", + "bytes": 8097, + "sha256": "5ea3de68f87dda0fa0793e2d87428990645d61fa70cba14e44b8fb4d73c6f3b9" + }, + "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/artifacts/original-completion-2026-10-02/p23-02-guest-need-normalization-local-r296/measurement/evaluation-fixtures.varbytes.zst": { + "path": "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/artifacts/original-completion-2026-10-02/p23-02-guest-need-normalization-local-r296/measurement/evaluation-fixtures.varbytes.zst", + "bytes": 3540, + "sha256": "c21dad1a71f30a2c8d4cab558bbfb4543f2daca4487b1fbdbcbb1b1a97b9143d" + }, + "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/artifacts/original-completion-2026-10-02/p23-02-guest-need-normalization-local-r296/measurement/report.json": { + "path": "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/artifacts/original-completion-2026-10-02/p23-02-guest-need-normalization-local-r296/measurement/report.json", + "bytes": 1128, + "sha256": "fcf08d05ca7109beed8e0c4fc75f422baab72a515467e09debf58c812fb0689d" + }, + "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/artifacts/original-completion-2026-10-02/p23-02-guest-need-normalization-local-r296/engine/stderr.log": { + "path": "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/artifacts/original-completion-2026-10-02/p23-02-guest-need-normalization-local-r296/engine/stderr.log", + "bytes": 0, + "sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + }, + "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/artifacts/original-completion-2026-10-02/p23-02-guest-need-normalization-local-r296/engine/ended.json": { + "path": "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/artifacts/original-completion-2026-10-02/p23-02-guest-need-normalization-local-r296/engine/ended.json", + "bytes": 1772, + "sha256": "63f84a8e05af97d63b2a37ed47e1075263b1b8fe51190ff14f934f24149c3919" + }, + "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/artifacts/original-completion-2026-10-02/p23-02-guest-need-normalization-local-r296/engine/adapter-owned-cleanup.json": { + "path": "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/artifacts/original-completion-2026-10-02/p23-02-guest-need-normalization-local-r296/engine/adapter-owned-cleanup.json", + "bytes": 351, + "sha256": "c1f3d289b362cc61b9be169a3994be27df39f35e6221881b4a9678ce3c72ba54" + }, + "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/artifacts/original-completion-2026-10-02/p23-02-guest-need-normalization-local-r296/engine/engine-native.log": { + "path": "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/artifacts/original-completion-2026-10-02/p23-02-guest-need-normalization-local-r296/engine/engine-native.log", + "bytes": 7725, + "sha256": "067ea4cc50ac8e60720e8e58d4e83729afd1574522b290e8127a030eca581c1c" + }, + "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/artifacts/original-completion-2026-10-02/p23-02-guest-need-normalization-local-r296/engine/stdout.log": { + "path": "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/artifacts/original-completion-2026-10-02/p23-02-guest-need-normalization-local-r296/engine/stdout.log", + "bytes": 7875, + "sha256": "f9e15b96bce11af6984f0562320758063cdcc880b237b802f09de863d616d839" + }, + "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/artifacts/original-completion-2026-10-02/p23-02-guest-need-normalization-local-r296/engine/started.json": { + "path": "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/artifacts/original-completion-2026-10-02/p23-02-guest-need-normalization-local-r296/engine/started.json", + "bytes": 2392, + "sha256": "25d185b097982970d1d089fc6f45edcf97b461035e3c326d171922b3b1758384" + }, + "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/artifacts/original-completion-2026-10-02/p23-02-guest-need-normalization-local-r296/engine/process.json": { + "path": "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/artifacts/original-completion-2026-10-02/p23-02-guest-need-normalization-local-r296/engine/process.json", + "bytes": 6661, + "sha256": "4ddf96836dc928b532bb708f81b6798705e314aac4b3369d068a5ac93cbf633b" + }, + "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/artifacts/original-completion-2026-10-02/p23-02-guest-need-normalization-local-r296/engine/outer-owned-finally.json": { + "path": "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/artifacts/original-completion-2026-10-02/p23-02-guest-need-normalization-local-r296/engine/outer-owned-finally.json", + "bytes": 964, + "sha256": "13c2101303504e43e70f76c53389ef9894e4dd801a6de2939738f5e858eac729" + }, + "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/artifacts/original-completion-2026-10-02/p28-ai-r03-typed-difference-independent-review-r01/finite_variant_codec.py": { + "path": "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/artifacts/original-completion-2026-10-02/p28-ai-r03-typed-difference-independent-review-r01/finite_variant_codec.py", + "bytes": 5438, + "sha256": "8c7077b53e28da4054bdf7dd88e82de02a51149269a8fce59bae0e74f95b4734" + } + }, + "canonical_doc4_context_pins": { + "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/docs/plans/original-completion-2026-10-02/IMPLEMENTATION_PLAN.md": { + "path": "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/docs/plans/original-completion-2026-10-02/IMPLEMENTATION_PLAN.md", + "bytes": 492299, + "sha256": "61776f0673770277ab6d182107337ed40a108ff3b5ab2c1a8f29fc4f2c3015ad" + }, + "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/docs/plans/original-completion-2026-10-02/PROGRESS_INTEGRATION.md": { + "path": "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/docs/plans/original-completion-2026-10-02/PROGRESS_INTEGRATION.md", + "bytes": 410363, + "sha256": "8b0488910dba35b34da96d7062b9a69d293e70a1963c9c5890ec32b2891baaea" + }, + "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/docs/plans/original-completion-2026-10-02/work_packages.json": { + "path": "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/docs/plans/original-completion-2026-10-02/work_packages.json", + "bytes": 1184337, + "sha256": "32d80e911910bc762287dc3ddf0ee0ea3072ca2f0158aa2f9f355b9202cfdfeb" + }, + "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/docs/plans/original-completion-2026-10-02/progress_snapshot.json": { + "path": "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/docs/plans/original-completion-2026-10-02/progress_snapshot.json", + "bytes": 539512, + "sha256": "b417ee1b945dfe4666f3a2f3501f906756826ea88d741bb116f3dd20250c8958" + } + }, + "observations": { + "engine_pid": 44204, + "owned_normal_exit_code": 0, + "checks": 1619, + "failures": 0, + "cases": 231, + "callback_cases": 2, + "typed_record_rows": 233, + "whole_raw_bytes": 408484, + "whole_raw_sha256": "ae97195121e2def68615caf2b0cb3bda59a130097ef34cb9656c75ac387b7343", + "finitecodec": { + "nodes": 28007, + "headers": { + "28": 676, + "27": 2078, + "4": 1288, + "21": 13636, + "65538": 288, + "2": 10028, + "0": 1, + "1": 8, + "3": 4 + }, + "file_envelope_bytes": 0, + "all_bytes_roundtrip": true + }, + "independent_paired_full_span_equal": 233, + "private_profile_conditions": 5, + "original_service_full_body_baseline": "Only class_name renamed; original body authenticated by accepted preexecution proof and matching pin.", + "actual_loaded_scope": "r295 GDScript GuestService finite evaluate/rank, not new native ABI/library acceptance.", + "script_parse_and_listed_paths_observed": true, + "input_and_state_observation": "Pinned QA passed full service snapshot/error/input comparisons and 224 actual nested-readonly checks. Readonly/alias flags are QA assertions, not independently encoded flags in Variant byte archive. Returned fresh Dictionary checked 231 times.", + "microbenchmark": { + "input_index": 50, + "need_map_entries": 1, + "calls_each": 2000, + "original_us": 28461, + "candidate_us": 41815, + "fixed_order": [ + "original", + "candidate" + ], + "performance_accepted": false + } + }, + "scope_limits": [ + "233 stored expected/actual ordered typed result spans independently match all bytes; full snapshots/errors/input preservation and guard selection are evidenced by passing pinned QA assertions, not additional snapshot records in this archive.", + "Two custom preference cases genuinely mutate guest.needs and nested candidate relief, including explicit preference eager-default dispatch, with original fallback results and state equality. This is not general reentry or arbitrary custom override acceptance.", + "Inputs[50] is one Need map, not representative six-map workload; one-process fixed-order 2000-call microbenchmark is not whole tick, stable benefit/regression, pure CPU, P95/D10 or 50ms budget evidence.", + "No whole647/full172/normal OS/same-version Windows/performance/adoption/unit completion acceptance. Candidate remains unadopted.", + "16MiB output restriction is post-phase, not live peak." + ], + "reviewer_execution": { + "engine": 0, + "compiler": 0, + "tests": 0, + "validator": 0, + "author": 0, + "new_agents": 0, + "existing_edits": 0, + "external_zstd_read_only": 1, + "finite_codec_read_only": 1 + } +} diff --git a/docs/plans/original-completion-2026-10-02/checkpoints/guest-need-normalization-r295-r298/p23-02-guest-need-normalization-local-preexecution-independent-review-r296.json b/docs/plans/original-completion-2026-10-02/checkpoints/guest-need-normalization-r295-r298/p23-02-guest-need-normalization-local-preexecution-independent-review-r296.json new file mode 100644 index 0000000..75a19aa --- /dev/null +++ b/docs/plans/original-completion-2026-10-02/checkpoints/guest-need-normalization-r295-r298/p23-02-guest-need-normalization-local-preexecution-independent-review-r296.json @@ -0,0 +1,155 @@ +{ + "review_id": "p23-02-guest-need-normalization-local-preexecution-independent-review-r296", + "execution_allowed": true, + "blockers": [], + "evidence_sha256": { + "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/artifacts/original-completion-2026-10-02/p23-02-native-reconstruction-local-r209/stage/scripts/guest/guest_service.gd": { + "path": "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/artifacts/original-completion-2026-10-02/p23-02-native-reconstruction-local-r209/stage/scripts/guest/guest_service.gd", + "bytes": 61414, + "sha256": "56880410e9a00816f9a95d30821982955532de8dd976980934e12016a9f2b21e" + }, + "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/artifacts/original-completion-2026-10-02/p23-02-guest-need-normalization-candidate-r295/source/guest_service.gd": { + "path": "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/artifacts/original-completion-2026-10-02/p23-02-guest-need-normalization-candidate-r295/source/guest_service.gd", + "bytes": 64563, + "sha256": "2794f20630168d902e6a7fb55a0b5aacfbbcfcccd51d336f0724f03ee35ea249" + }, + "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/artifacts/original-completion-2026-10-02/p23-02-guest-need-normalization-local-r296/original_guest_service.gd": { + "path": "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/artifacts/original-completion-2026-10-02/p23-02-guest-need-normalization-local-r296/original_guest_service.gd", + "bytes": 61426, + "sha256": "a70362ac6326266573a873319146d50a540829b751ad4c07746937fef85f5193" + }, + "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/artifacts/original-completion-2026-10-02/p23-02-guest-need-normalization-local-r296/custom_definition.gd": { + "path": "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/artifacts/original-completion-2026-10-02/p23-02-guest-need-normalization-local-r296/custom_definition.gd", + "bytes": 281, + "sha256": "d29c18196c89452a0ba6db8ec17d0ac2e75be2b6092533c734288230bc44f0c7" + }, + "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/artifacts/original-completion-2026-10-02/p23-02-guest-need-normalization-local-r296/local.gd": { + "path": "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/artifacts/original-completion-2026-10-02/p23-02-guest-need-normalization-local-r296/local.gd", + "bytes": 7048, + "sha256": "bd9cb24e28bc97c0e8f906b6d2b0d60c14964fdaabbac3ac6561334e434ff97f" + }, + "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/artifacts/original-completion-2026-10-02/p23-02-guest-need-normalization-local-r296/guarded.gd": { + "path": "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/artifacts/original-completion-2026-10-02/p23-02-guest-need-normalization-local-r296/guarded.gd", + "bytes": 525, + "sha256": "5db64f0527aee0c74d0dff52103aefeec112824faa82137ab78d546979655658" + }, + "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/tools/run_all_tests.py": { + "path": "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/tools/run_all_tests.py", + "bytes": 26296, + "sha256": "320ca668732b4ff72fc4449998af2327d4e5c527b06896740806aaf0487282b4" + }, + "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/tools/regression/owned_outer.py": { + "path": "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/tools/regression/owned_outer.py", + "bytes": 4177, + "sha256": "c2e3483b76db1a3ff940f23f88ea0083fc78736e874ebcdb385d766dde8866e7" + }, + "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/artifacts/original-completion-2026-10-02/p23-02-guest-need-normalization-static-independent-review-r295.json": { + "path": "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/artifacts/original-completion-2026-10-02/p23-02-guest-need-normalization-static-independent-review-r295.json", + "bytes": 9244, + "sha256": "0954f512e6065c26ffc7d06c087fe973379a48c9cebef6752f19ec52d24f59d8" + }, + "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/artifacts/original-completion-2026-10-02/p23-02-guest-need-normalization-candidate-r295/input.json": { + "path": "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/artifacts/original-completion-2026-10-02/p23-02-guest-need-normalization-candidate-r295/input.json", + "bytes": 4366, + "sha256": "f592b9895d9253be9e7f829cd88888fe166e77e00959570f7d4a7f0c3c169489" + }, + "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/artifacts/original-completion-2026-10-02/p23-02-build-preview-monitor-compressed-r06.gd": { + "path": "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/artifacts/original-completion-2026-10-02/p23-02-build-preview-monitor-compressed-r06.gd", + "bytes": 17582, + "sha256": "92d5e1956f4e613e12555d756de4f71fef03678cde159ac68fac3188ce3f336e" + }, + "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/artifacts/original-completion-2026-10-02/p23-02-native-cache-public-mutations-r144/profile_guard.gd": { + "path": "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/artifacts/original-completion-2026-10-02/p23-02-native-cache-public-mutations-r144/profile_guard.gd", + "bytes": 2276, + "sha256": "597696c705cc8f0e79a35265ca3ef8df55f7fc3c4e0e5a2ce9a4e20e29e29a6f" + }, + "/Applications/Godot.app/Contents/MacOS/Godot": { + "path": "/Applications/Godot.app/Contents/MacOS/Godot", + "bytes": 351312912, + "sha256": "c7cccbf8fb143e34e02fd6521e09be2c2b974f0d5db080b19071c9c570718ccf" + }, + "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/artifacts/original-completion-2026-10-02/p23-02-guest-need-normalization-local-r296.py": { + "path": "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/artifacts/original-completion-2026-10-02/p23-02-guest-need-normalization-local-r296.py", + "bytes": 5232, + "sha256": "b9059debc1eb3aea9497d5abea646238edec5c73291d170737c29ce9026803eb" + }, + "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/artifacts/original-completion-2026-10-02/p23-02-guest-need-normalization-local-r296/input.json": { + "path": "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/artifacts/original-completion-2026-10-02/p23-02-guest-need-normalization-local-r296/input.json", + "bytes": 968619, + "sha256": "3bba39c1ff62fb59a7b844515072dc322728295c5d7680aef4055af5d10494ee" + } + }, + "canonical_doc4_context_pins": { + "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/docs/plans/original-completion-2026-10-02/IMPLEMENTATION_PLAN.md": { + "path": "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/docs/plans/original-completion-2026-10-02/IMPLEMENTATION_PLAN.md", + "bytes": 492299, + "sha256": "61776f0673770277ab6d182107337ed40a108ff3b5ab2c1a8f29fc4f2c3015ad" + }, + "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/docs/plans/original-completion-2026-10-02/PROGRESS_INTEGRATION.md": { + "path": "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/docs/plans/original-completion-2026-10-02/PROGRESS_INTEGRATION.md", + "bytes": 410363, + "sha256": "8b0488910dba35b34da96d7062b9a69d293e70a1963c9c5890ec32b2891baaea" + }, + "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/docs/plans/original-completion-2026-10-02/work_packages.json": { + "path": "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/docs/plans/original-completion-2026-10-02/work_packages.json", + "bytes": 1184337, + "sha256": "32d80e911910bc762287dc3ddf0ee0ea3072ca2f0158aa2f9f355b9202cfdfeb" + }, + "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/docs/plans/original-completion-2026-10-02/progress_snapshot.json": { + "path": "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/docs/plans/original-completion-2026-10-02/progress_snapshot.json", + "bytes": 539512, + "sha256": "b417ee1b945dfe4666f3a2f3501f906756826ea88d741bb116f3dd20250c8958" + } + }, + "allowed_scope": "One bounded owned headless engine, original class-name-only baseline versus transient r295 GuestService; restore original GuestService/project after child termination. No compiler, clone, adoption or whole execution.", + "independent_derivation": { + "enumerated_builtin_cases": 224, + "additional_cases": 7, + "candidate_cases": 231, + "callback_cases": 2, + "checks": { + "setup": 4, + "builtin_comparisons": 1120, + "additional_comparisons": 28, + "rank": 1, + "custom_callback": 10, + "actual_nested_readonly": 224, + "fresh_return": 231, + "exclusive_typed": 1, + "total": 1619 + }, + "starter_assertions": 0, + "inventory_files_each": 1299, + "transient_inventory_difference": [ + "scripts/guest/guest_service.gd" + ], + "baseline_body_exact_inverse": true, + "capacity_free_bytes_observed": 1255604224, + "floor_bytes": 536870912, + "admission_bytes": 553648128 + }, + "verified_controls": [ + "Original class_name is the only baseline change; full evaluate and other methods remain byte-identical. Candidate source and static review pins match.", + "224 canonical map fixtures independently restore nested readonly after duplicate(true); 7 additional fixtures cover scalar/invalid/float/explicit/alias spellings. No count assumes a registration assertion in the Guard starter.", + "CustomDefinition inherits a Resource definition with no autoload-dependent startup references; preference_for performs real needs and nested relief mutation; eager default call must execute once even with explicit preference.", + "Snapshot/input/ordered return comparisons, fresh returned Dictionary checks and one ranked tie/order comparison remain asserted.", + "QA inherits deferred synchronous run; no coroutine super.run issue. Typed archive exclusive check occurs before report construction. Guard activates private runtime profile before deferred evaluation.", + "Driver validates preexecution pins and bound root SHA, exact source/default/current docs/SUPPORT/shared and immutable parent, 512MiB floor plus 16MiB admission, no live foreign engine before patch, owned90s capture/strict error logs/private5/orphan rejection. Restoration in finally requires all captured engines ended; final full integrity protection." + ], + "inference_limits": [ + "Textual GDScript syntax/type review is not actual parse execution; isolated engine may still reject types and must retain failure evidence.", + "One process 2000 evaluations per side is a helper microbenchmark only, fixed original-first order; no stable gain, whole tick, pure CPU, P95/D10 or 50ms inference.", + "16MiB output budget is checked after phase, not enforced as live peak. Capacity acceptance is observation-specific and runtime admission remains mandatory.", + "Finite callback covers custom definition preference mutation; arbitrary service/guest override, concurrent mutation, malicious same-path resource replacement and generic reentry remain unaccepted.", + "No whole647/final172/normal OS/same-version Windows/performance/adoption/unit acceptance." + ], + "reviewer_execution": { + "engine": 0, + "compiler": 0, + "tests": 0, + "validator": 0, + "author": 0, + "existing_edits": 0, + "new_agents": 0 + } +} diff --git a/docs/plans/original-completion-2026-10-02/checkpoints/guest-need-normalization-r295-r298/p23-02-guest-need-normalization-local-r296.py b/docs/plans/original-completion-2026-10-02/checkpoints/guest-need-normalization-r295-r298/p23-02-guest-need-normalization-local-r296.py new file mode 100644 index 0000000..76db0df --- /dev/null +++ b/docs/plans/original-completion-2026-10-02/checkpoints/guest-need-normalization-r295-r298/p23-02-guest-need-normalization-local-r296.py @@ -0,0 +1,43 @@ +# coding: utf-8 +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';D=R/'docs/plans/original-completion-2026-10-02';O=A/'p23-02-guest-need-normalization-local-r296' +def mod(name,path): + s=importlib.util.spec_from_file_location(name,path);m=importlib.util.module_from_spec(s);s.loader.exec_module(m);return m +def main(): + r=mod('r296',R/'tools/run_all_tests.py');outer=mod('outer296',r.SUPPORT/'owned_outer.py');plan=r.read(O/'input.json');assert not (O/'execution.json').exists() + prepath=A/'p23-02-guest-need-normalization-local-preexecution-independent-review-r296.json';pre=r.read(prepath);assert pre['execution_allowed'] and not pre['blockers'] + for group in ['evidence_sha256','canonical_doc4_context_pins']: + for key,q in pre[group].items(): + p=Path(q.get('path',key));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) + stage=Path(plan['stage']);parent=Path(plan['parent']);guest=Path(plan['guest_source']);original=guest.read_bytes();modified=Path(plan['modified_guest']).read_bytes();public=(stage/'project.godot').read_bytes();patched=False + def protect(): + assert r.source_refs(R)==plan['source_before'] and r.default_player(R)==plan['default_before'];assert {n:r.sha(D/n) for n in plan['doc4_before']}==plan['doc4_before'];assert r.inventory(r.SUPPORT)==plan['support_inventory'] + assert r.inventory(parent,stage=True)==plan['expected_stage'];assert r.inventory(stage,stage=True)==(plan['patched_stage'] if patched else plan['expected_stage']) + for key,q in plan['pins'].items(): + p=Path(q['path']) + if p==guest:assert p.read_bytes()==(modified if patched else original) + else:assert p.stat().st_size==q['bytes'] and r.sha(p)==q['sha256'],key + protect();assert not outer.native.helper['live_engines']();assert shutil.disk_usage(A).free>=plan['capacity_floor_bytes']+plan['output_budget_bytes'] + error=None;phase=None;report=None;cleanup=[];cleanup_errors=[] + try: + guest.write_bytes(modified);patched=True;protect();output=O/'measurement';output.mkdir();ident=uuid.uuid4().hex;startup='TheTree-PCK-tests-'+ident+'-needs-startup';profile='TheTree-PCK-tests-'+ident+'-needs-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/'profile.json'),THE_TREE_RUNTIME_MONITOR_OUTPUT=str(output)) + (stage/'project.godot').write_bytes(r.private_project(public,startup));argv=[plan['engine_path'],'--headless','--path',str(stage),'--script',str(O/'guarded.gd'),'--verbose','--log-file',str(O/'engine/engine-native.log')] + result,capture=outer.run_phase(argv,O/'engine',plan['timeout_seconds'],env,now_fn=r.now,dump_fn=r.write,sha_fn=r.sha,live_engines_fn=outer.native.helper['live_engines']);phase=dict(result=result,capture=capture);r.write(O/'phase.json',phase) + rp=output/'report.json';report=r.read(rp) if rp.exists() else None;assert result and result['exit_code']==0 and not result['error_lines'] and not result['timed_out'] and capture['passed'];assert report and report['passed'] and not report['failures'] and report['checks']==plan['expected_checks'] and report['cases']==plan['local_cases'] and report['callback_cases']==2 and report['pid']==result['pid'] + logs=''.join((O/'engine'/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;r.profile_gate(r.profile_path(profile),O/'profile.json',profile) + assert sum(p.stat().st_size for p in O.rglob('*') if p.is_file())<=plan['output_budget_bytes'];(stage/'project.godot').write_bytes(public);guest.write_bytes(original);patched=False;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):guest.write_bytes(original);patched=False;(stage/'project.godot').write_bytes(public) + else:cleanup_errors.append('Restoration refused while owned child remains live') + guard_errors=[] + try:assert guest.read_bytes()==original and not patched;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 phase is not None and report is not None + r.write(O/'execution.json',dict(passed=passed,error=error,guard_errors=guard_errors,phase=phase,report=report,cleanup=cleanup,cleanup_errors=cleanup_errors,source_default_doc4_support_stage_preserved=not guard_errors,guest_project_restored_byteexact=not patched,compiler_runs=0,unit_accepted=False,performance_accepted=False,candidate_adopted=False));print(json.dumps(dict(passed=passed,error=error,guard_errors=guard_errors,timings=report.get('timings') if report else None)),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/guest-need-normalization-r295-r298/p23-02-guest-need-normalization-local-r296/actual-root-verification.json b/docs/plans/original-completion-2026-10-02/checkpoints/guest-need-normalization-r295-r298/p23-02-guest-need-normalization-local-r296/actual-root-verification.json new file mode 100644 index 0000000..e2a2830 --- /dev/null +++ b/docs/plans/original-completion-2026-10-02/checkpoints/guest-need-normalization-r295-r298/p23-02-guest-need-normalization-local-r296/actual-root-verification.json @@ -0,0 +1,183 @@ +{ + "passed": true, + "review_sha256": "ce06bfe9156ed8e1c9ec53519d6cc52b26c963ca0e8b4a263ff70d1a1abf9a23", + "verified_entries": [ + { + "path": "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/artifacts/original-completion-2026-10-02/p23-02-native-reconstruction-local-r209/stage/scripts/guest/guest_service.gd", + "bytes": 61414, + "sha256": "56880410e9a00816f9a95d30821982955532de8dd976980934e12016a9f2b21e" + }, + { + "path": "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/artifacts/original-completion-2026-10-02/p23-02-guest-need-normalization-candidate-r295/source/guest_service.gd", + "bytes": 64563, + "sha256": "2794f20630168d902e6a7fb55a0b5aacfbbcfcccd51d336f0724f03ee35ea249" + }, + { + "path": "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/artifacts/original-completion-2026-10-02/p23-02-guest-need-normalization-local-r296/original_guest_service.gd", + "bytes": 61426, + "sha256": "a70362ac6326266573a873319146d50a540829b751ad4c07746937fef85f5193" + }, + { + "path": "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/artifacts/original-completion-2026-10-02/p23-02-guest-need-normalization-local-r296/custom_definition.gd", + "bytes": 281, + "sha256": "d29c18196c89452a0ba6db8ec17d0ac2e75be2b6092533c734288230bc44f0c7" + }, + { + "path": "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/artifacts/original-completion-2026-10-02/p23-02-guest-need-normalization-local-r296/local.gd", + "bytes": 7048, + "sha256": "bd9cb24e28bc97c0e8f906b6d2b0d60c14964fdaabbac3ac6561334e434ff97f" + }, + { + "path": "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/artifacts/original-completion-2026-10-02/p23-02-guest-need-normalization-local-r296/guarded.gd", + "bytes": 525, + "sha256": "5db64f0527aee0c74d0dff52103aefeec112824faa82137ab78d546979655658" + }, + { + "path": "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/tools/run_all_tests.py", + "bytes": 26296, + "sha256": "320ca668732b4ff72fc4449998af2327d4e5c527b06896740806aaf0487282b4" + }, + { + "path": "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/tools/regression/owned_outer.py", + "bytes": 4177, + "sha256": "c2e3483b76db1a3ff940f23f88ea0083fc78736e874ebcdb385d766dde8866e7" + }, + { + "path": "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/artifacts/original-completion-2026-10-02/p23-02-guest-need-normalization-static-independent-review-r295.json", + "bytes": 9244, + "sha256": "0954f512e6065c26ffc7d06c087fe973379a48c9cebef6752f19ec52d24f59d8" + }, + { + "path": "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/artifacts/original-completion-2026-10-02/p23-02-guest-need-normalization-candidate-r295/input.json", + "bytes": 4366, + "sha256": "f592b9895d9253be9e7f829cd88888fe166e77e00959570f7d4a7f0c3c169489" + }, + { + "path": "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/artifacts/original-completion-2026-10-02/p23-02-build-preview-monitor-compressed-r06.gd", + "bytes": 17582, + "sha256": "92d5e1956f4e613e12555d756de4f71fef03678cde159ac68fac3188ce3f336e" + }, + { + "path": "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/artifacts/original-completion-2026-10-02/p23-02-native-cache-public-mutations-r144/profile_guard.gd", + "bytes": 2276, + "sha256": "597696c705cc8f0e79a35265ca3ef8df55f7fc3c4e0e5a2ce9a4e20e29e29a6f" + }, + { + "path": "/Applications/Godot.app/Contents/MacOS/Godot", + "bytes": 351312912, + "sha256": "c7cccbf8fb143e34e02fd6521e09be2c2b974f0d5db080b19071c9c570718ccf" + }, + { + "path": "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/artifacts/original-completion-2026-10-02/p23-02-guest-need-normalization-local-r296.py", + "bytes": 5232, + "sha256": "b9059debc1eb3aea9497d5abea646238edec5c73291d170737c29ce9026803eb" + }, + { + "path": "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/artifacts/original-completion-2026-10-02/p23-02-guest-need-normalization-local-preexecution-independent-review-r296.json", + "bytes": 11638, + "sha256": "dc2662c7f9b92de16e9d4d97daf86d3a2eb537f2587cc76946d77e7021907071" + }, + { + "path": "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/artifacts/original-completion-2026-10-02/p23-02-guest-need-normalization-local-r296/input.json", + "bytes": 968619, + "sha256": "3bba39c1ff62fb59a7b844515072dc322728295c5d7680aef4055af5d10494ee" + }, + { + "path": "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/artifacts/original-completion-2026-10-02/p23-02-guest-need-normalization-local-r296/preexecution-root-verification.json", + "bytes": 5516, + "sha256": "9877abcde2f6fa06aeaa81a8a8d6b8a5b06bb7db8012bbe321f9cf8e106a8f12" + }, + { + "path": "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/artifacts/original-completion-2026-10-02/p23-02-guest-need-normalization-local-r296/execution.json", + "bytes": 10788, + "sha256": "d01a859ce75a18084cbec7a081c906e267d1e8c7a42bce55e2539dd3a7ede8c3" + }, + { + "path": "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/artifacts/original-completion-2026-10-02/p23-02-guest-need-normalization-local-r296/profile.json", + "bytes": 907, + "sha256": "f52b9cc1f3642a8b150c97a40f2cf3cd9a80c793cba5a6693811369294a50a64" + }, + { + "path": "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/artifacts/original-completion-2026-10-02/p23-02-guest-need-normalization-local-r296/phase.json", + "bytes": 8097, + "sha256": "5ea3de68f87dda0fa0793e2d87428990645d61fa70cba14e44b8fb4d73c6f3b9" + }, + { + "path": "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/artifacts/original-completion-2026-10-02/p23-02-guest-need-normalization-local-r296/measurement/evaluation-fixtures.varbytes.zst", + "bytes": 3540, + "sha256": "c21dad1a71f30a2c8d4cab558bbfb4543f2daca4487b1fbdbcbb1b1a97b9143d" + }, + { + "path": "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/artifacts/original-completion-2026-10-02/p23-02-guest-need-normalization-local-r296/measurement/report.json", + "bytes": 1128, + "sha256": "fcf08d05ca7109beed8e0c4fc75f422baab72a515467e09debf58c812fb0689d" + }, + { + "path": "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/artifacts/original-completion-2026-10-02/p23-02-guest-need-normalization-local-r296/engine/stderr.log", + "bytes": 0, + "sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + }, + { + "path": "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/artifacts/original-completion-2026-10-02/p23-02-guest-need-normalization-local-r296/engine/ended.json", + "bytes": 1772, + "sha256": "63f84a8e05af97d63b2a37ed47e1075263b1b8fe51190ff14f934f24149c3919" + }, + { + "path": "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/artifacts/original-completion-2026-10-02/p23-02-guest-need-normalization-local-r296/engine/adapter-owned-cleanup.json", + "bytes": 351, + "sha256": "c1f3d289b362cc61b9be169a3994be27df39f35e6221881b4a9678ce3c72ba54" + }, + { + "path": "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/artifacts/original-completion-2026-10-02/p23-02-guest-need-normalization-local-r296/engine/engine-native.log", + "bytes": 7725, + "sha256": "067ea4cc50ac8e60720e8e58d4e83729afd1574522b290e8127a030eca581c1c" + }, + { + "path": "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/artifacts/original-completion-2026-10-02/p23-02-guest-need-normalization-local-r296/engine/stdout.log", + "bytes": 7875, + "sha256": "f9e15b96bce11af6984f0562320758063cdcc880b237b802f09de863d616d839" + }, + { + "path": "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/artifacts/original-completion-2026-10-02/p23-02-guest-need-normalization-local-r296/engine/started.json", + "bytes": 2392, + "sha256": "25d185b097982970d1d089fc6f45edcf97b461035e3c326d171922b3b1758384" + }, + { + "path": "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/artifacts/original-completion-2026-10-02/p23-02-guest-need-normalization-local-r296/engine/process.json", + "bytes": 6661, + "sha256": "4ddf96836dc928b532bb708f81b6798705e314aac4b3369d068a5ac93cbf633b" + }, + { + "path": "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/artifacts/original-completion-2026-10-02/p23-02-guest-need-normalization-local-r296/engine/outer-owned-finally.json", + "bytes": 964, + "sha256": "13c2101303504e43e70f76c53389ef9894e4dd801a6de2939738f5e858eac729" + }, + { + "path": "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/artifacts/original-completion-2026-10-02/p28-ai-r03-typed-difference-independent-review-r01/finite_variant_codec.py", + "bytes": 5438, + "sha256": "8c7077b53e28da4054bdf7dd88e82de02a51149269a8fce59bae0e74f95b4734" + }, + { + "path": "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/docs/plans/original-completion-2026-10-02/IMPLEMENTATION_PLAN.md", + "bytes": 492299, + "sha256": "61776f0673770277ab6d182107337ed40a108ff3b5ab2c1a8f29fc4f2c3015ad" + }, + { + "path": "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/docs/plans/original-completion-2026-10-02/PROGRESS_INTEGRATION.md", + "bytes": 410363, + "sha256": "8b0488910dba35b34da96d7062b9a69d293e70a1963c9c5890ec32b2891baaea" + }, + { + "path": "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/docs/plans/original-completion-2026-10-02/work_packages.json", + "bytes": 1184337, + "sha256": "32d80e911910bc762287dc3ddf0ee0ea3072ca2f0158aa2f9f355b9202cfdfeb" + }, + { + "path": "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/docs/plans/original-completion-2026-10-02/progress_snapshot.json", + "bytes": 539512, + "sha256": "b417ee1b945dfe4666f3a2f3501f906756826ea88d741bb116f3dd20250c8958" + } + ], + "unit_accepted": false, + "performance_accepted": false +} diff --git a/docs/plans/original-completion-2026-10-02/checkpoints/guest-need-normalization-r295-r298/p23-02-guest-need-normalization-local-r296/custom_definition.gd b/docs/plans/original-completion-2026-10-02/checkpoints/guest-need-normalization-r295-r298/p23-02-guest-need-normalization-local-r296/custom_definition.gd new file mode 100644 index 0000000..06ac6ff --- /dev/null +++ b/docs/plans/original-completion-2026-10-02/checkpoints/guest-need-normalization-r295-r298/p23-02-guest-need-normalization-local-r296/custom_definition.gd @@ -0,0 +1,9 @@ +extends "res://scripts/guest/guest_definition.gd" +var watched_guest: Variant +var watched_candidate: Dictionary +var calls := 0 +func preference_for(_tags: Variant) -> int: + calls += 1 + watched_guest.needs[&"hunger"] = 99 + watched_candidate[&"need_relief"][&"hunger"] = 80 + return 13 diff --git a/docs/plans/original-completion-2026-10-02/checkpoints/guest-need-normalization-r295-r298/p23-02-guest-need-normalization-local-r296/execution.json b/docs/plans/original-completion-2026-10-02/checkpoints/guest-need-normalization-r295-r298/p23-02-guest-need-normalization-local-r296/execution.json new file mode 100644 index 0000000..b94d15b --- /dev/null +++ b/docs/plans/original-completion-2026-10-02/checkpoints/guest-need-normalization-r295-r298/p23-02-guest-need-normalization-local-r296/execution.json @@ -0,0 +1,262 @@ +{ + "passed": true, + "error": null, + "guard_errors": [], + "phase": { + "result": { + "argv": [ + "/Applications/Godot.app/Contents/MacOS/Godot", + "--headless", + "--path", + "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/artifacts/original-completion-2026-10-02/p23-02-native-reconstruction-local-r209/stage", + "--script", + "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/artifacts/original-completion-2026-10-02/p23-02-guest-need-normalization-local-r296/guarded.gd", + "--verbose", + "--log-file", + "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/artifacts/original-completion-2026-10-02/p23-02-guest-need-normalization-local-r296/engine/engine-native.log" + ], + "pid": 44204, + "started_utc": "2026-10-11T23:34:52.715973+00:00", + "ended_utc": "2026-10-11T23:34:54.109697+00:00", + "wall_seconds": 1.393236333, + "exit_code": 0, + "helper_exit_code": 0, + "timed_out": false, + "stdout_sha256": "f9e15b96bce11af6984f0562320758063cdcc880b237b802f09de863d616d839", + "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": 44204, + "owner_pid": 44180, + "command": [ + "/Applications/Godot.app/Contents/MacOS/Godot", + "--headless", + "--path", + "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/artifacts/original-completion-2026-10-02/p23-02-native-reconstruction-local-r209/stage", + "--script", + "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/artifacts/original-completion-2026-10-02/p23-02-guest-need-normalization-local-r296/guarded.gd", + "--verbose", + "--log-file", + "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/artifacts/original-completion-2026-10-02/p23-02-guest-need-normalization-local-r296/engine/engine-native.log" + ], + "monotonic_ns": 2578288416, + "pid": 44204, + "argv": [ + "/Applications/Godot.app/Contents/MacOS/Godot", + "--headless", + "--path", + "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/artifacts/original-completion-2026-10-02/p23-02-native-reconstruction-local-r209/stage", + "--script", + "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/artifacts/original-completion-2026-10-02/p23-02-guest-need-normalization-local-r296/guarded.gd", + "--verbose", + "--log-file", + "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/artifacts/original-completion-2026-10-02/p23-02-guest-need-normalization-local-r296/engine/engine-native.log" + ], + "started_utc": "2026-10-11T23:34:52.715973+00:00", + "current_process_identity": { + "pid": 44204, + "ppid": 44180, + "pgid": 44204, + "args": "/Applications/Godot.app/Contents/MacOS/Godot --headless --path /Users/korizatou114/Projects/とりにく工房ゲーム制作部/artifacts/original-completion-2026-10-02/p23-02-native-reconstruction-local-r209/stage --script /Users/korizatou114/Projects/とりにく工房ゲーム制作部/artifacts/original-completion-2026-10-02/p23-02-guest-need-normalization-local-r296/guarded.gd --verbose --log-file /Users/korizatou114/Projects/とりにく工房ゲーム制作部/artifacts/original-completion-2026-10-02/p23-02-guest-need-normalization-local-r296/engine/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": 44204, + "owner_pid": 44180, + "command": [ + "/Applications/Godot.app/Contents/MacOS/Godot", + "--headless", + "--path", + "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/artifacts/original-completion-2026-10-02/p23-02-native-reconstruction-local-r209/stage", + "--script", + "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/artifacts/original-completion-2026-10-02/p23-02-guest-need-normalization-local-r296/guarded.gd", + "--verbose", + "--log-file", + "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/artifacts/original-completion-2026-10-02/p23-02-guest-need-normalization-local-r296/engine/engine-native.log" + ], + "monotonic_ns": 3964880666, + "exit_code": 0, + "actual_process_exit_code": 0, + "engine_ended": true, + "timed_out": false, + "lifecycle_error": null, + "start_receipt_written": true, + "pid": 44204, + "argv": [ + "/Applications/Godot.app/Contents/MacOS/Godot", + "--headless", + "--path", + "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/artifacts/original-completion-2026-10-02/p23-02-native-reconstruction-local-r209/stage", + "--script", + "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/artifacts/original-completion-2026-10-02/p23-02-guest-need-normalization-local-r296/guarded.gd", + "--verbose", + "--log-file", + "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/artifacts/original-completion-2026-10-02/p23-02-guest-need-normalization-local-r296/engine/engine-native.log" + ], + "ended_utc": "2026-10-11T23:34:54.107599+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": [ + 44204 + ], + "cleanup": [ + { + "pid": 44204, + "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 + } + }, + "report": { + "callback_cases": 2, + "candidate_adopted": false, + "cases": 231, + "checks": 1619, + "failures": [], + "fixtures": { + "bytes": 3540, + "codec": "zstd", + "field_deletions": 0, + "file_envelope": false, + "full_bytes_roundtrip": true, + "path": "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/artifacts/original-completion-2026-10-02/p23-02-guest-need-normalization-local-r296/measurement/evaluation-fixtures.varbytes.zst", + "sha256": "c21dad1a71f30a2c8d4cab558bbfb4543f2daca4487b1fbdbcbb1b1a97b9143d", + "uncompressed_bytes": 408484, + "uncompressed_sha256": "ae97195121e2def68615caf2b0cb3bda59a130097ef34cb9656c75ac387b7343" + }, + "limits": [ + "One process finite builtin and custom-preference observations only. Adjacent helper microbenchmark is not representative whole-tick, stable gain, P95/D10/50ms, full regression, OS or Windows acceptance." + ], + "passed": true, + "performance_accepted": false, + "pid": 44204, + "timings": [ + { + "calls": 2000, + "elapsed_us": 28461, + "mode": "original" + }, + { + "calls": 2000, + "elapsed_us": 41815, + "mode": "candidate" + } + ], + "unit_accepted": false + }, + "cleanup": [ + { + "pid": 44204, + "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 + } + ], + "cleanup_errors": [], + "source_default_doc4_support_stage_preserved": true, + "guest_project_restored_byteexact": true, + "compiler_runs": 0, + "unit_accepted": false, + "performance_accepted": false, + "candidate_adopted": false +} diff --git a/docs/plans/original-completion-2026-10-02/checkpoints/guest-need-normalization-r295-r298/p23-02-guest-need-normalization-local-r296/guarded.gd b/docs/plans/original-completion-2026-10-02/checkpoints/guest-need-normalization-r295-r298/p23-02-guest-need-normalization-local-r296/guarded.gd new file mode 100644 index 0000000..7aa3046 --- /dev/null +++ b/docs/plans/original-completion-2026-10-02/checkpoints/guest-need-normalization-r295-r298/p23-02-guest-need-normalization-local-r296/guarded.gd @@ -0,0 +1,5 @@ +extends "/Users/korizatou114/Projects/\u3068\u308a\u306b\u304f\u5de5\u623f\u30b2\u30fc\u30e0\u5236\u4f5c\u90e8/artifacts/original-completion-2026-10-02/p23-02-guest-need-normalization-local-r296/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() diff --git a/docs/plans/original-completion-2026-10-02/checkpoints/guest-need-normalization-r295-r298/p23-02-guest-need-normalization-local-r296/input.json.gz b/docs/plans/original-completion-2026-10-02/checkpoints/guest-need-normalization-r295-r298/p23-02-guest-need-normalization-local-r296/input.json.gz new file mode 100644 index 0000000..58959e3 Binary files /dev/null and b/docs/plans/original-completion-2026-10-02/checkpoints/guest-need-normalization-r295-r298/p23-02-guest-need-normalization-local-r296/input.json.gz differ diff --git a/docs/plans/original-completion-2026-10-02/checkpoints/guest-need-normalization-r295-r298/p23-02-guest-need-normalization-local-r296/local.gd b/docs/plans/original-completion-2026-10-02/checkpoints/guest-need-normalization-r295-r298/p23-02-guest-need-normalization-local-r296/local.gd new file mode 100644 index 0000000..499e9a1 --- /dev/null +++ b/docs/plans/original-completion-2026-10-02/checkpoints/guest-need-normalization-r295-r298/p23-02-guest-need-normalization-local-r296/local.gd @@ -0,0 +1,87 @@ +extends "/Users/korizatou114/Projects/\u3068\u308a\u306b\u304f\u5de5\u623f\u30b2\u30fc\u30e0\u5236\u4f5c\u90e8/artifacts/original-completion-2026-10-02/p23-02-build-preview-monitor-compressed-r06.gd" +const Original = preload("/Users/korizatou114/Projects/\u3068\u308a\u306b\u304f\u5de5\u623f\u30b2\u30fc\u30e0\u5236\u4f5c\u90e8/artifacts/original-completion-2026-10-02/p23-02-guest-need-normalization-local-r296/original_guest_service.gd") +const Candidate = preload("res://scripts/guest/guest_service.gd") +const CustomDefinition = preload("/Users/korizatou114/Projects/\u3068\u308a\u306b\u304f\u5de5\u623f\u30b2\u30fc\u30e0\u5236\u4f5c\u90e8/artifacts/original-completion-2026-10-02/p23-02-guest-need-normalization-local-r296/custom_definition.gd") +func setup(service: Variant) -> Variant: + var definition: Variant = service.create_definition(&"test", {&"preference_weights": {&"dining": 3}, &"risk_tolerance": 40}) + check(definition != null, "definition registered") + var guest: Variant = service.spawn_guest(&"g", &"test", 0, {&"wallet_gold": 120, &"credit_limit_gold": 40}) + check(guest != null, "guest registered") + return guest +func run() -> void: + output = OS.get_environment("THE_TREE_RUNTIME_MONITOR_OUTPUT") + if not output.is_absolute_path(): quit(98); return + var original: Variant = Original.new() + var candidate: Variant = Candidate.new() + var og: Variant = setup(original) + var cg: Variant = setup(candidate) + var fixtures: Array = [] + var needs: Array[StringName] = [&"hunger", &"sleep", &"hygiene", &"medical", &"leisure", &"intimacy"] + var inputs: Array[Dictionary] = [] + for magnitude: int in [-9223372036854775807 - 1, -1, 0, 1, 99, 100, 101, 9223372036854775807]: + for selected: int in range(7): + for string_keys: bool in [false, true]: + for readonly: bool in [false, true]: + var relief: Dictionary = {} + for ni: int in range(6): + if selected == 6 or ni == selected: relief[String(needs[ni]) if string_keys else needs[ni]] = magnitude + if readonly: relief.make_read_only() + inputs.append({&"facility_id": &"f", &"facility_tags": [&"dining"], &"need_relief": relief, &"expected_quality": 80, &"estimated_total_gold": 15, &"travel_minutes": 2, &"expected_wait": 3}) + for extra: Dictionary in [{&"need_relief": 45}, {&"need_relief": null}, {&"need_relief": {&"unknown": 1}}, {&"need_effects": {&"hunger": 2.5}}, {&"expected_quality": 12.5}, {&"preference": 7, &"urgency": 9}, {&"instance_id": "fallback", &"facility_id": "", &"tags": ["dining"]}]: + var row: Dictionary = {&"facility_id": &"f", &"need_relief": {&"hunger": 80}} + row.merge(extra, true); inputs.append(row) + for index: int in range(inputs.size()): + var left: Dictionary = inputs[index].duplicate(true) + var right: Dictionary = inputs[index].duplicate(true) + if index < 224: + if index % 2 == 1: + left[&"need_relief"].make_read_only(); right[&"need_relief"].make_read_only() + check(left[&"need_relief"].is_read_only() == (index % 2 == 1) and right[&"need_relief"].is_read_only() == (index % 2 == 1), "actual nested readonly shape " + str(index)) + if index % 8 == 7: + left.make_read_only(); right.make_read_only() + var lb: PackedByteArray = var_to_bytes(left) + var rb: PackedByteArray = var_to_bytes(right) + var expected: Dictionary = original.evaluate_candidate(&"g", left) + var actual: Dictionary = candidate.evaluate_candidate(&"g", right) + check(var_to_bytes(expected) == var_to_bytes(actual), "full ordered evaluate " + str(index)) + check(original.last_error == candidate.last_error, "error observation " + str(index)) + check(var_to_bytes(left) == lb and var_to_bytes(right) == rb, "input unchanged " + str(index)) + check(var_to_bytes(original.create_snapshot()) == var_to_bytes(candidate.create_snapshot()), "full service state " + str(index)) + if index < 224: check(candidate._can_share_candidate_need_map(cg, candidate.get_definition(&"test"), right), "exact builtin shared guard " + str(index)) + check(not is_same(actual, right) and not is_same(actual, expected), "fresh returned dictionary " + str(index)) + fixtures.append({"input": right, "expected": expected, "actual": actual}) + var ranking: Array = inputs.slice(0, 20) + check(var_to_bytes(original.rank_candidates(&"g", ranking)) == var_to_bytes(candidate.rank_candidates(&"g", ranking)), "full ranked ties/order") + for explicit: bool in [false, true]: + var osvc: Variant = Original.new(); var csvc: Variant = Candidate.new() + var od: Variant = CustomDefinition.new().configure(&"callback", {}) + var cd: Variant = CustomDefinition.new().configure(&"callback", {}) + check(osvc.register_definition(od) and csvc.register_definition(cd), "custom definitions registered") + var oguest: Variant = osvc.spawn_guest(&"g", &"callback", 0) + var cguest: Variant = csvc.spawn_guest(&"g", &"callback", 0) + var left: Dictionary = {&"facility_id": &"f", &"need_relief": {&"hunger": 10}} + if explicit: left[&"preference"] = 7 + var right: Dictionary = left.duplicate(true) + od.watched_guest = oguest; od.watched_candidate = left + cd.watched_guest = cguest; cd.watched_candidate = right + check(not csvc._can_share_candidate_need_map(cguest, cd, right), "custom preference fallback") + var expected: Dictionary = osvc.evaluate_candidate(&"g", left) + var actual: Dictionary = csvc.evaluate_candidate(&"g", right) + check(var_to_bytes(expected) == var_to_bytes(actual), "callback full ordered result") + check(od.calls == 1 and cd.calls == 1, "original eager default callback position") + check(var_to_bytes(left) == var_to_bytes(right) and var_to_bytes(osvc.create_snapshot()) == var_to_bytes(csvc.create_snapshot()), "callback input/full state equality") + fixtures.append({"callback_explicit": explicit, "expected": expected, "actual": actual}) + var scalar: Dictionary = inputs[50].duplicate(true) + var times: Array[Dictionary] = [] + for mode: String in ["original", "candidate"]: + var service: Variant = original if mode == "original" else candidate + var start: int = Time.get_ticks_usec() + for index: int in range(2000): service.evaluate_candidate(&"g", scalar) + times.append({"mode": mode, "calls": 2000, "elapsed_us": Time.get_ticks_usec() - start}) + var bytes_pin: Dictionary = typed("evaluation-fixtures", fixtures) + var report: Dictionary = {"checks": checks, "failures": failures, "passed": failures.is_empty(), "pid": OS.get_process_id(), "cases": inputs.size(), "callback_cases": 2, "fixtures": bytes_pin, "timings": times, "unit_accepted": false, "performance_accepted": false, "candidate_adopted": false, "limits": ["One process finite builtin and custom-preference observations only. Adjacent helper microbenchmark is not representative whole-tick, stable gain, P95/D10/50ms, full regression, OS or Windows acceptance."]} + var file: FileAccess = FileAccess.open(output.path_join("report.json"), FileAccess.WRITE) + if file == null: quit(98); return + file.store_string(JSON.stringify(report, "\t")); file.close() + print("Guest Need normalization local: %d checks, %d failures" % [checks, failures.size()]) + quit(0 if failures.is_empty() else 1) diff --git a/docs/plans/original-completion-2026-10-02/checkpoints/guest-need-normalization-r295-r298/p23-02-guest-need-normalization-local-r296/measurement/evaluation-fixtures.varbytes.zst b/docs/plans/original-completion-2026-10-02/checkpoints/guest-need-normalization-r295-r298/p23-02-guest-need-normalization-local-r296/measurement/evaluation-fixtures.varbytes.zst new file mode 100644 index 0000000..2a9ce48 Binary files /dev/null and b/docs/plans/original-completion-2026-10-02/checkpoints/guest-need-normalization-r295-r298/p23-02-guest-need-normalization-local-r296/measurement/evaluation-fixtures.varbytes.zst differ diff --git a/docs/plans/original-completion-2026-10-02/checkpoints/guest-need-normalization-r295-r298/p23-02-guest-need-normalization-local-r296/measurement/report.json b/docs/plans/original-completion-2026-10-02/checkpoints/guest-need-normalization-r295-r298/p23-02-guest-need-normalization-local-r296/measurement/report.json new file mode 100644 index 0000000..e74660c --- /dev/null +++ b/docs/plans/original-completion-2026-10-02/checkpoints/guest-need-normalization-r295-r298/p23-02-guest-need-normalization-local-r296/measurement/report.json @@ -0,0 +1,37 @@ +{ + "callback_cases": 2, + "candidate_adopted": false, + "cases": 231, + "checks": 1619, + "failures": [], + "fixtures": { + "bytes": 3540, + "codec": "zstd", + "field_deletions": 0, + "file_envelope": false, + "full_bytes_roundtrip": true, + "path": "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/artifacts/original-completion-2026-10-02/p23-02-guest-need-normalization-local-r296/measurement/evaluation-fixtures.varbytes.zst", + "sha256": "c21dad1a71f30a2c8d4cab558bbfb4543f2daca4487b1fbdbcbb1b1a97b9143d", + "uncompressed_bytes": 408484, + "uncompressed_sha256": "ae97195121e2def68615caf2b0cb3bda59a130097ef34cb9656c75ac387b7343" + }, + "limits": [ + "One process finite builtin and custom-preference observations only. Adjacent helper microbenchmark is not representative whole-tick, stable gain, P95/D10/50ms, full regression, OS or Windows acceptance." + ], + "passed": true, + "performance_accepted": false, + "pid": 44204, + "timings": [ + { + "calls": 2000, + "elapsed_us": 28461, + "mode": "original" + }, + { + "calls": 2000, + "elapsed_us": 41815, + "mode": "candidate" + } + ], + "unit_accepted": false +} \ No newline at end of file diff --git a/docs/plans/original-completion-2026-10-02/checkpoints/guest-need-normalization-r295-r298/p23-02-guest-need-normalization-local-r296/original_guest_service.gd b/docs/plans/original-completion-2026-10-02/checkpoints/guest-need-normalization-r295-r298/p23-02-guest-need-normalization-local-r296/original_guest_service.gd new file mode 100644 index 0000000..37c8b0b --- /dev/null +++ b/docs/plans/original-completion-2026-10-02/checkpoints/guest-need-normalization-r295-r298/p23-02-guest-need-normalization-local-r296/original_guest_service.gd @@ -0,0 +1,1688 @@ +class_name GuestServiceOriginalR296 +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) + 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/guest-need-normalization-r295-r298/p23-02-guest-need-normalization-static-independent-review-r295.json b/docs/plans/original-completion-2026-10-02/checkpoints/guest-need-normalization-r295-r298/p23-02-guest-need-normalization-static-independent-review-r295.json new file mode 100644 index 0000000..9cabae4 --- /dev/null +++ b/docs/plans/original-completion-2026-10-02/checkpoints/guest-need-normalization-r295-r298/p23-02-guest-need-normalization-static-independent-review-r295.json @@ -0,0 +1,106 @@ +{ + "review_id": "p23-02-guest-need-normalization-static-independent-review-r295", + "implementation_supported": true, + "static_accepted": true, + "build_allowed": false, + "execution_allowed": false, + "blockers": [], + "evidence_sha256": { + "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/artifacts/original-completion-2026-10-02/p23-02-native-reconstruction-local-r209/stage/scripts/guest/guest_service.gd": { + "path": "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/artifacts/original-completion-2026-10-02/p23-02-native-reconstruction-local-r209/stage/scripts/guest/guest_service.gd", + "bytes": 61414, + "sha256": "56880410e9a00816f9a95d30821982955532de8dd976980934e12016a9f2b21e" + }, + "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/artifacts/original-completion-2026-10-02/p23-02-native-pure-tag-projection-candidate-r274/input.json": { + "path": "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/artifacts/original-completion-2026-10-02/p23-02-native-pure-tag-projection-candidate-r274/input.json", + "bytes": 4511, + "sha256": "6bd46c589bc57ab5afad1d7195f7293ec450aafeed055e394a3a880744666ca3" + }, + "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/artifacts/original-completion-2026-10-02/p23-02-native-pure-tag-projection-candidate-r274/source/tree_numeric_core.cpp": { + "path": "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/artifacts/original-completion-2026-10-02/p23-02-native-pure-tag-projection-candidate-r274/source/tree_numeric_core.cpp", + "bytes": 54002, + "sha256": "ce20a90adb8a410dc1b6cbf607b7bfa8bf21eaef158c5c6bfc7a9bbcc703d2b2" + }, + "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/artifacts/original-completion-2026-10-02/p23-02-native-integer-window-codec-impact-whole-r293-r04/execution.json": { + "path": "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/artifacts/original-completion-2026-10-02/p23-02-native-integer-window-codec-impact-whole-r293-r04/execution.json", + "bytes": 33451, + "sha256": "44d6c26b797a99e7729d1eb77e725330b2b95bd643876eb41cd11237f2488848" + }, + "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/artifacts/original-completion-2026-10-02/p23-02-current-residual-diagnostic-actual-independent-review-r238.json": { + "path": "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/artifacts/original-completion-2026-10-02/p23-02-current-residual-diagnostic-actual-independent-review-r238.json", + "bytes": 36734, + "sha256": "e41634755f116006c91ad251b48eb8b71286a53a6a687d6f371366f3140cc66d" + }, + "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/scripts/guest/guest_definition.gd": { + "path": "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/scripts/guest/guest_definition.gd", + "bytes": 9374, + "sha256": "082aa3f33b9cb17daf10a2cf8a1dece7b79dd80c2ae21e1a424d1b2ceb52b918" + }, + "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/scripts/guest/guest_runtime.gd": { + "path": "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/scripts/guest/guest_runtime.gd", + "bytes": 14267, + "sha256": "57ba99e5d977b5302987f4359a19056f1ab09c8fb7e4762ee8d79cbb9316741a" + }, + "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/artifacts/original-completion-2026-10-02/p23-02-guest-need-normalization-candidate-r295/source/guest_service.gd": { + "path": "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/artifacts/original-completion-2026-10-02/p23-02-guest-need-normalization-candidate-r295/source/guest_service.gd", + "bytes": 64563, + "sha256": "2794f20630168d902e6a7fb55a0b5aacfbbcfcccd51d336f0724f03ee35ea249" + }, + "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/artifacts/original-completion-2026-10-02/p23-02-guest-need-normalization-candidate-r295/input.json": { + "path": "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/artifacts/original-completion-2026-10-02/p23-02-guest-need-normalization-candidate-r295/input.json", + "bytes": 4366, + "sha256": "f592b9895d9253be9e7f829cd88888fe166e77e00959570f7d4a7f0c3c169489" + } + }, + "canonical_doc4_context_pins": { + "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/docs/plans/original-completion-2026-10-02/IMPLEMENTATION_PLAN.md": { + "path": "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/docs/plans/original-completion-2026-10-02/IMPLEMENTATION_PLAN.md", + "bytes": 492299, + "sha256": "61776f0673770277ab6d182107337ed40a108ff3b5ab2c1a8f29fc4f2c3015ad" + }, + "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/docs/plans/original-completion-2026-10-02/PROGRESS_INTEGRATION.md": { + "path": "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/docs/plans/original-completion-2026-10-02/PROGRESS_INTEGRATION.md", + "bytes": 410363, + "sha256": "8b0488910dba35b34da96d7062b9a69d293e70a1963c9c5890ec32b2891baaea" + }, + "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/docs/plans/original-completion-2026-10-02/progress_snapshot.json": { + "path": "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/docs/plans/original-completion-2026-10-02/progress_snapshot.json", + "bytes": 539512, + "sha256": "b417ee1b945dfe4666f3a2f3501f906756826ea88d741bb116f3dd20250c8958" + }, + "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/docs/plans/original-completion-2026-10-02/work_packages.json": { + "path": "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/docs/plans/original-completion-2026-10-02/work_packages.json", + "bytes": 1184337, + "sha256": "32d80e911910bc762287dc3ddf0ee0ea3072ca2f0158aa2f9f355b9202cfdfeb" + } + }, + "verified_scope": [ + "Entire original GuestService is recovered byte-for-byte by removing appended four helpers and reversing the three evaluation replacements.", + "Original candidate validation remains before the guard. Integer relief, absent relief, custom GuestService/GuestRuntime/GuestDefinition, unsafe candidate conversions and noninteger preference weights retain original helper paths.", + "For exact builtin, valid Dictionary relief and listed primitive inputs, eager preference/default helpers between relief/urgency/priority are synchronous primitive operations with no signal, await, Callable or external Object callback. No assumption that explicit preference/urgency skips evaluation of its default expression.", + "All three normalized helper arithmetic/loop bodies are byte-identical to originals after normalization, preserving NEED order, max, division and signed INT expressions.", + "Original utility terms insertion order, weights, result deep duplication, price/preference/helper calls and public result assembly are unchanged. Shared normalized Dictionary is invocation-local with no retained cache or ownership escape." + ], + "cost_and_limitations": [ + "Guard adds script/load identity checks, six needs reads, scans of 17 optional integer fields, id/tag element checks and all preference_weights values; it allocates values()/literal arrays and a normalized Dictionary. No net speed benefit is established.", + "GDScript was reviewed textually only; Variant shared_need_map is passed to Dictionary parameters only on non-null validated-normalization path. Actual parser/type checking and runtime behavior remain unverified.", + "Exact-resource identity assumes authenticated builtin resources, not malicious same-path replacement, arbitrary concurrent mutation, or subclass collision/override of newly introduced private helper names.", + "Guard prevents builtin preference conversions from invoking custom conversion behavior; custom definition.preference_for remains original fallback and must be tested with real script/input mutation.", + "INT extrema preserve original arithmetic text, but actual overflow/division behavior and all typed/read-only/string-key input combinations require finite local tests.", + "Static support does not prove native load, local/whole equality, performance/P95/D10/50ms, full172, OS/Windows, adoption or unit completion." + ], + "required_next": [ + "Parse candidate in isolated controlled stage; do not edit production.", + "Compare original versus candidate evaluate/rank/decision full typed results and public state for empty/partial/all six canonical Need maps, String/StringName keys, integer scalar and absent relief, invalid maps, readonly/typed dictionaries and INT boundaries.", + "Exercise custom service/guest/definition and preference callback changing relief, needs or script; require original fallback observation order.", + "Verify caller mutation isolation and utility_terms/deepdup ownership, then necessary finite impact/whole and qualified performance evidence before any adoption." + ], + "reviewer_execution": { + "engine": 0, + "compiler": 0, + "tests": 0, + "validator": 0, + "existing_edits": 0, + "new_agents": 0 + } +} diff --git a/docs/plans/original-completion-2026-10-02/checkpoints/guest-need-normalization-r295-r298/p23-02-guest-need-normalization-static-independent-review-r298.json b/docs/plans/original-completion-2026-10-02/checkpoints/guest-need-normalization-r295-r298/p23-02-guest-need-normalization-static-independent-review-r298.json new file mode 100644 index 0000000..5929ba1 --- /dev/null +++ b/docs/plans/original-completion-2026-10-02/checkpoints/guest-need-normalization-r295-r298/p23-02-guest-need-normalization-static-independent-review-r298.json @@ -0,0 +1,185 @@ +{ + "review_id": "p23-02-guest-need-normalization-static-independent-review-r298", + "implementation_supported": true, + "static_accepted": true, + "execution_allowed": false, + "build_allowed": false, + "blockers": [], + "evidence_sha256": { + "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/artifacts/original-completion-2026-10-02/p23-02-guest-need-normalization-candidate-r295/source/guest_service.gd": { + "path": "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/artifacts/original-completion-2026-10-02/p23-02-guest-need-normalization-candidate-r295/source/guest_service.gd", + "bytes": 64563, + "sha256": "2794f20630168d902e6a7fb55a0b5aacfbbcfcccd51d336f0724f03ee35ea249" + }, + "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/artifacts/original-completion-2026-10-02/p23-02-guest-need-normalization-candidate-r298/source/guest_service.gd": { + "path": "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/artifacts/original-completion-2026-10-02/p23-02-guest-need-normalization-candidate-r298/source/guest_service.gd", + "bytes": 63639, + "sha256": "5895d5da6488c767685b109ef01f9e2b57f41272d49cc9e3c415865769adc11d" + }, + "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/artifacts/original-completion-2026-10-02/p23-02-guest-need-normalization-static-independent-review-r295.json": { + "path": "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/artifacts/original-completion-2026-10-02/p23-02-guest-need-normalization-static-independent-review-r295.json", + "bytes": 9244, + "sha256": "0954f512e6065c26ffc7d06c087fe973379a48c9cebef6752f19ec52d24f59d8" + }, + "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/artifacts/original-completion-2026-10-02/p23-02-guest-need-normalization-local-actual-independent-review-r296.json": { + "path": "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/artifacts/original-completion-2026-10-02/p23-02-guest-need-normalization-local-actual-independent-review-r296.json", + "bytes": 19007, + "sha256": "ce06bfe9156ed8e1c9ec53519d6cc52b26c963ca0e8b4a263ff70d1a1abf9a23" + }, + "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/artifacts/original-completion-2026-10-02/p23-02-guest-need-normalization-local-r296/actual-root-verification.json": { + "path": "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/artifacts/original-completion-2026-10-02/p23-02-guest-need-normalization-local-r296/actual-root-verification.json", + "bytes": 10454, + "sha256": "6d9dc235ac63d6629e4375e5b75516f522f47d4d491b40cda790da14fa65f4fe" + }, + "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/artifacts/original-completion-2026-10-02/p23-02-guest-need-normalization-local-r296/execution.json": { + "path": "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/artifacts/original-completion-2026-10-02/p23-02-guest-need-normalization-local-r296/execution.json", + "bytes": 10788, + "sha256": "d01a859ce75a18084cbec7a081c906e267d1e8c7a42bce55e2539dd3a7ede8c3" + }, + "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/artifacts/original-completion-2026-10-02/p23-02-native-pure-tag-projection-candidate-r274/input.json": { + "path": "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/artifacts/original-completion-2026-10-02/p23-02-native-pure-tag-projection-candidate-r274/input.json", + "bytes": 4511, + "sha256": "6bd46c589bc57ab5afad1d7195f7293ec450aafeed055e394a3a880744666ca3" + }, + "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/scripts/guest/guest_definition.gd": { + "path": "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/scripts/guest/guest_definition.gd", + "bytes": 9374, + "sha256": "082aa3f33b9cb17daf10a2cf8a1dece7b79dd80c2ae21e1a424d1b2ceb52b918" + }, + "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/scripts/guest/guest_runtime.gd": { + "path": "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/scripts/guest/guest_runtime.gd", + "bytes": 14267, + "sha256": "57ba99e5d977b5302987f4359a19056f1ab09c8fb7e4762ee8d79cbb9316741a" + }, + "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/artifacts/original-completion-2026-10-02/p23-02-guest-need-conversion-source-r297/variant.cpp": { + "path": "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/artifacts/original-completion-2026-10-02/p23-02-guest-need-conversion-source-r297/variant.cpp", + "bytes": 94629, + "sha256": "4af0a620aa59d2376d1b35ab201f26edd352f056c908b970890ad9a3449bb5c0" + }, + "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/artifacts/original-completion-2026-10-02/p23-02-guest-need-conversion-source-r297/variant.cpp.source.json": { + "path": "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/artifacts/original-completion-2026-10-02/p23-02-guest-need-conversion-source-r297/variant.cpp.source.json", + "bytes": 278, + "sha256": "dfce2e51d892ac01b198055551cc899c06aa1b9962f20bca687dbb1c31e2bd9d" + }, + "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/artifacts/original-completion-2026-10-02/p23-02-guest-need-conversion-source-r297/variant.h": { + "path": "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/artifacts/original-completion-2026-10-02/p23-02-guest-need-conversion-source-r297/variant.h", + "bytes": 34956, + "sha256": "3d7d7c8bdc02d4ac73d06b16f487a9ba0f9a188c854ab0befc57b98f807523b2" + }, + "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/artifacts/original-completion-2026-10-02/p23-02-guest-need-conversion-source-r297/variant.h.source.json": { + "path": "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/artifacts/original-completion-2026-10-02/p23-02-guest-need-conversion-source-r297/variant.h.source.json", + "bytes": 276, + "sha256": "42d0b1ef9b9f5329029e4250cc97533bd800ccfea2fa18d6586c21f7973009af" + }, + "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/artifacts/original-completion-2026-10-02/p23-02-guest-need-conversion-source-r297/variant_construct.cpp": { + "path": "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/artifacts/original-completion-2026-10-02/p23-02-guest-need-conversion-source-r297/variant_construct.cpp", + "bytes": 19030, + "sha256": "51e8ebbbffb9c65959aa4c86f7dd1a21ed121ef82706996893743de077c82645" + }, + "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/artifacts/original-completion-2026-10-02/p23-02-guest-need-conversion-source-r297/variant_construct.cpp.source.json": { + "path": "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/artifacts/original-completion-2026-10-02/p23-02-guest-need-conversion-source-r297/variant_construct.cpp.source.json", + "bytes": 288, + "sha256": "7aed774b87af26bf775abeaa63a22df93cb1552398cc52ca3230800b1abdd555" + }, + "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/artifacts/original-completion-2026-10-02/p23-02-guest-need-conversion-source-r297/variant_construct.h": { + "path": "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/artifacts/original-completion-2026-10-02/p23-02-guest-need-conversion-source-r297/variant_construct.h", + "bytes": 25252, + "sha256": "e6aecba36f8ff90af382427fb6be8b69e161583cffba7751b2c2ffdfa5e632c9" + }, + "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/artifacts/original-completion-2026-10-02/p23-02-guest-need-conversion-source-r297/variant_construct.h.source.json": { + "path": "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/artifacts/original-completion-2026-10-02/p23-02-guest-need-conversion-source-r297/variant_construct.h.source.json", + "bytes": 286, + "sha256": "e7c85dca32c621dd842706fe0851e74a63e288848b4055ca895d657059bcc312" + }, + "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/artifacts/original-completion-2026-10-02/p23-02-guest-need-conversion-source-r297/dictionary.cpp": { + "path": "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/artifacts/original-completion-2026-10-02/p23-02-guest-need-conversion-source-r297/dictionary.cpp", + "bytes": 27721, + "sha256": "13eb030c2e59ac0e7060852949bf5b7a62624e31474b781d89e72515410daa7d" + }, + "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/artifacts/original-completion-2026-10-02/p23-02-guest-need-conversion-source-r297/dictionary.cpp.source.json": { + "path": "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/artifacts/original-completion-2026-10-02/p23-02-guest-need-conversion-source-r297/dictionary.cpp.source.json", + "bytes": 281, + "sha256": "032aaf8bb193500745770c940088a2223c1f3f6bb9f8edf6300693bb3e26fc7b" + }, + "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/artifacts/original-completion-2026-10-02/p23-02-guest-need-conversion-source-r297/variant_setget.cpp": { + "path": "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/artifacts/original-completion-2026-10-02/p23-02-guest-need-conversion-source-r297/variant_setget.cpp", + "bytes": 62857, + "sha256": "e3f73e1df096bedd4856e29227b302f879d0520c66718467338556b589f685c9" + }, + "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/artifacts/original-completion-2026-10-02/p23-02-guest-need-conversion-source-r297/variant_setget.cpp.source.json": { + "path": "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/artifacts/original-completion-2026-10-02/p23-02-guest-need-conversion-source-r297/variant_setget.cpp.source.json", + "bytes": 276, + "sha256": "f3002c603869280c99e5efc5f1e923258726c48c404571af34ba6f3bdc009cd2" + }, + "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/artifacts/original-completion-2026-10-02/p23-02-guest-need-normalization-candidate-r298/input.json": { + "path": "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/artifacts/original-completion-2026-10-02/p23-02-guest-need-normalization-candidate-r298/input.json", + "bytes": 11472, + "sha256": "3e2eaa147e365a74145224d8995bade5b44528d59b5508dc159458555081ed99" + }, + "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/artifacts/original-completion-2026-10-02/p23-02-guest-need-conversion-source-r297/variant_deep_duplicate.h": { + "path": "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/artifacts/original-completion-2026-10-02/p23-02-guest-need-conversion-source-r297/variant_deep_duplicate.h", + "bytes": 2592, + "sha256": "e43e4ee411e969ea4b584ac330394a452aabca90780087f8d5876167fc71ac4e" + }, + "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/artifacts/original-completion-2026-10-02/p23-02-guest-need-conversion-source-r297/variant_deep_duplicate.h.source.json": { + "path": "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/artifacts/original-completion-2026-10-02/p23-02-guest-need-conversion-source-r297/variant_deep_duplicate.h.source.json", + "bytes": 281, + "sha256": "5a0ccfd31fc7f3ad6349b414a42a0a9e3277b061ee613e98b39e2bd24c3988ba" + }, + "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/artifacts/original-completion-2026-10-02/p23-02-integer-window-codec-array-source-r289/array.cpp": { + "path": "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/artifacts/original-completion-2026-10-02/p23-02-integer-window-codec-array-source-r289/array.cpp", + "bytes": 27906, + "sha256": "ad9ebc6db6a962c817e2dbcee7f9c529dccd042090137e00dcf1a0594696f70b" + }, + "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/artifacts/original-completion-2026-10-02/p23-02-integer-window-codec-array-source-r289/source.json": { + "path": "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/artifacts/original-completion-2026-10-02/p23-02-integer-window-codec-array-source-r289/source.json", + "bytes": 291, + "sha256": "cd89af6dd2258fe71036edf9f92ce6302d823b68c257405e2a56795726e87834" + } + }, + "canonical_doc4_context_pins": { + "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/docs/plans/original-completion-2026-10-02/IMPLEMENTATION_PLAN.md": { + "path": "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/docs/plans/original-completion-2026-10-02/IMPLEMENTATION_PLAN.md", + "bytes": 492299, + "sha256": "61776f0673770277ab6d182107337ed40a108ff3b5ab2c1a8f29fc4f2c3015ad" + }, + "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/docs/plans/original-completion-2026-10-02/PROGRESS_INTEGRATION.md": { + "path": "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/docs/plans/original-completion-2026-10-02/PROGRESS_INTEGRATION.md", + "bytes": 410363, + "sha256": "8b0488910dba35b34da96d7062b9a69d293e70a1963c9c5890ec32b2891baaea" + }, + "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/docs/plans/original-completion-2026-10-02/work_packages.json": { + "path": "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/docs/plans/original-completion-2026-10-02/work_packages.json", + "bytes": 1184337, + "sha256": "32d80e911910bc762287dc3ddf0ee0ea3072ca2f0158aa2f9f355b9202cfdfeb" + }, + "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/docs/plans/original-completion-2026-10-02/progress_snapshot.json": { + "path": "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/docs/plans/original-completion-2026-10-02/progress_snapshot.json", + "bytes": 539512, + "sha256": "b417ee1b945dfe4666f3a2f3501f906756826ea88d741bb116f3dd20250c8958" + } + }, + "verified_scope": [ + "Only _can_share_candidate_need_map differs; replacing its complete body with r295 recovers r295 byte-for-byte. All normalization, three helper loops, evaluation arithmetic, call order, deepdup and original fallback text are unchanged.", + "Same fixed ed1daf0bf primary source: _to_int is a scalar switch with no Object/user dispatch; int constructors accept numeric/bool/string paths and StringName constructors accept String/StringName through strict constructor matching, not arbitrary Object stringify. Invalid conversions may error and remain a required local boundary, not callback evidence.", + "Exact builtin preference_for/_sorted_unique_names retain only name conversion, Array iteration/has and fixed pure sort comparison, integer dictionary weights. Builtin price/facility helpers similarly use fixed conversions/arithmetic/dictionary lookup; no signal/Callable/await or external Object invocation lies between three Need reads.", + "Dictionary.duplicate(true) explicitly selects RESOURCE_DEEP_DUPLICATE_NONE. Nested Dictionary and Array recursion propagate mode and positive recursion_count. Variant OBJECT branch returns the existing object before Resource::_duplicate_from_variant when mode NONE and recursion_count>0. No nested Resource getter/script duplicate hook is invoked on that path.", + "Three exact Script identities and Dictionary relief remain mandatory. Existing validation rejects noncanonical/noninteger Need maps before normalization. Custom service/guest/definition uses original fallback, including definition preference callbacks and eager default evaluation." + ], + "cost_and_remaining": [ + "Removed six Need/17 scalar/id/tag/preference-weight scans; retained resource/script identity checks and one Dictionary normalization have unmeasured cost. No speed benefit is supported.", + "Primary source review supports synchronous conversion/duplication purity; it does not assert permissive conversion of every Object/malformed type, exact diagnostic-stack equality, NaN/Inf overflow behavior, arbitrary concurrent mutation or malicious same-path script replacement.", + "Expanded guard admits inputs rejected by r295 sharing guard, including convertible scalar/tag/weight/need values and Object-bearing extra fields; r296 local evidence does not validate r298 expanded guard, parser behavior or new ownership/error boundaries.", + "Required isolated finite local comparison: bool/float/string/StringName conversions, invalid Object constructors, nested Resource/Object extras with callback counters, typed/readonly dictionaries/arrays, INT extrema, custom guest/service/definition and explicit-preference mutations. Compare ordered evaluate/rank/decision results, error observations, all public state/input and returned ownership.", + "Then necessary impact/whole and qualified performance validation; whole647/full172/P95D10/50ms/normal OS/Windows/adoption/unit acceptance remain false." + ], + "reviewer_execution": { + "engine": 0, + "compiler": 0, + "tests": 0, + "validator": 0, + "author": 0, + "existing_edits": 0, + "new_agents": 0 + } +} diff --git a/docs/plans/original-completion-2026-10-02/checkpoints/guest-need-normalization-r295-r298/p23-02-guest-need-normalization-static-root-verification-r295.json b/docs/plans/original-completion-2026-10-02/checkpoints/guest-need-normalization-r295-r298/p23-02-guest-need-normalization-static-root-verification-r295.json new file mode 100644 index 0000000..88beefa --- /dev/null +++ b/docs/plans/original-completion-2026-10-02/checkpoints/guest-need-normalization-r295-r298/p23-02-guest-need-normalization-static-root-verification-r295.json @@ -0,0 +1,73 @@ +{ + "passed": true, + "review_sha256": "0954f512e6065c26ffc7d06c087fe973379a48c9cebef6752f19ec52d24f59d8", + "verified_entries": [ + { + "path": "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/artifacts/original-completion-2026-10-02/p23-02-native-reconstruction-local-r209/stage/scripts/guest/guest_service.gd", + "bytes": 61414, + "sha256": "56880410e9a00816f9a95d30821982955532de8dd976980934e12016a9f2b21e" + }, + { + "path": "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/artifacts/original-completion-2026-10-02/p23-02-native-pure-tag-projection-candidate-r274/input.json", + "bytes": 4511, + "sha256": "6bd46c589bc57ab5afad1d7195f7293ec450aafeed055e394a3a880744666ca3" + }, + { + "path": "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/artifacts/original-completion-2026-10-02/p23-02-native-pure-tag-projection-candidate-r274/source/tree_numeric_core.cpp", + "bytes": 54002, + "sha256": "ce20a90adb8a410dc1b6cbf607b7bfa8bf21eaef158c5c6bfc7a9bbcc703d2b2" + }, + { + "path": "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/artifacts/original-completion-2026-10-02/p23-02-native-integer-window-codec-impact-whole-r293-r04/execution.json", + "bytes": 33451, + "sha256": "44d6c26b797a99e7729d1eb77e725330b2b95bd643876eb41cd11237f2488848" + }, + { + "path": "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/artifacts/original-completion-2026-10-02/p23-02-current-residual-diagnostic-actual-independent-review-r238.json", + "bytes": 36734, + "sha256": "e41634755f116006c91ad251b48eb8b71286a53a6a687d6f371366f3140cc66d" + }, + { + "path": "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/scripts/guest/guest_definition.gd", + "bytes": 9374, + "sha256": "082aa3f33b9cb17daf10a2cf8a1dece7b79dd80c2ae21e1a424d1b2ceb52b918" + }, + { + "path": "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/scripts/guest/guest_runtime.gd", + "bytes": 14267, + "sha256": "57ba99e5d977b5302987f4359a19056f1ab09c8fb7e4762ee8d79cbb9316741a" + }, + { + "path": "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/artifacts/original-completion-2026-10-02/p23-02-guest-need-normalization-candidate-r295/source/guest_service.gd", + "bytes": 64563, + "sha256": "2794f20630168d902e6a7fb55a0b5aacfbbcfcccd51d336f0724f03ee35ea249" + }, + { + "path": "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/artifacts/original-completion-2026-10-02/p23-02-guest-need-normalization-candidate-r295/input.json", + "bytes": 4366, + "sha256": "f592b9895d9253be9e7f829cd88888fe166e77e00959570f7d4a7f0c3c169489" + }, + { + "path": "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/docs/plans/original-completion-2026-10-02/IMPLEMENTATION_PLAN.md", + "bytes": 492299, + "sha256": "61776f0673770277ab6d182107337ed40a108ff3b5ab2c1a8f29fc4f2c3015ad" + }, + { + "path": "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/docs/plans/original-completion-2026-10-02/PROGRESS_INTEGRATION.md", + "bytes": 410363, + "sha256": "8b0488910dba35b34da96d7062b9a69d293e70a1963c9c5890ec32b2891baaea" + }, + { + "path": "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/docs/plans/original-completion-2026-10-02/progress_snapshot.json", + "bytes": 539512, + "sha256": "b417ee1b945dfe4666f3a2f3501f906756826ea88d741bb116f3dd20250c8958" + }, + { + "path": "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/docs/plans/original-completion-2026-10-02/work_packages.json", + "bytes": 1184337, + "sha256": "32d80e911910bc762287dc3ddf0ee0ea3072ca2f0158aa2f9f355b9202cfdfeb" + } + ], + "unit_accepted": false, + "performance_accepted": false +} diff --git a/docs/plans/original-completion-2026-10-02/checkpoints/guest-need-normalization-r295-r298/p23-02-guest-need-normalization-static-root-verification-r298.json b/docs/plans/original-completion-2026-10-02/checkpoints/guest-need-normalization-r295-r298/p23-02-guest-need-normalization-static-root-verification-r298.json new file mode 100644 index 0000000..b0b6e99 --- /dev/null +++ b/docs/plans/original-completion-2026-10-02/checkpoints/guest-need-normalization-r295-r298/p23-02-guest-need-normalization-static-root-verification-r298.json @@ -0,0 +1,158 @@ +{ + "passed": true, + "review_sha256": "940dd5f51b55942f07ef7ad091ec557af4d0d7bb07d48c08b6a98bde570f32b2", + "verified_entries": [ + { + "path": "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/artifacts/original-completion-2026-10-02/p23-02-guest-need-normalization-candidate-r295/source/guest_service.gd", + "bytes": 64563, + "sha256": "2794f20630168d902e6a7fb55a0b5aacfbbcfcccd51d336f0724f03ee35ea249" + }, + { + "path": "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/artifacts/original-completion-2026-10-02/p23-02-guest-need-normalization-candidate-r298/source/guest_service.gd", + "bytes": 63639, + "sha256": "5895d5da6488c767685b109ef01f9e2b57f41272d49cc9e3c415865769adc11d" + }, + { + "path": "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/artifacts/original-completion-2026-10-02/p23-02-guest-need-normalization-static-independent-review-r295.json", + "bytes": 9244, + "sha256": "0954f512e6065c26ffc7d06c087fe973379a48c9cebef6752f19ec52d24f59d8" + }, + { + "path": "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/artifacts/original-completion-2026-10-02/p23-02-guest-need-normalization-local-actual-independent-review-r296.json", + "bytes": 19007, + "sha256": "ce06bfe9156ed8e1c9ec53519d6cc52b26c963ca0e8b4a263ff70d1a1abf9a23" + }, + { + "path": "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/artifacts/original-completion-2026-10-02/p23-02-guest-need-normalization-local-r296/actual-root-verification.json", + "bytes": 10454, + "sha256": "6d9dc235ac63d6629e4375e5b75516f522f47d4d491b40cda790da14fa65f4fe" + }, + { + "path": "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/artifacts/original-completion-2026-10-02/p23-02-guest-need-normalization-local-r296/execution.json", + "bytes": 10788, + "sha256": "d01a859ce75a18084cbec7a081c906e267d1e8c7a42bce55e2539dd3a7ede8c3" + }, + { + "path": "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/artifacts/original-completion-2026-10-02/p23-02-native-pure-tag-projection-candidate-r274/input.json", + "bytes": 4511, + "sha256": "6bd46c589bc57ab5afad1d7195f7293ec450aafeed055e394a3a880744666ca3" + }, + { + "path": "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/scripts/guest/guest_definition.gd", + "bytes": 9374, + "sha256": "082aa3f33b9cb17daf10a2cf8a1dece7b79dd80c2ae21e1a424d1b2ceb52b918" + }, + { + "path": "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/scripts/guest/guest_runtime.gd", + "bytes": 14267, + "sha256": "57ba99e5d977b5302987f4359a19056f1ab09c8fb7e4762ee8d79cbb9316741a" + }, + { + "path": "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/artifacts/original-completion-2026-10-02/p23-02-guest-need-conversion-source-r297/variant.cpp", + "bytes": 94629, + "sha256": "4af0a620aa59d2376d1b35ab201f26edd352f056c908b970890ad9a3449bb5c0" + }, + { + "path": "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/artifacts/original-completion-2026-10-02/p23-02-guest-need-conversion-source-r297/variant.cpp.source.json", + "bytes": 278, + "sha256": "dfce2e51d892ac01b198055551cc899c06aa1b9962f20bca687dbb1c31e2bd9d" + }, + { + "path": "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/artifacts/original-completion-2026-10-02/p23-02-guest-need-conversion-source-r297/variant.h", + "bytes": 34956, + "sha256": "3d7d7c8bdc02d4ac73d06b16f487a9ba0f9a188c854ab0befc57b98f807523b2" + }, + { + "path": "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/artifacts/original-completion-2026-10-02/p23-02-guest-need-conversion-source-r297/variant.h.source.json", + "bytes": 276, + "sha256": "42d0b1ef9b9f5329029e4250cc97533bd800ccfea2fa18d6586c21f7973009af" + }, + { + "path": "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/artifacts/original-completion-2026-10-02/p23-02-guest-need-conversion-source-r297/variant_construct.cpp", + "bytes": 19030, + "sha256": "51e8ebbbffb9c65959aa4c86f7dd1a21ed121ef82706996893743de077c82645" + }, + { + "path": "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/artifacts/original-completion-2026-10-02/p23-02-guest-need-conversion-source-r297/variant_construct.cpp.source.json", + "bytes": 288, + "sha256": "7aed774b87af26bf775abeaa63a22df93cb1552398cc52ca3230800b1abdd555" + }, + { + "path": "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/artifacts/original-completion-2026-10-02/p23-02-guest-need-conversion-source-r297/variant_construct.h", + "bytes": 25252, + "sha256": "e6aecba36f8ff90af382427fb6be8b69e161583cffba7751b2c2ffdfa5e632c9" + }, + { + "path": "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/artifacts/original-completion-2026-10-02/p23-02-guest-need-conversion-source-r297/variant_construct.h.source.json", + "bytes": 286, + "sha256": "e7c85dca32c621dd842706fe0851e74a63e288848b4055ca895d657059bcc312" + }, + { + "path": "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/artifacts/original-completion-2026-10-02/p23-02-guest-need-conversion-source-r297/dictionary.cpp", + "bytes": 27721, + "sha256": "13eb030c2e59ac0e7060852949bf5b7a62624e31474b781d89e72515410daa7d" + }, + { + "path": "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/artifacts/original-completion-2026-10-02/p23-02-guest-need-conversion-source-r297/dictionary.cpp.source.json", + "bytes": 281, + "sha256": "032aaf8bb193500745770c940088a2223c1f3f6bb9f8edf6300693bb3e26fc7b" + }, + { + "path": "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/artifacts/original-completion-2026-10-02/p23-02-guest-need-conversion-source-r297/variant_setget.cpp", + "bytes": 62857, + "sha256": "e3f73e1df096bedd4856e29227b302f879d0520c66718467338556b589f685c9" + }, + { + "path": "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/artifacts/original-completion-2026-10-02/p23-02-guest-need-conversion-source-r297/variant_setget.cpp.source.json", + "bytes": 276, + "sha256": "f3002c603869280c99e5efc5f1e923258726c48c404571af34ba6f3bdc009cd2" + }, + { + "path": "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/artifacts/original-completion-2026-10-02/p23-02-guest-need-normalization-candidate-r298/input.json", + "bytes": 11472, + "sha256": "3e2eaa147e365a74145224d8995bade5b44528d59b5508dc159458555081ed99" + }, + { + "path": "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/artifacts/original-completion-2026-10-02/p23-02-guest-need-conversion-source-r297/variant_deep_duplicate.h", + "bytes": 2592, + "sha256": "e43e4ee411e969ea4b584ac330394a452aabca90780087f8d5876167fc71ac4e" + }, + { + "path": "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/artifacts/original-completion-2026-10-02/p23-02-guest-need-conversion-source-r297/variant_deep_duplicate.h.source.json", + "bytes": 281, + "sha256": "5a0ccfd31fc7f3ad6349b414a42a0a9e3277b061ee613e98b39e2bd24c3988ba" + }, + { + "path": "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/artifacts/original-completion-2026-10-02/p23-02-integer-window-codec-array-source-r289/array.cpp", + "bytes": 27906, + "sha256": "ad9ebc6db6a962c817e2dbcee7f9c529dccd042090137e00dcf1a0594696f70b" + }, + { + "path": "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/artifacts/original-completion-2026-10-02/p23-02-integer-window-codec-array-source-r289/source.json", + "bytes": 291, + "sha256": "cd89af6dd2258fe71036edf9f92ce6302d823b68c257405e2a56795726e87834" + }, + { + "path": "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/docs/plans/original-completion-2026-10-02/IMPLEMENTATION_PLAN.md", + "bytes": 492299, + "sha256": "61776f0673770277ab6d182107337ed40a108ff3b5ab2c1a8f29fc4f2c3015ad" + }, + { + "path": "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/docs/plans/original-completion-2026-10-02/PROGRESS_INTEGRATION.md", + "bytes": 410363, + "sha256": "8b0488910dba35b34da96d7062b9a69d293e70a1963c9c5890ec32b2891baaea" + }, + { + "path": "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/docs/plans/original-completion-2026-10-02/work_packages.json", + "bytes": 1184337, + "sha256": "32d80e911910bc762287dc3ddf0ee0ea3072ca2f0158aa2f9f355b9202cfdfeb" + }, + { + "path": "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/docs/plans/original-completion-2026-10-02/progress_snapshot.json", + "bytes": 539512, + "sha256": "b417ee1b945dfe4666f3a2f3501f906756826ea88d741bb116f3dd20250c8958" + } + ], + "unit_accepted": false, + "performance_accepted": false +}