feat: add recoil malfunction and aim fixes

This commit is contained in:
kedou 2026-08-07 23:37:55 +08:00
parent 632dc46116
commit 99d599026d
7 changed files with 331 additions and 14 deletions

View File

@ -43,8 +43,19 @@ namespace
return value;
}
bool IsFiniteRotation(const Vector2& value)
{
return std::isfinite(value.x) && std::isfinite(value.y);
}
bool IsUsableAimAddress(const uintptr_t address)
{
return address > 0x10000ULL && address < 0x0000800000000000ULL;
}
float FrameScale()
{
float deltaTime = ImGui::GetIO().DeltaTime;
if (!(deltaTime > 0.f) || deltaTime > 0.2f)
deltaTime = 1.f / 60.f;
@ -382,14 +393,28 @@ void CFramework::AimMouse(const Vector2& target, const Vector2& center)
// オフセットは 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 + (uintptr_t)g.g_AimMcOffset);
if (!mc)
if (!IsUsableAimAddress(local.m_address) ||
g.g_AimMcOffset <= 0 || g.g_AimMcOffset > 0x1000 ||
g.g_AimRotOffset < 0 || g.g_AimRotOffset > 0x1000)
{
return;
}
// 現在の回転 [rad]
Vector2 rot = m.Read<Vector2>(mc + (uintptr_t)g.g_AimRotOffset);
uintptr_t mc = 0;
if (!m.TryRead(local.m_address + static_cast<uintptr_t>(g.g_AimMcOffset), mc) ||
!IsUsableAimAddress(mc))
{
return;
}
// MovementContext.Rotation_1 is a dump-confirmed Vector2 at +0x27C in this build.
Vector2 rot{};
if (!m.TryRead(mc + static_cast<uintptr_t>(g.g_AimRotOffset), rot) ||
!IsFiniteRotation(rot))
{
return;
}
// 目標までの方向
Vector3 delta = target - localPos;
float len2d = sqrtf(delta.x * delta.x + delta.z * delta.z);
if (len2d < 0.001f)
@ -401,19 +426,27 @@ void CFramework::AimMemory(const Vector3& target, const Vector3& localPos)
yaw = ClampAngle(yaw);
pitch = ClampAngle(pitch);
// Distance-adaptive exponential smoothing. It is never 0 or 1, so the
// slider cannot produce either a one-frame snap or a permanently lagging aim.
float dyaw = ClampAngle(yaw - rot.x);
float dpitch = ClampAngle(pitch - rot.y);
float angleError = sqrtf(dyaw * dyaw + dpitch * dpitch);
if (!(angleError > 0.0001f) || !std::isfinite(angleError))
return;
float t = AimResponse(angleError, g.g_AimSmooth, 0.75f);
// Cap the absolute angular step. AimResponse alone still gives a large
// first-frame move for a far target, which looks like a teleport in memory mode.
const float smooth = Clamp01(g.g_AimSmooth * 0.01f);
const float maxAngleStep = 0.10f + (1.f - smooth) * 0.12f;
if (angleError > maxAngleStep)
t = min(t, maxAngleStep / angleError);
Vector2 newRot{
ClampAngle(rot.x + dyaw * t),
ClampAngle(rot.y + dpitch * t)
};
m.Write<Vector2>(mc + (uintptr_t)g.g_AimRotOffset, newRot);
m.TryWrite<Vector2>(mc + static_cast<uintptr_t>(g.g_AimRotOffset), newRot);
}
// ============================================================

View File

