From 4b56960bf8883054f2170503b732b8ec9028d616 Mon Sep 17 00:00:00 2001 From: Eldbury Date: Sun, 19 Apr 2026 14:05:31 +0930 Subject: [PATCH 1/7] Add validated developer observability tools on modawan baseline --- .gitignore | 1 + docs/documentation.md | 452 ++++++++++++++++++++++++++++++++ docs/plans.md | 110 ++++++++ evals/README.md | 48 ++++ include/reone/game/game.h | 29 +++ include/reone/system/logger.h | 2 + scripts/build.ps1 | 201 +++++++++++++++ scripts/capture_logs.ps1 | 44 ++++ scripts/eval_smoke.ps1 | 122 +++++++++ scripts/run_k1.ps1 | 139 ++++++++++ scripts/run_k2.ps1 | 139 ++++++++++ scripts/smoke_test.ps1 | 356 ++++++++++++++++++++++++++ src/apps/engine/main.cpp | 4 + src/libs/game/game.cpp | 469 ++++++++++++++++++++++++++++++++++ src/libs/system/logger.cpp | 8 + 15 files changed, 2124 insertions(+) create mode 100644 docs/documentation.md create mode 100644 docs/plans.md create mode 100644 evals/README.md create mode 100644 scripts/build.ps1 create mode 100644 scripts/capture_logs.ps1 create mode 100644 scripts/eval_smoke.ps1 create mode 100644 scripts/run_k1.ps1 create mode 100644 scripts/run_k2.ps1 create mode 100644 scripts/smoke_test.ps1 diff --git a/.gitignore b/.gitignore index 11791d1da..28cecccf9 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,5 @@ .cache/ +.agent/ .vs/ .vscode/ build/ diff --git a/docs/documentation.md b/docs/documentation.md new file mode 100644 index 000000000..165b91625 --- /dev/null +++ b/docs/documentation.md @@ -0,0 +1,452 @@ +# Documentation Log + +## 2026-04-19: First Modawan-Baseline Migration Slice + +Migration decision: + +- `D:\git\reone-modawan-main` is the working repository and the new runtime baseline. +- `D:\reone-master` is a read-only donor/reference repository for selected features only. +- The old branch must not be wholesale merged into modawan. +- Old combat legality, reciprocal hostility, boarding-party hostility persistence, and older combat-runtime compensation patches are intentionally excluded from this first migration slice. +- PR #77 is intentionally excluded from this first migration slice. +- Preserve modawan's stronger combat, items, and runtime behavior unless a later bounded slice proves a specific donor change is still needed. + +Donor categories inspected: + +- Runtime developer/debug overlay and hotkey tools. +- Eval, smoke, logging, and instrumentation harness. +- Early K1 Endar Spire first-door Trask/Carth behavior. + +Bounded first-migration options considered: + +1. Tooling-first migration. + - Leverage: high. Restores repeatable build, run, smoke, log capture, and smoke eval loops before gameplay changes. + - Risk: low. Touches generic scripts, docs, and a shallow startup log/flush only. + - Likely files: `.gitignore`, `scripts/build.ps1`, `scripts/run_k1.ps1`, `scripts/run_k2.ps1`, `scripts/smoke_test.ps1`, `scripts/eval_smoke.ps1`, `scripts/capture_logs.ps1`, `evals/README.md`, `src/apps/engine/main.cpp`, `include/reone/system/logger.h`, `src/libs/system/logger.cpp`, `docs/documentation.md`, `docs/plans.md`. + - Expected K1/K2 impact: positive observability for both games; no gameplay, combat, party, or first-door behavior changes. +2. First-door sequence-first migration. + - Leverage: medium to high for K1 because it targets the earliest Trask/Carth handoff area directly. + - Risk: medium. Donor changes are tangled with older script execution instrumentation, trigger debug state, action continuation details, and some combat-facing click logging. + - Likely files: `src/libs/game/action/docommand.cpp`, `src/libs/game/object/area.cpp`, `src/libs/game/object/trigger.cpp`, `src/libs/game/script/runner.cpp`, `src/libs/script/virtualmachine.cpp`, plus focused docs/eval logs. + - Expected K1/K2 impact: likely positive for K1 Endar Spire if isolated correctly; intended neutral K2 impact, but script/runtime coupling makes it too broad for the first slice. +3. Combined small-slice migration: tooling + logging + first-door fixes only. + - Leverage: highest if successful because it would add harness proof and one visible early-game behavior improvement together. + - Risk: medium to high for a first slice. It mixes infrastructure with gameplay-facing script/trigger behavior, making regressions harder to attribute. + - Likely files: all tooling-first files plus the first-door files listed above. + - Expected K1/K2 impact: positive K1 observability and possible Endar Spire improvement; K2 should be neutral, but combined blast radius is larger than needed. + +Chosen option: + +- Option 1, tooling-first migration. +- Rationale: it is the smallest high-leverage slice and gives modawan its own verification loop before any gameplay donor code is considered. + +What was ported: + +- Generic Windows PowerShell build/run/smoke/log scripts from the donor harness. +- `evals/README.md` describing the smoke eval contract. +- `.agent/` ignore coverage for generated harness logs. +- A minimal startup signal, `reone smoke signal: engine startup`, emitted immediately after logging initializes. +- Public `Logger::flush()` support so bounded smoke runs persist the startup signal before timeout-based process termination. + +What was intentionally not ported: + +- Developer overlay rendering and hotkey panels from donor `Game`; modawan already has developer console/tools, and the donor overlay is larger than this slice. +- Early Endar Spire first-door Trask/Carth script, trigger, or action-continuation behavior. +- Old combat legality, reciprocal hostility, boarding-party hostility persistence, combat-entry, and encounter band-aid patches. +- Focused donor eval scripts for interaction, party, vitality, combat, or K1 interactables, because those depend on donor tests and older migration history not present in modawan yet. + +Local validation procedure: + +- Configure/build from PowerShell: + - `cd D:\git\reone-modawan-main` + - `powershell -NoProfile -ExecutionPolicy Bypass -File .\scripts\build.ps1 -Config RelWithDebInfo -Target engine -Configure` + - To force a specific vcpkg tree, set `$env:VCPKG_ROOT = 'D:\vcpkg'` before running the command, or pass `-CMakeArgs '-DCMAKE_TOOLCHAIN_FILE=D:\vcpkg\scripts\buildsystems\vcpkg.cmake'`. + - The build script defaults to `D:\vcpkg\scripts\buildsystems\vcpkg.cmake` when `VCPKG_ROOT` is unset and that file exists. +- Smoke test without a game install: + - `powershell -NoProfile -ExecutionPolicy Bypass -File .\scripts\smoke_test.ps1 -AllowMissingGame` + - `powershell -NoProfile -ExecutionPolicy Bypass -File .\scripts\eval_smoke.ps1 -SmokeDir .\.agent\logs\smoke_latest -AllowMissingGame` + - This path still builds `engine` and `create_shaderpack`; it only skips launching the engine when no legal KOTOR directory is provided. +- K1 smoke/eval: + - `powershell -NoProfile -ExecutionPolicy Bypass -File .\scripts\smoke_test.ps1 -GameDir 'D:\SteamLibrary\steamapps\common\swkotor' -Game k1` + - `powershell -NoProfile -ExecutionPolicy Bypass -File .\scripts\eval_smoke.ps1 -SmokeDir .\.agent\logs\smoke_latest` + - Alternatively, set `$env:KOTOR1_DIR` to a legal KOTOR 1 install directory and omit `-GameDir`. +- K2 smoke/eval: + - `powershell -NoProfile -ExecutionPolicy Bypass -File .\scripts\smoke_test.ps1 -GameDir 'D:\SteamLibrary\steamapps\common\Knights of the Old Republic II' -Game k2` + - `powershell -NoProfile -ExecutionPolicy Bypass -File .\scripts\eval_smoke.ps1 -SmokeDir .\.agent\logs\smoke_latest` + - Alternatively, set `$env:KOTOR2_DIR` to a legal KOTOR 2 install directory and omit `-GameDir`. +- Common failure modes: + - SDL3/vcpkg: this repo uses SDL3 headers and `find_package(SDL3 CONFIG REQUIRED)`, with the vcpkg package name `sdl3` and CMake target `SDL3::SDL3`. A working classic vcpkg install should provide `D:\vcpkg\installed\x64-windows\share\sdl3\SDL3Config.cmake` or equivalent. If CMake reports missing `SDL3Config.cmake` or `sdl3-config.cmake`, install the missing external prerequisite with `& 'D:\vcpkg\vcpkg.exe' install sdl3:x64-windows`, or point `VCPKG_ROOT`/`CMAKE_TOOLCHAIN_FILE` to a vcpkg tree that already has SDL3 for the target triplet. + - Toolchain detection: if CMake cannot find vcpkg packages that are installed elsewhere, verify `$env:VCPKG_ROOT`, `-DCMAKE_TOOLCHAIN_FILE=...`, and any `-DVCPKG_TARGET_TRIPLET=...` value all refer to the same vcpkg tree/triplet. + - CMake detection: if `cmake.exe` is not on `PATH`, set `$env:CMAKE_EXE` to the full path, for example `C:\Program Files\CMake\bin\cmake.exe`. + - Build cache drift: if the `build` directory contains a stale or partial CMake cache, rerun `scripts\build.ps1` with `-Configure`; if that still fails, remove only the local `build` directory and configure again. + - Game install checks: K1 requires `swkotor.exe` in the supplied directory, and K2 requires `swkotor2.exe`. The smoke harness will skip launch only when `-AllowMissingGame` is used. + +Verification results: + +- PowerShell syntax parsing passed for `scripts/build.ps1`, `scripts/capture_logs.ps1`, `scripts/eval_smoke.ps1`, `scripts/run_k1.ps1`, `scripts/run_k2.ps1`, and `scripts/smoke_test.ps1`. +- `powershell -NoProfile -ExecutionPolicy Bypass -File .\scripts\build.ps1 -Config RelWithDebInfo -Target engine -Configure` + - Result: failed during CMake configure because local vcpkg does not provide SDL3 package config files (`SDL3Config.cmake`, `sdl3-config.cmake`, or equivalent). +- `powershell -NoProfile -ExecutionPolicy Bypass -File .\scripts\smoke_test.ps1 -AllowMissingGame` + - Result: failed before launch for the same SDL3 configure blocker; smoke manifest was written. +- `powershell -NoProfile -ExecutionPolicy Bypass -File .\scripts\eval_smoke.ps1 -SmokeDir .\.agent\logs\smoke_latest -AllowMissingGame` + - Result: failed as expected because the manifest records failed build, missing `engine.exe`, and missing `shaderpack.erf`. +- `powershell -NoProfile -ExecutionPolicy Bypass -File .\scripts\smoke_test.ps1 -GameDir 'D:\SteamLibrary\steamapps\common\swkotor' -Game k1` + - Result: failed before launch for the same SDL3 configure blocker; K1 marker path was available. +- `powershell -NoProfile -ExecutionPolicy Bypass -File .\scripts\eval_smoke.ps1 -SmokeDir .\.agent\logs\smoke_latest` + - Result after K1 smoke: failed because build did not succeed and launch was not attempted. +- `powershell -NoProfile -ExecutionPolicy Bypass -File .\scripts\smoke_test.ps1 -GameDir 'D:\SteamLibrary\steamapps\common\Knights of the Old Republic II' -Game k2` + - Result: failed before launch for the same SDL3 configure blocker; K2 marker path was available. +- `powershell -NoProfile -ExecutionPolicy Bypass -File .\scripts\eval_smoke.ps1 -SmokeDir .\.agent\logs\smoke_latest` + - Result after K2 smoke: failed because build did not succeed and launch was not attempted. + +Remaining blocker: + +- Install or point CMake at SDL3 for the active vcpkg/toolchain environment, then rerun the same build and K1/K2 smoke/eval commands. This slice did not install external dependencies or modify files outside the working repository. + +## 2026-04-19: Local Validation Bootstrap Follow-Up + +Scope: + +- This was a validation/bootstrap step only. +- No donor gameplay behavior, overlay migration, first-door behavior, combat, hostility, boarding-party, or modernization code was ported. +- `scripts\build.ps1` now emits a targeted warning when the active classic vcpkg toolchain is found but `installed\\share\sdl3` is missing. CMake still performs the authoritative configure step and writes the configure log. + +Latest local environment result: + +- Current branch: `port-dev-tools`. +- CMake was found at `C:\Program Files\CMake\bin\cmake.exe`. +- vcpkg toolchain was found at `D:\vcpkg\scripts\buildsystems\vcpkg.cmake`. +- `D:\vcpkg\installed\x64-windows\share\sdl2` exists. +- `D:\vcpkg\installed\x64-windows\share\sdl3` does not exist. +- K1 marker exists at `D:\SteamLibrary\steamapps\common\swkotor\swkotor.exe`. +- K2 marker exists at `D:\SteamLibrary\steamapps\common\Knights of the Old Republic II\swkotor2.exe`. + +Latest local validation result: + +- PowerShell parser checks passed for all scripts under `scripts`. +- `git diff --check` reported only existing CRLF normalization warnings. +- Configure/build still fails because the active vcpkg tree does not have SDL3 installed for `x64-windows`. +- Generic smoke, K1 smoke, and K2 smoke all stop before launch for the same SDL3 configure blocker. +- Generic, K1, and K2 smoke evals ran against the generated manifests and failed as expected because the build did not succeed, `engine.exe` was not produced, and `shaderpack.erf` was not produced. + +## 2026-04-19: Local Validation Closure After SDL3 Install + +Scope: + +- This was a validation closure step only. +- No donor gameplay behavior, overlay migration, first-door behavior, combat, hostility, boarding-party, or modernization code was ported. +- The previous SDL3 blocker is resolved on this machine because `D:\vcpkg\installed\x64-windows\share\sdl3\SDL3Config.cmake` is now present. + +Confirmed local procedure and result: + +- Configure/build: + - `powershell -NoProfile -ExecutionPolicy Bypass -File .\scripts\build.ps1 -Config RelWithDebInfo -Target engine -Configure` + - Result: passed. CMake configured with `D:\vcpkg\scripts\buildsystems\vcpkg.cmake` and built `D:\Git\reone-modawan-main\build\bin\engine.exe`. +- Smoke test without game install: + - `powershell -NoProfile -ExecutionPolicy Bypass -File .\scripts\smoke_test.ps1 -AllowMissingGame` + - `powershell -NoProfile -ExecutionPolicy Bypass -File .\scripts\eval_smoke.ps1 -SmokeDir .\.agent\logs\smoke_latest -AllowMissingGame` + - Result: passed. Launch was skipped by design because no game directory was supplied. `engine.exe` and `shaderpack.erf` were verified in `build\bin`. +- K1 smoke/eval: + - `powershell -NoProfile -ExecutionPolicy Bypass -File .\scripts\smoke_test.ps1 -GameDir 'D:\SteamLibrary\steamapps\common\swkotor' -Game k1` + - `powershell -NoProfile -ExecutionPolicy Bypass -File .\scripts\eval_smoke.ps1 -SmokeDir .\.agent\logs\smoke_latest` + - Result: passed. Smoke artifacts were written to `.agent\logs\smoke_20260419_092431`. +- K2 smoke/eval: + - `powershell -NoProfile -ExecutionPolicy Bypass -File .\scripts\smoke_test.ps1 -GameDir 'D:\SteamLibrary\steamapps\common\Knights of the Old Republic II' -Game k2` + - `powershell -NoProfile -ExecutionPolicy Bypass -File .\scripts\eval_smoke.ps1 -SmokeDir .\.agent\logs\smoke_latest` + - Result: passed. Smoke artifacts were written to `.agent\logs\smoke_20260419_092459`. + +Artifacts confirmed: + +- `D:\Git\reone-modawan-main\build\bin\engine.exe` +- `D:\Git\reone-modawan-main\build\bin\shaderpack.erf` + +Remaining blocker: + +- None for the tooling-first validation slice on this machine. + +## 2026-04-19: Runtime Developer Overlay Observability Slice + +Scope: + +- This is an observability-only runtime overlay slice. +- `D:\reone-master` was used only as a read-only donor/reference for the old developer overlay and hotkey shape. +- No donor gameplay behavior, first-door behavior, combat legality, reciprocal hostility, boarding-party hostility persistence, encounter/combat band-aids, PR #77, Dear ImGui draft work, or modernization code was ported. + +Overlay-only options considered, smallest to largest: + +1. Status/watch overlay shell. + - Adds a developer-mode-gated, read-only in-game overlay with a small help banner and compact watch panel. + - Hotkeys: `Ctrl+Shift+D` toggles the overlay, and `Ctrl+Shift+W` toggles the watch panel. + - Risk: low. Reads existing runtime state and only mutates overlay visibility flags. +2. Status/watch overlay plus developer event feed. + - Adds option 1 plus the donor-style feed panel and `addDeveloperEvent` plumbing, initially logging only overlay toggle events. + - Risk: low to medium. More public game API and event buffer surface. +3. Full donor visual overlay minus gameplay patches. + - Adds trigger outlines, actor labels, target inspector, event feed, and watched values. + - Risk: medium. It touches trigger debug state, object inspection, context-action/hostility-adjacent labels, and broader rendering helpers. + +Chosen option: + +- Option 1, status/watch overlay shell. +- Rationale: it is the smallest high-leverage overlay slice and keeps the implementation read-only, isolated, and easy to disable. + +What was ported: + +- Developer-mode-gated in-game overlay visibility state in `Game`. +- Donor-style `Ctrl+Shift` hotkey chord handling for overlay tools. +- A small on-screen developer banner showing the overlay hotkeys and existing console/profiler shortcuts. +- A read-only watch panel showing screen, module, area, camera mode, game speed, pause state, relative mouse state, party leader id/tag/HP/position, selected object id/tag/resource/type/HP, and hovered object id/tag/resource/type/HP. + +Hotkeys: + +- `Ctrl+Shift+D`: toggle the developer observability overlay. +- `Ctrl+Shift+W`: toggle the watch panel. If the overlay is hidden, this also opens it. +- Existing tools remain available: backquote opens the console, `F5` toggles the profiler, `V` toggles in-game camera mode in developer mode, and `+`/`-` adjust developer game speed. + +How to use: + +- Launch with developer mode enabled, for example `scripts\run_k1.ps1 -Dev` or `scripts\run_k2.ps1 -Dev`, or enable Developer Mode in the launcher. +- Enter an in-game module. +- Press `Ctrl+Shift+D` to show or hide the overlay. +- Press `Ctrl+Shift+W` to show or hide the watch panel. + +How to disable: + +- Launch with developer mode disabled, for example omit `-Dev` from the run scripts or pass `--dev false` to `engine.exe`. +- While in game, press `Ctrl+Shift+D` to hide the overlay. +- Press `Ctrl+Shift+W` to hide only the watch panel while leaving the banner visible. + +What was intentionally not ported: + +- Donor trigger outlines and trigger debug state. +- Donor actor labels. +- Donor target inspector. +- Donor event feed and `addDeveloperEvent` instrumentation. +- Donor script/action/combat/area/creature instrumentation. +- Any donor gameplay behavior, hostility, party, item, cutscene, encounter, or first-door logic. + +Validation results: + +- `powershell -NoProfile -ExecutionPolicy Bypass -File .\scripts\build.ps1 -Config RelWithDebInfo -Target engine -Configure` + - Result: passed. `D:\Git\reone-modawan-main\build\bin\engine.exe` was produced. +- `powershell -NoProfile -ExecutionPolicy Bypass -File .\scripts\smoke_test.ps1 -AllowMissingGame` + - Result: passed. Launch was skipped by design; smoke artifacts were written to `.agent\logs\smoke_20260419_093609`. +- `powershell -NoProfile -ExecutionPolicy Bypass -File .\scripts\eval_smoke.ps1 -SmokeDir .\.agent\logs\smoke_latest -AllowMissingGame` + - Result: passed. +- `powershell -NoProfile -ExecutionPolicy Bypass -File .\scripts\smoke_test.ps1 -GameDir 'D:\SteamLibrary\steamapps\common\swkotor' -Game k1` + - Result: passed. Smoke artifacts were written to `.agent\logs\smoke_20260419_093617`. +- `powershell -NoProfile -ExecutionPolicy Bypass -File .\scripts\eval_smoke.ps1 -SmokeDir .\.agent\logs\smoke_latest` + - Result after K1 smoke: passed. +- `powershell -NoProfile -ExecutionPolicy Bypass -File .\scripts\smoke_test.ps1 -GameDir 'D:\SteamLibrary\steamapps\common\Knights of the Old Republic II' -Game k2` + - Result: passed. Smoke artifacts were written to `.agent\logs\smoke_20260419_093630`. +- `powershell -NoProfile -ExecutionPolicy Bypass -File .\scripts\eval_smoke.ps1 -SmokeDir .\.agent\logs\smoke_latest` + - Result after K2 smoke: passed. +- `git diff --check` + - Result: no whitespace errors; only CRLF normalization warnings. + +Human verification needed: + +- Automated smoke validates startup and the smoke signal, but it does not press overlay hotkeys. +- Human in-game verification should launch with developer mode enabled and confirm `Ctrl+Shift+D` shows/hides the overlay and `Ctrl+Shift+W` shows/hides the watch panel. + +## 2026-04-19: Trigger Zone Developer Overlay Slice + +Scope: + +- This is an observability-only extension to the developer overlay. +- `D:\reone-master` was used only as a read-only donor/reference for the old trigger visualization, entity label, and launcher developer-option wiring. +- No donor gameplay behavior, first-door behavior, combat legality, reciprocal hostility, boarding-party hostility persistence, encounter/combat band-aids, PR #77, Dear ImGui draft work, or modernization code was ported. + +Options considered: + +1. A: trigger zone visualization only. + - Leverage: high. Restores the most visible missing spatial observability from the old branch. + - Risk: low. Reads existing trigger geometry and draws screen-space outlines/labels only. + - Likely files: `include/reone/game/object/trigger.h`, `include/reone/game/game.h`, `src/libs/game/game.cpp`, docs. + - Runtime blast radius: developer-mode render path only. + - Disable: launch without developer mode, press `Ctrl+Shift+D` to hide the overlay, or press `Ctrl+Shift+T` to hide triggers. +2. B: entity/world labels only. + - Leverage: medium to high. Useful for object inspection, faction-style debugging, and hover/selection review. + - Risk: medium. Rich labels tend to pull in creature, faction, hostility, and context-action interpretation. + - Likely files: `include/reone/game/game.h`, `src/libs/game/game.cpp`, docs. + - Runtime blast radius: developer-mode render path over area objects. + - Disable: launch without developer mode, hide the overlay, or add a labels toggle. +3. C: minimal launcher-level developer options wiring. + - Leverage: medium for workflow, low for immediate in-game observability because Developer Mode already exists in the launcher. + - Risk: low. Launcher/config surface only. + - Likely files: `src/apps/launcher/frame.h`, `src/apps/launcher/frame.cpp`, docs. + - Runtime blast radius: launcher/config only. + - Disable: uncheck launcher options. + +Chosen option: + +- Option A, trigger zone visualization only. +- Rationale: it closes the largest manually observed overlay gap with the smallest runtime surface and avoids the faction/hostility-adjacent data needed for useful entity labels. + +What was added: + +- A read-only `Trigger::geometry()` accessor so the overlay can inspect existing trigger polygon points without changing trigger behavior. +- `Ctrl+Shift+T` developer hotkey for trigger zone visualization. +- Screen-space trigger polygon outlines in the developer overlay. +- Trigger labels at the polygon centroid showing object id, tag, and `OnEnter` script when available. + +How to enable: + +- Launch with developer mode enabled, for example `scripts\run_k1.ps1 -Dev`, `scripts\run_k2.ps1 -Dev`, or the launcher Developer Mode checkbox. +- Enter an in-game module. +- Press `Ctrl+Shift+D` to show the developer overlay. Trigger visualization is enabled by default when the overlay is shown. +- Press `Ctrl+Shift+T` to show or hide trigger zones. If the overlay is hidden, this hotkey also opens it. + +Hotkeys: + +- `Ctrl+Shift+D`: toggle the developer overlay. +- `Ctrl+Shift+T`: toggle trigger zone visualization. +- `Ctrl+Shift+W`: toggle the watch panel. + +How to disable: + +- Launch without developer mode. +- Press `Ctrl+Shift+D` to hide the whole overlay. +- Press `Ctrl+Shift+T` to hide trigger zones while keeping the rest of the overlay visible. + +What was intentionally not ported: + +- Donor trigger debug state, colors, occupancy/test/enter timers, or Area/Trigger instrumentation. +- Donor entity/world actor labels. +- Donor target inspector. +- Donor event feed and `addDeveloperEvent` instrumentation. +- Any launcher UI redesign or new launcher controls. +- Any gameplay, combat, hostility, boarding-party, encounter, item, cutscene, party, or first-door behavior. + +Validation results: + +- Default build output `D:\Git\reone-modawan-main\build\bin\engine.exe` was locked by a stale `engine` process from manual verification. PowerShell could see process id `112868` but could not stop it due access permissions. +- Validation was completed with a clean ignored build directory: `.\build\overlay_validation`. +- `powershell -NoProfile -ExecutionPolicy Bypass -File .\scripts\build.ps1 -BuildDir .\build\overlay_validation -Config RelWithDebInfo -Target engine -Configure` + - Result: passed. `D:\Git\reone-modawan-main\build\overlay_validation\bin\engine.exe` was produced. +- `powershell -NoProfile -ExecutionPolicy Bypass -File .\scripts\smoke_test.ps1 -BuildDir .\build\overlay_validation -AllowMissingGame` + - Result: passed. Launch was skipped by design. +- `powershell -NoProfile -ExecutionPolicy Bypass -File .\scripts\eval_smoke.ps1 -SmokeDir .\.agent\logs\smoke_latest -AllowMissingGame` + - Result: passed. +- `powershell -NoProfile -ExecutionPolicy Bypass -File .\scripts\smoke_test.ps1 -BuildDir .\build\overlay_validation -GameDir 'D:\SteamLibrary\steamapps\common\swkotor' -Game k1` + - Result: passed. Smoke artifacts were written to `.agent\logs\smoke_20260419_122754`. +- `powershell -NoProfile -ExecutionPolicy Bypass -File .\scripts\eval_smoke.ps1 -SmokeDir .\.agent\logs\smoke_latest` + - Result after K1 smoke: passed. +- `powershell -NoProfile -ExecutionPolicy Bypass -File .\scripts\smoke_test.ps1 -BuildDir .\build\overlay_validation -GameDir 'D:\SteamLibrary\steamapps\common\Knights of the Old Republic II' -Game k2` + - Result: passed. Smoke artifacts were written to `.agent\logs\smoke_20260419_122825`. +- `powershell -NoProfile -ExecutionPolicy Bypass -File .\scripts\eval_smoke.ps1 -SmokeDir .\.agent\logs\smoke_latest` + - Result after K2 smoke: passed. + +Human verification needed: + +- Automated smoke validates startup, but it does not press overlay hotkeys. +- Human in-game verification should launch with developer mode enabled and confirm `Ctrl+Shift+D` shows/hides the overlay and `Ctrl+Shift+T` shows/hides trigger outlines and labels. + +Default build retry after closing game: + +- The previous default build lock was caused by a running `engine.exe` from manual verification. +- After that process was closed, the normal default build directory validated successfully. +- `powershell -NoProfile -ExecutionPolicy Bypass -File .\scripts\build.ps1 -Config RelWithDebInfo -Target engine -Configure` + - Result: passed. `D:\Git\reone-modawan-main\build\bin\engine.exe` was produced. +- `powershell -NoProfile -ExecutionPolicy Bypass -File .\scripts\smoke_test.ps1 -AllowMissingGame` + - Result: passed. Launch was skipped by design. +- `powershell -NoProfile -ExecutionPolicy Bypass -File .\scripts\eval_smoke.ps1 -SmokeDir .\.agent\logs\smoke_latest -AllowMissingGame` + - Result: passed. +- `powershell -NoProfile -ExecutionPolicy Bypass -File .\scripts\smoke_test.ps1 -GameDir 'D:\SteamLibrary\steamapps\common\swkotor' -Game k1` + - Result: passed. Smoke artifacts were written to `.agent\logs\smoke_20260419_123202`. +- `powershell -NoProfile -ExecutionPolicy Bypass -File .\scripts\eval_smoke.ps1 -SmokeDir .\.agent\logs\smoke_latest` + - Result after K1 smoke: passed. +- `powershell -NoProfile -ExecutionPolicy Bypass -File .\scripts\smoke_test.ps1 -GameDir 'D:\SteamLibrary\steamapps\common\Knights of the Old Republic II' -Game k2` + - Result: passed. Smoke artifacts were written to `.agent\logs\smoke_20260419_123230`. +- `powershell -NoProfile -ExecutionPolicy Bypass -File .\scripts\eval_smoke.ps1 -SmokeDir .\.agent\logs\smoke_latest` + - Result after K2 smoke: passed. + +## 2026-04-19: Developer Overlay Gap-Fix Slice + +Scope: + +- This is an observability-only follow-up to the trigger zone overlay slice. +- `D:\reone-master` was used only as a read-only donor/reference for old trigger state coloring and lightweight entity labels. +- No donor gameplay behavior, first-door behavior, combat legality, reciprocal hostility, boarding-party hostility persistence, encounter/combat band-aids, PR #77, Dear ImGui draft work, launcher redesign, or modernization code was ported. + +What was added: + +- Trigger zones now expose read-only debug state in the overlay: + - `default`: normal blue outline. + - `tested`: yellow outline after a creature movement tested the trigger. + - `inside`: green outline while an object is inside or was just detected inside. + - `enter`: orange outline briefly after a trigger enters/fires. +- Trigger labels now include the debug state, for example `#230 end_trig02 k_pend_trig02 [inside]`. +- Lightweight entity/world labels can be toggled with `Ctrl+Shift+A`. +- Entity labels show read-only id, tag, template, faction, hostile-to-player flag for creatures, selectable flag, commandable flag, visible flag, and plot flag. +- Door and placeable faction accessors were added only so the developer label can display existing loaded faction data. + +Hotkeys: + +- `Ctrl+Shift+D`: toggle the developer overlay. +- `Ctrl+Shift+T`: toggle trigger zones and trigger state labels. +- `Ctrl+Shift+A`: toggle entity/world labels. +- `Ctrl+Shift+W`: toggle the watch panel. + +How to enable: + +- Launch with developer mode enabled, for example `scripts\run_k1.ps1 -Dev`, `scripts\run_k2.ps1 -Dev`, or the launcher Developer Mode checkbox. +- Enter an in-game module. +- Press `Ctrl+Shift+D` to show the developer overlay. +- Press `Ctrl+Shift+T` for trigger zones and `Ctrl+Shift+A` for entity labels. If the overlay is hidden, those feature hotkeys open the overlay with the requested feature enabled. + +How to disable: + +- Launch without developer mode. +- Press `Ctrl+Shift+D` to hide the whole overlay. +- Press `Ctrl+Shift+T` to hide trigger zones. +- Press `Ctrl+Shift+A` to hide entity labels. + +What was intentionally not ported: + +- Donor target inspector. +- Donor event feed and broad `addDeveloperEvent` instrumentation. +- Donor trigger/area encounter logging and first-door-specific trigger diagnostics. +- Donor launcher developer-option controls. +- Any gameplay, combat, hostility, boarding-party, encounter, item, cutscene, party, first-door, or modernization behavior. + +Validation: + +- Default build note: + - `powershell -NoProfile -ExecutionPolicy Bypass -File .\scripts\build.ps1 -Config RelWithDebInfo -Target engine -Configure` + - Result: compile passed, but the final link failed with `LNK1168: cannot open D:\Git\reone-modawan-main\build\bin\engine.exe for writing`, indicating the default output executable was locked locally. +- Default build retry: + - `powershell -NoProfile -ExecutionPolicy Bypass -File .\scripts\build.ps1 -Config RelWithDebInfo -Target engine` + - Result: passed after the local executable lock was gone. `D:\Git\reone-modawan-main\build\bin\engine.exe` now contains the `Ctrl+Shift+A` label hotkey. +- Hotkey follow-up: + - Feature hotkeys now open the overlay with that feature enabled when the overlay is hidden, instead of toggling a default-on feature off. + - `Ctrl+Shift+T`, `Ctrl+Shift+A`, and `Ctrl+Shift+W` keep their normal toggle behavior while the overlay is already visible. +- Validation build: + - `powershell -NoProfile -ExecutionPolicy Bypass -File .\scripts\build.ps1 -BuildDir .\build\overlay_labels_validation -Config RelWithDebInfo -Target engine -Configure` + - Result: passed. `D:\Git\reone-modawan-main\build\overlay_labels_validation\bin\engine.exe` was produced. +- Smoke test without game install: + - `powershell -NoProfile -ExecutionPolicy Bypass -File .\scripts\smoke_test.ps1 -BuildDir .\build\overlay_labels_validation -AllowMissingGame` + - `powershell -NoProfile -ExecutionPolicy Bypass -File .\scripts\eval_smoke.ps1 -SmokeDir .\.agent\logs\smoke_latest -AllowMissingGame` + - Result: passed. Launch was skipped by design. +- K1 smoke/eval: + - `powershell -NoProfile -ExecutionPolicy Bypass -File .\scripts\smoke_test.ps1 -BuildDir .\build\overlay_labels_validation -GameDir 'D:\SteamLibrary\steamapps\common\swkotor' -Game k1` + - `powershell -NoProfile -ExecutionPolicy Bypass -File .\scripts\eval_smoke.ps1 -SmokeDir .\.agent\logs\smoke_latest` + - Result: passed. Smoke artifacts were written to `.agent\logs\smoke_20260419_130346`. +- K2 smoke/eval: + - `powershell -NoProfile -ExecutionPolicy Bypass -File .\scripts\smoke_test.ps1 -BuildDir .\build\overlay_labels_validation -GameDir 'D:\SteamLibrary\steamapps\common\Knights of the Old Republic II' -Game k2` + - `powershell -NoProfile -ExecutionPolicy Bypass -File .\scripts\eval_smoke.ps1 -SmokeDir .\.agent\logs\smoke_latest` + - Result: passed. Smoke artifacts were written to `.agent\logs\smoke_20260419_130425`. +- Artifact check: + - `D:\Git\reone-modawan-main\build\overlay_labels_validation\bin\engine.exe` exists. + - `D:\Git\reone-modawan-main\build\overlay_labels_validation\bin\shaderpack.erf` exists. +- `git diff --check` + - Result: no whitespace errors; only CRLF normalization warnings. +- Default no-game smoke follow-up: + - `powershell -NoProfile -ExecutionPolicy Bypass -File .\scripts\smoke_test.ps1 -AllowMissingGame` + - `powershell -NoProfile -ExecutionPolicy Bypass -File .\scripts\eval_smoke.ps1 -SmokeDir .\.agent\logs\smoke_latest -AllowMissingGame` + - Result: passed. Launch was skipped by design. + +Human verification needed: + +- Automated smoke validates startup, but it does not press overlay hotkeys or move through trigger volumes. +- Human in-game verification should launch with developer mode enabled and confirm `Ctrl+Shift+A` shows/hides entity labels, and trigger zones change to `tested`, `inside`, and `enter` states when crossed. diff --git a/docs/plans.md b/docs/plans.md new file mode 100644 index 000000000..2b906dce1 --- /dev/null +++ b/docs/plans.md @@ -0,0 +1,110 @@ +# Plans + +## Tiny Migration Milestone: Tooling-First Harness + +Goal: give the modawan baseline its own bounded build, launch, smoke, log-capture, and smoke-eval loop before any gameplay donor code is considered. + +Acceptance criteria: + +- `scripts/build.ps1`, `scripts/run_k1.ps1`, `scripts/run_k2.ps1`, `scripts/smoke_test.ps1`, `scripts/eval_smoke.ps1`, and `scripts/capture_logs.ps1` exist in the working repo. +- `evals/README.md` documents the smoke eval contract. +- Engine startup emits `reone smoke signal: engine startup` after logging initializes. +- Smoke eval can verify build success, `engine.exe`, `shaderpack.erf`, the startup signal when launch is attempted, and absence of obvious fatal output. +- K1 and K2 smoke runs are attempted only against legal local game directories or explicitly skipped with `-AllowMissingGame`. +- No combat, boarding-party hostility, first-door, party, save/load, item, or modernization behavior changes are included. + +Verification: + +- Build the engine target. +- Build or verify the shader pack via the smoke harness. +- Run generic smoke eval with `-AllowMissingGame` if legal game paths are unavailable. +- Run K1 and K2 smoke/eval if legal install paths are available. + +Non-goals: + +- Do not port donor combat or hostility patches. +- Do not port donor Endar Spire first-door fixes in this milestone. +- Do not port donor developer overlay rendering or hotkey panels in this milestone. +- Do not modernize build, runtime, input, UI, rendering, or gameplay systems. + +## Tiny Migration Milestone: Developer Overlay Observability + +Goal: add the smallest useful in-game developer overlay and hotkeys for runtime observability without changing gameplay behavior. + +Acceptance criteria: + +- Developer overlay is gated by existing developer mode. +- `Ctrl+Shift+D` toggles the overlay. +- `Ctrl+Shift+W` toggles the read-only watch panel. +- Watch panel reads only existing state: screen, module, area, camera, speed, pause, relative mouse, leader, selected object, and hovered object. +- Overlay is off by default and can be disabled by hiding it or launching without developer mode. +- No donor trigger debug state, actor labels, target inspector, event feed, gameplay instrumentation, combat, hostility, boarding-party, first-door, item, party, cutscene, or modernization changes are included. + +Verification: + +- Build the engine target. +- Run generic smoke/eval with `-AllowMissingGame`. +- Run K1 smoke/eval. +- Run K2 smoke/eval. +- Human in-game verification should confirm `Ctrl+Shift+D` and `Ctrl+Shift+W` render and hide the overlay while developer mode is enabled. + +Non-goals: + +- Do not port donor trigger overlays in this milestone. +- Do not port donor actor labels or target inspector in this milestone. +- Do not port donor developer event feed or instrumentation in this milestone. +- Do not port donor gameplay behavior or modernize runtime systems. + +## Tiny Migration Milestone: Trigger Zone Overlay + +Goal: restore developer-mode trigger zone visualization as the next smallest high-leverage observability feature. + +Acceptance criteria: + +- Trigger visualization is gated by existing developer mode. +- `Ctrl+Shift+T` toggles trigger zone visualization. +- Trigger polygons are rendered as screen-space outlines inside the existing developer overlay. +- Trigger labels show read-only id, tag, and `OnEnter` script when available. +- The implementation reads existing trigger geometry only and does not add trigger occupancy, debug-state timers, script instrumentation, or gameplay behavior changes. +- The feature can be disabled by launching without developer mode, hiding the overlay with `Ctrl+Shift+D`, or hiding triggers with `Ctrl+Shift+T`. + +Verification: + +- Build the engine target. +- Run generic smoke/eval with `-AllowMissingGame`. +- Run K1 smoke/eval. +- Run K2 smoke/eval. +- Human in-game verification should confirm `Ctrl+Shift+T` shows and hides trigger outlines and labels while developer mode is enabled. + +Non-goals: + +- Do not port donor trigger debug state or occupancy instrumentation in this milestone. +- Do not port donor entity/world labels in this milestone. +- Do not port launcher developer-option UI changes in this milestone. +- Do not port gameplay, combat, hostility, boarding-party, encounter, first-door, item, party, cutscene, or modernization changes. + +## Tiny Migration Milestone: Overlay Debug Labels And Trigger State + +Goal: close the manually observed overlay gaps by restoring trigger state coloring and lightweight entity labels without changing gameplay behavior. + +Acceptance criteria: + +- Developer overlay remains gated by existing developer mode. +- `Ctrl+Shift+T` trigger zones show default/tested/inside/enter state in color and label text. +- `Ctrl+Shift+A` toggles lightweight entity labels over nearby creatures, doors, and placeables. +- Labels are read-only and limited to existing object state: id, tag, template, faction, hostile-to-player flag for creatures, selectable, commandable, visible, and plot. +- No donor target inspector, event feed, first-door diagnostics, combat, hostility, boarding-party, encounter, item, party, cutscene, launcher redesign, or modernization changes are included. + +Verification: + +- Build the engine target. +- Run generic smoke/eval with `-AllowMissingGame`. +- Run K1 smoke/eval. +- Run K2 smoke/eval. +- Human in-game verification should confirm `Ctrl+Shift+A` shows and hides entity labels, and trigger zones change from default/tested to inside/enter when crossed. + +Non-goals: + +- Do not port donor target inspector or event feed in this milestone. +- Do not port donor launcher developer-option controls in this milestone. +- Do not port donor gameplay behavior or modernize runtime systems. diff --git a/evals/README.md b/evals/README.md new file mode 100644 index 000000000..866935395 --- /dev/null +++ b/evals/README.md @@ -0,0 +1,48 @@ +# Smoke Evals + +This first migration slice ports only the generic smoke harness. Focused donor evals for interaction, party selection, vitality, combat entry, and K1 interactables are intentionally not included yet. + +## Run + +Without a legal game install, verify build/executable discovery and record that launch was skipped: + +```powershell +powershell -NoProfile -ExecutionPolicy Bypass -File .\scripts\smoke_test.ps1 -AllowMissingGame +powershell -NoProfile -ExecutionPolicy Bypass -File .\scripts\eval_smoke.ps1 -SmokeDir .\.agent\logs\smoke_latest -AllowMissingGame +``` + +With a legal KOTOR 1 install: + +```powershell +powershell -NoProfile -ExecutionPolicy Bypass -File .\scripts\smoke_test.ps1 -GameDir "C:\Path\To\KotOR" -Game k1 +powershell -NoProfile -ExecutionPolicy Bypass -File .\scripts\eval_smoke.ps1 -SmokeDir .\.agent\logs\smoke_latest +``` + +With a legal KOTOR 2 install: + +```powershell +powershell -NoProfile -ExecutionPolicy Bypass -File .\scripts\smoke_test.ps1 -GameDir "C:\Path\To\KotOR2" -Game k2 +powershell -NoProfile -ExecutionPolicy Bypass -File .\scripts\eval_smoke.ps1 -SmokeDir .\.agent\logs\smoke_latest +``` + +## Contract + +`smoke_test.ps1` writes: + +- `stdout.log` +- `stderr.log` +- `engine.log` +- `smoke_manifest.json` + +Artifacts are written to `.agent\logs\smoke_` and copied to `.agent\logs\smoke_latest`. + +`eval_smoke.ps1` checks: + +- Build success or executable discovery. +- Built `engine.exe` discovery. +- Built `shaderpack.erf` discovery. +- Engine process launch when a legal game directory is available. +- The deterministic startup signal `reone smoke signal: engine startup` when launch is attempted. +- Absence of obvious fatal patterns such as `Engine failure`, `FATAL`, `Unhandled exception`, access violations, CMake errors, and failed build markers. + +The eval returns a non-zero exit code for deterministic failure. diff --git a/include/reone/game/game.h b/include/reone/game/game.h index 2f28420bc..d3c2bf7ba 100644 --- a/include/reone/game/game.h +++ b/include/reone/game/game.h @@ -17,8 +17,11 @@ #pragma once +#include + #include "reone/audio/source.h" #include "reone/graphics/cursor.h" +#include "reone/graphics/types.h" #include "reone/input/event.h" #include "reone/movie/movie.h" #include "reone/script/routines.h" @@ -71,6 +74,12 @@ class GUI; } +namespace graphics { + +class Font; + +} + namespace game { class Game : boost::noncopyable { @@ -320,6 +329,16 @@ class Game : boost::noncopyable { Screen _screen {Screen::None}; + struct DeveloperOverlay { + bool visible {false}; + bool triggers {true}; + bool actorLabels {true}; + bool watchedValues {true}; + }; + + DeveloperOverlay _developerOverlay; + std::shared_ptr _developerFont; + std::shared_ptr _movie; resource::CursorType _cursorType {resource::CursorType::None}; std::shared_ptr _cursor; @@ -399,6 +418,7 @@ class Game : boost::noncopyable { bool handleMouseMotion(const input::MouseMotionEvent &event); bool handleMouseButtonDown(const input::MouseButtonEvent &event); bool handleMouseButtonUp(const input::MouseButtonEvent &event); + bool handleDeveloperKeyDown(const input::KeyEvent &event); void onModuleSelected(const std::string &name); void renderHUD(); @@ -419,6 +439,15 @@ class Game : boost::noncopyable { void renderScene(); void renderGUI(); + void renderDeveloperOverlay(); + void renderDeveloperBanner(); + void renderDeveloperTriggerOverlay(const glm::mat4 &projection, const glm::mat4 &view); + void renderDeveloperActorLabels(const glm::mat4 &projection, const glm::mat4 &view); + void renderDeveloperWatchedValues(); + void renderDeveloperText(const std::string &text, const glm::vec3 &position, const glm::vec3 &color, graphics::TextGravity gravity = graphics::TextGravity::LeftTop); + void renderDeveloperPanel(const std::vector &lines, glm::vec2 position, glm::vec3 color); + void renderDeveloperLine(glm::vec2 a, glm::vec2 b, glm::vec4 color, float width); + void renderDeveloperRect(glm::vec2 position, glm::vec2 size, glm::vec4 color); // END Rendering diff --git a/include/reone/system/logger.h b/include/reone/system/logger.h index 1f87ea718..c894e5a6b 100644 --- a/include/reone/system/logger.h +++ b/include/reone/system/logger.h @@ -34,6 +34,8 @@ class Logger { LogChannel channel, LogSeverity severity); + void flush(); + bool isChannelEnabled(LogChannel channel) const { return _enabledChannels.count(channel) > 0; } diff --git a/scripts/build.ps1 b/scripts/build.ps1 new file mode 100644 index 000000000..c114cd839 --- /dev/null +++ b/scripts/build.ps1 @@ -0,0 +1,201 @@ +#requires -Version 5.1 +[CmdletBinding()] +param( + [string]$BuildDir, + + [ValidateSet("Debug", "Release", "RelWithDebInfo", "MinSizeRel")] + [string]$Config = "RelWithDebInfo", + + [string]$Target = "engine", + + [switch]$Configure, + + [string[]]$CMakeArgs = @() +) + +$ErrorActionPreference = "Stop" + +$RepoRoot = (Resolve-Path (Join-Path $PSScriptRoot "..")).Path +if (-not $BuildDir) { + $BuildDir = Join-Path $RepoRoot "build" +} +$BuildDir = $ExecutionContext.SessionState.Path.GetUnresolvedProviderPathFromPSPath($BuildDir) +$LogsDir = Join-Path $RepoRoot ".agent\logs" +New-Item -ItemType Directory -Force -Path $BuildDir | Out-Null +New-Item -ItemType Directory -Force -Path $LogsDir | Out-Null + +function Resolve-CMakeExe { + $cmd = Get-Command cmake -ErrorAction SilentlyContinue + if ($cmd) { + return $cmd.Source + } + + if ($env:CMAKE_EXE -and (Test-Path $env:CMAKE_EXE)) { + return $env:CMAKE_EXE + } + + $defaultPath = "C:\Program Files\CMake\bin\cmake.exe" + if (Test-Path $defaultPath) { + return $defaultPath + } + + throw "cmake.exe was not found. Add CMake to PATH or set CMAKE_EXE." +} + +function Resolve-VcpkgToolchain { + if ($env:VCPKG_ROOT) { + $fromEnv = Join-Path $env:VCPKG_ROOT "scripts\buildsystems\vcpkg.cmake" + if (Test-Path $fromEnv) { + return $fromEnv + } + } + + $defaultPath = "D:\vcpkg\scripts\buildsystems\vcpkg.cmake" + if (Test-Path $defaultPath) { + return $defaultPath + } + + return $null +} + +function Resolve-VcpkgRootFromToolchain { + param([string]$Toolchain) + + if (-not $Toolchain) { + return $null + } + + $toolchainPath = $ExecutionContext.SessionState.Path.GetUnresolvedProviderPathFromPSPath($Toolchain) + $buildsystemsDir = Split-Path -Parent $toolchainPath + $scriptsDir = Split-Path -Parent $buildsystemsDir + + if ((Split-Path -Leaf $buildsystemsDir) -ne "buildsystems" -or + (Split-Path -Leaf $scriptsDir) -ne "scripts") { + return $null + } + + return (Split-Path -Parent $scriptsDir) +} + +function Resolve-VcpkgTargetTriplet { + param([string[]]$Arguments) + + foreach ($argument in $Arguments) { + if ($argument -like "-DVCPKG_TARGET_TRIPLET=*") { + return $argument.Substring("-DVCPKG_TARGET_TRIPLET=".Length) + } + } + + if ($env:VCPKG_DEFAULT_TRIPLET) { + return $env:VCPKG_DEFAULT_TRIPLET + } + + return "x64-windows" +} + +function Write-VcpkgPackageConfigHint { + param( + [string]$Toolchain, + [string[]]$Arguments + ) + + $vcpkgRoot = Resolve-VcpkgRootFromToolchain -Toolchain $Toolchain + if (-not $vcpkgRoot) { + return + } + + if (Test-Path (Join-Path $RepoRoot "vcpkg.json")) { + return + } + + $triplet = Resolve-VcpkgTargetTriplet -Arguments $Arguments + $sdl3ConfigDir = Join-Path $vcpkgRoot "installed\$triplet\share\sdl3" + if (Test-Path $sdl3ConfigDir) { + return + } + + $vcpkgExe = Join-Path $vcpkgRoot "vcpkg.exe" + $installCommand = "& `"$vcpkgExe`" install sdl3:${triplet}" + Write-Warning "SDL3 vcpkg config was not found at $sdl3ConfigDir. CMake requires find_package(SDL3 CONFIG REQUIRED). Install it with '$installCommand' or set VCPKG_ROOT/CMAKE_TOOLCHAIN_FILE to a vcpkg tree that has sdl3:${triplet}." +} + +function Invoke-CMakeLogged { + param( + [string[]]$Arguments, + [string]$LogPath + ) + + Write-Host "Using CMake: $script:CMakeExe" + Write-Host "cmake $($Arguments -join ' ')" + $previousErrorActionPreference = $ErrorActionPreference + $ErrorActionPreference = "Continue" + try { + & $script:CMakeExe @Arguments 2>&1 | Tee-Object -FilePath $LogPath + $exitCode = $LASTEXITCODE + } finally { + $ErrorActionPreference = $previousErrorActionPreference + } + if ($exitCode -ne 0) { + throw "CMake failed with exit code $exitCode. See $LogPath" + } +} + +function Test-CMakeProjectReady { + if (-not (Test-Path $cachePath)) { + return $false + } + + $generatedProject = Get-ChildItem -Path $BuildDir -File -ErrorAction SilentlyContinue | + Where-Object { $_.Name -eq "build.ninja" -or $_.Name -eq "Makefile" -or $_.Extension -eq ".sln" } | + Select-Object -First 1 + + return $null -ne $generatedProject +} + +$script:CMakeExe = Resolve-CMakeExe +$timestamp = Get-Date -Format "yyyyMMdd_HHmmss" +$buildLog = Join-Path $LogsDir "build_$timestamp.log" +$configureLog = Join-Path $LogsDir "configure_$timestamp.log" +$latestBuildLog = Join-Path $LogsDir "latest_build.log" +$latestConfigureLog = Join-Path $LogsDir "latest_configure.log" +$cachePath = Join-Path $BuildDir "CMakeCache.txt" + +try { + if ($Configure -or -not (Test-CMakeProjectReady)) { + $configureArgs = @("-S", $RepoRoot, "-B", $BuildDir, "-DCMAKE_BUILD_TYPE=$Config") + $vcpkgToolchain = Resolve-VcpkgToolchain + if ($vcpkgToolchain) { + $configureArgs += "-DCMAKE_TOOLCHAIN_FILE=$vcpkgToolchain" + } + if ($CMakeArgs.Count -gt 0) { + $configureArgs += $CMakeArgs + } + + Write-VcpkgPackageConfigHint -Toolchain $vcpkgToolchain -Arguments $configureArgs + Invoke-CMakeLogged -Arguments $configureArgs -LogPath $configureLog + Copy-Item -Force -Path $configureLog -Destination $latestConfigureLog + } + + $buildArgs = @("--build", $BuildDir, "--config", $Config) + if ($Target) { + $buildArgs += @("--target", $Target) + } + $buildArgs += "--parallel" + + Invoke-CMakeLogged -Arguments $buildArgs -LogPath $buildLog + Copy-Item -Force -Path $buildLog -Destination $latestBuildLog + + Write-Host "Build succeeded for target '$Target' ($Config)." + exit 0 +} catch { + if (Test-Path $buildLog) { + Copy-Item -Force -Path $buildLog -Destination $latestBuildLog + } elseif (Test-Path $configureLog) { + Copy-Item -Force -Path $configureLog -Destination $latestBuildLog + } + if (Test-Path $configureLog) { + Copy-Item -Force -Path $configureLog -Destination $latestConfigureLog + } + Write-Error $_ + exit 1 +} diff --git a/scripts/capture_logs.ps1 b/scripts/capture_logs.ps1 new file mode 100644 index 000000000..2504e5a9d --- /dev/null +++ b/scripts/capture_logs.ps1 @@ -0,0 +1,44 @@ +#requires -Version 5.1 +[CmdletBinding()] +param( + [string]$Destination, + + [string]$SmokeDir +) + +$ErrorActionPreference = "Stop" + +$RepoRoot = (Resolve-Path (Join-Path $PSScriptRoot "..")).Path +$LogsRoot = Join-Path $RepoRoot ".agent\logs" + +if (-not $Destination) { + $timestamp = Get-Date -Format "yyyyMMdd_HHmmss" + $Destination = Join-Path $LogsRoot "capture_$timestamp" +} + +$Destination = $ExecutionContext.SessionState.Path.GetUnresolvedProviderPathFromPSPath($Destination) +New-Item -ItemType Directory -Force -Path $Destination | Out-Null + +$paths = @( + (Join-Path $RepoRoot "engine.log"), + (Join-Path $RepoRoot "build\bin\engine.log"), + (Join-Path $RepoRoot "build\debug\bin\engine.log"), + (Join-Path $LogsRoot "latest_build.log"), + (Join-Path $LogsRoot "latest_configure.log") +) + +if (-not $SmokeDir) { + $SmokeDir = Join-Path $LogsRoot "smoke_latest" +} + +if (Test-Path $SmokeDir) { + $paths += $SmokeDir +} + +foreach ($path in $paths) { + if (Test-Path $path) { + Copy-Item -Recurse -Force -Path $path -Destination $Destination + } +} + +Write-Host "Captured available logs to $Destination" diff --git a/scripts/eval_smoke.ps1 b/scripts/eval_smoke.ps1 new file mode 100644 index 000000000..0771ce83f --- /dev/null +++ b/scripts/eval_smoke.ps1 @@ -0,0 +1,122 @@ +#requires -Version 5.1 +[CmdletBinding()] +param( + [string]$SmokeDir, + + [switch]$AllowMissingGame +) + +$ErrorActionPreference = "Stop" + +$RepoRoot = (Resolve-Path (Join-Path $PSScriptRoot "..")).Path +$LogsRoot = Join-Path $RepoRoot ".agent\logs" + +if (-not $SmokeDir) { + $SmokeDir = Join-Path $LogsRoot "smoke_latest" +} + +$SmokeDir = $ExecutionContext.SessionState.Path.GetUnresolvedProviderPathFromPSPath($SmokeDir) +$manifestPath = Join-Path $SmokeDir "smoke_manifest.json" + +if (-not (Test-Path $manifestPath)) { + Write-Error "Smoke manifest not found: $manifestPath" + exit 1 +} + +$manifest = Get-Content -Raw -Path $manifestPath | ConvertFrom-Json +$errors = New-Object System.Collections.Generic.List[string] +$warnings = New-Object System.Collections.Generic.List[string] + +if (-not $manifest.build.success) { + $errors.Add("Build did not succeed according to smoke manifest.") +} + +if (-not $manifest.build.engineExe -or -not (Test-Path $manifest.build.engineExe)) { + $errors.Add("Built engine executable was not found: $($manifest.build.engineExe)") +} + +if (-not $manifest.build.shaderpackErf -or -not (Test-Path $manifest.build.shaderpackErf)) { + $errors.Add("Built shaderpack.erf was not found: $($manifest.build.shaderpackErf)") +} + +if (-not $manifest.launch.attempted) { + if ($AllowMissingGame -and $manifest.launch.skipReason) { + $warnings.Add("Launch was skipped: $($manifest.launch.skipReason)") + } else { + $errors.Add("Engine process launch was not attempted.") + } +} else { + if (-not $manifest.launch.started) { + $errors.Add("Engine process launch was attempted but did not start.") + } + + if (-not $manifest.launch.timedOut -and $manifest.launch.exitCode -ne $null -and [int]$manifest.launch.exitCode -ne 0) { + $errors.Add("Engine exited with non-zero code $($manifest.launch.exitCode).") + } + + if (-not $manifest.launch.shaderpackErf -or -not (Test-Path $manifest.launch.shaderpackErf)) { + $errors.Add("Smoke launch shaderpack.erf was not found: $($manifest.launch.shaderpackErf)") + } +} + +$logPaths = @($manifest.build.log, $manifest.launch.stdout, $manifest.launch.stderr, $manifest.launch.engineLog) +$fatalPatterns = @( + "Engine failure", + "FATAL", + "Unhandled exception", + "Access violation", + "Segmentation fault", + "CMake Error", + "Build FAILED", + "fatal error" +) +$startupSignal = "reone smoke signal: engine startup" +if ($manifest.evalHints -and $manifest.evalHints.startupSignal) { + $startupSignal = $manifest.evalHints.startupSignal +} + +foreach ($path in $logPaths) { + if ($path -and (Test-Path $path)) { + $matches = Select-String -Path $path -Pattern $fatalPatterns -SimpleMatch -ErrorAction SilentlyContinue + foreach ($match in $matches) { + $errors.Add("Fatal pattern in $path line $($match.LineNumber): $($match.Line.Trim())") + } + } +} + +$engineLog = $manifest.launch.engineLog +if ($manifest.launch.attempted -and $engineLog -and (Test-Path $engineLog)) { + $engineLogText = Get-Content -Raw -Path $engineLog + if ($null -eq $engineLogText) { + $engineLogText = "" + } + if ($engineLogText.Trim().Length -gt 0) { + if ($engineLogText -notmatch "(?m)^\s*(ERROR|WARN|INFO|DEBUG)\s+\[") { + $errors.Add("engine.log exists but does not contain expected reone-formatted startup log lines.") + } + $escapedStartupSignal = [regex]::Escape($startupSignal) + if ($engineLogText -notmatch "(?m)^\s*INFO\s+\[main\]\[global\]\s+$escapedStartupSignal\s*$") { + $errors.Add("engine.log does not contain required startup signal: $startupSignal") + } + } else { + $errors.Add("engine.log was empty; required startup signal was not emitted.") + } +} elseif ($manifest.launch.attempted) { + $errors.Add("engine.log was not available to inspect for required startup signal.") +} + +if ($warnings.Count -gt 0) { + foreach ($warning in $warnings) { + Write-Warning $warning + } +} + +if ($errors.Count -gt 0) { + foreach ($errorMessage in $errors) { + Write-Error $errorMessage -ErrorAction Continue + } + exit 1 +} + +Write-Host "Smoke eval passed for $SmokeDir" +exit 0 diff --git a/scripts/run_k1.ps1 b/scripts/run_k1.ps1 new file mode 100644 index 000000000..dd9e0cad0 --- /dev/null +++ b/scripts/run_k1.ps1 @@ -0,0 +1,139 @@ +#requires -Version 5.1 +[CmdletBinding()] +param( + [string]$GameDir = $env:KOTOR1_DIR, + + [string]$BuildDir, + + [ValidateSet("Debug", "Release", "RelWithDebInfo", "MinSizeRel")] + [string]$Config = "RelWithDebInfo", + + [switch]$Dev, + + [ValidateRange(0, 3)] + [int]$LogSeverity, + + [string[]]$EngineArgs = @() +) + +$ErrorActionPreference = "Stop" + +$RepoRoot = (Resolve-Path (Join-Path $PSScriptRoot "..")).Path +if (-not $BuildDir) { + $BuildDir = Join-Path $RepoRoot "build" +} +$BuildDir = $ExecutionContext.SessionState.Path.GetUnresolvedProviderPathFromPSPath($BuildDir) + +function Resolve-EngineExe { + $candidates = @( + (Join-Path $BuildDir "bin\engine.exe"), + (Join-Path $BuildDir "debug\bin\engine.exe") + ) + + foreach ($candidate in $candidates) { + if (Test-Path $candidate) { + return (Resolve-Path $candidate).Path + } + } + + $found = Get-ChildItem -Path $BuildDir -Recurse -Filter "engine.exe" -ErrorAction SilentlyContinue | Select-Object -First 1 + if ($found) { + return $found.FullName + } + + return $null +} + +function Resolve-ShaderPackErf { + $candidates = @( + (Join-Path $BuildDir "bin\shaderpack.erf"), + (Join-Path $BuildDir "debug\bin\shaderpack.erf") + ) + + foreach ($candidate in $candidates) { + if (Test-Path $candidate) { + return (Resolve-Path $candidate).Path + } + } + + $found = Get-ChildItem -Path $BuildDir -Recurse -Filter "shaderpack.erf" -ErrorAction SilentlyContinue | Select-Object -First 1 + if ($found) { + return $found.FullName + } + + return $null +} + +$engineExe = Resolve-EngineExe +if (-not $engineExe) { + & powershell -NoProfile -ExecutionPolicy Bypass -File (Join-Path $PSScriptRoot "build.ps1") -BuildDir $BuildDir -Config $Config -Target "engine" + if ($LASTEXITCODE -ne 0) { + exit $LASTEXITCODE + } + $engineExe = Resolve-EngineExe +} + +$shaderPackErf = Resolve-ShaderPackErf +if (-not $shaderPackErf) { + & powershell -NoProfile -ExecutionPolicy Bypass -File (Join-Path $PSScriptRoot "build.ps1") -BuildDir $BuildDir -Config $Config -Target "create_shaderpack" + if ($LASTEXITCODE -ne 0) { + exit $LASTEXITCODE + } + $shaderPackErf = Resolve-ShaderPackErf +} + +if (-not $engineExe) { + Write-Error "engine.exe was not found after build." + exit 1 +} + +if (-not $shaderPackErf) { + Write-Error "shaderpack.erf was not found after building create_shaderpack." + exit 1 +} + +if (-not $GameDir) { + Write-Error "Pass -GameDir or set KOTOR1_DIR to a legal KOTOR 1 install directory." + exit 2 +} + +$GameDir = (Resolve-Path $GameDir).Path +$marker = Join-Path $GameDir "swkotor.exe" +if (-not (Test-Path $marker)) { + Write-Error "KOTOR 1 marker executable was not found: $marker" + exit 2 +} + +if ($Dev) { + $hasDevArg = $false + foreach ($arg in $EngineArgs) { + if ($arg -eq "--dev" -or $arg -like "--dev=*") { + $hasDevArg = $true + break + } + } + if (-not $hasDevArg) { + $EngineArgs = @("--dev", "true") + $EngineArgs + } +} + +if ($PSBoundParameters.ContainsKey("LogSeverity")) { + $hasLogSeverityArg = $false + foreach ($arg in $EngineArgs) { + if ($arg -eq "--logsev" -or $arg -like "--logsev=*") { + $hasLogSeverityArg = $true + break + } + } + if (-not $hasLogSeverityArg) { + $EngineArgs = @("--logsev", "$LogSeverity") + $EngineArgs + } +} + +Push-Location (Split-Path -Parent $engineExe) +try { + & $engineExe "--game" $GameDir @EngineArgs + exit $LASTEXITCODE +} finally { + Pop-Location +} diff --git a/scripts/run_k2.ps1 b/scripts/run_k2.ps1 new file mode 100644 index 000000000..18eeaa33a --- /dev/null +++ b/scripts/run_k2.ps1 @@ -0,0 +1,139 @@ +#requires -Version 5.1 +[CmdletBinding()] +param( + [string]$GameDir = $env:KOTOR2_DIR, + + [string]$BuildDir, + + [ValidateSet("Debug", "Release", "RelWithDebInfo", "MinSizeRel")] + [string]$Config = "RelWithDebInfo", + + [switch]$Dev, + + [ValidateRange(0, 3)] + [int]$LogSeverity, + + [string[]]$EngineArgs = @() +) + +$ErrorActionPreference = "Stop" + +$RepoRoot = (Resolve-Path (Join-Path $PSScriptRoot "..")).Path +if (-not $BuildDir) { + $BuildDir = Join-Path $RepoRoot "build" +} +$BuildDir = $ExecutionContext.SessionState.Path.GetUnresolvedProviderPathFromPSPath($BuildDir) + +function Resolve-EngineExe { + $candidates = @( + (Join-Path $BuildDir "bin\engine.exe"), + (Join-Path $BuildDir "debug\bin\engine.exe") + ) + + foreach ($candidate in $candidates) { + if (Test-Path $candidate) { + return (Resolve-Path $candidate).Path + } + } + + $found = Get-ChildItem -Path $BuildDir -Recurse -Filter "engine.exe" -ErrorAction SilentlyContinue | Select-Object -First 1 + if ($found) { + return $found.FullName + } + + return $null +} + +function Resolve-ShaderPackErf { + $candidates = @( + (Join-Path $BuildDir "bin\shaderpack.erf"), + (Join-Path $BuildDir "debug\bin\shaderpack.erf") + ) + + foreach ($candidate in $candidates) { + if (Test-Path $candidate) { + return (Resolve-Path $candidate).Path + } + } + + $found = Get-ChildItem -Path $BuildDir -Recurse -Filter "shaderpack.erf" -ErrorAction SilentlyContinue | Select-Object -First 1 + if ($found) { + return $found.FullName + } + + return $null +} + +$engineExe = Resolve-EngineExe +if (-not $engineExe) { + & powershell -NoProfile -ExecutionPolicy Bypass -File (Join-Path $PSScriptRoot "build.ps1") -BuildDir $BuildDir -Config $Config -Target "engine" + if ($LASTEXITCODE -ne 0) { + exit $LASTEXITCODE + } + $engineExe = Resolve-EngineExe +} + +$shaderPackErf = Resolve-ShaderPackErf +if (-not $shaderPackErf) { + & powershell -NoProfile -ExecutionPolicy Bypass -File (Join-Path $PSScriptRoot "build.ps1") -BuildDir $BuildDir -Config $Config -Target "create_shaderpack" + if ($LASTEXITCODE -ne 0) { + exit $LASTEXITCODE + } + $shaderPackErf = Resolve-ShaderPackErf +} + +if (-not $engineExe) { + Write-Error "engine.exe was not found after build." + exit 1 +} + +if (-not $shaderPackErf) { + Write-Error "shaderpack.erf was not found after building create_shaderpack." + exit 1 +} + +if (-not $GameDir) { + Write-Error "Pass -GameDir or set KOTOR2_DIR to a legal KOTOR 2 install directory." + exit 2 +} + +$GameDir = (Resolve-Path $GameDir).Path +$marker = Join-Path $GameDir "swkotor2.exe" +if (-not (Test-Path $marker)) { + Write-Error "KOTOR 2 marker executable was not found: $marker" + exit 2 +} + +if ($Dev) { + $hasDevArg = $false + foreach ($arg in $EngineArgs) { + if ($arg -eq "--dev" -or $arg -like "--dev=*") { + $hasDevArg = $true + break + } + } + if (-not $hasDevArg) { + $EngineArgs = @("--dev", "true") + $EngineArgs + } +} + +if ($PSBoundParameters.ContainsKey("LogSeverity")) { + $hasLogSeverityArg = $false + foreach ($arg in $EngineArgs) { + if ($arg -eq "--logsev" -or $arg -like "--logsev=*") { + $hasLogSeverityArg = $true + break + } + } + if (-not $hasLogSeverityArg) { + $EngineArgs = @("--logsev", "$LogSeverity") + $EngineArgs + } +} + +Push-Location (Split-Path -Parent $engineExe) +try { + & $engineExe "--game" $GameDir @EngineArgs + exit $LASTEXITCODE +} finally { + Pop-Location +} diff --git a/scripts/smoke_test.ps1 b/scripts/smoke_test.ps1 new file mode 100644 index 000000000..f35e6da5e --- /dev/null +++ b/scripts/smoke_test.ps1 @@ -0,0 +1,356 @@ +#requires -Version 5.1 +[CmdletBinding()] +param( + [string]$GameDir, + + [ValidateSet("auto", "k1", "k2")] + [string]$Game = "auto", + + [string]$BuildDir, + + [ValidateSet("Debug", "Release", "RelWithDebInfo", "MinSizeRel")] + [string]$Config = "RelWithDebInfo", + + [int]$TimeoutSeconds = 20, + + [switch]$NoBuild, + + [switch]$ForceBuild, + + [switch]$AllowMissingGame +) + +$ErrorActionPreference = "Stop" + +$RepoRoot = (Resolve-Path (Join-Path $PSScriptRoot "..")).Path +if (-not $BuildDir) { + $BuildDir = Join-Path $RepoRoot "build" +} +$BuildDir = $ExecutionContext.SessionState.Path.GetUnresolvedProviderPathFromPSPath($BuildDir) +$LogsRoot = Join-Path $RepoRoot ".agent\logs" +$timestamp = Get-Date -Format "yyyyMMdd_HHmmss" +$SmokeDir = Join-Path $LogsRoot "smoke_$timestamp" +$LatestSmokeDir = Join-Path $LogsRoot "smoke_latest" +New-Item -ItemType Directory -Force -Path $SmokeDir | Out-Null + +$stdoutPath = Join-Path $SmokeDir "stdout.log" +$stderrPath = Join-Path $SmokeDir "stderr.log" +$engineLogPath = Join-Path $SmokeDir "engine.log" +$manifestPath = Join-Path $SmokeDir "smoke_manifest.json" +$startupSignal = "reone smoke signal: engine startup" +New-Item -ItemType File -Force -Path $stdoutPath | Out-Null +New-Item -ItemType File -Force -Path $stderrPath | Out-Null +New-Item -ItemType File -Force -Path $engineLogPath | Out-Null + +function Resolve-EngineExe { + $candidates = @( + (Join-Path $BuildDir "bin\engine.exe"), + (Join-Path $BuildDir "debug\bin\engine.exe") + ) + + foreach ($candidate in $candidates) { + if (Test-Path $candidate) { + return (Resolve-Path $candidate).Path + } + } + + $found = Get-ChildItem -Path $BuildDir -Recurse -Filter "engine.exe" -ErrorAction SilentlyContinue | Select-Object -First 1 + if ($found) { + return $found.FullName + } + + return $null +} + +function Resolve-ShaderPackErf { + $candidates = @( + (Join-Path $BuildDir "bin\shaderpack.erf"), + (Join-Path $BuildDir "debug\bin\shaderpack.erf") + ) + + foreach ($candidate in $candidates) { + if (Test-Path $candidate) { + return (Resolve-Path $candidate).Path + } + } + + $found = Get-ChildItem -Path $BuildDir -Recurse -Filter "shaderpack.erf" -ErrorAction SilentlyContinue | Select-Object -First 1 + if ($found) { + return $found.FullName + } + + return $null +} + +function Resolve-GameDirectory { + param( + [string]$Path, + [string]$RequestedGame + ) + + if (-not $Path) { + if ($env:KOTOR1_DIR) { + $Path = $env:KOTOR1_DIR + if ($RequestedGame -eq "auto") { + $RequestedGame = "k1" + } + } elseif ($env:KOTOR2_DIR) { + $Path = $env:KOTOR2_DIR + if ($RequestedGame -eq "auto") { + $RequestedGame = "k2" + } + } + } + + if (-not $Path) { + return $null + } + + $resolved = (Resolve-Path $Path).Path + $k1Marker = Join-Path $resolved "swkotor.exe" + $k2Marker = Join-Path $resolved "swkotor2.exe" + + if ($RequestedGame -eq "k1" -and -not (Test-Path $k1Marker)) { + throw "KOTOR 1 marker executable was not found: $k1Marker" + } + if ($RequestedGame -eq "k2" -and -not (Test-Path $k2Marker)) { + throw "KOTOR 2 marker executable was not found: $k2Marker" + } + if ($RequestedGame -eq "auto") { + if (Test-Path $k1Marker) { + $RequestedGame = "k1" + } elseif (Test-Path $k2Marker) { + $RequestedGame = "k2" + } else { + throw "No KOTOR marker executable was found in: $resolved" + } + } + + return [ordered]@{ + path = $resolved + game = $RequestedGame + } +} + +function Write-Manifest { + param([object]$Manifest) + $Manifest | ConvertTo-Json -Depth 8 | Set-Content -Path $manifestPath -Encoding UTF8 +} + +function Publish-LatestSmoke { + if (Test-Path $LatestSmokeDir) { + Remove-Item -Recurse -Force -Path $LatestSmokeDir + } + Copy-Item -Recurse -Force -Path $SmokeDir -Destination $LatestSmokeDir +} + +function Find-FatalMatches { + param([string[]]$Paths) + + $fatalPatterns = @( + "Engine failure", + "FATAL", + "Unhandled exception", + "Access violation", + "Segmentation fault", + "CMake Error", + "Build FAILED", + "fatal error" + ) + + $matches = @() + foreach ($path in $Paths) { + if (Test-Path $path) { + $matches += Select-String -Path $path -Pattern $fatalPatterns -SimpleMatch -ErrorAction SilentlyContinue + } + } + + return $matches +} + +function Join-ProcessArguments { + param([string[]]$Arguments) + + $quoted = foreach ($argument in $Arguments) { + $escaped = $argument -replace '"', '\"' + if ($escaped -match '\s') { + '"' + $escaped + '"' + } else { + $escaped + } + } + + return ($quoted -join " ") +} + +$manifest = [ordered]@{ + schema = 1 + createdAt = (Get-Date).ToString("o") + repoRoot = $RepoRoot + build = [ordered]@{ + success = $false + action = "not-started" + config = $Config + target = "engine" + buildDir = $BuildDir + engineExe = $null + shaderpackAction = "not-started" + shaderpackErf = $null + log = (Join-Path $LogsRoot "latest_build.log") + } + launch = [ordered]@{ + attempted = $false + started = $false + timedOut = $false + timeoutSeconds = $TimeoutSeconds + exitCode = $null + game = $Game + gameDir = $null + skipReason = $null + stdout = $stdoutPath + stderr = $stderrPath + engineLog = $engineLogPath + shaderpackErf = $null + } + evalHints = [ordered]@{ + obviousFatalPatterns = @("Engine failure", "FATAL", "Unhandled exception", "Access violation", "Segmentation fault", "CMake Error", "Build FAILED", "fatal error") + startupSignal = $startupSignal + } +} + +try { + $gameInfo = Resolve-GameDirectory -Path $GameDir -RequestedGame $Game + if ($gameInfo) { + $manifest.launch.game = $gameInfo.game + $manifest.launch.gameDir = $gameInfo.path + } + + $engineExe = Resolve-EngineExe + if (($ForceBuild -or -not $engineExe) -and -not $NoBuild) { + $manifest.build.action = "built" + Write-Manifest -Manifest $manifest + & powershell -NoProfile -ExecutionPolicy Bypass -File (Join-Path $PSScriptRoot "build.ps1") -BuildDir $BuildDir -Config $Config -Target "engine" + if ($LASTEXITCODE -ne 0) { + $manifest.build.success = $false + $manifest.launch.skipReason = "Build failed before launch. See $($manifest.build.log)." + Write-Manifest -Manifest $manifest + Publish-LatestSmoke + exit $LASTEXITCODE + } + $engineExe = Resolve-EngineExe + } elseif ($engineExe) { + $manifest.build.action = "verified-existing" + } else { + $manifest.build.action = "missing" + } + + if (-not $engineExe) { + $manifest.build.success = $false + $manifest.launch.skipReason = "engine.exe was not found before launch." + Write-Manifest -Manifest $manifest + Publish-LatestSmoke + Write-Error "engine.exe was not found. Run scripts\build.ps1 or omit -NoBuild." + exit 1 + } + + $manifest.build.success = $true + $manifest.build.engineExe = $engineExe + + $shaderPackErf = Resolve-ShaderPackErf + if (($ForceBuild -or -not $shaderPackErf) -and -not $NoBuild) { + $manifest.build.shaderpackAction = "built" + Write-Manifest -Manifest $manifest + & powershell -NoProfile -ExecutionPolicy Bypass -File (Join-Path $PSScriptRoot "build.ps1") -BuildDir $BuildDir -Config $Config -Target "create_shaderpack" + if ($LASTEXITCODE -ne 0) { + $manifest.build.success = $false + $manifest.launch.skipReason = "Shader pack build failed before launch. See $($manifest.build.log)." + Write-Manifest -Manifest $manifest + Publish-LatestSmoke + exit $LASTEXITCODE + } + $shaderPackErf = Resolve-ShaderPackErf + } elseif ($shaderPackErf) { + $manifest.build.shaderpackAction = "verified-existing" + } else { + $manifest.build.shaderpackAction = "missing" + } + + if (-not $shaderPackErf) { + $manifest.build.success = $false + $manifest.launch.skipReason = "shaderpack.erf was not found before launch." + Write-Manifest -Manifest $manifest + Publish-LatestSmoke + Write-Error "shaderpack.erf was not found. Build the create_shaderpack target or omit -NoBuild." + exit 1 + } + + $manifest.build.shaderpackErf = $shaderPackErf + + if (-not $gameInfo) { + $manifest.launch.skipReason = "No legal KOTOR or TSL game directory was provided. Pass -GameDir or set KOTOR1_DIR/KOTOR2_DIR." + Write-Manifest -Manifest $manifest + Publish-LatestSmoke + if ($AllowMissingGame) { + Write-Host $manifest.launch.skipReason + exit 0 + } + Write-Error $manifest.launch.skipReason + exit 2 + } + + $manifest.launch.attempted = $true + $manifest.launch.game = $gameInfo.game + $manifest.launch.gameDir = $gameInfo.path + $manifest.launch.shaderpackErf = Join-Path $SmokeDir "shaderpack.erf" + Copy-Item -Force -Path $shaderPackErf -Destination $manifest.launch.shaderpackErf + Write-Manifest -Manifest $manifest + + $engineArgs = @( + "--game", $gameInfo.path, + "--width", "640", + "--height", "480", + "--winscale", "100", + "--fullscreen", "false", + "--logsev", "1" + ) + + $process = Start-Process -FilePath $engineExe -ArgumentList (Join-ProcessArguments -Arguments $engineArgs) -WorkingDirectory $SmokeDir -RedirectStandardOutput $stdoutPath -RedirectStandardError $stderrPath -PassThru + $manifest.launch.started = $true + Write-Manifest -Manifest $manifest + + $finished = $process.WaitForExit($TimeoutSeconds * 1000) + if ($finished) { + $manifest.launch.exitCode = $process.ExitCode + } else { + $manifest.launch.timedOut = $true + Stop-Process -Id $process.Id -Force -ErrorAction SilentlyContinue + Start-Sleep -Milliseconds 250 + } + + if (-not (Test-Path $engineLogPath)) { + New-Item -ItemType File -Path $engineLogPath | Out-Null + } + + $fatalMatches = Find-FatalMatches -Paths @($stdoutPath, $stderrPath, $engineLogPath) + Write-Manifest -Manifest $manifest + Publish-LatestSmoke + + if ($fatalMatches.Count -gt 0) { + Write-Error "Smoke found obvious fatal output. See $SmokeDir" + exit 1 + } + + if ($manifest.launch.exitCode -ne $null -and [int]$manifest.launch.exitCode -ne 0) { + Write-Error "Engine exited with code $($manifest.launch.exitCode). See $SmokeDir" + exit 1 + } + + Write-Host "Smoke completed. Artifacts: $SmokeDir" + exit 0 +} catch { + $manifest.launch.skipReason = $_.Exception.Message + Write-Manifest -Manifest $manifest + Publish-LatestSmoke + Write-Error $_ + exit 1 +} diff --git a/src/apps/engine/main.cpp b/src/apps/engine/main.cpp index bb4bfd753..ddb8d5603 100644 --- a/src/apps/engine/main.cpp +++ b/src/apps/engine/main.cpp @@ -22,6 +22,7 @@ #endif #include "reone/system/logger.h" +#include "reone/system/logutil.h" #include "reone/system/threadutil.h" #include "engine.h" @@ -31,6 +32,7 @@ using namespace reone; using namespace reone::graphics; static constexpr char kLogFilename[] = "engine.log"; +static constexpr char kEngineStartupMessage[] = "reone smoke signal: engine startup"; int main(int argc, char **argv) { #ifdef _WIN32 @@ -48,6 +50,8 @@ int main(int argc, char **argv) { } try { Logger::instance.init(options->logging.severity, options->logging.channels, kLogFilename); + info(kEngineStartupMessage); + Logger::instance.flush(); } catch (const std::exception &ex) { std::cerr << "Error initializing logging: " << ex.what() << std::endl; return 2; diff --git a/src/libs/game/game.cpp b/src/libs/game/game.cpp index 221e3c576..816fd4e22 100644 --- a/src/libs/game/game.cpp +++ b/src/libs/game/game.cpp @@ -35,6 +35,7 @@ #include "reone/game/surfaces.h" #include "reone/graphics/context.h" #include "reone/graphics/di/services.h" +#include "reone/graphics/font.h" #include "reone/graphics/format/tgawriter.h" #include "reone/graphics/meshregistry.h" #include "reone/graphics/renderbuffer.h" @@ -53,6 +54,7 @@ #include "reone/resource/provider/audioclips.h" #include "reone/resource/provider/cursors.h" #include "reone/resource/provider/dialogs.h" +#include "reone/resource/provider/fonts.h" #include "reone/resource/provider/gffs.h" #include "reone/resource/provider/lips.h" #include "reone/resource/provider/models.h" @@ -87,6 +89,138 @@ namespace reone { namespace game { +static constexpr char kDeveloperOverlayToggleHelp[] = "Ctrl+Shift+D"; +static constexpr char kDeveloperTriggerToggleHelp[] = "Ctrl+Shift+T"; +static constexpr char kDeveloperActorToggleHelp[] = "Ctrl+Shift+A"; +static constexpr char kDeveloperWatchToggleHelp[] = "Ctrl+Shift+W"; +static constexpr float kDeveloperActorLabelDistance = 32.0f; +static constexpr float kDeveloperLineWidth = 2.0f; + +static const char *screenName(Game::Screen screen) { + switch (screen) { + case Game::Screen::None: + return "None"; + case Game::Screen::MainMenu: + return "MainMenu"; + case Game::Screen::Loading: + return "Loading"; + case Game::Screen::CharacterGeneration: + return "CharacterGeneration"; + case Game::Screen::InGame: + return "InGame"; + case Game::Screen::InGameMenu: + return "InGameMenu"; + case Game::Screen::Conversation: + return "Conversation"; + case Game::Screen::Container: + return "Container"; + case Game::Screen::PartySelection: + return "PartySelection"; + case Game::Screen::SaveLoad: + return "SaveLoad"; + default: + return "Unknown"; + } +} + +static const char *cameraTypeName(CameraType type) { + switch (type) { + case CameraType::FirstPerson: + return "FirstPerson"; + case CameraType::ThirdPerson: + return "ThirdPerson"; + case CameraType::Static: + return "Static"; + case CameraType::Animated: + return "Animated"; + case CameraType::Dialog: + return "Dialog"; + default: + return "Unknown"; + } +} + +static const char *objectTypeName(ObjectType type) { + switch (type) { + case ObjectType::Creature: + return "creature"; + case ObjectType::Item: + return "item"; + case ObjectType::Trigger: + return "trigger"; + case ObjectType::Door: + return "door"; + case ObjectType::Waypoint: + return "waypoint"; + case ObjectType::Placeable: + return "placeable"; + case ObjectType::Store: + return "store"; + case ObjectType::Encounter: + return "encounter"; + case ObjectType::Sound: + return "sound"; + case ObjectType::Module: + return "module"; + case ObjectType::Area: + return "area"; + case ObjectType::Room: + return "room"; + case ObjectType::Camera: + return "camera"; + default: + return "object"; + } +} + +static bool isDeveloperOverlayChord(const input::KeyEvent &event) { + bool control = (event.mod & input::KeyModifiers::control) != 0; + bool shift = (event.mod & input::KeyModifiers::shift) != 0; + return control && shift; +} + +static const char *triggerDebugStateName(Trigger::DebugState state) { + switch (state) { + case Trigger::DebugState::Entered: + return "enter"; + case Trigger::DebugState::Inside: + return "inside"; + case Trigger::DebugState::Tested: + return "tested"; + default: + return "default"; + } +} + +static glm::vec4 triggerDebugColor(Trigger::DebugState state) { + switch (state) { + case Trigger::DebugState::Entered: + return glm::vec4(1.0f, 0.42f, 0.12f, 0.95f); + case Trigger::DebugState::Inside: + return glm::vec4(0.16f, 0.95f, 0.38f, 0.95f); + case Trigger::DebugState::Tested: + return glm::vec4(1.0f, 0.88f, 0.18f, 0.95f); + default: + return glm::vec4(0.48f, 0.74f, 1.0f, 0.85f); + } +} + +static int getDebugFaction(const std::shared_ptr &object) { + if (!object) { + return -1; + } + if (auto creature = std::dynamic_pointer_cast(object)) { + return static_cast(creature->faction()); + } + if (auto door = std::dynamic_pointer_cast(object)) { + return static_cast(door->faction()); + } + if (auto placeable = std::dynamic_pointer_cast(object)) { + return static_cast(placeable->faction()); + } + return -1; +} + void Game::init() { initConsole(); initLocalServices(); @@ -254,6 +388,10 @@ bool Game::handleKeyDown(const input::KeyEvent &event) { if (event.repeat) return false; + if (handleDeveloperKeyDown(event)) { + return true; + } + switch (event.code) { case input::KeyCode::Minus: if (_options.game.developer && _gameSpeed > 1.0f) { @@ -283,6 +421,47 @@ bool Game::handleKeyDown(const input::KeyEvent &event) { return false; } +bool Game::handleDeveloperKeyDown(const input::KeyEvent &event) { + if (!_options.game.developer || _screen != Screen::InGame) { + return false; + } + if (!isDeveloperOverlayChord(event)) { + return false; + } + + switch (event.code) { + case input::KeyCode::D: + _developerOverlay.visible = !_developerOverlay.visible; + return true; + case input::KeyCode::T: + if (!_developerOverlay.visible) { + _developerOverlay.visible = true; + _developerOverlay.triggers = true; + } else { + _developerOverlay.triggers = !_developerOverlay.triggers; + } + return true; + case input::KeyCode::A: + if (!_developerOverlay.visible) { + _developerOverlay.visible = true; + _developerOverlay.actorLabels = true; + } else { + _developerOverlay.actorLabels = !_developerOverlay.actorLabels; + } + return true; + case input::KeyCode::W: + if (!_developerOverlay.visible) { + _developerOverlay.visible = true; + _developerOverlay.watchedValues = true; + } else { + _developerOverlay.watchedValues = !_developerOverlay.watchedValues; + } + return true; + default: + return false; + } +} + bool Game::handleMouseMotion(const input::MouseMotionEvent &event) { _cursor->setPosition({event.x, event.y}); return false; @@ -524,6 +703,296 @@ void Game::renderGUI() { if (_cursor && !_relativeMouseMode) { _cursor->render(); } + renderDeveloperOverlay(); +} + +void Game::renderDeveloperOverlay() { + if (!_options.game.developer || !_developerOverlay.visible || !_module || _screen != Screen::InGame) { + return; + } + if (!_developerFont) { + _developerFont = _services.resource.fonts.get("fnt_console"); + } + if (!_developerFont) { + return; + } + + auto camera = getActiveCamera(); + bool hasCamera = camera != nullptr; + glm::mat4 projection(1.0f); + glm::mat4 view(1.0f); + if (camera) { + projection = camera->cameraSceneNode()->camera()->projection(); + view = camera->cameraSceneNode()->camera()->view(); + } + + _services.graphics.uniforms.setGlobals([this](auto &globals) { + globals.reset(); + globals.projection = glm::ortho( + 0.0f, + static_cast(_options.graphics.width), + static_cast(_options.graphics.height), + 0.0f, 0.0f, 100.0f); + globals.projectionInv = glm::inverse(globals.projection); + }); + _services.graphics.context.withBlendMode(BlendMode::Normal, [this]() { + renderDeveloperBanner(); + }); + _services.graphics.context.withBlendMode(BlendMode::Normal, [this, hasCamera, &projection, &view]() { + if (_developerOverlay.triggers && hasCamera) { + renderDeveloperTriggerOverlay(projection, view); + } + if (_developerOverlay.actorLabels && hasCamera) { + renderDeveloperActorLabels(projection, view); + } + if (_developerOverlay.watchedValues) { + renderDeveloperWatchedValues(); + } + }); +} + +void Game::renderDeveloperBanner() { + std::vector lines; + lines.push_back("DEV OBSERVABILITY"); + lines.push_back(str(boost::format("%s overlay | %s triggers | %s labels") % + kDeveloperOverlayToggleHelp % + kDeveloperTriggerToggleHelp % + kDeveloperActorToggleHelp)); + lines.push_back(str(boost::format("%s watch | ` console | F5 profiler") % + kDeveloperWatchToggleHelp)); + lines.push_back("V camera | +/- speed"); + + float maxWidth = 0.0f; + for (const auto &line : lines) { + maxWidth = glm::max(maxWidth, _developerFont->measure(line)); + } + renderDeveloperPanel( + lines, + glm::vec2(0.5f * (static_cast(_options.graphics.width) - maxWidth - 14.0f), 12.0f), + glm::vec3(0.58f, 1.0f, 0.58f)); +} + +void Game::renderDeveloperTriggerOverlay(const glm::mat4 &projection, const glm::mat4 &view) { + static glm::vec4 viewport(0.0f, 0.0f, 1.0f, 1.0f); + auto area = _module ? _module->area() : nullptr; + if (!area) { + return; + } + + const auto &opts = _options.graphics; + for (const auto &object : area->getObjectsByType(ObjectType::Trigger)) { + auto trigger = std::static_pointer_cast(object); + const auto &geometry = trigger->geometry(); + if (geometry.size() < 2) { + continue; + } + + std::vector points; + points.reserve(geometry.size()); + glm::vec3 centroid(0.0f); + bool anyVisible = false; + for (const auto &localPoint : geometry) { + glm::vec3 worldPoint = trigger->position() + localPoint; + centroid += worldPoint; + glm::vec3 screen = glm::project(worldPoint, view, projection, viewport); + points.push_back(glm::vec2(opts.width * screen.x, opts.height * (1.0f - screen.y))); + if (screen.z < 1.0f) { + anyVisible = true; + } + } + if (!anyVisible) { + continue; + } + + auto state = trigger->debugState(); + glm::vec4 color = triggerDebugColor(state); + for (size_t i = 0; i < points.size(); ++i) { + size_t next = (i + 1) % points.size(); + renderDeveloperLine(points[i], points[next], color, kDeveloperLineWidth); + } + + centroid /= static_cast(geometry.size()); + glm::vec3 labelScreen = glm::project(centroid, view, projection, viewport); + if (labelScreen.z < 1.0f) { + std::string label = str(boost::format("#%u %s %s [%s]") % + trigger->id() % + trigger->tag() % + (trigger->getOnEnter().empty() ? "-" : trigger->getOnEnter()) % + triggerDebugStateName(state)); + glm::vec3 position(opts.width * labelScreen.x, opts.height * (1.0f - labelScreen.y), 0.0f); + renderDeveloperText(label, position, glm::vec3(color), TextGravity::CenterBottom); + } + } +} + +void Game::renderDeveloperActorLabels(const glm::mat4 &projection, const glm::mat4 &view) { + auto area = _module ? _module->area() : nullptr; + auto leader = _party.getLeader(); + if (!area || !leader) { + return; + } + + const auto &opts = _options.graphics; + int rendered = 0; + for (const auto &object : area->objects()) { + bool supported = object->type() == ObjectType::Creature || + object->type() == ObjectType::Door || + object->type() == ObjectType::Placeable; + bool inspected = object == area->hilightedObject() || object == area->selectedObject(); + if (!supported && !inspected) { + continue; + } + + float distance = object->getDistanceTo(*leader); + if (!inspected && distance > kDeveloperActorLabelDistance) { + continue; + } + + glm::vec3 screen = area->getSelectableScreenCoords(object, projection, view); + if (screen.z >= 1.0f) { + continue; + } + + bool hostile = false; + auto creature = std::dynamic_pointer_cast(object); + if (creature) { + hostile = !creature->isDead() && _services.game.reputes.getIsEnemy(*leader, *creature); + } + + glm::vec3 color = inspected ? glm::vec3(1.0f, 1.0f, 1.0f) : (hostile ? glm::vec3(1.0f, 0.42f, 0.36f) : glm::vec3(0.68f, 0.92f, 1.0f)); + std::string label = str(boost::format("#%u %s %s f=%d H=%d sel=%d cmd=%d vis=%d plot=%d") % + object->id() % + object->tag() % + object->blueprintResRef() % + getDebugFaction(object) % + static_cast(hostile) % + static_cast(object->isSelectable()) % + static_cast(object->isCommandable()) % + static_cast(object->visible()) % + static_cast(object->plotFlag())); + + glm::vec3 position(opts.width * screen.x, opts.height * (1.0f - screen.y) - 18.0f - (rendered % 2) * 10.0f, 0.0f); + renderDeveloperText(label, position, color, TextGravity::CenterBottom); + if (++rendered >= 16) { + break; + } + } +} + +void Game::renderDeveloperWatchedValues() { + auto area = _module ? _module->area() : nullptr; + auto leader = _party.getLeader(); + auto selected = area ? area->selectedObject() : nullptr; + auto hover = area ? area->hilightedObject() : nullptr; + std::string room = leader && leader->room() ? leader->room()->name() : "-"; + glm::vec3 position = leader ? leader->position() : glm::vec3(0.0f); + + std::vector lines; + lines.push_back(str(boost::format("Watch (%s)") % kDeveloperWatchToggleHelp)); + lines.push_back(str(boost::format("screen=%s module=%s area=%s camera=%s") % + screenName(_screen) % + (_module ? _module->name() : "-") % + (area ? area->localizedName() : "-") % + cameraTypeName(_cameraType))); + lines.push_back(str(boost::format("speed=%.1fx paused=%d relativeMouse=%d room=%s") % + _gameSpeed % + static_cast(_paused) % + static_cast(_relativeMouseMode) % + room)); + lines.push_back(str(boost::format("leader=#%u %s hp=%d/%d pos=%.2f,%.2f,%.2f") % + (leader ? leader->id() : 0) % + (leader ? leader->tag() : "-") % + (leader ? leader->currentHitPoints() : -1) % + (leader ? leader->maxHitPoints() : -1) % + position.x % + position.y % + position.z)); + lines.push_back(str(boost::format("selected=#%u %s/%s type=%s hp=%d/%d") % + (selected ? selected->id() : 0) % + (selected ? selected->tag() : "-") % + (selected ? selected->blueprintResRef() : "-") % + (selected ? objectTypeName(selected->type()) : "-") % + (selected ? selected->currentHitPoints() : -1) % + (selected ? selected->maxHitPoints() : -1))); + lines.push_back(str(boost::format("hover=#%u %s/%s type=%s hp=%d/%d") % + (hover ? hover->id() : 0) % + (hover ? hover->tag() : "-") % + (hover ? hover->blueprintResRef() : "-") % + (hover ? objectTypeName(hover->type()) : "-") % + (hover ? hover->currentHitPoints() : -1) % + (hover ? hover->maxHitPoints() : -1))); + + renderDeveloperPanel( + lines, + glm::vec2(static_cast(_options.graphics.width) - 460.0f, 16.0f), + glm::vec3(0.92f)); +} + +void Game::renderDeveloperText(const std::string &text, const glm::vec3 &position, const glm::vec3 &color, TextGravity gravity) { + if (!_developerFont) { + return; + } + _developerFont->render(text, position + glm::vec3(1.0f, 1.0f, 0.0f), glm::vec3(0.0f), gravity); + _developerFont->render(text, position, color, gravity); +} + +void Game::renderDeveloperPanel(const std::vector &lines, glm::vec2 position, glm::vec3 color) { + if (!_developerFont || lines.empty()) { + return; + } + + float maxWidth = 0.0f; + for (const auto &line : lines) { + maxWidth = glm::max(maxWidth, _developerFont->measure(line)); + } + float lineHeight = _developerFont->height() + 2.0f; + glm::vec2 size(maxWidth + 14.0f, lineHeight * lines.size() + 10.0f); + position.x = glm::clamp(position.x, 4.0f, static_cast(_options.graphics.width) - size.x - 4.0f); + position.y = glm::clamp(position.y, 4.0f, static_cast(_options.graphics.height) - size.y - 4.0f); + + renderDeveloperRect(position, size, glm::vec4(0.0f, 0.0f, 0.0f, 0.58f)); + glm::vec3 textPosition(position.x + 7.0f, position.y + 5.0f, 0.0f); + for (const auto &line : lines) { + renderDeveloperText(line, textPosition, color, TextGravity::LeftTop); + textPosition.y += lineHeight; + } +} + +void Game::renderDeveloperLine(glm::vec2 a, glm::vec2 b, glm::vec4 color, float width) { + glm::vec2 delta = b - a; + float length = glm::length(delta); + if (length < 0.1f) { + return; + } + + float angle = std::atan2(delta.y, delta.x); + glm::mat4 transform(1.0f); + transform = glm::translate(transform, glm::vec3(a.x, a.y, 0.0f)); + transform = glm::rotate(transform, angle, glm::vec3(0.0f, 0.0f, 1.0f)); + transform = glm::translate(transform, glm::vec3(0.0f, -0.5f * width, 0.0f)); + transform = glm::scale(transform, glm::vec3(length, width, 1.0f)); + + _services.graphics.uniforms.setLocals([transform, color](auto &locals) { + locals.reset(); + locals.model = transform; + locals.color = color; + }); + _services.graphics.context.useProgram(_services.graphics.shaderRegistry.get(ShaderProgramId::mvpColor)); + _services.graphics.meshRegistry.get(MeshName::quad).draw(_services.graphics.statistic); +} + +void Game::renderDeveloperRect(glm::vec2 position, glm::vec2 size, glm::vec4 color) { + glm::mat4 transform(1.0f); + transform = glm::translate(transform, glm::vec3(position.x, position.y, 0.0f)); + transform = glm::scale(transform, glm::vec3(size.x, size.y, 1.0f)); + + _services.graphics.uniforms.setLocals([transform, color](auto &locals) { + locals.reset(); + locals.model = transform; + locals.color = color; + }); + _services.graphics.context.useProgram(_services.graphics.shaderRegistry.get(ShaderProgramId::mvpColor)); + _services.graphics.meshRegistry.get(MeshName::quad).draw(_services.graphics.statistic); } void Game::updateMovie(float dt) { diff --git a/src/libs/system/logger.cpp b/src/libs/system/logger.cpp index a0592cb13..178ec9500 100644 --- a/src/libs/system/logger.cpp +++ b/src/libs/system/logger.cpp @@ -98,6 +98,13 @@ void Logger::append(std::string message, } } +void Logger::flush() { + if (!_inited || !buffer) { + return; + } + flush(buffer->get()); +} + void Logger::flush(std::ostringstream &buffer) { if (!_stream) { return; @@ -106,6 +113,7 @@ void Logger::flush(std::ostringstream &buffer) { { std::lock_guard lock {_streamMutex}; _stream->write(&str[0], str.length()); + _stream->flush(); } buffer.str(""); } From 01dce907c1493ebc519a196a90419ea92c739bde Mon Sep 17 00:00:00 2001 From: Eldbury Date: Sun, 19 Apr 2026 14:25:05 +0930 Subject: [PATCH 2/7] Add remaining trigger and object observability changes --- include/reone/game/object/door.h | 1 + include/reone/game/object/placeable.h | 1 + include/reone/game/object/trigger.h | 16 ++++++++++++++ src/libs/game/object/area.cpp | 5 ++++- src/libs/game/object/trigger.cpp | 32 +++++++++++++++++++++++++++ 5 files changed, 54 insertions(+), 1 deletion(-) diff --git a/include/reone/game/object/door.h b/include/reone/game/object/door.h index 4978215ad..932207cb6 100644 --- a/include/reone/game/object/door.h +++ b/include/reone/game/object/door.h @@ -66,6 +66,7 @@ class Door : public Object { const std::string &getOnFailToOpen() const { return _onFailToOpen; } int genericType() const { return _genericType; } + Faction faction() const { return _faction; } const std::string &linkedToModule() const { return _linkedToModule; } const std::string &linkedTo() const { return _linkedTo; } const std::string &transitionDestin() const { return _transitionDestin; } diff --git a/include/reone/game/object/placeable.h b/include/reone/game/object/placeable.h index a31ee74f2..12cc88867 100644 --- a/include/reone/game/object/placeable.h +++ b/include/reone/game/object/placeable.h @@ -55,6 +55,7 @@ class Placeable : public Object { bool isUsable() const { return _usable; } int appearance() const { return _appearance; } + Faction faction() const { return _faction; } std::shared_ptr walkmesh() const { return _walkmesh; } // Scripts diff --git a/include/reone/game/object/trigger.h b/include/reone/game/object/trigger.h index 26024b05b..a1dd42a4c 100644 --- a/include/reone/game/object/trigger.h +++ b/include/reone/game/object/trigger.h @@ -29,6 +29,13 @@ namespace game { class Trigger : public Object { public: + enum class DebugState { + Default, + Tested, + Inside, + Entered + }; + Trigger( uint32_t id, std::string sceneName, @@ -56,6 +63,12 @@ class Trigger : public Object { bool isIn(const glm::vec2 &point) const; bool isTenant(const std::shared_ptr &object) const; + const std::vector &geometry() const { return _geometry; } + DebugState debugState() const; + + void markDebugTested(bool inside); + void markDebugEntered(); + const std::string &getOnEnter() const { return _onEnter; } const std::string &getOnExit() const { return _onExit; } @@ -79,6 +92,9 @@ class Trigger : public Object { std::vector _geometry; std::set> _tenants; std::string _keyName; + float _debugTestAge {0.0f}; + float _debugInsideAge {0.0f}; + float _debugEnterAge {0.0f}; // Scripts diff --git a/src/libs/game/object/area.cpp b/src/libs/game/object/area.cpp index 6661dc415..a295842db 100644 --- a/src/libs/game/object/area.cpp +++ b/src/libs/game/object/area.cpp @@ -877,11 +877,14 @@ void Area::checkTriggersIntersection(const std::shared_ptr &triggerrer) for (auto &object : _objectsByType[ObjectType::Trigger]) { auto trigger = std::static_pointer_cast(object); - if (trigger->isTenant(triggerrer) || !trigger->isIn(position2d)) { + bool inside = trigger->isIn(position2d); + trigger->markDebugTested(inside); + if (trigger->isTenant(triggerrer) || !inside) { continue; } debug(str(boost::format("Trigger '%s' triggerred by '%s'") % trigger->tag() % triggerrer->tag())); trigger->addTenant(triggerrer); + trigger->markDebugEntered(); if (!trigger->linkedToModule().empty()) { _game.scheduleModuleTransition(trigger->linkedToModule(), trigger->linkedTo()); diff --git a/src/libs/game/object/trigger.cpp b/src/libs/game/object/trigger.cpp index 29779a80c..681cbc56c 100644 --- a/src/libs/game/object/trigger.cpp +++ b/src/libs/game/object/trigger.cpp @@ -37,6 +37,10 @@ namespace reone { namespace game { +static constexpr float kDebugTestDuration = 0.25f; +static constexpr float kDebugInsideDuration = 0.25f; +static constexpr float kDebugEnterDuration = 1.5f; + void Trigger::loadFromGIT(const resource::generated::GIT_TriggerList &git) { std::string templateResRef(boost::to_lower_copy(git.TemplateResRef)); loadFromBlueprint(templateResRef); @@ -83,6 +87,10 @@ void Trigger::loadFromBlueprint(const std::string &resRef) { } void Trigger::update(float dt) { + _debugTestAge = glm::max(0.0f, _debugTestAge - dt); + _debugInsideAge = glm::max(0.0f, _debugInsideAge - dt); + _debugEnterAge = glm::max(0.0f, _debugEnterAge - dt); + std::set> tenantsToRemove; for (auto &tenant : _tenants) { if (tenant) { @@ -128,6 +136,30 @@ bool Trigger::isTenant(const std::shared_ptr &object) const { return maybeTenant != _tenants.end(); } +Trigger::DebugState Trigger::debugState() const { + if (_debugEnterAge > 0.0f) { + return DebugState::Entered; + } + if (!_tenants.empty() || _debugInsideAge > 0.0f) { + return DebugState::Inside; + } + if (_debugTestAge > 0.0f) { + return DebugState::Tested; + } + return DebugState::Default; +} + +void Trigger::markDebugTested(bool inside) { + _debugTestAge = kDebugTestDuration; + if (inside) { + _debugInsideAge = kDebugInsideDuration; + } +} + +void Trigger::markDebugEntered() { + _debugEnterAge = kDebugEnterDuration; +} + void Trigger::loadUTT(const resource::generated::UTT &utt) { _tag = boost::to_lower_copy(utt.Tag); _blueprintResRef = boost::to_lower_copy(utt.TemplateResRef); From 4d2e94c4ceeda903fa064196a16435ec0168d0be Mon Sep 17 00:00:00 2001 From: Eldbury Date: Sun, 19 Apr 2026 14:25:32 +0930 Subject: [PATCH 3/7] Ignore local runtime log and config files --- .gitignore | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.gitignore b/.gitignore index 28cecccf9..b8fb91599 100644 --- a/.gitignore +++ b/.gitignore @@ -3,3 +3,6 @@ .vs/ .vscode/ build/ + +engine.log +reone.cfg From 33b82f52dd8314622d951406d5d429c2babb0a62 Mon Sep 17 00:00:00 2001 From: Eldbury Date: Sun, 19 Apr 2026 14:55:09 +0930 Subject: [PATCH 4/7] Fix trigger-owned delayed actions. Fixes early Carth sequence --- docs/documentation.md | 60 ++++++++++++++++++++++++++++++++ docs/plans.md | 25 +++++++++++++ src/libs/game/object/trigger.cpp | 2 ++ 3 files changed, 87 insertions(+) diff --git a/docs/documentation.md b/docs/documentation.md index 165b91625..f0e547afd 100644 --- a/docs/documentation.md +++ b/docs/documentation.md @@ -450,3 +450,63 @@ Human verification needed: - Automated smoke validates startup, but it does not press overlay hotkeys or move through trigger volumes. - Human in-game verification should launch with developer mode enabled and confirm `Ctrl+Shift+A` shows/hides entity labels, and trigger zones change to `tested`, `inside`, and `enter` states when crossed. + +## 2026-04-19: K1 Early Carth Cutscene / Trigger Delayed-Action Parity Slice + +Bug description: + +- In K1's early Endar Spire handoff path, the Carth-related message/cutscene progression after the first-room/Trask-door band can fail to appear. +- The old branch identified this as a shared engine delayed-action issue rather than a Carth content hack: `k_pend_trig02` runs with the trigger as caller and schedules a `DelayCommand`; that delayed action is owned by the trigger object. +- Before the old fix, trigger objects ran tenant maintenance but skipped base `Object::update(dt)`, so trigger-owned delayed actions did not mature or execute. + +Donor fix source: + +- `D:\reone-master\docs\documentation.md`, lines 2440-2506: old diagnosis and implementation summary. +- `D:\reone-master\docs\plans.md`, lines 1290-1332: tiny milestone describing the selected fix. +- `D:\reone-master\src\libs\game\object\trigger.cpp`, lines 163-165: `Trigger::update` calls `Object::update(dt)` before trigger tenant maintenance. + +Minimal port: + +- Ported only the causal runtime behavior: `Trigger::update(float dt)` now calls `Object::update(dt)` before developer debug timers and tenant exit maintenance. +- This lets trigger-owned action queues, delayed actions, and effects process through the existing modawan base object update path. + +What was intentionally not ported: + +- Donor scoped `DelayCommand` scheduling logs in script routines. +- Donor `DoCommandAction` execution/result logs. +- Donor live-log eval gates and old K1 combat-entry legal-data tests. +- Donor first-Sith combat, hostility, reciprocal-hostility, boarding-party persistence, encounter band-aids, target inspector/event feed, launcher changes, or content hacks. + +Why those were excluded: + +- The minimal causal fix is the base `Object::update(dt)` call on trigger objects. +- The donor logging/eval changes were useful during old investigation but are not required to restore the delayed action behavior in this bounded parity slice. +- The excluded combat/hostility and encounter changes belong to older compensating runtime work and are outside this single-bug branch. + +Validation: + +- Build: + - `powershell -NoProfile -ExecutionPolicy Bypass -File .\scripts\build.ps1 -Config RelWithDebInfo -Target engine -Configure` + - Result: passed. `D:\git\reone-modawan-main\build\bin\engine.exe` was produced. +- Smoke test without game install: + - `powershell -NoProfile -ExecutionPolicy Bypass -File .\scripts\smoke_test.ps1 -AllowMissingGame` + - `powershell -NoProfile -ExecutionPolicy Bypass -File .\scripts\eval_smoke.ps1 -SmokeDir .\.agent\logs\smoke_latest -AllowMissingGame` + - Result: passed. Launch was skipped by design. +- K1 smoke/eval: + - `powershell -NoProfile -ExecutionPolicy Bypass -File .\scripts\smoke_test.ps1 -GameDir 'D:\SteamLibrary\steamapps\common\swkotor' -Game k1` + - `powershell -NoProfile -ExecutionPolicy Bypass -File .\scripts\eval_smoke.ps1 -SmokeDir .\.agent\logs\smoke_latest` + - Result: passed. Smoke artifacts were written to `.agent\logs\smoke_20260419_143200`. +- K2 smoke/eval: + - `powershell -NoProfile -ExecutionPolicy Bypass -File .\scripts\smoke_test.ps1 -GameDir 'D:\SteamLibrary\steamapps\common\Knights of the Old Republic II' -Game k2` + - `powershell -NoProfile -ExecutionPolicy Bypass -File .\scripts\eval_smoke.ps1 -SmokeDir .\.agent\logs\smoke_latest` + - Result: passed. Smoke artifacts were written to `.agent\logs\smoke_20260419_143241`. +- Artifact check: + - `D:\git\reone-modawan-main\build\bin\engine.exe` exists. + - `D:\git\reone-modawan-main\build\bin\shaderpack.erf` exists. +- `git diff --check` + - Result: no whitespace errors; only CRLF normalization warnings. + +Human verification needed: + +- Automated smoke verifies startup only; it does not drive the Endar Spire sequence. +- Launch K1 with the rebuilt engine, play through the early Endar Spire first-room/Trask-door/Carth handoff band without console or forced combat, and confirm the previously missing Carth-related message/cutscene progression occurs. diff --git a/docs/plans.md b/docs/plans.md index 2b906dce1..f91bf4b09 100644 --- a/docs/plans.md +++ b/docs/plans.md @@ -108,3 +108,28 @@ Non-goals: - Do not port donor target inspector or event feed in this milestone. - Do not port donor launcher developer-option controls in this milestone. - Do not port donor gameplay behavior or modernize runtime systems. + +## Tiny Migration Milestone: K1 Trigger-Owned Delayed Actions + +Goal: restore the old K1 early Carth/Trask handoff parity fix by allowing delayed actions queued on trigger objects to execute. + +Acceptance criteria: + +- `Trigger::update(float dt)` runs base `Object::update(dt)` before trigger tenant maintenance. +- The change is limited to trigger-owned base action/effect processing and does not alter trigger containment geometry, script dispatch arguments, combat legality, hostility, boarding-party persistence, encounter sequencing, or Carth content. +- Donor logging/eval instrumentation is not ported unless needed to prove a blocker. +- K1 and K2 smoke/eval pass after the change. + +Verification: + +- Build the engine target. +- Run generic smoke/eval with `-AllowMissingGame`. +- Run K1 smoke/eval. +- Run K2 smoke/eval. +- Human K1 verification should reach the early Endar Spire first-room/Trask-door/Carth handoff band and confirm the previously missing Carth-related cutscene/message progression occurs. + +Non-goals: + +- Do not force any first-Sith actor hostile. +- Do not port donor combat, reciprocal hostility, boarding-party, encounter, journal, Carth content, launcher, or modernization changes. +- Do not add broad diagnostics unless this minimal port fails validation or cannot be isolated. diff --git a/src/libs/game/object/trigger.cpp b/src/libs/game/object/trigger.cpp index 681cbc56c..7035c4810 100644 --- a/src/libs/game/object/trigger.cpp +++ b/src/libs/game/object/trigger.cpp @@ -87,6 +87,8 @@ void Trigger::loadFromBlueprint(const std::string &resRef) { } void Trigger::update(float dt) { + Object::update(dt); + _debugTestAge = glm::max(0.0f, _debugTestAge - dt); _debugInsideAge = glm::max(0.0f, _debugInsideAge - dt); _debugEnterAge = glm::max(0.0f, _debugEnterAge - dt); From fdad4a2a597016bf1f9afb9062a2577337788deb Mon Sep 17 00:00:00 2001 From: Eldbury Date: Sun, 19 Apr 2026 17:06:45 +0930 Subject: [PATCH 5/7] Fix early Trask auto-dialogue continuation --- docs/documentation.md | 185 +++++++++++++++++++++ docs/plans.md | 51 ++++++ include/reone/game/gui/partyselect.h | 1 + include/reone/game/object.h | 1 + src/libs/game/action/startconversation.cpp | 31 ++++ src/libs/game/gui/partyselect.cpp | 58 ++++++- src/libs/game/object.cpp | 26 ++- src/libs/game/object/area.cpp | 28 +++- src/libs/game/script/routine/impl/main.cpp | 53 ++++++ src/libs/game/script/runner.cpp | 36 +++- src/libs/script/virtualmachine.cpp | 48 ++++++ 11 files changed, 508 insertions(+), 10 deletions(-) diff --git a/docs/documentation.md b/docs/documentation.md index f0e547afd..35a15da47 100644 --- a/docs/documentation.md +++ b/docs/documentation.md @@ -510,3 +510,188 @@ Human verification needed: - Automated smoke verifies startup only; it does not drive the Endar Spire sequence. - Launch K1 with the rebuilt engine, play through the early Endar Spire first-room/Trask-door/Carth handoff band without console or forced combat, and confirm the previously missing Carth-related message/cutscene progression occurs. + +## 2026-04-19: K1 Early Trask Auto-Dialogue Dispatch Slice + +Bug description: + +- In K1's early Endar Spire Trask sequence, the scripted follow-up conversations can fail to auto-play even though manual conversation with Trask reaches the expected dialogue state. +- The visible symptoms are the missing unlock-door guidance after the party-management screen closes and the missing "you better take the lead" follow-up after Trask opens the door. +- This points at scripted conversation dispatch, not dialogue-state progression, combat, hostility, boarding-party, or encounter behavior. + +Donor source checked: + +- `D:\reone-master\docs\documentation.md`, lines 2413-2506: old `END_TRASK_DLG` consumer diagnosis and the trigger-owned delayed-action fix. +- `D:\reone-master\docs\plans.md`, lines 1290-1360: the tiny K1 handoff parity milestone for trigger-owned delayed actions. +- `D:\reone-master\src\libs\game\object\trigger.cpp`, lines 163-165: donor `Trigger::update(float dt)` calls `Object::update(dt)`. +- `D:\reone-master\src\libs\game\action\startconversation.cpp`, lines 30-43: donor scripted conversation action dispatches through `Area::startDialog`. +- `D:\reone-master\src\libs\game\object\area.cpp`, lines 960-968: donor still has the same empty-resref fallback guard defect, so there was no additional donor code patch to copy for this auto-dialogue-specific failure. + +Minimal fix: + +- Kept the already-integrated trigger delayed-action fix as-is. +- Fixed the shared conversation dispatcher in `src\libs\game\object\area.cpp` so `Area::startDialog` validates the resolved dialogue resref after falling back to `object->conversation()`. +- This allows scripted `ActionStartConversation` calls with an empty dialogue resref to use the target object's default conversation, matching the manual-talk path. +- After live verification showed the first Trask callback still did not auto-play after party selection, fixed `src\libs\game\gui\partyselect.cpp` so the party-selection exit script runs as the current party leader/player instead of running with no caller. +- This gives K1's `k_pend_reset` a valid `OBJECT_SELF` for its caller-sensitive `ClearAllActions()` and follow-up conversation assignment without changing sequence content. + +What was intentionally not ported: + +- Donor party-selection forced-companion integrity work. +- Donor party roster/HUD logging and party leader observability. +- Donor delayed-command logging/eval instrumentation beyond what is already in this branch. +- Donor combat legality, reciprocal hostility, boarding-party persistence, encounter band-aids, first-Sith hostility work, Carth content changes, launcher changes, or modernization. + +Why those were excluded: + +- The missing auto-dialogue shape is explained by two shared dispatch issues: the conversation fallback returned before `Game::startDialog` could run, and the party-selection exit script callback ran without a caller. +- The trigger delayed-action donor fix was already integrated and validated in the previous slice. +- The old party-selection donor work addresses roster corruption around the Trask join flow, not this empty-dialog-resref dispatch failure. +- No K1 content hack or gameplay/runtime combat change is needed for this slice. + +Validation: + +- Build: + - `powershell -NoProfile -ExecutionPolicy Bypass -File .\scripts\build.ps1 -Config RelWithDebInfo -Target engine -Configure` + - Result: passed. `D:\git\reone-modawan-main\build\bin\engine.exe` was produced. +- Smoke test without game install: + - `powershell -NoProfile -ExecutionPolicy Bypass -File .\scripts\smoke_test.ps1 -AllowMissingGame` + - `powershell -NoProfile -ExecutionPolicy Bypass -File .\scripts\eval_smoke.ps1 -SmokeDir .\.agent\logs\smoke_latest -AllowMissingGame` + - Result: passed. Launch was skipped by design. +- K1 smoke/eval: + - `powershell -NoProfile -ExecutionPolicy Bypass -File .\scripts\smoke_test.ps1 -GameDir 'D:\SteamLibrary\steamapps\common\swkotor' -Game k1` + - `powershell -NoProfile -ExecutionPolicy Bypass -File .\scripts\eval_smoke.ps1 -SmokeDir .\.agent\logs\smoke_latest` + - Result: passed after the party-selection caller follow-up. Smoke artifacts were written to `.agent\logs\smoke_20260419_154359`. +- K2 smoke/eval: + - `powershell -NoProfile -ExecutionPolicy Bypass -File .\scripts\smoke_test.ps1 -GameDir 'D:\SteamLibrary\steamapps\common\Knights of the Old Republic II' -Game k2` + - `powershell -NoProfile -ExecutionPolicy Bypass -File .\scripts\eval_smoke.ps1 -SmokeDir .\.agent\logs\smoke_latest` + - Result: passed after the party-selection caller follow-up. Smoke artifacts were written to `.agent\logs\smoke_20260419_154438`. +- Artifact check: + - `D:\git\reone-modawan-main\build\bin\engine.exe` exists. + - `D:\git\reone-modawan-main\build\bin\shaderpack.erf` exists. +- `git diff --check` + - Result: no whitespace errors; only CRLF normalization warnings. + +Human verification needed: + +- Automated smoke verifies startup only; it does not drive the Endar Spire sequence. +- Launch K1 with the rebuilt engine, reach the Trask join point, close party management, and confirm the unlock-door guidance auto-plays without manually talking to Trask. +- Let Trask open the first door without manually forcing the dialogue first, then confirm the "you better take the lead" follow-up auto-plays. +- Do not use console commands, forced combat, faction edits, or unusual routing for the parity check. + +Live verification follow-up: + +- Human testing confirmed the Carth-side trigger delayed-action fix works, but the first Trask auto-dialogue still did not play immediately after Trask joins the party. +- The remaining first-half cause is a separate shared callback issue: the party-selection GUI ran its configured exit script with no caller, while K1's `k_pend_reset` expects a valid `OBJECT_SELF` before it can pass `ClearAllActions()` and assign Trask's follow-up conversation. +- The minimal follow-up fix keeps the sequence content untouched and runs the party-selection exit script as the current party leader/player after the party screen closes. +- This avoids running the callback as the pre-party Trask object, which could cancel the delayed cleanup queued by the join script. +- No donor combat, hostility, boarding-party, encounter, first-door content, party roster, or modernization code was ported. + +## 2026-04-19: Temporary K1 Trask Auto-Dialogue Trace + +Purpose: + +- This is a temporary instrumentation slice only. +- It is meant to identify the exact live failure point after Trask joins the party and the party-selection screen closes. +- It does not intentionally change gameplay, combat, hostility, encounter, party roster, item, cutscene, or dialogue content behavior. + +Temporary trace prefix: + +- All added trace lines use `reone trask autodialog trace:` so they can be filtered from `build\bin\engine.log`. + +Temporary trace points: + +- `src\libs\game\script\routine\impl\main.cpp` + - `ShowPartySelectionGUI`: logs only when the exit script is `k_pend_reset`; records forced NPC args, allow-cancel arg, caller object, and parent script args. + - `AssignCommand`: logs when the command subject is a Trask-like object; records the subject and saved action context. +- `src\libs\game\gui\partyselect.cpp` + - `PartySelection::prepare`: logs only for `k_pend_reset`; records forced NPCs and current party state. + - `BTN_DONE` close callback: logs that Done closed party selection before `changeParty()`. + - `BTN_BACK` close callback: logs that Back closed party selection without `changeParty()`. + - `PartySelection::runExitScript`: logs the exact exit script, selected caller, and party state before dispatch. +- `src\libs\game\script\runner.cpp` + - `ScriptRunner::run`: logs begin/missing/end only for `k_pend_reset`, including script args and VM result. +- `src\libs\script\virtualmachine.cpp` + - `VirtualMachine::executeACTION`: logs begin/end only inside script `k_pend_reset` and only for `ClearAllActions`, `GetPartyMemberByIndex`, `AssignCommand`, `ActionStartConversation`, and `SetGlobalNumber`. +- `src\libs\game\action\startconversation.cpp` + - `StartConversationAction::execute`: logs only when the actor or target is Trask-like; records reached/not-reached, actor, target, dialog resref, and range-ignore flag. +- `src\libs\game\object\area.cpp` + - `Area::startDialog`: logs only when the dialogue owner is Trask-like; records input resref and resolved final resref. + +Manual evidence capture: + +- Rebuild the engine. +- Launch `D:\git\reone-modawan-main\build\bin\launcher.exe`. +- Start K1, play normally until Trask joins and party management closes. +- Close the game after confirming whether the unlock-door dialogue auto-plays. +- Inspect `D:\git\reone-modawan-main\build\bin\engine.log` and filter for `reone trask autodialog trace:`. + +Hypotheses tested: + +- No `ShowPartySelectionGUI` trace means the live path is not using the expected K1 party-selection script call. +- `ShowPartySelectionGUI` without `PartySelection::prepare` means the GUI context is not reaching the party-selection screen. +- `PartySelection::prepare` without a close-path trace means the screen is closing through an uninstrumented path. +- Close-path trace without `runExitScript` means the exit-script dispatch helper is not reached. +- `runExitScript` without `ScriptRunner::run begin` means dispatch does not enter the script runner. +- Script runner begin without VM actions means `k_pend_reset` is missing, empty, or halting before the traced calls. +- VM `AssignCommand` without later `ActionStartConversation` or `StartConversationAction` means the assigned action is not executing. +- `StartConversationAction` without `Area::startDialog` means the action is blocked before dialogue dispatch, likely by range/navigation. +- `Area::startDialog` with an empty final resref means dialogue owner/default conversation resolution is still wrong. + +## 2026-04-19: K1 Trask Auto-Dialogue Action Continuation Fix + +Traced failure point: + +- Live trace proved `ShowPartySelectionGUI("k_pend_reset", 0, -1)` runs and the party-selection screen closes through `BTN_DONE`. +- `k_pend_reset` runs, resolves the new party Trask through `GetPartyMemberByIndex(1)`, and queues two `AssignCommand` actions on Trask. +- The first assigned action starts on Trask and runs `ClearAllActions()`. +- After that clear, the follow-up assigned action that should reach `ActionStartConversation()` does not execute. + +Actual engine cause: + +- `Object::clearAllActions()` cleared the full action queue even when called from an action currently executing on that same object. +- For this sequence, the first queued `DoCommandAction` resumed at `ClearAllActions()`. The queue still contained that active action and the next queued `DoCommandAction` carrying the follow-up conversation. +- Clearing the whole queue erased the current action frame and the queued continuation behind it, so the action chain died before the saved `ActionStartConversation()` state could run. + +Minimal fix: + +- `Object::executeActions()` now records the currently executing action while it calls `Action::execute()`. +- `Object::clearAllActions(false)` now preserves the currently executing action and the continuation actions behind it when the clear is invoked from that active action frame. +- Forced clears still clear the queue, and clears outside active action execution keep the existing queue-clearing behavior. +- This is a shared action semantics fix; it has no Trask-specific, Endar-Spire-specific, dialogue-content-specific, combat, hostility, encounter, boarding-party, item, or modernization branch. + +Temporary instrumentation status: + +- The temporary `reone trask autodialog trace:` logs remain in this validation build so the next live run can prove whether the preserved continuation reaches `ActionStartConversation()` and `Area::startDialog()`. +- Once live verification confirms the exact path, remove or reduce the temporary trace lines. + +What was intentionally not changed: + +- No K1 content scripts, dialogue files, module data, Trask-specific special cases, combat legality, reciprocal hostility, boarding-party hostility persistence, encounter sequencing, item behavior, PR #77 work, Dear ImGui work, or modernization were ported or added. +- The earlier party-selection caller patch is not treated as the causal live fix; the latest trace shows the real blocker is the action queue clear inside the assigned command chain. It can be reconsidered after live validation of the engine fix. + +Validation: + +- Build: + - `powershell -NoProfile -ExecutionPolicy Bypass -File .\scripts\build.ps1 -Config RelWithDebInfo -Target engine -Configure` + - Result: passed. `D:\git\reone-modawan-main\build\bin\engine.exe` was rebuilt. +- Smoke test without game install: + - `powershell -NoProfile -ExecutionPolicy Bypass -File .\scripts\smoke_test.ps1 -AllowMissingGame` + - `powershell -NoProfile -ExecutionPolicy Bypass -File .\scripts\eval_smoke.ps1 -SmokeDir .\.agent\logs\smoke_latest -AllowMissingGame` + - Result: passed. Launch was skipped by design. +- K1 smoke/eval: + - `powershell -NoProfile -ExecutionPolicy Bypass -File .\scripts\smoke_test.ps1 -GameDir 'D:\SteamLibrary\steamapps\common\swkotor' -Game k1` + - `powershell -NoProfile -ExecutionPolicy Bypass -File .\scripts\eval_smoke.ps1 -SmokeDir .\.agent\logs\smoke_latest` + - Result: passed. Smoke artifacts were written to `.agent\logs\smoke_20260419_161924`. +- K2 smoke/eval: + - `powershell -NoProfile -ExecutionPolicy Bypass -File .\scripts\smoke_test.ps1 -GameDir 'D:\SteamLibrary\steamapps\common\Knights of the Old Republic II' -Game k2` + - `powershell -NoProfile -ExecutionPolicy Bypass -File .\scripts\eval_smoke.ps1 -SmokeDir .\.agent\logs\smoke_latest` + - Result: passed. Smoke artifacts were written to `.agent\logs\smoke_20260419_162011`. +- `git diff --check` + - Result: no whitespace errors; only CRLF normalization warnings. + +Human verification needed: + +- Automated smoke verifies startup only; it does not drive the Trask sequence. +- Launch K1 with the rebuilt engine, close party selection after Trask joins, and confirm whether the unlock-door dialogue auto-plays. +- Filter `D:\git\reone-modawan-main\build\bin\engine.log` for `reone trask autodialog trace:` and confirm the preserved continuation reaches `ActionStartConversation` and `Area::startDialog`. diff --git a/docs/plans.md b/docs/plans.md index f91bf4b09..c1d24fe18 100644 --- a/docs/plans.md +++ b/docs/plans.md @@ -133,3 +133,54 @@ Non-goals: - Do not force any first-Sith actor hostile. - Do not port donor combat, reciprocal hostility, boarding-party, encounter, journal, Carth content, launcher, or modernization changes. - Do not add broad diagnostics unless this minimal port fails validation or cannot be isolated. + +## Tiny Migration Milestone: K1 Trask Auto-Dialogue Dispatch + +Goal: restore the early Endar Spire scripted Trask follow-up conversations by fixing the shared conversation fallback and party-selection exit-script callback paths. + +Acceptance criteria: + +- `ActionStartConversation` calls with an empty dialogue resref can fall back to the target object's default conversation. +- Party-selection exit scripts run with the current party leader/player as caller so K1 callback scripts that begin with caller-sensitive actions can dispatch their follow-up conversations. +- The changes are limited to shared script dispatch/callback wiring and do not alter Trask content, trigger geometry, combat legality, hostility, boarding-party persistence, encounter sequencing, item behavior, or cutscene logic. +- The previously integrated trigger-owned delayed-action fix remains unchanged. +- K1 and K2 smoke/eval pass after the change. + +Verification: + +- Build the engine target. +- Run generic smoke/eval with `-AllowMissingGame`. +- Run K1 smoke/eval. +- Run K2 smoke/eval. +- Human K1 verification should reach the Trask join and first-door handoff band and confirm both scripted follow-up dialogues auto-play without manual Trask conversation. + +Non-goals: + +- Do not port donor party-selection forced-companion work in this milestone. +- Do not port donor combat, reciprocal hostility, boarding-party, encounter, journal, Carth content, launcher, or modernization changes. +- Do not add broad diagnostics unless this minimal dispatch fix fails validation or cannot be isolated. + +## Tiny Migration Milestone: Action Continuation-Safe ClearAllActions + +Goal: preserve active assigned action continuations when a queued script action calls `ClearAllActions()`. + +Acceptance criteria: + +- `Object::clearAllActions(false)` no longer erases the currently executing action frame or continuation actions behind it when invoked from that active action. +- Forced clears and clears outside active action execution retain existing behavior. +- The change is generic action queue semantics, not Trask-specific content logic. +- K1 and K2 smoke/eval pass after the change. + +Verification: + +- Build the engine target. +- Run generic smoke/eval with `-AllowMissingGame`. +- Run K1 smoke/eval. +- Run K2 smoke/eval. +- Human K1 verification should close party selection after Trask joins and confirm the trace reaches `ActionStartConversation`/`Area::startDialog` and the unlock-door dialogue auto-plays. + +Non-goals: + +- Do not port donor party-selection forced-companion work. +- Do not port combat, hostility, boarding-party, encounter, journal, content, launcher, or modernization changes. +- Do not turn temporary Trask trace logging into a permanent broad logging system. diff --git a/include/reone/game/gui/partyselect.h b/include/reone/game/gui/partyselect.h index fa69f3da0..1092c1126 100644 --- a/include/reone/game/gui/partyselect.h +++ b/include/reone/game/gui/partyselect.h @@ -165,6 +165,7 @@ class PartySelection : public GameGUI { void refreshAvailableCount(); void refreshNpcButtons(); void removeNpc(int npc); + void runExitScript(); gui::ToggleButton &getNpcButton(int npc); diff --git a/include/reone/game/object.h b/include/reone/game/object.h index 5f50f2909..69ea5e78b 100644 --- a/include/reone/game/object.h +++ b/include/reone/game/object.h @@ -256,6 +256,7 @@ class Object : public scene::IUser, boost::noncopyable { std::deque> _actions; std::vector _delayed; + std::weak_ptr _executingAction; // END Actions diff --git a/src/libs/game/action/startconversation.cpp b/src/libs/game/action/startconversation.cpp index a74240e09..2cb96929a 100644 --- a/src/libs/game/action/startconversation.cpp +++ b/src/libs/game/action/startconversation.cpp @@ -20,6 +20,7 @@ #include "reone/game/di/services.h" #include "reone/game/game.h" #include "reone/game/party.h" +#include "reone/system/logutil.h" namespace reone { @@ -27,6 +28,27 @@ namespace game { static constexpr float kMaxConversationDistance = 4.0f; +static bool containsTraskTraceToken(const std::string &value) { + return value.find("trask") != std::string::npos; +} + +static bool isTraskTraceObject(const std::shared_ptr &object) { + return object && + (containsTraskTraceToken(object->tag()) || + containsTraskTraceToken(object->blueprintResRef()) || + containsTraskTraceToken(object->conversation())); +} + +static std::string describeTraskTraceObject(const std::shared_ptr &object) { + if (!object) { + return "null"; + } + return "#" + std::to_string(object->id()) + + " tag='" + object->tag() + + "' blueprint='" + object->blueprintResRef() + + "' conv='" + object->conversation() + "'"; +} + void StartConversationAction::execute(std::shared_ptr self, Object &actor, float dt) { auto actorPtr = _game.getObjectById(actor.id()); auto creatureActor = std::static_pointer_cast(actorPtr); @@ -36,6 +58,15 @@ void StartConversationAction::execute(std::shared_ptr self, Object &acto _ignoreStartRange || creatureActor->navigateTo(_objectToConverse->position(), true, kMaxConversationDistance, dt); + bool traskTrace = isTraskTraceObject(actorPtr) || isTraskTraceObject(_objectToConverse); + if (traskTrace) { + info("reone trask autodialog trace: StartConversationAction execute reached=" + std::to_string(static_cast(reached)) + + " actor=" + describeTraskTraceObject(actorPtr) + + " target=" + describeTraskTraceObject(_objectToConverse) + + " dialogResRef='" + _dialogResRef + "'" + + " ignoreStartRange=" + std::to_string(static_cast(_ignoreStartRange))); + } + if (reached) { bool isActorLeader = _game.party().getLeader() == actorPtr; _game.module()->area()->startDialog(isActorLeader ? _objectToConverse : actorPtr, _dialogResRef); diff --git a/src/libs/game/gui/partyselect.cpp b/src/libs/game/gui/partyselect.cpp index 7eaee27d3..0d2f5c8c1 100644 --- a/src/libs/game/gui/partyselect.cpp +++ b/src/libs/game/gui/partyselect.cpp @@ -29,6 +29,7 @@ #include "reone/resource/resources.h" #include "reone/resource/strings.h" #include "reone/script/types.h" +#include "reone/system/logutil.h" using namespace reone::audio; @@ -50,6 +51,28 @@ static int g_strRefRemove = 38456; static glm::vec3 g_kotorColorOn = {0.984314f, 1.0f, 0}; static glm::vec3 g_kotorColorAdded = {0, 0.831373f, 0.090196f}; +static bool isTraskAutoDialogTrace(const PartySelectionContext &ctx) { + return ctx.exitScript == "k_pend_reset"; +} + +static std::string describeTraceObject(const std::shared_ptr &object) { + if (!object) { + return "null"; + } + return "#" + std::to_string(object->id()) + + " tag='" + object->tag() + + "' blueprint='" + object->blueprintResRef() + + "' conv='" + object->conversation() + "'"; +} + +static std::string describeTraceParty(const Party &party) { + std::string result("size=" + std::to_string(party.getSize())); + for (int i = 0; i < party.getSize(); ++i) { + result += " member" + std::to_string(i) + "=" + describeTraceObject(party.getMember(i)); + } + return result; +} + PartySelection::PartySelection(Game &game, ServicesView &services) : GameGUI(game, services) { @@ -78,17 +101,19 @@ void PartySelection::onGUILoaded() { onAcceptButtonClick(); }); _controls.BTN_DONE->setOnClick([this]() { + if (isTraskAutoDialogTrace(_context)) { + info("reone trask autodialog trace: partyselect close path=BTN_DONE before changeParty " + describeTraceParty(_game.party())); + } changeParty(); _game.openInGame(); - if (!_context.exitScript.empty()) { - _game.scriptRunner().run(_context.exitScript); - } + runExitScript(); }); _controls.BTN_BACK->setOnClick([this]() { - _game.openInGame(); - if (!_context.exitScript.empty()) { - _game.scriptRunner().run(_context.exitScript); + if (isTraskAutoDialogTrace(_context)) { + info("reone trask autodialog trace: partyselect close path=BTN_BACK no changeParty " + describeTraceParty(_game.party())); } + _game.openInGame(); + runExitScript(); }); _controls.BTN_NPC0->setOnClick([this]() { onNpcButtonClick(0); @@ -135,6 +160,13 @@ void PartySelection::prepare(const PartySelectionContext &ctx) { _context = ctx; _availableCount = kMaxFollowerCount; + if (isTraskAutoDialogTrace(_context)) { + info("reone trask autodialog trace: partyselect prepare exitScript='" + _context.exitScript + + "' forceNpc1=" + std::to_string(_context.forceNpc1) + + " forceNpc2=" + std::to_string(_context.forceNpc2) + + " " + describeTraceParty(_game.party())); + } + for (int i = 0; i < kNpcCount; ++i) { _added[i] = false; getNpcButton(i).setUseBorderColorOverride(false); @@ -305,6 +337,20 @@ void PartySelection::changeParty() { area->reloadParty(); } +void PartySelection::runExitScript() { + if (_context.exitScript.empty()) { + return; + } + + auto leader = _game.party().getLeader(); + if (isTraskAutoDialogTrace(_context)) { + info("reone trask autodialog trace: partyselect runExitScript exitScript='" + _context.exitScript + + "' caller=" + describeTraceObject(leader) + + " " + describeTraceParty(_game.party())); + } + _game.scriptRunner().run(_context.exitScript, leader ? leader->id() : 0); +} + } // namespace game } // namespace reone diff --git a/src/libs/game/object.cpp b/src/libs/game/object.cpp index ebe9c6716..0eb2b96fb 100644 --- a/src/libs/game/object.cpp +++ b/src/libs/game/object.cpp @@ -65,6 +65,23 @@ void Object::setLocalNumber(int index, int value) { } void Object::clearAllActions(bool force) { + if (!force) { + auto executingAction = _executingAction.lock(); + if (executingAction) { + while (!_actions.empty() && _actions.front() != executingAction) { + const std::shared_ptr &action = _actions.front(); + if (action->locked()) { + break; + } + action->cancel(action, *this); + _actions.pop_front(); + } + if (!_actions.empty() && _actions.front() == executingAction) { + return; + } + } + } + while (!_actions.empty()) { const std::shared_ptr &action = _actions.back(); if (!force && action->locked()) { @@ -129,7 +146,14 @@ void Object::executeActions(float dt) { return; } std::shared_ptr action(_actions.front()); - action->execute(action, *this, dt); + _executingAction = action; + try { + action->execute(action, *this, dt); + } catch (...) { + _executingAction.reset(); + throw; + } + _executingAction.reset(); } bool Object::hasUserActionsPending() const { diff --git a/src/libs/game/object/area.cpp b/src/libs/game/object/area.cpp index a295842db..d4af3ac45 100644 --- a/src/libs/game/object/area.cpp +++ b/src/libs/game/object/area.cpp @@ -78,6 +78,27 @@ static constexpr float kMaxCollisionDistance2 = kMaxCollisionDistance * kMaxColl static glm::vec3 g_defaultAmbientColor {0.2f}; static CameraStyle g_defaultCameraStyle {"", 3.2f, 83.0f, 0.45f, 55.0f}; +static bool containsTraskTraceToken(const std::string &value) { + return value.find("trask") != std::string::npos; +} + +static bool isTraskTraceObject(const std::shared_ptr &object) { + return object && + (containsTraskTraceToken(object->tag()) || + containsTraskTraceToken(object->blueprintResRef()) || + containsTraskTraceToken(object->conversation())); +} + +static std::string describeTraskTraceObject(const std::shared_ptr &object) { + if (!object) { + return "null"; + } + return "#" + std::to_string(object->id()) + + " tag='" + object->tag() + + "' blueprint='" + object->blueprintResRef() + + "' conv='" + object->conversation() + "'"; +} + Area::Area( uint32_t id, std::string sceneName, @@ -802,7 +823,12 @@ void Area::startDialog(const std::shared_ptr &object, const std::string if (resRef.empty()) { finalResRef = object->conversation(); } - if (resRef.empty()) { + if (isTraskTraceObject(object)) { + info("reone trask autodialog trace: Area::startDialog owner=" + describeTraskTraceObject(object) + + " inputResRef='" + resRef + + "' finalResRef='" + finalResRef + "'"); + } + if (finalResRef.empty()) { return; } _game.startDialog(object, finalResRef); diff --git a/src/libs/game/script/routine/impl/main.cpp b/src/libs/game/script/routine/impl/main.cpp index 91d7143ea..1bf48fbd2 100644 --- a/src/libs/game/script/routine/impl/main.cpp +++ b/src/libs/game/script/routine/impl/main.cpp @@ -63,6 +63,42 @@ namespace reone { namespace game { +static bool containsTraskTraceToken(const std::string &value) { + return value.find("trask") != std::string::npos; +} + +static bool isTraskTraceObject(const std::shared_ptr &object) { + return object && + (containsTraskTraceToken(object->tag()) || + containsTraskTraceToken(object->blueprintResRef()) || + containsTraskTraceToken(object->conversation())); +} + +static std::string describeTraskTraceObject(const std::shared_ptr &object) { + if (!object) { + return "null"; + } + return "#" + std::to_string(object->id()) + + " tag='" + object->tag() + + "' blueprint='" + object->blueprintResRef() + + "' conv='" + object->conversation() + "'"; +} + +static std::string describeTraskTraceArgs(const std::vector &args) { + if (args.empty()) { + return "[]"; + } + std::string result("["); + for (size_t i = 0; i < args.size(); ++i) { + if (i != 0) { + result += ", "; + } + result += args[i].toString(); + } + result += "]"; + return result; +} + static Variable Random(const std::vector &args, const RoutineContext &ctx) { // Load auto nMaxInteger = getInt(args, 0); @@ -139,6 +175,11 @@ static Variable AssignCommand(const std::vector &args, const RoutineCo // Transform // Execute + if (isTraskTraceObject(oActionSubject)) { + info("reone trask autodialog trace: AssignCommand subject=" + describeTraskTraceObject(oActionSubject) + + " savedState=" + std::string(aActionToAssign && aActionToAssign->savedState ? "yes" : "no") + + " actionArgs=" + (aActionToAssign ? describeTraskTraceArgs(aActionToAssign->args) : "null")); + } auto commandAction = ctx.game.newAction(std::move(aActionToAssign)); oActionSubject->addAction(std::move(commandAction)); return Variable::ofNull(); @@ -5439,6 +5480,18 @@ static Variable ShowPartySelectionGUI(const std::vector &args, const R auto exitScript = boost::to_lower_copy(sExitScript); // Execute + if (exitScript == "k_pend_reset") { + std::string callerDesc("none"); + if (const Variable *caller = ctx.execution.findArg(ArgKind::Caller)) { + callerDesc = describeTraskTraceObject(ctx.game.getObjectById(caller->objectId)); + } + info("reone trask autodialog trace: ShowPartySelectionGUI exitScript='" + exitScript + + "' forceNpc1=" + std::to_string(nForceNPC1) + + " forceNpc2=" + std::to_string(nForceNPC2) + + " allowCancel=" + std::to_string(nAllowCancel) + + " caller=" + callerDesc + + " parentArgs=" + describeTraskTraceArgs(ctx.execution.args)); + } PartySelectionContext partyCtx; partyCtx.exitScript = exitScript; partyCtx.forceNpc1 = nForceNPC1; diff --git a/src/libs/game/script/runner.cpp b/src/libs/game/script/runner.cpp index 906a76193..dbebfd857 100644 --- a/src/libs/game/script/runner.cpp +++ b/src/libs/game/script/runner.cpp @@ -22,6 +22,7 @@ #include "reone/script/executioncontext.h" #include "reone/script/routines.h" #include "reone/script/virtualmachine.h" +#include "reone/system/logutil.h" using namespace reone::script; @@ -30,16 +31,47 @@ namespace reone { namespace game { +static bool isTraskAutoDialogTraceScript(const std::string &resRef) { + return resRef == "k_pend_reset"; +} + +static std::string describeTraceArgs(const std::vector &args) { + if (args.empty()) { + return "[]"; + } + std::string result("["); + for (size_t i = 0; i < args.size(); ++i) { + if (i != 0) { + result += ", "; + } + result += args[i].toString(); + } + result += "]"; + return result; +} + int ScriptRunner::run(const std::string &resRef, const std::vector &args) { + if (isTraskAutoDialogTraceScript(resRef)) { + info("reone trask autodialog trace: scriptRunner run begin resRef='" + resRef + "' args=" + describeTraceArgs(args)); + } + auto program = _scripts.get(resRef); - if (!program) + if (!program) { + if (isTraskAutoDialogTraceScript(resRef)) { + info("reone trask autodialog trace: scriptRunner run missing resRef='" + resRef + "'"); + } return -1; + } auto ctx = std::make_unique(); ctx->routines = &_routines; ctx->args = args; - return VirtualMachine(program, std::move(ctx)).run(); + int result = VirtualMachine(program, std::move(ctx)).run(); + if (isTraskAutoDialogTraceScript(resRef)) { + info("reone trask autodialog trace: scriptRunner run end resRef='" + resRef + "' result=" + std::to_string(result)); + } + return result; } int ScriptRunner::run(const std::string &resRef, uint32_t callerId) { diff --git a/src/libs/script/virtualmachine.cpp b/src/libs/script/virtualmachine.cpp index 9a38112df..ed1284a0b 100644 --- a/src/libs/script/virtualmachine.cpp +++ b/src/libs/script/virtualmachine.cpp @@ -33,6 +33,44 @@ namespace script { static constexpr int kStartInstructionOffset = 13; static constexpr float kFloatTolerance = 1e-5; +static bool isTraskAutoDialogTraceRoutine(const std::string &routineName) { + return routineName == "ClearAllActions" || + routineName == "GetPartyMemberByIndex" || + routineName == "AssignCommand" || + routineName == "ActionStartConversation" || + routineName == "SetGlobalNumber"; +} + +static std::string describeTraskTraceVariables(const std::vector &args) { + if (args.empty()) { + return "[]"; + } + std::string result("["); + for (size_t i = 0; i < args.size(); ++i) { + if (i != 0) { + result += ", "; + } + result += args[i].toString(); + } + result += "]"; + return result; +} + +static std::string describeTraskTraceContextArgs(const std::vector &args) { + if (args.empty()) { + return "[]"; + } + std::string result("["); + for (size_t i = 0; i < args.size(); ++i) { + if (i != 0) { + result += ", "; + } + result += args[i].toString(); + } + result += "]"; + return result; +} + VirtualMachine::VirtualMachine(std::shared_ptr program, std::unique_ptr context) : _context(std::move(context)), _program(std::move(program)) { @@ -401,7 +439,17 @@ void VirtualMachine::executeACTION(const Instruction &ins) { } } + bool traskTrace = _program->name() == "k_pend_reset" && isTraskAutoDialogTraceRoutine(routine.name()); + if (traskTrace) { + info(str(boost::format("reone trask autodialog trace: vm action begin script='%s' offset=%04x routine='%s' args=%s contextArgs=%s") % + _program->name() % ins.offset % routine.name() % describeTraskTraceVariables(args) % describeTraskTraceContextArgs(_context->args))); + } + Variable retValue = routine.invoke(args, *_context); + if (traskTrace) { + info(str(boost::format("reone trask autodialog trace: vm action end script='%s' offset=%04x routine='%s' result=%s") % + _program->name() % ins.offset % routine.name() % retValue.toString())); + } if (Logger::instance.isChannelEnabled(LogChannel::Script2)) { std::vector argStrings; for (auto &arg : args) { From 1e44fe571bc1885e1f04d1e281587063df582bb7 Mon Sep 17 00:00:00 2001 From: Eldbury Date: Sun, 19 Apr 2026 17:36:32 +0930 Subject: [PATCH 6/7] Clean up Trask diagnostics and integrate dev hotkey help --- docs/documentation.md | 145 ++++++++++++--------- docs/plans.md | 32 ++++- include/reone/game/gui/partyselect.h | 1 - src/apps/launcher/frame.cpp | 3 + src/libs/game/action/startconversation.cpp | 31 ----- src/libs/game/gui/partyselect.cpp | 58 +-------- src/libs/game/object/area.cpp | 26 ---- src/libs/game/script/routine/impl/main.cpp | 53 -------- src/libs/game/script/runner.cpp | 36 +---- src/libs/script/virtualmachine.cpp | 48 ------- 10 files changed, 121 insertions(+), 312 deletions(-) diff --git a/docs/documentation.md b/docs/documentation.md index 35a15da47..8863fa483 100644 --- a/docs/documentation.md +++ b/docs/documentation.md @@ -532,8 +532,8 @@ Minimal fix: - Kept the already-integrated trigger delayed-action fix as-is. - Fixed the shared conversation dispatcher in `src\libs\game\object\area.cpp` so `Area::startDialog` validates the resolved dialogue resref after falling back to `object->conversation()`. - This allows scripted `ActionStartConversation` calls with an empty dialogue resref to use the target object's default conversation, matching the manual-talk path. -- After live verification showed the first Trask callback still did not auto-play after party selection, fixed `src\libs\game\gui\partyselect.cpp` so the party-selection exit script runs as the current party leader/player instead of running with no caller. -- This gives K1's `k_pend_reset` a valid `OBJECT_SELF` for its caller-sensitive `ClearAllActions()` and follow-up conversation assignment without changing sequence content. +- A short-lived party-selection caller follow-up was tested during diagnosis, but later live tracing showed it was not the causal fix. +- The final causal follow-up is documented in the action continuation section below: assigned `DoCommandAction` already supplies the assigned actor as `OBJECT_SELF`, and `ClearAllActions()` needed to preserve the active assigned action continuation. What was intentionally not ported: @@ -544,7 +544,7 @@ What was intentionally not ported: Why those were excluded: -- The missing auto-dialogue shape is explained by two shared dispatch issues: the conversation fallback returned before `Game::startDialog` could run, and the party-selection exit script callback ran without a caller. +- The missing auto-dialogue shape is explained by shared engine dispatch issues: the conversation fallback returned before `Game::startDialog` could run, and the assigned action continuation could be erased by `ClearAllActions()`. - The trigger delayed-action donor fix was already integrated and validated in the previous slice. - The old party-selection donor work addresses roster corruption around the Trask join flow, not this empty-dialog-resref dispatch failure. - No K1 content hack or gameplay/runtime combat change is needed for this slice. @@ -582,62 +582,10 @@ Human verification needed: Live verification follow-up: - Human testing confirmed the Carth-side trigger delayed-action fix works, but the first Trask auto-dialogue still did not play immediately after Trask joins the party. -- The remaining first-half cause is a separate shared callback issue: the party-selection GUI ran its configured exit script with no caller, while K1's `k_pend_reset` expects a valid `OBJECT_SELF` before it can pass `ClearAllActions()` and assign Trask's follow-up conversation. -- The minimal follow-up fix keeps the sequence content untouched and runs the party-selection exit script as the current party leader/player after the party screen closes. -- This avoids running the callback as the pre-party Trask object, which could cancel the delayed cleanup queued by the join script. +- A temporary party-selection caller patch and trace logging proved the party screen closed, `k_pend_reset` ran, Trask was resolved, and `AssignCommand` queued the follow-up work. +- The trace also proved the real remaining failure was lower-level: the assigned action chain died immediately after `ClearAllActions()`. - No donor combat, hostility, boarding-party, encounter, first-door content, party roster, or modernization code was ported. -## 2026-04-19: Temporary K1 Trask Auto-Dialogue Trace - -Purpose: - -- This is a temporary instrumentation slice only. -- It is meant to identify the exact live failure point after Trask joins the party and the party-selection screen closes. -- It does not intentionally change gameplay, combat, hostility, encounter, party roster, item, cutscene, or dialogue content behavior. - -Temporary trace prefix: - -- All added trace lines use `reone trask autodialog trace:` so they can be filtered from `build\bin\engine.log`. - -Temporary trace points: - -- `src\libs\game\script\routine\impl\main.cpp` - - `ShowPartySelectionGUI`: logs only when the exit script is `k_pend_reset`; records forced NPC args, allow-cancel arg, caller object, and parent script args. - - `AssignCommand`: logs when the command subject is a Trask-like object; records the subject and saved action context. -- `src\libs\game\gui\partyselect.cpp` - - `PartySelection::prepare`: logs only for `k_pend_reset`; records forced NPCs and current party state. - - `BTN_DONE` close callback: logs that Done closed party selection before `changeParty()`. - - `BTN_BACK` close callback: logs that Back closed party selection without `changeParty()`. - - `PartySelection::runExitScript`: logs the exact exit script, selected caller, and party state before dispatch. -- `src\libs\game\script\runner.cpp` - - `ScriptRunner::run`: logs begin/missing/end only for `k_pend_reset`, including script args and VM result. -- `src\libs\script\virtualmachine.cpp` - - `VirtualMachine::executeACTION`: logs begin/end only inside script `k_pend_reset` and only for `ClearAllActions`, `GetPartyMemberByIndex`, `AssignCommand`, `ActionStartConversation`, and `SetGlobalNumber`. -- `src\libs\game\action\startconversation.cpp` - - `StartConversationAction::execute`: logs only when the actor or target is Trask-like; records reached/not-reached, actor, target, dialog resref, and range-ignore flag. -- `src\libs\game\object\area.cpp` - - `Area::startDialog`: logs only when the dialogue owner is Trask-like; records input resref and resolved final resref. - -Manual evidence capture: - -- Rebuild the engine. -- Launch `D:\git\reone-modawan-main\build\bin\launcher.exe`. -- Start K1, play normally until Trask joins and party management closes. -- Close the game after confirming whether the unlock-door dialogue auto-plays. -- Inspect `D:\git\reone-modawan-main\build\bin\engine.log` and filter for `reone trask autodialog trace:`. - -Hypotheses tested: - -- No `ShowPartySelectionGUI` trace means the live path is not using the expected K1 party-selection script call. -- `ShowPartySelectionGUI` without `PartySelection::prepare` means the GUI context is not reaching the party-selection screen. -- `PartySelection::prepare` without a close-path trace means the screen is closing through an uninstrumented path. -- Close-path trace without `runExitScript` means the exit-script dispatch helper is not reached. -- `runExitScript` without `ScriptRunner::run begin` means dispatch does not enter the script runner. -- Script runner begin without VM actions means `k_pend_reset` is missing, empty, or halting before the traced calls. -- VM `AssignCommand` without later `ActionStartConversation` or `StartConversationAction` means the assigned action is not executing. -- `StartConversationAction` without `Area::startDialog` means the action is blocked before dialogue dispatch, likely by range/navigation. -- `Area::startDialog` with an empty final resref means dialogue owner/default conversation resolution is still wrong. - ## 2026-04-19: K1 Trask Auto-Dialogue Action Continuation Fix Traced failure point: @@ -660,15 +608,16 @@ Minimal fix: - Forced clears still clear the queue, and clears outside active action execution keep the existing queue-clearing behavior. - This is a shared action semantics fix; it has no Trask-specific, Endar-Spire-specific, dialogue-content-specific, combat, hostility, encounter, boarding-party, item, or modernization branch. -Temporary instrumentation status: +Cleanup status: -- The temporary `reone trask autodialog trace:` logs remain in this validation build so the next live run can prove whether the preserved continuation reaches `ActionStartConversation()` and `Area::startDialog()`. -- Once live verification confirms the exact path, remove or reduce the temporary trace lines. +- Human testing confirmed the preserved continuation now reaches the scripted follow-up dialogue. +- The temporary `reone trask autodialog trace:` instrumentation was removed after validation. +- The earlier party-selection caller patch was removed because `DoCommandAction` already updates the resumed saved script context caller to the assigned actor; the causal fix is the action continuation-safe clear. What was intentionally not changed: - No K1 content scripts, dialogue files, module data, Trask-specific special cases, combat legality, reciprocal hostility, boarding-party hostility persistence, encounter sequencing, item behavior, PR #77 work, Dear ImGui work, or modernization were ported or added. -- The earlier party-selection caller patch is not treated as the causal live fix; the latest trace shows the real blocker is the action queue clear inside the assigned command chain. It can be reconsidered after live validation of the engine fix. +- The earlier party-selection caller patch is not retained; the latest trace showed the real blocker was the action queue clear inside the assigned command chain. Validation: @@ -693,5 +642,75 @@ Validation: Human verification needed: - Automated smoke verifies startup only; it does not drive the Trask sequence. -- Launch K1 with the rebuilt engine, close party selection after Trask joins, and confirm whether the unlock-door dialogue auto-plays. -- Filter `D:\git\reone-modawan-main\build\bin\engine.log` for `reone trask autodialog trace:` and confirm the preserved continuation reaches `ActionStartConversation` and `Area::startDialog`. +- Launch K1 with the rebuilt engine, close party selection after Trask joins, and confirm the unlock-door dialogue auto-plays. +- Let Trask open the first door and confirm the follow-up dialogue auto-plays. + +## 2026-04-19: Stable Branch Cleanup and Developer Tools Launcher Polish + +Cleanup done: + +- Removed the temporary `reone trask autodialog trace:` instrumentation from script dispatch, party selection, the VM action hook, `StartConversationAction`, and `Area::startDialog`. +- Removed the temporary party-selection caller helper and restored party-selection exit scripts to the existing plain `ScriptRunner::run(_context.exitScript)` path. +- Kept the real engine fixes intact: trigger-owned delayed actions, empty dialogue-resref fallback, and continuation-safe `ClearAllActions()`. + +Party-selection patch decision: + +- The party-selection caller patch was removed. +- Live tracing showed it was not the causal Trask fix: the assigned `DoCommandAction` path updates the saved script context caller to the assigned actor before resuming the VM. +- With that behavior, `k_pend_reset` does not need the party-selection screen to provide a special caller for the assigned Trask continuation. + +Developer tools access: + +- The developer tools continue to use the existing launcher Developer Mode checkbox and `-Dev`/`--dev` flow. +- No parallel custom launcher path or new config flag was added. +- The launcher now labels the Developer Mode area with the current in-game tool hotkeys. + +Hotkeys: + +- `Ctrl+Shift+D`: toggle the developer overlay. +- `Ctrl+Shift+T`: toggle trigger zones and trigger state labels. +- `Ctrl+Shift+A`: toggle entity/world labels. +- `Ctrl+Shift+W`: toggle the watch panel. + +How to enable: + +- Check Developer Mode in `launcher.exe`, or launch with `scripts\run_k1.ps1 -Dev` / `scripts\run_k2.ps1 -Dev`. +- Enter an in-game module and use the hotkeys above. + +How to disable: + +- Launch without Developer Mode, or hide the overlay/tools with the same hotkeys. + +Validation: + +- `git diff --check` + - Result: no whitespace errors; only CRLF normalization warnings. +- Build: + - `powershell -NoProfile -ExecutionPolicy Bypass -File .\scripts\build.ps1 -Config RelWithDebInfo -Target engine -Configure` + - Result: passed. `D:\git\reone-modawan-main\build\bin\engine.exe` was rebuilt at 2026-04-19 17:22:14 local time. + - CMake still emits a non-blocking Boost CMP0167 developer warning through vcpkg. +- Launcher build: + - `powershell -NoProfile -ExecutionPolicy Bypass -File .\scripts\build.ps1 -Config RelWithDebInfo -Target launcher` + - Result: passed. `D:\git\reone-modawan-main\build\bin\launcher.exe` was rebuilt at 2026-04-19 17:22:45 local time. +- Smoke test without game install: + - `powershell -NoProfile -ExecutionPolicy Bypass -File .\scripts\smoke_test.ps1 -AllowMissingGame` + - `powershell -NoProfile -ExecutionPolicy Bypass -File .\scripts\eval_smoke.ps1 -SmokeDir .\.agent\logs\smoke_latest -AllowMissingGame` + - Result: passed. Launch was skipped by design. +- K1 smoke/eval: + - `powershell -NoProfile -ExecutionPolicy Bypass -File .\scripts\smoke_test.ps1 -GameDir 'D:\SteamLibrary\steamapps\common\swkotor' -Game k1` + - `powershell -NoProfile -ExecutionPolicy Bypass -File .\scripts\eval_smoke.ps1 -SmokeDir .\.agent\logs\smoke_latest` + - Result: passed. Smoke artifacts were written to `.agent\logs\smoke_20260419_172329`. +- K2 smoke/eval: + - `powershell -NoProfile -ExecutionPolicy Bypass -File .\scripts\smoke_test.ps1 -GameDir 'D:\SteamLibrary\steamapps\common\Knights of the Old Republic II' -Game k2` + - `powershell -NoProfile -ExecutionPolicy Bypass -File .\scripts\eval_smoke.ps1 -SmokeDir .\.agent\logs\smoke_latest` + - Result: passed. Smoke artifacts were written to `.agent\logs\smoke_20260419_172412`. +- Artifact check: + - `D:\git\reone-modawan-main\build\bin\engine.exe` exists. + - `D:\git\reone-modawan-main\build\bin\launcher.exe` exists. + - `D:\git\reone-modawan-main\build\bin\shaderpack.erf` exists. + +Human verification needed: + +- Automated smoke validates startup only; it does not press launcher checkboxes or in-game overlay hotkeys. +- Launch through `D:\git\reone-modawan-main\build\bin\launcher.exe`, confirm the Developer Mode area shows the hotkey help, check Developer Mode, and confirm `Ctrl+Shift+D`, `Ctrl+Shift+T`, `Ctrl+Shift+A`, and `Ctrl+Shift+W` still work in game. +- Because the party-selection caller patch was removed after the deeper action fix, re-check the K1 Trask join and first-door handoff once in game: the unlock-door and "take the lead" dialogues should still auto-play. diff --git a/docs/plans.md b/docs/plans.md index c1d24fe18..38f751dd1 100644 --- a/docs/plans.md +++ b/docs/plans.md @@ -136,13 +136,13 @@ Non-goals: ## Tiny Migration Milestone: K1 Trask Auto-Dialogue Dispatch -Goal: restore the early Endar Spire scripted Trask follow-up conversations by fixing the shared conversation fallback and party-selection exit-script callback paths. +Goal: restore the early Endar Spire scripted Trask follow-up conversations by fixing shared conversation fallback and assigned-action continuation semantics. Acceptance criteria: - `ActionStartConversation` calls with an empty dialogue resref can fall back to the target object's default conversation. -- Party-selection exit scripts run with the current party leader/player as caller so K1 callback scripts that begin with caller-sensitive actions can dispatch their follow-up conversations. -- The changes are limited to shared script dispatch/callback wiring and do not alter Trask content, trigger geometry, combat legality, hostility, boarding-party persistence, encounter sequencing, item behavior, or cutscene logic. +- `Object::clearAllActions(false)` preserves the active assigned action frame and queued continuation behind it when invoked from that active frame. +- The changes are limited to shared script/action semantics and do not alter Trask content, trigger geometry, combat legality, hostility, boarding-party persistence, encounter sequencing, item behavior, or cutscene logic. - The previously integrated trigger-owned delayed-action fix remains unchanged. - K1 and K2 smoke/eval pass after the change. @@ -177,10 +177,34 @@ Verification: - Run generic smoke/eval with `-AllowMissingGame`. - Run K1 smoke/eval. - Run K2 smoke/eval. -- Human K1 verification should close party selection after Trask joins and confirm the trace reaches `ActionStartConversation`/`Area::startDialog` and the unlock-door dialogue auto-plays. +- Human K1 verification should close party selection after Trask joins and confirm the unlock-door dialogue auto-plays without temporary trace logging. Non-goals: - Do not port donor party-selection forced-companion work. - Do not port combat, hostility, boarding-party, encounter, journal, content, launcher, or modernization changes. - Do not turn temporary Trask trace logging into a permanent broad logging system. + +## Tiny Migration Milestone: Stable Dev Tools Cleanup + +Goal: remove temporary Trask tracing and keep developer tools on the existing launcher Developer Mode path. + +Acceptance criteria: + +- Temporary `reone trask autodialog trace:` log points are removed after live validation. +- The temporary party-selection caller helper is removed because the assigned action continuation fix is causal and `DoCommandAction` supplies the assigned actor as caller. +- The launcher Developer Mode area documents the current dev-tool hotkeys without adding a parallel config path. +- Trigger, dialogue fallback, and action continuation fixes remain intact. +- K1 and K2 smoke/eval pass after the cleanup. + +Verification: + +- Build engine and launcher targets. +- Run generic smoke/eval with `-AllowMissingGame`. +- Run K1 smoke/eval. +- Run K2 smoke/eval. +- Human verification should launch through `launcher.exe`, check Developer Mode, confirm the hotkey help is visible, and confirm `Ctrl+Shift+D/T/A/W` still work in game. + +Non-goals: + +- Do not add gameplay fixes, donor gameplay behavior, combat/hostility/boarding-party changes, broad launcher redesign, Dear ImGui work, or modernization. diff --git a/include/reone/game/gui/partyselect.h b/include/reone/game/gui/partyselect.h index 1092c1126..fa69f3da0 100644 --- a/include/reone/game/gui/partyselect.h +++ b/include/reone/game/gui/partyselect.h @@ -165,7 +165,6 @@ class PartySelection : public GameGUI { void refreshAvailableCount(); void refreshNpcButtons(); void removeNpc(int npc); - void runExitScript(); gui::ToggleButton &getNpcButton(int npc); diff --git a/src/apps/launcher/frame.cpp b/src/apps/launcher/frame.cpp index 915642f71..8c18447c0 100644 --- a/src/apps/launcher/frame.cpp +++ b/src/apps/launcher/frame.cpp @@ -50,6 +50,8 @@ LauncherFrame::LauncherFrame() : _checkBoxDev = new wxCheckBox(this, wxID_ANY, "Developer Mode", wxDefaultPosition, wxDefaultSize); _checkBoxDev->SetValue(_config.devMode); + auto devToolsHelp = new wxStaticText(this, wxID_ANY, "Developer tools: Ctrl+Shift+D overlay, Ctrl+Shift+T triggers, Ctrl+Shift+A labels, Ctrl+Shift+W watch", wxDefaultPosition, wxDefaultSize); + devToolsHelp->Wrap(600); // END Setup controls @@ -341,6 +343,7 @@ LauncherFrame::LauncherFrame() : topSizer2->SetMinSize(640, 100); topSizer2->Add(gameSizer, wxSizerFlags(0).Expand().Border(wxALL, 3)); topSizer2->Add(_checkBoxDev, wxSizerFlags(0).Expand().Border(wxALL, 3)); + topSizer2->Add(devToolsHelp, wxSizerFlags(0).Expand().Border(wxLEFT | wxRIGHT | wxBOTTOM, 3)); topSizer2->Add(topSizer, wxSizerFlags(0).Expand().Border(wxALL, 3)); topSizer2->Add(new wxButton(this, WindowID::launch, "Launch"), wxSizerFlags(0).Expand().Border(wxALL, 3)); topSizer2->Add(new wxButton(this, WindowID::saveConfig, "Save Configuration"), wxSizerFlags(0).Expand().Border(wxALL, 3)); diff --git a/src/libs/game/action/startconversation.cpp b/src/libs/game/action/startconversation.cpp index 2cb96929a..a74240e09 100644 --- a/src/libs/game/action/startconversation.cpp +++ b/src/libs/game/action/startconversation.cpp @@ -20,7 +20,6 @@ #include "reone/game/di/services.h" #include "reone/game/game.h" #include "reone/game/party.h" -#include "reone/system/logutil.h" namespace reone { @@ -28,27 +27,6 @@ namespace game { static constexpr float kMaxConversationDistance = 4.0f; -static bool containsTraskTraceToken(const std::string &value) { - return value.find("trask") != std::string::npos; -} - -static bool isTraskTraceObject(const std::shared_ptr &object) { - return object && - (containsTraskTraceToken(object->tag()) || - containsTraskTraceToken(object->blueprintResRef()) || - containsTraskTraceToken(object->conversation())); -} - -static std::string describeTraskTraceObject(const std::shared_ptr &object) { - if (!object) { - return "null"; - } - return "#" + std::to_string(object->id()) + - " tag='" + object->tag() + - "' blueprint='" + object->blueprintResRef() + - "' conv='" + object->conversation() + "'"; -} - void StartConversationAction::execute(std::shared_ptr self, Object &actor, float dt) { auto actorPtr = _game.getObjectById(actor.id()); auto creatureActor = std::static_pointer_cast(actorPtr); @@ -58,15 +36,6 @@ void StartConversationAction::execute(std::shared_ptr self, Object &acto _ignoreStartRange || creatureActor->navigateTo(_objectToConverse->position(), true, kMaxConversationDistance, dt); - bool traskTrace = isTraskTraceObject(actorPtr) || isTraskTraceObject(_objectToConverse); - if (traskTrace) { - info("reone trask autodialog trace: StartConversationAction execute reached=" + std::to_string(static_cast(reached)) + - " actor=" + describeTraskTraceObject(actorPtr) + - " target=" + describeTraskTraceObject(_objectToConverse) + - " dialogResRef='" + _dialogResRef + "'" + - " ignoreStartRange=" + std::to_string(static_cast(_ignoreStartRange))); - } - if (reached) { bool isActorLeader = _game.party().getLeader() == actorPtr; _game.module()->area()->startDialog(isActorLeader ? _objectToConverse : actorPtr, _dialogResRef); diff --git a/src/libs/game/gui/partyselect.cpp b/src/libs/game/gui/partyselect.cpp index 0d2f5c8c1..7eaee27d3 100644 --- a/src/libs/game/gui/partyselect.cpp +++ b/src/libs/game/gui/partyselect.cpp @@ -29,7 +29,6 @@ #include "reone/resource/resources.h" #include "reone/resource/strings.h" #include "reone/script/types.h" -#include "reone/system/logutil.h" using namespace reone::audio; @@ -51,28 +50,6 @@ static int g_strRefRemove = 38456; static glm::vec3 g_kotorColorOn = {0.984314f, 1.0f, 0}; static glm::vec3 g_kotorColorAdded = {0, 0.831373f, 0.090196f}; -static bool isTraskAutoDialogTrace(const PartySelectionContext &ctx) { - return ctx.exitScript == "k_pend_reset"; -} - -static std::string describeTraceObject(const std::shared_ptr &object) { - if (!object) { - return "null"; - } - return "#" + std::to_string(object->id()) + - " tag='" + object->tag() + - "' blueprint='" + object->blueprintResRef() + - "' conv='" + object->conversation() + "'"; -} - -static std::string describeTraceParty(const Party &party) { - std::string result("size=" + std::to_string(party.getSize())); - for (int i = 0; i < party.getSize(); ++i) { - result += " member" + std::to_string(i) + "=" + describeTraceObject(party.getMember(i)); - } - return result; -} - PartySelection::PartySelection(Game &game, ServicesView &services) : GameGUI(game, services) { @@ -101,19 +78,17 @@ void PartySelection::onGUILoaded() { onAcceptButtonClick(); }); _controls.BTN_DONE->setOnClick([this]() { - if (isTraskAutoDialogTrace(_context)) { - info("reone trask autodialog trace: partyselect close path=BTN_DONE before changeParty " + describeTraceParty(_game.party())); - } changeParty(); _game.openInGame(); - runExitScript(); + if (!_context.exitScript.empty()) { + _game.scriptRunner().run(_context.exitScript); + } }); _controls.BTN_BACK->setOnClick([this]() { - if (isTraskAutoDialogTrace(_context)) { - info("reone trask autodialog trace: partyselect close path=BTN_BACK no changeParty " + describeTraceParty(_game.party())); - } _game.openInGame(); - runExitScript(); + if (!_context.exitScript.empty()) { + _game.scriptRunner().run(_context.exitScript); + } }); _controls.BTN_NPC0->setOnClick([this]() { onNpcButtonClick(0); @@ -160,13 +135,6 @@ void PartySelection::prepare(const PartySelectionContext &ctx) { _context = ctx; _availableCount = kMaxFollowerCount; - if (isTraskAutoDialogTrace(_context)) { - info("reone trask autodialog trace: partyselect prepare exitScript='" + _context.exitScript + - "' forceNpc1=" + std::to_string(_context.forceNpc1) + - " forceNpc2=" + std::to_string(_context.forceNpc2) + - " " + describeTraceParty(_game.party())); - } - for (int i = 0; i < kNpcCount; ++i) { _added[i] = false; getNpcButton(i).setUseBorderColorOverride(false); @@ -337,20 +305,6 @@ void PartySelection::changeParty() { area->reloadParty(); } -void PartySelection::runExitScript() { - if (_context.exitScript.empty()) { - return; - } - - auto leader = _game.party().getLeader(); - if (isTraskAutoDialogTrace(_context)) { - info("reone trask autodialog trace: partyselect runExitScript exitScript='" + _context.exitScript + - "' caller=" + describeTraceObject(leader) + - " " + describeTraceParty(_game.party())); - } - _game.scriptRunner().run(_context.exitScript, leader ? leader->id() : 0); -} - } // namespace game } // namespace reone diff --git a/src/libs/game/object/area.cpp b/src/libs/game/object/area.cpp index d4af3ac45..9a9a0f38f 100644 --- a/src/libs/game/object/area.cpp +++ b/src/libs/game/object/area.cpp @@ -78,27 +78,6 @@ static constexpr float kMaxCollisionDistance2 = kMaxCollisionDistance * kMaxColl static glm::vec3 g_defaultAmbientColor {0.2f}; static CameraStyle g_defaultCameraStyle {"", 3.2f, 83.0f, 0.45f, 55.0f}; -static bool containsTraskTraceToken(const std::string &value) { - return value.find("trask") != std::string::npos; -} - -static bool isTraskTraceObject(const std::shared_ptr &object) { - return object && - (containsTraskTraceToken(object->tag()) || - containsTraskTraceToken(object->blueprintResRef()) || - containsTraskTraceToken(object->conversation())); -} - -static std::string describeTraskTraceObject(const std::shared_ptr &object) { - if (!object) { - return "null"; - } - return "#" + std::to_string(object->id()) + - " tag='" + object->tag() + - "' blueprint='" + object->blueprintResRef() + - "' conv='" + object->conversation() + "'"; -} - Area::Area( uint32_t id, std::string sceneName, @@ -823,11 +802,6 @@ void Area::startDialog(const std::shared_ptr &object, const std::string if (resRef.empty()) { finalResRef = object->conversation(); } - if (isTraskTraceObject(object)) { - info("reone trask autodialog trace: Area::startDialog owner=" + describeTraskTraceObject(object) + - " inputResRef='" + resRef + - "' finalResRef='" + finalResRef + "'"); - } if (finalResRef.empty()) { return; } diff --git a/src/libs/game/script/routine/impl/main.cpp b/src/libs/game/script/routine/impl/main.cpp index 1bf48fbd2..91d7143ea 100644 --- a/src/libs/game/script/routine/impl/main.cpp +++ b/src/libs/game/script/routine/impl/main.cpp @@ -63,42 +63,6 @@ namespace reone { namespace game { -static bool containsTraskTraceToken(const std::string &value) { - return value.find("trask") != std::string::npos; -} - -static bool isTraskTraceObject(const std::shared_ptr &object) { - return object && - (containsTraskTraceToken(object->tag()) || - containsTraskTraceToken(object->blueprintResRef()) || - containsTraskTraceToken(object->conversation())); -} - -static std::string describeTraskTraceObject(const std::shared_ptr &object) { - if (!object) { - return "null"; - } - return "#" + std::to_string(object->id()) + - " tag='" + object->tag() + - "' blueprint='" + object->blueprintResRef() + - "' conv='" + object->conversation() + "'"; -} - -static std::string describeTraskTraceArgs(const std::vector &args) { - if (args.empty()) { - return "[]"; - } - std::string result("["); - for (size_t i = 0; i < args.size(); ++i) { - if (i != 0) { - result += ", "; - } - result += args[i].toString(); - } - result += "]"; - return result; -} - static Variable Random(const std::vector &args, const RoutineContext &ctx) { // Load auto nMaxInteger = getInt(args, 0); @@ -175,11 +139,6 @@ static Variable AssignCommand(const std::vector &args, const RoutineCo // Transform // Execute - if (isTraskTraceObject(oActionSubject)) { - info("reone trask autodialog trace: AssignCommand subject=" + describeTraskTraceObject(oActionSubject) + - " savedState=" + std::string(aActionToAssign && aActionToAssign->savedState ? "yes" : "no") + - " actionArgs=" + (aActionToAssign ? describeTraskTraceArgs(aActionToAssign->args) : "null")); - } auto commandAction = ctx.game.newAction(std::move(aActionToAssign)); oActionSubject->addAction(std::move(commandAction)); return Variable::ofNull(); @@ -5480,18 +5439,6 @@ static Variable ShowPartySelectionGUI(const std::vector &args, const R auto exitScript = boost::to_lower_copy(sExitScript); // Execute - if (exitScript == "k_pend_reset") { - std::string callerDesc("none"); - if (const Variable *caller = ctx.execution.findArg(ArgKind::Caller)) { - callerDesc = describeTraskTraceObject(ctx.game.getObjectById(caller->objectId)); - } - info("reone trask autodialog trace: ShowPartySelectionGUI exitScript='" + exitScript + - "' forceNpc1=" + std::to_string(nForceNPC1) + - " forceNpc2=" + std::to_string(nForceNPC2) + - " allowCancel=" + std::to_string(nAllowCancel) + - " caller=" + callerDesc + - " parentArgs=" + describeTraskTraceArgs(ctx.execution.args)); - } PartySelectionContext partyCtx; partyCtx.exitScript = exitScript; partyCtx.forceNpc1 = nForceNPC1; diff --git a/src/libs/game/script/runner.cpp b/src/libs/game/script/runner.cpp index dbebfd857..906a76193 100644 --- a/src/libs/game/script/runner.cpp +++ b/src/libs/game/script/runner.cpp @@ -22,7 +22,6 @@ #include "reone/script/executioncontext.h" #include "reone/script/routines.h" #include "reone/script/virtualmachine.h" -#include "reone/system/logutil.h" using namespace reone::script; @@ -31,47 +30,16 @@ namespace reone { namespace game { -static bool isTraskAutoDialogTraceScript(const std::string &resRef) { - return resRef == "k_pend_reset"; -} - -static std::string describeTraceArgs(const std::vector &args) { - if (args.empty()) { - return "[]"; - } - std::string result("["); - for (size_t i = 0; i < args.size(); ++i) { - if (i != 0) { - result += ", "; - } - result += args[i].toString(); - } - result += "]"; - return result; -} - int ScriptRunner::run(const std::string &resRef, const std::vector &args) { - if (isTraskAutoDialogTraceScript(resRef)) { - info("reone trask autodialog trace: scriptRunner run begin resRef='" + resRef + "' args=" + describeTraceArgs(args)); - } - auto program = _scripts.get(resRef); - if (!program) { - if (isTraskAutoDialogTraceScript(resRef)) { - info("reone trask autodialog trace: scriptRunner run missing resRef='" + resRef + "'"); - } + if (!program) return -1; - } auto ctx = std::make_unique(); ctx->routines = &_routines; ctx->args = args; - int result = VirtualMachine(program, std::move(ctx)).run(); - if (isTraskAutoDialogTraceScript(resRef)) { - info("reone trask autodialog trace: scriptRunner run end resRef='" + resRef + "' result=" + std::to_string(result)); - } - return result; + return VirtualMachine(program, std::move(ctx)).run(); } int ScriptRunner::run(const std::string &resRef, uint32_t callerId) { diff --git a/src/libs/script/virtualmachine.cpp b/src/libs/script/virtualmachine.cpp index ed1284a0b..9a38112df 100644 --- a/src/libs/script/virtualmachine.cpp +++ b/src/libs/script/virtualmachine.cpp @@ -33,44 +33,6 @@ namespace script { static constexpr int kStartInstructionOffset = 13; static constexpr float kFloatTolerance = 1e-5; -static bool isTraskAutoDialogTraceRoutine(const std::string &routineName) { - return routineName == "ClearAllActions" || - routineName == "GetPartyMemberByIndex" || - routineName == "AssignCommand" || - routineName == "ActionStartConversation" || - routineName == "SetGlobalNumber"; -} - -static std::string describeTraskTraceVariables(const std::vector &args) { - if (args.empty()) { - return "[]"; - } - std::string result("["); - for (size_t i = 0; i < args.size(); ++i) { - if (i != 0) { - result += ", "; - } - result += args[i].toString(); - } - result += "]"; - return result; -} - -static std::string describeTraskTraceContextArgs(const std::vector &args) { - if (args.empty()) { - return "[]"; - } - std::string result("["); - for (size_t i = 0; i < args.size(); ++i) { - if (i != 0) { - result += ", "; - } - result += args[i].toString(); - } - result += "]"; - return result; -} - VirtualMachine::VirtualMachine(std::shared_ptr program, std::unique_ptr context) : _context(std::move(context)), _program(std::move(program)) { @@ -439,17 +401,7 @@ void VirtualMachine::executeACTION(const Instruction &ins) { } } - bool traskTrace = _program->name() == "k_pend_reset" && isTraskAutoDialogTraceRoutine(routine.name()); - if (traskTrace) { - info(str(boost::format("reone trask autodialog trace: vm action begin script='%s' offset=%04x routine='%s' args=%s contextArgs=%s") % - _program->name() % ins.offset % routine.name() % describeTraskTraceVariables(args) % describeTraskTraceContextArgs(_context->args))); - } - Variable retValue = routine.invoke(args, *_context); - if (traskTrace) { - info(str(boost::format("reone trask autodialog trace: vm action end script='%s' offset=%04x routine='%s' result=%s") % - _program->name() % ins.offset % routine.name() % retValue.toString())); - } if (Logger::instance.isChannelEnabled(LogChannel::Script2)) { std::vector argStrings; for (auto &arg : args) { From 72c188f3527dc5ddcbbd79bd9686a7dd78a5749e Mon Sep 17 00:00:00 2001 From: Eldbury Date: Mon, 20 Apr 2026 18:51:12 +0930 Subject: [PATCH 7/7] Remove unrelated docs and scripts from PR --- docs/documentation.md | 716 --------------------------------------- docs/plans.md | 210 ------------ evals/README.md | 48 --- scripts/build.ps1 | 201 ----------- scripts/capture_logs.ps1 | 44 --- scripts/eval_smoke.ps1 | 122 ------- scripts/run_k1.ps1 | 139 -------- scripts/run_k2.ps1 | 139 -------- scripts/smoke_test.ps1 | 356 ------------------- 9 files changed, 1975 deletions(-) delete mode 100644 docs/documentation.md delete mode 100644 docs/plans.md delete mode 100644 evals/README.md delete mode 100644 scripts/build.ps1 delete mode 100644 scripts/capture_logs.ps1 delete mode 100644 scripts/eval_smoke.ps1 delete mode 100644 scripts/run_k1.ps1 delete mode 100644 scripts/run_k2.ps1 delete mode 100644 scripts/smoke_test.ps1 diff --git a/docs/documentation.md b/docs/documentation.md deleted file mode 100644 index 8863fa483..000000000 --- a/docs/documentation.md +++ /dev/null @@ -1,716 +0,0 @@ -# Documentation Log - -## 2026-04-19: First Modawan-Baseline Migration Slice - -Migration decision: - -- `D:\git\reone-modawan-main` is the working repository and the new runtime baseline. -- `D:\reone-master` is a read-only donor/reference repository for selected features only. -- The old branch must not be wholesale merged into modawan. -- Old combat legality, reciprocal hostility, boarding-party hostility persistence, and older combat-runtime compensation patches are intentionally excluded from this first migration slice. -- PR #77 is intentionally excluded from this first migration slice. -- Preserve modawan's stronger combat, items, and runtime behavior unless a later bounded slice proves a specific donor change is still needed. - -Donor categories inspected: - -- Runtime developer/debug overlay and hotkey tools. -- Eval, smoke, logging, and instrumentation harness. -- Early K1 Endar Spire first-door Trask/Carth behavior. - -Bounded first-migration options considered: - -1. Tooling-first migration. - - Leverage: high. Restores repeatable build, run, smoke, log capture, and smoke eval loops before gameplay changes. - - Risk: low. Touches generic scripts, docs, and a shallow startup log/flush only. - - Likely files: `.gitignore`, `scripts/build.ps1`, `scripts/run_k1.ps1`, `scripts/run_k2.ps1`, `scripts/smoke_test.ps1`, `scripts/eval_smoke.ps1`, `scripts/capture_logs.ps1`, `evals/README.md`, `src/apps/engine/main.cpp`, `include/reone/system/logger.h`, `src/libs/system/logger.cpp`, `docs/documentation.md`, `docs/plans.md`. - - Expected K1/K2 impact: positive observability for both games; no gameplay, combat, party, or first-door behavior changes. -2. First-door sequence-first migration. - - Leverage: medium to high for K1 because it targets the earliest Trask/Carth handoff area directly. - - Risk: medium. Donor changes are tangled with older script execution instrumentation, trigger debug state, action continuation details, and some combat-facing click logging. - - Likely files: `src/libs/game/action/docommand.cpp`, `src/libs/game/object/area.cpp`, `src/libs/game/object/trigger.cpp`, `src/libs/game/script/runner.cpp`, `src/libs/script/virtualmachine.cpp`, plus focused docs/eval logs. - - Expected K1/K2 impact: likely positive for K1 Endar Spire if isolated correctly; intended neutral K2 impact, but script/runtime coupling makes it too broad for the first slice. -3. Combined small-slice migration: tooling + logging + first-door fixes only. - - Leverage: highest if successful because it would add harness proof and one visible early-game behavior improvement together. - - Risk: medium to high for a first slice. It mixes infrastructure with gameplay-facing script/trigger behavior, making regressions harder to attribute. - - Likely files: all tooling-first files plus the first-door files listed above. - - Expected K1/K2 impact: positive K1 observability and possible Endar Spire improvement; K2 should be neutral, but combined blast radius is larger than needed. - -Chosen option: - -- Option 1, tooling-first migration. -- Rationale: it is the smallest high-leverage slice and gives modawan its own verification loop before any gameplay donor code is considered. - -What was ported: - -- Generic Windows PowerShell build/run/smoke/log scripts from the donor harness. -- `evals/README.md` describing the smoke eval contract. -- `.agent/` ignore coverage for generated harness logs. -- A minimal startup signal, `reone smoke signal: engine startup`, emitted immediately after logging initializes. -- Public `Logger::flush()` support so bounded smoke runs persist the startup signal before timeout-based process termination. - -What was intentionally not ported: - -- Developer overlay rendering and hotkey panels from donor `Game`; modawan already has developer console/tools, and the donor overlay is larger than this slice. -- Early Endar Spire first-door Trask/Carth script, trigger, or action-continuation behavior. -- Old combat legality, reciprocal hostility, boarding-party hostility persistence, combat-entry, and encounter band-aid patches. -- Focused donor eval scripts for interaction, party, vitality, combat, or K1 interactables, because those depend on donor tests and older migration history not present in modawan yet. - -Local validation procedure: - -- Configure/build from PowerShell: - - `cd D:\git\reone-modawan-main` - - `powershell -NoProfile -ExecutionPolicy Bypass -File .\scripts\build.ps1 -Config RelWithDebInfo -Target engine -Configure` - - To force a specific vcpkg tree, set `$env:VCPKG_ROOT = 'D:\vcpkg'` before running the command, or pass `-CMakeArgs '-DCMAKE_TOOLCHAIN_FILE=D:\vcpkg\scripts\buildsystems\vcpkg.cmake'`. - - The build script defaults to `D:\vcpkg\scripts\buildsystems\vcpkg.cmake` when `VCPKG_ROOT` is unset and that file exists. -- Smoke test without a game install: - - `powershell -NoProfile -ExecutionPolicy Bypass -File .\scripts\smoke_test.ps1 -AllowMissingGame` - - `powershell -NoProfile -ExecutionPolicy Bypass -File .\scripts\eval_smoke.ps1 -SmokeDir .\.agent\logs\smoke_latest -AllowMissingGame` - - This path still builds `engine` and `create_shaderpack`; it only skips launching the engine when no legal KOTOR directory is provided. -- K1 smoke/eval: - - `powershell -NoProfile -ExecutionPolicy Bypass -File .\scripts\smoke_test.ps1 -GameDir 'D:\SteamLibrary\steamapps\common\swkotor' -Game k1` - - `powershell -NoProfile -ExecutionPolicy Bypass -File .\scripts\eval_smoke.ps1 -SmokeDir .\.agent\logs\smoke_latest` - - Alternatively, set `$env:KOTOR1_DIR` to a legal KOTOR 1 install directory and omit `-GameDir`. -- K2 smoke/eval: - - `powershell -NoProfile -ExecutionPolicy Bypass -File .\scripts\smoke_test.ps1 -GameDir 'D:\SteamLibrary\steamapps\common\Knights of the Old Republic II' -Game k2` - - `powershell -NoProfile -ExecutionPolicy Bypass -File .\scripts\eval_smoke.ps1 -SmokeDir .\.agent\logs\smoke_latest` - - Alternatively, set `$env:KOTOR2_DIR` to a legal KOTOR 2 install directory and omit `-GameDir`. -- Common failure modes: - - SDL3/vcpkg: this repo uses SDL3 headers and `find_package(SDL3 CONFIG REQUIRED)`, with the vcpkg package name `sdl3` and CMake target `SDL3::SDL3`. A working classic vcpkg install should provide `D:\vcpkg\installed\x64-windows\share\sdl3\SDL3Config.cmake` or equivalent. If CMake reports missing `SDL3Config.cmake` or `sdl3-config.cmake`, install the missing external prerequisite with `& 'D:\vcpkg\vcpkg.exe' install sdl3:x64-windows`, or point `VCPKG_ROOT`/`CMAKE_TOOLCHAIN_FILE` to a vcpkg tree that already has SDL3 for the target triplet. - - Toolchain detection: if CMake cannot find vcpkg packages that are installed elsewhere, verify `$env:VCPKG_ROOT`, `-DCMAKE_TOOLCHAIN_FILE=...`, and any `-DVCPKG_TARGET_TRIPLET=...` value all refer to the same vcpkg tree/triplet. - - CMake detection: if `cmake.exe` is not on `PATH`, set `$env:CMAKE_EXE` to the full path, for example `C:\Program Files\CMake\bin\cmake.exe`. - - Build cache drift: if the `build` directory contains a stale or partial CMake cache, rerun `scripts\build.ps1` with `-Configure`; if that still fails, remove only the local `build` directory and configure again. - - Game install checks: K1 requires `swkotor.exe` in the supplied directory, and K2 requires `swkotor2.exe`. The smoke harness will skip launch only when `-AllowMissingGame` is used. - -Verification results: - -- PowerShell syntax parsing passed for `scripts/build.ps1`, `scripts/capture_logs.ps1`, `scripts/eval_smoke.ps1`, `scripts/run_k1.ps1`, `scripts/run_k2.ps1`, and `scripts/smoke_test.ps1`. -- `powershell -NoProfile -ExecutionPolicy Bypass -File .\scripts\build.ps1 -Config RelWithDebInfo -Target engine -Configure` - - Result: failed during CMake configure because local vcpkg does not provide SDL3 package config files (`SDL3Config.cmake`, `sdl3-config.cmake`, or equivalent). -- `powershell -NoProfile -ExecutionPolicy Bypass -File .\scripts\smoke_test.ps1 -AllowMissingGame` - - Result: failed before launch for the same SDL3 configure blocker; smoke manifest was written. -- `powershell -NoProfile -ExecutionPolicy Bypass -File .\scripts\eval_smoke.ps1 -SmokeDir .\.agent\logs\smoke_latest -AllowMissingGame` - - Result: failed as expected because the manifest records failed build, missing `engine.exe`, and missing `shaderpack.erf`. -- `powershell -NoProfile -ExecutionPolicy Bypass -File .\scripts\smoke_test.ps1 -GameDir 'D:\SteamLibrary\steamapps\common\swkotor' -Game k1` - - Result: failed before launch for the same SDL3 configure blocker; K1 marker path was available. -- `powershell -NoProfile -ExecutionPolicy Bypass -File .\scripts\eval_smoke.ps1 -SmokeDir .\.agent\logs\smoke_latest` - - Result after K1 smoke: failed because build did not succeed and launch was not attempted. -- `powershell -NoProfile -ExecutionPolicy Bypass -File .\scripts\smoke_test.ps1 -GameDir 'D:\SteamLibrary\steamapps\common\Knights of the Old Republic II' -Game k2` - - Result: failed before launch for the same SDL3 configure blocker; K2 marker path was available. -- `powershell -NoProfile -ExecutionPolicy Bypass -File .\scripts\eval_smoke.ps1 -SmokeDir .\.agent\logs\smoke_latest` - - Result after K2 smoke: failed because build did not succeed and launch was not attempted. - -Remaining blocker: - -- Install or point CMake at SDL3 for the active vcpkg/toolchain environment, then rerun the same build and K1/K2 smoke/eval commands. This slice did not install external dependencies or modify files outside the working repository. - -## 2026-04-19: Local Validation Bootstrap Follow-Up - -Scope: - -- This was a validation/bootstrap step only. -- No donor gameplay behavior, overlay migration, first-door behavior, combat, hostility, boarding-party, or modernization code was ported. -- `scripts\build.ps1` now emits a targeted warning when the active classic vcpkg toolchain is found but `installed\\share\sdl3` is missing. CMake still performs the authoritative configure step and writes the configure log. - -Latest local environment result: - -- Current branch: `port-dev-tools`. -- CMake was found at `C:\Program Files\CMake\bin\cmake.exe`. -- vcpkg toolchain was found at `D:\vcpkg\scripts\buildsystems\vcpkg.cmake`. -- `D:\vcpkg\installed\x64-windows\share\sdl2` exists. -- `D:\vcpkg\installed\x64-windows\share\sdl3` does not exist. -- K1 marker exists at `D:\SteamLibrary\steamapps\common\swkotor\swkotor.exe`. -- K2 marker exists at `D:\SteamLibrary\steamapps\common\Knights of the Old Republic II\swkotor2.exe`. - -Latest local validation result: - -- PowerShell parser checks passed for all scripts under `scripts`. -- `git diff --check` reported only existing CRLF normalization warnings. -- Configure/build still fails because the active vcpkg tree does not have SDL3 installed for `x64-windows`. -- Generic smoke, K1 smoke, and K2 smoke all stop before launch for the same SDL3 configure blocker. -- Generic, K1, and K2 smoke evals ran against the generated manifests and failed as expected because the build did not succeed, `engine.exe` was not produced, and `shaderpack.erf` was not produced. - -## 2026-04-19: Local Validation Closure After SDL3 Install - -Scope: - -- This was a validation closure step only. -- No donor gameplay behavior, overlay migration, first-door behavior, combat, hostility, boarding-party, or modernization code was ported. -- The previous SDL3 blocker is resolved on this machine because `D:\vcpkg\installed\x64-windows\share\sdl3\SDL3Config.cmake` is now present. - -Confirmed local procedure and result: - -- Configure/build: - - `powershell -NoProfile -ExecutionPolicy Bypass -File .\scripts\build.ps1 -Config RelWithDebInfo -Target engine -Configure` - - Result: passed. CMake configured with `D:\vcpkg\scripts\buildsystems\vcpkg.cmake` and built `D:\Git\reone-modawan-main\build\bin\engine.exe`. -- Smoke test without game install: - - `powershell -NoProfile -ExecutionPolicy Bypass -File .\scripts\smoke_test.ps1 -AllowMissingGame` - - `powershell -NoProfile -ExecutionPolicy Bypass -File .\scripts\eval_smoke.ps1 -SmokeDir .\.agent\logs\smoke_latest -AllowMissingGame` - - Result: passed. Launch was skipped by design because no game directory was supplied. `engine.exe` and `shaderpack.erf` were verified in `build\bin`. -- K1 smoke/eval: - - `powershell -NoProfile -ExecutionPolicy Bypass -File .\scripts\smoke_test.ps1 -GameDir 'D:\SteamLibrary\steamapps\common\swkotor' -Game k1` - - `powershell -NoProfile -ExecutionPolicy Bypass -File .\scripts\eval_smoke.ps1 -SmokeDir .\.agent\logs\smoke_latest` - - Result: passed. Smoke artifacts were written to `.agent\logs\smoke_20260419_092431`. -- K2 smoke/eval: - - `powershell -NoProfile -ExecutionPolicy Bypass -File .\scripts\smoke_test.ps1 -GameDir 'D:\SteamLibrary\steamapps\common\Knights of the Old Republic II' -Game k2` - - `powershell -NoProfile -ExecutionPolicy Bypass -File .\scripts\eval_smoke.ps1 -SmokeDir .\.agent\logs\smoke_latest` - - Result: passed. Smoke artifacts were written to `.agent\logs\smoke_20260419_092459`. - -Artifacts confirmed: - -- `D:\Git\reone-modawan-main\build\bin\engine.exe` -- `D:\Git\reone-modawan-main\build\bin\shaderpack.erf` - -Remaining blocker: - -- None for the tooling-first validation slice on this machine. - -## 2026-04-19: Runtime Developer Overlay Observability Slice - -Scope: - -- This is an observability-only runtime overlay slice. -- `D:\reone-master` was used only as a read-only donor/reference for the old developer overlay and hotkey shape. -- No donor gameplay behavior, first-door behavior, combat legality, reciprocal hostility, boarding-party hostility persistence, encounter/combat band-aids, PR #77, Dear ImGui draft work, or modernization code was ported. - -Overlay-only options considered, smallest to largest: - -1. Status/watch overlay shell. - - Adds a developer-mode-gated, read-only in-game overlay with a small help banner and compact watch panel. - - Hotkeys: `Ctrl+Shift+D` toggles the overlay, and `Ctrl+Shift+W` toggles the watch panel. - - Risk: low. Reads existing runtime state and only mutates overlay visibility flags. -2. Status/watch overlay plus developer event feed. - - Adds option 1 plus the donor-style feed panel and `addDeveloperEvent` plumbing, initially logging only overlay toggle events. - - Risk: low to medium. More public game API and event buffer surface. -3. Full donor visual overlay minus gameplay patches. - - Adds trigger outlines, actor labels, target inspector, event feed, and watched values. - - Risk: medium. It touches trigger debug state, object inspection, context-action/hostility-adjacent labels, and broader rendering helpers. - -Chosen option: - -- Option 1, status/watch overlay shell. -- Rationale: it is the smallest high-leverage overlay slice and keeps the implementation read-only, isolated, and easy to disable. - -What was ported: - -- Developer-mode-gated in-game overlay visibility state in `Game`. -- Donor-style `Ctrl+Shift` hotkey chord handling for overlay tools. -- A small on-screen developer banner showing the overlay hotkeys and existing console/profiler shortcuts. -- A read-only watch panel showing screen, module, area, camera mode, game speed, pause state, relative mouse state, party leader id/tag/HP/position, selected object id/tag/resource/type/HP, and hovered object id/tag/resource/type/HP. - -Hotkeys: - -- `Ctrl+Shift+D`: toggle the developer observability overlay. -- `Ctrl+Shift+W`: toggle the watch panel. If the overlay is hidden, this also opens it. -- Existing tools remain available: backquote opens the console, `F5` toggles the profiler, `V` toggles in-game camera mode in developer mode, and `+`/`-` adjust developer game speed. - -How to use: - -- Launch with developer mode enabled, for example `scripts\run_k1.ps1 -Dev` or `scripts\run_k2.ps1 -Dev`, or enable Developer Mode in the launcher. -- Enter an in-game module. -- Press `Ctrl+Shift+D` to show or hide the overlay. -- Press `Ctrl+Shift+W` to show or hide the watch panel. - -How to disable: - -- Launch with developer mode disabled, for example omit `-Dev` from the run scripts or pass `--dev false` to `engine.exe`. -- While in game, press `Ctrl+Shift+D` to hide the overlay. -- Press `Ctrl+Shift+W` to hide only the watch panel while leaving the banner visible. - -What was intentionally not ported: - -- Donor trigger outlines and trigger debug state. -- Donor actor labels. -- Donor target inspector. -- Donor event feed and `addDeveloperEvent` instrumentation. -- Donor script/action/combat/area/creature instrumentation. -- Any donor gameplay behavior, hostility, party, item, cutscene, encounter, or first-door logic. - -Validation results: - -- `powershell -NoProfile -ExecutionPolicy Bypass -File .\scripts\build.ps1 -Config RelWithDebInfo -Target engine -Configure` - - Result: passed. `D:\Git\reone-modawan-main\build\bin\engine.exe` was produced. -- `powershell -NoProfile -ExecutionPolicy Bypass -File .\scripts\smoke_test.ps1 -AllowMissingGame` - - Result: passed. Launch was skipped by design; smoke artifacts were written to `.agent\logs\smoke_20260419_093609`. -- `powershell -NoProfile -ExecutionPolicy Bypass -File .\scripts\eval_smoke.ps1 -SmokeDir .\.agent\logs\smoke_latest -AllowMissingGame` - - Result: passed. -- `powershell -NoProfile -ExecutionPolicy Bypass -File .\scripts\smoke_test.ps1 -GameDir 'D:\SteamLibrary\steamapps\common\swkotor' -Game k1` - - Result: passed. Smoke artifacts were written to `.agent\logs\smoke_20260419_093617`. -- `powershell -NoProfile -ExecutionPolicy Bypass -File .\scripts\eval_smoke.ps1 -SmokeDir .\.agent\logs\smoke_latest` - - Result after K1 smoke: passed. -- `powershell -NoProfile -ExecutionPolicy Bypass -File .\scripts\smoke_test.ps1 -GameDir 'D:\SteamLibrary\steamapps\common\Knights of the Old Republic II' -Game k2` - - Result: passed. Smoke artifacts were written to `.agent\logs\smoke_20260419_093630`. -- `powershell -NoProfile -ExecutionPolicy Bypass -File .\scripts\eval_smoke.ps1 -SmokeDir .\.agent\logs\smoke_latest` - - Result after K2 smoke: passed. -- `git diff --check` - - Result: no whitespace errors; only CRLF normalization warnings. - -Human verification needed: - -- Automated smoke validates startup and the smoke signal, but it does not press overlay hotkeys. -- Human in-game verification should launch with developer mode enabled and confirm `Ctrl+Shift+D` shows/hides the overlay and `Ctrl+Shift+W` shows/hides the watch panel. - -## 2026-04-19: Trigger Zone Developer Overlay Slice - -Scope: - -- This is an observability-only extension to the developer overlay. -- `D:\reone-master` was used only as a read-only donor/reference for the old trigger visualization, entity label, and launcher developer-option wiring. -- No donor gameplay behavior, first-door behavior, combat legality, reciprocal hostility, boarding-party hostility persistence, encounter/combat band-aids, PR #77, Dear ImGui draft work, or modernization code was ported. - -Options considered: - -1. A: trigger zone visualization only. - - Leverage: high. Restores the most visible missing spatial observability from the old branch. - - Risk: low. Reads existing trigger geometry and draws screen-space outlines/labels only. - - Likely files: `include/reone/game/object/trigger.h`, `include/reone/game/game.h`, `src/libs/game/game.cpp`, docs. - - Runtime blast radius: developer-mode render path only. - - Disable: launch without developer mode, press `Ctrl+Shift+D` to hide the overlay, or press `Ctrl+Shift+T` to hide triggers. -2. B: entity/world labels only. - - Leverage: medium to high. Useful for object inspection, faction-style debugging, and hover/selection review. - - Risk: medium. Rich labels tend to pull in creature, faction, hostility, and context-action interpretation. - - Likely files: `include/reone/game/game.h`, `src/libs/game/game.cpp`, docs. - - Runtime blast radius: developer-mode render path over area objects. - - Disable: launch without developer mode, hide the overlay, or add a labels toggle. -3. C: minimal launcher-level developer options wiring. - - Leverage: medium for workflow, low for immediate in-game observability because Developer Mode already exists in the launcher. - - Risk: low. Launcher/config surface only. - - Likely files: `src/apps/launcher/frame.h`, `src/apps/launcher/frame.cpp`, docs. - - Runtime blast radius: launcher/config only. - - Disable: uncheck launcher options. - -Chosen option: - -- Option A, trigger zone visualization only. -- Rationale: it closes the largest manually observed overlay gap with the smallest runtime surface and avoids the faction/hostility-adjacent data needed for useful entity labels. - -What was added: - -- A read-only `Trigger::geometry()` accessor so the overlay can inspect existing trigger polygon points without changing trigger behavior. -- `Ctrl+Shift+T` developer hotkey for trigger zone visualization. -- Screen-space trigger polygon outlines in the developer overlay. -- Trigger labels at the polygon centroid showing object id, tag, and `OnEnter` script when available. - -How to enable: - -- Launch with developer mode enabled, for example `scripts\run_k1.ps1 -Dev`, `scripts\run_k2.ps1 -Dev`, or the launcher Developer Mode checkbox. -- Enter an in-game module. -- Press `Ctrl+Shift+D` to show the developer overlay. Trigger visualization is enabled by default when the overlay is shown. -- Press `Ctrl+Shift+T` to show or hide trigger zones. If the overlay is hidden, this hotkey also opens it. - -Hotkeys: - -- `Ctrl+Shift+D`: toggle the developer overlay. -- `Ctrl+Shift+T`: toggle trigger zone visualization. -- `Ctrl+Shift+W`: toggle the watch panel. - -How to disable: - -- Launch without developer mode. -- Press `Ctrl+Shift+D` to hide the whole overlay. -- Press `Ctrl+Shift+T` to hide trigger zones while keeping the rest of the overlay visible. - -What was intentionally not ported: - -- Donor trigger debug state, colors, occupancy/test/enter timers, or Area/Trigger instrumentation. -- Donor entity/world actor labels. -- Donor target inspector. -- Donor event feed and `addDeveloperEvent` instrumentation. -- Any launcher UI redesign or new launcher controls. -- Any gameplay, combat, hostility, boarding-party, encounter, item, cutscene, party, or first-door behavior. - -Validation results: - -- Default build output `D:\Git\reone-modawan-main\build\bin\engine.exe` was locked by a stale `engine` process from manual verification. PowerShell could see process id `112868` but could not stop it due access permissions. -- Validation was completed with a clean ignored build directory: `.\build\overlay_validation`. -- `powershell -NoProfile -ExecutionPolicy Bypass -File .\scripts\build.ps1 -BuildDir .\build\overlay_validation -Config RelWithDebInfo -Target engine -Configure` - - Result: passed. `D:\Git\reone-modawan-main\build\overlay_validation\bin\engine.exe` was produced. -- `powershell -NoProfile -ExecutionPolicy Bypass -File .\scripts\smoke_test.ps1 -BuildDir .\build\overlay_validation -AllowMissingGame` - - Result: passed. Launch was skipped by design. -- `powershell -NoProfile -ExecutionPolicy Bypass -File .\scripts\eval_smoke.ps1 -SmokeDir .\.agent\logs\smoke_latest -AllowMissingGame` - - Result: passed. -- `powershell -NoProfile -ExecutionPolicy Bypass -File .\scripts\smoke_test.ps1 -BuildDir .\build\overlay_validation -GameDir 'D:\SteamLibrary\steamapps\common\swkotor' -Game k1` - - Result: passed. Smoke artifacts were written to `.agent\logs\smoke_20260419_122754`. -- `powershell -NoProfile -ExecutionPolicy Bypass -File .\scripts\eval_smoke.ps1 -SmokeDir .\.agent\logs\smoke_latest` - - Result after K1 smoke: passed. -- `powershell -NoProfile -ExecutionPolicy Bypass -File .\scripts\smoke_test.ps1 -BuildDir .\build\overlay_validation -GameDir 'D:\SteamLibrary\steamapps\common\Knights of the Old Republic II' -Game k2` - - Result: passed. Smoke artifacts were written to `.agent\logs\smoke_20260419_122825`. -- `powershell -NoProfile -ExecutionPolicy Bypass -File .\scripts\eval_smoke.ps1 -SmokeDir .\.agent\logs\smoke_latest` - - Result after K2 smoke: passed. - -Human verification needed: - -- Automated smoke validates startup, but it does not press overlay hotkeys. -- Human in-game verification should launch with developer mode enabled and confirm `Ctrl+Shift+D` shows/hides the overlay and `Ctrl+Shift+T` shows/hides trigger outlines and labels. - -Default build retry after closing game: - -- The previous default build lock was caused by a running `engine.exe` from manual verification. -- After that process was closed, the normal default build directory validated successfully. -- `powershell -NoProfile -ExecutionPolicy Bypass -File .\scripts\build.ps1 -Config RelWithDebInfo -Target engine -Configure` - - Result: passed. `D:\Git\reone-modawan-main\build\bin\engine.exe` was produced. -- `powershell -NoProfile -ExecutionPolicy Bypass -File .\scripts\smoke_test.ps1 -AllowMissingGame` - - Result: passed. Launch was skipped by design. -- `powershell -NoProfile -ExecutionPolicy Bypass -File .\scripts\eval_smoke.ps1 -SmokeDir .\.agent\logs\smoke_latest -AllowMissingGame` - - Result: passed. -- `powershell -NoProfile -ExecutionPolicy Bypass -File .\scripts\smoke_test.ps1 -GameDir 'D:\SteamLibrary\steamapps\common\swkotor' -Game k1` - - Result: passed. Smoke artifacts were written to `.agent\logs\smoke_20260419_123202`. -- `powershell -NoProfile -ExecutionPolicy Bypass -File .\scripts\eval_smoke.ps1 -SmokeDir .\.agent\logs\smoke_latest` - - Result after K1 smoke: passed. -- `powershell -NoProfile -ExecutionPolicy Bypass -File .\scripts\smoke_test.ps1 -GameDir 'D:\SteamLibrary\steamapps\common\Knights of the Old Republic II' -Game k2` - - Result: passed. Smoke artifacts were written to `.agent\logs\smoke_20260419_123230`. -- `powershell -NoProfile -ExecutionPolicy Bypass -File .\scripts\eval_smoke.ps1 -SmokeDir .\.agent\logs\smoke_latest` - - Result after K2 smoke: passed. - -## 2026-04-19: Developer Overlay Gap-Fix Slice - -Scope: - -- This is an observability-only follow-up to the trigger zone overlay slice. -- `D:\reone-master` was used only as a read-only donor/reference for old trigger state coloring and lightweight entity labels. -- No donor gameplay behavior, first-door behavior, combat legality, reciprocal hostility, boarding-party hostility persistence, encounter/combat band-aids, PR #77, Dear ImGui draft work, launcher redesign, or modernization code was ported. - -What was added: - -- Trigger zones now expose read-only debug state in the overlay: - - `default`: normal blue outline. - - `tested`: yellow outline after a creature movement tested the trigger. - - `inside`: green outline while an object is inside or was just detected inside. - - `enter`: orange outline briefly after a trigger enters/fires. -- Trigger labels now include the debug state, for example `#230 end_trig02 k_pend_trig02 [inside]`. -- Lightweight entity/world labels can be toggled with `Ctrl+Shift+A`. -- Entity labels show read-only id, tag, template, faction, hostile-to-player flag for creatures, selectable flag, commandable flag, visible flag, and plot flag. -- Door and placeable faction accessors were added only so the developer label can display existing loaded faction data. - -Hotkeys: - -- `Ctrl+Shift+D`: toggle the developer overlay. -- `Ctrl+Shift+T`: toggle trigger zones and trigger state labels. -- `Ctrl+Shift+A`: toggle entity/world labels. -- `Ctrl+Shift+W`: toggle the watch panel. - -How to enable: - -- Launch with developer mode enabled, for example `scripts\run_k1.ps1 -Dev`, `scripts\run_k2.ps1 -Dev`, or the launcher Developer Mode checkbox. -- Enter an in-game module. -- Press `Ctrl+Shift+D` to show the developer overlay. -- Press `Ctrl+Shift+T` for trigger zones and `Ctrl+Shift+A` for entity labels. If the overlay is hidden, those feature hotkeys open the overlay with the requested feature enabled. - -How to disable: - -- Launch without developer mode. -- Press `Ctrl+Shift+D` to hide the whole overlay. -- Press `Ctrl+Shift+T` to hide trigger zones. -- Press `Ctrl+Shift+A` to hide entity labels. - -What was intentionally not ported: - -- Donor target inspector. -- Donor event feed and broad `addDeveloperEvent` instrumentation. -- Donor trigger/area encounter logging and first-door-specific trigger diagnostics. -- Donor launcher developer-option controls. -- Any gameplay, combat, hostility, boarding-party, encounter, item, cutscene, party, first-door, or modernization behavior. - -Validation: - -- Default build note: - - `powershell -NoProfile -ExecutionPolicy Bypass -File .\scripts\build.ps1 -Config RelWithDebInfo -Target engine -Configure` - - Result: compile passed, but the final link failed with `LNK1168: cannot open D:\Git\reone-modawan-main\build\bin\engine.exe for writing`, indicating the default output executable was locked locally. -- Default build retry: - - `powershell -NoProfile -ExecutionPolicy Bypass -File .\scripts\build.ps1 -Config RelWithDebInfo -Target engine` - - Result: passed after the local executable lock was gone. `D:\Git\reone-modawan-main\build\bin\engine.exe` now contains the `Ctrl+Shift+A` label hotkey. -- Hotkey follow-up: - - Feature hotkeys now open the overlay with that feature enabled when the overlay is hidden, instead of toggling a default-on feature off. - - `Ctrl+Shift+T`, `Ctrl+Shift+A`, and `Ctrl+Shift+W` keep their normal toggle behavior while the overlay is already visible. -- Validation build: - - `powershell -NoProfile -ExecutionPolicy Bypass -File .\scripts\build.ps1 -BuildDir .\build\overlay_labels_validation -Config RelWithDebInfo -Target engine -Configure` - - Result: passed. `D:\Git\reone-modawan-main\build\overlay_labels_validation\bin\engine.exe` was produced. -- Smoke test without game install: - - `powershell -NoProfile -ExecutionPolicy Bypass -File .\scripts\smoke_test.ps1 -BuildDir .\build\overlay_labels_validation -AllowMissingGame` - - `powershell -NoProfile -ExecutionPolicy Bypass -File .\scripts\eval_smoke.ps1 -SmokeDir .\.agent\logs\smoke_latest -AllowMissingGame` - - Result: passed. Launch was skipped by design. -- K1 smoke/eval: - - `powershell -NoProfile -ExecutionPolicy Bypass -File .\scripts\smoke_test.ps1 -BuildDir .\build\overlay_labels_validation -GameDir 'D:\SteamLibrary\steamapps\common\swkotor' -Game k1` - - `powershell -NoProfile -ExecutionPolicy Bypass -File .\scripts\eval_smoke.ps1 -SmokeDir .\.agent\logs\smoke_latest` - - Result: passed. Smoke artifacts were written to `.agent\logs\smoke_20260419_130346`. -- K2 smoke/eval: - - `powershell -NoProfile -ExecutionPolicy Bypass -File .\scripts\smoke_test.ps1 -BuildDir .\build\overlay_labels_validation -GameDir 'D:\SteamLibrary\steamapps\common\Knights of the Old Republic II' -Game k2` - - `powershell -NoProfile -ExecutionPolicy Bypass -File .\scripts\eval_smoke.ps1 -SmokeDir .\.agent\logs\smoke_latest` - - Result: passed. Smoke artifacts were written to `.agent\logs\smoke_20260419_130425`. -- Artifact check: - - `D:\Git\reone-modawan-main\build\overlay_labels_validation\bin\engine.exe` exists. - - `D:\Git\reone-modawan-main\build\overlay_labels_validation\bin\shaderpack.erf` exists. -- `git diff --check` - - Result: no whitespace errors; only CRLF normalization warnings. -- Default no-game smoke follow-up: - - `powershell -NoProfile -ExecutionPolicy Bypass -File .\scripts\smoke_test.ps1 -AllowMissingGame` - - `powershell -NoProfile -ExecutionPolicy Bypass -File .\scripts\eval_smoke.ps1 -SmokeDir .\.agent\logs\smoke_latest -AllowMissingGame` - - Result: passed. Launch was skipped by design. - -Human verification needed: - -- Automated smoke validates startup, but it does not press overlay hotkeys or move through trigger volumes. -- Human in-game verification should launch with developer mode enabled and confirm `Ctrl+Shift+A` shows/hides entity labels, and trigger zones change to `tested`, `inside`, and `enter` states when crossed. - -## 2026-04-19: K1 Early Carth Cutscene / Trigger Delayed-Action Parity Slice - -Bug description: - -- In K1's early Endar Spire handoff path, the Carth-related message/cutscene progression after the first-room/Trask-door band can fail to appear. -- The old branch identified this as a shared engine delayed-action issue rather than a Carth content hack: `k_pend_trig02` runs with the trigger as caller and schedules a `DelayCommand`; that delayed action is owned by the trigger object. -- Before the old fix, trigger objects ran tenant maintenance but skipped base `Object::update(dt)`, so trigger-owned delayed actions did not mature or execute. - -Donor fix source: - -- `D:\reone-master\docs\documentation.md`, lines 2440-2506: old diagnosis and implementation summary. -- `D:\reone-master\docs\plans.md`, lines 1290-1332: tiny milestone describing the selected fix. -- `D:\reone-master\src\libs\game\object\trigger.cpp`, lines 163-165: `Trigger::update` calls `Object::update(dt)` before trigger tenant maintenance. - -Minimal port: - -- Ported only the causal runtime behavior: `Trigger::update(float dt)` now calls `Object::update(dt)` before developer debug timers and tenant exit maintenance. -- This lets trigger-owned action queues, delayed actions, and effects process through the existing modawan base object update path. - -What was intentionally not ported: - -- Donor scoped `DelayCommand` scheduling logs in script routines. -- Donor `DoCommandAction` execution/result logs. -- Donor live-log eval gates and old K1 combat-entry legal-data tests. -- Donor first-Sith combat, hostility, reciprocal-hostility, boarding-party persistence, encounter band-aids, target inspector/event feed, launcher changes, or content hacks. - -Why those were excluded: - -- The minimal causal fix is the base `Object::update(dt)` call on trigger objects. -- The donor logging/eval changes were useful during old investigation but are not required to restore the delayed action behavior in this bounded parity slice. -- The excluded combat/hostility and encounter changes belong to older compensating runtime work and are outside this single-bug branch. - -Validation: - -- Build: - - `powershell -NoProfile -ExecutionPolicy Bypass -File .\scripts\build.ps1 -Config RelWithDebInfo -Target engine -Configure` - - Result: passed. `D:\git\reone-modawan-main\build\bin\engine.exe` was produced. -- Smoke test without game install: - - `powershell -NoProfile -ExecutionPolicy Bypass -File .\scripts\smoke_test.ps1 -AllowMissingGame` - - `powershell -NoProfile -ExecutionPolicy Bypass -File .\scripts\eval_smoke.ps1 -SmokeDir .\.agent\logs\smoke_latest -AllowMissingGame` - - Result: passed. Launch was skipped by design. -- K1 smoke/eval: - - `powershell -NoProfile -ExecutionPolicy Bypass -File .\scripts\smoke_test.ps1 -GameDir 'D:\SteamLibrary\steamapps\common\swkotor' -Game k1` - - `powershell -NoProfile -ExecutionPolicy Bypass -File .\scripts\eval_smoke.ps1 -SmokeDir .\.agent\logs\smoke_latest` - - Result: passed. Smoke artifacts were written to `.agent\logs\smoke_20260419_143200`. -- K2 smoke/eval: - - `powershell -NoProfile -ExecutionPolicy Bypass -File .\scripts\smoke_test.ps1 -GameDir 'D:\SteamLibrary\steamapps\common\Knights of the Old Republic II' -Game k2` - - `powershell -NoProfile -ExecutionPolicy Bypass -File .\scripts\eval_smoke.ps1 -SmokeDir .\.agent\logs\smoke_latest` - - Result: passed. Smoke artifacts were written to `.agent\logs\smoke_20260419_143241`. -- Artifact check: - - `D:\git\reone-modawan-main\build\bin\engine.exe` exists. - - `D:\git\reone-modawan-main\build\bin\shaderpack.erf` exists. -- `git diff --check` - - Result: no whitespace errors; only CRLF normalization warnings. - -Human verification needed: - -- Automated smoke verifies startup only; it does not drive the Endar Spire sequence. -- Launch K1 with the rebuilt engine, play through the early Endar Spire first-room/Trask-door/Carth handoff band without console or forced combat, and confirm the previously missing Carth-related message/cutscene progression occurs. - -## 2026-04-19: K1 Early Trask Auto-Dialogue Dispatch Slice - -Bug description: - -- In K1's early Endar Spire Trask sequence, the scripted follow-up conversations can fail to auto-play even though manual conversation with Trask reaches the expected dialogue state. -- The visible symptoms are the missing unlock-door guidance after the party-management screen closes and the missing "you better take the lead" follow-up after Trask opens the door. -- This points at scripted conversation dispatch, not dialogue-state progression, combat, hostility, boarding-party, or encounter behavior. - -Donor source checked: - -- `D:\reone-master\docs\documentation.md`, lines 2413-2506: old `END_TRASK_DLG` consumer diagnosis and the trigger-owned delayed-action fix. -- `D:\reone-master\docs\plans.md`, lines 1290-1360: the tiny K1 handoff parity milestone for trigger-owned delayed actions. -- `D:\reone-master\src\libs\game\object\trigger.cpp`, lines 163-165: donor `Trigger::update(float dt)` calls `Object::update(dt)`. -- `D:\reone-master\src\libs\game\action\startconversation.cpp`, lines 30-43: donor scripted conversation action dispatches through `Area::startDialog`. -- `D:\reone-master\src\libs\game\object\area.cpp`, lines 960-968: donor still has the same empty-resref fallback guard defect, so there was no additional donor code patch to copy for this auto-dialogue-specific failure. - -Minimal fix: - -- Kept the already-integrated trigger delayed-action fix as-is. -- Fixed the shared conversation dispatcher in `src\libs\game\object\area.cpp` so `Area::startDialog` validates the resolved dialogue resref after falling back to `object->conversation()`. -- This allows scripted `ActionStartConversation` calls with an empty dialogue resref to use the target object's default conversation, matching the manual-talk path. -- A short-lived party-selection caller follow-up was tested during diagnosis, but later live tracing showed it was not the causal fix. -- The final causal follow-up is documented in the action continuation section below: assigned `DoCommandAction` already supplies the assigned actor as `OBJECT_SELF`, and `ClearAllActions()` needed to preserve the active assigned action continuation. - -What was intentionally not ported: - -- Donor party-selection forced-companion integrity work. -- Donor party roster/HUD logging and party leader observability. -- Donor delayed-command logging/eval instrumentation beyond what is already in this branch. -- Donor combat legality, reciprocal hostility, boarding-party persistence, encounter band-aids, first-Sith hostility work, Carth content changes, launcher changes, or modernization. - -Why those were excluded: - -- The missing auto-dialogue shape is explained by shared engine dispatch issues: the conversation fallback returned before `Game::startDialog` could run, and the assigned action continuation could be erased by `ClearAllActions()`. -- The trigger delayed-action donor fix was already integrated and validated in the previous slice. -- The old party-selection donor work addresses roster corruption around the Trask join flow, not this empty-dialog-resref dispatch failure. -- No K1 content hack or gameplay/runtime combat change is needed for this slice. - -Validation: - -- Build: - - `powershell -NoProfile -ExecutionPolicy Bypass -File .\scripts\build.ps1 -Config RelWithDebInfo -Target engine -Configure` - - Result: passed. `D:\git\reone-modawan-main\build\bin\engine.exe` was produced. -- Smoke test without game install: - - `powershell -NoProfile -ExecutionPolicy Bypass -File .\scripts\smoke_test.ps1 -AllowMissingGame` - - `powershell -NoProfile -ExecutionPolicy Bypass -File .\scripts\eval_smoke.ps1 -SmokeDir .\.agent\logs\smoke_latest -AllowMissingGame` - - Result: passed. Launch was skipped by design. -- K1 smoke/eval: - - `powershell -NoProfile -ExecutionPolicy Bypass -File .\scripts\smoke_test.ps1 -GameDir 'D:\SteamLibrary\steamapps\common\swkotor' -Game k1` - - `powershell -NoProfile -ExecutionPolicy Bypass -File .\scripts\eval_smoke.ps1 -SmokeDir .\.agent\logs\smoke_latest` - - Result: passed after the party-selection caller follow-up. Smoke artifacts were written to `.agent\logs\smoke_20260419_154359`. -- K2 smoke/eval: - - `powershell -NoProfile -ExecutionPolicy Bypass -File .\scripts\smoke_test.ps1 -GameDir 'D:\SteamLibrary\steamapps\common\Knights of the Old Republic II' -Game k2` - - `powershell -NoProfile -ExecutionPolicy Bypass -File .\scripts\eval_smoke.ps1 -SmokeDir .\.agent\logs\smoke_latest` - - Result: passed after the party-selection caller follow-up. Smoke artifacts were written to `.agent\logs\smoke_20260419_154438`. -- Artifact check: - - `D:\git\reone-modawan-main\build\bin\engine.exe` exists. - - `D:\git\reone-modawan-main\build\bin\shaderpack.erf` exists. -- `git diff --check` - - Result: no whitespace errors; only CRLF normalization warnings. - -Human verification needed: - -- Automated smoke verifies startup only; it does not drive the Endar Spire sequence. -- Launch K1 with the rebuilt engine, reach the Trask join point, close party management, and confirm the unlock-door guidance auto-plays without manually talking to Trask. -- Let Trask open the first door without manually forcing the dialogue first, then confirm the "you better take the lead" follow-up auto-plays. -- Do not use console commands, forced combat, faction edits, or unusual routing for the parity check. - -Live verification follow-up: - -- Human testing confirmed the Carth-side trigger delayed-action fix works, but the first Trask auto-dialogue still did not play immediately after Trask joins the party. -- A temporary party-selection caller patch and trace logging proved the party screen closed, `k_pend_reset` ran, Trask was resolved, and `AssignCommand` queued the follow-up work. -- The trace also proved the real remaining failure was lower-level: the assigned action chain died immediately after `ClearAllActions()`. -- No donor combat, hostility, boarding-party, encounter, first-door content, party roster, or modernization code was ported. - -## 2026-04-19: K1 Trask Auto-Dialogue Action Continuation Fix - -Traced failure point: - -- Live trace proved `ShowPartySelectionGUI("k_pend_reset", 0, -1)` runs and the party-selection screen closes through `BTN_DONE`. -- `k_pend_reset` runs, resolves the new party Trask through `GetPartyMemberByIndex(1)`, and queues two `AssignCommand` actions on Trask. -- The first assigned action starts on Trask and runs `ClearAllActions()`. -- After that clear, the follow-up assigned action that should reach `ActionStartConversation()` does not execute. - -Actual engine cause: - -- `Object::clearAllActions()` cleared the full action queue even when called from an action currently executing on that same object. -- For this sequence, the first queued `DoCommandAction` resumed at `ClearAllActions()`. The queue still contained that active action and the next queued `DoCommandAction` carrying the follow-up conversation. -- Clearing the whole queue erased the current action frame and the queued continuation behind it, so the action chain died before the saved `ActionStartConversation()` state could run. - -Minimal fix: - -- `Object::executeActions()` now records the currently executing action while it calls `Action::execute()`. -- `Object::clearAllActions(false)` now preserves the currently executing action and the continuation actions behind it when the clear is invoked from that active action frame. -- Forced clears still clear the queue, and clears outside active action execution keep the existing queue-clearing behavior. -- This is a shared action semantics fix; it has no Trask-specific, Endar-Spire-specific, dialogue-content-specific, combat, hostility, encounter, boarding-party, item, or modernization branch. - -Cleanup status: - -- Human testing confirmed the preserved continuation now reaches the scripted follow-up dialogue. -- The temporary `reone trask autodialog trace:` instrumentation was removed after validation. -- The earlier party-selection caller patch was removed because `DoCommandAction` already updates the resumed saved script context caller to the assigned actor; the causal fix is the action continuation-safe clear. - -What was intentionally not changed: - -- No K1 content scripts, dialogue files, module data, Trask-specific special cases, combat legality, reciprocal hostility, boarding-party hostility persistence, encounter sequencing, item behavior, PR #77 work, Dear ImGui work, or modernization were ported or added. -- The earlier party-selection caller patch is not retained; the latest trace showed the real blocker was the action queue clear inside the assigned command chain. - -Validation: - -- Build: - - `powershell -NoProfile -ExecutionPolicy Bypass -File .\scripts\build.ps1 -Config RelWithDebInfo -Target engine -Configure` - - Result: passed. `D:\git\reone-modawan-main\build\bin\engine.exe` was rebuilt. -- Smoke test without game install: - - `powershell -NoProfile -ExecutionPolicy Bypass -File .\scripts\smoke_test.ps1 -AllowMissingGame` - - `powershell -NoProfile -ExecutionPolicy Bypass -File .\scripts\eval_smoke.ps1 -SmokeDir .\.agent\logs\smoke_latest -AllowMissingGame` - - Result: passed. Launch was skipped by design. -- K1 smoke/eval: - - `powershell -NoProfile -ExecutionPolicy Bypass -File .\scripts\smoke_test.ps1 -GameDir 'D:\SteamLibrary\steamapps\common\swkotor' -Game k1` - - `powershell -NoProfile -ExecutionPolicy Bypass -File .\scripts\eval_smoke.ps1 -SmokeDir .\.agent\logs\smoke_latest` - - Result: passed. Smoke artifacts were written to `.agent\logs\smoke_20260419_161924`. -- K2 smoke/eval: - - `powershell -NoProfile -ExecutionPolicy Bypass -File .\scripts\smoke_test.ps1 -GameDir 'D:\SteamLibrary\steamapps\common\Knights of the Old Republic II' -Game k2` - - `powershell -NoProfile -ExecutionPolicy Bypass -File .\scripts\eval_smoke.ps1 -SmokeDir .\.agent\logs\smoke_latest` - - Result: passed. Smoke artifacts were written to `.agent\logs\smoke_20260419_162011`. -- `git diff --check` - - Result: no whitespace errors; only CRLF normalization warnings. - -Human verification needed: - -- Automated smoke verifies startup only; it does not drive the Trask sequence. -- Launch K1 with the rebuilt engine, close party selection after Trask joins, and confirm the unlock-door dialogue auto-plays. -- Let Trask open the first door and confirm the follow-up dialogue auto-plays. - -## 2026-04-19: Stable Branch Cleanup and Developer Tools Launcher Polish - -Cleanup done: - -- Removed the temporary `reone trask autodialog trace:` instrumentation from script dispatch, party selection, the VM action hook, `StartConversationAction`, and `Area::startDialog`. -- Removed the temporary party-selection caller helper and restored party-selection exit scripts to the existing plain `ScriptRunner::run(_context.exitScript)` path. -- Kept the real engine fixes intact: trigger-owned delayed actions, empty dialogue-resref fallback, and continuation-safe `ClearAllActions()`. - -Party-selection patch decision: - -- The party-selection caller patch was removed. -- Live tracing showed it was not the causal Trask fix: the assigned `DoCommandAction` path updates the saved script context caller to the assigned actor before resuming the VM. -- With that behavior, `k_pend_reset` does not need the party-selection screen to provide a special caller for the assigned Trask continuation. - -Developer tools access: - -- The developer tools continue to use the existing launcher Developer Mode checkbox and `-Dev`/`--dev` flow. -- No parallel custom launcher path or new config flag was added. -- The launcher now labels the Developer Mode area with the current in-game tool hotkeys. - -Hotkeys: - -- `Ctrl+Shift+D`: toggle the developer overlay. -- `Ctrl+Shift+T`: toggle trigger zones and trigger state labels. -- `Ctrl+Shift+A`: toggle entity/world labels. -- `Ctrl+Shift+W`: toggle the watch panel. - -How to enable: - -- Check Developer Mode in `launcher.exe`, or launch with `scripts\run_k1.ps1 -Dev` / `scripts\run_k2.ps1 -Dev`. -- Enter an in-game module and use the hotkeys above. - -How to disable: - -- Launch without Developer Mode, or hide the overlay/tools with the same hotkeys. - -Validation: - -- `git diff --check` - - Result: no whitespace errors; only CRLF normalization warnings. -- Build: - - `powershell -NoProfile -ExecutionPolicy Bypass -File .\scripts\build.ps1 -Config RelWithDebInfo -Target engine -Configure` - - Result: passed. `D:\git\reone-modawan-main\build\bin\engine.exe` was rebuilt at 2026-04-19 17:22:14 local time. - - CMake still emits a non-blocking Boost CMP0167 developer warning through vcpkg. -- Launcher build: - - `powershell -NoProfile -ExecutionPolicy Bypass -File .\scripts\build.ps1 -Config RelWithDebInfo -Target launcher` - - Result: passed. `D:\git\reone-modawan-main\build\bin\launcher.exe` was rebuilt at 2026-04-19 17:22:45 local time. -- Smoke test without game install: - - `powershell -NoProfile -ExecutionPolicy Bypass -File .\scripts\smoke_test.ps1 -AllowMissingGame` - - `powershell -NoProfile -ExecutionPolicy Bypass -File .\scripts\eval_smoke.ps1 -SmokeDir .\.agent\logs\smoke_latest -AllowMissingGame` - - Result: passed. Launch was skipped by design. -- K1 smoke/eval: - - `powershell -NoProfile -ExecutionPolicy Bypass -File .\scripts\smoke_test.ps1 -GameDir 'D:\SteamLibrary\steamapps\common\swkotor' -Game k1` - - `powershell -NoProfile -ExecutionPolicy Bypass -File .\scripts\eval_smoke.ps1 -SmokeDir .\.agent\logs\smoke_latest` - - Result: passed. Smoke artifacts were written to `.agent\logs\smoke_20260419_172329`. -- K2 smoke/eval: - - `powershell -NoProfile -ExecutionPolicy Bypass -File .\scripts\smoke_test.ps1 -GameDir 'D:\SteamLibrary\steamapps\common\Knights of the Old Republic II' -Game k2` - - `powershell -NoProfile -ExecutionPolicy Bypass -File .\scripts\eval_smoke.ps1 -SmokeDir .\.agent\logs\smoke_latest` - - Result: passed. Smoke artifacts were written to `.agent\logs\smoke_20260419_172412`. -- Artifact check: - - `D:\git\reone-modawan-main\build\bin\engine.exe` exists. - - `D:\git\reone-modawan-main\build\bin\launcher.exe` exists. - - `D:\git\reone-modawan-main\build\bin\shaderpack.erf` exists. - -Human verification needed: - -- Automated smoke validates startup only; it does not press launcher checkboxes or in-game overlay hotkeys. -- Launch through `D:\git\reone-modawan-main\build\bin\launcher.exe`, confirm the Developer Mode area shows the hotkey help, check Developer Mode, and confirm `Ctrl+Shift+D`, `Ctrl+Shift+T`, `Ctrl+Shift+A`, and `Ctrl+Shift+W` still work in game. -- Because the party-selection caller patch was removed after the deeper action fix, re-check the K1 Trask join and first-door handoff once in game: the unlock-door and "take the lead" dialogues should still auto-play. diff --git a/docs/plans.md b/docs/plans.md deleted file mode 100644 index 38f751dd1..000000000 --- a/docs/plans.md +++ /dev/null @@ -1,210 +0,0 @@ -# Plans - -## Tiny Migration Milestone: Tooling-First Harness - -Goal: give the modawan baseline its own bounded build, launch, smoke, log-capture, and smoke-eval loop before any gameplay donor code is considered. - -Acceptance criteria: - -- `scripts/build.ps1`, `scripts/run_k1.ps1`, `scripts/run_k2.ps1`, `scripts/smoke_test.ps1`, `scripts/eval_smoke.ps1`, and `scripts/capture_logs.ps1` exist in the working repo. -- `evals/README.md` documents the smoke eval contract. -- Engine startup emits `reone smoke signal: engine startup` after logging initializes. -- Smoke eval can verify build success, `engine.exe`, `shaderpack.erf`, the startup signal when launch is attempted, and absence of obvious fatal output. -- K1 and K2 smoke runs are attempted only against legal local game directories or explicitly skipped with `-AllowMissingGame`. -- No combat, boarding-party hostility, first-door, party, save/load, item, or modernization behavior changes are included. - -Verification: - -- Build the engine target. -- Build or verify the shader pack via the smoke harness. -- Run generic smoke eval with `-AllowMissingGame` if legal game paths are unavailable. -- Run K1 and K2 smoke/eval if legal install paths are available. - -Non-goals: - -- Do not port donor combat or hostility patches. -- Do not port donor Endar Spire first-door fixes in this milestone. -- Do not port donor developer overlay rendering or hotkey panels in this milestone. -- Do not modernize build, runtime, input, UI, rendering, or gameplay systems. - -## Tiny Migration Milestone: Developer Overlay Observability - -Goal: add the smallest useful in-game developer overlay and hotkeys for runtime observability without changing gameplay behavior. - -Acceptance criteria: - -- Developer overlay is gated by existing developer mode. -- `Ctrl+Shift+D` toggles the overlay. -- `Ctrl+Shift+W` toggles the read-only watch panel. -- Watch panel reads only existing state: screen, module, area, camera, speed, pause, relative mouse, leader, selected object, and hovered object. -- Overlay is off by default and can be disabled by hiding it or launching without developer mode. -- No donor trigger debug state, actor labels, target inspector, event feed, gameplay instrumentation, combat, hostility, boarding-party, first-door, item, party, cutscene, or modernization changes are included. - -Verification: - -- Build the engine target. -- Run generic smoke/eval with `-AllowMissingGame`. -- Run K1 smoke/eval. -- Run K2 smoke/eval. -- Human in-game verification should confirm `Ctrl+Shift+D` and `Ctrl+Shift+W` render and hide the overlay while developer mode is enabled. - -Non-goals: - -- Do not port donor trigger overlays in this milestone. -- Do not port donor actor labels or target inspector in this milestone. -- Do not port donor developer event feed or instrumentation in this milestone. -- Do not port donor gameplay behavior or modernize runtime systems. - -## Tiny Migration Milestone: Trigger Zone Overlay - -Goal: restore developer-mode trigger zone visualization as the next smallest high-leverage observability feature. - -Acceptance criteria: - -- Trigger visualization is gated by existing developer mode. -- `Ctrl+Shift+T` toggles trigger zone visualization. -- Trigger polygons are rendered as screen-space outlines inside the existing developer overlay. -- Trigger labels show read-only id, tag, and `OnEnter` script when available. -- The implementation reads existing trigger geometry only and does not add trigger occupancy, debug-state timers, script instrumentation, or gameplay behavior changes. -- The feature can be disabled by launching without developer mode, hiding the overlay with `Ctrl+Shift+D`, or hiding triggers with `Ctrl+Shift+T`. - -Verification: - -- Build the engine target. -- Run generic smoke/eval with `-AllowMissingGame`. -- Run K1 smoke/eval. -- Run K2 smoke/eval. -- Human in-game verification should confirm `Ctrl+Shift+T` shows and hides trigger outlines and labels while developer mode is enabled. - -Non-goals: - -- Do not port donor trigger debug state or occupancy instrumentation in this milestone. -- Do not port donor entity/world labels in this milestone. -- Do not port launcher developer-option UI changes in this milestone. -- Do not port gameplay, combat, hostility, boarding-party, encounter, first-door, item, party, cutscene, or modernization changes. - -## Tiny Migration Milestone: Overlay Debug Labels And Trigger State - -Goal: close the manually observed overlay gaps by restoring trigger state coloring and lightweight entity labels without changing gameplay behavior. - -Acceptance criteria: - -- Developer overlay remains gated by existing developer mode. -- `Ctrl+Shift+T` trigger zones show default/tested/inside/enter state in color and label text. -- `Ctrl+Shift+A` toggles lightweight entity labels over nearby creatures, doors, and placeables. -- Labels are read-only and limited to existing object state: id, tag, template, faction, hostile-to-player flag for creatures, selectable, commandable, visible, and plot. -- No donor target inspector, event feed, first-door diagnostics, combat, hostility, boarding-party, encounter, item, party, cutscene, launcher redesign, or modernization changes are included. - -Verification: - -- Build the engine target. -- Run generic smoke/eval with `-AllowMissingGame`. -- Run K1 smoke/eval. -- Run K2 smoke/eval. -- Human in-game verification should confirm `Ctrl+Shift+A` shows and hides entity labels, and trigger zones change from default/tested to inside/enter when crossed. - -Non-goals: - -- Do not port donor target inspector or event feed in this milestone. -- Do not port donor launcher developer-option controls in this milestone. -- Do not port donor gameplay behavior or modernize runtime systems. - -## Tiny Migration Milestone: K1 Trigger-Owned Delayed Actions - -Goal: restore the old K1 early Carth/Trask handoff parity fix by allowing delayed actions queued on trigger objects to execute. - -Acceptance criteria: - -- `Trigger::update(float dt)` runs base `Object::update(dt)` before trigger tenant maintenance. -- The change is limited to trigger-owned base action/effect processing and does not alter trigger containment geometry, script dispatch arguments, combat legality, hostility, boarding-party persistence, encounter sequencing, or Carth content. -- Donor logging/eval instrumentation is not ported unless needed to prove a blocker. -- K1 and K2 smoke/eval pass after the change. - -Verification: - -- Build the engine target. -- Run generic smoke/eval with `-AllowMissingGame`. -- Run K1 smoke/eval. -- Run K2 smoke/eval. -- Human K1 verification should reach the early Endar Spire first-room/Trask-door/Carth handoff band and confirm the previously missing Carth-related cutscene/message progression occurs. - -Non-goals: - -- Do not force any first-Sith actor hostile. -- Do not port donor combat, reciprocal hostility, boarding-party, encounter, journal, Carth content, launcher, or modernization changes. -- Do not add broad diagnostics unless this minimal port fails validation or cannot be isolated. - -## Tiny Migration Milestone: K1 Trask Auto-Dialogue Dispatch - -Goal: restore the early Endar Spire scripted Trask follow-up conversations by fixing shared conversation fallback and assigned-action continuation semantics. - -Acceptance criteria: - -- `ActionStartConversation` calls with an empty dialogue resref can fall back to the target object's default conversation. -- `Object::clearAllActions(false)` preserves the active assigned action frame and queued continuation behind it when invoked from that active frame. -- The changes are limited to shared script/action semantics and do not alter Trask content, trigger geometry, combat legality, hostility, boarding-party persistence, encounter sequencing, item behavior, or cutscene logic. -- The previously integrated trigger-owned delayed-action fix remains unchanged. -- K1 and K2 smoke/eval pass after the change. - -Verification: - -- Build the engine target. -- Run generic smoke/eval with `-AllowMissingGame`. -- Run K1 smoke/eval. -- Run K2 smoke/eval. -- Human K1 verification should reach the Trask join and first-door handoff band and confirm both scripted follow-up dialogues auto-play without manual Trask conversation. - -Non-goals: - -- Do not port donor party-selection forced-companion work in this milestone. -- Do not port donor combat, reciprocal hostility, boarding-party, encounter, journal, Carth content, launcher, or modernization changes. -- Do not add broad diagnostics unless this minimal dispatch fix fails validation or cannot be isolated. - -## Tiny Migration Milestone: Action Continuation-Safe ClearAllActions - -Goal: preserve active assigned action continuations when a queued script action calls `ClearAllActions()`. - -Acceptance criteria: - -- `Object::clearAllActions(false)` no longer erases the currently executing action frame or continuation actions behind it when invoked from that active action. -- Forced clears and clears outside active action execution retain existing behavior. -- The change is generic action queue semantics, not Trask-specific content logic. -- K1 and K2 smoke/eval pass after the change. - -Verification: - -- Build the engine target. -- Run generic smoke/eval with `-AllowMissingGame`. -- Run K1 smoke/eval. -- Run K2 smoke/eval. -- Human K1 verification should close party selection after Trask joins and confirm the unlock-door dialogue auto-plays without temporary trace logging. - -Non-goals: - -- Do not port donor party-selection forced-companion work. -- Do not port combat, hostility, boarding-party, encounter, journal, content, launcher, or modernization changes. -- Do not turn temporary Trask trace logging into a permanent broad logging system. - -## Tiny Migration Milestone: Stable Dev Tools Cleanup - -Goal: remove temporary Trask tracing and keep developer tools on the existing launcher Developer Mode path. - -Acceptance criteria: - -- Temporary `reone trask autodialog trace:` log points are removed after live validation. -- The temporary party-selection caller helper is removed because the assigned action continuation fix is causal and `DoCommandAction` supplies the assigned actor as caller. -- The launcher Developer Mode area documents the current dev-tool hotkeys without adding a parallel config path. -- Trigger, dialogue fallback, and action continuation fixes remain intact. -- K1 and K2 smoke/eval pass after the cleanup. - -Verification: - -- Build engine and launcher targets. -- Run generic smoke/eval with `-AllowMissingGame`. -- Run K1 smoke/eval. -- Run K2 smoke/eval. -- Human verification should launch through `launcher.exe`, check Developer Mode, confirm the hotkey help is visible, and confirm `Ctrl+Shift+D/T/A/W` still work in game. - -Non-goals: - -- Do not add gameplay fixes, donor gameplay behavior, combat/hostility/boarding-party changes, broad launcher redesign, Dear ImGui work, or modernization. diff --git a/evals/README.md b/evals/README.md deleted file mode 100644 index 866935395..000000000 --- a/evals/README.md +++ /dev/null @@ -1,48 +0,0 @@ -# Smoke Evals - -This first migration slice ports only the generic smoke harness. Focused donor evals for interaction, party selection, vitality, combat entry, and K1 interactables are intentionally not included yet. - -## Run - -Without a legal game install, verify build/executable discovery and record that launch was skipped: - -```powershell -powershell -NoProfile -ExecutionPolicy Bypass -File .\scripts\smoke_test.ps1 -AllowMissingGame -powershell -NoProfile -ExecutionPolicy Bypass -File .\scripts\eval_smoke.ps1 -SmokeDir .\.agent\logs\smoke_latest -AllowMissingGame -``` - -With a legal KOTOR 1 install: - -```powershell -powershell -NoProfile -ExecutionPolicy Bypass -File .\scripts\smoke_test.ps1 -GameDir "C:\Path\To\KotOR" -Game k1 -powershell -NoProfile -ExecutionPolicy Bypass -File .\scripts\eval_smoke.ps1 -SmokeDir .\.agent\logs\smoke_latest -``` - -With a legal KOTOR 2 install: - -```powershell -powershell -NoProfile -ExecutionPolicy Bypass -File .\scripts\smoke_test.ps1 -GameDir "C:\Path\To\KotOR2" -Game k2 -powershell -NoProfile -ExecutionPolicy Bypass -File .\scripts\eval_smoke.ps1 -SmokeDir .\.agent\logs\smoke_latest -``` - -## Contract - -`smoke_test.ps1` writes: - -- `stdout.log` -- `stderr.log` -- `engine.log` -- `smoke_manifest.json` - -Artifacts are written to `.agent\logs\smoke_` and copied to `.agent\logs\smoke_latest`. - -`eval_smoke.ps1` checks: - -- Build success or executable discovery. -- Built `engine.exe` discovery. -- Built `shaderpack.erf` discovery. -- Engine process launch when a legal game directory is available. -- The deterministic startup signal `reone smoke signal: engine startup` when launch is attempted. -- Absence of obvious fatal patterns such as `Engine failure`, `FATAL`, `Unhandled exception`, access violations, CMake errors, and failed build markers. - -The eval returns a non-zero exit code for deterministic failure. diff --git a/scripts/build.ps1 b/scripts/build.ps1 deleted file mode 100644 index c114cd839..000000000 --- a/scripts/build.ps1 +++ /dev/null @@ -1,201 +0,0 @@ -#requires -Version 5.1 -[CmdletBinding()] -param( - [string]$BuildDir, - - [ValidateSet("Debug", "Release", "RelWithDebInfo", "MinSizeRel")] - [string]$Config = "RelWithDebInfo", - - [string]$Target = "engine", - - [switch]$Configure, - - [string[]]$CMakeArgs = @() -) - -$ErrorActionPreference = "Stop" - -$RepoRoot = (Resolve-Path (Join-Path $PSScriptRoot "..")).Path -if (-not $BuildDir) { - $BuildDir = Join-Path $RepoRoot "build" -} -$BuildDir = $ExecutionContext.SessionState.Path.GetUnresolvedProviderPathFromPSPath($BuildDir) -$LogsDir = Join-Path $RepoRoot ".agent\logs" -New-Item -ItemType Directory -Force -Path $BuildDir | Out-Null -New-Item -ItemType Directory -Force -Path $LogsDir | Out-Null - -function Resolve-CMakeExe { - $cmd = Get-Command cmake -ErrorAction SilentlyContinue - if ($cmd) { - return $cmd.Source - } - - if ($env:CMAKE_EXE -and (Test-Path $env:CMAKE_EXE)) { - return $env:CMAKE_EXE - } - - $defaultPath = "C:\Program Files\CMake\bin\cmake.exe" - if (Test-Path $defaultPath) { - return $defaultPath - } - - throw "cmake.exe was not found. Add CMake to PATH or set CMAKE_EXE." -} - -function Resolve-VcpkgToolchain { - if ($env:VCPKG_ROOT) { - $fromEnv = Join-Path $env:VCPKG_ROOT "scripts\buildsystems\vcpkg.cmake" - if (Test-Path $fromEnv) { - return $fromEnv - } - } - - $defaultPath = "D:\vcpkg\scripts\buildsystems\vcpkg.cmake" - if (Test-Path $defaultPath) { - return $defaultPath - } - - return $null -} - -function Resolve-VcpkgRootFromToolchain { - param([string]$Toolchain) - - if (-not $Toolchain) { - return $null - } - - $toolchainPath = $ExecutionContext.SessionState.Path.GetUnresolvedProviderPathFromPSPath($Toolchain) - $buildsystemsDir = Split-Path -Parent $toolchainPath - $scriptsDir = Split-Path -Parent $buildsystemsDir - - if ((Split-Path -Leaf $buildsystemsDir) -ne "buildsystems" -or - (Split-Path -Leaf $scriptsDir) -ne "scripts") { - return $null - } - - return (Split-Path -Parent $scriptsDir) -} - -function Resolve-VcpkgTargetTriplet { - param([string[]]$Arguments) - - foreach ($argument in $Arguments) { - if ($argument -like "-DVCPKG_TARGET_TRIPLET=*") { - return $argument.Substring("-DVCPKG_TARGET_TRIPLET=".Length) - } - } - - if ($env:VCPKG_DEFAULT_TRIPLET) { - return $env:VCPKG_DEFAULT_TRIPLET - } - - return "x64-windows" -} - -function Write-VcpkgPackageConfigHint { - param( - [string]$Toolchain, - [string[]]$Arguments - ) - - $vcpkgRoot = Resolve-VcpkgRootFromToolchain -Toolchain $Toolchain - if (-not $vcpkgRoot) { - return - } - - if (Test-Path (Join-Path $RepoRoot "vcpkg.json")) { - return - } - - $triplet = Resolve-VcpkgTargetTriplet -Arguments $Arguments - $sdl3ConfigDir = Join-Path $vcpkgRoot "installed\$triplet\share\sdl3" - if (Test-Path $sdl3ConfigDir) { - return - } - - $vcpkgExe = Join-Path $vcpkgRoot "vcpkg.exe" - $installCommand = "& `"$vcpkgExe`" install sdl3:${triplet}" - Write-Warning "SDL3 vcpkg config was not found at $sdl3ConfigDir. CMake requires find_package(SDL3 CONFIG REQUIRED). Install it with '$installCommand' or set VCPKG_ROOT/CMAKE_TOOLCHAIN_FILE to a vcpkg tree that has sdl3:${triplet}." -} - -function Invoke-CMakeLogged { - param( - [string[]]$Arguments, - [string]$LogPath - ) - - Write-Host "Using CMake: $script:CMakeExe" - Write-Host "cmake $($Arguments -join ' ')" - $previousErrorActionPreference = $ErrorActionPreference - $ErrorActionPreference = "Continue" - try { - & $script:CMakeExe @Arguments 2>&1 | Tee-Object -FilePath $LogPath - $exitCode = $LASTEXITCODE - } finally { - $ErrorActionPreference = $previousErrorActionPreference - } - if ($exitCode -ne 0) { - throw "CMake failed with exit code $exitCode. See $LogPath" - } -} - -function Test-CMakeProjectReady { - if (-not (Test-Path $cachePath)) { - return $false - } - - $generatedProject = Get-ChildItem -Path $BuildDir -File -ErrorAction SilentlyContinue | - Where-Object { $_.Name -eq "build.ninja" -or $_.Name -eq "Makefile" -or $_.Extension -eq ".sln" } | - Select-Object -First 1 - - return $null -ne $generatedProject -} - -$script:CMakeExe = Resolve-CMakeExe -$timestamp = Get-Date -Format "yyyyMMdd_HHmmss" -$buildLog = Join-Path $LogsDir "build_$timestamp.log" -$configureLog = Join-Path $LogsDir "configure_$timestamp.log" -$latestBuildLog = Join-Path $LogsDir "latest_build.log" -$latestConfigureLog = Join-Path $LogsDir "latest_configure.log" -$cachePath = Join-Path $BuildDir "CMakeCache.txt" - -try { - if ($Configure -or -not (Test-CMakeProjectReady)) { - $configureArgs = @("-S", $RepoRoot, "-B", $BuildDir, "-DCMAKE_BUILD_TYPE=$Config") - $vcpkgToolchain = Resolve-VcpkgToolchain - if ($vcpkgToolchain) { - $configureArgs += "-DCMAKE_TOOLCHAIN_FILE=$vcpkgToolchain" - } - if ($CMakeArgs.Count -gt 0) { - $configureArgs += $CMakeArgs - } - - Write-VcpkgPackageConfigHint -Toolchain $vcpkgToolchain -Arguments $configureArgs - Invoke-CMakeLogged -Arguments $configureArgs -LogPath $configureLog - Copy-Item -Force -Path $configureLog -Destination $latestConfigureLog - } - - $buildArgs = @("--build", $BuildDir, "--config", $Config) - if ($Target) { - $buildArgs += @("--target", $Target) - } - $buildArgs += "--parallel" - - Invoke-CMakeLogged -Arguments $buildArgs -LogPath $buildLog - Copy-Item -Force -Path $buildLog -Destination $latestBuildLog - - Write-Host "Build succeeded for target '$Target' ($Config)." - exit 0 -} catch { - if (Test-Path $buildLog) { - Copy-Item -Force -Path $buildLog -Destination $latestBuildLog - } elseif (Test-Path $configureLog) { - Copy-Item -Force -Path $configureLog -Destination $latestBuildLog - } - if (Test-Path $configureLog) { - Copy-Item -Force -Path $configureLog -Destination $latestConfigureLog - } - Write-Error $_ - exit 1 -} diff --git a/scripts/capture_logs.ps1 b/scripts/capture_logs.ps1 deleted file mode 100644 index 2504e5a9d..000000000 --- a/scripts/capture_logs.ps1 +++ /dev/null @@ -1,44 +0,0 @@ -#requires -Version 5.1 -[CmdletBinding()] -param( - [string]$Destination, - - [string]$SmokeDir -) - -$ErrorActionPreference = "Stop" - -$RepoRoot = (Resolve-Path (Join-Path $PSScriptRoot "..")).Path -$LogsRoot = Join-Path $RepoRoot ".agent\logs" - -if (-not $Destination) { - $timestamp = Get-Date -Format "yyyyMMdd_HHmmss" - $Destination = Join-Path $LogsRoot "capture_$timestamp" -} - -$Destination = $ExecutionContext.SessionState.Path.GetUnresolvedProviderPathFromPSPath($Destination) -New-Item -ItemType Directory -Force -Path $Destination | Out-Null - -$paths = @( - (Join-Path $RepoRoot "engine.log"), - (Join-Path $RepoRoot "build\bin\engine.log"), - (Join-Path $RepoRoot "build\debug\bin\engine.log"), - (Join-Path $LogsRoot "latest_build.log"), - (Join-Path $LogsRoot "latest_configure.log") -) - -if (-not $SmokeDir) { - $SmokeDir = Join-Path $LogsRoot "smoke_latest" -} - -if (Test-Path $SmokeDir) { - $paths += $SmokeDir -} - -foreach ($path in $paths) { - if (Test-Path $path) { - Copy-Item -Recurse -Force -Path $path -Destination $Destination - } -} - -Write-Host "Captured available logs to $Destination" diff --git a/scripts/eval_smoke.ps1 b/scripts/eval_smoke.ps1 deleted file mode 100644 index 0771ce83f..000000000 --- a/scripts/eval_smoke.ps1 +++ /dev/null @@ -1,122 +0,0 @@ -#requires -Version 5.1 -[CmdletBinding()] -param( - [string]$SmokeDir, - - [switch]$AllowMissingGame -) - -$ErrorActionPreference = "Stop" - -$RepoRoot = (Resolve-Path (Join-Path $PSScriptRoot "..")).Path -$LogsRoot = Join-Path $RepoRoot ".agent\logs" - -if (-not $SmokeDir) { - $SmokeDir = Join-Path $LogsRoot "smoke_latest" -} - -$SmokeDir = $ExecutionContext.SessionState.Path.GetUnresolvedProviderPathFromPSPath($SmokeDir) -$manifestPath = Join-Path $SmokeDir "smoke_manifest.json" - -if (-not (Test-Path $manifestPath)) { - Write-Error "Smoke manifest not found: $manifestPath" - exit 1 -} - -$manifest = Get-Content -Raw -Path $manifestPath | ConvertFrom-Json -$errors = New-Object System.Collections.Generic.List[string] -$warnings = New-Object System.Collections.Generic.List[string] - -if (-not $manifest.build.success) { - $errors.Add("Build did not succeed according to smoke manifest.") -} - -if (-not $manifest.build.engineExe -or -not (Test-Path $manifest.build.engineExe)) { - $errors.Add("Built engine executable was not found: $($manifest.build.engineExe)") -} - -if (-not $manifest.build.shaderpackErf -or -not (Test-Path $manifest.build.shaderpackErf)) { - $errors.Add("Built shaderpack.erf was not found: $($manifest.build.shaderpackErf)") -} - -if (-not $manifest.launch.attempted) { - if ($AllowMissingGame -and $manifest.launch.skipReason) { - $warnings.Add("Launch was skipped: $($manifest.launch.skipReason)") - } else { - $errors.Add("Engine process launch was not attempted.") - } -} else { - if (-not $manifest.launch.started) { - $errors.Add("Engine process launch was attempted but did not start.") - } - - if (-not $manifest.launch.timedOut -and $manifest.launch.exitCode -ne $null -and [int]$manifest.launch.exitCode -ne 0) { - $errors.Add("Engine exited with non-zero code $($manifest.launch.exitCode).") - } - - if (-not $manifest.launch.shaderpackErf -or -not (Test-Path $manifest.launch.shaderpackErf)) { - $errors.Add("Smoke launch shaderpack.erf was not found: $($manifest.launch.shaderpackErf)") - } -} - -$logPaths = @($manifest.build.log, $manifest.launch.stdout, $manifest.launch.stderr, $manifest.launch.engineLog) -$fatalPatterns = @( - "Engine failure", - "FATAL", - "Unhandled exception", - "Access violation", - "Segmentation fault", - "CMake Error", - "Build FAILED", - "fatal error" -) -$startupSignal = "reone smoke signal: engine startup" -if ($manifest.evalHints -and $manifest.evalHints.startupSignal) { - $startupSignal = $manifest.evalHints.startupSignal -} - -foreach ($path in $logPaths) { - if ($path -and (Test-Path $path)) { - $matches = Select-String -Path $path -Pattern $fatalPatterns -SimpleMatch -ErrorAction SilentlyContinue - foreach ($match in $matches) { - $errors.Add("Fatal pattern in $path line $($match.LineNumber): $($match.Line.Trim())") - } - } -} - -$engineLog = $manifest.launch.engineLog -if ($manifest.launch.attempted -and $engineLog -and (Test-Path $engineLog)) { - $engineLogText = Get-Content -Raw -Path $engineLog - if ($null -eq $engineLogText) { - $engineLogText = "" - } - if ($engineLogText.Trim().Length -gt 0) { - if ($engineLogText -notmatch "(?m)^\s*(ERROR|WARN|INFO|DEBUG)\s+\[") { - $errors.Add("engine.log exists but does not contain expected reone-formatted startup log lines.") - } - $escapedStartupSignal = [regex]::Escape($startupSignal) - if ($engineLogText -notmatch "(?m)^\s*INFO\s+\[main\]\[global\]\s+$escapedStartupSignal\s*$") { - $errors.Add("engine.log does not contain required startup signal: $startupSignal") - } - } else { - $errors.Add("engine.log was empty; required startup signal was not emitted.") - } -} elseif ($manifest.launch.attempted) { - $errors.Add("engine.log was not available to inspect for required startup signal.") -} - -if ($warnings.Count -gt 0) { - foreach ($warning in $warnings) { - Write-Warning $warning - } -} - -if ($errors.Count -gt 0) { - foreach ($errorMessage in $errors) { - Write-Error $errorMessage -ErrorAction Continue - } - exit 1 -} - -Write-Host "Smoke eval passed for $SmokeDir" -exit 0 diff --git a/scripts/run_k1.ps1 b/scripts/run_k1.ps1 deleted file mode 100644 index dd9e0cad0..000000000 --- a/scripts/run_k1.ps1 +++ /dev/null @@ -1,139 +0,0 @@ -#requires -Version 5.1 -[CmdletBinding()] -param( - [string]$GameDir = $env:KOTOR1_DIR, - - [string]$BuildDir, - - [ValidateSet("Debug", "Release", "RelWithDebInfo", "MinSizeRel")] - [string]$Config = "RelWithDebInfo", - - [switch]$Dev, - - [ValidateRange(0, 3)] - [int]$LogSeverity, - - [string[]]$EngineArgs = @() -) - -$ErrorActionPreference = "Stop" - -$RepoRoot = (Resolve-Path (Join-Path $PSScriptRoot "..")).Path -if (-not $BuildDir) { - $BuildDir = Join-Path $RepoRoot "build" -} -$BuildDir = $ExecutionContext.SessionState.Path.GetUnresolvedProviderPathFromPSPath($BuildDir) - -function Resolve-EngineExe { - $candidates = @( - (Join-Path $BuildDir "bin\engine.exe"), - (Join-Path $BuildDir "debug\bin\engine.exe") - ) - - foreach ($candidate in $candidates) { - if (Test-Path $candidate) { - return (Resolve-Path $candidate).Path - } - } - - $found = Get-ChildItem -Path $BuildDir -Recurse -Filter "engine.exe" -ErrorAction SilentlyContinue | Select-Object -First 1 - if ($found) { - return $found.FullName - } - - return $null -} - -function Resolve-ShaderPackErf { - $candidates = @( - (Join-Path $BuildDir "bin\shaderpack.erf"), - (Join-Path $BuildDir "debug\bin\shaderpack.erf") - ) - - foreach ($candidate in $candidates) { - if (Test-Path $candidate) { - return (Resolve-Path $candidate).Path - } - } - - $found = Get-ChildItem -Path $BuildDir -Recurse -Filter "shaderpack.erf" -ErrorAction SilentlyContinue | Select-Object -First 1 - if ($found) { - return $found.FullName - } - - return $null -} - -$engineExe = Resolve-EngineExe -if (-not $engineExe) { - & powershell -NoProfile -ExecutionPolicy Bypass -File (Join-Path $PSScriptRoot "build.ps1") -BuildDir $BuildDir -Config $Config -Target "engine" - if ($LASTEXITCODE -ne 0) { - exit $LASTEXITCODE - } - $engineExe = Resolve-EngineExe -} - -$shaderPackErf = Resolve-ShaderPackErf -if (-not $shaderPackErf) { - & powershell -NoProfile -ExecutionPolicy Bypass -File (Join-Path $PSScriptRoot "build.ps1") -BuildDir $BuildDir -Config $Config -Target "create_shaderpack" - if ($LASTEXITCODE -ne 0) { - exit $LASTEXITCODE - } - $shaderPackErf = Resolve-ShaderPackErf -} - -if (-not $engineExe) { - Write-Error "engine.exe was not found after build." - exit 1 -} - -if (-not $shaderPackErf) { - Write-Error "shaderpack.erf was not found after building create_shaderpack." - exit 1 -} - -if (-not $GameDir) { - Write-Error "Pass -GameDir or set KOTOR1_DIR to a legal KOTOR 1 install directory." - exit 2 -} - -$GameDir = (Resolve-Path $GameDir).Path -$marker = Join-Path $GameDir "swkotor.exe" -if (-not (Test-Path $marker)) { - Write-Error "KOTOR 1 marker executable was not found: $marker" - exit 2 -} - -if ($Dev) { - $hasDevArg = $false - foreach ($arg in $EngineArgs) { - if ($arg -eq "--dev" -or $arg -like "--dev=*") { - $hasDevArg = $true - break - } - } - if (-not $hasDevArg) { - $EngineArgs = @("--dev", "true") + $EngineArgs - } -} - -if ($PSBoundParameters.ContainsKey("LogSeverity")) { - $hasLogSeverityArg = $false - foreach ($arg in $EngineArgs) { - if ($arg -eq "--logsev" -or $arg -like "--logsev=*") { - $hasLogSeverityArg = $true - break - } - } - if (-not $hasLogSeverityArg) { - $EngineArgs = @("--logsev", "$LogSeverity") + $EngineArgs - } -} - -Push-Location (Split-Path -Parent $engineExe) -try { - & $engineExe "--game" $GameDir @EngineArgs - exit $LASTEXITCODE -} finally { - Pop-Location -} diff --git a/scripts/run_k2.ps1 b/scripts/run_k2.ps1 deleted file mode 100644 index 18eeaa33a..000000000 --- a/scripts/run_k2.ps1 +++ /dev/null @@ -1,139 +0,0 @@ -#requires -Version 5.1 -[CmdletBinding()] -param( - [string]$GameDir = $env:KOTOR2_DIR, - - [string]$BuildDir, - - [ValidateSet("Debug", "Release", "RelWithDebInfo", "MinSizeRel")] - [string]$Config = "RelWithDebInfo", - - [switch]$Dev, - - [ValidateRange(0, 3)] - [int]$LogSeverity, - - [string[]]$EngineArgs = @() -) - -$ErrorActionPreference = "Stop" - -$RepoRoot = (Resolve-Path (Join-Path $PSScriptRoot "..")).Path -if (-not $BuildDir) { - $BuildDir = Join-Path $RepoRoot "build" -} -$BuildDir = $ExecutionContext.SessionState.Path.GetUnresolvedProviderPathFromPSPath($BuildDir) - -function Resolve-EngineExe { - $candidates = @( - (Join-Path $BuildDir "bin\engine.exe"), - (Join-Path $BuildDir "debug\bin\engine.exe") - ) - - foreach ($candidate in $candidates) { - if (Test-Path $candidate) { - return (Resolve-Path $candidate).Path - } - } - - $found = Get-ChildItem -Path $BuildDir -Recurse -Filter "engine.exe" -ErrorAction SilentlyContinue | Select-Object -First 1 - if ($found) { - return $found.FullName - } - - return $null -} - -function Resolve-ShaderPackErf { - $candidates = @( - (Join-Path $BuildDir "bin\shaderpack.erf"), - (Join-Path $BuildDir "debug\bin\shaderpack.erf") - ) - - foreach ($candidate in $candidates) { - if (Test-Path $candidate) { - return (Resolve-Path $candidate).Path - } - } - - $found = Get-ChildItem -Path $BuildDir -Recurse -Filter "shaderpack.erf" -ErrorAction SilentlyContinue | Select-Object -First 1 - if ($found) { - return $found.FullName - } - - return $null -} - -$engineExe = Resolve-EngineExe -if (-not $engineExe) { - & powershell -NoProfile -ExecutionPolicy Bypass -File (Join-Path $PSScriptRoot "build.ps1") -BuildDir $BuildDir -Config $Config -Target "engine" - if ($LASTEXITCODE -ne 0) { - exit $LASTEXITCODE - } - $engineExe = Resolve-EngineExe -} - -$shaderPackErf = Resolve-ShaderPackErf -if (-not $shaderPackErf) { - & powershell -NoProfile -ExecutionPolicy Bypass -File (Join-Path $PSScriptRoot "build.ps1") -BuildDir $BuildDir -Config $Config -Target "create_shaderpack" - if ($LASTEXITCODE -ne 0) { - exit $LASTEXITCODE - } - $shaderPackErf = Resolve-ShaderPackErf -} - -if (-not $engineExe) { - Write-Error "engine.exe was not found after build." - exit 1 -} - -if (-not $shaderPackErf) { - Write-Error "shaderpack.erf was not found after building create_shaderpack." - exit 1 -} - -if (-not $GameDir) { - Write-Error "Pass -GameDir or set KOTOR2_DIR to a legal KOTOR 2 install directory." - exit 2 -} - -$GameDir = (Resolve-Path $GameDir).Path -$marker = Join-Path $GameDir "swkotor2.exe" -if (-not (Test-Path $marker)) { - Write-Error "KOTOR 2 marker executable was not found: $marker" - exit 2 -} - -if ($Dev) { - $hasDevArg = $false - foreach ($arg in $EngineArgs) { - if ($arg -eq "--dev" -or $arg -like "--dev=*") { - $hasDevArg = $true - break - } - } - if (-not $hasDevArg) { - $EngineArgs = @("--dev", "true") + $EngineArgs - } -} - -if ($PSBoundParameters.ContainsKey("LogSeverity")) { - $hasLogSeverityArg = $false - foreach ($arg in $EngineArgs) { - if ($arg -eq "--logsev" -or $arg -like "--logsev=*") { - $hasLogSeverityArg = $true - break - } - } - if (-not $hasLogSeverityArg) { - $EngineArgs = @("--logsev", "$LogSeverity") + $EngineArgs - } -} - -Push-Location (Split-Path -Parent $engineExe) -try { - & $engineExe "--game" $GameDir @EngineArgs - exit $LASTEXITCODE -} finally { - Pop-Location -} diff --git a/scripts/smoke_test.ps1 b/scripts/smoke_test.ps1 deleted file mode 100644 index f35e6da5e..000000000 --- a/scripts/smoke_test.ps1 +++ /dev/null @@ -1,356 +0,0 @@ -#requires -Version 5.1 -[CmdletBinding()] -param( - [string]$GameDir, - - [ValidateSet("auto", "k1", "k2")] - [string]$Game = "auto", - - [string]$BuildDir, - - [ValidateSet("Debug", "Release", "RelWithDebInfo", "MinSizeRel")] - [string]$Config = "RelWithDebInfo", - - [int]$TimeoutSeconds = 20, - - [switch]$NoBuild, - - [switch]$ForceBuild, - - [switch]$AllowMissingGame -) - -$ErrorActionPreference = "Stop" - -$RepoRoot = (Resolve-Path (Join-Path $PSScriptRoot "..")).Path -if (-not $BuildDir) { - $BuildDir = Join-Path $RepoRoot "build" -} -$BuildDir = $ExecutionContext.SessionState.Path.GetUnresolvedProviderPathFromPSPath($BuildDir) -$LogsRoot = Join-Path $RepoRoot ".agent\logs" -$timestamp = Get-Date -Format "yyyyMMdd_HHmmss" -$SmokeDir = Join-Path $LogsRoot "smoke_$timestamp" -$LatestSmokeDir = Join-Path $LogsRoot "smoke_latest" -New-Item -ItemType Directory -Force -Path $SmokeDir | Out-Null - -$stdoutPath = Join-Path $SmokeDir "stdout.log" -$stderrPath = Join-Path $SmokeDir "stderr.log" -$engineLogPath = Join-Path $SmokeDir "engine.log" -$manifestPath = Join-Path $SmokeDir "smoke_manifest.json" -$startupSignal = "reone smoke signal: engine startup" -New-Item -ItemType File -Force -Path $stdoutPath | Out-Null -New-Item -ItemType File -Force -Path $stderrPath | Out-Null -New-Item -ItemType File -Force -Path $engineLogPath | Out-Null - -function Resolve-EngineExe { - $candidates = @( - (Join-Path $BuildDir "bin\engine.exe"), - (Join-Path $BuildDir "debug\bin\engine.exe") - ) - - foreach ($candidate in $candidates) { - if (Test-Path $candidate) { - return (Resolve-Path $candidate).Path - } - } - - $found = Get-ChildItem -Path $BuildDir -Recurse -Filter "engine.exe" -ErrorAction SilentlyContinue | Select-Object -First 1 - if ($found) { - return $found.FullName - } - - return $null -} - -function Resolve-ShaderPackErf { - $candidates = @( - (Join-Path $BuildDir "bin\shaderpack.erf"), - (Join-Path $BuildDir "debug\bin\shaderpack.erf") - ) - - foreach ($candidate in $candidates) { - if (Test-Path $candidate) { - return (Resolve-Path $candidate).Path - } - } - - $found = Get-ChildItem -Path $BuildDir -Recurse -Filter "shaderpack.erf" -ErrorAction SilentlyContinue | Select-Object -First 1 - if ($found) { - return $found.FullName - } - - return $null -} - -function Resolve-GameDirectory { - param( - [string]$Path, - [string]$RequestedGame - ) - - if (-not $Path) { - if ($env:KOTOR1_DIR) { - $Path = $env:KOTOR1_DIR - if ($RequestedGame -eq "auto") { - $RequestedGame = "k1" - } - } elseif ($env:KOTOR2_DIR) { - $Path = $env:KOTOR2_DIR - if ($RequestedGame -eq "auto") { - $RequestedGame = "k2" - } - } - } - - if (-not $Path) { - return $null - } - - $resolved = (Resolve-Path $Path).Path - $k1Marker = Join-Path $resolved "swkotor.exe" - $k2Marker = Join-Path $resolved "swkotor2.exe" - - if ($RequestedGame -eq "k1" -and -not (Test-Path $k1Marker)) { - throw "KOTOR 1 marker executable was not found: $k1Marker" - } - if ($RequestedGame -eq "k2" -and -not (Test-Path $k2Marker)) { - throw "KOTOR 2 marker executable was not found: $k2Marker" - } - if ($RequestedGame -eq "auto") { - if (Test-Path $k1Marker) { - $RequestedGame = "k1" - } elseif (Test-Path $k2Marker) { - $RequestedGame = "k2" - } else { - throw "No KOTOR marker executable was found in: $resolved" - } - } - - return [ordered]@{ - path = $resolved - game = $RequestedGame - } -} - -function Write-Manifest { - param([object]$Manifest) - $Manifest | ConvertTo-Json -Depth 8 | Set-Content -Path $manifestPath -Encoding UTF8 -} - -function Publish-LatestSmoke { - if (Test-Path $LatestSmokeDir) { - Remove-Item -Recurse -Force -Path $LatestSmokeDir - } - Copy-Item -Recurse -Force -Path $SmokeDir -Destination $LatestSmokeDir -} - -function Find-FatalMatches { - param([string[]]$Paths) - - $fatalPatterns = @( - "Engine failure", - "FATAL", - "Unhandled exception", - "Access violation", - "Segmentation fault", - "CMake Error", - "Build FAILED", - "fatal error" - ) - - $matches = @() - foreach ($path in $Paths) { - if (Test-Path $path) { - $matches += Select-String -Path $path -Pattern $fatalPatterns -SimpleMatch -ErrorAction SilentlyContinue - } - } - - return $matches -} - -function Join-ProcessArguments { - param([string[]]$Arguments) - - $quoted = foreach ($argument in $Arguments) { - $escaped = $argument -replace '"', '\"' - if ($escaped -match '\s') { - '"' + $escaped + '"' - } else { - $escaped - } - } - - return ($quoted -join " ") -} - -$manifest = [ordered]@{ - schema = 1 - createdAt = (Get-Date).ToString("o") - repoRoot = $RepoRoot - build = [ordered]@{ - success = $false - action = "not-started" - config = $Config - target = "engine" - buildDir = $BuildDir - engineExe = $null - shaderpackAction = "not-started" - shaderpackErf = $null - log = (Join-Path $LogsRoot "latest_build.log") - } - launch = [ordered]@{ - attempted = $false - started = $false - timedOut = $false - timeoutSeconds = $TimeoutSeconds - exitCode = $null - game = $Game - gameDir = $null - skipReason = $null - stdout = $stdoutPath - stderr = $stderrPath - engineLog = $engineLogPath - shaderpackErf = $null - } - evalHints = [ordered]@{ - obviousFatalPatterns = @("Engine failure", "FATAL", "Unhandled exception", "Access violation", "Segmentation fault", "CMake Error", "Build FAILED", "fatal error") - startupSignal = $startupSignal - } -} - -try { - $gameInfo = Resolve-GameDirectory -Path $GameDir -RequestedGame $Game - if ($gameInfo) { - $manifest.launch.game = $gameInfo.game - $manifest.launch.gameDir = $gameInfo.path - } - - $engineExe = Resolve-EngineExe - if (($ForceBuild -or -not $engineExe) -and -not $NoBuild) { - $manifest.build.action = "built" - Write-Manifest -Manifest $manifest - & powershell -NoProfile -ExecutionPolicy Bypass -File (Join-Path $PSScriptRoot "build.ps1") -BuildDir $BuildDir -Config $Config -Target "engine" - if ($LASTEXITCODE -ne 0) { - $manifest.build.success = $false - $manifest.launch.skipReason = "Build failed before launch. See $($manifest.build.log)." - Write-Manifest -Manifest $manifest - Publish-LatestSmoke - exit $LASTEXITCODE - } - $engineExe = Resolve-EngineExe - } elseif ($engineExe) { - $manifest.build.action = "verified-existing" - } else { - $manifest.build.action = "missing" - } - - if (-not $engineExe) { - $manifest.build.success = $false - $manifest.launch.skipReason = "engine.exe was not found before launch." - Write-Manifest -Manifest $manifest - Publish-LatestSmoke - Write-Error "engine.exe was not found. Run scripts\build.ps1 or omit -NoBuild." - exit 1 - } - - $manifest.build.success = $true - $manifest.build.engineExe = $engineExe - - $shaderPackErf = Resolve-ShaderPackErf - if (($ForceBuild -or -not $shaderPackErf) -and -not $NoBuild) { - $manifest.build.shaderpackAction = "built" - Write-Manifest -Manifest $manifest - & powershell -NoProfile -ExecutionPolicy Bypass -File (Join-Path $PSScriptRoot "build.ps1") -BuildDir $BuildDir -Config $Config -Target "create_shaderpack" - if ($LASTEXITCODE -ne 0) { - $manifest.build.success = $false - $manifest.launch.skipReason = "Shader pack build failed before launch. See $($manifest.build.log)." - Write-Manifest -Manifest $manifest - Publish-LatestSmoke - exit $LASTEXITCODE - } - $shaderPackErf = Resolve-ShaderPackErf - } elseif ($shaderPackErf) { - $manifest.build.shaderpackAction = "verified-existing" - } else { - $manifest.build.shaderpackAction = "missing" - } - - if (-not $shaderPackErf) { - $manifest.build.success = $false - $manifest.launch.skipReason = "shaderpack.erf was not found before launch." - Write-Manifest -Manifest $manifest - Publish-LatestSmoke - Write-Error "shaderpack.erf was not found. Build the create_shaderpack target or omit -NoBuild." - exit 1 - } - - $manifest.build.shaderpackErf = $shaderPackErf - - if (-not $gameInfo) { - $manifest.launch.skipReason = "No legal KOTOR or TSL game directory was provided. Pass -GameDir or set KOTOR1_DIR/KOTOR2_DIR." - Write-Manifest -Manifest $manifest - Publish-LatestSmoke - if ($AllowMissingGame) { - Write-Host $manifest.launch.skipReason - exit 0 - } - Write-Error $manifest.launch.skipReason - exit 2 - } - - $manifest.launch.attempted = $true - $manifest.launch.game = $gameInfo.game - $manifest.launch.gameDir = $gameInfo.path - $manifest.launch.shaderpackErf = Join-Path $SmokeDir "shaderpack.erf" - Copy-Item -Force -Path $shaderPackErf -Destination $manifest.launch.shaderpackErf - Write-Manifest -Manifest $manifest - - $engineArgs = @( - "--game", $gameInfo.path, - "--width", "640", - "--height", "480", - "--winscale", "100", - "--fullscreen", "false", - "--logsev", "1" - ) - - $process = Start-Process -FilePath $engineExe -ArgumentList (Join-ProcessArguments -Arguments $engineArgs) -WorkingDirectory $SmokeDir -RedirectStandardOutput $stdoutPath -RedirectStandardError $stderrPath -PassThru - $manifest.launch.started = $true - Write-Manifest -Manifest $manifest - - $finished = $process.WaitForExit($TimeoutSeconds * 1000) - if ($finished) { - $manifest.launch.exitCode = $process.ExitCode - } else { - $manifest.launch.timedOut = $true - Stop-Process -Id $process.Id -Force -ErrorAction SilentlyContinue - Start-Sleep -Milliseconds 250 - } - - if (-not (Test-Path $engineLogPath)) { - New-Item -ItemType File -Path $engineLogPath | Out-Null - } - - $fatalMatches = Find-FatalMatches -Paths @($stdoutPath, $stderrPath, $engineLogPath) - Write-Manifest -Manifest $manifest - Publish-LatestSmoke - - if ($fatalMatches.Count -gt 0) { - Write-Error "Smoke found obvious fatal output. See $SmokeDir" - exit 1 - } - - if ($manifest.launch.exitCode -ne $null -and [int]$manifest.launch.exitCode -ne 0) { - Write-Error "Engine exited with code $($manifest.launch.exitCode). See $SmokeDir" - exit 1 - } - - Write-Host "Smoke completed. Artifacts: $SmokeDir" - exit 0 -} catch { - $manifest.launch.skipReason = $_.Exception.Message - Write-Manifest -Manifest $manifest - Publish-LatestSmoke - Write-Error $_ - exit 1 -}