Reversal, Structs and Offsets

05/27/2026 15:55 teslatx#1
Code:
// =============================================================================
// SCUM Auto-Voltage Solver — Forum Guide
// =============================================================================
//
// What this does
// --------------
// SCUM's "Voltage Matching" minigame asks you to flip a set of switches so
// that an input number, run through two parallel chains of math operations,
// produces two target output numbers (A and B) at the same time.
//
// Each switch maps to ONE pair of operations: { Left op, Right op }.
//   - "Left op"  contributes to Output A
//   - "Right op" contributes to Output B
//
// Flip switch ON  -> its pair is applied to both chains, in order
// Flip switch OFF -> its pair is skipped
//
// With at most 8 switches and 4 operation types (+, -, *, /), the entire
// search space is 2^8 = 256 combinations — trivial to brute-force. The hard
// part isn't the math; it's getting the data out of the UE4 object cleanly.
//
// This post covers:
//   1. The class layout we care about
//   2. How to safely read each property (FindOffset + fallback pattern)
//   3. The solver itself
//   4. An overlay that tells the player exactly which switches to flip
=============================================================================

#pragma once
#include <cmath>
#include <vector>
#include <map>
#include <string>
#include <imgui.h>

// -----------------------------------------------------------------------------
// 1) Class layout (from Dumper-7 SDK)
// -----------------------------------------------------------------------------
//
// // Class SCUM.VoltageMatchingMinigame
// // 0x01A8 (0x0568 - 0x03C0)
// class AVoltageMatchingMinigame final : public AMinigame
// {
//     uint8                            Pad_3C0[0x20];                          // 0x03C0
//     USwitchboardComponent*           _switchboardComponent;                  // 0x03E0
//     ...
//     TArray<FVoltageMatchingSwitchElementsPairing> _switchPairings;           // 0x0480
//     TArray<UStaticMeshComponent*>    _electricalElementsMeshComponents;      // 0x0490
//     TArray<FVoltageMatchingElementRow> _elementRows;                         // 0x04A0
//     float                            _timeLimit;                             // 0x04B0
//     float                            _waitingForPlayerTimeLimit;             // 0x04B4
//     ...
//     TArray<uint32>                   _additionValues;                        // 0x04D0
//     TArray<uint32>                   _subtractionValues;                     // 0x04E0
//     TArray<uint32>                   _multiplicationValues;                  // 0x04F0
//     TArray<uint32>                   _divisionValues;                        // 0x0500
//     TArray<uint32>                   _inputValues;                           // 0x0510
//     int32                            _maximumNumberOfElementsPerType;        // 0x0520
//     int32                            _minActiveSwitches;                     // 0x0524
//     int32                            _maxActiveSwitches;                     // 0x0528
//     uint8                            Pad_52C[0x3C];                          // 0x052C  <-- hidden state lives here
// };
//
// Three things live inside that Pad_52C[0x3C] block that aren't named in the
// dump but matter for solving. We found them by walking memory:
//
//   0x0530  TArray<FOperationDefinition>   // (Data ptr at +0x530, Count at +0x538)
//   0x0548  float CurrentInputValue        // the actual input shown on screen
//   0x054C  float ElapsedSolveTime
//   0x0550  float ElapsedWaitingTime
//
// FOperationDefinition entries are 0x10 bytes wide:
//
//   +0x00  uint64  Identifier        // matches IDs stored in switch pairings
//   +0x08  uint8   OperationType     // 0=NOP, 1=+, 2=-, 3=*, 4=/
//   +0x0C  float   Value             // operand for the operation
//
// FVoltageMatchingSwitchElementsPairing entries are 0x18 bytes wide:
//
//   +0x00  ???                       // owner / state byte we don't need
//   +0x08  uint64*  OperationIds.Data
//   +0x10  int32    OperationIds.Count   // 2 (Left at [0], Right at [1])
//
// FVoltageMatchingElementRow entries are 0x18 bytes wide (we treat them as
// two rows of 0x18 = one struct holding both outputs):
//
//   row[0] (output A): +0x10 = current, +0x14 = target
//   row[1] (output B): +0x28 = current, +0x2C = target   (i.e. row[1] +0x10/+0x14)
//
// -----------------------------------------------------------------------------

