From b30e9f8edb49fbdec9d8f62ae9b674911b9c8dac Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 2 Sep 2026 02:00:14 +0000 Subject: [PATCH] Harden Papyrus marshaling boundary and add per-hit diagnostics Hit-zone classification is unchanged (verified bit-for-bit against the previous math over a scenario sweep and 2000 fuzzed inputs). Changes: - GetHitZone validates array lengths before indexing. CommonLibSSE turns a None or short Papyrus array into an empty/short std::vector, and the old d[5]/p[4]/s[5] reads were undefined behaviour in that case. Now logs an error and returns 0. - Compute all pitch thresholds and lateral ranges up front, then emit one trace line per hit with every input and threshold so real in-game values can be captured when debugging misclassified hits. - Warn when dist is not positive or when asin() produces NaN (dispWidth/dx outside [-1,1]); NaN ranges silently fall through to chest/back. - Use std::fabs instead of unqualified abs on floats. MSVC's ucrt resolves abs(float) to the float overload, but a conforming libstdc++ picks abs(int) and truncates. - ArmHitCalcSKSE takes float armRange (matches Papyrus Float and the original registration), add , include SKSE/SKSE.h for logging. Co-Authored-By: Claude Fable 5.1 Claude-Session: https://claude.ai/code/session_01VxJjofpgPX8JaNDtgDcZ28 --- src/LocationalDamage.cpp | 148 ++++++++++++++++++++++++--------------- src/LocationalDamage.h | 4 +- 2 files changed, 96 insertions(+), 56 deletions(-) diff --git a/src/LocationalDamage.cpp b/src/LocationalDamage.cpp index fecf89a..d6d3b54 100644 --- a/src/LocationalDamage.cpp +++ b/src/LocationalDamage.cpp @@ -1,10 +1,25 @@ #include "LocationalDamage.h" +#include + namespace LocDamageNamespace { constexpr std::string_view PapyrusClass = "LDHitZones"; + namespace { + // Papyrus arrays arrive through CommonLibSSE as std::vector copies. A None array, or an + // array the script built with too few elements, becomes an empty/short vector here, and + // indexing past its end is undefined behaviour (silent garbage or a crash). Validate first. + bool checkArray(std::size_t actual, std::size_t expected, const char* name) { + if (actual < expected) { + SKSE::log::error("GetHitZone: array '{}' has {} element(s), expected at least {}", name, actual, expected); + return false; + } + return true; + } + } + int armHitCalc(float facing, float headingAngle, double armRange) { - if (abs(facing) < 45) { + if (std::fabs(facing) < 45) { if (headingAngle > armRange) { return rArm; } @@ -21,7 +36,7 @@ namespace LocDamageNamespace { else if (facing > 45 && facing < 135) { return rArm; } - else if (abs(facing) >= 135) { + else if (std::fabs(facing) >= 135) { if (headingAngle > armRange) { return lArm; } @@ -37,7 +52,7 @@ namespace LocDamageNamespace { } int shoulderhitCalc(bool powerAttack, float facing, float headingAngle, float shoulderRange, int weaponType) { - if (abs(facing) < 45) { + if (std::fabs(facing) < 45) { // In front of the target if (headingAngle > shoulderRange) { return rShoulder; @@ -64,7 +79,7 @@ namespace LocDamageNamespace { else if (facing > 45 && facing < 135) { return rShoulder; } - else if (abs(facing) >= 135) { + else if (std::fabs(facing) >= 135) { // Behind the target if (headingAngle > shoulderRange) { return lShoulder; @@ -95,7 +110,6 @@ namespace LocDamageNamespace { double legZone; double zHeight; - double zHeightMe; if (s.isSneaking) { net_height = net_height * .75; @@ -109,79 +123,103 @@ namespace LocDamageNamespace { } zHeight = p.zPos + net_height; - zHeightMe = p.zPosMe + net_height_me; - // Y-AXIS ZONES + // Y-AXIS ZONES (pitch thresholds, degrees, positive = aggressor looking down). + // None of these depend on one another, so they are all computed up front; this lets the + // trace line below report the full picture for every hit without changing any result. dispHeight = zHeight - (p.zPosMe + net_height_me * 0.900); headZone = atan(dispHeight / p.dist) * 180 / PI; - // Head strike - if (a.ax < headZone) { - return head; - } - dispHeight = zHeight - (p.zPosMe + net_height_me * 0.850); neckZone = atan(dispHeight / p.dist) * 180 / PI; - // Neck strike - if (a.ax < neckZone) { - if (a.facing <= -135 || a.facing >= 135) { - return nape; - } - else { - return neck; - } - } - - // X-AXIS ZONES - dispWidth = d.width - d.widthMe * .900; - shoulderRange = abs(asin(dispWidth / p.dx) * 180 / PI); - dispWidth = d.width - d.widthMe * .800; - armRange = abs(asin(dispWidth / p.dx) * 180 / PI); - dispHeight = zHeight - (p.zPosMe + net_height_me * 0.750); shoulderZone = atan(dispHeight / p.dist) * 180 / PI; - // Shoulder strike - if (a.ax < shoulderZone) { - return shoulderhitCalc(s.isPowerAttack, a.facing, a.headingAngle, shoulderRange, s.weaponType); - } - dispHeight = zHeight - (p.zPosMe + net_height_me * 0.500); chestZone = atan(dispHeight / p.dist) * 180 / PI; - // Chest strike - if (a.ax < chestZone) { - return armHitCalc(a.facing, a.headingAngle, armRange); - } - dispHeight = zHeight - (p.zPosMe + net_height_me * 0.400); groinZone = atan(dispHeight / p.dist) * 180 / PI; - // Groin strike - if (a.ax < groinZone) { - if (abs(a.facing) < 45) { - return groin; + dispHeight = zHeight - (p.zPosMe + net_height_me * 0.200); + legZone = atan(dispHeight / p.dist) * 180 / PI; + + // X-AXIS ZONES (lateral aim tolerance, degrees) + dispWidth = d.width - d.widthMe * .900; + shoulderRange = std::fabs(asin(dispWidth / p.dx) * 180 / PI); + dispWidth = d.width - d.widthMe * .800; + armRange = std::fabs(asin(dispWidth / p.dx) * 180 / PI); + + if (!(p.dist > 0)) { + SKSE::log::warn("hitDetection: dist={} is not positive; pitch thresholds are degenerate", p.dist); + } + if (std::isnan(armRange) || std::isnan(shoulderRange)) { + // asin() of |x| > 1 (or dx == 0). NaN compares false against everything, so every + // lateral test fails and the hit falls through to chest/back. + SKSE::log::warn("hitDetection: lateral range is NaN (dispWidth/dx out of [-1,1]); width={} widthMe={} dx={}", + d.width, d.widthMe, p.dx); + } + + uint32_t zone; + + if (a.ax < headZone) { + // Head strike + zone = head; + } + else if (a.ax < neckZone) { + // Neck strike + if (a.facing <= -135 || a.facing >= 135) { + zone = nape; } else { - return legs; + zone = neck; } } - - dispHeight = zHeight - (p.zPosMe + net_height_me * 0.200); - legZone = atan(dispHeight / p.dist) * 180 / PI; - - // Leg strike - if (a.ax < legZone) { - return legs; + else if (a.ax < shoulderZone) { + // Shoulder strike + zone = shoulderhitCalc(s.isPowerAttack, a.facing, a.headingAngle, shoulderRange, s.weaponType); + } + else if (a.ax < chestZone) { + // Chest strike + zone = armHitCalc(a.facing, a.headingAngle, armRange); + } + else if (a.ax < groinZone) { + // Groin strike + if (std::fabs(a.facing) < 45) { + zone = groin; + } + else { + zone = legs; + } + } + else if (a.ax < legZone) { + // Leg strike + zone = legs; + } + else { + // Feet strike + zone = feet; } - // Feet strike - return feet; + SKSE::log::trace( + "hit zone={} | in: facing={} heading={} ax={} dist={} dx={} dy={} z={} zMe={} w={} wMe={} h={} hMe={} scale={} scaleMe={} " + "sneak={} sneakMe={} sprintMe={} recoilMe={} power={} weapon={} | " + "thresholds: head<{:.2f} neck<{:.2f} shoulder<{:.2f} chest<{:.2f} groin<{:.2f} legs<{:.2f} shoulderRange={:.2f} armRange={:.2f}", + zone, a.facing, a.headingAngle, a.ax, p.dist, p.dx, p.dy, p.zPos, p.zPosMe, d.width, d.widthMe, d.height, d.heightMe, + d.scale, d.scaleMe, s.isSneaking, s.isSneakingMe, s.isSprintingMe, s.isRecoilingMe, s.isPowerAttack, s.weaponType, + headZone, neckZone, shoulderZone, chestZone, groinZone, legZone, shoulderRange, armRange); + + return zone; } uint32_t GetHitZone(RE::StaticFunctionTag*, std::vector d, std::vector a, std::vector p, std::vector s) { - //gLog.SetLogLevel(IDebugLog::kLevel_Message); + if (!checkArray(d.size(), 6, "d (dimensions)") || + !checkArray(a.size(), 3, "a (angles)") || + !checkArray(p.size(), 5, "p (positions)") || + !checkArray(s.size(), 6, "s (states)")) { + return 0; + } // Dimensions const auto scale = d[0]; @@ -219,7 +257,7 @@ namespace LocDamageNamespace { return hitDetection(dimensions, angles, pos, actions); } - uint32_t ArmHitCalcSKSE(RE::StaticFunctionTag*, float facing, float headingAngle, double armRange) { + uint32_t ArmHitCalcSKSE(RE::StaticFunctionTag*, float facing, float headingAngle, float armRange) { return (uint32_t)armHitCalc(facing, headingAngle, armRange); } @@ -229,4 +267,4 @@ namespace LocDamageNamespace { return true; } -} \ No newline at end of file +} diff --git a/src/LocationalDamage.h b/src/LocationalDamage.h index 317b8e4..43d9fa3 100644 --- a/src/LocationalDamage.h +++ b/src/LocationalDamage.h @@ -5,9 +5,11 @@ #ifndef LOCATIONALDAMAGE_H #define LOCATIONALDAMAGE_H +#include #include #include +#include #include #define PI 3.14159265 @@ -101,7 +103,7 @@ namespace LocDamageNamespace uint32_t GetHitZone(RE::StaticFunctionTag*, std::vector d, std::vector a, std::vector p, std::vector s); - uint32_t ArmHitCalcSKSE(RE::StaticFunctionTag*, float facing, float headingAngle, double armRange); + uint32_t ArmHitCalcSKSE(RE::StaticFunctionTag*, float facing, float headingAngle, float armRange); bool RegisterFuncs(RE::BSScript::IVirtualMachine* vm);