From fddbac7d9d06cb16778bd4e3a01fcd668d01973c Mon Sep 17 00:00:00 2001 From: BenCat07 Date: Wed, 15 Apr 2020 16:15:26 +0200 Subject: [PATCH] Rewrite crithack --- data/menu/nullifiedcat/crits.xml | 12 +- include/core/netvars.hpp | 1 + include/core/offsets.hpp | 4 + include/crits.hpp | 36 +- include/globals.h | 2 + include/helpers.hpp | 3 + include/hooks/HookTools.hpp | 2 + include/hooks/HookedMethods.hpp | 1 + include/reclasses/C_TFWeaponBase.hpp | 2 +- include/sdk/usercmd.hpp | 157 +++++- src/core/netvars.cpp | 1 + src/crits.cpp | 802 +++++++++++++++++---------- src/globals.cpp | 1 + src/hack.cpp | 1 + src/helpers.cpp | 9 + src/hooks/CreateMove.cpp | 40 ++ src/hooks/GetUserCmd.cpp | 28 +- 17 files changed, 739 insertions(+), 363 deletions(-) diff --git a/data/menu/nullifiedcat/crits.xml b/data/menu/nullifiedcat/crits.xml index c7f0084e..902ce510 100755 --- a/data/menu/nullifiedcat/crits.xml +++ b/data/menu/nullifiedcat/crits.xml @@ -1,13 +1,11 @@ - - - - - - - + + + + + diff --git a/include/core/netvars.hpp b/include/core/netvars.hpp index 9b7cd3fd..ca26c35a 100644 --- a/include/core/netvars.hpp +++ b/include/core/netvars.hpp @@ -121,6 +121,7 @@ public: offset_t flChargeBeginTime; offset_t flLastFireTime; + offset_t flObservedCritChance; offset_t hThrower; offset_t hMyWeapons; diff --git a/include/core/offsets.hpp b/include/core/offsets.hpp index f30b016d..4b2d0e70 100644 --- a/include/core/offsets.hpp +++ b/include/core/offsets.hpp @@ -80,6 +80,10 @@ struct offsets { return PlatformOffset(7, undefined, undefined); } + static constexpr uint32_t CreateMoveLate() + { + return PlatformOffset(21, undefined, undefined); + } static constexpr uint32_t CreateMove() { return PlatformOffset(22, undefined, undefined); diff --git a/include/crits.hpp b/include/crits.hpp index a12fd7ae..465906b9 100644 --- a/include/crits.hpp +++ b/include/crits.hpp @@ -1,37 +1,7 @@ -/* - * crits.h - * - * Created on: Feb 25, 2017 - * Author: nullifiedcat - */ - #pragma once - -class CUserCmd; -class IClientEntity; - -// BUGBUG TODO this struct is outdated -struct crithack_saved_state -{ - float unknown2868; - float unknown2864; - int unknown2620; - float unknown2880; - char unknown2839; - float bucket2616; - int seed2876; - - void Save(IClientEntity *entity); - void Load(IClientEntity *entity); -}; - +#include "settings/Bool.hpp" namespace criticals { - -bool random_crits_enabled(); +extern settings::Boolean enabled; +extern settings::Boolean melee; } // namespace criticals - -#include - -extern int *g_PredictionRandomSeed; -extern std::unordered_map command_number_mod; diff --git a/include/globals.h b/include/globals.h index 81c59a96..f6f80fca 100755 --- a/include/globals.h +++ b/include/globals.h @@ -40,5 +40,7 @@ public: bool isHackActive(); extern CUserCmd *current_user_cmd; +// Seperate user cmd used in the late createmove +extern CUserCmd *current_late_user_cmd; extern GlobalSettings g_Settings; diff --git a/include/helpers.hpp b/include/helpers.hpp index 254cc43d..c7c827ff 100644 --- a/include/helpers.hpp +++ b/include/helpers.hpp @@ -119,6 +119,9 @@ std::unique_ptr strfmt(const char *fmt, ...); bool HasWeapon(CachedEntity *ent, int wantedId); bool IsAmbassador(CachedEntity *ent); bool AmbassadorCanHeadshot(); +// Validate a UserCmd +#define VERIFIED_CMD_SIZE 90 +void ValidateUserCmd(CUserCmd *cmd, int sequence_nr); // Convert a TF2 handle into an IDX -> ENTITY(IDX) int HandleToIDX(int handle); diff --git a/include/hooks/HookTools.hpp b/include/hooks/HookTools.hpp index a897bfc6..c0c1ccb4 100644 --- a/include/hooks/HookTools.hpp +++ b/include/hooks/HookTools.hpp @@ -11,6 +11,8 @@ enum ec_types { /* Note: engine prediction is run on this kind of CreateMove */ CreateMove = 0, + /* Note: this is the CreatMove one layer higher, and should only be used for things that mess with command number*/ + CreateMoveLate, /* This kind of CreateMove will run earlier than all CreateMove events * and guranteed to run before EnginePrediction */ diff --git a/include/hooks/HookedMethods.hpp b/include/hooks/HookedMethods.hpp index ddf1ea02..985334ea 100644 --- a/include/hooks/HookedMethods.hpp +++ b/include/hooks/HookedMethods.hpp @@ -41,6 +41,7 @@ DECLARE_HOOKED_METHOD(LevelShutdown, void, void *); // ClientMode + 4 DECLARE_HOOKED_METHOD(FireGameEvent, void, void *, IGameEvent *); // IBaseClient +DECLARE_HOOKED_METHOD(CreateMoveLate, void, void *this_, int sequence_nr, float input_sample_time, bool arg3) DECLARE_HOOKED_METHOD(DispatchUserMessage, bool, void *, int, bf_read &); DECLARE_HOOKED_METHOD(IN_KeyEvent, int, void *, int, ButtonCode_t, const char *); DECLARE_HOOKED_METHOD(FrameStageNotify, void, void *, ClientFrameStage_t); diff --git a/include/reclasses/C_TFWeaponBase.hpp b/include/reclasses/C_TFWeaponBase.hpp index c81f41f7..55dc7d3b 100644 --- a/include/reclasses/C_TFWeaponBase.hpp +++ b/include/reclasses/C_TFWeaponBase.hpp @@ -129,7 +129,7 @@ public: float mult2 = *(float *) (unk3); float multiplier = 0.5f; - int seed = C_BaseEntity::m_nPredictionRandomSeed() ^ (owner->entindex() | self->entindex()); + int seed = C_BaseEntity::m_nPredictionRandomSeed() ^ (owner->entindex() | (self->entindex() << 8)); RandomSeed(seed); bool result = true; diff --git a/include/sdk/usercmd.hpp b/include/sdk/usercmd.hpp index 6608cabf..a8f693a0 100755 --- a/include/sdk/usercmd.hpp +++ b/include/sdk/usercmd.hpp @@ -10,23 +10,168 @@ #include #include +#include "checksum_crc.h" class CUserCmd { public: - virtual ~CUserCmd(){}; + CUserCmd() + { + Reset(); + } + + void Reset() + { + command_number = 0; + tick_count = 0; + viewangles.Init(); + forwardmove = 0.0f; + sidemove = 0.0f; + upmove = 0.0f; + buttons = 0; + impulse = 0; + weaponselect = 0; + weaponsubtype = 0; + random_seed = 0; + mousedx = 0; + mousedy = 0; + + hasbeenpredicted = false; + } + + CUserCmd &operator=(const CUserCmd &src) + { + if (this == &src) + return *this; + + command_number = src.command_number; + tick_count = src.tick_count; + viewangles = src.viewangles; + forwardmove = src.forwardmove; + sidemove = src.sidemove; + upmove = src.upmove; + buttons = src.buttons; + impulse = src.impulse; + weaponselect = src.weaponselect; + weaponsubtype = src.weaponsubtype; + random_seed = src.random_seed; + mousedx = src.mousedx; + mousedy = src.mousedy; + + hasbeenpredicted = src.hasbeenpredicted; + + return *this; + } + + CUserCmd(const CUserCmd &src) + { + *this = src; + } + + CRC32_t GetChecksum(void) const + { + CRC32_t crc; + + CRC32_Init(&crc); + CRC32_ProcessBuffer(&crc, &command_number, sizeof(command_number)); + CRC32_ProcessBuffer(&crc, &tick_count, sizeof(tick_count)); + CRC32_ProcessBuffer(&crc, &viewangles, sizeof(viewangles)); + CRC32_ProcessBuffer(&crc, &forwardmove, sizeof(forwardmove)); + CRC32_ProcessBuffer(&crc, &sidemove, sizeof(sidemove)); + CRC32_ProcessBuffer(&crc, &upmove, sizeof(upmove)); + CRC32_ProcessBuffer(&crc, &buttons, sizeof(buttons)); + CRC32_ProcessBuffer(&crc, &impulse, sizeof(impulse)); + CRC32_ProcessBuffer(&crc, &weaponselect, sizeof(weaponselect)); + CRC32_ProcessBuffer(&crc, &weaponsubtype, sizeof(weaponsubtype)); + CRC32_ProcessBuffer(&crc, &random_seed, sizeof(random_seed)); + CRC32_ProcessBuffer(&crc, &mousedx, sizeof(mousedx)); + CRC32_ProcessBuffer(&crc, &mousedy, sizeof(mousedy)); + CRC32_Final(&crc); + + return crc; + } + + // Allow command, but negate gameplay-affecting values + void MakeInert(void) + { + viewangles.Init(); + forwardmove = 0.f; + sidemove = 0.f; + upmove = 0.f; + buttons = 0; + impulse = 0; + } + + char pad0[4]; + // For matching server and client commands for debugging int command_number; + + // the tick the client created this command int tick_count; + + // Player instantaneous view angles. Vector viewangles; + // Intended velocities + // forward velocity. float forwardmove; + // sideways velocity. float sidemove; + // upward velocity. float upmove; + // Attack button states int buttons; - uint8_t impulse; + // Impulse command issued. + byte impulse; + char pad1[3]; + // Current weapon id int weaponselect; int weaponsubtype; - int random_seed; - short mousedx; - short mousedy; + + int random_seed; // For shared random functions + short mousedx; // mouse accum in x from create move + short mousedy; // mouse accum in y from create move + + // Client only, tracks whether we've predicted this command at least once bool hasbeenpredicted; -}; + char pad2[3]; +} __attribute__((packed)); + +class CVerifiedUserCmd +{ +public: + CUserCmd m_cmd; + CRC32_t m_crc; +} __attribute__((packed)); + +inline CRC32_t GetChecksum(CUserCmd *cmd) +{ + CRC32_t crc; + + CRC32_Init(&crc); + CRC32_ProcessBuffer(&crc, &cmd->command_number, sizeof(cmd->command_number)); + CRC32_ProcessBuffer(&crc, &cmd->tick_count, sizeof(cmd->tick_count)); + CRC32_ProcessBuffer(&crc, &cmd->viewangles, sizeof(cmd->viewangles)); + CRC32_ProcessBuffer(&crc, &cmd->forwardmove, sizeof(cmd->forwardmove)); + CRC32_ProcessBuffer(&crc, &cmd->sidemove, sizeof(cmd->sidemove)); + CRC32_ProcessBuffer(&crc, &cmd->upmove, sizeof(cmd->upmove)); + CRC32_ProcessBuffer(&crc, &cmd->buttons, sizeof(cmd->buttons)); + CRC32_ProcessBuffer(&crc, &cmd->impulse, sizeof(cmd->impulse)); + CRC32_ProcessBuffer(&crc, &cmd->weaponselect, sizeof(cmd->weaponselect)); + CRC32_ProcessBuffer(&crc, &cmd->weaponsubtype, sizeof(cmd->weaponsubtype)); + CRC32_ProcessBuffer(&crc, &cmd->random_seed, sizeof(cmd->random_seed)); + CRC32_ProcessBuffer(&crc, &cmd->mousedx, sizeof(cmd->mousedx)); + CRC32_ProcessBuffer(&crc, &cmd->mousedy, sizeof(cmd->mousedy)); + CRC32_Final(&crc); + + return crc; +} + +inline CUserCmd *GetCmds(void *iinput) +{ + return *(CUserCmd **) ((uintptr_t) iinput + 0xfc); +} + +inline CVerifiedUserCmd *GetVerifiedCmds(void *iinput) +{ + return *(CVerifiedUserCmd **) ((uintptr_t) iinput + 0x100); +} diff --git a/src/core/netvars.cpp b/src/core/netvars.cpp index 6fcbf230..fb10aaac 100644 --- a/src/core/netvars.cpp +++ b/src/core/netvars.cpp @@ -50,6 +50,7 @@ void NetVars::Init() "m_AttributeList"); // hmmm this->flChargeBeginTime = gNetvars.get_offset("DT_WeaponPipebombLauncher", "PipebombLauncherLocalData", "m_flChargeBeginTime"); this->flLastFireTime = gNetvars.get_offset("DT_TFWeaponBase", "LocalActiveTFWeaponData", "m_flLastFireTime"); + this->flObservedCritChance = gNetvars.get_offset("DT_TFWeaponBase", "LocalActiveTFWeaponData", "m_flObservedCritChance"); this->bDistributed = gNetvars.get_offset("DT_CurrencyPack", "m_bDistributed"); this->_condition_bits = gNetvars.get_offset("DT_TFPlayer", "m_Shared", "m_ConditionList", "_condition_bits"); this->m_flStealthNoAttackExpire = gNetvars.get_offset("DT_TFPlayer", "m_Shared", "tfsharedlocaldata", "m_flStealthNoAttackExpire"); diff --git a/src/crits.cpp b/src/crits.cpp index aefd0ffc..74b74d9e 100644 --- a/src/crits.cpp +++ b/src/crits.cpp @@ -1,357 +1,571 @@ -/* - * crits.cpp - * - * Created on: Feb 25, 2017 - * Author: nullifiedcat - */ - -#include #include "common.hpp" -#include "Backtrack.hpp" -#include "PlayerTools.hpp" +#include "crits.hpp" +#include "netadr.h" std::unordered_map command_number_mod{}; namespace criticals { - -static settings::Boolean crit_info{ "crit.info", "false" }; +settings::Boolean enabled{ "crit.enabled", "false" }; +static settings::Boolean draw{ "crit.draw-info", "false" }; +settings::Boolean melee{ "crit.melee", "false" }; static settings::Button crit_key{ "crit.key", "" }; -static settings::Boolean auto_crit{ "crit.auto", "false" }; -static settings::Boolean crit_melee{ "crit.melee", "false" }; -static settings::Boolean crit_legiter{ "crit.force-gameplay", "false" }; -static settings::Boolean crit_experimental{ "crit.experimental", "false" }; +static settings::Boolean old_mode{ "crit.old-mode", "false" }; -int find_next_random_crit_for_weapon(IClientEntity *weapon) +// How much is added to bucket per shot? +float added_per_shot = 0.0f; +class weapon_info { - int tries = 0, number = current_user_cmd->command_number, found = 0, seed_backup; - - crithack_saved_state state{}; - state.Save(weapon); - - seed_backup = *g_PredictionRandomSeed; - while (!found && tries < 4096) +public: + float crit_bucket{}; + unsigned int weapon_seed{}; + unsigned unknown1{}; + unsigned unknown2{}; + bool unknown3{}; + float unknown4{}; + int crit_attempts{}; + int crit_count{}; + float observed_crit_chance{}; + bool unknown7{}; + weapon_info() { - *g_PredictionRandomSeed = MD5_PseudoRandom(number) & 0x7FFFFFFF; - found = re::C_TFWeaponBase::CalcIsAttackCritical(weapon); - if (found) - break; - ++tries; - ++number; + } + void Load(IClientEntity *weapon) + { + crit_bucket = *(float *) ((uintptr_t) weapon + 0xa38); + weapon_seed = *(unsigned int *) ((uintptr_t) weapon + 0xb3c); + unknown1 = *(unsigned int *) ((uintptr_t) weapon + 0xb30); + unknown2 = *(unsigned int *) ((uintptr_t) weapon + 0xb34); + unknown3 = *(bool *) ((uintptr_t) weapon + 0xb17); + unknown4 = *(float *) ((uintptr_t) weapon + 0xb40); + crit_attempts = *(int *) ((uintptr_t) weapon + 0xa3c); + crit_count = *(int *) ((uintptr_t) weapon + 0xa40); + observed_crit_chance = *(float *) ((uintptr_t) weapon + 0xbfc); + unknown7 = *(bool *) ((uintptr_t) weapon + 0xb18); + } + weapon_info(IClientEntity *weapon) + { + Load(weapon); + } + void restore_data(IClientEntity *weapon) + { + *(float *) ((uintptr_t) weapon + 0xa38) = crit_bucket; + *(unsigned int *) ((uintptr_t) weapon + 0xb3c) = weapon_seed; + *(unsigned int *) ((uintptr_t) weapon + 0xb30) = unknown1; + *(unsigned int *) ((uintptr_t) weapon + 0xb34) = unknown2; + *(bool *) ((uintptr_t) weapon + 0xb17) = unknown3; + *(float *) ((uintptr_t) weapon + 0xb40) = unknown4; + *(int *) ((uintptr_t) weapon + 0xa3c) = crit_attempts; + *(int *) ((uintptr_t) weapon + 0xa40) = crit_count; + *(float *) ((uintptr_t) weapon + 0xbfc) = observed_crit_chance; + *(bool *) ((uintptr_t) weapon + 0xb18) = unknown7; + } +}; + +float GetWithdrawMult(IClientEntity *wep) +{ + weapon_info info(wep); + // Increment call count + int call_count = info.crit_count + 1; + // How many times there was a check for crits + int crit_checks = info.crit_attempts + 1; + float flMultiply = 0.5; + + if (g_pLocalPlayer->weapon_mode != weapon_melee) + { + float flTmp = min(max((((float) call_count / (float) crit_checks) - 0.1) * 1.111, 0.0), 1.0); + flMultiply = (flTmp * 2.0) + 1.0; } - *g_PredictionRandomSeed = seed_backup; - if (!crit_experimental || g_pLocalPlayer->weapon_mode == weaponmode::weapon_melee) - state.Load(weapon); - if (found) - return number; - return 0; + float flToRemove = flMultiply * 3.0; + return flToRemove; } -void unfuck_bucket(IClientEntity *weapon) +static CatCommand debug_print_mult("debug_print_mult", "print Withdraw multiplier", []() { + if (CE_BAD(LOCAL_E) || CE_BAD(LOCAL_W)) + return; + logging::Info("Multiplier: %f", GetWithdrawMult(RAW_ENT(LOCAL_W))); +}); + +static Timer bucket_cap_refresh{}; +float GetBucketCap() { - static bool changed; + static float return_value; + static ConVar *tf_weapon_criticals = g_ICvar->FindVar("tf_weapon_criticals_bucket_cap"); + if (bucket_cap_refresh.test_and_set(5000)) + return_value = tf_weapon_criticals->GetFloat(); + return return_value; +} + +// Add damage parameter adds the same value that AddToCritbucket would add +bool IsAllowedToWithdrawFromCritBucket(IClientEntity *wep, float flDamage, bool add_damage = true) +{ + weapon_info info(wep); + if (add_damage) + { + info.crit_bucket += flDamage; + if (info.crit_bucket > GetBucketCap()) + info.crit_bucket = GetBucketCap(); + } + float flToRemove = GetWithdrawMult(wep) * flDamage; + // Can remove + if (flToRemove <= info.crit_bucket) + return true; + + return false; +} + +// As opposed to the function above this does actual writing +void SimulateNormalShot(IClientEntity *wep, float flDamage) +{ + weapon_info info(wep); + info.crit_bucket += flDamage; + if (info.crit_bucket > GetBucketCap()) + info.crit_bucket = GetBucketCap(); + // Write other values important for iteration + info.crit_attempts++; + info.restore_data(wep); +} + +bool CanWeaponCrit(IClientEntity *wep) +{ + // Are we allowed to even use that much? + if (g_pLocalPlayer->weapon_mode != weapon_melee && !IsAllowedToWithdrawFromCritBucket(wep, added_per_shot)) + return false; + return true; +} + +// Calculate shots until crit +int ShotsUntilCrit(IClientEntity *wep) +{ + weapon_info info(wep); + // How many shots until we can crit + int shots = 0; + // Predicting 100 shots should be fine + for (shots = 0; shots < 100; shots++) + { + if (IsAllowedToWithdrawFromCritBucket(wep, added_per_shot, true)) + break; + // Do calculations + SimulateNormalShot(wep, added_per_shot); + } + // Restore variables + info.restore_data(wep); + return shots; +} + +typedef float (*AttribHookFloat_t)(float, const char *, IClientEntity *, void *, bool); + +// Calculate Crit penalty +float CalcCritPenalty(IClientEntity *wep) +{ + // Need this to get crit chance from weapon + static uintptr_t AttribHookFloat = gSignatures.GetClientSignature("55 89 E5 57 56 53 83 EC 6C C7 45 ? 00 00 00 00 A1 ? ? ? ? C7 45 ? 00 00 00 00 8B 75 ? 85 C0 0F 84 ? ? ? ? 8D 55 ? 89 04 24 31 DB 89 54 24"); + static AttribHookFloat_t AttribHookFloat_fn = AttribHookFloat_t(AttribHookFloat); + + // Player specific Multiplier + float crit_mult = re::CTFPlayerShared::GetCritMult((re::CTFPlayerShared *) (((uintptr_t) RAW_ENT(LOCAL_E)) + 0x5f3)); + + // Weapon specific Multiplier + static std::string query = "mult_crit_chance"; + float flMultCritChance = AttribHookFloat_fn(crit_mult * 0.02, query.c_str(), wep, 0, 1); + + return flMultCritChance; +} + +std::pair CritMultInfo(IClientEntity *wep) +{ + float cur_crit = CalcCritPenalty(wep); + float observed_chance = CE_FLOAT(LOCAL_W, netvar.flObservedCritChance); + float needed_chance = cur_crit + 0.1f; + return std::pair(observed_chance, needed_chance); +} + +CatCommand debug_observed("debug_observed_crit", "debug", []() { + if (CE_GOOD(LOCAL_E) && CE_GOOD(LOCAL_W)) + logging::Info("%f", CE_FLOAT(LOCAL_W, netvar.flObservedCritChance)); +}); + +CatCommand debug_damage("debug_print_damage_until_crit", "debug", []() { + if (CE_GOOD(LOCAL_E) && CE_GOOD(LOCAL_W)) + { + float penalty = CalcCritPenalty(RAW_ENT(LOCAL_W)); + + float observed_chance = CE_FLOAT(LOCAL_W, netvar.flObservedCritChance); + + rgba_t color = colors::green; + if (penalty + 0.1f < observed_chance) + color = colors::red; + + Color valve_color(color.r * 255.0f, color.g * 255.0f, color.b * 255.0f, color.a * 255.0f); + g_ICvar->ConsoleColorPrintf(valve_color, "%f\n", observed_chance); + } +}); + +CatCommand debug_shots("debug_print_shots_until_crit", "debug", []() { + if (CE_GOOD(LOCAL_E) && CE_GOOD(LOCAL_W)) + logging::Info("%d", ShotsUntilCrit(RAW_ENT(LOCAL_W))); +}); + +int next_crit_tick() +{ + auto wep = RAW_ENT(LOCAL_W); + + int old_seed = MD5_PseudoRandom(current_late_user_cmd->command_number) & 0x7FFFFFFF; + // Try 4096 times + for (int i = 0; i < 4096; i++) + { + int cmd_number = current_late_user_cmd->command_number + i; + // Set random seed + *g_PredictionRandomSeed = MD5_PseudoRandom(cmd_number) & 0x7FFFFFFF; + // Save weapon state to not break anything + weapon_info info(wep); + bool is_crit = re::C_TFWeaponBase::CalcIsAttackCritical(wep); + // Restore state + info.restore_data(wep); + // Is a crit + if (is_crit) + { + *g_PredictionRandomSeed = old_seed; + return cmd_number; + } + } + *g_PredictionRandomSeed = old_seed; + return -1; +} + +static Timer random_crit_refresh{}; +bool random_crits_enabled() +{ + static bool return_value; + static ConVar *tf_weapon_criticals = g_ICvar->FindVar("tf_weapon_criticals"); + if (random_crit_refresh.test_and_set(3000)) + return_value = tf_weapon_criticals->GetBool(); + return return_value; +} + +static int force_ticks = 0; + +bool IsEnabled() +{ + // Random crits on + if (!random_crits_enabled()) + return false; + // Weapon specific checks + if (force_ticks || ((melee && g_pLocalPlayer->weapon_mode == weapon_melee) || (enabled && g_pLocalPlayer->weapon_mode != weapon_melee && (!crit_key || crit_key.isKeyDown())))) + return true; + return false; +} +// clang-format on + +static int idx = 0; +static std::vector crit_cmds; + +void force_crit() +{ + if (!IsEnabled()) + return; + if (!old_mode && g_pLocalPlayer->weapon_mode != weapon_melee && LOCAL_W->m_iClassID() != CL_CLASS(CTFPipebombLauncher)) + { + if (crit_cmds.size()) + { + if (idx > crit_cmds.size()) + idx = 0; + // Magic crit cmds + current_late_user_cmd->command_number = crit_cmds[idx]; + current_late_user_cmd->random_seed = MD5_PseudoRandom(current_late_user_cmd->command_number) & 0x7FFFFFFF; + idx++; + } + } + else + { + int next_crit = next_crit_tick(); + if (next_crit != -1) + { + // We can just force to nearest crit for melee, and sticky launchers apparently + if (g_pLocalPlayer->weapon_mode == weapon_melee || LOCAL_W->m_iClassID() == CL_CLASS(CTFPipebombLauncher)) + { + if (LOCAL_W->m_iClassID() == CL_CLASS(CTFPipebombLauncher)) + { + if (!force_ticks) + force_ticks = 3; + force_ticks--; + } + current_late_user_cmd->command_number = next_crit; + current_late_user_cmd->random_seed = MD5_PseudoRandom(next_crit) & 0x7FFFFFFF; + } + // No crit, don't attack + else if (current_late_user_cmd->command_number != next_crit) + current_user_cmd->buttons &= ~IN_ATTACK; + } + } +} + +void update_cmds() +{ + // Melee weapons do not matter + if (g_pLocalPlayer->weapon_mode == weapon_melee) + return; + auto weapon = RAW_ENT(LOCAL_W); static float last_bucket; static int last_weapon; - if (current_user_cmd->command_number) - changed = false; - float &bucket = re::C_TFWeaponBase::crit_bucket_(weapon); - if (bucket != last_bucket) + int lowest_value = INT_MAX; + int cur_cmdnum = current_late_user_cmd->command_number; + bool old_cmds = false; + for (auto &cmd : crit_cmds) + if (cmd < cur_cmdnum) + old_cmds = true; + if (weapon->entindex() != last_weapon || old_cmds) { - if (changed && weapon->entindex() == last_weapon) + // Used later + int xor_dat = (weapon->entindex() << 8 | LOCAL_E->m_IDX); + // Clear old data + crit_cmds.clear(); + added_per_shot = 0.0f; + + // 30.000.000 seems like a good cap + for (int i = cur_cmdnum, j = 5; i <= cur_cmdnum + 30000000 + 200 && j > 0; i++) { - bucket = last_bucket; + // Reverse seed + int iSeed = MD5_PseudoRandom(i) & 0x7fffffff; + int tempSeed = iSeed ^ (xor_dat); + RandomSeed(tempSeed); + int iResult = RandomInt(0, 9999); + // Returns something low? Store it. + if (iResult <= lowest_value) + { + if (i > cur_cmdnum + 200) + { + // Found lower value + if (iResult < lowest_value) + { + crit_cmds.clear(); + lowest_value = iResult; + j = 5; + } + crit_cmds.push_back(i); + if (added_per_shot == 0.0f) + { + int backup_seed = *g_PredictionRandomSeed; + // Set random seed + *g_PredictionRandomSeed = MD5_PseudoRandom(i) & 0x7FFFFFFF; + // Save weapon state to not break anything + weapon_info info(weapon); + // We modify and write using this + weapon_info write(weapon); + + // Set Values so they don't get in the way + write.crit_bucket = GetBucketCap(); + write.crit_attempts = 100000000; + write.crit_count = 0; + write.observed_crit_chance = 0.0f; + + // Write onto weapon + write.restore_data(weapon); + + bool is_crit = re::C_TFWeaponBase::CalcIsAttackCritical(weapon); + + if (is_crit) + { + weapon_info new_info(weapon); + // How much is Subtracted per shot + added_per_shot = (GetBucketCap() - new_info.crit_bucket) / 3.0f; + } + + // Restore original state + info.restore_data(weapon); + // Adjust with multiplier + + *g_PredictionRandomSeed = backup_seed; + } + j--; + } + } } - changed = true; } last_weapon = weapon->entindex(); last_bucket = bucket; } -struct cached_calculation_s +// Fix bucket on non-local servers +void unfuck_bucket(IClientEntity *weapon) { - int command_number; - int init_command; - int weapon_entity; -}; + INetChannel *ch = (INetChannel *) g_IEngine->GetNetChannelInfo(); + if (!ch) + return; + auto addr = ch->GetRemoteAddress(); + // Local server needs no fixing + if (addr.type == NA_LOOPBACK) + return; + // Melee doesn't either + if (g_pLocalPlayer->weapon_mode == weapon_melee) + return; -static cached_calculation_s cached_calculation{}; + static float last_bucket; + static int last_weapon; + static unsigned last_tick; + static float last_fire_time; -static int number = 0; -static int lastnumber = 0; -static int lastusercmd = 0; -static const model_t *lastweapon = nullptr; - -bool force_crit(IClientEntity *weapon) -{ - auto command_number = current_user_cmd->command_number; - - if (lastnumber < command_number || lastweapon != weapon->GetModel() || lastnumber - command_number > 1000) + weapon_info info(weapon); + float bucket = info.crit_bucket; + float fire_time = NET_FLOAT(weapon, netvar.flLastFireTime); + if (weapon->entindex() == last_weapon) { - if (!*crit_experimental || g_pLocalPlayer->weapon_mode == weapon_melee) + if (bucket != last_bucket && last_bucket != 0) { - if (cached_calculation.init_command > command_number || command_number - cached_calculation.init_command > 4096 || (command_number && (cached_calculation.command_number < command_number))) - cached_calculation.weapon_entity = 0; - if (cached_calculation.weapon_entity == weapon->entindex()) - return bool(cached_calculation.command_number); - } - number = find_next_random_crit_for_weapon(weapon); - } - else - number = lastnumber; - // logging::Info("Found critical: %d -> %d", command_number, - // number); - if (crit_experimental && GetWeaponMode() != weapon_melee) - { - cached_calculation.command_number = number; - cached_calculation.weapon_entity = LOCAL_W->m_IDX; - if (!crit_legiter) - { - if (number && number != command_number) + if (last_tick == tickcount) { - command_number_mod[command_number] = number; - auto ch = (INetChannel *) g_IEngine->GetNetChannelInfo(); - *(int *) ((uint64_t) ch + offsets::m_nOutSequenceNr()) = number - 1; + bucket = last_bucket; + info.crit_bucket = bucket; + info.restore_data(weapon); + return; } - } - else if (number && number != command_number && number - 30 < command_number) - { - command_number_mod[command_number] = number; - auto ch = (INetChannel *) g_IEngine->GetNetChannelInfo(); - *(int *) ((uint64_t) ch + offsets::m_nOutSequenceNr()) = number - 1; + // We want to adjust it ourselves + if (bucket > last_bucket) + bucket = last_bucket; + } + + // We shot, add to bucket + if (last_fire_time != fire_time) + { + bucket += added_per_shot; + // Don't overshoot + if (bucket > GetBucketCap()) + bucket = GetBucketCap(); } } - else - { - if (!crit_legiter) - { - if (command_number != number && number && number != command_number) - current_user_cmd->buttons &= ~IN_ATTACK; - else - current_user_cmd->buttons |= IN_ATTACK; - } - else - { - if (command_number + 30 > number && number && number != command_number) - current_user_cmd->buttons &= ~IN_ATTACK; - else - current_user_cmd->buttons |= IN_ATTACK; - } - } - lastweapon = weapon->GetModel(); - lastnumber = number; - return number != 0; + + last_fire_time = fire_time; + last_tick = tickcount; + last_weapon = weapon->entindex(); + last_bucket = bucket; + + info.crit_bucket = bucket; + info.restore_data(weapon); } -void handle_melee_crit(IClientEntity *weapon) +void CreateMove() { - // Should i crit or save my crits? - bool in_range = false; - for (int i = 1; i <= g_IEngine->GetMaxClients(); i++) - { - // We already found out we should crit - if (in_range) - break; - CachedEntity *target = ENTITY(i); - - // Various ignores - if (CE_BAD(target) || !target->m_bAlivePlayer() || !player_tools::shouldTarget(target) || !target->m_bEnemy()) - continue; - - // Shorten code - namespace bt = hacks::shared::backtrack; - - // If backtracking, use the backtrack ticks - if (bt::isBacktrackEnabled) - { - // Loop all ticks - for (int j = 0; j < 66; j++) - { - auto bt_data = bt::headPositions[i][j]; - // Invalid tick - if (!bt::ValidTick(bt_data, target)) - continue; - // If within 400 HU range then crit, else just farm crits - if (bt_data.collidable.center.DistTo(g_pLocalPlayer->v_Eye) < 400.0f) - { - in_range = true; - break; - } - } - } - else - { - Vector ent_orig = target->m_vecOrigin(); - // Center ent origin - ent_orig += (RAW_ENT(target)->GetCollideable()->OBBMaxs() + RAW_ENT(target)->GetCollideable()->OBBMins()) / 2; - // In range, crit! - if (ent_orig.DistTo(g_pLocalPlayer->v_Eye) < 400.0f) - { - in_range = true; - break; - } - } - } - if (in_range) - force_crit(weapon); - else - { - // Please don't crit thanks - if (find_next_random_crit_for_weapon(weapon) == current_user_cmd->command_number) - { - // Backup - int original_cmd = current_user_cmd->command_number; - // How many crits in a row? - int found = 0; - // Ensure not to cockblock when critboosted, 5 should be plently - for (int i = 0; i < 10; i++) - { - current_user_cmd->command_number++; - if (find_next_random_crit_for_weapon(weapon) == current_user_cmd->command_number) - found++; - } - // If we found less than 10 crits in a row, please don't crit thanks - if (found < 10) - current_user_cmd->buttons &= ~IN_ATTACK; - current_user_cmd->command_number = original_cmd; - } - } -} -void create_move() -{ - if (!crit_key && !crit_melee && !auto_crit) + if (!enabled && !melee && !draw) return; - if (!random_crits_enabled()) + if (CE_BAD(LOCAL_E) || CE_BAD(LOCAL_W)) return; - if (CE_BAD(LOCAL_W)) + + unfuck_bucket(RAW_ENT(LOCAL_W)); + // Update cmds + update_cmds(); + if (!enabled && !melee) return; - if (current_user_cmd->command_number) - lastusercmd = current_user_cmd->command_number; + if (!force_ticks && (!(melee && g_pLocalPlayer->weapon_mode == weapon_melee) && crit_key && !crit_key.isKeyDown())) + return; + if (!current_late_user_cmd->command_number) + return; + IClientEntity *weapon = RAW_ENT(LOCAL_W); if (!re::C_TFWeaponBase::IsBaseCombatWeapon(weapon)) return; if (!re::C_TFWeaponBase::AreRandomCritsEnabled(weapon)) return; - if (!CanShoot()) + if (!re::C_TFWeaponBase::CanFireCriticalShot(weapon, false, nullptr)) return; - unfuck_bucket(weapon); - if ((current_user_cmd->buttons & IN_ATTACK) && ((crit_key && crit_key.isKeyDown()) || auto_crit) && current_user_cmd->command_number) + + if (force_ticks || (CanShoot() && current_late_user_cmd->buttons & IN_ATTACK)) { - if (GetWeaponMode() == weapon_melee) - handle_melee_crit(weapon); + // Are we even allowed to crit? + if (CanWeaponCrit(RAW_ENT(LOCAL_W))) + force_crit(); + } +} + +// Storage +static int last_crit_tick = -1; +static int last_bucket = 0; +static int shots_until_crit = 0; +static int last_wep = 0; +static std::pair crit_mult_info; +void Draw() +{ + if (!draw) + return; + if (!g_IEngine->GetNetChannelInfo()) + last_crit_tick = -1; + if (CE_GOOD(LOCAL_E) && CE_GOOD(LOCAL_W)) + { + auto wep = RAW_ENT(LOCAL_W); + float bucket = weapon_info(wep).crit_bucket; + + // Reset because too old + if (wep->entindex() != last_wep || last_crit_tick - current_late_user_cmd->command_number < 0 || (last_crit_tick - current_late_user_cmd->command_number) * g_GlobalVars->interval_per_tick > 30) + last_crit_tick = next_crit_tick(); + + // Used by multiple things + bool can_crit = CanWeaponCrit(wep); + + if (bucket != last_bucket || wep->entindex() != last_wep) + { + // Recalculate shots until crit + if (!can_crit) + shots_until_crit = ShotsUntilCrit(wep); + // Recalc crit multiplier info + crit_mult_info = CritMultInfo(wep); + } + + // Display for when crithack is active/can't be active + if (IsEnabled() && last_crit_tick != -1) + AddCenterString("Forcing Crits!", colors::red); + + // Mark if the weapon can randomly crit or not + if (!re::C_TFWeaponBase::CanFireCriticalShot(RAW_ENT(LOCAL_W), false, nullptr)) + { + AddCenterString("Weapon cannot randomly crit.", colors::red); + return; + } + + if (!can_crit) + AddCenterString("Shots until crit: " + std::to_string(shots_until_crit), colors::orange); + + // Mark bucket as ready/not ready + auto color = colors::red; + if (can_crit) + color = colors::green; + AddCenterString("Crit Bucket: " + std::to_string(bucket), color); + + // Print crit Multiplier information + + // Crit chance is low enough + if (crit_mult_info.first < crit_mult_info.second) + color = colors::green; + // Cannot crit else - force_crit(weapon); - } - else if ((current_user_cmd->buttons & IN_ATTACK) && current_user_cmd->command_number && GetWeaponMode() == weapon_melee && (crit_melee || auto_crit) && g_pLocalPlayer->weapon()->m_iClassID() != CL_CLASS(CTFKnife)) - { - handle_melee_crit(weapon); - } - else if ((current_user_cmd->buttons & IN_ATTACK) && current_user_cmd->command_number && GetWeaponMode() != weapon_melee && crit_key) - { - // Save crits! - if (find_next_random_crit_for_weapon(weapon) == current_user_cmd->command_number) + color = colors::red; + AddCenterString("Crit Penalty: " + std::to_string(crit_mult_info.first), color); + + // Time until crit + if (old_mode && can_crit && last_crit_tick != -1) { - // Backup - int original_cmd = current_user_cmd->command_number; - // How many crits in a row? - int found = 0; - // Ensure not to cockblock when critboosted, 5 should be plently - for (int i = 0; i < 6; i++) - { - current_user_cmd->command_number++; - if (find_next_random_crit_for_weapon(weapon) == current_user_cmd->command_number) - found++; - } - // If we found less than 5 crits in a row, please don't crit thanks - if (found < 5) - current_user_cmd->buttons &= ~IN_ATTACK; - current_user_cmd->command_number = original_cmd; + // Ticks / Ticks per second + float time = (last_crit_tick - current_late_user_cmd->command_number) * g_GlobalVars->interval_per_tick; + AddCenterString("Crit in " + std::to_string(time) + "s", colors::orange); } + + // Update + last_bucket = bucket; + last_wep = wep->entindex(); } } -bool random_crits_enabled() +void LevelShutdown() { - static ConVar *tf_weapon_criticals = g_ICvar->FindVar("tf_weapon_criticals"); - return tf_weapon_criticals->GetBool(); + last_crit_tick = -1; } -#if ENABLE_VISUALS -void draw() -{ - if (CE_BAD(LOCAL_W)) - return; - IClientEntity *weapon = RAW_ENT(LOCAL_W); - if (!weapon) - return; - if (!re::C_TFWeaponBase::IsBaseCombatWeapon(weapon)) - return; - if (!re::C_TFWeaponBase::AreRandomCritsEnabled(weapon)) - return; - if (crit_info && CE_GOOD(LOCAL_W)) - { - if (crit_key.isKeyDown() || auto_crit) - { - AddCenterString("FORCED CRITS!", colors::red); - } - IF_GAME(IsTF2()) - { - if (!random_crits_enabled()) - AddCenterString("Random crits are disabled", colors::yellow); - else - { - if (!re::C_TFWeaponBase::CanFireCriticalShot(RAW_ENT(LOCAL_W), false, nullptr)) - AddCenterString("Weapon can't randomly crit", colors::yellow); - else if (lastusercmd) - { - if (number > lastusercmd) - { - float nextcrit = ((float) number - (float) lastusercmd) / (float) 90; - if (nextcrit > 0.0f) - { - AddCenterString(format("Time to next crit: ", nextcrit, "s"), colors::orange); - } - } - AddCenterString("Weapon can randomly crit"); - } - } - AddCenterString(format("Bucket: ", re::C_TFWeaponBase::crit_bucket_(RAW_ENT(LOCAL_W)))); - } - // AddCenterString(format("Time: ", - // *(float*)((uintptr_t)RAW_ENT(LOCAL_W) + 2872u))); - } -} -#endif static InitRoutine init([]() { - EC::Register(EC::CreateMove, criticals::create_move, "cm_crits", EC::very_late); -#if ENABLE_VISUALS - EC::Register(EC::Draw, criticals::draw, "draw_crits"); -#endif + EC::Register(EC::CreateMoveLate, CreateMove, "crit_cm"); + EC::Register(EC::Draw, Draw, "crit_draw"); + EC::Register(EC::LevelShutdown, LevelShutdown, "crit_lvlshutdown"); }); } // namespace criticals - -void crithack_saved_state::Load(IClientEntity *entity) -{ - *(float *) (uintptr_t(entity) + 2868) = unknown2868; - *(float *) (uintptr_t(entity) + 2864) = unknown2864; - *(float *) (uintptr_t(entity) + 2880) = unknown2880; - *(float *) (uintptr_t(entity) + 2616) = bucket2616; - *(int *) (uintptr_t(entity) + 2620) = unknown2620; - *(int *) (uintptr_t(entity) + 2876) = seed2876; - *(char *) (uintptr_t(entity) + 2839) = unknown2839; -} - -void crithack_saved_state::Save(IClientEntity *entity) -{ - unknown2868 = *(float *) (uintptr_t(entity) + 2868); - unknown2864 = *(float *) (uintptr_t(entity) + 2864); - unknown2880 = *(float *) (uintptr_t(entity) + 2880); - bucket2616 = *(float *) (uintptr_t(entity) + 2616); - unknown2620 = *(int *) (uintptr_t(entity) + 2620); - seed2876 = *(int *) (uintptr_t(entity) + 2876); - unknown2839 = *(char *) (uintptr_t(entity) + 2839); -} diff --git a/src/globals.cpp b/src/globals.cpp index 8ecb8e2f..174c80a7 100644 --- a/src/globals.cpp +++ b/src/globals.cpp @@ -42,6 +42,7 @@ void GlobalSettings::Init() } CUserCmd *current_user_cmd{ nullptr }; +CUserCmd *current_late_user_cmd{ nullptr }; bool isHackActive() { diff --git a/src/hack.cpp b/src/hack.cpp index 88626511..bc207585 100644 --- a/src/hack.cpp +++ b/src/hack.cpp @@ -185,6 +185,7 @@ void hack::Hook() hooks::client.Set(g_IBaseClient); hooks::client.HookMethod(HOOK_ARGS(DispatchUserMessage)); + hooks::client.HookMethod(HOOK_ARGS(CreateMoveLate)); #if ENABLE_VISUALS hooks::client.HookMethod(HOOK_ARGS(FrameStageNotify)); hooks::client.HookMethod(HOOK_ARGS(IN_KeyEvent)); diff --git a/src/helpers.cpp b/src/helpers.cpp index 54b59ccf..538d5024 100644 --- a/src/helpers.cpp +++ b/src/helpers.cpp @@ -1575,6 +1575,15 @@ bool IsEntityVisiblePenetration(CachedEntity *entity, int hb) return false; } +void ValidateUserCmd(CUserCmd *cmd, int sequence_nr) +{ + CRC32_t crc = GetChecksum(cmd); + if (crc != GetVerifiedCmds(g_IInput)[sequence_nr % VERIFIED_CMD_SIZE].m_crc) + { + *cmd = GetVerifiedCmds(g_IInput)[sequence_nr % VERIFIED_CMD_SIZE].m_cmd; + } +} + // Used for getting class names CatCommand print_classnames("debug_print_classnames", "Lists classnames currently available in console", []() { // Create a tmp ent for the loop diff --git a/src/hooks/CreateMove.cpp b/src/hooks/CreateMove.cpp index 88b1048f..31a464be 100644 --- a/src/hooks/CreateMove.cpp +++ b/src/hooks/CreateMove.cpp @@ -411,4 +411,44 @@ DEFINE_HOOKED_METHOD(CreateMove, bool, void *this_, float input_sample_time, CUs } return ret; } + +void WriteCmd(IInput *input, CUserCmd *cmd, int sequence_nr) +{ + // Write the usercmd + GetVerifiedCmds(input)[sequence_nr % VERIFIED_CMD_SIZE].m_cmd = *cmd; + GetVerifiedCmds(input)[sequence_nr % VERIFIED_CMD_SIZE].m_crc = GetChecksum(cmd); + GetCmds(input)[sequence_nr % VERIFIED_CMD_SIZE] = *cmd; +} + +DEFINE_HOOKED_METHOD(CreateMoveLate, void, void *this_, int sequence_nr, float input_sample_time, bool arg3) +{ + // Call original function, includes Early CreateMove + original::CreateMoveLate(this_, sequence_nr, input_sample_time, arg3); + CUserCmd *cmd = g_IInput->GetUserCmd(sequence_nr); + + if (!cmd) + return; + + current_late_user_cmd = cmd; + + if (!isHackActive()) + { + WriteCmd(g_IInput, current_late_user_cmd, sequence_nr); + return; + } + + if (!g_IEngine->IsInGame()) + { + WriteCmd(g_IInput, current_late_user_cmd, sequence_nr); + return; + } + + PROF_SECTION(CreateMoveLate); + + // Run EC + EC::run(EC::CreateMoveLate); + + // Write the usercmd + WriteCmd(g_IInput, current_late_user_cmd, sequence_nr); +} } // namespace hooked_methods diff --git a/src/hooks/GetUserCmd.cpp b/src/hooks/GetUserCmd.cpp index 3d787ee3..58fe3dc5 100644 --- a/src/hooks/GetUserCmd.cpp +++ b/src/hooks/GetUserCmd.cpp @@ -10,26 +10,10 @@ namespace hooked_methods DEFINE_HOOKED_METHOD(GetUserCmd, CUserCmd *, IInput *this_, int sequence_number) { - CUserCmd *def = original::GetUserCmd(this_, sequence_number); - int oldcmd; - INetChannel *ch; - - if (def == nullptr) - return def; - - if (command_number_mod.find(def->command_number) != command_number_mod.end()) - { - // logging::Info("Replacing command %i with %i", def->command_number, - // command_number_mod[def->command_number]); - oldcmd = def->command_number; - def->command_number = command_number_mod[def->command_number]; - def->random_seed = MD5_PseudoRandom(unsigned(def->command_number)) & 0x7fffffff; - command_number_mod.erase(command_number_mod.find(oldcmd)); - *(int *) ((unsigned) g_IBaseClientState + offsets::lastoutgoingcommand()) = def->command_number - 1; - ch = (INetChannel *) g_IEngine->GetNetChannelInfo(); //*(INetChannel**)((unsigned)g_IBaseClientState - //+ offsets::m_NetChannel()); - *(int *) ((unsigned) ch + offsets::m_nOutSequenceNr()) = def->command_number - 1; - } - return def; + // We need to overwrite this if crithack is on + if (criticals::enabled || criticals::melee) + return &GetCmds(this_)[sequence_number % VERIFIED_CMD_SIZE]; + else + return original::GetUserCmd(this_, sequence_number); } -} // namespace hooked_methods \ No newline at end of file +} // namespace hooked_methods