namespace VoltageSolver
{
    // -------------------------------------------------------------------------
    // 2) Safe property reading — FindOffset + fallback
    // -------------------------------------------------------------------------
    //
    // We never trust a hardcoded offset blindly. The primary source is the
    // engine's property table: given the class name and property name, the
    // engine walks the UClass and returns the field offset. If that fails
    // (engine reflection stripped, class renamed, etc.) we fall back to the
    // hardcoded value from the dump.
    //
    // This pattern is what keeps the solver alive across small game updates —
    // properties keep their names even when struct sizes shift.
    //
    // The functions below are skeletons. Replace `UnrealEngine::FindOffset`
    // with whatever your project uses to walk UProperty linked lists.
    // -------------------------------------------------------------------------

    template <typename T>
    static T ReadProperty(void* self, const char* propName, uint32_t fallback)
    {
        static uint32_t cached = 0;
        if (!cached) {
            cached = UnrealEngine::FindOffset(L"VoltageMatchingMinigame", propName);
            if (!cached) cached = fallback;
        }
        if (!self) return {};
        __try { return *reinterpret_cast<T*>((uintptr_t)self + cached); }
        __except (1) { return {}; }
    }

    // For TArrays we just expose Data/Count/Max — the actual element layout is
    // walked by hand in the solver because UE4 array templates don't survive
    // generic reinterpret_cast cleanly across module boundaries.
    struct RawArray { uintptr_t Data; int32_t Count; int32_t Max; };

    static RawArray ReadArray(void* self, const char* propName, uint32_t fallback)
    {
        static uint32_t cached = 0;
        if (!cached) {
            cached = UnrealEngine::FindOffset(L"VoltageMatchingMinigame", propName);
            if (!cached) cached = fallback;
        }
        if (!self) return {};
        __try {
            struct R { void* d; int32_t c; int32_t m; };
            auto* a = reinterpret_cast<R*>((uintptr_t)self + cached);
            return { (uintptr_t)a->d, a->c, a->m };
        }
        __except (1) { return {}; }
    }

    // -------------------------------------------------------------------------
    // 3) Snapshot — everything we need to solve one frame of the puzzle
    // -------------------------------------------------------------------------

    struct Operation { uint64_t Id; uint8_t Type; float Value; bool Valid; };
    struct SwitchRule {
        int       Index;          // 1..8 (display number)
        uint64_t  LeftId;
        uint64_t  RightId;
        Operation Left;
        Operation Right;
        bool      IsActive;       // currently flipped ON in-game
        bool      ShouldBeActive; // solver's recommendation
    };

    struct Snapshot {
        float InputValue;
        float OutputACurrent, OutputATarget;
        float OutputBCurrent, OutputBTarget;
        float ElapsedSolveTime, ElapsedWaitingTime;
        std::vector<uint16_t> ActiveSwitchIds; // from USwitchboardComponent
    };

    // -------------------------------------------------------------------------
    // 4) Reading the puzzle data
    // -------------------------------------------------------------------------
    //
    // ElementRows is the trickiest piece. It's a TArray of structs, but the
    // first ~0x10 bytes of each row are meta we don't care about. The two
    // floats we want (current, target) live at +0x10 and +0x14 of each row.
    // Row 0 = Output A, Row 1 = Output B.
    //
    // Rather than write a row struct, we just deref the TArray.Data pointer
    // once and add the magic offsets. The "magic" pair (0x10/0x14 and
    // 0x28/0x2C) is just row[0]+0x10/0x14 and row[1]+0x10/0x14 where each
    // row is 0x18 bytes — i.e. base + (rowIndex * 0x18) + 0x10.
    // -------------------------------------------------------------------------

