From d51122fef92a17f6899c9762998eb8a4a4c5a796 Mon Sep 17 00:00:00 2001 From: dev-camo Date: Thu, 1 Oct 2026 14:33:13 -0600 Subject: [PATCH 1/5] Record 1.4.0 execution plan --- .agents/PLAN.md | 521 ++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 521 insertions(+) create mode 100644 .agents/PLAN.md diff --git a/.agents/PLAN.md b/.agents/PLAN.md new file mode 100644 index 0000000..6d1a476 --- /dev/null +++ b/.agents/PLAN.md @@ -0,0 +1,521 @@ +# P3RFix 1.4.0 release plan: four focused maintenance changes + +This release should make automatic menu texture scaling consistent, reject invalid raw-input packets safely, install independent fixes before the optional console startup wait, and replace the release ZIPs' scattered license files with one complete `LICENSES` file. Players should retain the working console, the validated viewport-based ultrawide behavior, their existing configuration, and the current installation method. + +Target release: **1.4.0**. Baseline examined: **1.3.0**, repository commit `6d55ef6`, on 2026-10-01. Execution began from branch `1.4.0`, commit `d352a6b`, with only this untracked plan in the working tree. No target release date is assigned and no release has been published. + +This ExecPlan is a living document. Update `Progress`, `Surprises & Discoveries`, `Decision Log`, and `Outcomes & Retrospective` as implementation and validation proceed. Source line numbers are orientation aids from the examined baseline; find the named functions again if intervening changes move them. + +## Purpose / Big Picture + +The four changes belong in a small maintenance release, separate from the four larger changes proposed for 1.5.0. Automatic render-target scaling should retain the intended native-quality baseline on low-height displays while honoring a user's explicit multiplier. A failed Windows raw-input read should have no effect on the camera and should not lead to parsing uninitialized or incomplete storage. Enabling the developer console should not put unrelated hook installation behind its engine-discovery retry loop. Installation should leave one readable licensing document alongside the existing package files, with the project's own license and every retained dependency notice together instead of an entire `licenses/` directory. + +A render target is a texture that the game draws into before displaying it elsewhere, such as a menu character preview or a captured menu background. A hook is a small interception installed at a game or Windows function so P3RFix can inspect or adjust behavior. These changes affect P3RFix's decisions around those existing hooks; they do not replace the game's renderer or input system. + +Success requires meaningful automated checks for the scaling and packet-handling behavior, inspection of both archives for complete consolidated notices, and an actual game comparison against 1.3.0. Compilation alone does not establish compatibility with a game executable, show that a menu texture still looks correct, or establish that the packaged license bundle is complete. + +## Release scope and established facts + +The release contains exactly four primary changes: automatic render-target scaling, raw-input read validation, startup ordering, and consolidated release licensing. Version metadata, release notes, and narrowly necessary checks accompany those changes. Keep existing configuration sections and keys intact. No new player-facing setting is necessary for these changes. + +The maintainer has verified that the developer console works in 1.3.0. The historical console crash in the shipped upstream 1.2.4 release is not an outstanding confirmed bug in the current baseline. Its cause remains unconfirmed: the newer bundled Ultimate ASI Loader and the eleven upstream commits between 1.2.4 and the maintenance takeover are possible explanations. Do not attribute that recovery to the focused Unreal refactor without evidence. + +The maintainer has also validated the viewport-based ultrawide fix through extended gameplay. Commit `cc2582b36b8c3fe37a47781ea5fc98638c3f75b9`, shipped in 1.2.5, reads the actual viewport dimensions. Preserve that acquisition path and its handling of empty or minimized viewports. Do not restore requested-resolution tracking, multiply width by screen percentage to guess a display width, or replace it with the desktop-width workaround from Codeberg PR #5. + +The shipping installation method and package placement are established as correct. A historical user installing files in the wrong location is not a reason to redesign the archives. The maintainer has separately requested one deliberate package-content improvement: replace the licensing directory and separate licensing documents with a single `LICENSES` file. Preserve the established binary/configuration destinations, archive names, loader placement, and installation procedure while making that specific change. If documentation is clarified while preparing this release, describe the actual package accurately and keep the final files beside `P3R.exe` for the standalone installation. + +The binary remains a **Windows x64 ASI built with MSVC**. Preserve the existing Visual Studio 2022, Windows SDK, xmake, CMake, PowerShell, dependency pins, and release workflows. Proton testing means testing the Windows binary under Proton; it does not mean adding a Linux binary or Linux cross-compilation. Codeberg PR #5 and its recommendations are excluded from this work. + +Separate gameplay/menu/background frame-rate limits, new HUD/FOV controls, live configuration, mouse sensitivity redesign, smoothing, per-target graphics budgets, and new platform support are not part of 1.4.0. + +## Progress + +- [x] (2026-10-01) Reviewed the three original proposals against the 1.3.0 source and preserved the maintainer's current validation and build constraints. +- [x] (2026-10-01) Renamed and expanded the former `pre-v2-fixes.md` notes into a targeted 1.4.0 release plan. +- [x] (2026-10-01) Added the maintainer's fourth release change: one generated `LICENSES` document in each ZIP, preserving existing notices and the source license files. +- [x] (2026-10-01) Read the entire plan, repository instructions, and ExecPlan skill; inspected the baseline source and submodule pins. Assigned independent scaling, packet-reader, and licensing implementation work while integrating the hooks and Windows validation centrally. +- [ ] Implement and test the automatic render-target scaling policy. +- [ ] Implement and test checked raw-input reading without changing camera interpretation. +- [ ] Adjust startup ordering and verify console-enabled and console-disabled startup. +- [ ] Generate the complete combined `LICENSES` file in the existing PowerShell release script and stop copying separate licensing files/directories into either package. +- [ ] Validate license completeness, repeatable generation, and the contents of both final ZIPs; update source and release documentation to describe the new package contract. +- [ ] Run existing Unreal integration tests and the source inventory in Windows x64 debug and release configurations. +- [ ] Compare menu rendering, input, console operation, intro skipping, and ultrawide behavior in the game. +- [ ] Prepare 1.4.0 metadata, notes, and both existing package formats. +- [ ] Record actual validation evidence and release outcome. + +## Surprises & Discoveries + +The render-target guard does more than contain an obvious repeated variable. It currently reads `iCurrentResX <= 1920 || iCurrentResX <= 1080`, which is equivalent to the first comparison alone. Because the multiplier calculation uses vertical rendering resolution, a mechanical replacement of the second variable would still leave a width-dependent policy. This plan chooses an explicit vertical-resolution policy instead. + +At 2560x720 and 100% screen percentage, the old automatic multiplier is approximately 0.667 because the width-only guard does not activate. At 1920x2160 and 100%, it instead forces the automatic multiplier to 1. These are results of the examined arithmetic, not claims that the maintainer reproduced a visual defect at those resolutions. + +The raw-input path does not check the size-query result. It then logs an unexpected read result but continues to access the packet. Its `new BYTE[dwSize]` null check also does not handle the exception normally raised by an unsuccessful ordinary C++ allocation. Packet validation therefore needs to cover the entire read path, not only add a return after the existing log line. + +The console retry loop runs on P3RFix's initialization worker. It does not itself pause the entire game thread. Its observable ordering problem is that later independent fixes are installed only after the retry finishes. Preserve that distinction in release notes. + +The current packaging loop in `create_release.ps1` copies `LICENSE.md`, `THIRD_PARTY_NOTICES.md`, and the entire `licenses/` directory into both release packages. The repository currently has eleven component-license files under `licenses/`, in addition to the project's own license and the provenance document. Some component files contain additional notices or exceptions: the MinHook file includes the Hacker Disassembler Engine notices, and the fmt file includes its optional binary exception. Combining only a generic MIT paragraph would lose material already present in the release inputs. + +The Windows VM at `camo@192.168.122.141` accepts SSH and uses a Windows command shell. Tool locations and a dedicated validation checkout are being established. Game execution is expressly reserved for the maintainer; automated validation must stop at a concrete package and test handoff. + +## Decision Log + +Decision: target the original three behavior fixes and the subsequently requested licensing cleanup at 1.4.0 rather than a pre-v2 backlog. Rationale: the maintainer wants a concrete maintenance release and separate treatment from the 1.5.0 feature proposals. Date/author: 2026-10-01, maintainer direction recorded by Codex. + +Decision: the automatic multiplier has a minimum of 1, and explicit user multipliers are applied afterward. Rationale: preserve native render-target quality automatically without removing the user's deliberate ability to undersample. Date/author: 2026-10-01, proposed implementation design by Codex. + +Decision: preserve the current raw-mouse interpretation in this release. Rationale: packet-read safety can be established independently of the larger camera and device-handoff work proposed for 1.5.0. Date/author: 2026-10-01, proposed implementation design by Codex. + +Decision: solve console-related ordering by moving its existing wait after independent installation on the same initialization worker. Rationale: another thread or a console implementation rewrite is unnecessary for the stated problem and would add concurrency risk to a working feature. Date/author: 2026-10-01, proposed implementation design by Codex. + +Decision: retain Windows x64/MSVC, the current build tooling, working viewport discovery, working console behavior, archive names, and binary/configuration placement; change only the specified licensing payload. Rationale: these are established project constraints and validated behavior, with an explicit maintainer request to simplify release licensing. Date/author: 2026-10-01, maintainer direction recorded by Codex. + +Decision: use the exact extensionless filename `LICENSES` for the combined release document. Rationale: the plural name describes the project's license plus multiple component licenses and notices without implying that one license governs every component. The maintainer authorized either `LICENSE` or `LICENSES`; selecting one spelling keeps both packages and their documentation consistent. Date/author: 2026-10-01, maintainer direction and proposed filename recorded by Codex. + +Decision: generate `LICENSES` from the existing checked-in license texts and provenance during packaging, using the existing PowerShell script, and put the same combined contents in both package formats. Rationale: keeping editable source notices avoids a second manually maintained copy of every license, while one shared generated output prevents package-specific omissions. Loader sections will explicitly state that the loader binary is bundled only with the standalone package. Date/author: 2026-10-01, proposed implementation design by Codex. + +Decision: reserve actual game runs and install-method compatibility confirmation for the maintainer, and retain this plan until that confirmation arrives. Rationale: the execution instructions prohibit the agent from running the game and require user validation before deleting the plan or completing/pushing the branch. Prepare instructions, packages, and a result form under `/tmp/P3RFix/`. Date/author: 2026-10-01, maintainer instruction recorded by Codex. + +## Context and Orientation + +The repository examined is `/home/camo/Projects/P3RFix`. On a Windows build machine, use that machine's checkout path rather than attempting to use this Linux path. The source produces a Windows ASI: a DLL with an `.asi` filename loaded into the game by the standalone loader or Reloaded-II. + +`src/dllmain.cpp` contains configuration parsing, startup, signature discovery, and most hook callbacks. `Main` is the initialization worker created by `DllMain`. `Resolution` installs the viewport-size hook and screen-percentage hook. `UTextureRenderTarget2D_PostLoad_hk` calculates new texture dimensions and forwards to the original game function. `PeekMessageW_Injected` observes messages after the original Windows message function returns. + +`src/unreal/Integration.hpp` and `src/unreal/Integration.cpp` expose focused render-target and console operations. Private memory layouts and reflected calls live in `src/unreal/detail/`. The generated SDK is no longer a build dependency. Keep that separation. The existing independent byte fixtures in `tests/unreal_integration_tests.cpp` verify memory boundaries and console behavior; they do not currently test the scaling decision or Windows packet-reader failure paths. + +`xmake.lua` defines the existing ASI and `unreal-integration-tests` targets. `create_release.ps1` builds and packages a version supplied through `-Version`. The version default is also present in `xmake.lua` and as a fallback macro in `src/dllmain.cpp`. `.github/workflows/build.yml` checks inventory and runs the Unreal tests before packaging. `P3RFix.ini`, `README.md`, `CHANGELOG.md`, `release_body.md`, and `assets/r2-package/ModConfig.json` supply the player-facing configuration, documentation, and package metadata. + +`LICENSE.md` contains P3RFix's MIT license and the original/current-maintenance copyright notices. `THIRD_PARTY_NOTICES.md` identifies compiled dependencies, the pinned standalone loader and its dependencies, and retained Unreal integration provenance. The eleven component texts under `licenses/` are the inputs to preserve in full. They remain source-repository files; the release script will combine their contents into one package document instead of copying the source directory into a player's game installation. + +Use small independent helpers for arithmetic and packet validation so tests exercise the shipped implementation. Keep the hook callbacks as adapters around those helpers. Do not turn this release into a general reorganization of `dllmain.cpp`. + +## Change 1: make automatic render-target scaling consistent + +### Current behavior and defect + +The relevant calculation begins around `src/dllmain.cpp:378`: + + float fOptimalRenTexResMulti = + (iCurrentResY * (fScreenPercentage / 100)) / 1080; + + if (iCurrentResX <= 1920 || iCurrentResX <= 1080) + fOptimalRenTexResMulti = 1.0f; + +The function then applies `[Render Texture Resolution].Multiplier`, clamps the result to 0.25 through 4, multiplies the original target's width and height, and updates the capture-size globals when the target is RGBA16F. RGBA16F is the floating-point texture format used by the existing code to select those capture dimensions. + +The problem is the mismatch between the height-derived calculation and the redundant width-only guard. The release should define the automatic baseline consistently for standard, ultrawide, and narrow resolutions. + +### Required policy + +For a positive viewport height and finite positive screen percentage, calculate the effective vertical rendering resolution as: + + effectiveHeight = viewportHeight * screenPercentage / 100.0 + automaticMultiplier = max(1.0, effectiveHeight / 1080.0) + finalMultiplier = clamp(automaticMultiplier * userMultiplier, 0.25, 4.0) + +Apply the upper limit to the final combined multiplier. Do not clamp the automatic multiplier to 4 before applying the user value: a large effective height combined with a multiplier below 1 should retain its intended combined meaning. + +The user multiplier remains relative to the automatic baseline. A value of 1 uses automatic scaling; a value of 0.5 halves that baseline; a value of 2 doubles it, subject to the final limit. This preserves the existing documented meaning of the setting. + +If the viewport height has not yet become valid, use an automatic baseline of 1 until a valid height is available. The existing viewport code already keeps the last valid dimensions during minimization; do not reset those dimensions here. A finite nonpositive or nonfinite screen percentage cannot safely drive arithmetic; use 100 for this calculation and report the invalid observation without changing the user's INI on disk. Use 1 for an invalid nonpositive or nonfinite user multiplier at this boundary. Ordinary finite user values should continue to be constrained to the documented 0.25-to-4 range by configuration validation. + +These defensive fallbacks cover unexpected runtime input to the helper. They are not an invitation to change all configuration parsing in 1.4.0. + +### Proposed implementation boundary + +Create `src/render/Scaling.hpp` and `src/render/Scaling.cpp` with a small pure arithmetic interface under `p3r::render`. A pure function makes its decision only from its arguments; it does not read game memory, perform a pattern scan, or change globals. + +Use the following interface, with a result field or equivalent diagnostic status for sanitized inputs: + + struct ScaleResult { + double automaticMultiplier; + float finalMultiplier; + bool usedFallback; + }; + + ScaleResult ComputeRenderTargetScale( + int viewportHeight, + float screenPercentage, + float userMultiplier) noexcept; + + struct ScaledDimensions { + std::int32_t width; + std::int32_t height; + }; + + std::optional ScaleDimensions( + std::int32_t width, + std::int32_t height, + float multiplier) noexcept; + +Have `UTextureRenderTarget2D_PostLoad_hk` use this helper and retain its existing integration calls. Calculate in a sufficiently wide floating-point type, check finiteness and integer bounds before narrowing dimensions, and reject nonpositive source dimensions. Preserve the current truncation toward zero for positive products, with a minimum of one pixel for a valid deliberately undersampled target. A floating-point value outside the `int32_t` range must never be cast and written to game memory. + +On rejected dimensions, leave the target untouched and forward to the original function exactly once. Keep the RGBA16F capture-size behavior. Do not add target-category selection, a graphics-memory budget, or rescaling of already-created targets in this release. Those require different feature work. + +### Expected arithmetic examples + +At 1280x720, 1280x800, 1920x1080, or 2560x720 with 100% screen percentage and user multiplier 1, the final multiplier is 1. At 3440x1440 or 5120x1440 with the same settings, it is approximately 1.333333. At 3840x2160 it is 2. At 1920x2160 it is also 2; width does not decide the vertical-quality baseline. + +At a 2160-pixel viewport height with 50% screen percentage, the automatic baseline is 1. At a 1440-pixel height with 150%, it is 2. A user multiplier of 0.5 applied to the 1440p/100% baseline yields approximately 0.666667, demonstrating that explicit undersampling still works. A combined multiplier above 4 is capped at 4. + +A 1920x1080 source target becomes 2560x1440 for the normal 1440p/100%/1 case and 3840x2160 for the 2160p/100%/1 case. Use tolerances for intermediate floating-point comparisons. Expected integer dimensions should be checked directly. + +### Validation and acceptance for this change + +Add tests with independent expected answers for native-baseline retention, height-driven scaling, screen-percentage interaction, user undersampling, combined clamping, invalid arithmetic inputs, invalid source dimensions, one-pixel minimums, and integer overflow rejection. Include a case that fails under the old guard, such as 2560x720 with automatic scaling. + +In the actual game, compare at least one below-1080p display mode, a 1440p ultrawide mode, and a 2160p mode against the baseline. Inspect menu model previews and captured backgrounds, record old and new target sizes from the log, and watch for unexpected hitching. Verify that changing aspect ratio alone at the same vertical rendering resolution does not alter the quality baseline. + +Acceptance requires correct calculated sizes, original-function forwarding on every path, no unrelated viewport change, and no new visual or loading regression. The release notes should describe the scaling guard correction; they should not claim a universal performance improvement. + +## Change 2: validate raw-input reads before parsing + +### Current behavior and defect + +The relevant path begins around `src/dllmain.cpp:855`. It queries `dwSize`, allocates that many bytes, performs a second read, and then interprets the buffer even if the read fails or is incomplete. + +The Windows API contract is specific: a successful size query with a null data pointer returns zero; a successful data read returns the copied byte count; an error returns `(UINT)-1`. The buffer must have at least 32-bit alignment. Those facts are summarized here so implementation does not depend on external context. [Microsoft GetRawInputData reference](https://learn.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-getrawinputdata). + +### Required behavior + +A failed size query, zero or undersized required buffer, allocation failure, failed data read, copied count outside the allocated capacity, inconsistent packet header, or truncated mouse payload must result in no P3RFix camera update. Return the original `PeekMessageW` result. Observing a bad packet must not consume another Windows message or prevent the game from performing its own handling. + +Keep the queried allocation capacity separate from the size variable passed back to the second API call. Store the read's returned count before comparing it. Do not depend on the evaluation order of an API call and a comparison against a variable that the call can modify. + +Validate that the copied bytes contain a `RAWINPUTHEADER`. Inspect the header through a checked copy or properly aligned storage. Validate its declared packet size against the copied count and capacity. If the type is mouse, require enough bytes for the mouse payload at the Windows-defined union offset before accessing movement. Non-mouse packets need no camera processing. + +For 1.4.0, retain the current movement-selection behavior: only the packets accepted by the existing `usFlags == 0` condition contribute to the custom relative mouse movement. Changing the interpretation of other valid flags belongs to 1.5.0. Retain signed X/Y values, the existing division by 1200, mouse-activity behavior for valid nonzero motion, and exactly-once accumulation when the message is removed. + +Use scoped ownership for the buffer. An allocation failure must not throw through a Windows interception callback. Keep any catch localized to allocation/read preparation; avoid a broad catch that hides unrelated defects. + +### Proposed implementation boundary + +Create `src/input/RawMousePacket.hpp` and `src/input/RawMousePacket.cpp`. Use Windows SDK structures directly; do not create packed replicas of them. Keep the helper independent of SafetyHook, logging, and game-memory layouts. + +Define a reader function type compatible with the actual Windows function so tests can supply deterministic read outcomes: + + using RawInputReader = + UINT (WINAPI*)(HRAWINPUT, UINT, LPVOID, PUINT, UINT); + + struct MouseDelta { + LONG x; + LONG y; + }; + + enum class PacketStatus { + RelativeMouse, + Ignored, + Invalid, + AllocationFailed + }; + + struct PacketResult { + PacketStatus status; + MouseDelta delta; + }; + + PacketResult ReadRelativeMousePacket( + HRAWINPUT input, + RawInputReader reader) noexcept; + +Production passes `GetRawInputData`; tests pass a controlled implementation of that same contract. An aligned array of 32-bit words is one simple way to own the requested byte capacity. Check the rounding calculation before allocating. Parse only the bytes the API reports as copied. + +The hook remains responsible for applying the returned valid delta to the existing accumulators. Invalid, ignored, and allocation-failure results do not alter movement or ownership state. Keep original-call forwarding at the top of the message hook as it is today. Preserve the original result on every early return. + +Log invalid reads without flooding the log on repeated device events. Allocation/read failures can include an available error code captured at the point of failure, but do not invent an error code for a structurally short packet. Normal ignored keyboard or nonrelative packets are not errors. + +### Validation and acceptance for this change + +Use fixtures that exercise the production reader with a successful two-call mouse read, a size-query error, zero queried size, a queried size smaller than the header, a data-read error, a short data read, an oversized returned count, a header declaring more data than was copied, and a truncated mouse payload. Add valid non-mouse and currently ignored flag cases. + +Use both positive and negative motion, including zero motion. Confirm that a valid delta is accumulated once and that an invalid result leaves existing accumulated X/Y and the active-input flag unchanged. The allocation-failure path should be verified through an appropriately small allocation seam or a controlled failing allocator if practical; do not attempt to exhaust system memory as a test. + +In game, enable the existing mouse fix and check movement, strafing, dialogue, menus, and controller switching against 1.3.0. This release's valid-input camera response should remain equivalent. A changed sensitivity curve or newly introduced smoothing would violate its scope. + +Acceptance requires no parsing outside the copied packet, no leaked storage on early returns, no exception escaping the callback, no duplicated message processing, and preserved valid-input behavior. + +## Change 3: remove the console wait from independent installation + +### Current behavior and ordering + +In the baseline, `Main` calls `EnableConsole` after `RenderTextures` and before `IntroSkip`, `AspectRatioFOV`, `HUDFix`, `Framerate`, `WindowFocus`, and `MouseFix`. + +`EnableConsole` returns immediately when disabled. When enabled, it retries up to 200 pending results with a 100 ms sleep between attempts. That is the existing approximately 20-second pending window, plus lookup work. A missing dependency or other nonpending error already exits the loop early. + +### Proposed order and preserved behavior + +Keep initialization on the existing worker and use this order: + + Logging + Configuration + InitializeUnrealIntegration + Resolution + RenderTextures + IntroSkip + AspectRatioFOV + HUDFix + Framerate + WindowFocus + MouseFix + EnableConsole + +Check that no moved installer relies on a constructed console. The examined source provides no such dependency: these functions scan and install their own hooks. Render-target access is also independent of console-runtime discovery through the focused integration. + +Preserve the console retry count, pending interpretation, result reporting, key reporting, and console construction behavior. Do not add a second worker, call game-object construction from another new thread, or replace the console implementation. Do not move work into `DllMain`; it should remain the small loader entry point that starts the existing worker. + +Add a concise initialization-complete log marker after independent installation and before console initialization. Its wording must distinguish attempting independent installation from every requested feature succeeding. Individual scan errors still need their existing reporting. + +### Validation and acceptance for this change + +Check source dependencies and actual startup logs. With the console disabled, startup behavior should remain equivalent. With it enabled and ready, all independent installation attempts should appear before console success/key reporting. With dependencies unavailable, independent attempts should still precede the console error and the game should retain those successfully installed features. + +For a genuinely delayed pending console, verify the ordering through a controlled startup observation or development-only test harness. A harness may model pending console discovery without touching game memory, but do not retain a special delay toggle in the shipped player configuration merely to test this reorder. Do not build a large startup abstraction just to assert a literal function list. + +Test intro skipping with the console enabled so time-sensitive hooks have a chance to act at boot. Also verify that the working console still opens with its configured game binding and that repeated access works. This change should improve availability of independent fixes during the pending period, not change the console's own functionality. + +## Change 4: consolidate all release licensing into one `LICENSES` file + +### Current package contents and requested outcome + +The baseline release script requires `LICENSE.md`, `THIRD_PARTY_NOTICES.md`, and `licenses/`, then copies all of them into both package staging directories. The two archive formats therefore carry multiple separate licensing documents and a nested directory that becomes extra installation clutter when extracted. + +Replace that distribution payload with exactly one extensionless `LICENSES` document at each package's existing content root. It must contain the complete P3RFix license, dependency license texts, and retained attribution/provenance. Neither ZIP should separately contain `LICENSE.md`, `THIRD_PARTY_NOTICES.md`, or any `licenses/` entry. Keep the archive root, any established game-relative nesting, ASI/INI/loader destinations, and Reloaded-II metadata placement unchanged. The new file occupies the same package level where the existing top-level licensing documents belong; it is not a reason to add another enclosing folder. + +The source checkout retains `LICENSE.md`, `THIRD_PARTY_NOTICES.md`, and the component files under `licenses/`. The requested simplification concerns what players download and extract. Generating the release document from those source inputs makes future maintenance easier and preserves the repository's existing license references. + +### Contents and component coverage + +Start the combined document with a short plain-text introduction explaining that it collects P3RFix's license and the notices for components used or credited by the release. Explain that each component retains its own terms and that loader notices describe the loader bundled with the standalone ZIP. Put the P3RFix license first, followed by the provenance/attribution section and individually labeled component sections. + +The P3RFix section must preserve both copyright lines from `LICENSE.md`, including the original author's credit. The provenance section must retain the component identities, pinned revisions/version information, the distinction between compiled dependencies and the standalone loader, and the existing retained Unreal integration attribution. The loader remains Ultimate ASI Loader v9.7.4 with the existing SHA256 verification and DLL naming; this change does not upgrade or replace it. + +Preserve the entire text of every component file, including copyrights, grants, conditions, disclaimers, embedded subcomponent notices, and exceptions. Do not replace several MIT variants with one shared MIT paragraph, drop the Boost text because the package also includes MIT-licensed software, or omit MinHook's additional Hacker Disassembler Engine notices. Preserve fmt's optional exception as part of its source text even though this plan does not depend on invoking it. + +Retain the existing Dumper-7, UnrealContainers, UTF-N, and Unhandled Exception Tracer provenance as written and correctly attributed. Moving notices does not assign a new license to inherited code or change what the existing provenance document says about the source trees examined when it was prepared. Keep original source attribution comments in place. + +Use this explicit ordered input manifest inside `create_release.ps1`; it defines thirteen source documents, comprising twelve full license texts and one provenance document: + + LICENSE.md + THIRD_PARTY_NOTICES.md + licenses/inipp/LICENSE.txt + licenses/spdlog/LICENSE + licenses/fmt/LICENSE + licenses/SafetyHook/LICENSE + licenses/Zydis/LICENSE + licenses/Zycore/LICENSE + licenses/UnrealContainers/LICENSE + licenses/Ultimate-ASI-Loader/LICENSE + licenses/Ultimate-ASI-Loader/miniz-LICENSE + licenses/Ultimate-ASI-Loader/MinHook-LICENSE.txt + licenses/Ultimate-ASI-Loader/injector-utility-LICENSE.txt + +Give each input a readable component heading and its source-repository path. Those path labels describe where the maintained source text lives; they do not claim that the original individual files are also shipped. Distinguish the P3RFix license section from the provenance section and distinguish the four loader-related inputs from components compiled into the ASI. + +Use the same combined document in both packages. In the introduction and loader sections, state plainly that Reloaded-II does not include the standalone loader binary. Including the same clearly qualified loader notices in both archives preserves existing notice coverage and keeps generation/validation simple; it must not suggest that Reloaded-II loads or requires `dsound.dll` from this package. + +### Readability and text preservation + +Write ordinary UTF-8 text without a byte-order mark at the beginning of the combined file. A byte-order mark is an encoding marker that some source text files contain; stripping that marker from an input prevents invisible markers from appearing between sections. Preserve Unicode author names and meaningful text. Use one consistent newline convention and a final newline so the document opens cleanly in common Windows editors. + +Place clear separators and component titles outside the copied text. License payloads may normalize line endings and remove only an initial encoding marker; do not trim lines, reflow paragraphs, remove trailing spaces as a side effect of broad cleanup, summarize wording, or rewrite license terms. If a source file has no final newline, add a separating newline before the next section so the last sentence and next heading do not run together. + +`THIRD_PARTY_NOTICES.md` currently contains Markdown links to repository-local license paths. Preserve their source/provenance meaning by explaining, immediately before the included notice, that repository-relative paths refer to the source checkout and the full license texts follow in this same document. Update that source document's package-description sentence to say releases carry one combined `LICENSES` file. This avoids telling readers that the old `licenses/` tree exists inside the new ZIP while retaining the repository's useful navigable references. + +Do not depend on release-page links or an internet connection to provide the actual license terms. Links and source revision identifiers supplement the copied texts; a player reading the extracted document offline should have all of the license wording already present. + +### Implementation within the existing release script + +Add a small `Write-CombinedLicenseFile` helper inside `create_release.ps1`. Its inputs are the repository root, the explicit ordered section records, and a destination path. Each section record contains a component title and repository-relative input filename. The function validates every input as a nonempty readable file, constructs the complete text in the manifest order, and writes one output. Do not create a new packaging framework, add a tool dependency, introduce a new build target, or download license texts at release time. + +Validate these inputs before building or creating either final ZIP so a missing document fails the release with a message naming its path. Treat a missing or empty required license/provenance input as an error rather than silently skipping it. Compare the source `licenses/` file inventory with the manifest: if a component notice is added or removed, packaging should report the mismatch and require the manifest to be updated deliberately. This prevents a future checked-in notice from being forgotten merely because the original list was hardcoded. + +Generate the combined file once at `build/package-staging/LICENSES`. Copy it into both `$StandaloneDirectory` and `$ReloadedDirectory` alongside the existing ASI and INI. Replace only the baseline loop's separate `LICENSE.md`/`THIRD_PARTY_NOTICES.md` copy and recursive `licenses/` copy. Keep the loader copy, metadata substitution, compression destinations, release-body generation, existing source-input validation, and staging cleanup behavior. + +Keep the helper's section order deterministic, using the explicit manifest rather than filesystem enumeration order. Do not insert a current timestamp, machine path, username, locale-dependent string, or dynamically fetched revision into the document. Repeated generation from the same source texts must produce the same `LICENSES` bytes. Reproducibility here concerns the document contents; ZIP container timestamps need not become a separate release project. + +Update `README.md` and the existing `THIRD_PARTY_NOTICES.md` introduction to distinguish source license files from the single document shipped in releases. Add a concise 1.4.0 changelog item explaining the archive cleanup. Add release-body wording where it helps describe the actual archive contents; retain the established extraction instructions and game directory. No ASI behavior, player setting, loader configuration, or dependency revision changes as part of this item. + +### Verification of generated contents and both archives + +Verify each generated section against its maintained input after only the declared encoding/newline normalization. Identify sections by their manifest records and unique headings, rather than checking only that the output contains the word `MIT` or that a filename exists. A present but incomplete file fails acceptance. Check that all thirteen source documents appear once in the intended order and that the final output is not empty. + +Add a focused package-content assertion in the same PowerShell script after both archives are created, using .NET's built-in ZIP reader. Open each ZIP, normalize its entry separators for comparison, and require exactly one file with the expected `LICENSES` path at the established package root. Check entry names case-insensitively for a `licenses` directory or descendant and for the separate top-level `LICENSE.md` and `THIRD_PARTY_NOTICES.md` documents; reject any of those legacy licensing outputs. Do not reject unrelated files merely because their names contain the substring `license`. + +Read the actual archived `LICENSES` entry and compare its bytes to the generated reference. Require the two package documents to match each other. In the same archive inspection, verify that the established ASI/INI entries retain their paths, that the standalone loader appears only in the standalone package, and that Reloaded-II retains the expected `ModConfig.json`. Preserve the script's existing build/loader-input checks as well. Close every ZIP stream and entry stream before the script performs staging cleanup or another package run. + +These assertions run as part of the existing packaging invocation, so the current build and tagged-release workflows exercise them without another CI platform or utility. A short diagnostic should identify which archive and which notice/entry failed. A validation failure must stop packaging successfully completing and prevent publishing through the existing workflow; it must not emit a reassuring success message for incomplete archives. + +For fault-path verification during implementation, test the document helper against a disposable source fixture with one required notice missing, one empty, and one unexpected file under the fixture's `licenses/` directory. These should fail with the relevant path and leave no accepted output. Include a source without a final newline and an input with an initial encoding marker to confirm separator/encoding handling. Keep fixtures in temporary build output; do not delete or edit real repository licenses merely to provoke an error. This is a small packaging-boundary check, not another C++ test framework. + +### Installation and upgrade behavior + +Fresh installation should extract one `LICENSES` document instead of the old licensing directory and separate documents. Compare actual ZIP inventories before and after implementation to show that the ASI, active INI, standalone loader, and Reloaded-II metadata retain their established package destinations. Validate extraction through both supported install methods. Licensing consolidation must not introduce an extra folder that changes where the game loads the mod. + +Extraction over an older installation does not remove files the older archive already created. Do not claim that an ordinary ZIP update automatically cleans an existing `licenses/` directory or old top-level notice files. The release note may state that older notice files can remain after an overwrite update and that the new combined file is the release's complete notice document. Do not introduce an installer, automatic deletion routine, or broad cleanup instruction that could affect licensing files belonging to another mod. + +Acceptance requires one complete readable `LICENSES` file in each archive, no separately packaged licensing tree/documents, full preservation of the maintained texts and attribution, matching documents in both formats, repeatable generation, and unchanged binary installation behavior. + +## Plan of Work and Milestones + +### Milestone 1: isolate and correct render-scaling decisions + +Implement `src/render/Scaling.hpp` and `src/render/Scaling.cpp`, wire the existing render-target callback to them, and create arithmetic/dimension tests with independent expected values. At the end of this milestone, an isolated Windows test executable should demonstrate the intended baseline and explicit multiplier behavior. + +Add a focused `fix-behavior-tests` target in the existing `xmake.lua`. This is a necessary test target in the current tooling, not a build-platform change. Compile the new helper into the ASI by listing its source under the existing target; the current `src/*.cpp` pattern does not recursively include subdirectories. + +### Milestone 2: make packet handling fail safely + +Implement the checked reader and wire it into `PeekMessageW_Injected`. Extend `tests/fix_behavior_tests.cpp` with the controlled Windows reader fixtures. Keep tests linked to the actual helper instead of duplicating its validation logic in a test implementation. + +At the end of this milestone, read failures and truncated payloads should be reproducible in tests with no camera update, while valid signed movement produces the expected delta. The existing game memory fixture target remains separate and unchanged in purpose. + +### Milestone 3: move the optional console wait + +Move the existing call in `Main`, add the concise installation marker, and review the resulting dependency order. Verify normal startup in game, then capture console-disabled, console-enabled, and unavailable/deferred-console evidence as described above. + +At the end of this milestone, independently installed features should no longer wait behind console discovery, and the confirmed working console should remain working. + +### Milestone 4: consolidate and verify the licensing payload + +Implement the ordered document generator and archive assertions inside `create_release.ps1`, update the source/release descriptions, and exercise the missing/empty/extra-input and encoding/separator checks with disposable fixtures. Run the packaging script and inspect both actual ZIPs. At the end of this milestone, each archive should contain exactly one complete `LICENSES` document with the same bytes and no legacy licensing directory or separate notice documents. + +Keep the source license texts intact and record the input manifest, document hash, and archive-entry evidence. Confirm the loader remains confined to the standalone package and extraction still places each supported package's actual mod files where its installation instructions expect them. + +### Milestone 5: prepare and validate the release + +Update the version fallback/default to 1.4.0, add a truthful `CHANGELOG.md` entry, and adjust build examples and release text where necessary. Preserve the existing Reloaded-II metadata substitution and pinned loader. + +Run the automated checks and live comparisons, then build both archives with the consolidated licensing checks enabled. Inspect their actual contents and validate installation through each supported method. An archive inspection proves package contents; it does not prove game compatibility. Record both kinds of evidence. + +## Concrete Steps + +Use a Windows x64 development environment with Visual Studio 2022 C++ build tools, Windows SDK, CMake, PowerShell 7.2 or newer, and the project's existing xmake. Run commands from the repository root. Initialize the already-pinned submodules if this checkout has not done so: + + git submodule update --init --recursive + +For debug checks after the new behavior target exists: + + xmake f -y -p windows -a x64 -m debug --fix_version=1.4.0 + xmake build -y unreal-integration-tests + xmake run unreal-integration-tests + xmake build -y fix-behavior-tests + xmake run fix-behavior-tests + python tools/unreal_inventory.py --phase minimal + +Repeat in release mode: + + xmake f -y -p windows -a x64 -m release --fix_version=1.4.0 + xmake build -y unreal-integration-tests + xmake run unreal-integration-tests + xmake build -y fix-behavior-tests + xmake run fix-behavior-tests + python tools/unreal_inventory.py --phase minimal + +Both test executables must return zero and report all checks passing. The existing Unreal test runner prints `[PASS]` or `[FAIL]` per case and ends with a passed/total count. Give the new runner the same straightforward result convention. Record the actual counts after implementation; no count is claimed by this plan. + +Extend the existing build workflow's debug/release validation step to run the new target too. Keep its platform, pinned actions, dependencies, permissions, packaging flow, and release handoff intact. + +Once code and release metadata are ready, build staged archives using the existing script: + + ./create_release.ps1 -Version 1.4.0 + +Expected release outputs are `build/P3RFix_1.4.0.zip`, `build/P3RFix_Reloaded-II.zip`, and `build/release_body.md`. The ASI is produced at `build/windows/x64/release/P3RFix.asi`. The script's staging directories are disposable build output; do not put user settings or validation captures there. + +The packaging invocation must also validate the combined licensing manifest and both ZIP contents before reporting success. For a separate manual inspection, use the existing PowerShell/.NET environment from the repository root: + + Add-Type -AssemblyName System.IO.Compression.FileSystem + foreach ($ArchivePath in @('build/P3RFix_1.4.0.zip', 'build/P3RFix_Reloaded-II.zip')) { + $Archive = [System.IO.Compression.ZipFile]::OpenRead((Resolve-Path $ArchivePath).Path) + try { + Write-Host "Contents of $ArchivePath" + $Archive.Entries | Select-Object FullName, Length + } finally { + $Archive.Dispose() + } + } + +Expect one nonempty `LICENSES` entry at each established package root and no `licenses/`, `LICENSE.md`, or `THIRD_PARTY_NOTICES.md` entry. Inspect the extracted document in a Windows text editor and confirm that the project, compiled-dependency, retained-provenance, and standalone-loader sections are readable. The automatic assertions prove section coverage and matching contents; this manual step proves the layout and text are understandable to a reader. Archive entry listings and checks described here are expected future verification, not already captured evidence. + +Actual publishing is a subsequent release operation after validation. Use the project's existing tag-driven workflow and a new 1.4.0 tag on the exact tested commit. Do not move an existing release tag or imply that writing this plan published a release. + +## Validation and Acceptance + +The maintenance release is acceptable when the three behavior fixes and the fourth licensing/package change are implemented, their scope remains narrow, the existing integration/inventory checks and new focused checks pass, both packages contain one complete `LICENSES` document, and the game shows no regression in the validated console or viewport behavior. + +For presentation, test 16:9 and at least one wide viewport at the same vertical resolution, below-1080p rendering, normal 1440p automatic scaling, and 2160p automatic scaling. Compare old/new menu targets and captures. Include borderless resizing and minimization so the unchanged viewport hook is exercised. + +For input, use mouse fix disabled as a baseline, then enabled with ordinary mouse movement and a controller present. Test device switching and opening/closing menus. Automated invalid-packet tests establish the safety improvement; do not pretend it is necessary or reliable to cause an actual bad Windows packet during play. + +For startup, test intro skipping and console access together. Keep log evidence showing independent installation before the optional console result. In Proton, validate the shipped Windows binary with the existing loader override and console setting. Do not revive the historical 1.2.4 crash as a current issue merely because it appears in an upstream thread. + +Where Steam and Xbox/MS Store executable variants are available, record which variant was actually tested. An untested variant remains untested; passing a Steam run does not imply a WinGDK run. The release can record unavailable coverage honestly, but must not replace it with an invented compatibility claim. + +For each live case, record the mod build/commit, game executable identifier, loader/install method, relevant settings, viewport size, action sequence, result, and a short log or visual comparison. Use a stable saved-game scene where possible so before/after presentation is comparable. + +For packaging, record both ZIP inventories, the consolidated document hash, and the thirteen-input coverage result. Demonstrate that omitted/empty/unlisted notice inputs fail the helper or package checks, that the final two documents match, and that the old licensing payload is absent. Fresh extraction should show the single document; an overwrite upgrade may retain old files from a previous install, as explicitly explained in Change 4. + +## Idempotence and Recovery + +Build and test steps may be repeated in the checkout. The package script recreates its own staging/output files. Preserve useful release artifacts and captured evidence outside that disposable staging directory before rerunning it. + +Before swapping a game installation's ASI or loader for validation, keep recoverable copies of the working baseline files and the player's INI. Restore that exact baseline if a regression appears. Do not overwrite or delete unrelated mods, game configuration, or saves. + +Each primary change should be independently reviewable and revertible. If render scaling fails live comparison, revert that helper/wiring while retaining independently verified packet safety and startup ordering. If a feature requires an unrelated redesign, move it out of this maintenance release rather than expanding its scope silently. + +License generation is repeatable and writes only to disposable package staging/output. If a license input fails validation, fix the manifest or maintained source text and regenerate the packages; do not bypass the check or hand-edit only one ZIP. If consolidation itself needs to be reverted, restore the old script copy operations with the matching documentation and keep the source license files intact. Do not ship a package without either the validated combined document or the previous complete notice payload. + +Do not add generated SDK files or new memory-layout fields for the three runtime fixes or for licensing consolidation. Existing integration tests already cover render writes and console boundaries. If an actual discovered requirement changes that assumption, document the evidence and revise this plan before implementing broader memory access. + +## Artifacts and Notes + +Keep implementation evidence with the release work: the final source diff, debug and release test output, inventory result, startup-order log excerpt, representative render-target sizes, actual-game comparison notes, the combined-license input manifest/hash, and both archive-content inspections. + +The following are expected example results, not captured output: + + [PASS] automatic scale retains native baseline on a wide 720p viewport + [PASS] explicit undersampling applies after the automatic baseline + [PASS] failed raw-input read produces no mouse delta + [PASS] truncated mouse payload is rejected + N/N checks passed + +An expected startup sequence is: + + Independent fix installation attempts completed. + Enable Console: Console object constructed. + Enable Console: Console enabled - access it using the reported key. + +Use the actual console log wording and key from the resulting build. The first marker proves order, while individual installation logs establish which features succeeded. + +An illustrative packaging result is: + + Combined licensing document: all 13 source documents included. + P3RFix_1.4.0.zip: one LICENSES document; no separate licensing directory/documents. + P3RFix_Reloaded-II.zip: one LICENSES document; no separate licensing directory/documents. + Both packaged LICENSES documents match the generated reference. + +This transcript is an expected result, not captured output. Use the implemented script's actual wording and output hashes in the release evidence. + +Draft release-note content should describe the corrected vertical-quality baseline, checked raw-input reads, earlier independent installation, and one combined `LICENSES` document replacing the old licensing folder/separate documents in release ZIPs. Do not include unreleased 1.5.0 controls, claim the old console crash was repaired here, or describe the viewport fix as new in 1.4.0. + +## Interfaces and Dependencies + +Keep using the existing `p3r::unreal::ReadRenderTarget`, `SetRenderTargetSize`, `SetCaptureSize`, and console interface. No generated SDK or additional library is needed. + +The proposed new sources are `src/render/Scaling.hpp`, `src/render/Scaling.cpp`, `src/input/RawMousePacket.hpp`, `src/input/RawMousePacket.cpp`, and `tests/fix_behavior_tests.cpp`. Add them explicitly to the appropriate existing xmake targets. The behavior tests may link the already-used Windows `user32` library for the SDK/API boundary; they should not depend on the game, hook installation, or Zydis. + +Use the current C++ standard setting and MSVC ABI. ABI means the binary rules for calling functions and laying out SDK structures. In particular, keep the correct Windows calling convention for the injected reader and use the Windows SDK's own `RAWINPUTHEADER` and `RAWMOUSE` definitions. + +The file names and signatures above are an implementation proposal, not files already created by this documentation task. If a smaller equivalent organization fits the code better during implementation, record that decision and update every affected command and interface description rather than leaving stale names in the plan. + +Licensing consolidation uses only the existing PowerShell 7.2-or-newer release environment and built-in .NET text/ZIP support. The ordered generator and archive-content assertion live in `create_release.ps1`; no new external dependency, compiler, source target, or runtime DLL is required. `LICENSE.md`, `THIRD_PARTY_NOTICES.md`, and all eleven source component-license files remain the maintained inputs. `LICENSES` is generated package content, not another independent source-of-truth file to edit by hand. + +## Outcomes & Retrospective + +As of this revision, the outcome is a detailed targeted 1.4.0 plan preserving the original three maintenance proposals and adding the maintainer's requested single-file release licensing as Change 4. The old `pre-v2-fixes.md` filename is superseded by this document. No ASI code, build target, test target, source license text, release script, package, or published version has been changed by the documentation work. + +Implementation, automated Windows checks, game comparisons, packaging, and publication remain outstanding. Once those occur, replace this paragraph with the actual results and retained limitations. In particular, separate confirmed source arithmetic, simulated API failures, and real gameplay evidence. + +Revision note, 2026-10-01: renamed the original pre-v2 notes for a 1.4.0 release and expanded them into a self-contained maintenance plan at the maintainer's request. The scaling policy, failure handling, startup ordering, implementation boundaries, validation, and release criteria are now explicit; the established console, viewport, packaging, and MSVC constraints are retained. + +Revision note, 2026-10-01: added the maintainer's fourth 1.4.0 change, replacing the release licensing folder and separate license/notice files with one generated `LICENSES` document in both existing package formats. Expanded the scope, decisions, milestones, commands, archive/content validation, upgrade behavior, recovery, and release criteria to cover complete notice preservation. Source licensing files and build tooling remain unchanged; the separate 1.5.0 plan's baseline references now inherit this packaging contract. From 817f9d2e6001293d48ca9f353da0e2857d6c8a6e Mon Sep 17 00:00:00 2001 From: dev-camo Date: Thu, 1 Oct 2026 14:48:00 -0600 Subject: [PATCH 2/5] Fix scaling, raw input, and startup ordering --- .agents/PLAN.md | 23 ++- .github/workflows/build.yml | 6 +- src/dllmain.cpp | 103 ++++++------ src/input/RawMousePacket.cpp | 92 +++++++++++ src/input/RawMousePacket.hpp | 45 ++++++ src/render/Scaling.cpp | 64 ++++++++ src/render/Scaling.hpp | 32 ++++ tests/fix_behavior_tests.cpp | 43 +++++ tests/raw_mouse_packet_tests.hpp | 262 +++++++++++++++++++++++++++++++ tests/render_scaling_tests.hpp | 217 +++++++++++++++++++++++++ xmake.lua | 21 ++- 11 files changed, 842 insertions(+), 66 deletions(-) create mode 100644 src/input/RawMousePacket.cpp create mode 100644 src/input/RawMousePacket.hpp create mode 100644 src/render/Scaling.cpp create mode 100644 src/render/Scaling.hpp create mode 100644 tests/fix_behavior_tests.cpp create mode 100644 tests/raw_mouse_packet_tests.hpp create mode 100644 tests/render_scaling_tests.hpp diff --git a/.agents/PLAN.md b/.agents/PLAN.md index 6d1a476..862eb01 100644 --- a/.agents/PLAN.md +++ b/.agents/PLAN.md @@ -34,12 +34,13 @@ Separate gameplay/menu/background frame-rate limits, new HUD/FOV controls, live - [x] (2026-10-01) Renamed and expanded the former `pre-v2-fixes.md` notes into a targeted 1.4.0 release plan. - [x] (2026-10-01) Added the maintainer's fourth release change: one generated `LICENSES` document in each ZIP, preserving existing notices and the source license files. - [x] (2026-10-01) Read the entire plan, repository instructions, and ExecPlan skill; inspected the baseline source and submodule pins. Assigned independent scaling, packet-reader, and licensing implementation work while integrating the hooks and Windows validation centrally. -- [ ] Implement and test the automatic render-target scaling policy. -- [ ] Implement and test checked raw-input reading without changing camera interpretation. -- [ ] Adjust startup ordering and verify console-enabled and console-disabled startup. -- [ ] Generate the complete combined `LICENSES` file in the existing PowerShell release script and stop copying separate licensing files/directories into either package. +- [x] (2026-10-01) Implemented height-based scaling and checked dimension conversion; Windows debug/release behavior tests cover native retention, explicit undersampling, invalid inputs, truncation, and integer boundaries. +- [x] (2026-10-01) Implemented checked raw-input reading, an allocation-failure seam, and shared delta application. Independent literal Windows x64 fixtures cover two-call failures, malformed/truncated packets, signed motion, ignored flags, and unchanged accumulators/ownership on rejection. +- [x] (2026-10-01) Moved the unchanged console initialization call after all independent installers and added the attempt-complete marker. Remaining: development harness and real startup/console observations. +- [ ] Verify console-enabled, disabled, unavailable, and delayed startup with the development harness and maintainer game tests. +- [x] (2026-10-01) Implemented the thirteen-input `LICENSES` generator and archive assertions in the existing release script. Remaining: fault fixtures and real package validation. - [ ] Validate license completeness, repeatable generation, and the contents of both final ZIPs; update source and release documentation to describe the new package contract. -- [ ] Run existing Unreal integration tests and the source inventory in Windows x64 debug and release configurations. +- [x] (2026-10-01) Built the ASI with VS2022/MSVC 14.44.35207 on the authorized VM in debug and release. In each configuration: Unreal 69/69, behavior 35/35, minimal inventory PASS. Behavior sources compile with `/W4 /WX`. - [ ] Compare menu rendering, input, console operation, intro skipping, and ultrawide behavior in the game. - [ ] Prepare 1.4.0 metadata, notes, and both existing package formats. - [ ] Record actual validation evidence and release outcome. @@ -58,6 +59,10 @@ The current packaging loop in `create_release.ps1` copies `LICENSE.md`, `THIRD_P The Windows VM at `camo@192.168.122.141` accepts SSH and uses a Windows command shell. Tool locations and a dedicated validation checkout are being established. Game execution is expressly reserved for the maintainer; automated validation must stop at a concrete package and test handoff. +The validation tools from the prior 1.3.0 work are available at `C:\Users\camo\Projects\P3RFix-sdk-refactor\tools`: portable PowerShell, xmake 3.1.1, Python 3.13.7, and Git. CMake is bundled with `C:\BuildTools2022`, and MSVC is 14.44.35207. This release uses a separate checkout at `C:\Users\camo\Projects\P3RFix-1.4.0\source`, with transcripts under its sibling `evidence` directory. The earlier baseline/refactor checkouts are preserved. + +The first behavior-test build exposed Windows `min`/`max` macros after including the raw-input SDK header. Defining `NOMINMAX` in the test runner, as the production precompiled header already does, fixed the conflict. Both complete Windows configurations subsequently passed. SSH command length also limits large encoded PowerShell scripts; the handoff helper now uploads scripts and executes `-File` instead. + ## Decision Log Decision: target the original three behavior fixes and the subsequently requested licensing cleanup at 1.4.0 rather than a pre-v2 backlog. Rationale: the maintainer wants a concrete maintenance release and separate treatment from the 1.5.0 feature proposals. Date/author: 2026-10-01, maintainer direction recorded by Codex. @@ -76,6 +81,8 @@ Decision: generate `LICENSES` from the existing checked-in license texts and pro Decision: reserve actual game runs and install-method compatibility confirmation for the maintainer, and retain this plan until that confirmation arrives. Rationale: the execution instructions prohibit the agent from running the game and require user validation before deleting the plan or completing/pushing the branch. Prepare instructions, packages, and a result form under `/tmp/P3RFix/`. Date/author: 2026-10-01, maintainer instruction recorded by Codex. +Decision: expose a small nothrow raw-input allocator seam and delta-application helper, with a separate fixture header alongside the scaling cases. Rationale: allocation failure can be proven without exhausting memory, and tests exercise the shipped accumulator behavior rather than duplicating it. Date/author: 2026-10-01, Codex. + ## Context and Orientation The repository examined is `/home/camo/Projects/P3RFix`. On a Windows build machine, use that machine's checkout path rather than attempting to use this Linux path. The source produces a Windows ASI: a DLL with an `.asi` filename loaded into the game by the standalone loader or Reloaded-II. @@ -512,10 +519,12 @@ Licensing consolidation uses only the existing PowerShell 7.2-or-newer release e ## Outcomes & Retrospective -As of this revision, the outcome is a detailed targeted 1.4.0 plan preserving the original three maintenance proposals and adding the maintainer's requested single-file release licensing as Change 4. The old `pre-v2-fixes.md` filename is superseded by this document. No ASI code, build target, test target, source license text, release script, package, or published version has been changed by the documentation work. +The three runtime changes are implemented, version defaults target 1.4.0, and both Windows configurations build the ASI and pass Unreal 69/69, behavior 35/35, and the minimal inventory. Those results prove synthetic boundaries and arithmetic, not gameplay compatibility. The licensing generator and ZIP assertions are implemented; fault fixtures and real archives remain under validation. README, repository instructions, changelog, and release text describe the new behavior and package contract; the changelog truthfully marks 1.4.0 unreleased. -Implementation, automated Windows checks, game comparisons, packaging, and publication remain outstanding. Once those occur, replace this paragraph with the actual results and retained limitations. In particular, separate confirmed source arithmetic, simulated API failures, and real gameplay evidence. +Resume from the current branch and inspect the worktree. Local validation scripts and console output are under `/tmp/P3RFix/`; `remote.py` uploads a PowerShell script to the dedicated VM directory and runs it. Next: complete `license-fixtures.ps1`, run a development-only harness using the actual `Main`/console/hook bodies, package through `create_release.ps1`, retrieve and independently inspect both ZIPs, record hashes/transcripts, and write the maintainer game-test form. Do not run the game, delete this plan, push the branch, or claim completion before the required maintainer validation arrives. No release tag or publication has been attempted. Revision note, 2026-10-01: renamed the original pre-v2 notes for a 1.4.0 release and expanded them into a self-contained maintenance plan at the maintainer's request. The scaling policy, failure handling, startup ordering, implementation boundaries, validation, and release criteria are now explicit; the established console, viewport, packaging, and MSVC constraints are retained. Revision note, 2026-10-01: added the maintainer's fourth 1.4.0 change, replacing the release licensing folder and separate license/notice files with one generated `LICENSES` document in both existing package formats. Expanded the scope, decisions, milestones, commands, archive/content validation, upgrade behavior, recovery, and release criteria to cover complete notice preservation. Source licensing files and build tooling remain unchanged; the separate 1.5.0 plan's baseline references now inherit this packaging contract. + +Revision note, 2026-10-01: recorded the recovered implementation state, completed Windows validation, test seams, and dedicated VM/tool paths. Split implementation from outstanding live validation so future continuations cannot mistake compilation or synthetic tests for confirmed gameplay. diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 7b14256..d303357 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -75,7 +75,7 @@ jobs: python tools/unreal_inventory.py --phase minimal if ($LASTEXITCODE -ne 0) { throw 'Unreal source inventory failed.' } - - name: Test Unreal integration in debug and release + - name: Test Unreal integration and fix behavior in debug and release env: BUILD_VERSION: ${{ steps.metadata.outputs.version }} run: | @@ -86,6 +86,10 @@ jobs: if ($LASTEXITCODE -ne 0) { throw "Unreal $Mode test build failed." } & xmake run unreal-integration-tests if ($LASTEXITCODE -ne 0) { throw "Unreal $Mode tests failed." } + & xmake build -y -v fix-behavior-tests + if ($LASTEXITCODE -ne 0) { throw "Fix behavior $Mode test build failed." } + & xmake run fix-behavior-tests + if ($LASTEXITCODE -ne 0) { throw "Fix behavior $Mode tests failed." } } - name: Build and package Windows x64 release diff --git a/src/dllmain.cpp b/src/dllmain.cpp index 717543c..894243b 100644 --- a/src/dllmain.cpp +++ b/src/dllmain.cpp @@ -7,6 +7,8 @@ #include #include "unreal/Integration.hpp" +#include "input/RawMousePacket.hpp" +#include "render/Scaling.hpp" #include #include @@ -20,7 +22,7 @@ HMODULE thisModule; // Fix details #ifndef P3RFIX_VERSION -#define P3RFIX_VERSION "1.3.0" +#define P3RFIX_VERSION "1.4.0" #endif std::string sFixName = "P3RFix"; std::string sFixVersion = P3RFIX_VERSION; @@ -74,7 +76,6 @@ int iCurrentResX = 0; int iCurrentResY = 0; bool bIntroSkipHasRun = false; int iFadeStatus = 0; -float fRenTexResMulti = 1.0f; int iRTCapX = 1920; int iRTCapY = 1080; float fRawMouseX = 0.0f; @@ -375,34 +376,31 @@ void* UTextureRenderTarget2D_PostLoad_hk(void* pRenderTarget) if (!target) return UTextureRenderTarget2D_PostLoad_fn.stdcall(pRenderTarget); - // Calculate optimal resolution multiplier assuming target is 1080p - // Screen percentage is only retrieved when the hook is run, meaning that on first boot we have to assume it is 100% - float fOptimalRenTexResMulti = (iCurrentResY * (fScreenPercentage / 100)) / 1080; - - if (iCurrentResX <= 1920 || iCurrentResX <= 1080) - fOptimalRenTexResMulti = 1.0f; // Avoid lowering resolution of render targets when resolution is <1080p. - - if (fRenTexResUserMulti == 1.0f) - fRenTexResMulti = fOptimalRenTexResMulti; // If set to 1, use the calculated optimal multiplier. - else - fRenTexResMulti = fOptimalRenTexResMulti * fRenTexResUserMulti; // If not set to 1, then multiply on top using user defined value. - - if (fRenTexResMulti < 0.25f || fRenTexResMulti > 4.0f) { - fRenTexResMulti = std::clamp(fRenTexResMulti, (float)0.25, (float)4); - spdlog::warn("Render Texture 2D Resolution: fRenTexResMulti value invalid, clamped to {}", fRenTexResMulti); + // Keep the automatic baseline at native quality, then apply the user's multiplier. + const auto scale = p3r::render::ComputeRenderTargetScale( + iCurrentResY, fScreenPercentage, fRenTexResUserMulti); + static bool bLoggedScaleFallback = false; + if (scale.usedFallback && !bLoggedScaleFallback) { + spdlog::warn("Render Texture 2D Resolution: Using safe scaling defaults for height {}, screen percentage {}, multiplier {}. Further fallback warnings are suppressed.", + iCurrentResY, fScreenPercentage, fRenTexResUserMulti); + bLoggedScaleFallback = true; } - spdlog::info("Render Texture 2D Resolution: fRenTexResMulti = {}", fRenTexResMulti); + spdlog::info("Render Texture 2D Resolution: fRenTexResMulti = {}", scale.finalMultiplier); spdlog::info("Render Texture 2D Resolution: Old render texture resolution = {}x{}", target->width, target->height); - const auto width = static_cast(target->width * fRenTexResMulti); - const auto height = static_cast(target->height * fRenTexResMulti); - if (p3r::unreal::SetRenderTargetSize(pRenderTarget, width, height)) { + const auto dimensions = p3r::render::ScaleDimensions( + target->width, target->height, scale.finalMultiplier); + if (!dimensions) { + spdlog::warn("Render Texture 2D Resolution: Invalid or overflowing dimensions; leaving the target unchanged."); + } + else if (p3r::unreal::SetRenderTargetSize(pRenderTarget, dimensions->width, dimensions->height)) { if (target->isRgba16f) { - iRTCapX = width; - iRTCapY = height; + iRTCapX = dimensions->width; + iRTCapY = dimensions->height; } - spdlog::info("Render Texture 2D Resolution: New render texture resolution = {}x{}", width, height); + spdlog::info("Render Texture 2D Resolution: New render texture resolution = {}x{}", + dimensions->width, dimensions->height); } // Forward to the original function once, including unavailable targets. @@ -844,40 +842,32 @@ BOOL __stdcall RegisterRawInputDevices_Injected(PCRAWINPUTDEVICE pRawInputDevice } static SafetyHookInline PeekMessageWHook{}; -UINT __stdcall PeekMessageW_Injected(LPMSG lpMsg, HWND hWnd, UINT wMsgFilterMin, UINT wMsgFilterMax, UINT wRemoveMsg) { - // Alrighty boys, we need to basically write our own input handling so buckle up +UINT __stdcall PeekMessageW_Injected(LPMSG lpMsg, HWND hWnd, UINT wMsgFilterMin, UINT wMsgFilterMax, UINT wRemoveMsg) +{ + // Observe only messages removed by the game; peeking must not accumulate motion twice. auto output = PeekMessageWHook.call(lpMsg, hWnd, wMsgFilterMin, wMsgFilterMax, wRemoveMsg); - if (output && wRemoveMsg & 0b1) { - // Only read (valid) messages on consumption so we don't accidentally double-process anything - // Game seems to process raw mouse input despite unregistering it (thank God) so we don't have to fight it for messages - if (lpMsg->message == WM_INPUT) { - // We've just received an input message. - UINT dwSize = 0; - GetRawInputData((HRAWINPUT)lpMsg->lParam, RID_INPUT, NULL, &dwSize, sizeof(RAWINPUTHEADER)); //Get size of rawinput structure - - LPBYTE lpb = new BYTE[dwSize]; - if (lpb == NULL) - return output; // No Raw input data, abort current message injection - - if (GetRawInputData((HRAWINPUT)lpMsg->lParam, RID_INPUT, lpb, &dwSize, sizeof(RAWINPUTHEADER)) != dwSize) //Actually retrieve raw input data into lpb - spdlog::debug("Win32 API Error: GetRawInputData does not return correct size!"); //Doubt this'll happen but might as well log it - - RAWINPUT* raw = (RAWINPUT*)lpb; - if (raw->header.dwType == RIM_TYPEMOUSE) - { - // We got the good stuff, mouse input data! - if (!raw->data.mouse.usFlags) { - if (raw->data.mouse.lLastX || raw->data.mouse.lLastY) { - bLastValidInputWasFromMouse = true; - } - // These numbers were completely just eyeballed. TODO: if you're bored you can find more "accurate" numbers - fRawMouseX += raw->data.mouse.lLastX / 1200.0f; - fRawMouseY += raw->data.mouse.lLastY / 1200.0f; - } - } - delete[] lpb; + if (!output || !(wRemoveMsg & PM_REMOVE) || !lpMsg || lpMsg->message != WM_INPUT) + return output; + + const auto packet = p3r::input::ReadRelativeMousePacket( + reinterpret_cast(lpMsg->lParam), GetRawInputData); + if (packet.status == p3r::input::PacketStatus::Invalid) { + static bool bLoggedInvalidPacket = false; + if (!bLoggedInvalidPacket) { + spdlog::warn("Mouse Fix: Ignoring invalid or incomplete raw-input data. Further invalid-packet warnings are suppressed."); + bLoggedInvalidPacket = true; + } + } + else if (packet.status == p3r::input::PacketStatus::AllocationFailed) { + static bool bLoggedAllocationFailure = false; + if (!bLoggedAllocationFailure) { + spdlog::warn("Mouse Fix: Unable to allocate raw-input storage. Further allocation warnings are suppressed."); + bLoggedAllocationFailure = true; } } + + p3r::input::ApplyRelativeMousePacket( + packet, fRawMouseX, fRawMouseY, bLastValidInputWasFromMouse); return output; } @@ -1003,13 +993,14 @@ DWORD __stdcall Main(void*) InitializeUnrealIntegration(); Resolution(); RenderTextures(); - EnableConsole(); IntroSkip(); AspectRatioFOV(); HUDFix(); Framerate(); WindowFocus(); MouseFix(); + spdlog::info("Independent fix installation attempts completed."); + EnableConsole(); return true; } diff --git a/src/input/RawMousePacket.cpp b/src/input/RawMousePacket.cpp new file mode 100644 index 0000000..0f54716 --- /dev/null +++ b/src/input/RawMousePacket.cpp @@ -0,0 +1,92 @@ +#include "RawMousePacket.hpp" + +#include +#include +#include + +namespace p3r::input +{ +namespace +{ +constexpr UINT readError = static_cast(-1); + +RawInputBuffer AllocateBuffer(std::size_t wordCount) noexcept +{ + return RawInputBuffer(new (std::nothrow) std::uint32_t[wordCount]); +} + +PacketResult NoMovement(PacketStatus status) noexcept +{ + return {status, {0, 0}}; +} +} // namespace + +PacketResult ReadRelativeMousePacket(HRAWINPUT input, RawInputReader reader, + RawInputAllocator allocator) noexcept +{ + if (input == nullptr || reader == nullptr) + return NoMovement(PacketStatus::Invalid); + + UINT capacity = 0; + const UINT queryResult = reader(input, RID_INPUT, nullptr, &capacity, sizeof(RAWINPUTHEADER)); + if (queryResult != 0 || capacity < sizeof(RAWINPUTHEADER)) + return NoMovement(PacketStatus::Invalid); + + // Round up for DWORD alignment without allowing the allocation arithmetic + // to overflow. The API still receives the original capacity in bytes. + constexpr std::size_t wordSize = sizeof(std::uint32_t); + const std::size_t byteCapacity = capacity; + if (byteCapacity > (std::numeric_limits::max)() - (wordSize - 1)) + return NoMovement(PacketStatus::Invalid); + + const std::size_t wordCount = (byteCapacity + wordSize - 1) / wordSize; + RawInputBuffer buffer = (allocator != nullptr ? allocator : &AllocateBuffer)(wordCount); + if (!buffer) + return NoMovement(PacketStatus::AllocationFailed); + + UINT readSize = capacity; + const UINT copiedCount = reader(input, RID_INPUT, buffer.get(), &readSize, sizeof(RAWINPUTHEADER)); + if (copiedCount == readError || copiedCount != capacity || readSize != capacity) + return NoMovement(PacketStatus::Invalid); + + // Copy into SDK objects after checking the available bytes. DWORD alignment + // alone does not imply the stronger alignment of the x64 packet header. + RAWINPUTHEADER header{}; + std::memcpy(&header, buffer.get(), sizeof(header)); + if (header.dwSize < sizeof(header) || header.dwSize != copiedCount) + return NoMovement(PacketStatus::Invalid); + + if (header.dwType != RIM_TYPEMOUSE) + return NoMovement(PacketStatus::Ignored); + + constexpr std::size_t mouseOffset = offsetof(RAWINPUT, data); + constexpr std::size_t mousePacketSize = mouseOffset + sizeof(RAWMOUSE); + if (copiedCount < mousePacketSize) + return NoMovement(PacketStatus::Invalid); + + RAWMOUSE mouse{}; + const auto* bytes = reinterpret_cast(buffer.get()); + std::memcpy(&mouse, bytes + mouseOffset, sizeof(mouse)); + + // Retain the current fix's exact flag selection. Other relative flag + // combinations need a separate camera-behavior change and validation. + if (mouse.usFlags != 0) + return NoMovement(PacketStatus::Ignored); + + return {PacketStatus::RelativeMouse, {mouse.lLastX, mouse.lLastY}}; +} + +void ApplyRelativeMousePacket(const PacketResult& packet, float& accumulatedX, float& accumulatedY, + bool& lastValidInputWasFromMouse) noexcept +{ + if (packet.status != PacketStatus::RelativeMouse) + return; + + if (packet.delta.x != 0 || packet.delta.y != 0) + lastValidInputWasFromMouse = true; + + // Preserve the existing mouse fix's sensitivity and signed movement. + accumulatedX += static_cast(packet.delta.x) / 1200.0f; + accumulatedY += static_cast(packet.delta.y) / 1200.0f; +} +} // namespace p3r::input diff --git a/src/input/RawMousePacket.hpp b/src/input/RawMousePacket.hpp new file mode 100644 index 0000000..2b0db79 --- /dev/null +++ b/src/input/RawMousePacket.hpp @@ -0,0 +1,45 @@ +#pragma once + +#include +#include +#include + +#include + +namespace p3r::input +{ +using RawInputReader = UINT(WINAPI*)(HRAWINPUT, UINT, LPVOID, PUINT, UINT); + +struct MouseDelta +{ + LONG x; + LONG y; +}; + +enum class PacketStatus +{ + RelativeMouse, + Ignored, + Invalid, + AllocationFailed +}; + +struct PacketResult +{ + PacketStatus status; + MouseDelta delta; +}; + +// DWORD-aligned storage satisfies GetRawInputData's buffer alignment contract. +// Tests may supply a failing allocator without exhausting system memory. +using RawInputBuffer = std::unique_ptr; +using RawInputAllocator = RawInputBuffer (*)(std::size_t wordCount) noexcept; + +PacketResult ReadRelativeMousePacket(HRAWINPUT input, RawInputReader reader, + RawInputAllocator allocator = nullptr) noexcept; + +// The message hook calls this only for removed WM_INPUT messages. Keeping this +// small operation shared lets tests verify the existing camera interpretation. +void ApplyRelativeMousePacket(const PacketResult& packet, float& accumulatedX, float& accumulatedY, + bool& lastValidInputWasFromMouse) noexcept; +} // namespace p3r::input diff --git a/src/render/Scaling.cpp b/src/render/Scaling.cpp new file mode 100644 index 0000000..02ce740 --- /dev/null +++ b/src/render/Scaling.cpp @@ -0,0 +1,64 @@ +#include "Scaling.hpp" + +#include +#include +#include + +namespace p3r::render +{ +ScaleResult ComputeRenderTargetScale(int viewportHeight, float screenPercentage, + float userMultiplier) noexcept +{ + bool usedFallback = viewportHeight <= 0; + + if (!std::isfinite(screenPercentage) || screenPercentage <= 0.0f) + { + screenPercentage = 100.0f; + usedFallback = true; + } + + if (!std::isfinite(userMultiplier) || userMultiplier <= 0.0f) + { + userMultiplier = 1.0f; + usedFallback = true; + } + + double automaticMultiplier = 1.0; + if (viewportHeight > 0) + { + const double effectiveHeight = static_cast(viewportHeight) * screenPercentage / 100.0; + automaticMultiplier = std::max(1.0, effectiveHeight / 1080.0); + } + + // Apply limits to the combined value, not the automatic baseline. A high + // baseline and deliberate undersampling must keep their relative meaning. + const double combinedMultiplier = automaticMultiplier * userMultiplier; + const float finalMultiplier = static_cast(std::clamp(combinedMultiplier, 0.25, 4.0)); + + return {automaticMultiplier, finalMultiplier, usedFallback}; +} + +std::optional ScaleDimensions(std::int32_t width, std::int32_t height, + float multiplier) noexcept +{ + if (width <= 0 || height <= 0 || !std::isfinite(multiplier) || multiplier <= 0.0f) + { + return std::nullopt; + } + + const double scaledWidth = static_cast(width) * multiplier; + const double scaledHeight = static_cast(height) * multiplier; + constexpr double maximumDimension = static_cast(std::numeric_limits::max()); + + // Check floating-point bounds before either cast: casting an out-of-range + // value to an integer is unsafe even when a later clamp would discard it. + if (!std::isfinite(scaledWidth) || !std::isfinite(scaledHeight) || scaledWidth > maximumDimension || + scaledHeight > maximumDimension) + { + return std::nullopt; + } + + return ScaledDimensions{static_cast(std::max(1.0, scaledWidth)), + static_cast(std::max(1.0, scaledHeight))}; +} +} // namespace p3r::render diff --git a/src/render/Scaling.hpp b/src/render/Scaling.hpp new file mode 100644 index 0000000..bfc267f --- /dev/null +++ b/src/render/Scaling.hpp @@ -0,0 +1,32 @@ +#pragma once + +#include +#include + +namespace p3r::render +{ +struct ScaleResult +{ + // The automatic baseline can exceed the float range for extreme inputs. + // Keep it wide so diagnostics describe the value used before final clamping. + double automaticMultiplier; + float finalMultiplier; + bool usedFallback; +}; + +// Automatic scaling preserves native quality below 1080 effective vertical +// pixels. An explicit user multiplier applies afterward, before final limits. +ScaleResult ComputeRenderTargetScale(int viewportHeight, float screenPercentage, + float userMultiplier) noexcept; + +struct ScaledDimensions +{ + std::int32_t width; + std::int32_t height; +}; + +// Reject invalid or overflowing dimensions without producing a memory-write +// value. Valid positive products truncate toward zero, with a one-pixel minimum. +std::optional ScaleDimensions(std::int32_t width, std::int32_t height, + float multiplier) noexcept; +} // namespace p3r::render diff --git a/tests/fix_behavior_tests.cpp b/tests/fix_behavior_tests.cpp new file mode 100644 index 0000000..01a6cf5 --- /dev/null +++ b/tests/fix_behavior_tests.cpp @@ -0,0 +1,43 @@ +// Test the shipped helpers with independent arithmetic answers and Windows packets. +#define NOMINMAX + +#include +#include +#include +#include + +namespace +{ +void Require(bool condition, const char* explanation) +{ + if (!condition) + throw std::runtime_error(explanation); +} +} // namespace + +#include "raw_mouse_packet_tests.hpp" +#include "render_scaling_tests.hpp" + +int main() +{ + int passed = 0; + int total = 0; + const auto runTest = [&](const char* name, const std::function& test) { + ++total; + try + { + test(); + ++passed; + std::cout << "[PASS] " << name << '\n'; + } + catch (const std::exception& error) + { + std::cerr << "[FAIL] " << name << ": " << error.what() << '\n'; + } + }; + + RunRenderScalingTests(runTest); + RunRawMousePacketTests(runTest); + std::cout << passed << '/' << total << " checks passed\n"; + return passed == total ? 0 : 1; +} diff --git a/tests/raw_mouse_packet_tests.hpp b/tests/raw_mouse_packet_tests.hpp new file mode 100644 index 0000000..17adae3 --- /dev/null +++ b/tests/raw_mouse_packet_tests.hpp @@ -0,0 +1,262 @@ +#pragma once + +#include "input/RawMousePacket.hpp" + +#include +#include +#include +#include +#include +#include + +namespace raw_mouse_tests +{ +using namespace p3r::input; + +// These literal offsets are the independently reviewed Windows x64 ABI: +// RAWINPUTHEADER is 24 bytes, followed by a 24-byte RAWMOUSE payload. +constexpr UINT headerSize = 0x18; +constexpr UINT mouseSize = 0x30; +constexpr UINT readError = static_cast(-1); + +struct ReaderFixture +{ + std::array bytes{}; + UINT queryResult = 0; + UINT capacity = mouseSize; + UINT copiedCount = mouseSize; + UINT returnedSize = mouseSize; + int calls = 0; + bool argumentsValid = true; + + template + void Write(std::size_t offset, const T& value) + { + std::memcpy(bytes.data() + offset, &value, sizeof(value)); + } + + ReaderFixture(LONG x = 1200, LONG y = -2400) + { + Write(0x00, static_cast(RIM_TYPEMOUSE)); + Write(0x04, static_cast(mouseSize)); + Write(0x18, static_cast(0)); + Write(0x24, x); + Write(0x28, y); + } +}; + +inline ReaderFixture* activeFixture = nullptr; + +HRAWINPUT FixtureHandle() +{ + return reinterpret_cast(static_cast(0x1234)); +} + +UINT WINAPI FixtureReader(HRAWINPUT input, UINT command, LPVOID data, PUINT size, UINT sizeHeader) +{ + auto& fixture = *activeFixture; + ++fixture.calls; + fixture.argumentsValid &= + input == FixtureHandle() && command == RID_INPUT && size != nullptr && sizeHeader == headerSize; + if (data == nullptr) + { + fixture.argumentsValid &= fixture.calls == 1 && *size == 0; + *size = fixture.capacity; + return fixture.queryResult; + } + + fixture.argumentsValid &= + fixture.calls == 2 && *size == fixture.capacity && reinterpret_cast(data) % 4 == 0; + const std::size_t available = std::min(fixture.capacity, fixture.bytes.size()); + const std::size_t written = + fixture.copiedCount == readError ? 0 : std::min(available, fixture.copiedCount); + std::memcpy(data, fixture.bytes.data(), written); + *size = fixture.returnedSize; + return fixture.copiedCount; +} + +PacketResult Read(ReaderFixture& fixture, RawInputAllocator allocator = nullptr) +{ + activeFixture = &fixture; + const auto result = ReadRelativeMousePacket(FixtureHandle(), FixtureReader, allocator); + activeFixture = nullptr; + Require(fixture.argumentsValid, "reader receives the correct two-call Windows contract"); + return result; +} + +void RequireNoMovement(const PacketResult& packet, PacketStatus expected) +{ + Require(packet.status == expected, "packet has expected failure or ignored status"); + Require(packet.delta.x == 0 && packet.delta.y == 0, "unaccepted packet has no delta"); + for (const bool initialMouseActivity : {false, true}) + { + float x = 2.0f; + float y = -3.0f; + bool mouseActivity = initialMouseActivity; + ApplyRelativeMousePacket(packet, x, y, mouseActivity); + Require(x == 2.0f && y == -3.0f, "unaccepted packet leaves accumulated motion unchanged"); + Require(mouseActivity == initialMouseActivity, "unaccepted packet preserves input ownership"); + } +} + +RawInputBuffer FailAllocation(std::size_t) noexcept +{ + return {}; +} +} // namespace raw_mouse_tests + +void RunRawMousePacketTests(const std::function&)>& runTest) +{ + using namespace raw_mouse_tests; + + runTest("successful two-call mouse read preserves signed movement and sensitivity", [] { + ReaderFixture fixture; + const auto packet = Read(fixture); + Require(fixture.calls == 2, "one query and one read"); + Require(packet.status == PacketStatus::RelativeMouse, "valid relative mouse packet"); + Require(packet.delta.x == 1200 && packet.delta.y == -2400, "signed deltas preserved"); + float x = 0.5f; + float y = 0.25f; + bool mouseActivity = false; + ApplyRelativeMousePacket(packet, x, y, mouseActivity); + Require(x == 1.5f && y == -1.75f, "valid delta is accumulated once using the existing divisor"); + Require(mouseActivity, "nonzero relative motion activates the mouse"); + }); + + runTest("zero motion preserves existing input ownership", [] { + ReaderFixture fixture(0, 0); + const auto packet = Read(fixture); + Require(packet.status == PacketStatus::RelativeMouse, "zero-motion packet remains valid"); + for (const bool initialMouseActivity : {false, true}) + { + float x = 1.0f; + float y = -1.0f; + bool mouseActivity = initialMouseActivity; + ApplyRelativeMousePacket(packet, x, y, mouseActivity); + Require(x == 1.0f && y == -1.0f, "zero motion preserves accumulators"); + Require(mouseActivity == initialMouseActivity, "zero motion preserves active device"); + } + }); + + runTest("extreme signed deltas retain the existing camera interpretation", [] { + ReaderFixture fixture((std::numeric_limits::min)(), (std::numeric_limits::max)()); + const auto packet = Read(fixture); + float x = 0.0f; + float y = 0.0f; + bool mouseActivity = false; + ApplyRelativeMousePacket(packet, x, y, mouseActivity); + Require(std::isfinite(x) && std::isfinite(y) && x < 0.0f && y > 0.0f, + "extreme signed motion produces finite deltas with the correct signs"); + Require(mouseActivity, "extreme motion activates the mouse"); + }); + + runTest("invalid reader or input handle is rejected without a read", [] { + RequireNoMovement(ReadRelativeMousePacket(FixtureHandle(), nullptr), PacketStatus::Invalid); + ReaderFixture fixture; + activeFixture = &fixture; + const auto result = ReadRelativeMousePacket(nullptr, FixtureReader); + activeFixture = nullptr; + RequireNoMovement(result, PacketStatus::Invalid); + Require(fixture.calls == 0, "invalid handle never reaches the reader"); + }); + + runTest("size-query errors and unexpected return values stop before allocation", [] { + for (const UINT result : {readError, 1u}) + { + ReaderFixture fixture; + fixture.queryResult = result; + RequireNoMovement(Read(fixture), PacketStatus::Invalid); + Require(fixture.calls == 1, "failed query has no data read"); + } + }); + + runTest("zero and undersized queried capacities are rejected", [] { + for (const UINT capacity : {0u, headerSize - 1}) + { + ReaderFixture fixture; + fixture.capacity = capacity; + RequireNoMovement(Read(fixture), PacketStatus::Invalid); + Require(fixture.calls == 1, "undersized query has no data read"); + } + }); + + runTest("allocation failure leaves movement and ownership unchanged", [] { + ReaderFixture fixture; + RequireNoMovement(Read(fixture, FailAllocation), PacketStatus::AllocationFailed); + Require(fixture.calls == 1, "allocation failure prevents the data read"); + }); + + runTest("failed and short data reads are rejected before header parsing", [] { + for (const UINT copied : {readError, 0u, headerSize - 1, headerSize, mouseSize - 1}) + { + ReaderFixture fixture; + fixture.copiedCount = copied; + RequireNoMovement(Read(fixture), PacketStatus::Invalid); + Require(fixture.calls == 2, "bad data read still uses only the original two calls"); + } + }); + + runTest("an oversized returned byte count is rejected", [] { + ReaderFixture fixture; + fixture.copiedCount = mouseSize + 1; + RequireNoMovement(Read(fixture), PacketStatus::Invalid); + }); + + runTest("the API cannot disguise a short read by changing the size argument", [] { + ReaderFixture fixture; + fixture.copiedCount = mouseSize - 1; + fixture.returnedSize = mouseSize - 1; + RequireNoMovement(Read(fixture), PacketStatus::Invalid); + }); + + runTest("inconsistent returned size arguments are rejected", [] { + for (const UINT size : {0u, mouseSize - 1, mouseSize + 1}) + { + ReaderFixture fixture; + fixture.returnedSize = size; + RequireNoMovement(Read(fixture), PacketStatus::Invalid); + } + }); + + runTest("inconsistent declared packet sizes are rejected", [] { + for (const DWORD size : {0ul, static_cast(headerSize - 1), static_cast(mouseSize - 1), + static_cast(mouseSize + 1)}) + { + ReaderFixture fixture; + fixture.Write(0x04, size); + RequireNoMovement(Read(fixture), PacketStatus::Invalid); + } + }); + + runTest("a complete header cannot authorize a truncated mouse payload", [] { + for (const UINT size : {headerSize, mouseSize - 1}) + { + ReaderFixture fixture; + fixture.capacity = size; + fixture.copiedCount = size; + fixture.returnedSize = size; + fixture.Write(0x04, static_cast(size)); + RequireNoMovement(Read(fixture), PacketStatus::Invalid); + } + }); + + runTest("valid non-mouse packets are ignored", [] { + for (const DWORD type : {static_cast(RIM_TYPEKEYBOARD), static_cast(RIM_TYPEHID)}) + { + ReaderFixture fixture; + fixture.Write(0x00, type); + RequireNoMovement(Read(fixture), PacketStatus::Ignored); + } + }); + + runTest("nonzero mouse flags retain the baseline selection behavior", [] { + for (const USHORT flags : + {static_cast(MOUSE_MOVE_ABSOLUTE), static_cast(MOUSE_VIRTUAL_DESKTOP), + static_cast(MOUSE_ATTRIBUTES_CHANGED), static_cast(MOUSE_MOVE_NOCOALESCE)}) + { + ReaderFixture fixture; + fixture.Write(0x18, flags); + RequireNoMovement(Read(fixture), PacketStatus::Ignored); + } + }); +} diff --git a/tests/render_scaling_tests.hpp b/tests/render_scaling_tests.hpp new file mode 100644 index 0000000..0340091 --- /dev/null +++ b/tests/render_scaling_tests.hpp @@ -0,0 +1,217 @@ +#pragma once + +#include "render/Scaling.hpp" + +#include +#include +#include +#include +#include + +// Include after the runner's Require(bool, const char*) declaration. Each case +// uses the actual shipped helper and independent expected arithmetic answers. +void RunRenderScalingTests(const std::function&)>& runTest) +{ + using namespace p3r::render; + + runTest("automatic scale retains native baseline on a wide 720p viewport", [] { + // Width is deliberately absent from the policy. A 2560x720 viewport + // therefore has the same baseline as 1280x720; the old guard did not. + const auto result = ComputeRenderTargetScale(720, 100.0f, 1.0f); + Require(result.automaticMultiplier == 1.0, "720p automatic baseline"); + Require(result.finalMultiplier == 1.0f, "720p final multiplier"); + Require(!result.usedFallback, "valid 720p inputs need no fallback"); + }); + + runTest("automatic scale retains native baseline at 800p and 1080p", [] { + for (const int height : {800, 1080}) + { + const auto result = ComputeRenderTargetScale(height, 100.0f, 1.0f); + Require(result.automaticMultiplier == 1.0, "native automatic baseline"); + Require(result.finalMultiplier == 1.0f, "native final multiplier"); + } + }); + + runTest("automatic scale follows vertical rendering resolution", [] { + const auto result1440 = ComputeRenderTargetScale(1440, 100.0f, 1.0f); + Require(std::abs(result1440.automaticMultiplier - 1.3333333333333333) < 1e-12, + "1440p automatic baseline"); + Require(std::abs(result1440.finalMultiplier - 1.3333333333333333f) < 1e-6f, "1440p final multiplier"); + + // This also covers a narrow 1920x2160 viewport: width cannot suppress + // the height-based baseline as it did in the old width-only guard. + const auto result2160 = ComputeRenderTargetScale(2160, 100.0f, 1.0f); + Require(result2160.automaticMultiplier == 2.0, "2160p automatic baseline"); + Require(result2160.finalMultiplier == 2.0f, "2160p final multiplier"); + }); + + runTest("screen percentage changes the automatic baseline", [] { + const auto half2160 = ComputeRenderTargetScale(2160, 50.0f, 1.0f); + Require(half2160.automaticMultiplier == 1.0, "2160p at 50 percent baseline"); + Require(half2160.finalMultiplier == 1.0f, "2160p at 50 percent final"); + + const auto boosted1440 = ComputeRenderTargetScale(1440, 150.0f, 1.0f); + Require(boosted1440.automaticMultiplier == 2.0, "1440p at 150 percent baseline"); + Require(boosted1440.finalMultiplier == 2.0f, "1440p at 150 percent final"); + }); + + runTest("explicit undersampling applies after the automatic baseline", [] { + const auto result = ComputeRenderTargetScale(1440, 100.0f, 0.5f); + Require(std::abs(result.automaticMultiplier - 1.3333333333333333) < 1e-12, + "undersampling keeps the automatic baseline"); + Require(std::abs(result.finalMultiplier - 0.6666666666666667f) < 1e-6f, + "explicit undersampling halves the automatic baseline"); + }); + + runTest("only the combined multiplier receives upper and lower limits", [] { + const auto highBaseline = ComputeRenderTargetScale(10800, 100.0f, 0.25f); + Require(highBaseline.automaticMultiplier == 10.0, "automatic baseline is uncapped"); + Require(highBaseline.finalMultiplier == 2.5f, "undersampling precedes final cap"); + + const auto capped = ComputeRenderTargetScale(2160, 100.0f, 4.0f); + Require(capped.finalMultiplier == 4.0f, "combined upper limit"); + + const auto floored = ComputeRenderTargetScale(1080, 100.0f, 0.125f); + Require(floored.finalMultiplier == 0.25f, "combined lower limit"); + Require(!floored.usedFallback, "positive finite undersampling is valid"); + }); + + runTest("invalid viewport heights use a temporary native baseline", [] { + for (const int height : {0, -1, std::numeric_limits::min()}) + { + const auto result = ComputeRenderTargetScale(height, 150.0f, 0.5f); + Require(result.automaticMultiplier == 1.0, "invalid height native baseline"); + Require(result.finalMultiplier == 0.5f, "invalid height still honors user multiplier"); + Require(result.usedFallback, "invalid height is reported"); + } + }); + + runTest("invalid screen percentages use 100 percent and report fallback", [] { + const std::array invalidValues{0.0f, -100.0f, std::numeric_limits::infinity(), + -std::numeric_limits::infinity(), + std::numeric_limits::quiet_NaN()}; + for (const float percentage : invalidValues) + { + const auto result = ComputeRenderTargetScale(2160, percentage, 0.5f); + Require(result.automaticMultiplier == 2.0, "invalid percentage uses 100 percent"); + Require(result.finalMultiplier == 1.0f, "percentage fallback honors user multiplier"); + Require(result.usedFallback, "invalid percentage is reported"); + } + }); + + runTest("invalid user multipliers use one and report fallback", [] { + const std::array invalidValues{0.0f, -1.0f, std::numeric_limits::infinity(), + -std::numeric_limits::infinity(), + std::numeric_limits::quiet_NaN()}; + for (const float multiplier : invalidValues) + { + const auto result = ComputeRenderTargetScale(2160, 100.0f, multiplier); + Require(result.automaticMultiplier == 2.0, "user fallback keeps automatic baseline"); + Require(result.finalMultiplier == 2.0f, "invalid user multiplier uses one"); + Require(result.usedFallback, "invalid user multiplier is reported"); + } + }); + + runTest("large finite inputs remain safe before the final clamp", [] { + const auto result = + ComputeRenderTargetScale(std::numeric_limits::max(), std::numeric_limits::max(), + std::numeric_limits::max()); + Require(std::isfinite(result.automaticMultiplier), "wide automatic arithmetic stays finite"); + Require(result.automaticMultiplier > std::numeric_limits::max(), + "automatic diagnostic retains values beyond the float range"); + Require(result.finalMultiplier == 4.0f, "large combined value receives final cap"); + Require(!result.usedFallback, "large positive finite inputs remain valid"); + }); + + runTest("a baseline beyond the float range still permits combined undersampling", [] { + const float maximum = std::numeric_limits::max(); + const auto result = ComputeRenderTargetScale(216000, maximum, 1.0f / maximum); + Require(result.automaticMultiplier > maximum, "automatic baseline exceeds float range"); + Require(std::abs(result.finalMultiplier - 2.0f) < 1e-6f, + "wide automatic baseline combines before float narrowing"); + Require(!result.usedFallback, "finite extreme undersampling remains valid"); + }); + + runTest("tiny positive inputs retain deliberate multiplier semantics", [] { + const float tiny = std::numeric_limits::denorm_min(); + const auto tinyPercentage = ComputeRenderTargetScale(2160, tiny, 1.0f); + Require(tinyPercentage.automaticMultiplier == 1.0, "tiny percentage keeps native floor"); + Require(!tinyPercentage.usedFallback, "tiny positive percentage remains valid"); + + const auto tinyMultiplier = ComputeRenderTargetScale(2160, 100.0f, tiny); + Require(tinyMultiplier.finalMultiplier == 0.25f, "tiny positive user value reaches final floor"); + Require(!tinyMultiplier.usedFallback, "tiny positive user multiplier remains valid"); + }); + + runTest("normal automatic scales produce exact expected target dimensions", [] { + const auto scale1440 = ComputeRenderTargetScale(1440, 100.0f, 1.0f); + const auto dimensions1440 = ScaleDimensions(1920, 1080, scale1440.finalMultiplier); + Require(dimensions1440.has_value(), "1440p dimensions are valid"); + Require(dimensions1440->width == 2560, "1440p target width"); + Require(dimensions1440->height == 1440, "1440p target height"); + + const auto scale2160 = ComputeRenderTargetScale(2160, 100.0f, 1.0f); + const auto dimensions2160 = ScaleDimensions(1920, 1080, scale2160.finalMultiplier); + Require(dimensions2160.has_value(), "2160p dimensions are valid"); + Require(dimensions2160->width == 3840, "2160p target width"); + Require(dimensions2160->height == 2160, "2160p target height"); + }); + + runTest("positive scaled dimensions truncate toward zero", [] { + const auto dimensions = ScaleDimensions(5, 7, 0.5f); + Require(dimensions.has_value(), "fractional dimensions are valid"); + Require(dimensions->width == 2, "fractional width truncates"); + Require(dimensions->height == 3, "fractional height truncates"); + }); + + runTest("valid undersampled dimensions retain at least one pixel", [] { + const auto dimensions = ScaleDimensions(1, 3, 0.25f); + Require(dimensions.has_value(), "small target is valid"); + Require(dimensions->width == 1, "undersampled width minimum"); + Require(dimensions->height == 1, "undersampled height minimum"); + + const auto tiny = ScaleDimensions(1, 1, std::numeric_limits::denorm_min()); + Require(tiny.has_value(), "tiny positive scale is valid"); + Require(tiny->width == 1 && tiny->height == 1, "tiny scale preserves one pixel"); + }); + + runTest("nonpositive source dimensions are rejected", [] { + Require(!ScaleDimensions(0, 1080, 1.0f), "zero width is rejected"); + Require(!ScaleDimensions(1920, 0, 1.0f), "zero height is rejected"); + Require(!ScaleDimensions(-1, 1080, 1.0f), "negative width is rejected"); + Require(!ScaleDimensions(1920, -1, 1.0f), "negative height is rejected"); + }); + + runTest("invalid dimension multipliers are rejected", [] { + const std::array invalidValues{0.0f, -1.0f, std::numeric_limits::infinity(), + -std::numeric_limits::infinity(), + std::numeric_limits::quiet_NaN()}; + for (const float multiplier : invalidValues) + Require(!ScaleDimensions(1920, 1080, multiplier), "invalid dimension scale"); + }); + + runTest("the largest int32 dimension is accepted without rounding upward", [] { + constexpr auto maximum = std::numeric_limits::max(); + const auto dimensions = ScaleDimensions(maximum, maximum, 1.0f); + Require(dimensions.has_value(), "largest int32 dimensions remain valid"); + Require(dimensions->width == maximum && dimensions->height == maximum, + "double arithmetic preserves exact int32 dimensions"); + }); + + runTest("integer overflow is rejected before dimensions are narrowed", [] { + constexpr auto maximum = std::numeric_limits::max(); + Require(!ScaleDimensions(maximum, 1, 2.0f), "width overflow is rejected"); + Require(!ScaleDimensions(1, maximum, 2.0f), "height overflow is rejected"); + Require(!ScaleDimensions(maximum, 1, std::nextafter(1.0f, 2.0f)), + "scale just above one would overflow width"); + Require(!ScaleDimensions(1, 1, std::numeric_limits::max()), + "large finite product is rejected"); + }); + + runTest("integer upper-bound checks distinguish adjacent source dimensions", [] { + const auto valid = ScaleDimensions(1073741823, 1, 2.0f); + Require(valid.has_value(), "product immediately below int32 limit is valid"); + Require(valid->width == 2147483646, "valid near-limit width is exact"); + Require(!ScaleDimensions(1073741824, 1, 2.0f), "product immediately above int32 limit is rejected"); + }); +} diff --git a/xmake.lua b/xmake.lua index 205f35d..4ec9d3c 100644 --- a/xmake.lua +++ b/xmake.lua @@ -6,7 +6,7 @@ set_languages("cxxlatest", "clatest") set_optimize("smallest") option("fix_version") - set_default("1.3.0") + set_default("1.4.0") set_showmenu(true) set_description("Version embedded in P3RFix (set from the release tag in CI)") option_end() @@ -36,7 +36,9 @@ target(name) set_kind("shared") set_prefixname("") set_extension(".asi") - add_files("src/*.cpp", "external/safetyhook/src/*.cpp", "src/unreal/Integration.cpp", "src/unreal/detail/Runtime.cpp") + add_files("src/*.cpp", "external/safetyhook/src/*.cpp", + "src/unreal/Integration.cpp", "src/unreal/detail/Runtime.cpp", + "src/render/Scaling.cpp", "src/input/RawMousePacket.cpp") add_syslinks("user32") add_deps("zydis") on_load(function (target) @@ -66,3 +68,18 @@ target("unreal-integration-tests") add_cxflags("/utf-8", "/GL") add_ldflags("/LTCG", "/OPT:REF", "/OPT:ICF") end + +-- Exercise the shipped scaling and Windows packet-reader boundaries without the game. +target("fix-behavior-tests") + set_kind("binary") + set_default(false) + add_files("tests/fix_behavior_tests.cpp", "src/render/Scaling.cpp", + "src/input/RawMousePacket.cpp") + add_includedirs("src") + add_syslinks("user32") + if is_plat("windows") then + set_toolchains("msvc") + set_runtimes("MT") + add_cxflags("/utf-8", "/W4", "/WX", "/GL") + add_ldflags("/LTCG", "/OPT:REF", "/OPT:ICF") + end From d13313277d657c0d58f768b940a3f8b2c38a532a Mon Sep 17 00:00:00 2001 From: dev-camo Date: Thu, 1 Oct 2026 15:03:33 -0600 Subject: [PATCH 3/5] Consolidate release licenses and prepare 1.4.0 --- .agents/PLAN.md | 27 +++- AGENTS.md | 10 +- CHANGELOG.md | 13 ++ README.md | 31 +++-- THIRD_PARTY_NOTICES.md | 2 +- create_release.ps1 | 281 ++++++++++++++++++++++++++++++++++++++++- release_body.md | 2 + src/dllmain.cpp | 29 ++++- 8 files changed, 361 insertions(+), 34 deletions(-) diff --git a/.agents/PLAN.md b/.agents/PLAN.md index 862eb01..c612b74 100644 --- a/.agents/PLAN.md +++ b/.agents/PLAN.md @@ -37,12 +37,15 @@ Separate gameplay/menu/background frame-rate limits, new HUD/FOV controls, live - [x] (2026-10-01) Implemented height-based scaling and checked dimension conversion; Windows debug/release behavior tests cover native retention, explicit undersampling, invalid inputs, truncation, and integer boundaries. - [x] (2026-10-01) Implemented checked raw-input reading, an allocation-failure seam, and shared delta application. Independent literal Windows x64 fixtures cover two-call failures, malformed/truncated packets, signed motion, ignored flags, and unchanged accumulators/ownership on rejection. - [x] (2026-10-01) Moved the unchanged console initialization call after all independent installers and added the attempt-complete marker. Remaining: development harness and real startup/console observations. -- [ ] Verify console-enabled, disabled, unavailable, and delayed startup with the development harness and maintainer game tests. +- [x] (2026-10-01) Temporary Windows harness passed 8/8 using verbatim production bodies for `Main`, `EnableConsole`, and the two changed callbacks. Console-disabled, ready, unavailable, delayed, and 200-attempt timeout cases pass; message/render adapters forward once and preserve memory/delta boundaries. Remaining: maintainer startup/console observations in game. +- [ ] Verify console-enabled and disabled startup, intro skipping, and repeated console access in game. - [x] (2026-10-01) Implemented the thirteen-input `LICENSES` generator and archive assertions in the existing release script. Remaining: fault fixtures and real package validation. -- [ ] Validate license completeness, repeatable generation, and the contents of both final ZIPs; update source and release documentation to describe the new package contract. +- [x] (2026-10-01) Licensing fixtures reject missing, empty, and unlisted notices; preserve BOM/Unicode/trailing-space/no-final-newline handling; reproduce identical document bytes; and reject legacy, incomplete, duplicate-location, missing-INI, and wrong-loader archive contents. Both real ZIPs pass production assertions and an independent Python comparison of all thirteen complete normalized inputs. +- [x] (2026-10-01) Updated README, AGENTS, third-party introduction, changelog, and release body. PowerShell ScriptAnalyzer reports zero warning/error diagnostics; selected new C++ files pass clang-format verification and the diff has no whitespace errors. - [x] (2026-10-01) Built the ASI with VS2022/MSVC 14.44.35207 on the authorized VM in debug and release. In each configuration: Unreal 69/69, behavior 35/35, minimal inventory PASS. Behavior sources compile with `/W4 /WX`. - [ ] Compare menu rendering, input, console operation, intro skipping, and ultrawide behavior in the game. -- [ ] Prepare 1.4.0 metadata, notes, and both existing package formats. +- [x] (2026-10-01) Prepared 1.4.0 binary defaults, unchanged Reloaded-II version substitution, generated release notes, and both actual Windows archives. Fresh temporary extractions and comparison with shipped 1.3.0 archives preserve all mod-file destinations and loader bytes. Live installation validation remains outstanding. +- [ ] Write the maintainer instructions and result form under `/tmp/P3RFix/`, recording exact build/hash identities and the pending live cases. - [ ] Record actual validation evidence and release outcome. ## Surprises & Discoveries @@ -63,6 +66,10 @@ The validation tools from the prior 1.3.0 work are available at `C:\Users\camo\P The first behavior-test build exposed Windows `min`/`max` macros after including the raw-input SDK header. Defining `NOMINMAX` in the test runner, as the production precompiled header already does, fixed the conflict. Both complete Windows configurations subsequently passed. SSH command length also limits large encoded PowerShell scripts; the handoff helper now uploads scripts and executes `-File` instead. +The empty-notice fixture found that .NET's culture-sensitive `StartsWith` treats the encoding marker as ignorable, causing an empty string to pass that check and fail at `Substring(1)`. An ordinal comparison now produces the required path-specific empty-input error. The fixture output must also be outside the fixture's `licenses/` directory: Windows treats a root file named `LICENSES` and that source directory as the same name. Production already separates source inputs and staging output correctly. + +The shipped 1.3.0 INI is 2,954 bytes (CRLF) and this Linux-sourced Windows validation package's INI is 2,876 bytes (LF). An independent comparison confirms that line endings are the only difference; no section, key, value, or comment changed. The document generator normalizes all maintained notice inputs, so both packages have identical 25,315-byte `LICENSES` files regardless of input line endings. + ## Decision Log Decision: target the original three behavior fixes and the subsequently requested licensing cleanup at 1.4.0 rather than a pre-v2 backlog. Rationale: the maintainer wants a concrete maintenance release and separate treatment from the 1.5.0 feature proposals. Date/author: 2026-10-01, maintainer direction recorded by Codex. @@ -83,6 +90,10 @@ Decision: reserve actual game runs and install-method compatibility confirmation Decision: expose a small nothrow raw-input allocator seam and delta-application helper, with a separate fixture header alongside the scaling cases. Rationale: allocation failure can be proven without exhausting memory, and tests exercise the shipped accumulator behavior rather than duplicating it. Date/author: 2026-10-01, Codex. +Decision: rate-limit invalid screen-percentage and user-multiplier warnings independently, without treating an unknown startup height as an error. Rationale: early viewport discovery is expected and must not suppress reporting a later invalid runtime observation. Date/author: 2026-10-01, Codex. + +Decision: use PowerShell's information stream for packaging diagnostics and a singular internal `Assert-PackageContent` name. Rationale: keep helper return bytes separate from diagnostics and pass ScriptAnalyzer without suppression or a new release dependency. The analyzer is installed only in the VM's disposable validation tools. Date/author: 2026-10-01, Codex. + ## Context and Orientation The repository examined is `/home/camo/Projects/P3RFix`. On a Windows build machine, use that machine's checkout path rather than attempting to use this Linux path. The source produces a Windows ASI: a DLL with an `.asi` filename loaded into the game by the standalone loader or Reloaded-II. @@ -478,6 +489,10 @@ Do not add generated SDK files or new memory-layout fields for the three runtime Keep implementation evidence with the release work: the final source diff, debug and release test output, inventory result, startup-order log excerpt, representative render-target sizes, actual-game comparison notes, the combined-license input manifest/hash, and both archive-content inspections. +Captured automated evidence is under `/tmp/P3RFix/evidence/`, mirrored from `C:\Users\camo\Projects\P3RFix-1.4.0\evidence`. The candidate archives and generated release text are under `/tmp/P3RFix/packages/`. `windows-validation.txt` records both ASI builds and both sets of 69/69 integration and 35/35 behavior results; `hook-harness.txt` records 8/8 development-only observations; `license-checks.txt`, `powershell-lint.txt`, and `packaging.txt` record the packaging checks. `archive-inspection.txt` independently verifies exact inventories, all thirteen payloads, configuration/loader preservation, Reloaded-II metadata, x64 PE identity, and matching documents/binaries. No actual-game evidence exists yet. + +The verified standalone inventory is `dsound.dll` (1,198,304 bytes), `LICENSES` (25,315), `P3RFix.asi` (1,203,200), and `P3RFix.ini` (2,876), all at the archive root. Reloaded-II replaces `dsound.dll` with `ModConfig.json` (945 bytes), retaining the other three paths. Both documents have SHA256 `f953af8a4c4e88df3c592a3fbf8dbec9cb37bddba1b17633b9fa38d3e2f1aa54`; the ASI hash is `c64347fb3d0f687a44bda30a5f7e796a538a956cee7c2b51a48d56dd882f91cf`. ZIP hashes are recorded in `archive-inspection.txt` and may change if packaging is repeated; compare actual bytes before handoff. + The following are expected example results, not captured output: [PASS] automatic scale retains native baseline on a wide 720p viewport @@ -519,12 +534,14 @@ Licensing consolidation uses only the existing PowerShell 7.2-or-newer release e ## Outcomes & Retrospective -The three runtime changes are implemented, version defaults target 1.4.0, and both Windows configurations build the ASI and pass Unreal 69/69, behavior 35/35, and the minimal inventory. Those results prove synthetic boundaries and arithmetic, not gameplay compatibility. The licensing generator and ZIP assertions are implemented; fault fixtures and real archives remain under validation. README, repository instructions, changelog, and release text describe the new behavior and package contract; the changelog truthfully marks 1.4.0 unreleased. +All four changes are implemented. Both Windows configurations build the ASI and pass Unreal 69/69, behavior 35/35, and the minimal inventory; the development harness passes 8/8. Both real ZIPs pass licensing and destination checks, independent thirteen-input comparisons, and fresh temporary extraction. PowerShell lint reports zero warning/error diagnostics. README, repository instructions, changelog, and release text describe the new behavior and package contract; the changelog truthfully marks 1.4.0 unreleased. These results establish arithmetic, simulated API failures, synthetic adapter ordering, and archive contents; they do not establish gameplay or loader compatibility. -Resume from the current branch and inspect the worktree. Local validation scripts and console output are under `/tmp/P3RFix/`; `remote.py` uploads a PowerShell script to the dedicated VM directory and runs it. Next: complete `license-fixtures.ps1`, run a development-only harness using the actual `Main`/console/hook bodies, package through `create_release.ps1`, retrieve and independently inspect both ZIPs, record hashes/transcripts, and write the maintainer game-test form. Do not run the game, delete this plan, push the branch, or claim completion before the required maintainer validation arrives. No release tag or publication has been attempted. +Resume from the current branch and inspect the worktree. Local validation scripts and console output are under `/tmp/P3RFix/`; `remote.py` uploads a PowerShell script to the dedicated VM directory and runs it. Next: finish the maintainer game-test form, audit the full plan against actual evidence, commit the release preparation, and hand over the candidate archives for testing. Live comparison of rendering/input/startup/console/intro/viewport behavior, readable Windows license presentation, and loading through the supported installation methods require maintainer results. Do not run the game, delete this plan, push the branch, or claim completion before the required maintainer validation arrives. No release tag or publication has been attempted. Revision note, 2026-10-01: renamed the original pre-v2 notes for a 1.4.0 release and expanded them into a self-contained maintenance plan at the maintainer's request. The scaling policy, failure handling, startup ordering, implementation boundaries, validation, and release criteria are now explicit; the established console, viewport, packaging, and MSVC constraints are retained. Revision note, 2026-10-01: added the maintainer's fourth 1.4.0 change, replacing the release licensing folder and separate license/notice files with one generated `LICENSES` document in both existing package formats. Expanded the scope, decisions, milestones, commands, archive/content validation, upgrade behavior, recovery, and release criteria to cover complete notice preservation. Source licensing files and build tooling remain unchanged; the separate 1.5.0 plan's baseline references now inherit this packaging contract. Revision note, 2026-10-01: recorded the recovered implementation state, completed Windows validation, test seams, and dedicated VM/tool paths. Split implementation from outstanding live validation so future continuations cannot mistake compilation or synthetic tests for confirmed gameplay. + +Revision note, 2026-10-01: recorded verified licensing fault paths, the actual package inventories and hashes, zero-diagnostic linting, and production-body development harness results. Live game and installation checks remain explicit release gates; no evidence has been inferred from compilation or archive success. diff --git a/AGENTS.md b/AGENTS.md index 2b1d70b..ab6f228 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -6,7 +6,9 @@ P3RFix is a Windows x64 C++ ASI plugin for Persona 3 Reload. - `src/dllmain.cpp` handles initialization, configuration, and game hooks; `src/helper.hpp` provides shared utilities. - `src/unreal/Integration.hpp` exposes the focused Unreal API. Keep memory layouts and runtime internals in `src/unreal/detail/`. +- `src/render/Scaling.*` defines render-target scaling; `src/input/RawMousePacket.*` validates Windows raw-input reads and applies the existing mouse deltas. - `tests/unreal_integration_tests.cpp` contains synthetic memory tests; `tools/unreal_inventory.py` audits integration sources. +- `tests/fix_behavior_tests.cpp` runs scaling and packet-reader cases from the adjacent test headers. - `external/` contains pinned dependency submodules. `assets/r2-package/` holds Reloaded-II metadata; `.github/` contains workflows, issue templates, and screenshots. ## Build, Test, and Development Commands @@ -15,15 +17,17 @@ Run from the repository root on Windows with Visual Studio 2022 C++ tools, Windo ```powershell git submodule update --init --recursive -xmake f -y -p windows -a x64 -m debug --fix_version=1.3.0 +xmake f -y -p windows -a x64 -m debug --fix_version=1.4.0 xmake build -y P3RFix xmake build -y unreal-integration-tests xmake run unreal-integration-tests +xmake build -y fix-behavior-tests +xmake run fix-behavior-tests python tools/unreal_inventory.py --phase minimal -./create_release.ps1 -Version 1.3.0 +./create_release.ps1 -Version 1.4.0 ``` -These commands initialize dependencies, configure a debug build, build the plugin, build/run tests, audit source boundaries, and package a release. Packaging creates standalone and Reloaded-II ZIPs under `build/`. Load the packaged fix with the game using the installation instructions in `README.md`. +These commands initialize dependencies, configure a debug build, build the plugin, build/run tests, audit source boundaries, and package a release. Packaging creates standalone and Reloaded-II ZIPs under `build/`, validates all thirteen license/notice inputs, and verifies each archive's combined `LICENSES` and binary paths. The maintained source license files remain in the repository. Load the packaged fix with the game using the installation instructions in `README.md`. ## Coding Style & Naming Conventions diff --git a/CHANGELOG.md b/CHANGELOG.md index 40878b1..e1d2b8c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,18 @@ # Changelog +## [1.4.0] - Unreleased + +### Fixed + +- Base automatic menu render texture scaling on effective viewport height, retaining native quality below 1080p while honoring explicit user multipliers and rejecting invalid or overflowing dimensions. +- Ignore failed, incomplete, and inconsistent raw-input reads before parsing mouse movement; preserve the existing sensitivity and accepted mouse flags. +- Attempt independent fix installation before the optional console discovery wait on the initialization worker, including intro skipping and mouse hooks. + +### Maintenance + +- Replace the release ZIPs' separate license documents and `licenses/` directory with one complete `LICENSES` file, preserving all maintained notices and existing binary destinations. Overwrite upgrades may retain earlier notice files. +- Run focused scaling and Windows packet-reader tests alongside the Unreal integration checks in debug and release builds. + ## [1.3.0] - 2026-10-01 ### Maintenance diff --git a/README.md b/README.md index 6b65853..0930901 100644 --- a/README.md +++ b/README.md @@ -23,6 +23,8 @@ Maintained by [dev-camo](https://github.com/dev-camo) and contributors, continui - Xbox/MS Store: `XboxGames\Persona 3 Reload\Content\P3R\Binaries\WinGDK` - `P3RFix.asi`, `P3RFix.ini` and `dsound.dll` should be alongside `P3R.exe`. +Both ZIPs include one `LICENSES` document with the project license and complete third-party notices. Overwriting an older installation can leave its previous notice files in place; `LICENSES` contains the complete notices for the new package. + ### Steam Deck/Linux Additional Instructions - Open up the game properties in Steam and add `WINEDLLOVERRIDES="dsound=n,b" %command%` to the launch options. @@ -44,6 +46,7 @@ If you previously installed the standalone package, back up your `P3RFix.ini` se ## Configuration - See **P3RFix.ini** to adjust settings for the fix. +- `[Render Texture Resolution].Multiplier` applies after automatic scaling based on viewport height and screen percentage. Automatic scaling keeps at least the native 1080p target quality; an explicit value below 1 can still reduce it. The combined multiplier is limited to 0.25–4. ## Screenshots @@ -67,10 +70,10 @@ Use Windows with Visual Studio 2022's C++ build tools and Windows SDK, CMake, Po git clone --recurse-submodules https://github.com/dev-camo/P3RFix.git cd P3RFix git submodule update --init --recursive -./create_release.ps1 -Version 1.3.0 +./create_release.ps1 -Version 1.4.0 ``` -The script builds Windows x64 release binaries and creates `build/P3RFix_1.3.0.zip` and `build/P3RFix_Reloaded-II.zip`. The version is embedded in the binary and Reloaded-II metadata. Dependencies use the revisions recorded in Git; the standalone package bundles a pinned Ultimate ASI Loader download. +The script builds Windows x64 release binaries and creates `build/P3RFix_1.4.0.zip` and `build/P3RFix_Reloaded-II.zip`. The version is embedded in the binary and Reloaded-II metadata. Dependencies use the revisions recorded in Git; the standalone package bundles a pinned Ultimate ASI Loader download. Packaging combines all thirteen maintained license/notice documents into `LICENSES` and checks both archives for complete matching notices and the expected binary/configuration paths. The [Build packages workflow](https://github.com/dev-camo/P3RFix/actions/workflows/build.yml) also builds and packages pushes and pull requests to `main`, and supports manual runs. Review its build result and downloadable artifacts before tagging a release. @@ -78,43 +81,47 @@ The [Build packages workflow](https://github.com/dev-camo/P3RFix/actions/workflo Console construction and menu render texture access use the focused interface in `src/unreal/Integration.hpp`. Game memory layouts and dispatch constants live privately in `src/unreal/detail/Layouts.hpp`, with their original Dumper-7 symbols recorded beside them. The full generated SDK is no longer a build or checkout dependency. Add only the fields needed for a new feature, with provenance and a boundary test. -Run the synthetic memory tests in Windows x64 debug and release configurations: +Run the synthetic memory and fix behavior tests in Windows x64 debug and release configurations: ```powershell -xmake f -y -p windows -a x64 -m debug --fix_version=1.3.0 +xmake f -y -p windows -a x64 -m debug --fix_version=1.4.0 xmake build -y unreal-integration-tests xmake run unreal-integration-tests -xmake f -y -p windows -a x64 -m release --fix_version=1.3.0 +xmake build -y fix-behavior-tests +xmake run fix-behavior-tests +xmake f -y -p windows -a x64 -m release --fix_version=1.4.0 xmake build -y unreal-integration-tests xmake run unreal-integration-tests +xmake build -y fix-behavior-tests +xmake run fix-behavior-tests python tools/unreal_inventory.py --phase minimal ``` -These tests check layouts, reflected console calls, failure handling, name conversion, and render writes using independent byte fixtures. They do not establish compatibility with a particular game update; compare console operation and menu rendering with the baseline DLL in the actual game. Console lookup assumes the inherited signatures, registry layout, and virtual dispatch entry; render access remains available if console dependencies cannot be discovered. +The integration tests check layouts, reflected console calls, failure handling, name conversion, and render writes using independent byte fixtures. The behavior tests check scaling arithmetic, integer bounds, raw-input read failures, signed mouse deltas, and allocation failure using a controlled Windows reader. They do not establish compatibility with a particular game update; compare console operation, input, and menu rendering with the baseline DLL in the actual game. Console lookup assumes the inherited signatures, registry layout, and virtual dispatch entry; independent hook installation runs before the optional console discovery wait. ## Releasing -Commit the release changes, then push the branch and a version tag pointing at that commit: +Validate the packages and game behavior, then have a human review the branch before merging. After approval, push a new version tag pointing at the exact tested release commit: ```sh git push origin HEAD -git tag -a 1.3.0 -m "Release 1.3.0" -git push origin 1.3.0 +git tag -a 1.4.0 -m "Release 1.4.0" +git push origin 1.4.0 ``` -Use a new version number for each release. Tags may use `1.3.0` or `v1.3.0`. The [Publish release workflow](https://github.com/dev-camo/P3RFix/actions/workflows/release.yml) checks out the tagged commit and its submodules, builds that version and publishes both ZIP files on the matching [GitHub Release](https://github.com/dev-camo/P3RFix/releases). +Use a new version number for each release. Tags may use `1.4.0` or `v1.4.0`. The [Publish release workflow](https://github.com/dev-camo/P3RFix/actions/workflows/release.yml) checks out the tagged commit and its submodules, builds that version and publishes both ZIP files on the matching [GitHub Release](https://github.com/dev-camo/P3RFix/releases). To retry a release for an existing tag, open **Publish release → Run workflow** in GitHub Actions and enter the tag in the `tag` field, or use GitHub CLI: ```sh -gh workflow run release.yml -f tag=1.3.0 +gh workflow run release.yml -f tag=1.4.0 ``` A retry rebuilds the commit referenced by that tag and replaces the release's ZIP attachments. The workflow checks the tag's commit before building and publishing. ## License and Credits -P3RFix is distributed under the [MIT License](LICENSE.md). Original copyrights and third-party attributions are preserved. See [third-party notices](THIRD_PARTY_NOTICES.md) and [license texts](licenses/) for dependency licensing and retained Unreal integration provenance. +P3RFix is distributed under the [MIT License](LICENSE.md). Original copyrights and third-party attributions are preserved. In the source checkout, see [third-party notices](THIRD_PARTY_NOTICES.md) and [license texts](licenses/) for dependency licensing and retained Unreal integration provenance. Release ZIPs ship those complete texts together in one generated `LICENSES` document; individual source notice files remain maintained here. - Lyall for the original P3RFix implementation. - [Ultimate ASI Loader](https://github.com/ThirteenAG/Ultimate-ASI-Loader) for ASI loading. diff --git a/THIRD_PARTY_NOTICES.md b/THIRD_PARTY_NOTICES.md index 8d4b3ea..4180847 100644 --- a/THIRD_PARTY_NOTICES.md +++ b/THIRD_PARTY_NOTICES.md @@ -1,6 +1,6 @@ # Third-party notices -P3RFix retains its original MIT license and copyright notice in [LICENSE.md](LICENSE.md), with an additional copyright notice for current maintenance. Third-party components retain their own licenses and attribution. Both release packages include this notice and the `licenses/` directory. +P3RFix retains its original MIT license and copyright notice in [LICENSE.md](LICENSE.md), with an additional copyright notice for current maintenance. Third-party components retain their own licenses and attribution. Both release packages carry one combined `LICENSES` document containing the project license, this notice, and the complete component license texts. The linked individual license files remain maintained in the source repository. ## Components compiled into P3RFix.asi diff --git a/create_release.ps1 b/create_release.ps1 index 2967b4f..1d483dd 100644 --- a/create_release.ps1 +++ b/create_release.ps1 @@ -7,8 +7,263 @@ param ( Set-StrictMode -Version Latest $ErrorActionPreference = 'Stop' +# Keep this manifest explicit: adding or removing a source notice requires review. +# Loader-related sections are retained in both packages; only standalone bundles +# the loader binary itself. +$LicenseSections = @( + @{ Title = 'P3RFix license'; Path = 'LICENSE.md' } + @{ Title = 'Third-party attribution and provenance'; Path = 'THIRD_PARTY_NOTICES.md' } + @{ Title = 'Compiled dependency: inipp'; Path = 'licenses/inipp/LICENSE.txt' } + @{ Title = 'Compiled dependency: spdlog'; Path = 'licenses/spdlog/LICENSE' } + @{ Title = 'Compiled dependency: fmt'; Path = 'licenses/fmt/LICENSE' } + @{ Title = 'Compiled dependency: SafetyHook'; Path = 'licenses/SafetyHook/LICENSE' } + @{ Title = 'Compiled dependency: Zydis'; Path = 'licenses/Zydis/LICENSE' } + @{ Title = 'Compiled dependency: Zycore'; Path = 'licenses/Zycore/LICENSE' } + @{ Title = 'Retained Unreal support: UnrealContainers'; Path = 'licenses/UnrealContainers/LICENSE' } + @{ Title = 'Standalone loader: Ultimate ASI Loader'; Path = 'licenses/Ultimate-ASI-Loader/LICENSE' } + @{ Title = 'Standalone loader dependency: miniz'; Path = 'licenses/Ultimate-ASI-Loader/miniz-LICENSE' } + @{ Title = 'Standalone loader dependency: MinHook and Hacker Disassembler Engine'; Path = 'licenses/Ultimate-ASI-Loader/MinHook-LICENSE.txt' } + @{ Title = 'Standalone loader dependency: injector utility'; Path = 'licenses/Ultimate-ASI-Loader/injector-utility-LICENSE.txt' } +) + +function Read-LicenseText { + param ( + [string]$RepositoryRoot, + [string]$RelativePath + ) + + $InputPath = Join-Path $RepositoryRoot $RelativePath + if (-not (Test-Path -LiteralPath $InputPath -PathType Leaf)) { + throw "Missing license input: $RelativePath" + } + + try { + # Strict decoding reports damaged input rather than replacing author names. + $Utf8 = [System.Text.UTF8Encoding]::new($false, $true) + $Text = $Utf8.GetString([System.IO.File]::ReadAllBytes($InputPath)) + } catch { + throw "Could not read UTF-8 license input '$RelativePath': $($_.Exception.Message)" + } + if ($Text.StartsWith([string][char]0xFEFF, [System.StringComparison]::Ordinal)) { + $Text = $Text.Substring(1) + } + if ([string]::IsNullOrWhiteSpace($Text)) { + throw "Empty license input: $RelativePath" + } + + # Normalize only line endings. Preserve wording, indentation, and whitespace. + return $Text.Replace("`r`n", "`n").Replace("`r", "`n").Replace("`n", "`r`n") +} + +function Write-CombinedLicenseFile { + param ( + [string]$RepositoryRoot, + [object[]]$Sections, + [string]$DestinationPath + ) + + $InputPaths = [System.Collections.Generic.HashSet[string]]::new( + [System.StringComparer]::OrdinalIgnoreCase) + $Titles = [System.Collections.Generic.HashSet[string]]::new( + [System.StringComparer]::Ordinal) + $Inputs = @() + foreach ($Section in $Sections) { + if (-not $InputPaths.Add($Section.Path)) { + throw "Duplicate license input in manifest: $($Section.Path)" + } + if (-not $Titles.Add($Section.Title)) { + throw "Duplicate license section title: $($Section.Title)" + } + $Inputs += @{ + Title = $Section.Title + Path = $Section.Path + Text = Read-LicenseText -RepositoryRoot $RepositoryRoot -RelativePath $Section.Path + } + } + + # Enumeration is only for completeness checking; it never determines order. + $LicenseDirectory = Join-Path $RepositoryRoot 'licenses' + if (-not (Test-Path -LiteralPath $LicenseDirectory -PathType Container)) { + throw 'Missing license input directory: licenses' + } + $ActualPaths = [System.Collections.Generic.HashSet[string]]::new( + [System.StringComparer]::OrdinalIgnoreCase) + foreach ($File in Get-ChildItem -LiteralPath $LicenseDirectory -File -Recurse -Force) { + $RelativePath = [System.IO.Path]::GetRelativePath($RepositoryRoot, $File.FullName). + Replace('\', '/') + [void]$ActualPaths.Add($RelativePath) + if (-not $InputPaths.Contains($RelativePath)) { + throw "License input is not listed in the manifest: $RelativePath" + } + } + foreach ($InputPath in $InputPaths) { + if ($InputPath.StartsWith('licenses/', [System.StringComparison]::OrdinalIgnoreCase) -and + -not $ActualPaths.Contains($InputPath)) { + throw "Manifest license input is absent from licenses inventory: $InputPath" + } + } + + $Newline = "`r`n" + $Document = [System.Text.StringBuilder]::new() + [void]$Document.Append("P3RFix release licenses and notices$Newline$Newline") + [void]$Document.Append("This document collects P3RFix's license and the full notices for components$Newline") + [void]$Document.Append("used or credited by this release. Each component retains its own terms.$Newline") + [void]$Document.Append("Ultimate ASI Loader and its dependencies describe the loader bundled only$Newline") + [void]$Document.Append("with the standalone ZIP. The Reloaded-II ZIP does not include or require$Newline") + [void]$Document.Append("the standalone loader binary, dsound.dll.$Newline$Newline") + + $Payloads = @() + foreach ($SectionInput in $Inputs) { + $Heading = "======================================================================$Newline" + + "$($SectionInput.Title)$Newline" + + "Source repository path: $($SectionInput.Path)$Newline$Newline" + [void]$Document.Append($Heading) + if ($SectionInput.Path -eq 'THIRD_PARTY_NOTICES.md') { + [void]$Document.Append("Repository-relative links in the notice below refer to the source checkout.$Newline") + [void]$Document.Append("The full referenced license texts follow in this same document.$Newline$Newline") + } elseif ($SectionInput.Path.StartsWith('licenses/Ultimate-ASI-Loader/', + [System.StringComparison]::OrdinalIgnoreCase)) { + [void]$Document.Append("This loader component is bundled only with the standalone ZIP.$Newline") + [void]$Document.Append("The Reloaded-II ZIP does not include the standalone loader binary.$Newline$Newline") + } + + $Payloads += @{ + Heading = $Heading + Path = $SectionInput.Path + Offset = $Document.Length + Length = $SectionInput.Text.Length + } + [void]$Document.Append($SectionInput.Text) + if (-not $SectionInput.Text.EndsWith($Newline, [System.StringComparison]::Ordinal)) { + [void]$Document.Append($Newline) + } + [void]$Document.Append($Newline) + } + + $Utf8 = [System.Text.UTF8Encoding]::new($false, $true) + [System.IO.File]::WriteAllText($DestinationPath, $Document.ToString(), $Utf8) + + # Verify the written document, including every complete payload in order. + $WrittenBytes = [System.IO.File]::ReadAllBytes($DestinationPath) + $HasEncodingMarker = $WrittenBytes.Length -ge 3 -and + $WrittenBytes[0] -eq 0xEF -and $WrittenBytes[1] -eq 0xBB -and $WrittenBytes[2] -eq 0xBF + if ($WrittenBytes.Length -eq 0 -or $HasEncodingMarker) { + throw "Combined licensing document is empty or has an encoding marker: $DestinationPath" + } + $WrittenText = $Utf8.GetString($WrittenBytes) + $PreviousHeading = -1 + foreach ($Payload in $Payloads) { + $HeadingOffset = $WrittenText.IndexOf($Payload.Heading, [System.StringComparison]::Ordinal) + if ($HeadingOffset -le $PreviousHeading -or + $WrittenText.IndexOf($Payload.Heading, $HeadingOffset + 1, + [System.StringComparison]::Ordinal) -ne -1) { + throw "Combined licensing section is missing, duplicated, or out of order: $($Payload.Path)" + } + $ExpectedText = Read-LicenseText -RepositoryRoot $RepositoryRoot -RelativePath $Payload.Path + if ($WrittenText.Substring($Payload.Offset, $Payload.Length) -cne $ExpectedText) { + throw "Combined licensing section does not preserve its input: $($Payload.Path)" + } + $PreviousHeading = $HeadingOffset + } + if (-not $WrittenText.EndsWith($Newline, [System.StringComparison]::Ordinal)) { + throw "Combined licensing document has no final newline: $DestinationPath" + } + Write-Information "Combined licensing document: all $($Inputs.Count) source documents included." -InformationAction Continue +} + +function Test-ByteArraysEqual { + param ( + [byte[]]$First, + [byte[]]$Second + ) + + if ($First.Length -ne $Second.Length) { + return $false + } + for ($Index = 0; $Index -lt $First.Length; $Index++) { + if ($First[$Index] -ne $Second[$Index]) { + return $false + } + } + return $true +} + +function Assert-PackageContent { + param ( + [string]$ArchivePath, + [byte[]]$ReferenceLicenses, + [bool]$Standalone + ) + + $Archive = [System.IO.Compression.ZipFile]::OpenRead($ArchivePath) + try { + $Entries = @{} + $LicenseEntries = @() + $LoaderEntries = @() + foreach ($Entry in $Archive.Entries) { + $Name = $Entry.FullName.Replace('\', '/') + if ($Name -match '(^|/)licenses/' -or + $Name -ieq 'LICENSE.md' -or $Name -ieq 'THIRD_PARTY_NOTICES.md') { + throw "Archive '$ArchivePath' contains a legacy licensing entry: $Name" + } + if ($Name.EndsWith('/')) { + continue + } + if ($Entries.ContainsKey($Name)) { + throw "Archive '$ArchivePath' contains a duplicate entry: $Name" + } + $Entries[$Name] = $Entry + $FileName = ($Name -split '/')[-1] + if ($FileName -ieq 'LICENSES') { + $LicenseEntries += $Name + } + if ($FileName -ieq 'dsound.dll' -or $FileName -ieq 'dinput8.dll') { + $LoaderEntries += $Name + } + } + + if ($LicenseEntries.Count -ne 1 -or $LicenseEntries[0] -ine 'LICENSES') { + throw "Archive '$ArchivePath' must contain exactly one LICENSES file at its existing root." + } + $RequiredEntries = @('P3RFix.asi', 'P3RFix.ini', 'LICENSES') + if ($Standalone) { + $RequiredEntries += 'dsound.dll' + if ($LoaderEntries.Count -ne 1 -or $LoaderEntries[0] -ine 'dsound.dll') { + throw "Archive '$ArchivePath' must contain its standalone loader only at dsound.dll." + } + } else { + $RequiredEntries += 'ModConfig.json' + if ($LoaderEntries.Count -ne 0) { + throw "Archive '$ArchivePath' must not contain the standalone loader." + } + } + foreach ($Name in $RequiredEntries) { + if (-not $Entries.ContainsKey($Name) -or $Entries[$Name].Length -eq 0) { + throw "Archive '$ArchivePath' is missing a nonempty required root entry: $Name" + } + } + + $EntryStream = $Entries['LICENSES'].Open() + $Memory = [System.IO.MemoryStream]::new() + try { + $EntryStream.CopyTo($Memory) + $ArchivedLicenses = $Memory.ToArray() + } finally { + $Memory.Dispose() + $EntryStream.Dispose() + } + if (-not (Test-ByteArraysEqual -First $ArchivedLicenses -Second $ReferenceLicenses)) { + throw "Archive '$ArchivePath' has LICENSES bytes that differ from the generated reference." + } + Write-Information "$(Split-Path $ArchivePath -Leaf): one complete LICENSES; package destinations verified." -InformationAction Continue + return ,$ArchivedLicenses + } finally { + $Archive.Dispose() + } +} + if (-not $Version) { - throw 'Pass a release version, for example: ./create_release.ps1 -Version 1.3.0' + throw 'Pass a release version, for example: ./create_release.ps1 -Version 1.4.0' } if (-not $IsWindows) { throw 'Release builds require Windows with Visual Studio 2022, CMake, and xmake.' @@ -29,6 +284,7 @@ try { $StandaloneDirectory = Join-Path $StagingDirectory 'standalone' $ReloadedDirectory = Join-Path $StagingDirectory 'reloaded' $LoaderDirectory = Join-Path $StagingDirectory 'loader' + $CombinedLicensePath = Join-Path $StagingDirectory 'LICENSES' $ZipPath = Join-Path $BuildDirectory $ZipName $ReloadedZipPath = Join-Path $BuildDirectory 'P3RFix_Reloaded-II.zip' $ReleaseBodyPath = Join-Path $BuildDirectory 'release_body.md' @@ -46,13 +302,16 @@ try { } New-Item -ItemType Directory -Path $StandaloneDirectory, $ReloadedDirectory, $LoaderDirectory, $DownloadDirectory -Force | Out-Null + Write-CombinedLicenseFile -RepositoryRoot $PSScriptRoot -Sections $LicenseSections ` + -DestinationPath $CombinedLicensePath + foreach ($OutputPath in @($ZipPath, $ReloadedZipPath, $ReleaseBodyPath)) { if (Test-Path -LiteralPath $OutputPath) { Remove-Item -LiteralPath $OutputPath -Force } } - Write-Host "Building P3RFix $Version for Windows x64..." + Write-Information "Building P3RFix $Version for Windows x64..." -InformationAction Continue & xmake f -y -p windows -a x64 -m release "--fix_version=$Version" if ($LASTEXITCODE -ne 0) { throw "xmake configuration failed ($LASTEXITCODE)." } & xmake build -y -v @@ -78,9 +337,8 @@ try { } foreach ($PackageDirectory in @($StandaloneDirectory, $ReloadedDirectory)) { - Copy-Item -LiteralPath $BinaryPath, 'P3RFix.ini', 'LICENSE.md', - 'THIRD_PARTY_NOTICES.md' -Destination $PackageDirectory - Copy-Item -LiteralPath 'licenses' -Destination $PackageDirectory -Recurse + Copy-Item -LiteralPath $BinaryPath, 'P3RFix.ini', $CombinedLicensePath ` + -Destination $PackageDirectory } Copy-Item -LiteralPath $LoaderDllPath -Destination (Join-Path $StandaloneDirectory 'dsound.dll') @@ -91,6 +349,17 @@ try { Compress-Archive -Path (Join-Path $StandaloneDirectory '*') -DestinationPath $ZipPath Compress-Archive -Path (Join-Path $ReloadedDirectory '*') -DestinationPath $ReloadedZipPath + Add-Type -AssemblyName System.IO.Compression.FileSystem + $ReferenceLicenses = [System.IO.File]::ReadAllBytes($CombinedLicensePath) + $StandaloneLicenses = Assert-PackageContent -ArchivePath $ZipPath ` + -ReferenceLicenses $ReferenceLicenses -Standalone $true + $ReloadedLicenses = Assert-PackageContent -ArchivePath $ReloadedZipPath ` + -ReferenceLicenses $ReferenceLicenses -Standalone $false + if (-not (Test-ByteArraysEqual -First $StandaloneLicenses -Second $ReloadedLicenses)) { + throw 'Packaged LICENSES documents do not match each other.' + } + Write-Information 'Both packaged LICENSES documents match the generated reference.' -InformationAction Continue + $ReleaseBody = (Get-Content -LiteralPath 'release_body.md' -Raw). Replace('', $ZipName).Replace('', $Version) if (Test-Path -LiteralPath 'CHANGELOG.md') { @@ -105,7 +374,7 @@ try { $ReleaseBody | Set-Content -LiteralPath $ReleaseBodyPath -Encoding utf8NoBOM Remove-Item -LiteralPath $StagingDirectory -Recurse -Force - Write-Host "Created $ZipPath and $ReloadedZipPath" + Write-Information "Created $ZipPath and $ReloadedZipPath" -InformationAction Continue } finally { Pop-Location } diff --git a/release_body.md b/release_body.md index e315bde..f1ef113 100644 --- a/release_body.md +++ b/release_body.md @@ -2,6 +2,7 @@ - Download `` from the release assets below. - Extract its contents into the folder containing `P3R.exe`: `steamapps\common\P3R\P3R\Binaries\Win64` for Steam or `XboxGames\Persona 3 Reload\Content\P3R\Binaries\WinGDK` for Xbox/MS Store. +- The standalone ZIP contains `P3RFix.asi`, `P3RFix.ini`, `dsound.dll`, and one complete `LICENSES` document. An overwrite upgrade can leave older notice files in place. ### Steam Deck/Linux @@ -15,6 +16,7 @@ This option applies to both Windows and Steam Deck/Linux. - If you previously installed the standalone package, back up your configuration and remove its `P3RFix.ini`, `P3RFix.asi`, `dsound.ini` (if present), and `dsound.dll` from the game folder before switching. - Download `P3RFix_Reloaded-II.zip` from the release assets below and drag it onto the Reloaded-II window. See the [Reloaded-II installation guide](https://reloaded-project.github.io/Reloaded-II/QuickStart/) for manual setup. - Enable P3RFix in the Reloaded-II mod list and launch the game through Reloaded-II. +- This ZIP contains the ASI, INI, Reloaded-II metadata, and the same `LICENSES` document. It does not include the standalone loader binary. diff --git a/src/dllmain.cpp b/src/dllmain.cpp index 894243b..d0fd06c 100644 --- a/src/dllmain.cpp +++ b/src/dllmain.cpp @@ -12,6 +12,7 @@ #include #include +#include #include #include @@ -379,11 +380,23 @@ void* UTextureRenderTarget2D_PostLoad_hk(void* pRenderTarget) // Keep the automatic baseline at native quality, then apply the user's multiplier. const auto scale = p3r::render::ComputeRenderTargetScale( iCurrentResY, fScreenPercentage, fRenTexResUserMulti); - static bool bLoggedScaleFallback = false; - if (scale.usedFallback && !bLoggedScaleFallback) { - spdlog::warn("Render Texture 2D Resolution: Using safe scaling defaults for height {}, screen percentage {}, multiplier {}. Further fallback warnings are suppressed.", - iCurrentResY, fScreenPercentage, fRenTexResUserMulti); - bLoggedScaleFallback = true; + if (scale.usedFallback) { + // An unknown startup height is expected. Report invalid runtime values + // separately so that early startup cannot suppress a later warning. + static bool bLoggedInvalidScreenPercentage = false; + if ((!std::isfinite(fScreenPercentage) || fScreenPercentage <= 0.0f) && + !bLoggedInvalidScreenPercentage) { + spdlog::warn("Render Texture 2D Resolution: Invalid screen percentage {}; using 100 for scaling. " + "Further percentage warnings are suppressed.", fScreenPercentage); + bLoggedInvalidScreenPercentage = true; + } + static bool bLoggedInvalidUserMultiplier = false; + if ((!std::isfinite(fRenTexResUserMulti) || fRenTexResUserMulti <= 0.0f) && + !bLoggedInvalidUserMultiplier) { + spdlog::warn("Render Texture 2D Resolution: Invalid user multiplier {}; using 1 for scaling. " + "Further multiplier warnings are suppressed.", fRenTexResUserMulti); + bLoggedInvalidUserMultiplier = true; + } } spdlog::info("Render Texture 2D Resolution: fRenTexResMulti = {}", scale.finalMultiplier); @@ -854,14 +867,16 @@ UINT __stdcall PeekMessageW_Injected(LPMSG lpMsg, HWND hWnd, UINT wMsgFilterMin, if (packet.status == p3r::input::PacketStatus::Invalid) { static bool bLoggedInvalidPacket = false; if (!bLoggedInvalidPacket) { - spdlog::warn("Mouse Fix: Ignoring invalid or incomplete raw-input data. Further invalid-packet warnings are suppressed."); + spdlog::warn("Mouse Fix: Ignoring invalid or incomplete raw-input data. " + "Further invalid-packet warnings are suppressed."); bLoggedInvalidPacket = true; } } else if (packet.status == p3r::input::PacketStatus::AllocationFailed) { static bool bLoggedAllocationFailure = false; if (!bLoggedAllocationFailure) { - spdlog::warn("Mouse Fix: Unable to allocate raw-input storage. Further allocation warnings are suppressed."); + spdlog::warn("Mouse Fix: Unable to allocate raw-input storage. " + "Further allocation warnings are suppressed."); bLoggedAllocationFailure = true; } } From 3c9174f7d11a8f9861f3d91b1580290597948519 Mon Sep 17 00:00:00 2001 From: dev-camo Date: Thu, 1 Oct 2026 15:12:34 -0600 Subject: [PATCH 4/5] Record 1.4.0 validation handoff --- .agents/PLAN.md | 43 +++++++++++----- docs/1.4.0-validation.txt | 103 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 134 insertions(+), 12 deletions(-) create mode 100644 docs/1.4.0-validation.txt diff --git a/.agents/PLAN.md b/.agents/PLAN.md index c612b74..f95ce72 100644 --- a/.agents/PLAN.md +++ b/.agents/PLAN.md @@ -36,31 +36,32 @@ Separate gameplay/menu/background frame-rate limits, new HUD/FOV controls, live - [x] (2026-10-01) Read the entire plan, repository instructions, and ExecPlan skill; inspected the baseline source and submodule pins. Assigned independent scaling, packet-reader, and licensing implementation work while integrating the hooks and Windows validation centrally. - [x] (2026-10-01) Implemented height-based scaling and checked dimension conversion; Windows debug/release behavior tests cover native retention, explicit undersampling, invalid inputs, truncation, and integer boundaries. - [x] (2026-10-01) Implemented checked raw-input reading, an allocation-failure seam, and shared delta application. Independent literal Windows x64 fixtures cover two-call failures, malformed/truncated packets, signed motion, ignored flags, and unchanged accumulators/ownership on rejection. -- [x] (2026-10-01) Moved the unchanged console initialization call after all independent installers and added the attempt-complete marker. Remaining: development harness and real startup/console observations. +- [x] (2026-10-01) Moved the unchanged console initialization call after all independent installers and added the attempt-complete marker. The development harness is complete; real startup/console observations remain. - [x] (2026-10-01) Temporary Windows harness passed 8/8 using verbatim production bodies for `Main`, `EnableConsole`, and the two changed callbacks. Console-disabled, ready, unavailable, delayed, and 200-attempt timeout cases pass; message/render adapters forward once and preserve memory/delta boundaries. Remaining: maintainer startup/console observations in game. - [ ] Verify console-enabled and disabled startup, intro skipping, and repeated console access in game. -- [x] (2026-10-01) Implemented the thirteen-input `LICENSES` generator and archive assertions in the existing release script. Remaining: fault fixtures and real package validation. +- [x] (2026-10-01) Implemented the thirteen-input `LICENSES` generator and archive assertions in the existing release script; fault fixtures and real package validation are complete. - [x] (2026-10-01) Licensing fixtures reject missing, empty, and unlisted notices; preserve BOM/Unicode/trailing-space/no-final-newline handling; reproduce identical document bytes; and reject legacy, incomplete, duplicate-location, missing-INI, and wrong-loader archive contents. Both real ZIPs pass production assertions and an independent Python comparison of all thirteen complete normalized inputs. - [x] (2026-10-01) Updated README, AGENTS, third-party introduction, changelog, and release body. PowerShell ScriptAnalyzer reports zero warning/error diagnostics; selected new C++ files pass clang-format verification and the diff has no whitespace errors. - [x] (2026-10-01) Built the ASI with VS2022/MSVC 14.44.35207 on the authorized VM in debug and release. In each configuration: Unreal 69/69, behavior 35/35, minimal inventory PASS. Behavior sources compile with `/W4 /WX`. - [ ] Compare menu rendering, input, console operation, intro skipping, and ultrawide behavior in the game. - [x] (2026-10-01) Prepared 1.4.0 binary defaults, unchanged Reloaded-II version substitution, generated release notes, and both actual Windows archives. Fresh temporary extractions and comparison with shipped 1.3.0 archives preserve all mod-file destinations and loader bytes. Live installation validation remains outstanding. -- [ ] Write the maintainer instructions and result form under `/tmp/P3RFix/`, recording exact build/hash identities and the pending live cases. -- [ ] Record actual validation evidence and release outcome. +- [x] (2026-10-01) Prepared `/tmp/P3RFix/TESTING.txt`, `/tmp/P3RFix/RESULTS.txt`, both candidate ZIPs, baseline ZIP copies, extracted inventories, and the game-results directory. Runtime/package source is commit `d133132`; ASI and document hashes are recorded in the form and persistent `docs/1.4.0-validation.txt`. +- [x] (2026-10-01) Audited every implementation/automated requirement against current source, transcripts, and actual archive bytes. All automated gates are satisfied; game and actual installation evidence remains missing. +- [ ] Record maintainer live validation and release outcome, resolve failures or unavailable required coverage, remove this plan in the final commit only after testing/confirmation, and push the completed branch. Human review/approval remains required before merge; publication is a subsequent release operation. ## Surprises & Discoveries -The render-target guard does more than contain an obvious repeated variable. It currently reads `iCurrentResX <= 1920 || iCurrentResX <= 1080`, which is equivalent to the first comparison alone. Because the multiplier calculation uses vertical rendering resolution, a mechanical replacement of the second variable would still leave a width-dependent policy. This plan chooses an explicit vertical-resolution policy instead. +The baseline render-target guard did more than contain an obvious repeated variable. It read `iCurrentResX <= 1920 || iCurrentResX <= 1080`, which is equivalent to the first comparison alone. Because the multiplier calculation uses vertical rendering resolution, a mechanical replacement of the second variable would still leave a width-dependent policy. This plan chooses an explicit vertical-resolution policy instead. At 2560x720 and 100% screen percentage, the old automatic multiplier is approximately 0.667 because the width-only guard does not activate. At 1920x2160 and 100%, it instead forces the automatic multiplier to 1. These are results of the examined arithmetic, not claims that the maintainer reproduced a visual defect at those resolutions. -The raw-input path does not check the size-query result. It then logs an unexpected read result but continues to access the packet. Its `new BYTE[dwSize]` null check also does not handle the exception normally raised by an unsuccessful ordinary C++ allocation. Packet validation therefore needs to cover the entire read path, not only add a return after the existing log line. +The baseline raw-input path did not check the size-query result. It logged an unexpected read result but continued to access the packet. Its `new BYTE[dwSize]` null check also did not handle the exception normally raised by an unsuccessful ordinary C++ allocation. Packet validation therefore covers the entire read path, rather than only adding a return after the old log line. The console retry loop runs on P3RFix's initialization worker. It does not itself pause the entire game thread. Its observable ordering problem is that later independent fixes are installed only after the retry finishes. Preserve that distinction in release notes. -The current packaging loop in `create_release.ps1` copies `LICENSE.md`, `THIRD_PARTY_NOTICES.md`, and the entire `licenses/` directory into both release packages. The repository currently has eleven component-license files under `licenses/`, in addition to the project's own license and the provenance document. Some component files contain additional notices or exceptions: the MinHook file includes the Hacker Disassembler Engine notices, and the fmt file includes its optional binary exception. Combining only a generic MIT paragraph would lose material already present in the release inputs. +The baseline packaging loop in `create_release.ps1` copied `LICENSE.md`, `THIRD_PARTY_NOTICES.md`, and the entire `licenses/` directory into both release packages. The repository has eleven component-license files under `licenses/`, in addition to the project's own license and the provenance document. Some component files contain additional notices or exceptions: the MinHook file includes the Hacker Disassembler Engine notices, and the fmt file includes its optional binary exception. Combining only a generic MIT paragraph would lose material already present in the release inputs. -The Windows VM at `camo@192.168.122.141` accepts SSH and uses a Windows command shell. Tool locations and a dedicated validation checkout are being established. Game execution is expressly reserved for the maintainer; automated validation must stop at a concrete package and test handoff. +The Windows VM at `camo@192.168.122.141` accepts SSH and uses a Windows command shell. Tool locations and a dedicated validation checkout have been established below. Game execution is expressly reserved for the maintainer; automated validation stops at a concrete package and test handoff. The validation tools from the prior 1.3.0 work are available at `C:\Users\camo\Projects\P3RFix-sdk-refactor\tools`: portable PowerShell, xmake 3.1.1, Python 3.13.7, and Git. CMake is bundled with `C:\BuildTools2022`, and MSVC is 14.44.35207. This release uses a separate checkout at `C:\Users\camo\Projects\P3RFix-1.4.0\source`, with transcripts under its sibling `evidence` directory. The earlier baseline/refactor checkouts are preserved. @@ -102,7 +103,7 @@ The repository examined is `/home/camo/Projects/P3RFix`. On a Windows build mach `src/unreal/Integration.hpp` and `src/unreal/Integration.cpp` expose focused render-target and console operations. Private memory layouts and reflected calls live in `src/unreal/detail/`. The generated SDK is no longer a build dependency. Keep that separation. The existing independent byte fixtures in `tests/unreal_integration_tests.cpp` verify memory boundaries and console behavior; they do not currently test the scaling decision or Windows packet-reader failure paths. -`xmake.lua` defines the existing ASI and `unreal-integration-tests` targets. `create_release.ps1` builds and packages a version supplied through `-Version`. The version default is also present in `xmake.lua` and as a fallback macro in `src/dllmain.cpp`. `.github/workflows/build.yml` checks inventory and runs the Unreal tests before packaging. `P3RFix.ini`, `README.md`, `CHANGELOG.md`, `release_body.md`, and `assets/r2-package/ModConfig.json` supply the player-facing configuration, documentation, and package metadata. +`xmake.lua` defines the ASI, `unreal-integration-tests`, and new `fix-behavior-tests` targets. The behavior runner loads `tests/render_scaling_tests.hpp` and `tests/raw_mouse_packet_tests.hpp`. `create_release.ps1` builds and packages a version supplied through `-Version`. The version default is also present in `xmake.lua` and as a fallback macro in `src/dllmain.cpp`. `.github/workflows/build.yml` checks inventory and runs both test targets in debug/release before packaging. `P3RFix.ini`, `README.md`, `CHANGELOG.md`, `release_body.md`, and `assets/r2-package/ModConfig.json` supply the player-facing configuration, documentation, and package metadata. `LICENSE.md` contains P3RFix's MIT license and the original/current-maintenance copyright notices. `THIRD_PARTY_NOTICES.md` identifies compiled dependencies, the pinned standalone loader and its dependencies, and retained Unreal integration provenance. The eleven component texts under `licenses/` are the inputs to preserve in full. They remain source-repository files; the release script will combine their contents into one package document instead of copying the source directory into a player's game installation. @@ -233,9 +234,19 @@ Define a reader function type compatible with the actual Windows function so tes MouseDelta delta; }; + using RawInputBuffer = std::unique_ptr; + using RawInputAllocator = RawInputBuffer (*)(std::size_t wordCount) noexcept; + PacketResult ReadRelativeMousePacket( HRAWINPUT input, - RawInputReader reader) noexcept; + RawInputReader reader, + RawInputAllocator allocator = nullptr) noexcept; + + void ApplyRelativeMousePacket( + const PacketResult& packet, + float& accumulatedX, + float& accumulatedY, + bool& lastValidInputWasFromMouse) noexcept; Production passes `GetRawInputData`; tests pass a controlled implementation of that same contract. An aligned array of 32-bit words is one simple way to own the requested byte capacity. Check the rounding calculation before allocating. Parse only the bytes the API reports as copied. @@ -491,6 +502,8 @@ Keep implementation evidence with the release work: the final source diff, debug Captured automated evidence is under `/tmp/P3RFix/evidence/`, mirrored from `C:\Users\camo\Projects\P3RFix-1.4.0\evidence`. The candidate archives and generated release text are under `/tmp/P3RFix/packages/`. `windows-validation.txt` records both ASI builds and both sets of 69/69 integration and 35/35 behavior results; `hook-harness.txt` records 8/8 development-only observations; `license-checks.txt`, `powershell-lint.txt`, and `packaging.txt` record the packaging checks. `archive-inspection.txt` independently verifies exact inventories, all thirteen payloads, configuration/loader preservation, Reloaded-II metadata, x64 PE identity, and matching documents/binaries. No actual-game evidence exists yet. +`docs/1.4.0-validation.txt` permanently records the verified candidate and remaining gates. `/tmp/P3RFix/TESTING.txt` provides concrete install/rollback instructions and the rendering, input, startup, viewport, and package cases; `/tmp/P3RFix/RESULTS.txt` records common environment/build identifiers and per-case outcomes. Baseline archives are under `/tmp/P3RFix/baseline/`, and the maintainer can store logs/images under `/tmp/P3RFix/game-results/`. The candidate runtime/packaging source commit is `d133132`; subsequent handoff/evidence commits change documentation only. The supplied Steam `P3R.exe` was read for identification (SHA256 `88244bade1988eafd8ababd66d4c2227091c4c6c253e6455c7097b1b1e5299b8`), never executed. + The verified standalone inventory is `dsound.dll` (1,198,304 bytes), `LICENSES` (25,315), `P3RFix.asi` (1,203,200), and `P3RFix.ini` (2,876), all at the archive root. Reloaded-II replaces `dsound.dll` with `ModConfig.json` (945 bytes), retaining the other three paths. Both documents have SHA256 `f953af8a4c4e88df3c592a3fbf8dbec9cb37bddba1b17633b9fa38d3e2f1aa54`; the ASI hash is `c64347fb3d0f687a44bda30a5f7e796a538a956cee7c2b51a48d56dd882f91cf`. ZIP hashes are recorded in `archive-inspection.txt` and may change if packaging is repeated; compare actual bytes before handoff. The following are expected example results, not captured output: @@ -528,7 +541,7 @@ The proposed new sources are `src/render/Scaling.hpp`, `src/render/Scaling.cpp`, Use the current C++ standard setting and MSVC ABI. ABI means the binary rules for calling functions and laying out SDK structures. In particular, keep the correct Windows calling convention for the injected reader and use the Windows SDK's own `RAWINPUTHEADER` and `RAWMOUSE` definitions. -The file names and signatures above are an implementation proposal, not files already created by this documentation task. If a smaller equivalent organization fits the code better during implementation, record that decision and update every affected command and interface description rather than leaving stale names in the plan. +The helper sources and adjacent fixture headers now exist and use the interfaces described above. Future changes to these boundaries must update the decisions, commands, and interface descriptions together so this plan remains an accurate resume record. Licensing consolidation uses only the existing PowerShell 7.2-or-newer release environment and built-in .NET text/ZIP support. The ordered generator and archive-content assertion live in `create_release.ps1`; no new external dependency, compiler, source target, or runtime DLL is required. `LICENSE.md`, `THIRD_PARTY_NOTICES.md`, and all eleven source component-license files remain the maintained inputs. `LICENSES` is generated package content, not another independent source-of-truth file to edit by hand. @@ -536,7 +549,9 @@ Licensing consolidation uses only the existing PowerShell 7.2-or-newer release e All four changes are implemented. Both Windows configurations build the ASI and pass Unreal 69/69, behavior 35/35, and the minimal inventory; the development harness passes 8/8. Both real ZIPs pass licensing and destination checks, independent thirteen-input comparisons, and fresh temporary extraction. PowerShell lint reports zero warning/error diagnostics. README, repository instructions, changelog, and release text describe the new behavior and package contract; the changelog truthfully marks 1.4.0 unreleased. These results establish arithmetic, simulated API failures, synthetic adapter ordering, and archive contents; they do not establish gameplay or loader compatibility. -Resume from the current branch and inspect the worktree. Local validation scripts and console output are under `/tmp/P3RFix/`; `remote.py` uploads a PowerShell script to the dedicated VM directory and runs it. Next: finish the maintainer game-test form, audit the full plan against actual evidence, commit the release preparation, and hand over the candidate archives for testing. Live comparison of rendering/input/startup/console/intro/viewport behavior, readable Windows license presentation, and loading through the supported installation methods require maintainer results. Do not run the game, delete this plan, push the branch, or claim completion before the required maintainer validation arrives. No release tag or publication has been attempted. +The prepared candidate has reached the user-required game-test handoff. Implementation and automated/package verification are finished and committed, but release completion is unproven because actual game/installation results are missing. This is essential external validation reserved for the maintainer, not a failed build or an exhausted implementation path. The smallest input needed is the completed `/tmp/P3RFix/RESULTS.txt` with supporting startup/render logs and representative visual comparisons. + +Resume from the current branch and inspect the worktree, plan, persistent validation record, current package hashes, and any returned user results. Local validation scripts and console output are under `/tmp/P3RFix/`; `remote.py` uploads a PowerShell script to the dedicated VM directory and runs it. Next: reconcile each live acceptance case with the maintainer's evidence, investigate any regression, and obtain coverage for unverified required cases. Update this plan and `docs/1.4.0-validation.txt` with actual findings. After successful user validation/confirmation, remove this plan in the final commit and push branch `1.4.0` to origin. Do not run the game, delete this plan early, push before completion, merge without human approval, or infer a passed case from missing/unavailable evidence. No release tag or publication has been attempted. Revision note, 2026-10-01: renamed the original pre-v2 notes for a 1.4.0 release and expanded them into a self-contained maintenance plan at the maintainer's request. The scaling policy, failure handling, startup ordering, implementation boundaries, validation, and release criteria are now explicit; the established console, viewport, packaging, and MSVC constraints are retained. @@ -545,3 +560,7 @@ Revision note, 2026-10-01: added the maintainer's fourth 1.4.0 change, replacing Revision note, 2026-10-01: recorded the recovered implementation state, completed Windows validation, test seams, and dedicated VM/tool paths. Split implementation from outstanding live validation so future continuations cannot mistake compilation or synthetic tests for confirmed gameplay. Revision note, 2026-10-01: recorded verified licensing fault paths, the actual package inventories and hashes, zero-diagnostic linting, and production-body development harness results. Live game and installation checks remain explicit release gates; no evidence has been inferred from compilation or archive success. + +Revision note, 2026-10-01: completed the concrete maintainer handoff and permanent candidate validation record. The release remains unreleased and the goal remains active pending actual game/installation results; final plan removal and branch push retain the user's explicit testing/confirmation gate. + +Revision note, 2026-10-01: the final pre-handoff reread reconciled stale baseline wording and progress notes with the implemented state and documented the allocator/delta application interfaces in full. This is documentation alignment only; acceptance criteria and candidate runtime/package bytes are unchanged. diff --git a/docs/1.4.0-validation.txt b/docs/1.4.0-validation.txt new file mode 100644 index 0000000..1aabef3 --- /dev/null +++ b/docs/1.4.0-validation.txt @@ -0,0 +1,103 @@ +P3RFix 1.4.0 validation record +Date: 2026-10-01 +Status: candidate awaiting maintainer game and installation validation +Runtime and packaging source commit: d133132 (branch 1.4.0) + +Implemented scope +----------------- +Height-based automatic render-target scaling with native-quality floor and +explicit user multipliers; checked Windows raw-input reads with preserved valid +motion interpretation; independent installation before optional console wait; +one generated complete LICENSES document in both existing package formats. + +Automated verification +---------------------- +Authorized VM: camo@192.168.122.141 +Checkout: C:\Users\camo\Projects\P3RFix-1.4.0\source +MSVC: Visual Studio 2022, 14.44.35207, Windows x64 +xmake: 3.1.1; Python: 3.13.7; portable PowerShell and VS2022 CMake + +In each of debug and release: + P3RFix.asi builds successfully using the repository's MSVC toolchain. + unreal-integration-tests: 69/69 checks passed. + fix-behavior-tests: 35/35 checks passed; compiled with /W4 /WX. + tools/unreal_inventory.py --phase minimal: PASS; zero legacy references. + +Temporary production-body harness: 8/8 checks passed. It extracts verbatim Main, +EnableConsole, PeekMessageW_Injected, and UTextureRenderTarget2D_PostLoad_hk bodies +from dllmain.cpp. Controlled discovery models console-disabled, ready, +unavailable, delayed, and 200-attempt timeout cases with real 100 ms sleeps. +Independent installers precede discovery. Message/render callbacks forward once; +valid signed movement accumulates once, rejection leaves state unchanged, and +real integration writes respect target/capture boundaries. This establishes +synthetic behavior and ordering; installers and console discovery use fixtures. + +Production packaging invocation: ./create_release.ps1 -Version 1.4.0 succeeds. +Licensing fixtures verify repeatable bytes, missing/empty/unlisted-input failure, +BOM removal, Unicode and trailing-space preservation, and no-final-newline +separation. Archive fixtures reject legacy licensing entries, incomplete text, +an additional nested document, a missing INI, and a loader in Reloaded-II. +All streams close before fixture cleanup. PowerShell ScriptAnalyzer reports zero +warning/error diagnostics. New C++ sources pass clang-format verification, and +git diff --check passes. The analyzer and development harness are temporary +validation tools; neither adds a release dependency or player setting. + +Actual package evidence +----------------------- +Standalone ZIP root: + dsound.dll 1,198,304 bytes + LICENSES 25,315 bytes + P3RFix.asi 1,203,200 bytes + P3RFix.ini 2,876 bytes + +Reloaded-II ZIP root: + LICENSES 25,315 bytes + ModConfig.json 945 bytes + P3RFix.asi 1,203,200 bytes + P3RFix.ini 2,876 bytes + +Production ZIP assertions and an independent Python inspection verify exactly +one complete LICENSES, all thirteen maintained inputs once in manifest order, +UTF-8 without BOM, CRLF throughout, final newline, matching documents and ASIs, +Windows x64 DLL identity, and absence of separate legacy notice files/tree. +Fresh extraction into temporary directories preserves each package's mod paths. +Comparison with the shipped 1.3.0 ZIPs verifies the same binary/configuration +destinations and identical standalone loader bytes. INI text is unchanged; the +baseline ZIP uses CRLF (2,954 bytes), while the Linux-sourced candidate uses LF +(2,876). Reloaded-II metadata changes only ModVersion to 1.4.0. + +SHA256: + LICENSES + f953af8a4c4e88df3c592a3fbf8dbec9cb37bddba1b17633b9fa38d3e2f1aa54 + P3RFix.asi + c64347fb3d0f687a44bda30a5f7e796a538a956cee7c2b51a48d56dd882f91cf + P3RFix_1.4.0.zip + 7264a1b88c610f23582123a7a0203bc1f35ed50553869a7a8271c37a71608bca + P3RFix_Reloaded-II.zip + cba7ba6fbb0a43419dc516c3a5065ee5120096991e9863f97853e4ee933ae680 + +Local evidence and handoff +-------------------------- +/tmp/P3RFix/evidence/: Windows build/test, harness, licensing, lint, packaging, +and independent archive-inspection transcripts. +/tmp/P3RFix/packages/: both candidate ZIPs and generated release_body.md. +/tmp/P3RFix/TESTING.txt: installation and before/after game checks. +/tmp/P3RFix/RESULTS.txt: maintainer result form. +Temporary scripts remain under /tmp/P3RFix/; VM transcripts are under the +checkout's sibling evidence directory. The repository plan is the resume record. + +Remaining release gates +----------------------- +The agent has not executed the game. Actual rendering at below-1080p, 1440p +ultrawide, and 2160p; equal-height aspect ratios; screen percentage and user +undersampling; resizing/minimization; valid mouse/controller behavior; intro +skip with console; repeated console access; actual startup logs; readable +license presentation; and loading through the supported install methods await +maintainer results. An unavailable executable variant must be reported as such, +and synthetic tests cannot establish its compatibility. + +Retain .agents/PLAN.md until user testing and confirmation validate the release. +Then record live evidence and any coverage limits, make the final plan-removal +commit, and push the completed branch to origin. Human review and approval are +required before merging into main. No tag, release publication, or merge has +been performed as part of this candidate preparation. From e8b764f612b1b1a8f92563a62cacdd3b15f4219b Mon Sep 17 00:00:00 2001 From: dev-camo Date: Thu, 1 Oct 2026 15:33:09 -0600 Subject: [PATCH 5/5] Finish 1.4.0 validation The maintainer confirmed that all planned game and installation checks passed and approved final cleanup. Detailed session logs and visual captures were not retained. Automated validation passed on Windows x64 with MSVC in debug and release: 69/69 Unreal integration checks and 35/35 fix behavior checks per mode, plus the minimal source inventory. The production-hook harness passed 8/8 checks. License generation, archive fault checks, package inspection, and PowerShell lint also passed. Remove the completed execution plan and temporary validation document. --- .agents/PLAN.md | 566 -------------------------------------- docs/1.4.0-validation.txt | 103 ------- 2 files changed, 669 deletions(-) delete mode 100644 .agents/PLAN.md delete mode 100644 docs/1.4.0-validation.txt diff --git a/.agents/PLAN.md b/.agents/PLAN.md deleted file mode 100644 index f95ce72..0000000 --- a/.agents/PLAN.md +++ /dev/null @@ -1,566 +0,0 @@ -# P3RFix 1.4.0 release plan: four focused maintenance changes - -This release should make automatic menu texture scaling consistent, reject invalid raw-input packets safely, install independent fixes before the optional console startup wait, and replace the release ZIPs' scattered license files with one complete `LICENSES` file. Players should retain the working console, the validated viewport-based ultrawide behavior, their existing configuration, and the current installation method. - -Target release: **1.4.0**. Baseline examined: **1.3.0**, repository commit `6d55ef6`, on 2026-10-01. Execution began from branch `1.4.0`, commit `d352a6b`, with only this untracked plan in the working tree. No target release date is assigned and no release has been published. - -This ExecPlan is a living document. Update `Progress`, `Surprises & Discoveries`, `Decision Log`, and `Outcomes & Retrospective` as implementation and validation proceed. Source line numbers are orientation aids from the examined baseline; find the named functions again if intervening changes move them. - -## Purpose / Big Picture - -The four changes belong in a small maintenance release, separate from the four larger changes proposed for 1.5.0. Automatic render-target scaling should retain the intended native-quality baseline on low-height displays while honoring a user's explicit multiplier. A failed Windows raw-input read should have no effect on the camera and should not lead to parsing uninitialized or incomplete storage. Enabling the developer console should not put unrelated hook installation behind its engine-discovery retry loop. Installation should leave one readable licensing document alongside the existing package files, with the project's own license and every retained dependency notice together instead of an entire `licenses/` directory. - -A render target is a texture that the game draws into before displaying it elsewhere, such as a menu character preview or a captured menu background. A hook is a small interception installed at a game or Windows function so P3RFix can inspect or adjust behavior. These changes affect P3RFix's decisions around those existing hooks; they do not replace the game's renderer or input system. - -Success requires meaningful automated checks for the scaling and packet-handling behavior, inspection of both archives for complete consolidated notices, and an actual game comparison against 1.3.0. Compilation alone does not establish compatibility with a game executable, show that a menu texture still looks correct, or establish that the packaged license bundle is complete. - -## Release scope and established facts - -The release contains exactly four primary changes: automatic render-target scaling, raw-input read validation, startup ordering, and consolidated release licensing. Version metadata, release notes, and narrowly necessary checks accompany those changes. Keep existing configuration sections and keys intact. No new player-facing setting is necessary for these changes. - -The maintainer has verified that the developer console works in 1.3.0. The historical console crash in the shipped upstream 1.2.4 release is not an outstanding confirmed bug in the current baseline. Its cause remains unconfirmed: the newer bundled Ultimate ASI Loader and the eleven upstream commits between 1.2.4 and the maintenance takeover are possible explanations. Do not attribute that recovery to the focused Unreal refactor without evidence. - -The maintainer has also validated the viewport-based ultrawide fix through extended gameplay. Commit `cc2582b36b8c3fe37a47781ea5fc98638c3f75b9`, shipped in 1.2.5, reads the actual viewport dimensions. Preserve that acquisition path and its handling of empty or minimized viewports. Do not restore requested-resolution tracking, multiply width by screen percentage to guess a display width, or replace it with the desktop-width workaround from Codeberg PR #5. - -The shipping installation method and package placement are established as correct. A historical user installing files in the wrong location is not a reason to redesign the archives. The maintainer has separately requested one deliberate package-content improvement: replace the licensing directory and separate licensing documents with a single `LICENSES` file. Preserve the established binary/configuration destinations, archive names, loader placement, and installation procedure while making that specific change. If documentation is clarified while preparing this release, describe the actual package accurately and keep the final files beside `P3R.exe` for the standalone installation. - -The binary remains a **Windows x64 ASI built with MSVC**. Preserve the existing Visual Studio 2022, Windows SDK, xmake, CMake, PowerShell, dependency pins, and release workflows. Proton testing means testing the Windows binary under Proton; it does not mean adding a Linux binary or Linux cross-compilation. Codeberg PR #5 and its recommendations are excluded from this work. - -Separate gameplay/menu/background frame-rate limits, new HUD/FOV controls, live configuration, mouse sensitivity redesign, smoothing, per-target graphics budgets, and new platform support are not part of 1.4.0. - -## Progress - -- [x] (2026-10-01) Reviewed the three original proposals against the 1.3.0 source and preserved the maintainer's current validation and build constraints. -- [x] (2026-10-01) Renamed and expanded the former `pre-v2-fixes.md` notes into a targeted 1.4.0 release plan. -- [x] (2026-10-01) Added the maintainer's fourth release change: one generated `LICENSES` document in each ZIP, preserving existing notices and the source license files. -- [x] (2026-10-01) Read the entire plan, repository instructions, and ExecPlan skill; inspected the baseline source and submodule pins. Assigned independent scaling, packet-reader, and licensing implementation work while integrating the hooks and Windows validation centrally. -- [x] (2026-10-01) Implemented height-based scaling and checked dimension conversion; Windows debug/release behavior tests cover native retention, explicit undersampling, invalid inputs, truncation, and integer boundaries. -- [x] (2026-10-01) Implemented checked raw-input reading, an allocation-failure seam, and shared delta application. Independent literal Windows x64 fixtures cover two-call failures, malformed/truncated packets, signed motion, ignored flags, and unchanged accumulators/ownership on rejection. -- [x] (2026-10-01) Moved the unchanged console initialization call after all independent installers and added the attempt-complete marker. The development harness is complete; real startup/console observations remain. -- [x] (2026-10-01) Temporary Windows harness passed 8/8 using verbatim production bodies for `Main`, `EnableConsole`, and the two changed callbacks. Console-disabled, ready, unavailable, delayed, and 200-attempt timeout cases pass; message/render adapters forward once and preserve memory/delta boundaries. Remaining: maintainer startup/console observations in game. -- [ ] Verify console-enabled and disabled startup, intro skipping, and repeated console access in game. -- [x] (2026-10-01) Implemented the thirteen-input `LICENSES` generator and archive assertions in the existing release script; fault fixtures and real package validation are complete. -- [x] (2026-10-01) Licensing fixtures reject missing, empty, and unlisted notices; preserve BOM/Unicode/trailing-space/no-final-newline handling; reproduce identical document bytes; and reject legacy, incomplete, duplicate-location, missing-INI, and wrong-loader archive contents. Both real ZIPs pass production assertions and an independent Python comparison of all thirteen complete normalized inputs. -- [x] (2026-10-01) Updated README, AGENTS, third-party introduction, changelog, and release body. PowerShell ScriptAnalyzer reports zero warning/error diagnostics; selected new C++ files pass clang-format verification and the diff has no whitespace errors. -- [x] (2026-10-01) Built the ASI with VS2022/MSVC 14.44.35207 on the authorized VM in debug and release. In each configuration: Unreal 69/69, behavior 35/35, minimal inventory PASS. Behavior sources compile with `/W4 /WX`. -- [ ] Compare menu rendering, input, console operation, intro skipping, and ultrawide behavior in the game. -- [x] (2026-10-01) Prepared 1.4.0 binary defaults, unchanged Reloaded-II version substitution, generated release notes, and both actual Windows archives. Fresh temporary extractions and comparison with shipped 1.3.0 archives preserve all mod-file destinations and loader bytes. Live installation validation remains outstanding. -- [x] (2026-10-01) Prepared `/tmp/P3RFix/TESTING.txt`, `/tmp/P3RFix/RESULTS.txt`, both candidate ZIPs, baseline ZIP copies, extracted inventories, and the game-results directory. Runtime/package source is commit `d133132`; ASI and document hashes are recorded in the form and persistent `docs/1.4.0-validation.txt`. -- [x] (2026-10-01) Audited every implementation/automated requirement against current source, transcripts, and actual archive bytes. All automated gates are satisfied; game and actual installation evidence remains missing. -- [ ] Record maintainer live validation and release outcome, resolve failures or unavailable required coverage, remove this plan in the final commit only after testing/confirmation, and push the completed branch. Human review/approval remains required before merge; publication is a subsequent release operation. - -## Surprises & Discoveries - -The baseline render-target guard did more than contain an obvious repeated variable. It read `iCurrentResX <= 1920 || iCurrentResX <= 1080`, which is equivalent to the first comparison alone. Because the multiplier calculation uses vertical rendering resolution, a mechanical replacement of the second variable would still leave a width-dependent policy. This plan chooses an explicit vertical-resolution policy instead. - -At 2560x720 and 100% screen percentage, the old automatic multiplier is approximately 0.667 because the width-only guard does not activate. At 1920x2160 and 100%, it instead forces the automatic multiplier to 1. These are results of the examined arithmetic, not claims that the maintainer reproduced a visual defect at those resolutions. - -The baseline raw-input path did not check the size-query result. It logged an unexpected read result but continued to access the packet. Its `new BYTE[dwSize]` null check also did not handle the exception normally raised by an unsuccessful ordinary C++ allocation. Packet validation therefore covers the entire read path, rather than only adding a return after the old log line. - -The console retry loop runs on P3RFix's initialization worker. It does not itself pause the entire game thread. Its observable ordering problem is that later independent fixes are installed only after the retry finishes. Preserve that distinction in release notes. - -The baseline packaging loop in `create_release.ps1` copied `LICENSE.md`, `THIRD_PARTY_NOTICES.md`, and the entire `licenses/` directory into both release packages. The repository has eleven component-license files under `licenses/`, in addition to the project's own license and the provenance document. Some component files contain additional notices or exceptions: the MinHook file includes the Hacker Disassembler Engine notices, and the fmt file includes its optional binary exception. Combining only a generic MIT paragraph would lose material already present in the release inputs. - -The Windows VM at `camo@192.168.122.141` accepts SSH and uses a Windows command shell. Tool locations and a dedicated validation checkout have been established below. Game execution is expressly reserved for the maintainer; automated validation stops at a concrete package and test handoff. - -The validation tools from the prior 1.3.0 work are available at `C:\Users\camo\Projects\P3RFix-sdk-refactor\tools`: portable PowerShell, xmake 3.1.1, Python 3.13.7, and Git. CMake is bundled with `C:\BuildTools2022`, and MSVC is 14.44.35207. This release uses a separate checkout at `C:\Users\camo\Projects\P3RFix-1.4.0\source`, with transcripts under its sibling `evidence` directory. The earlier baseline/refactor checkouts are preserved. - -The first behavior-test build exposed Windows `min`/`max` macros after including the raw-input SDK header. Defining `NOMINMAX` in the test runner, as the production precompiled header already does, fixed the conflict. Both complete Windows configurations subsequently passed. SSH command length also limits large encoded PowerShell scripts; the handoff helper now uploads scripts and executes `-File` instead. - -The empty-notice fixture found that .NET's culture-sensitive `StartsWith` treats the encoding marker as ignorable, causing an empty string to pass that check and fail at `Substring(1)`. An ordinal comparison now produces the required path-specific empty-input error. The fixture output must also be outside the fixture's `licenses/` directory: Windows treats a root file named `LICENSES` and that source directory as the same name. Production already separates source inputs and staging output correctly. - -The shipped 1.3.0 INI is 2,954 bytes (CRLF) and this Linux-sourced Windows validation package's INI is 2,876 bytes (LF). An independent comparison confirms that line endings are the only difference; no section, key, value, or comment changed. The document generator normalizes all maintained notice inputs, so both packages have identical 25,315-byte `LICENSES` files regardless of input line endings. - -## Decision Log - -Decision: target the original three behavior fixes and the subsequently requested licensing cleanup at 1.4.0 rather than a pre-v2 backlog. Rationale: the maintainer wants a concrete maintenance release and separate treatment from the 1.5.0 feature proposals. Date/author: 2026-10-01, maintainer direction recorded by Codex. - -Decision: the automatic multiplier has a minimum of 1, and explicit user multipliers are applied afterward. Rationale: preserve native render-target quality automatically without removing the user's deliberate ability to undersample. Date/author: 2026-10-01, proposed implementation design by Codex. - -Decision: preserve the current raw-mouse interpretation in this release. Rationale: packet-read safety can be established independently of the larger camera and device-handoff work proposed for 1.5.0. Date/author: 2026-10-01, proposed implementation design by Codex. - -Decision: solve console-related ordering by moving its existing wait after independent installation on the same initialization worker. Rationale: another thread or a console implementation rewrite is unnecessary for the stated problem and would add concurrency risk to a working feature. Date/author: 2026-10-01, proposed implementation design by Codex. - -Decision: retain Windows x64/MSVC, the current build tooling, working viewport discovery, working console behavior, archive names, and binary/configuration placement; change only the specified licensing payload. Rationale: these are established project constraints and validated behavior, with an explicit maintainer request to simplify release licensing. Date/author: 2026-10-01, maintainer direction recorded by Codex. - -Decision: use the exact extensionless filename `LICENSES` for the combined release document. Rationale: the plural name describes the project's license plus multiple component licenses and notices without implying that one license governs every component. The maintainer authorized either `LICENSE` or `LICENSES`; selecting one spelling keeps both packages and their documentation consistent. Date/author: 2026-10-01, maintainer direction and proposed filename recorded by Codex. - -Decision: generate `LICENSES` from the existing checked-in license texts and provenance during packaging, using the existing PowerShell script, and put the same combined contents in both package formats. Rationale: keeping editable source notices avoids a second manually maintained copy of every license, while one shared generated output prevents package-specific omissions. Loader sections will explicitly state that the loader binary is bundled only with the standalone package. Date/author: 2026-10-01, proposed implementation design by Codex. - -Decision: reserve actual game runs and install-method compatibility confirmation for the maintainer, and retain this plan until that confirmation arrives. Rationale: the execution instructions prohibit the agent from running the game and require user validation before deleting the plan or completing/pushing the branch. Prepare instructions, packages, and a result form under `/tmp/P3RFix/`. Date/author: 2026-10-01, maintainer instruction recorded by Codex. - -Decision: expose a small nothrow raw-input allocator seam and delta-application helper, with a separate fixture header alongside the scaling cases. Rationale: allocation failure can be proven without exhausting memory, and tests exercise the shipped accumulator behavior rather than duplicating it. Date/author: 2026-10-01, Codex. - -Decision: rate-limit invalid screen-percentage and user-multiplier warnings independently, without treating an unknown startup height as an error. Rationale: early viewport discovery is expected and must not suppress reporting a later invalid runtime observation. Date/author: 2026-10-01, Codex. - -Decision: use PowerShell's information stream for packaging diagnostics and a singular internal `Assert-PackageContent` name. Rationale: keep helper return bytes separate from diagnostics and pass ScriptAnalyzer without suppression or a new release dependency. The analyzer is installed only in the VM's disposable validation tools. Date/author: 2026-10-01, Codex. - -## Context and Orientation - -The repository examined is `/home/camo/Projects/P3RFix`. On a Windows build machine, use that machine's checkout path rather than attempting to use this Linux path. The source produces a Windows ASI: a DLL with an `.asi` filename loaded into the game by the standalone loader or Reloaded-II. - -`src/dllmain.cpp` contains configuration parsing, startup, signature discovery, and most hook callbacks. `Main` is the initialization worker created by `DllMain`. `Resolution` installs the viewport-size hook and screen-percentage hook. `UTextureRenderTarget2D_PostLoad_hk` calculates new texture dimensions and forwards to the original game function. `PeekMessageW_Injected` observes messages after the original Windows message function returns. - -`src/unreal/Integration.hpp` and `src/unreal/Integration.cpp` expose focused render-target and console operations. Private memory layouts and reflected calls live in `src/unreal/detail/`. The generated SDK is no longer a build dependency. Keep that separation. The existing independent byte fixtures in `tests/unreal_integration_tests.cpp` verify memory boundaries and console behavior; they do not currently test the scaling decision or Windows packet-reader failure paths. - -`xmake.lua` defines the ASI, `unreal-integration-tests`, and new `fix-behavior-tests` targets. The behavior runner loads `tests/render_scaling_tests.hpp` and `tests/raw_mouse_packet_tests.hpp`. `create_release.ps1` builds and packages a version supplied through `-Version`. The version default is also present in `xmake.lua` and as a fallback macro in `src/dllmain.cpp`. `.github/workflows/build.yml` checks inventory and runs both test targets in debug/release before packaging. `P3RFix.ini`, `README.md`, `CHANGELOG.md`, `release_body.md`, and `assets/r2-package/ModConfig.json` supply the player-facing configuration, documentation, and package metadata. - -`LICENSE.md` contains P3RFix's MIT license and the original/current-maintenance copyright notices. `THIRD_PARTY_NOTICES.md` identifies compiled dependencies, the pinned standalone loader and its dependencies, and retained Unreal integration provenance. The eleven component texts under `licenses/` are the inputs to preserve in full. They remain source-repository files; the release script will combine their contents into one package document instead of copying the source directory into a player's game installation. - -Use small independent helpers for arithmetic and packet validation so tests exercise the shipped implementation. Keep the hook callbacks as adapters around those helpers. Do not turn this release into a general reorganization of `dllmain.cpp`. - -## Change 1: make automatic render-target scaling consistent - -### Current behavior and defect - -The relevant calculation begins around `src/dllmain.cpp:378`: - - float fOptimalRenTexResMulti = - (iCurrentResY * (fScreenPercentage / 100)) / 1080; - - if (iCurrentResX <= 1920 || iCurrentResX <= 1080) - fOptimalRenTexResMulti = 1.0f; - -The function then applies `[Render Texture Resolution].Multiplier`, clamps the result to 0.25 through 4, multiplies the original target's width and height, and updates the capture-size globals when the target is RGBA16F. RGBA16F is the floating-point texture format used by the existing code to select those capture dimensions. - -The problem is the mismatch between the height-derived calculation and the redundant width-only guard. The release should define the automatic baseline consistently for standard, ultrawide, and narrow resolutions. - -### Required policy - -For a positive viewport height and finite positive screen percentage, calculate the effective vertical rendering resolution as: - - effectiveHeight = viewportHeight * screenPercentage / 100.0 - automaticMultiplier = max(1.0, effectiveHeight / 1080.0) - finalMultiplier = clamp(automaticMultiplier * userMultiplier, 0.25, 4.0) - -Apply the upper limit to the final combined multiplier. Do not clamp the automatic multiplier to 4 before applying the user value: a large effective height combined with a multiplier below 1 should retain its intended combined meaning. - -The user multiplier remains relative to the automatic baseline. A value of 1 uses automatic scaling; a value of 0.5 halves that baseline; a value of 2 doubles it, subject to the final limit. This preserves the existing documented meaning of the setting. - -If the viewport height has not yet become valid, use an automatic baseline of 1 until a valid height is available. The existing viewport code already keeps the last valid dimensions during minimization; do not reset those dimensions here. A finite nonpositive or nonfinite screen percentage cannot safely drive arithmetic; use 100 for this calculation and report the invalid observation without changing the user's INI on disk. Use 1 for an invalid nonpositive or nonfinite user multiplier at this boundary. Ordinary finite user values should continue to be constrained to the documented 0.25-to-4 range by configuration validation. - -These defensive fallbacks cover unexpected runtime input to the helper. They are not an invitation to change all configuration parsing in 1.4.0. - -### Proposed implementation boundary - -Create `src/render/Scaling.hpp` and `src/render/Scaling.cpp` with a small pure arithmetic interface under `p3r::render`. A pure function makes its decision only from its arguments; it does not read game memory, perform a pattern scan, or change globals. - -Use the following interface, with a result field or equivalent diagnostic status for sanitized inputs: - - struct ScaleResult { - double automaticMultiplier; - float finalMultiplier; - bool usedFallback; - }; - - ScaleResult ComputeRenderTargetScale( - int viewportHeight, - float screenPercentage, - float userMultiplier) noexcept; - - struct ScaledDimensions { - std::int32_t width; - std::int32_t height; - }; - - std::optional ScaleDimensions( - std::int32_t width, - std::int32_t height, - float multiplier) noexcept; - -Have `UTextureRenderTarget2D_PostLoad_hk` use this helper and retain its existing integration calls. Calculate in a sufficiently wide floating-point type, check finiteness and integer bounds before narrowing dimensions, and reject nonpositive source dimensions. Preserve the current truncation toward zero for positive products, with a minimum of one pixel for a valid deliberately undersampled target. A floating-point value outside the `int32_t` range must never be cast and written to game memory. - -On rejected dimensions, leave the target untouched and forward to the original function exactly once. Keep the RGBA16F capture-size behavior. Do not add target-category selection, a graphics-memory budget, or rescaling of already-created targets in this release. Those require different feature work. - -### Expected arithmetic examples - -At 1280x720, 1280x800, 1920x1080, or 2560x720 with 100% screen percentage and user multiplier 1, the final multiplier is 1. At 3440x1440 or 5120x1440 with the same settings, it is approximately 1.333333. At 3840x2160 it is 2. At 1920x2160 it is also 2; width does not decide the vertical-quality baseline. - -At a 2160-pixel viewport height with 50% screen percentage, the automatic baseline is 1. At a 1440-pixel height with 150%, it is 2. A user multiplier of 0.5 applied to the 1440p/100% baseline yields approximately 0.666667, demonstrating that explicit undersampling still works. A combined multiplier above 4 is capped at 4. - -A 1920x1080 source target becomes 2560x1440 for the normal 1440p/100%/1 case and 3840x2160 for the 2160p/100%/1 case. Use tolerances for intermediate floating-point comparisons. Expected integer dimensions should be checked directly. - -### Validation and acceptance for this change - -Add tests with independent expected answers for native-baseline retention, height-driven scaling, screen-percentage interaction, user undersampling, combined clamping, invalid arithmetic inputs, invalid source dimensions, one-pixel minimums, and integer overflow rejection. Include a case that fails under the old guard, such as 2560x720 with automatic scaling. - -In the actual game, compare at least one below-1080p display mode, a 1440p ultrawide mode, and a 2160p mode against the baseline. Inspect menu model previews and captured backgrounds, record old and new target sizes from the log, and watch for unexpected hitching. Verify that changing aspect ratio alone at the same vertical rendering resolution does not alter the quality baseline. - -Acceptance requires correct calculated sizes, original-function forwarding on every path, no unrelated viewport change, and no new visual or loading regression. The release notes should describe the scaling guard correction; they should not claim a universal performance improvement. - -## Change 2: validate raw-input reads before parsing - -### Current behavior and defect - -The relevant path begins around `src/dllmain.cpp:855`. It queries `dwSize`, allocates that many bytes, performs a second read, and then interprets the buffer even if the read fails or is incomplete. - -The Windows API contract is specific: a successful size query with a null data pointer returns zero; a successful data read returns the copied byte count; an error returns `(UINT)-1`. The buffer must have at least 32-bit alignment. Those facts are summarized here so implementation does not depend on external context. [Microsoft GetRawInputData reference](https://learn.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-getrawinputdata). - -### Required behavior - -A failed size query, zero or undersized required buffer, allocation failure, failed data read, copied count outside the allocated capacity, inconsistent packet header, or truncated mouse payload must result in no P3RFix camera update. Return the original `PeekMessageW` result. Observing a bad packet must not consume another Windows message or prevent the game from performing its own handling. - -Keep the queried allocation capacity separate from the size variable passed back to the second API call. Store the read's returned count before comparing it. Do not depend on the evaluation order of an API call and a comparison against a variable that the call can modify. - -Validate that the copied bytes contain a `RAWINPUTHEADER`. Inspect the header through a checked copy or properly aligned storage. Validate its declared packet size against the copied count and capacity. If the type is mouse, require enough bytes for the mouse payload at the Windows-defined union offset before accessing movement. Non-mouse packets need no camera processing. - -For 1.4.0, retain the current movement-selection behavior: only the packets accepted by the existing `usFlags == 0` condition contribute to the custom relative mouse movement. Changing the interpretation of other valid flags belongs to 1.5.0. Retain signed X/Y values, the existing division by 1200, mouse-activity behavior for valid nonzero motion, and exactly-once accumulation when the message is removed. - -Use scoped ownership for the buffer. An allocation failure must not throw through a Windows interception callback. Keep any catch localized to allocation/read preparation; avoid a broad catch that hides unrelated defects. - -### Proposed implementation boundary - -Create `src/input/RawMousePacket.hpp` and `src/input/RawMousePacket.cpp`. Use Windows SDK structures directly; do not create packed replicas of them. Keep the helper independent of SafetyHook, logging, and game-memory layouts. - -Define a reader function type compatible with the actual Windows function so tests can supply deterministic read outcomes: - - using RawInputReader = - UINT (WINAPI*)(HRAWINPUT, UINT, LPVOID, PUINT, UINT); - - struct MouseDelta { - LONG x; - LONG y; - }; - - enum class PacketStatus { - RelativeMouse, - Ignored, - Invalid, - AllocationFailed - }; - - struct PacketResult { - PacketStatus status; - MouseDelta delta; - }; - - using RawInputBuffer = std::unique_ptr; - using RawInputAllocator = RawInputBuffer (*)(std::size_t wordCount) noexcept; - - PacketResult ReadRelativeMousePacket( - HRAWINPUT input, - RawInputReader reader, - RawInputAllocator allocator = nullptr) noexcept; - - void ApplyRelativeMousePacket( - const PacketResult& packet, - float& accumulatedX, - float& accumulatedY, - bool& lastValidInputWasFromMouse) noexcept; - -Production passes `GetRawInputData`; tests pass a controlled implementation of that same contract. An aligned array of 32-bit words is one simple way to own the requested byte capacity. Check the rounding calculation before allocating. Parse only the bytes the API reports as copied. - -The hook remains responsible for applying the returned valid delta to the existing accumulators. Invalid, ignored, and allocation-failure results do not alter movement or ownership state. Keep original-call forwarding at the top of the message hook as it is today. Preserve the original result on every early return. - -Log invalid reads without flooding the log on repeated device events. Allocation/read failures can include an available error code captured at the point of failure, but do not invent an error code for a structurally short packet. Normal ignored keyboard or nonrelative packets are not errors. - -### Validation and acceptance for this change - -Use fixtures that exercise the production reader with a successful two-call mouse read, a size-query error, zero queried size, a queried size smaller than the header, a data-read error, a short data read, an oversized returned count, a header declaring more data than was copied, and a truncated mouse payload. Add valid non-mouse and currently ignored flag cases. - -Use both positive and negative motion, including zero motion. Confirm that a valid delta is accumulated once and that an invalid result leaves existing accumulated X/Y and the active-input flag unchanged. The allocation-failure path should be verified through an appropriately small allocation seam or a controlled failing allocator if practical; do not attempt to exhaust system memory as a test. - -In game, enable the existing mouse fix and check movement, strafing, dialogue, menus, and controller switching against 1.3.0. This release's valid-input camera response should remain equivalent. A changed sensitivity curve or newly introduced smoothing would violate its scope. - -Acceptance requires no parsing outside the copied packet, no leaked storage on early returns, no exception escaping the callback, no duplicated message processing, and preserved valid-input behavior. - -## Change 3: remove the console wait from independent installation - -### Current behavior and ordering - -In the baseline, `Main` calls `EnableConsole` after `RenderTextures` and before `IntroSkip`, `AspectRatioFOV`, `HUDFix`, `Framerate`, `WindowFocus`, and `MouseFix`. - -`EnableConsole` returns immediately when disabled. When enabled, it retries up to 200 pending results with a 100 ms sleep between attempts. That is the existing approximately 20-second pending window, plus lookup work. A missing dependency or other nonpending error already exits the loop early. - -### Proposed order and preserved behavior - -Keep initialization on the existing worker and use this order: - - Logging - Configuration - InitializeUnrealIntegration - Resolution - RenderTextures - IntroSkip - AspectRatioFOV - HUDFix - Framerate - WindowFocus - MouseFix - EnableConsole - -Check that no moved installer relies on a constructed console. The examined source provides no such dependency: these functions scan and install their own hooks. Render-target access is also independent of console-runtime discovery through the focused integration. - -Preserve the console retry count, pending interpretation, result reporting, key reporting, and console construction behavior. Do not add a second worker, call game-object construction from another new thread, or replace the console implementation. Do not move work into `DllMain`; it should remain the small loader entry point that starts the existing worker. - -Add a concise initialization-complete log marker after independent installation and before console initialization. Its wording must distinguish attempting independent installation from every requested feature succeeding. Individual scan errors still need their existing reporting. - -### Validation and acceptance for this change - -Check source dependencies and actual startup logs. With the console disabled, startup behavior should remain equivalent. With it enabled and ready, all independent installation attempts should appear before console success/key reporting. With dependencies unavailable, independent attempts should still precede the console error and the game should retain those successfully installed features. - -For a genuinely delayed pending console, verify the ordering through a controlled startup observation or development-only test harness. A harness may model pending console discovery without touching game memory, but do not retain a special delay toggle in the shipped player configuration merely to test this reorder. Do not build a large startup abstraction just to assert a literal function list. - -Test intro skipping with the console enabled so time-sensitive hooks have a chance to act at boot. Also verify that the working console still opens with its configured game binding and that repeated access works. This change should improve availability of independent fixes during the pending period, not change the console's own functionality. - -## Change 4: consolidate all release licensing into one `LICENSES` file - -### Current package contents and requested outcome - -The baseline release script requires `LICENSE.md`, `THIRD_PARTY_NOTICES.md`, and `licenses/`, then copies all of them into both package staging directories. The two archive formats therefore carry multiple separate licensing documents and a nested directory that becomes extra installation clutter when extracted. - -Replace that distribution payload with exactly one extensionless `LICENSES` document at each package's existing content root. It must contain the complete P3RFix license, dependency license texts, and retained attribution/provenance. Neither ZIP should separately contain `LICENSE.md`, `THIRD_PARTY_NOTICES.md`, or any `licenses/` entry. Keep the archive root, any established game-relative nesting, ASI/INI/loader destinations, and Reloaded-II metadata placement unchanged. The new file occupies the same package level where the existing top-level licensing documents belong; it is not a reason to add another enclosing folder. - -The source checkout retains `LICENSE.md`, `THIRD_PARTY_NOTICES.md`, and the component files under `licenses/`. The requested simplification concerns what players download and extract. Generating the release document from those source inputs makes future maintenance easier and preserves the repository's existing license references. - -### Contents and component coverage - -Start the combined document with a short plain-text introduction explaining that it collects P3RFix's license and the notices for components used or credited by the release. Explain that each component retains its own terms and that loader notices describe the loader bundled with the standalone ZIP. Put the P3RFix license first, followed by the provenance/attribution section and individually labeled component sections. - -The P3RFix section must preserve both copyright lines from `LICENSE.md`, including the original author's credit. The provenance section must retain the component identities, pinned revisions/version information, the distinction between compiled dependencies and the standalone loader, and the existing retained Unreal integration attribution. The loader remains Ultimate ASI Loader v9.7.4 with the existing SHA256 verification and DLL naming; this change does not upgrade or replace it. - -Preserve the entire text of every component file, including copyrights, grants, conditions, disclaimers, embedded subcomponent notices, and exceptions. Do not replace several MIT variants with one shared MIT paragraph, drop the Boost text because the package also includes MIT-licensed software, or omit MinHook's additional Hacker Disassembler Engine notices. Preserve fmt's optional exception as part of its source text even though this plan does not depend on invoking it. - -Retain the existing Dumper-7, UnrealContainers, UTF-N, and Unhandled Exception Tracer provenance as written and correctly attributed. Moving notices does not assign a new license to inherited code or change what the existing provenance document says about the source trees examined when it was prepared. Keep original source attribution comments in place. - -Use this explicit ordered input manifest inside `create_release.ps1`; it defines thirteen source documents, comprising twelve full license texts and one provenance document: - - LICENSE.md - THIRD_PARTY_NOTICES.md - licenses/inipp/LICENSE.txt - licenses/spdlog/LICENSE - licenses/fmt/LICENSE - licenses/SafetyHook/LICENSE - licenses/Zydis/LICENSE - licenses/Zycore/LICENSE - licenses/UnrealContainers/LICENSE - licenses/Ultimate-ASI-Loader/LICENSE - licenses/Ultimate-ASI-Loader/miniz-LICENSE - licenses/Ultimate-ASI-Loader/MinHook-LICENSE.txt - licenses/Ultimate-ASI-Loader/injector-utility-LICENSE.txt - -Give each input a readable component heading and its source-repository path. Those path labels describe where the maintained source text lives; they do not claim that the original individual files are also shipped. Distinguish the P3RFix license section from the provenance section and distinguish the four loader-related inputs from components compiled into the ASI. - -Use the same combined document in both packages. In the introduction and loader sections, state plainly that Reloaded-II does not include the standalone loader binary. Including the same clearly qualified loader notices in both archives preserves existing notice coverage and keeps generation/validation simple; it must not suggest that Reloaded-II loads or requires `dsound.dll` from this package. - -### Readability and text preservation - -Write ordinary UTF-8 text without a byte-order mark at the beginning of the combined file. A byte-order mark is an encoding marker that some source text files contain; stripping that marker from an input prevents invisible markers from appearing between sections. Preserve Unicode author names and meaningful text. Use one consistent newline convention and a final newline so the document opens cleanly in common Windows editors. - -Place clear separators and component titles outside the copied text. License payloads may normalize line endings and remove only an initial encoding marker; do not trim lines, reflow paragraphs, remove trailing spaces as a side effect of broad cleanup, summarize wording, or rewrite license terms. If a source file has no final newline, add a separating newline before the next section so the last sentence and next heading do not run together. - -`THIRD_PARTY_NOTICES.md` currently contains Markdown links to repository-local license paths. Preserve their source/provenance meaning by explaining, immediately before the included notice, that repository-relative paths refer to the source checkout and the full license texts follow in this same document. Update that source document's package-description sentence to say releases carry one combined `LICENSES` file. This avoids telling readers that the old `licenses/` tree exists inside the new ZIP while retaining the repository's useful navigable references. - -Do not depend on release-page links or an internet connection to provide the actual license terms. Links and source revision identifiers supplement the copied texts; a player reading the extracted document offline should have all of the license wording already present. - -### Implementation within the existing release script - -Add a small `Write-CombinedLicenseFile` helper inside `create_release.ps1`. Its inputs are the repository root, the explicit ordered section records, and a destination path. Each section record contains a component title and repository-relative input filename. The function validates every input as a nonempty readable file, constructs the complete text in the manifest order, and writes one output. Do not create a new packaging framework, add a tool dependency, introduce a new build target, or download license texts at release time. - -Validate these inputs before building or creating either final ZIP so a missing document fails the release with a message naming its path. Treat a missing or empty required license/provenance input as an error rather than silently skipping it. Compare the source `licenses/` file inventory with the manifest: if a component notice is added or removed, packaging should report the mismatch and require the manifest to be updated deliberately. This prevents a future checked-in notice from being forgotten merely because the original list was hardcoded. - -Generate the combined file once at `build/package-staging/LICENSES`. Copy it into both `$StandaloneDirectory` and `$ReloadedDirectory` alongside the existing ASI and INI. Replace only the baseline loop's separate `LICENSE.md`/`THIRD_PARTY_NOTICES.md` copy and recursive `licenses/` copy. Keep the loader copy, metadata substitution, compression destinations, release-body generation, existing source-input validation, and staging cleanup behavior. - -Keep the helper's section order deterministic, using the explicit manifest rather than filesystem enumeration order. Do not insert a current timestamp, machine path, username, locale-dependent string, or dynamically fetched revision into the document. Repeated generation from the same source texts must produce the same `LICENSES` bytes. Reproducibility here concerns the document contents; ZIP container timestamps need not become a separate release project. - -Update `README.md` and the existing `THIRD_PARTY_NOTICES.md` introduction to distinguish source license files from the single document shipped in releases. Add a concise 1.4.0 changelog item explaining the archive cleanup. Add release-body wording where it helps describe the actual archive contents; retain the established extraction instructions and game directory. No ASI behavior, player setting, loader configuration, or dependency revision changes as part of this item. - -### Verification of generated contents and both archives - -Verify each generated section against its maintained input after only the declared encoding/newline normalization. Identify sections by their manifest records and unique headings, rather than checking only that the output contains the word `MIT` or that a filename exists. A present but incomplete file fails acceptance. Check that all thirteen source documents appear once in the intended order and that the final output is not empty. - -Add a focused package-content assertion in the same PowerShell script after both archives are created, using .NET's built-in ZIP reader. Open each ZIP, normalize its entry separators for comparison, and require exactly one file with the expected `LICENSES` path at the established package root. Check entry names case-insensitively for a `licenses` directory or descendant and for the separate top-level `LICENSE.md` and `THIRD_PARTY_NOTICES.md` documents; reject any of those legacy licensing outputs. Do not reject unrelated files merely because their names contain the substring `license`. - -Read the actual archived `LICENSES` entry and compare its bytes to the generated reference. Require the two package documents to match each other. In the same archive inspection, verify that the established ASI/INI entries retain their paths, that the standalone loader appears only in the standalone package, and that Reloaded-II retains the expected `ModConfig.json`. Preserve the script's existing build/loader-input checks as well. Close every ZIP stream and entry stream before the script performs staging cleanup or another package run. - -These assertions run as part of the existing packaging invocation, so the current build and tagged-release workflows exercise them without another CI platform or utility. A short diagnostic should identify which archive and which notice/entry failed. A validation failure must stop packaging successfully completing and prevent publishing through the existing workflow; it must not emit a reassuring success message for incomplete archives. - -For fault-path verification during implementation, test the document helper against a disposable source fixture with one required notice missing, one empty, and one unexpected file under the fixture's `licenses/` directory. These should fail with the relevant path and leave no accepted output. Include a source without a final newline and an input with an initial encoding marker to confirm separator/encoding handling. Keep fixtures in temporary build output; do not delete or edit real repository licenses merely to provoke an error. This is a small packaging-boundary check, not another C++ test framework. - -### Installation and upgrade behavior - -Fresh installation should extract one `LICENSES` document instead of the old licensing directory and separate documents. Compare actual ZIP inventories before and after implementation to show that the ASI, active INI, standalone loader, and Reloaded-II metadata retain their established package destinations. Validate extraction through both supported install methods. Licensing consolidation must not introduce an extra folder that changes where the game loads the mod. - -Extraction over an older installation does not remove files the older archive already created. Do not claim that an ordinary ZIP update automatically cleans an existing `licenses/` directory or old top-level notice files. The release note may state that older notice files can remain after an overwrite update and that the new combined file is the release's complete notice document. Do not introduce an installer, automatic deletion routine, or broad cleanup instruction that could affect licensing files belonging to another mod. - -Acceptance requires one complete readable `LICENSES` file in each archive, no separately packaged licensing tree/documents, full preservation of the maintained texts and attribution, matching documents in both formats, repeatable generation, and unchanged binary installation behavior. - -## Plan of Work and Milestones - -### Milestone 1: isolate and correct render-scaling decisions - -Implement `src/render/Scaling.hpp` and `src/render/Scaling.cpp`, wire the existing render-target callback to them, and create arithmetic/dimension tests with independent expected values. At the end of this milestone, an isolated Windows test executable should demonstrate the intended baseline and explicit multiplier behavior. - -Add a focused `fix-behavior-tests` target in the existing `xmake.lua`. This is a necessary test target in the current tooling, not a build-platform change. Compile the new helper into the ASI by listing its source under the existing target; the current `src/*.cpp` pattern does not recursively include subdirectories. - -### Milestone 2: make packet handling fail safely - -Implement the checked reader and wire it into `PeekMessageW_Injected`. Extend `tests/fix_behavior_tests.cpp` with the controlled Windows reader fixtures. Keep tests linked to the actual helper instead of duplicating its validation logic in a test implementation. - -At the end of this milestone, read failures and truncated payloads should be reproducible in tests with no camera update, while valid signed movement produces the expected delta. The existing game memory fixture target remains separate and unchanged in purpose. - -### Milestone 3: move the optional console wait - -Move the existing call in `Main`, add the concise installation marker, and review the resulting dependency order. Verify normal startup in game, then capture console-disabled, console-enabled, and unavailable/deferred-console evidence as described above. - -At the end of this milestone, independently installed features should no longer wait behind console discovery, and the confirmed working console should remain working. - -### Milestone 4: consolidate and verify the licensing payload - -Implement the ordered document generator and archive assertions inside `create_release.ps1`, update the source/release descriptions, and exercise the missing/empty/extra-input and encoding/separator checks with disposable fixtures. Run the packaging script and inspect both actual ZIPs. At the end of this milestone, each archive should contain exactly one complete `LICENSES` document with the same bytes and no legacy licensing directory or separate notice documents. - -Keep the source license texts intact and record the input manifest, document hash, and archive-entry evidence. Confirm the loader remains confined to the standalone package and extraction still places each supported package's actual mod files where its installation instructions expect them. - -### Milestone 5: prepare and validate the release - -Update the version fallback/default to 1.4.0, add a truthful `CHANGELOG.md` entry, and adjust build examples and release text where necessary. Preserve the existing Reloaded-II metadata substitution and pinned loader. - -Run the automated checks and live comparisons, then build both archives with the consolidated licensing checks enabled. Inspect their actual contents and validate installation through each supported method. An archive inspection proves package contents; it does not prove game compatibility. Record both kinds of evidence. - -## Concrete Steps - -Use a Windows x64 development environment with Visual Studio 2022 C++ build tools, Windows SDK, CMake, PowerShell 7.2 or newer, and the project's existing xmake. Run commands from the repository root. Initialize the already-pinned submodules if this checkout has not done so: - - git submodule update --init --recursive - -For debug checks after the new behavior target exists: - - xmake f -y -p windows -a x64 -m debug --fix_version=1.4.0 - xmake build -y unreal-integration-tests - xmake run unreal-integration-tests - xmake build -y fix-behavior-tests - xmake run fix-behavior-tests - python tools/unreal_inventory.py --phase minimal - -Repeat in release mode: - - xmake f -y -p windows -a x64 -m release --fix_version=1.4.0 - xmake build -y unreal-integration-tests - xmake run unreal-integration-tests - xmake build -y fix-behavior-tests - xmake run fix-behavior-tests - python tools/unreal_inventory.py --phase minimal - -Both test executables must return zero and report all checks passing. The existing Unreal test runner prints `[PASS]` or `[FAIL]` per case and ends with a passed/total count. Give the new runner the same straightforward result convention. Record the actual counts after implementation; no count is claimed by this plan. - -Extend the existing build workflow's debug/release validation step to run the new target too. Keep its platform, pinned actions, dependencies, permissions, packaging flow, and release handoff intact. - -Once code and release metadata are ready, build staged archives using the existing script: - - ./create_release.ps1 -Version 1.4.0 - -Expected release outputs are `build/P3RFix_1.4.0.zip`, `build/P3RFix_Reloaded-II.zip`, and `build/release_body.md`. The ASI is produced at `build/windows/x64/release/P3RFix.asi`. The script's staging directories are disposable build output; do not put user settings or validation captures there. - -The packaging invocation must also validate the combined licensing manifest and both ZIP contents before reporting success. For a separate manual inspection, use the existing PowerShell/.NET environment from the repository root: - - Add-Type -AssemblyName System.IO.Compression.FileSystem - foreach ($ArchivePath in @('build/P3RFix_1.4.0.zip', 'build/P3RFix_Reloaded-II.zip')) { - $Archive = [System.IO.Compression.ZipFile]::OpenRead((Resolve-Path $ArchivePath).Path) - try { - Write-Host "Contents of $ArchivePath" - $Archive.Entries | Select-Object FullName, Length - } finally { - $Archive.Dispose() - } - } - -Expect one nonempty `LICENSES` entry at each established package root and no `licenses/`, `LICENSE.md`, or `THIRD_PARTY_NOTICES.md` entry. Inspect the extracted document in a Windows text editor and confirm that the project, compiled-dependency, retained-provenance, and standalone-loader sections are readable. The automatic assertions prove section coverage and matching contents; this manual step proves the layout and text are understandable to a reader. Archive entry listings and checks described here are expected future verification, not already captured evidence. - -Actual publishing is a subsequent release operation after validation. Use the project's existing tag-driven workflow and a new 1.4.0 tag on the exact tested commit. Do not move an existing release tag or imply that writing this plan published a release. - -## Validation and Acceptance - -The maintenance release is acceptable when the three behavior fixes and the fourth licensing/package change are implemented, their scope remains narrow, the existing integration/inventory checks and new focused checks pass, both packages contain one complete `LICENSES` document, and the game shows no regression in the validated console or viewport behavior. - -For presentation, test 16:9 and at least one wide viewport at the same vertical resolution, below-1080p rendering, normal 1440p automatic scaling, and 2160p automatic scaling. Compare old/new menu targets and captures. Include borderless resizing and minimization so the unchanged viewport hook is exercised. - -For input, use mouse fix disabled as a baseline, then enabled with ordinary mouse movement and a controller present. Test device switching and opening/closing menus. Automated invalid-packet tests establish the safety improvement; do not pretend it is necessary or reliable to cause an actual bad Windows packet during play. - -For startup, test intro skipping and console access together. Keep log evidence showing independent installation before the optional console result. In Proton, validate the shipped Windows binary with the existing loader override and console setting. Do not revive the historical 1.2.4 crash as a current issue merely because it appears in an upstream thread. - -Where Steam and Xbox/MS Store executable variants are available, record which variant was actually tested. An untested variant remains untested; passing a Steam run does not imply a WinGDK run. The release can record unavailable coverage honestly, but must not replace it with an invented compatibility claim. - -For each live case, record the mod build/commit, game executable identifier, loader/install method, relevant settings, viewport size, action sequence, result, and a short log or visual comparison. Use a stable saved-game scene where possible so before/after presentation is comparable. - -For packaging, record both ZIP inventories, the consolidated document hash, and the thirteen-input coverage result. Demonstrate that omitted/empty/unlisted notice inputs fail the helper or package checks, that the final two documents match, and that the old licensing payload is absent. Fresh extraction should show the single document; an overwrite upgrade may retain old files from a previous install, as explicitly explained in Change 4. - -## Idempotence and Recovery - -Build and test steps may be repeated in the checkout. The package script recreates its own staging/output files. Preserve useful release artifacts and captured evidence outside that disposable staging directory before rerunning it. - -Before swapping a game installation's ASI or loader for validation, keep recoverable copies of the working baseline files and the player's INI. Restore that exact baseline if a regression appears. Do not overwrite or delete unrelated mods, game configuration, or saves. - -Each primary change should be independently reviewable and revertible. If render scaling fails live comparison, revert that helper/wiring while retaining independently verified packet safety and startup ordering. If a feature requires an unrelated redesign, move it out of this maintenance release rather than expanding its scope silently. - -License generation is repeatable and writes only to disposable package staging/output. If a license input fails validation, fix the manifest or maintained source text and regenerate the packages; do not bypass the check or hand-edit only one ZIP. If consolidation itself needs to be reverted, restore the old script copy operations with the matching documentation and keep the source license files intact. Do not ship a package without either the validated combined document or the previous complete notice payload. - -Do not add generated SDK files or new memory-layout fields for the three runtime fixes or for licensing consolidation. Existing integration tests already cover render writes and console boundaries. If an actual discovered requirement changes that assumption, document the evidence and revise this plan before implementing broader memory access. - -## Artifacts and Notes - -Keep implementation evidence with the release work: the final source diff, debug and release test output, inventory result, startup-order log excerpt, representative render-target sizes, actual-game comparison notes, the combined-license input manifest/hash, and both archive-content inspections. - -Captured automated evidence is under `/tmp/P3RFix/evidence/`, mirrored from `C:\Users\camo\Projects\P3RFix-1.4.0\evidence`. The candidate archives and generated release text are under `/tmp/P3RFix/packages/`. `windows-validation.txt` records both ASI builds and both sets of 69/69 integration and 35/35 behavior results; `hook-harness.txt` records 8/8 development-only observations; `license-checks.txt`, `powershell-lint.txt`, and `packaging.txt` record the packaging checks. `archive-inspection.txt` independently verifies exact inventories, all thirteen payloads, configuration/loader preservation, Reloaded-II metadata, x64 PE identity, and matching documents/binaries. No actual-game evidence exists yet. - -`docs/1.4.0-validation.txt` permanently records the verified candidate and remaining gates. `/tmp/P3RFix/TESTING.txt` provides concrete install/rollback instructions and the rendering, input, startup, viewport, and package cases; `/tmp/P3RFix/RESULTS.txt` records common environment/build identifiers and per-case outcomes. Baseline archives are under `/tmp/P3RFix/baseline/`, and the maintainer can store logs/images under `/tmp/P3RFix/game-results/`. The candidate runtime/packaging source commit is `d133132`; subsequent handoff/evidence commits change documentation only. The supplied Steam `P3R.exe` was read for identification (SHA256 `88244bade1988eafd8ababd66d4c2227091c4c6c253e6455c7097b1b1e5299b8`), never executed. - -The verified standalone inventory is `dsound.dll` (1,198,304 bytes), `LICENSES` (25,315), `P3RFix.asi` (1,203,200), and `P3RFix.ini` (2,876), all at the archive root. Reloaded-II replaces `dsound.dll` with `ModConfig.json` (945 bytes), retaining the other three paths. Both documents have SHA256 `f953af8a4c4e88df3c592a3fbf8dbec9cb37bddba1b17633b9fa38d3e2f1aa54`; the ASI hash is `c64347fb3d0f687a44bda30a5f7e796a538a956cee7c2b51a48d56dd882f91cf`. ZIP hashes are recorded in `archive-inspection.txt` and may change if packaging is repeated; compare actual bytes before handoff. - -The following are expected example results, not captured output: - - [PASS] automatic scale retains native baseline on a wide 720p viewport - [PASS] explicit undersampling applies after the automatic baseline - [PASS] failed raw-input read produces no mouse delta - [PASS] truncated mouse payload is rejected - N/N checks passed - -An expected startup sequence is: - - Independent fix installation attempts completed. - Enable Console: Console object constructed. - Enable Console: Console enabled - access it using the reported key. - -Use the actual console log wording and key from the resulting build. The first marker proves order, while individual installation logs establish which features succeeded. - -An illustrative packaging result is: - - Combined licensing document: all 13 source documents included. - P3RFix_1.4.0.zip: one LICENSES document; no separate licensing directory/documents. - P3RFix_Reloaded-II.zip: one LICENSES document; no separate licensing directory/documents. - Both packaged LICENSES documents match the generated reference. - -This transcript is an expected result, not captured output. Use the implemented script's actual wording and output hashes in the release evidence. - -Draft release-note content should describe the corrected vertical-quality baseline, checked raw-input reads, earlier independent installation, and one combined `LICENSES` document replacing the old licensing folder/separate documents in release ZIPs. Do not include unreleased 1.5.0 controls, claim the old console crash was repaired here, or describe the viewport fix as new in 1.4.0. - -## Interfaces and Dependencies - -Keep using the existing `p3r::unreal::ReadRenderTarget`, `SetRenderTargetSize`, `SetCaptureSize`, and console interface. No generated SDK or additional library is needed. - -The proposed new sources are `src/render/Scaling.hpp`, `src/render/Scaling.cpp`, `src/input/RawMousePacket.hpp`, `src/input/RawMousePacket.cpp`, and `tests/fix_behavior_tests.cpp`. Add them explicitly to the appropriate existing xmake targets. The behavior tests may link the already-used Windows `user32` library for the SDK/API boundary; they should not depend on the game, hook installation, or Zydis. - -Use the current C++ standard setting and MSVC ABI. ABI means the binary rules for calling functions and laying out SDK structures. In particular, keep the correct Windows calling convention for the injected reader and use the Windows SDK's own `RAWINPUTHEADER` and `RAWMOUSE` definitions. - -The helper sources and adjacent fixture headers now exist and use the interfaces described above. Future changes to these boundaries must update the decisions, commands, and interface descriptions together so this plan remains an accurate resume record. - -Licensing consolidation uses only the existing PowerShell 7.2-or-newer release environment and built-in .NET text/ZIP support. The ordered generator and archive-content assertion live in `create_release.ps1`; no new external dependency, compiler, source target, or runtime DLL is required. `LICENSE.md`, `THIRD_PARTY_NOTICES.md`, and all eleven source component-license files remain the maintained inputs. `LICENSES` is generated package content, not another independent source-of-truth file to edit by hand. - -## Outcomes & Retrospective - -All four changes are implemented. Both Windows configurations build the ASI and pass Unreal 69/69, behavior 35/35, and the minimal inventory; the development harness passes 8/8. Both real ZIPs pass licensing and destination checks, independent thirteen-input comparisons, and fresh temporary extraction. PowerShell lint reports zero warning/error diagnostics. README, repository instructions, changelog, and release text describe the new behavior and package contract; the changelog truthfully marks 1.4.0 unreleased. These results establish arithmetic, simulated API failures, synthetic adapter ordering, and archive contents; they do not establish gameplay or loader compatibility. - -The prepared candidate has reached the user-required game-test handoff. Implementation and automated/package verification are finished and committed, but release completion is unproven because actual game/installation results are missing. This is essential external validation reserved for the maintainer, not a failed build or an exhausted implementation path. The smallest input needed is the completed `/tmp/P3RFix/RESULTS.txt` with supporting startup/render logs and representative visual comparisons. - -Resume from the current branch and inspect the worktree, plan, persistent validation record, current package hashes, and any returned user results. Local validation scripts and console output are under `/tmp/P3RFix/`; `remote.py` uploads a PowerShell script to the dedicated VM directory and runs it. Next: reconcile each live acceptance case with the maintainer's evidence, investigate any regression, and obtain coverage for unverified required cases. Update this plan and `docs/1.4.0-validation.txt` with actual findings. After successful user validation/confirmation, remove this plan in the final commit and push branch `1.4.0` to origin. Do not run the game, delete this plan early, push before completion, merge without human approval, or infer a passed case from missing/unavailable evidence. No release tag or publication has been attempted. - -Revision note, 2026-10-01: renamed the original pre-v2 notes for a 1.4.0 release and expanded them into a self-contained maintenance plan at the maintainer's request. The scaling policy, failure handling, startup ordering, implementation boundaries, validation, and release criteria are now explicit; the established console, viewport, packaging, and MSVC constraints are retained. - -Revision note, 2026-10-01: added the maintainer's fourth 1.4.0 change, replacing the release licensing folder and separate license/notice files with one generated `LICENSES` document in both existing package formats. Expanded the scope, decisions, milestones, commands, archive/content validation, upgrade behavior, recovery, and release criteria to cover complete notice preservation. Source licensing files and build tooling remain unchanged; the separate 1.5.0 plan's baseline references now inherit this packaging contract. - -Revision note, 2026-10-01: recorded the recovered implementation state, completed Windows validation, test seams, and dedicated VM/tool paths. Split implementation from outstanding live validation so future continuations cannot mistake compilation or synthetic tests for confirmed gameplay. - -Revision note, 2026-10-01: recorded verified licensing fault paths, the actual package inventories and hashes, zero-diagnostic linting, and production-body development harness results. Live game and installation checks remain explicit release gates; no evidence has been inferred from compilation or archive success. - -Revision note, 2026-10-01: completed the concrete maintainer handoff and permanent candidate validation record. The release remains unreleased and the goal remains active pending actual game/installation results; final plan removal and branch push retain the user's explicit testing/confirmation gate. - -Revision note, 2026-10-01: the final pre-handoff reread reconciled stale baseline wording and progress notes with the implemented state and documented the allocator/delta application interfaces in full. This is documentation alignment only; acceptance criteria and candidate runtime/package bytes are unchanged. diff --git a/docs/1.4.0-validation.txt b/docs/1.4.0-validation.txt deleted file mode 100644 index 1aabef3..0000000 --- a/docs/1.4.0-validation.txt +++ /dev/null @@ -1,103 +0,0 @@ -P3RFix 1.4.0 validation record -Date: 2026-10-01 -Status: candidate awaiting maintainer game and installation validation -Runtime and packaging source commit: d133132 (branch 1.4.0) - -Implemented scope ------------------ -Height-based automatic render-target scaling with native-quality floor and -explicit user multipliers; checked Windows raw-input reads with preserved valid -motion interpretation; independent installation before optional console wait; -one generated complete LICENSES document in both existing package formats. - -Automated verification ----------------------- -Authorized VM: camo@192.168.122.141 -Checkout: C:\Users\camo\Projects\P3RFix-1.4.0\source -MSVC: Visual Studio 2022, 14.44.35207, Windows x64 -xmake: 3.1.1; Python: 3.13.7; portable PowerShell and VS2022 CMake - -In each of debug and release: - P3RFix.asi builds successfully using the repository's MSVC toolchain. - unreal-integration-tests: 69/69 checks passed. - fix-behavior-tests: 35/35 checks passed; compiled with /W4 /WX. - tools/unreal_inventory.py --phase minimal: PASS; zero legacy references. - -Temporary production-body harness: 8/8 checks passed. It extracts verbatim Main, -EnableConsole, PeekMessageW_Injected, and UTextureRenderTarget2D_PostLoad_hk bodies -from dllmain.cpp. Controlled discovery models console-disabled, ready, -unavailable, delayed, and 200-attempt timeout cases with real 100 ms sleeps. -Independent installers precede discovery. Message/render callbacks forward once; -valid signed movement accumulates once, rejection leaves state unchanged, and -real integration writes respect target/capture boundaries. This establishes -synthetic behavior and ordering; installers and console discovery use fixtures. - -Production packaging invocation: ./create_release.ps1 -Version 1.4.0 succeeds. -Licensing fixtures verify repeatable bytes, missing/empty/unlisted-input failure, -BOM removal, Unicode and trailing-space preservation, and no-final-newline -separation. Archive fixtures reject legacy licensing entries, incomplete text, -an additional nested document, a missing INI, and a loader in Reloaded-II. -All streams close before fixture cleanup. PowerShell ScriptAnalyzer reports zero -warning/error diagnostics. New C++ sources pass clang-format verification, and -git diff --check passes. The analyzer and development harness are temporary -validation tools; neither adds a release dependency or player setting. - -Actual package evidence ------------------------ -Standalone ZIP root: - dsound.dll 1,198,304 bytes - LICENSES 25,315 bytes - P3RFix.asi 1,203,200 bytes - P3RFix.ini 2,876 bytes - -Reloaded-II ZIP root: - LICENSES 25,315 bytes - ModConfig.json 945 bytes - P3RFix.asi 1,203,200 bytes - P3RFix.ini 2,876 bytes - -Production ZIP assertions and an independent Python inspection verify exactly -one complete LICENSES, all thirteen maintained inputs once in manifest order, -UTF-8 without BOM, CRLF throughout, final newline, matching documents and ASIs, -Windows x64 DLL identity, and absence of separate legacy notice files/tree. -Fresh extraction into temporary directories preserves each package's mod paths. -Comparison with the shipped 1.3.0 ZIPs verifies the same binary/configuration -destinations and identical standalone loader bytes. INI text is unchanged; the -baseline ZIP uses CRLF (2,954 bytes), while the Linux-sourced candidate uses LF -(2,876). Reloaded-II metadata changes only ModVersion to 1.4.0. - -SHA256: - LICENSES - f953af8a4c4e88df3c592a3fbf8dbec9cb37bddba1b17633b9fa38d3e2f1aa54 - P3RFix.asi - c64347fb3d0f687a44bda30a5f7e796a538a956cee7c2b51a48d56dd882f91cf - P3RFix_1.4.0.zip - 7264a1b88c610f23582123a7a0203bc1f35ed50553869a7a8271c37a71608bca - P3RFix_Reloaded-II.zip - cba7ba6fbb0a43419dc516c3a5065ee5120096991e9863f97853e4ee933ae680 - -Local evidence and handoff --------------------------- -/tmp/P3RFix/evidence/: Windows build/test, harness, licensing, lint, packaging, -and independent archive-inspection transcripts. -/tmp/P3RFix/packages/: both candidate ZIPs and generated release_body.md. -/tmp/P3RFix/TESTING.txt: installation and before/after game checks. -/tmp/P3RFix/RESULTS.txt: maintainer result form. -Temporary scripts remain under /tmp/P3RFix/; VM transcripts are under the -checkout's sibling evidence directory. The repository plan is the resume record. - -Remaining release gates ------------------------ -The agent has not executed the game. Actual rendering at below-1080p, 1440p -ultrawide, and 2160p; equal-height aspect ratios; screen percentage and user -undersampling; resizing/minimization; valid mouse/controller behavior; intro -skip with console; repeated console access; actual startup logs; readable -license presentation; and loading through the supported install methods await -maintainer results. An unavailable executable variant must be reported as such, -and synthetic tests cannot establish its compatibility. - -Retain .agents/PLAN.md until user testing and confirmation validate the release. -Then record live evidence and any coverage limits, make the final plan-removal -commit, and push the completed branch to origin. Human review and approval are -required before merging into main. No tag, release publication, or merge has -been performed as part of this candidate preparation.