自瞄/压枪: 添加旋转偏移扫描器、运行时偏移、Ignore Health 开关、Reset 按钮与数据链路诊断横幅

This commit is contained in:
kedou 2026-08-05 21:54:36 +08:00
parent 25431168f7
commit 71d89a6a06
5 changed files with 232 additions and 10 deletions

View File

@ -1,5 +1,7 @@
#include "Framework.h"
#include <cmath>
#include <cstdio>
#include <cstring>
// ============================================================
// Aimbot (自瞄) + Recoil Control (压枪)
@ -102,6 +104,8 @@ void CFramework::AimAll()
Vector2 bestScreen{};
uintptr_t bestAddr = 0;
m_diagTotal = 0; m_diagHealth = 0; m_diagSkipped = 0;
for (auto& entity : GetEntityList())
{
CPlayer* pEntity = &entity;
@ -109,31 +113,56 @@ void CFramework::AimAll()
if (pEntity->m_address == 0 || pEntity->m_address == local.m_address)
continue;
m_diagTotal++;
if (!pEntity->Update())
{
m_diagSkipped++;
continue;
}
// 距離チェック
float dist = GetDistance(local.m_vecOrigin, pEntity->m_vecOrigin);
if (dist > g.g_AimMaxDist)
{
m_diagSkipped++;
continue;
}
// 種別フィルタ (PMC / Scav / Boss)
if (!IsValidAimTarget(pEntity->m_iSpawnType))
{
m_diagSkipped++;
continue;
}
// 生存チェック (死体はスキップ)
// ※ Mono版で HealthController の offset が違うと全員死亡扱いになる。
// g_AimIgnoreHealth で判定をスキップ可能。
if (!g.g_AimIgnoreHealth)
{
pEntity->UpdateHealth();
if (pEntity->m_fHealth <= 0.f)
{
m_diagHealth++;
continue;
}
}
// 狙うボーン
Vector3 aim = GetAimBone(pEntity);
if (Vec3_Empty(aim))
{
m_diagSkipped++;
continue;
}
Vector2 s{};
if (!WorldToScreen(ViewMatrix, GameSize, aim, s))
{
m_diagSkipped++;
continue;
}
float d = sqrtf((s.x - Center.x) * (s.x - Center.x) + (s.y - Center.y) * (s.y - Center.y));
if (d < bestDist)
@ -145,6 +174,9 @@ void CFramework::AimAll()
}
}
m_diagBestAddr = bestAddr;
m_diagBestDist = bestDist;
if (bestAddr == 0)
return;
@ -264,14 +296,15 @@ void CFramework::AimMouse(const Vector2& target, const Vector2& center)
}
// Memory モード: MovementContext._rotation (Vector2 x=yaw y=pitch) へ書く
// オフセットは g.g_AimMcOffset / g.g_AimRotOffset (运行时可变) を使用
void CFramework::AimMemory(const Vector3& target, const Vector3& localPos)
{
uintptr_t mc = m.Read<uintptr_t>(local.m_address + offset::Player::pMovementContext);
uintptr_t mc = m.Read<uintptr_t>(local.m_address + (uintptr_t)g.g_AimMcOffset);
if (!mc)
return;
// 現在の回転 [rad]
Vector2 rot = m.Read<Vector2>(mc + offset::MovementContext::Rotation);
Vector2 rot = m.Read<Vector2>(mc + (uintptr_t)g.g_AimRotOffset);
// 目標までの方向
Vector3 delta = target - localPos;
@ -298,5 +331,118 @@ void CFramework::AimMemory(const Vector3& target, const Vector3& localPos)
ClampAngle(rot.y + dpitch * t)
};
m.Write<Vector2>(mc + offset::MovementContext::Rotation, newRot);
m.Write<Vector2>(mc + (uintptr_t)g.g_AimRotOffset, newRot);
}
// ============================================================
// 旋转偏移扫描器 (2 段階)
// Step1: Player 構造内のポインタ候補を列挙 → 各ポインタ先で [-π,π] の
// 隣接 float ペア (rotation らしきもの) を記録
// Step2: マウスを回した後に再度読み、yaw 変化が最大のペアを報告
// 見つかったら g.g_AimMcOffset / g.g_AimRotOffset に自動適用
// ============================================================
void CFramework::ScanRotationOffset()
{
struct Cand
{
uintptr_t mcOff; // Player + mcOff -> ポインタ
uintptr_t rotOff; // ポインタ + rotOff -> yaw (float)
float yaw; // Step1 時の yaw 値
};
static std::vector<Cand> s_cands;
if (!tarkov->IsInRaid() || local.m_address == 0)
{
strcpy_s(m_scanResult, "In Raid でのみスキャンできます (メニューを閉じて raid に入る)");
m_scanPhase = 0;
return;
}
if (m_scanPhase == 0)
{
s_cands.clear();
m_scanResult[0] = 0;
// Player 構造 [0x100, 0x700] 内の 8byte アライン済みポインタ
for (uintptr_t mcOff = 0x100; mcOff <= 0x700; mcOff += 0x8)
{
uintptr_t ptr = m.Read<uintptr_t>(local.m_address + mcOff);
if (!ptr)
continue;
// ポインタ先 [0x0, 0x500] で rotation らしきペアを探す
for (uintptr_t rotOff = 0x0; rotOff <= 0x500; rotOff += 0x4)
{
float x = m.Read<float>(ptr + rotOff);
float y = m.Read<float>(ptr + rotOff + 4);
if (fabsf(x) <= kPi && fabsf(y) <= kPi &&
(x != 0.f || y != 0.f))
{
s_cands.push_back({ mcOff, rotOff, x });
if (s_cands.size() >= 20000)
break;
}
}
if (s_cands.size() >= 20000)
break;
}
if (s_cands.empty())
{
strcpy_s(m_scanResult, "候補が見つかりません。範囲を広げて再試行 (0x100-0x700)");
m_scanPhase = 0;
return;
}
m_scanPhase = 1;
snprintf(m_scanResult, sizeof(m_scanResult),
"Step1 OK: %zu candidates. マウスを水平に大きく回してから、もう一度ボタンを押す",
s_cands.size());
}
else
{
// Step2: yaw 変化量が最大の候補を特定
float bestDelta = -1.f;
Cand best{ 0, 0, 0.f };
for (auto& c : s_cands)
{
uintptr_t ptr = m.Read<uintptr_t>(local.m_address + c.mcOff);
if (!ptr)
continue;
float x = m.Read<float>(ptr + c.rotOff);
// 角度の最短差分
float d = fabsf(x - c.yaw);
if (d > kPi)
d = kTwoPi - d;
if (d > bestDelta)
{
bestDelta = d;
best = c;
}
}
m_scanPhase = 0;
if (bestDelta > 0.3f)
{
g.g_AimMcOffset = (int)best.mcOff;
g.g_AimRotOffset = (int)best.rotOff;
snprintf(m_scanResult, sizeof(m_scanResult),
"Found! Player+0x%X -> +0x%X (yaw delta %.2f rad). 適用済み。Offset.h にも書いて固定する事。",
(unsigned)best.mcOff, (unsigned)best.rotOff, bestDelta);
}
else
{
snprintf(m_scanResult, sizeof(m_scanResult),
"変化が小さい (max %.2f rad)。マウスを大きく回してもう一度。",
bestDelta);
}
}
}