    static float ReadElementRowFloat(void* mg, uint32_t fieldOffset)
    {
        if (!mg) return 0.0f;
        __try {
            // _elementRows.Data lives at offset 0x4A0 of AVoltageMatchingMinigame
            auto* dataPtr = (uintptr_t*)((uintptr_t)mg + 0x4A0);
            uintptr_t base = *dataPtr;
            if (!base) return 0.0f;
            return *(float*)(base + fieldOffset);
        }
        __except (1) { return 0.0f; }
    }

    static Operation ReadOperation(void* mg, int index)
    {
        if (!mg || index < 0) return {};
        __try {
            auto* defsData  = (uintptr_t*)((uintptr_t)mg + 0x530); // TArray.Data
            auto* defsCount = (int32_t*)  ((uintptr_t)mg + 0x538); // TArray.Count
            if (!*defsData || index >= *defsCount) return {};

            uintptr_t entry = *defsData + (uintptr_t)index * 0x10;
            uint64_t id  = *(uint64_t*)(entry + 0x0);
            uint8_t  op  = *(uint8_t*) (entry + 0x8);
            float    val = *(float*)   (entry + 0xC);
            if (op > 4) return {}; // unknown op, ignore
            return { id, op, val, true };
        }
        __except (1) { return {}; }
    }

    static bool ReadSwitchPair(void* mg, int switchIndex, uint64_t& outLeft, uint64_t& outRight)
    {
        if (!mg || switchIndex < 0) return false;
        __try {
            auto arr = ReadArray(mg, "_switchPairings", 0x0480);
            if (!arr.Data || switchIndex >= arr.Count) return false;

            uintptr_t entry = arr.Data + (uintptr_t)switchIndex * 0x18;
            auto* opIdsData  = *(uint64_t**)(entry + 0x8);
            auto  opIdsCount = *(int32_t*) (entry + 0x10);
            if (!opIdsData || opIdsCount <= 0) return false;

            outLeft  = opIdsData[0];
            outRight = opIdsCount > 1 ? opIdsData[1] : 0;
            return true;
        }
        __except (1) { return false; }
    }

    // -------------------------------------------------------------------------
    // 5) Building the snapshot
    // -------------------------------------------------------------------------

    static constexpr int kMaxSwitches  = 8;
    static constexpr int kMaxOpDefs    = 32;

    static bool BuildSnapshot(void* mg, Snapshot& snap, std::vector<SwitchRule>& rules)
    {
        if (!mg) return false;

        // Hidden floats inside Pad_52C
        snap.InputValue         = *(float*)((uintptr_t)mg + 0x548);
        snap.ElapsedSolveTime   = *(float*)((uintptr_t)mg + 0x54C);
        snap.ElapsedWaitingTime = *(float*)((uintptr_t)mg + 0x550);

        // ElementRows[0] = output A, [1] = output B (each row 0x18 wide)
        snap.OutputACurrent = ReadElementRowFloat(mg, 0x10);
        snap.OutputATarget  = ReadElementRowFloat(mg, 0x14);
        snap.OutputBCurrent = ReadElementRowFloat(mg, 0x28);
        snap.OutputBTarget  = ReadElementRowFloat(mg, 0x2C);

        // Build a lookup of operation ID -> definition so each switch can
        // resolve its Left/Right operations in O(1).
        std::map<uint64_t, Operation> ops;
        for (int i = 0; i < kMaxOpDefs; ++i) {
            auto op = ReadOperation(mg, i);
            if (op.Valid) ops[op.Id] = op;
        }

        // Each switch references two op IDs
        rules.clear();
        rules.reserve(kMaxSwitches);
        for (int s = 0; s < kMaxSwitches; ++s) {
            uint64_t leftId = 0, rightId = 0;
            if (!ReadSwitchPair(mg, s, leftId, rightId)) continue;

            SwitchRule r{};
            r.Index   = s + 1;
            r.LeftId  = leftId;
            r.RightId = rightId;
            auto itL = ops.find(leftId);  if (itL != ops.end()) r.Left  = itL->second;
            auto itR = ops.find(rightId); if (itR != ops.end()) r.Right = itR->second;
            rules.push_back(r);
        }

        // (Optional) read live switch states from USwitchboardComponent here
        // so the solver knows which switches are currently flipped ON.

        return !rules.empty();
    }