@ -1,7 +1,7 @@
#include "Framework.h"
#include <cmath>
// 只实现 dump 明确的对象字段;后座/故障链路等类布局确认后再接入,当前不写内存。
// 只写入 dump 明确的对象字段;后座与故障也只使用本轮已确认的链路,弹道追踪暂不写。
void CFramework::GetESPInfo(const int& SpawnType, std::string& vOutStr, ImColor& vOutColor)
{
@ -227,10 +227,162 @@ void CFramework::UpdateFeatures()
UpdateSwayOverride();
else
ResetSwayOverride();
if (g.g_NoRecoil)
UpdateNoRecoilOverride();
else
ResetNoRecoilOverride();
if (g.g_NoMalfunction)
UpdateNoMalfunctionOverride();
else
ResetNoMalfunctionOverride();
}
bool CFramework::ResolveCurrentWeapon(uintptr_t& weaponAnimation, uintptr_t& shotEffector,
uintptr_t& weapon, uintptr_t& weaponTemplate)
{
weaponAnimation = 0;
shotEffector = 0;
weapon = 0;
weaponTemplate = 0;
if (!IsUsableAddress(local.m_address) ||
!m.TryRead(local.m_address + offset::Player::pWeaponAnimation, weaponAnimation) ||
!IsUsableAddress(weaponAnimation) ||
!m.TryRead(weaponAnimation + offset::WeaponAnimations::ShotEffector, shotEffector) ||
!IsUsableAddress(shotEffector) ||
!m.TryRead(shotEffector + offset::ShotEffector::MainWeaponInHands, weapon) ||
!IsUsableAddress(weapon) ||
!m.TryRead(weapon + offset::Weapon::Template, weaponTemplate) ||
!IsUsableAddress(weaponTemplate))
{
return false;
}
return true;
}
void CFramework::UpdateNoRecoilOverride()
{
uintptr_t weaponAnimation = 0;
uintptr_t shotEffector = 0;
uintptr_t weapon = 0;
uintptr_t weaponTemplate = 0;
uintptr_t recoilSettings = 0;
if (!ResolveCurrentWeapon(weaponAnimation, shotEffector, weapon, weaponTemplate) ||
!m.TryRead(weaponTemplate + offset::WeaponTemplate::WeaponRecoilSettings, recoilSettings) ||
!IsUsableAddress(recoilSettings))
{
ResetNoRecoilOverride();
return;
}
if (m_noRecoilOverride.captured &&
(m_noRecoilOverride.player != local.m_address ||
m_noRecoilOverride.weaponAnimation != weaponAnimation ||
m_noRecoilOverride.shotEffector != shotEffector ||
m_noRecoilOverride.weapon != weapon ||
m_noRecoilOverride.weaponTemplate != weaponTemplate ||
m_noRecoilOverride.recoilSettings != recoilSettings))
{
ResetNoRecoilOverride();
}
if (!m_noRecoilOverride.captured)
{
bool enable = false;
if (!m.TryRead(recoilSettings + offset::WeaponRecoil::Enable, enable))
{
ResetNoRecoilOverride();
return;
}
m_noRecoilOverride.player = local.m_address;
m_noRecoilOverride.weaponAnimation = weaponAnimation;
m_noRecoilOverride.shotEffector = shotEffector;
m_noRecoilOverride.weapon = weapon;
m_noRecoilOverride.weaponTemplate = weaponTemplate;
m_noRecoilOverride.recoilSettings = recoilSettings;
m_noRecoilOverride.enable = enable;
m_noRecoilOverride.captured = true;
}
// WeaponRecoil.Enable is a dump-confirmed instance field. Do not touch its Values array yet.
m.TryWrite<bool>(recoilSettings + offset::WeaponRecoil::Enable, false);
}
void CFramework::UpdateNoMalfunctionOverride()
{
uintptr_t weaponAnimation = 0;
uintptr_t shotEffector = 0;
uintptr_t weapon = 0;
uintptr_t weaponTemplate = 0;
uintptr_t malfunctionState = 0;
if (!ResolveCurrentWeapon(weaponAnimation, shotEffector, weapon, weaponTemplate) ||
!m.TryRead(weapon + offset::Weapon::MalfState, malfunctionState) ||
!IsUsableAddress(malfunctionState))
{
ResetNoMalfunctionOverride();
return;
}
if (m_noMalfunctionOverride.captured &&
(m_noMalfunctionOverride.player != local.m_address ||
m_noMalfunctionOverride.weapon != weapon ||
m_noMalfunctionOverride.weaponTemplate != weaponTemplate ||
m_noMalfunctionOverride.malfunctionState != malfunctionState))
{
ResetNoMalfunctionOverride();
}
if (!m_noMalfunctionOverride.captured)
{
bool allowJam = false;
bool allowFeed = false;
bool allowMisfire = false;
bool allowSlide = false;
float baseChance = 0.f;
int state = 0;
if (!m.TryRead(weaponTemplate + offset::WeaponTemplate::AllowJam, allowJam) ||
!m.TryRead(weaponTemplate + offset::WeaponTemplate::AllowFeed, allowFeed) ||
!m.TryRead(weaponTemplate + offset::WeaponTemplate::AllowMisfire, allowMisfire) ||
!m.TryRead(weaponTemplate + offset::WeaponTemplate::AllowSlide, allowSlide) ||
!m.TryRead(weaponTemplate + offset::WeaponTemplate::BaseMalfunctionChance, baseChance) ||
!m.TryRead(malfunctionState + offset::WeaponMalfunctionState::State, state) ||
!std::isfinite(baseChance))
{
ResetNoMalfunctionOverride();
return;
}
m_noMalfunctionOverride.player = local.m_address;
m_noMalfunctionOverride.weapon = weapon;
m_noMalfunctionOverride.weaponTemplate = weaponTemplate;
m_noMalfunctionOverride.malfunctionState = malfunctionState;
m_noMalfunctionOverride.allowJam = allowJam;
m_noMalfunctionOverride.allowFeed = allowFeed;
m_noMalfunctionOverride.allowMisfire = allowMisfire;
m_noMalfunctionOverride.allowSlide = allowSlide;
m_noMalfunctionOverride.baseChance = baseChance;
m_noMalfunctionOverride.state = state;
m_noMalfunctionOverride.captured = true;
}
m.TryWrite<bool>(weaponTemplate + offset::WeaponTemplate::AllowJam, false);
m.TryWrite<bool>(weaponTemplate + offset::WeaponTemplate::AllowFeed, false);
m.TryWrite<bool>(weaponTemplate + offset::WeaponTemplate::AllowMisfire, false);
m.TryWrite<bool>(weaponTemplate + offset::WeaponTemplate::AllowSlide, false);
m.TryWrite<float>(weaponTemplate + offset::WeaponTemplate::BaseMalfunctionChance, 0.f);
m.TryWrite<int>(malfunctionState + offset::WeaponMalfunctionState::State, 0);
}
void CFramework::UpdateStaminaOverride()
{
uintptr_t physical = 0;
uintptr_t stamina = 0;
uintptr_t handsStamina = 0;
@ -370,8 +522,48 @@ void CFramework::UpdateSwayOverride()
m.TryWrite<Vector3>(motionEffector + offset::MotionEffector::SwayFactors, Vector3{});
}
void CFramework::ResetNoRecoilOverride()
{
if (m_noRecoilOverride.captured && IsUsableAddress(m_noRecoilOverride.recoilSettings))
{
m.TryWrite<bool>(m_noRecoilOverride.recoilSettings + offset::WeaponRecoil::Enable,
m_noRecoilOverride.enable);
}
m_noRecoilOverride = {};
}
void CFramework::ResetNoMalfunctionOverride()
{
if (m_noMalfunctionOverride.captured)
{
if (IsUsableAddress(m_noMalfunctionOverride.weaponTemplate))
{
m.TryWrite<bool>(m_noMalfunctionOverride.weaponTemplate + offset::WeaponTemplate::AllowJam,
m_noMalfunctionOverride.allowJam);
m.TryWrite<bool>(m_noMalfunctionOverride.weaponTemplate + offset::WeaponTemplate::AllowFeed,
m_noMalfunctionOverride.allowFeed);
m.TryWrite<bool>(m_noMalfunctionOverride.weaponTemplate + offset::WeaponTemplate::AllowMisfire,
m_noMalfunctionOverride.allowMisfire);
m.TryWrite<bool>(m_noMalfunctionOverride.weaponTemplate + offset::WeaponTemplate::AllowSlide,
m_noMalfunctionOverride.allowSlide);
m.TryWrite<float>(m_noMalfunctionOverride.weaponTemplate + offset::WeaponTemplate::BaseMalfunctionChance,
m_noMalfunctionOverride.baseChance);
}
if (IsUsableAddress(m_noMalfunctionOverride.malfunctionState))
{
m.TryWrite<int>(m_noMalfunctionOverride.malfunctionState + offset::WeaponMalfunctionState::State,
m_noMalfunctionOverride.state);
}
}
m_noMalfunctionOverride = {};
}
void CFramework::ResetStaminaOverride()
{
if (m_staminaOverride.captured)
{
if (IsUsableAddress(m_staminaOverride.stamina))
@ -404,4 +596,6 @@ void CFramework::ResetFeatureOverrides()
ResetStaminaOverride();
ResetFallDamageOverride();
ResetSwayOverride();
ResetNoRecoilOverride();
ResetNoMalfunctionOverride();
}

View File

@ -53,6 +53,40 @@ private:
bool captured{ false };
};
struct NoRecoilOverrideState
{
uintptr_t player{ 0 };
uintptr_t weaponAnimation{ 0 };
uintptr_t shotEffector{ 0 };
uintptr_t weapon{ 0 };
uintptr_t weaponTemplate{ 0 };
uintptr_t recoilSettings{ 0 };
bool enable{ false };
bool captured{ false };
};
struct NoMalfunctionOverrideState
{
uintptr_t player{ 0 };
uintptr_t weapon{ 0 };
uintptr_t weaponTemplate{ 0 };
uintptr_t malfunctionState{ 0 };
bool allowJam{ false };
bool allowFeed{ false };
bool allowMisfire{ false };
bool allowSlide{ false };
float baseChance{ 0.f };
int state{ 0 };
bool captured{ false };
};
bool ResolveCurrentWeapon(uintptr_t& weaponAnimation, uintptr_t& shotEffector,
uintptr_t& weapon, uintptr_t& weaponTemplate);
void UpdateNoRecoilOverride();
void UpdateNoMalfunctionOverride();
void ResetNoRecoilOverride();
void ResetNoMalfunctionOverride();
void UpdateStaminaOverride();
void UpdateFallDamageOverride();
void UpdateSwayOverride();
@ -66,6 +100,8 @@ private:
StaminaOverrideState m_staminaOverride{};
FallDamageOverrideState m_fallDamageOverride{};
SwayOverrideState m_swayOverride{};
NoRecoilOverrideState m_noRecoilOverride{};
NoMalfunctionOverrideState m_noMalfunctionOverride{};
std::mutex m_mtxEntityList;