View File

@ -80,6 +80,18 @@ private:
Vector3 GetAimBone(CPlayer* pEntity);
void AimMouse(const Vector2& target, const Vector2& center);
void AimMemory(const Vector3& target, const Vector3& localPos);
void ScanRotationOffset(); // 2阶段扫描: Player->MC 与 MC->Rotation 偏移
// 旋转偏移扫描器状态
int m_scanPhase = 0; // 0 = 待机/Step1, 1 = 等待转动鼠标后 Step2
char m_scanResult[192]{ 0 };
// 自瞄诊断计数(菜单 Aim Status 显示)
int m_diagTotal = 0; // 遍历到的实体数
int m_diagHealth = 0; // 被健康检查判死跳过的数
int m_diagSkipped = 0; // 其他原因跳过的数
uintptr_t m_diagBestAddr = 0; // 最近一帧锁定的目标地址
float m_diagBestDist = 0.f;// 最近一帧目标到屏幕中心的距离
void DrawLine(ImVec2 a, ImVec2 b, ImColor color, float width)
{

View File

@ -178,29 +178,47 @@ void CFramework::RenderMenu()
ImGui::Checkbox("PMC", &g.g_AimTargetPMC);
ImGui::Checkbox("Scav", &g.g_AimTargetScav);
ImGui::Checkbox("Boss", &g.g_AimTargetBoss);
ImGui::Checkbox("Ignore Health", &g.g_AimIgnoreHealth);
ImGui::Checkbox("Draw FOV", &g.g_AimDrawFov);
ImGui::Checkbox("Draw Target", &g.g_AimDrawTarget);
ImGui::Spacing();
ImGui::Separator();
ImGui::Spacing();
// 数据链路状态: 哪一环断了看这里
ImGui::Text("Aim Status");
ImGui::Text("InRaid : %s", tarkov->IsInRaid() ? "YES" : "NO");
ImGui::Text("Local : 0x%llX", (unsigned long long)local.m_address);
ImGui::Text("Entity : %zu", GetEntityList().size());
ImGui::Text("Scan : %d ent / %d health / %d skip", m_diagTotal, m_diagHealth, m_diagSkipped);
ImGui::Text("Target : %s (d=%.0fpx)", m_diagBestAddr ? "LOCK" : "NONE", m_diagBestDist);
{
bool k0 = (GetAsyncKeyState(g.g_AimKey0) & 0x8000) != 0;
bool k1 = (GetAsyncKeyState(g.g_AimKey1) & 0x8000) != 0;
ImGui::Text("Key : %s", (k0 || k1) ? "HOLD" : "---");
}
ImGui::EndChild();
ImGui::BeginChild("##C101", ImVec2(ImGui::GetContentRegionAvail()), true);
ImGui::Text("Rot Debug (Memory 検証)");
ImGui::Text("Offset Debug (Memory)");
ImGui::Separator();
ImGui::Spacing();
if (tarkov->IsInRaid() && local.m_address != 0)
{
uintptr_t mc = m.Read<uintptr_t>(local.m_address + offset::Player::pMovementContext);
uintptr_t mc = m.Read<uintptr_t>(local.m_address + (uintptr_t)g.g_AimMcOffset);
if (mc)
{
Vector2 rot = m.Read<Vector2>(mc + offset::MovementContext::Rotation);
Vector2 rot = m.Read<Vector2>(mc + (uintptr_t)g.g_AimRotOffset);
ImGui::Text("yaw : %.3f", rot.x);
ImGui::Text("pitch : %.3f", rot.y);
ImGui::TextDisabled("Move mouse to verify the offset");
}
else
{
ImGui::TextDisabled("MovementContext null");
ImGui::TextDisabled("MC null (bad g_AimMcOffset?)");
}
}
else
@ -208,6 +226,30 @@ void CFramework::RenderMenu()
ImGui::TextDisabled("In Raid のみ表示");
}
ImGui::Spacing();
ImGui::Separator();
ImGui::Spacing();
ImGui::Text("Offset (hex)");
ImGui::InputInt("MC", &g.g_AimMcOffset, 8, 8, ImGuiInputTextFlags_CharsHexadecimal);
ImGui::InputInt("Rot", &g.g_AimRotOffset, 4, 4, ImGuiInputTextFlags_CharsHexadecimal);
// 误改输入框会直接导致 Memory 模式失效, 一键恢复默认
if (ImGui::Button("Reset MC/Rot (0x170/0x2C0)", ImVec2(ImGui::GetContentRegionAvail().x, 0.f)))
{
g.g_AimMcOffset = 0x170;
g.g_AimRotOffset = 0x2C0;
}
if (ImGui::Button("Scan Rotation (2-step)", ImVec2(ImGui::GetContentRegionAvail().x, 0.f)))
ScanRotationOffset();
if (m_scanPhase == 1)
ImGui::TextColored(TEXT_COLOR_ATTENTION.Value, "Now turn the mouse (yaw) then click again");
if (m_scanResult[0])
ImGui::TextWrapped("%s", m_scanResult);
ImGui::EndChild();
break;
case 1: // Visual