    // -------------------------------------------------------------------------
    // 6) The solver — brute force over 2^N switch masks
    // -------------------------------------------------------------------------
    //
    // For each candidate mask (bit i = switch i is ON):
    //   - run the input through Left ops to compute Output A
    //   - run the input through Right ops to compute Output B
    //   - if both match the targets, it's a valid solution
    //
    // Among all valid solutions, prefer the one closest (Hamming distance)
    // to the player's current switch configuration. This minimizes the
    // number of switches the player has to physically flip, which matters
    // because each flip has audio + animation latency in-game.
    // -------------------------------------------------------------------------

    static float ApplyOp(float v, const Operation& op)
    {
        switch (op.Type) {
            case 1: return v + op.Value;
            case 2: return v - op.Value;
            case 3: return v * op.Value;
            case 4: return op.Value != 0.0f ? v / op.Value : v;
            default: return v; // NOP / unknown
        }
    }

    static int PopCount(uint32_t v) { int c = 0; while (v) { c += v & 1; v >>= 1; } return c; }
    static bool Approx(float a, float b) { return fabsf(a - b) <= 0.5f; }

    static bool Solve(const Snapshot& snap, const std::vector<SwitchRule>& rules,
                      int minOn, int maxOn, uint32_t preferredMask, uint32_t& outMask)
    {
        const size_t N = rules.size();
        if (!N) return false;

        // Sanity: every switch must have both ops resolved
        for (auto& r : rules)
            if (!r.Left.Valid || !r.Right.Valid) return false;

        bool found = false;
        int  bestDist = INT_MAX;

        for (uint32_t mask = 0; mask < (1u << N); ++mask) {
            int bits = PopCount(mask);
            if (bits < minOn || bits > maxOn) continue;

            float a = snap.InputValue;
            float b = snap.InputValue;
            for (size_t i = 0; i < N; ++i) {
                if (!(mask & (1u << i))) continue;
                a = ApplyOp(a, rules[i].Left);
                b = ApplyOp(b, rules[i].Right);
            }

            if (!Approx(a, snap.OutputATarget) || !Approx(b, snap.OutputBTarget))
                continue;

            // Prefer solutions that require the fewest flips from current state
            int dist = PopCount(mask ^ preferredMask);
            if (!found || dist < bestDist) {
                found = true;
                bestDist = dist;
                outMask = mask;
            }
        }
        return found;
    }

    // -------------------------------------------------------------------------
    // 7) ESP overlay — markers on switches + step list
    // -------------------------------------------------------------------------
    //
    // The 8 left-column switches sit at fixed normalized screen positions on
    // the voltage panel mesh. We hardcoded these from observation; if the
    // panel ever changes, walk the USwitchboardComponent's mesh transforms
    // and project them through the player camera instead.
    // -------------------------------------------------------------------------

    struct GuidePoint { float X, Y; };
    static constexpr GuidePoint kSwitchGuidePoints[kMaxSwitches] = {
        { 0.3735f, 0.2204f }, { 0.3714f, 0.2935f },
        { 0.3710f, 0.3543f }, { 0.3732f, 0.4309f },
        { 0.3734f, 0.5002f }, { 0.3738f, 0.5730f },
        { 0.3701f, 0.6459f }, { 0.3725f, 0.7169f },
    };