View File

@ -123,11 +123,13 @@ void CFramework::RenderMenu()
ImGui::Checkbox("无限耐力", &g.g_InfStamina);
ImGui::Checkbox("无跌落伤害", &g.g_NoFallDamage);
ImGui::Checkbox("无摇摆", &g.g_NoSway);
ImGui::Checkbox("无后座(内存)", &g.g_NoRecoil);
ImGui::Checkbox("无故障/卡弹", &g.g_NoMalfunction);
ImGui::Spacing();
ImGui::Separator();
ImGui::TextDisabled("仅写入 dump 已确认的对象字段");
ImGui::TextDisabled("后座 / 故障:等待下一轮类布局确认");
ImGui::TextDisabled("只写入 dump 已确认的对象字段");
ImGui::TextDisabled("弹道追踪:暂不写入,等待 runtime 验证");
ImGui::EndChild();
ImGui::BeginChild("##C021", ImVec2(ImGui::GetContentRegionAvail()), true);

View File

@ -131,6 +131,20 @@ void CPlayer::UpdateHealth()
void CPlayer::UpdateBone()
{
// Refresh the direct PlayerBody -> PlayerBones link before reading the selected aim bone.
// The list thread is intentionally slow, while animation/weapon objects can be replaced sooner.
if (m_address)
{
uintptr_t playerBody = 0;
uintptr_t playerBones = 0;
if (m.TryRead(m_address + offset::Player::pPlayerBody, playerBody) && playerBody &&
m.TryRead(playerBody + offset::PlayerBody::PlayerBones, playerBones) && playerBones)
{
m_pPlayerBody = playerBody;
m_pPlayerBones = playerBones;
}
}
m_vecHeadOrigin = GetBonePosition(BONE_HEAD);
m_vecNeckOrigin = GetBonePosition(BONE_NECK);
m_vecBoneList.clear();

View File

@ -103,6 +103,7 @@ namespace offset
{
constexpr auto Breath = 0x30;
constexpr auto MotionReact = 0x40; // MotionEffector
constexpr auto ShotEffector = 0x50; // ShotEffector / Shootingg
constexpr auto isAiming = 0x1C5;
}
@ -112,6 +113,41 @@ namespace offset
constexpr auto SwayFactors = 0x94; // UnityEngine.Vector3
}
// -.ShotEffector
namespace ShotEffector
{
constexpr auto MainWeaponInHands = 0x38; // EFT.InventoryLogic.Weapon
}
// EFT.InventoryLogic.Weapon
namespace Weapon
{
constexpr auto Template = 0x40; // CompoundItem.Template
constexpr auto MalfState = 0xF0; // WeaponMalfunctionStateClass
}
// EFT.InventoryLogic.WeaponTemplate
namespace WeaponTemplate
{
constexpr auto WeaponRecoilSettings = 0x180; // WeaponRecoil
constexpr auto AllowJam = 0x2F0;
constexpr auto AllowFeed = 0x2F1;
constexpr auto AllowMisfire = 0x2F2;
constexpr auto AllowSlide = 0x2F3;
constexpr auto BaseMalfunctionChance = 0x2F4;
}
// -.WeaponRecoil / -.Weapon.WeaponMalfunctionStateClass
namespace WeaponRecoil
{
constexpr auto Enable = 0x18;
}
namespace WeaponMalfunctionState
{
constexpr auto State = 0x6C;
}
// -.GClass774 (stamina/oxygen state)
namespace PhysicalState
{

View File

@ -33,6 +33,8 @@ struct Globals
bool g_InfStamina = false;
bool g_NoFallDamage = false;
bool g_NoSway = false;
bool g_NoRecoil = false;
bool g_NoMalfunction = false;
// Aim (自瞄)