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
=============================================================================
=============================================================================