    static void DrawGuide(const std::vector<SwitchRule>& rules, uint32_t solvedMask, bool hasSolution)
    {
        auto& io = ImGui::GetIO();
        const float sw = io.DisplaySize.x, sh = io.DisplaySize.y;
        auto* dl = ImGui::GetForegroundDrawList();

        // Step list (top-left of screen)
        ImVec2 cursor(20.0f, 20.0f);
        dl->AddText(cursor, IM_COL32(120, 195, 255, 255), "[ AUTO VOLTAGE ]");
        cursor.y += 22.0f;

        if (!hasSolution) {
            dl->AddText(cursor, IM_COL32(255, 110, 110, 255), "Waiting for solver data...");
            return;
        }

        // Highlight each switch that needs to change
        for (size_t i = 0; i < rules.size(); ++i) {
            bool shouldBe = (solvedMask & (1u << i)) != 0;
            if (shouldBe == rules[i].IsActive) continue;

            ImU32 color = shouldBe ? IM_COL32(255, 220,  90, 255)   // turn ON
                                   : IM_COL32(255, 110, 110, 255); // turn OFF

            // Marker on the physical switch
            ImVec2 pos(kSwitchGuidePoints[i].X * sw, kSwitchGuidePoints[i].Y * sh);
            dl->AddCircle(pos, 16.0f, color, 32, 2.5f);
            dl->AddCircleFilled(pos, 4.5f, color);

            // Step text in the side panel
            char buf[32];
            snprintf(buf, sizeof(buf), "S%zu TURN %s", i + 1, shouldBe ? "ON" : "OFF");
            dl->AddText(cursor, color, buf);
            cursor.y += 18.0f;
        }
    }

    // -------------------------------------------------------------------------
    // 8) Tick — call this from your main loop, with the minigame pointer
    // -------------------------------------------------------------------------

    static void Tick(void* minigame, int minOn = 2, int maxOn = 7)
    {
        if (!minigame) return;

        Snapshot snap{};
        std::vector<SwitchRule> rules;
        if (!BuildSnapshot(minigame, snap, rules)) return;

        // Build "currently active" mask from the live switch states you read
        // out of USwitchboardComponent (omitted here for brevity).
        uint32_t currentMask = 0;
        for (size_t i = 0; i < rules.size(); ++i)
            if (rules[i].IsActive) currentMask |= (1u << i);

        uint32_t solvedMask = 0;
        bool ok = Solve(snap, rules, minOn, maxOn, currentMask, solvedMask);

        DrawGuide(rules, solvedMask, ok);
    }

} // namespace VoltageSolver

=============================================================================
=============================================================================
05/30/2026 22:47 RazexSkillz#2
Nice! Do you by chance have something for lockpick damage? I tried some things out but wasn't able to get it to work.

Tried so far:
- NOP patch on the durability function, server-side bypasses it on listen server
- Zeroing ALockpickingMinigame breaking multipliers (0x460/0x464), no Easy or tension tool multiplier field exists
- Freezing AItem._repPackedState (0x720), crashed, server drives it from an internal non-UPROPERTY variable anyway
- AItem._damageResponseFactor (0x574) to 0.0f, works maybe for the lockpick via _itemInHands, but going through LeftHandAttachmentSocket for the screwdriver crashes because the lock object sits in the same slot

Thinking vtable hook on AItem::TakeDamage or hooking OnLockpickDestroyed/OnTensionToolDestroyed on ALockpickingMinigame might be the way. Any ideas?
06/01/2026 14:29 teslatx#3
Quote:
Originally Posted by RazexSkillz View Post
Nice! Do you by chance have something for lockpick damage? I tried some things out but wasn't able to get it to work.

Tried so far:
- NOP patch on the durability function, server-side bypasses it on listen server
- Zeroing ALockpickingMinigame breaking multipliers (0x460/0x464), no Easy or tension tool multiplier field exists
- Freezing AItem._repPackedState (0x720), crashed, server drives it from an internal non-UPROPERTY variable anyway
- AItem._damageResponseFactor (0x574) to 0.0f, works maybe for the lockpick via _itemInHands, but going through LeftHandAttachmentSocket for the screwdriver crashes because the lock object sits in the same slot

Thinking vtable hook on AItem::TakeDamage or hooking OnLockpickDestroyed/OnTensionToolDestroyed on ALockpickingMinigame might be the way. Any ideas?
Din't checked it to be honest, but dm me on discord we can talk more about this