View File

@ -1,4 +1,5 @@
#include "Framework.h"
#include <cstdio>
bool InScreen(const Vector2& vIn)
{
@ -14,6 +15,20 @@ void CFramework::RenderInfo()
std::string InfoText{ "Tarkov PvE | " + std::to_string((int)ImGui::GetIO().Framerate) + "FPS" };
StringEx(ImVec2(8.f, 8.f), ImColor(1.f, 1.f, 1.f, 1.f), ImGui::GetFontSize(), InfoText.c_str());
// --- 数据链路诊断: ESP/自瞄不工作时看这一行 ---
// Init = GOM 初始化(GameObjectManager offset 是否有效)
// Raid = 是否在局内(GameWorld 是否找到)
// Local = 本地玩家地址(0 说明 MainPlayer offset 失效)
// Ent = 实体列表数量(0 说明 RegisteredPlayers/PlayerBody offset 失效)
char diag[192]{};
snprintf(diag, sizeof(diag),
"Init:%s Raid:%s Local:0x%llX Ent:%d",
tarkov->InitAddress() ? "OK" : "FAIL",
tarkov->IsInRaid() ? "Y" : "N",
(unsigned long long)local.m_address,
(int)GetEntityList().size());
StringEx(ImVec2(8.f, 24.f), ImColor(1.f, 1.f, 0.f, 1.f), ImGui::GetFontSize(), diag);
// Crosshair
if (g.g_Crosshair && !local.IsAiming())
{

View File

@ -46,6 +46,7 @@ struct Globals
bool g_AimTargetPMC = true;
bool g_AimTargetScav = true;
bool g_AimTargetBoss = true;
bool g_AimIgnoreHealth = true; // Mono版でHealthController offsetが違うと全員死亡扱いになる。true=判定スキップ
bool g_AimDrawFov = true; // 显示 FOV 圈
bool g_AimDrawTarget = true; // 显示目标连线
@ -54,6 +55,12 @@ struct Globals
float g_RecoilStrength = 4.f; // 每次下拉的鼠标单位(mickeys)
int g_RecoilInterval = 15; // 下拉间隔 [ms]
// Aim Offset (运行时可调, 默认 = Offset.h 的值)
// Memory 模式自瞄用: Player + g_AimMcOffset -> + g_AimRotOffset (Vector2 yaw/pitch)
// 内置扫描器 [Scan Rotation] 找到后会直接写到这里, 再复制进 Offset.h 固化
int g_AimMcOffset = 0x170;
int g_AimRotOffset = 0x2C0;
// System
bool g_StreamProof = false;
bool g_Crosshair = true;