diff --git a/addons/sourcemod/configs/vsh/vsh.cfg b/addons/sourcemod/configs/vsh/vsh.cfg index 4f0d7284..d3815576 100644 --- a/addons/sourcemod/configs/vsh/vsh.cfg +++ b/addons/sourcemod/configs/vsh/vsh.cfg @@ -2433,6 +2433,7 @@ "vsh_rps_enable" "0" //Allow everyone use Rock Paper Scissors Taunt? "vsh_blacklist_amount" "2" //How many bosses can a player blacklist? (0 disables this feature) "vsh_top_score_outline" "0" //Should bosses see the top scoring player through walls via an outline? + "vsh_block_fake_hit_sound" "1" //Should melee hitsound desync be blocked by the gamemode? "vsh_boss_chance_saxton" "0.30" //% chance for next boss to be Saxton Hale from normal bosses pool (0.0 - 1.0) "vsh_boss_chance_multi" "0.15" //% chance for next boss to be Multiple bosses (0.0 - 1.0) diff --git a/addons/sourcemod/gamedata/vsh.txt b/addons/sourcemod/gamedata/vsh.txt index 52f74939..08f44874 100644 --- a/addons/sourcemod/gamedata/vsh.txt +++ b/addons/sourcemod/gamedata/vsh.txt @@ -40,6 +40,12 @@ "linux" "@_ZN16CObjectDispenser15CouldHealTargetEP11CBaseEntity" "windows" "\x55\x8B\xEC\x53\x56\x8B\x75\x08\x57\x8B\xF9\x8B\x87\x38\x01\x00\x00" } + "CTFWeaponBaseMelee::DoSwingTraceInternal" + { + "library" "server" + "linux" "@_ZN18CTFWeaponBaseMelee20DoSwingTraceInternalER10CGameTracebP10CUtlVectorIS0_10CUtlMemoryIS0_iEE" + "windows" "\x53\x8B\xDC\x83\xEC\x08\x83\xE4\xF0\x83\xC4\x04\x55\x8B\x6B\x04\x89\x6C\x24\x04\x8B\xEC\x81\xEC\x38\x05\x00\x00" + } } "Functions" { @@ -93,6 +99,28 @@ } } } + "CTFWeaponBaseMelee::DoSwingTraceInternal" + { + "signature" "CTFWeaponBaseMelee::DoSwingTraceInternal" + "callconv" "thiscall" + "return" "bool" + "this" "entity" + "arguments" + { + "trace" + { + "type" "objectptr" + } + "bCleave" + { + "type" "bool" + } + "pTargetTraceVector" + { + "type" "vectorptr" + } + } + } } "Offsets" { diff --git a/addons/sourcemod/scripting/saxtonhale.sp b/addons/sourcemod/scripting/saxtonhale.sp index c7ad6356..f69eba83 100644 --- a/addons/sourcemod/scripting/saxtonhale.sp +++ b/addons/sourcemod/scripting/saxtonhale.sp @@ -462,6 +462,7 @@ ConVar tf_arena_preround_time; #include "vsh/function/func_native.sp" #include "vsh/blacklist.sp" +#include "vsh/custommelee.sp" #include "vsh/classlimit.sp" #include "vsh/command.sp" #include "vsh/console.sp" @@ -540,6 +541,7 @@ public void OnPluginStart() Command_Init(); Console_Init(); Cookies_Init(); + CustomMelee_Init(); Dome_Init(); Event_Init(); FuncClass_Init(); @@ -601,6 +603,7 @@ public void OnPluginStart() SaxtonHaleFunction("OnWeaponSwitchPost", ET_Ignore, Param_Cell); SaxtonHaleFunction("OnConditionAdded", ET_Ignore, Param_Cell); SaxtonHaleFunction("OnConditionRemoved", ET_Ignore, Param_Cell); + SaxtonHaleFunction("OnArenaRoundStart", ET_Ignore); func = SaxtonHaleFunction("OnSoundPlayed", ET_Hook, Param_Array, Param_CellByRef, Param_String, Param_CellByRef, Param_FloatByRef, Param_CellByRef, Param_CellByRef, Param_CellByRef, Param_String, Param_CellByRef); func.SetParam(1, Param_Array, VSHArrayType_Static, MAXPLAYERS); @@ -763,6 +766,7 @@ public void OnPluginStart() g_ConfigConvar.Create("vsh_rps_enable", "1", "Allow everyone use Rock Paper Scissors Taunt?", _, true, 0.0, true, 1.0); g_ConfigConvar.Create("vsh_blacklist_amount", "2", "Maximum amount of bosses a player can blacklist for themselves (0 disables the feature)", _, true, 0.0); g_ConfigConvar.Create("vsh_top_score_outline", "0", "Lets bosses see the top scoring player through walls via an outline.", _, true, 0.0, true, 1.0); + g_ConfigConvar.Create("vsh_block_fake_hit_sound", "1", "Makes the gamemode block fake client predicted melee hit sounds.", _, true, 0.0, true, 1.0); //Incase of lateload, call client join functions for (int iClient = 1; iClient <= MaxClients; iClient++) @@ -828,6 +832,7 @@ public void OnPluginEnd() RemoveClientGlowEnt(iClient); } + CustomMelee_OnPluginEnd(); Plugin_Cvars(false); } @@ -987,6 +992,7 @@ public void OnMapStart() g_iSpritesLaserbeam = PrecacheModel("materials/sprites/laserbeam.vmt", true); g_iSpritesGlow = PrecacheModel("materials/sprites/glow01.vmt", true); + CustomMelee_OnMapStart(); Dome_MapStart(); CreateTimer(60.0, Timer_WelcomeMessage); @@ -1083,6 +1089,11 @@ public void OnEntityCreated(int iEntity, const char[] sClassname) } } +public void OnEntityDestroyed(int iEntity) +{ + CustomMelee_OnEntityDestroyed(iEntity); +} + public Action Crossbow_OnTouch(int iEntity, int iToucher) { if (iToucher <= 0 || iToucher > MaxClients || !IsClientInGame(iToucher)) @@ -1748,8 +1759,13 @@ void Client_OnButtonRelease(int iClient, int button) public Action TF2_CalcIsAttackCritical(int iClient, int iWeapon, char[] sWepClassName, bool &bResult) { - if (!g_bEnabled) return Plugin_Continue; - if (g_iTotalRoundPlayed <= 0) return Plugin_Continue; + if (!g_bEnabled) + return Plugin_Continue; + + CustomMelee_CalcIsAttackCritical(iWeapon, sWepClassName); + + if (g_iTotalRoundPlayed <= 0) + return Plugin_Continue; SaxtonHaleBase boss = SaxtonHaleBase(iClient); if (boss.bValid) @@ -1790,6 +1806,8 @@ public Action NormalSoundHook(int clients[MAXPLAYERS], int &numClients, char sam if (boss.bValid) return boss.CallFunction("OnSoundPlayed", clients, numClients, sample, channel, volume, level, pitch, flags, soundEntry, seed); } + + CustomMelee_OnSoundHook(clients, numClients, entity, channel); return Plugin_Continue; } diff --git a/addons/sourcemod/scripting/vsh/abilities/ability_dash_jump.sp b/addons/sourcemod/scripting/vsh/abilities/ability_dash_jump.sp index a199cbd8..d5a27a51 100644 --- a/addons/sourcemod/scripting/vsh/abilities/ability_dash_jump.sp +++ b/addons/sourcemod/scripting/vsh/abilities/ability_dash_jump.sp @@ -30,6 +30,13 @@ public void DashJump_GetHudInfo(SaxtonHaleBase boss, char[] sMessage, int iLengt Format(sMessage, iLength, "%s\nDash charge: %d%%%%", sMessage, iCharge); } +public void DashJump_OnArenaRoundStart(SaxtonHaleBase boss) +{ + float flTimeUntilMaxCharge = boss.GetPropFloat("DashJump", "Cooldown") * boss.GetPropFloat("DashJump", "MaxCharge"); + g_flDashJumpCooldownWait[boss.iClient] = GetGameTime() + flTimeUntilMaxCharge; + boss.CallFunction("UpdateHudInfo", 0.0, flTimeUntilMaxCharge); // Update every frame until dash charge maxes out +} + public void DashJump_OnButtonPress(SaxtonHaleBase boss, int iButton) { if (iButton == IN_RELOAD && GameRules_GetRoundState() != RoundState_Preround && !TF2_IsPlayerInCondition(boss.iClient, TFCond_Dazed)) diff --git a/addons/sourcemod/scripting/vsh/abilities/ability_lunge.sp b/addons/sourcemod/scripting/vsh/abilities/ability_lunge.sp index b76231c8..7cbb39c1 100644 --- a/addons/sourcemod/scripting/vsh/abilities/ability_lunge.sp +++ b/addons/sourcemod/scripting/vsh/abilities/ability_lunge.sp @@ -42,6 +42,13 @@ public void Lunge_GetHudInfo(SaxtonHaleBase boss, char[] sMessage, int iLength, } } +public void Lunge_OnArenaRoundStart(SaxtonHaleBase boss) +{ + float flTimeUntilMaxCharge = boss.GetPropFloat("Lunge", "Cooldown"); + g_flLungeCooldownWait[boss.iClient] = GetGameTime() + flTimeUntilMaxCharge; + boss.CallFunction("UpdateHudInfo", 0.0, flTimeUntilMaxCharge); // Update every frame until charge maxes out +} + static bool CanLunge(SaxtonHaleBase boss) { return !TF2_IsPlayerInCondition(boss.iClient, TFCond_Dazed) && diff --git a/addons/sourcemod/scripting/vsh/bosses/boss_pyrocar.sp b/addons/sourcemod/scripting/vsh/bosses/boss_pyrocar.sp index f120bf30..8ab3d870 100644 --- a/addons/sourcemod/scripting/vsh/bosses/boss_pyrocar.sp +++ b/addons/sourcemod/scripting/vsh/bosses/boss_pyrocar.sp @@ -288,7 +288,7 @@ public Action PyroCar_OnAttackDamageAlive(SaxtonHaleBase boss, int victim, int & } } - g_hPyrocarHealTimer[victim] = CreateTimer(0.6, Timer_RemoveLessHealing, GetClientSerial(victim)); + g_hPyrocarHealTimer[victim] = CreateTimer(1.0, Timer_RemoveLessHealing, GetClientSerial(victim)); } if (g_flPyrocarGasCharge[boss.iClient] <= g_iMaxGasPassers * g_flGasMinCharge) diff --git a/addons/sourcemod/scripting/vsh/command.sp b/addons/sourcemod/scripting/vsh/command.sp index b7d33cd6..421afd1e 100644 --- a/addons/sourcemod/scripting/vsh/command.sp +++ b/addons/sourcemod/scripting/vsh/command.sp @@ -39,6 +39,7 @@ public void Command_Init() Command_Create("special", Command_ForceSpecialRound); Command_Create("dome", Command_ForceDome); Command_Create("rage", Command_SetRage); + Command_Create("blacklistdata", Command_BlacklistData); } stock void Command_Create(const char[] sCommand, ConCmd callback) @@ -517,5 +518,93 @@ public Action Command_SetRage(int iClient, int iArgs) } ReplyToCommand(iClient, "%s%s You do not have permission to use this command.", TEXT_TAG, TEXT_ERROR); + return Plugin_Handled; +} + +enum struct BlacklistData +{ + int iAmount; + char sType[32]; + char sName[64]; +} + +public Action Command_BlacklistData(int iClient, int iArgs) +{ + if (!g_bEnabled) return Plugin_Continue; + + // Command has no menu option and doesn't a reply to people without access on purpose + if (Client_HasFlag(iClient, ClientFlags_Admin)) + { + ArrayList aList = new ArrayList(sizeof(BlacklistData)); + for (int iOther = 1; iOther <= MaxClients; iOther++) + { + if (!IsClientInGame(iOther) || IsFakeClient(iOther)) + continue; + + ArrayList aBlacklist = Blacklist_Get(iOther); + int iLength = aBlacklist.Length; + if (iLength == 0) + { + delete aBlacklist; + continue; + } + + for (int i = 0; i < iLength; i++) + { + BlacklistData data; + aBlacklist.GetString(i, data.sType, sizeof(data.sType)); + + int iIndex = aList.FindString(data.sType, BlacklistData::sType); + if (iIndex == -1) + { + SaxtonHale_CallFunction(data.sType, "GetBossName", data.sName, sizeof(data.sName)); + data.iAmount = 1; + aList.PushArray(data); + } + else + { + aList.GetArray(iIndex, data); + data.iAmount++; + aList.SetArray(iIndex, data); + } + } + + delete aBlacklist; + } + + aList.Sort(Sort_Descending, Sort_Integer); + + char sMessage[2048]; + int iLength = aList.Length; + if (iLength == 0) + { + sMessage = " \nNo bosses are blacklisted in this session.\n " + } + else + { + sMessage = " \nList of bosses blacklisted in this session:\n \n" + + for (int i = 0; i < iLength; i++) + { + BlacklistData data; + aList.GetArray(i, data, sizeof(data)); + + Format(sMessage, sizeof(sMessage), "%s- %s: %d\n", sMessage, data.sName, data.iAmount); + } + } + + delete aList; + + if (iClient == 0) + { + PrintToServer(sMessage); + } + else + { + PrintToChat(iClient, "%s%s Blacklist data has been sent to your console, if it exists.", TEXT_TAG, TEXT_COLOR); + PrintToConsole(iClient, sMessage); + } + } + return Plugin_Handled; } \ No newline at end of file diff --git a/addons/sourcemod/scripting/vsh/custommelee.sp b/addons/sourcemod/scripting/vsh/custommelee.sp new file mode 100644 index 00000000..a554a4a1 --- /dev/null +++ b/addons/sourcemod/scripting/vsh/custommelee.sp @@ -0,0 +1,152 @@ +#pragma semicolon 1 +#pragma newdecls required + +enum struct MeleeData +{ + int iEntity; + float flMeleeRange; + float flMeleeBounds; +} + +static ArrayList g_aMeleeSavedData; +static bool g_bGlobalNextSound[MAXPLAYERS + 1]; + +void CustomMelee_Init() +{ + g_aMeleeSavedData = new ArrayList(sizeof(MeleeData)); +} + +void CustomMelee_OnMapStart() +{ + g_aMeleeSavedData.Clear(); +} + +void CustomMelee_OnEntityDestroyed(int iEntity) +{ + int iIndex = g_aMeleeSavedData.FindValue(iEntity); + if (iIndex != -1) + g_aMeleeSavedData.Erase(iIndex); +} + +void CustomMelee_OnPluginEnd() +{ + int iEntity = INVALID_ENT_REFERENCE; + while ((iEntity = FindEntityByClassname(iEntity, "tf_weap*")) != INVALID_ENT_REFERENCE) + { + int iIndex = g_aMeleeSavedData.FindValue(iEntity); + if (iIndex != -1) + RestoreMeleeData(iEntity); + } +} + +Action CustomMelee_OnSoundHook(int clients[MAXPLAYERS], int &numClients, int &entity, int &channel) +{ + if (channel == SNDCHAN_STATIC && entity > 0 && entity <= MaxClients) + { + // Play the server-sided sound to the client + if (g_bGlobalNextSound[entity]) + { + g_bGlobalNextSound[entity] = false; + + clients[numClients] = entity; + numClients++; + return Plugin_Changed; + } + } + + return Plugin_Continue; +} + +void CustomMelee_CalcIsAttackCritical(int iWeapon, const char[] sClassname) +{ + if (!g_ConfigConvar.LookupBool("vsh_block_fake_hit_sound")) + return; + + int iOwner = GetEntPropEnt(iWeapon, Prop_Send, "m_hOwnerEntity"); + if (iOwner <= 0 || iOwner > MaxClients) + return; + + if (TF2_GetItemInSlot(iOwner, TFWeaponSlot_Melee) != iWeapon) + return; + + // Ignore spy knives with this logic for now + if (StrEqual(sClassname, "tf_weapon_knife")) + return; + + SaveMeleeData(iWeapon); +} + +void CustomMelee_DoSwingTracePre(int iWeapon) +{ + if (!g_ConfigConvar.LookupBool("vsh_block_fake_hit_sound")) + return; + + RestoreMeleeData(iWeapon); +} + +void CustomMelee_DoSwingTracePost(int iWeapon, bool bHit) +{ + if (!g_ConfigConvar.LookupBool("vsh_block_fake_hit_sound")) + return; + + int iIndex = g_aMeleeSavedData.FindValue(iWeapon); + if (iIndex == -1) + return; + + TF2Attrib_SetByName(iWeapon, "melee bounds multiplier", 0.0); + TF2Attrib_SetByName(iWeapon, "melee range multiplier", 0.0); + + if (bHit) + { + // Play the server-sided sound to the client + int iOwner = GetEntPropEnt(iWeapon, Prop_Send, "m_hOwnerEntity"); + if (iOwner > 0 && iOwner <= MaxClients) + { + char sClassname[64]; + GetEntityClassname(iWeapon, sClassname, sizeof(sClassname)); + if (!StrEqual(sClassname, "tf_weapon_knife")) + g_bGlobalNextSound[iOwner] = true; + } + } +} + +static void SaveMeleeData(int iWeapon) +{ + float flBounds = TF2Attrib_HookValueFloat(1.0, "melee_bounds_multiplier", iWeapon); + float flRange = TF2Attrib_HookValueFloat(1.0, "melee_range_multiplier", iWeapon); + + int iIndex = g_aMeleeSavedData.FindValue(iWeapon); + if (flBounds > 0.0 || flRange > 0.0 || iIndex == -1) + { + MeleeData data; + data.iEntity = iWeapon; + data.flMeleeBounds = flBounds; + data.flMeleeRange = flRange; + + if (iIndex != -1) + g_aMeleeSavedData.SetArray(iIndex, data); + else + g_aMeleeSavedData.PushArray(data); + } + + TF2Attrib_SetByName(iWeapon, "melee bounds multiplier", 0.0); + TF2Attrib_SetByName(iWeapon, "melee range multiplier", 0.0); +} + +static void RestoreMeleeData(int iWeapon) +{ + int iIndex = g_aMeleeSavedData.FindValue(iWeapon); + if (iIndex == -1) + return; + + MeleeData data; + g_aMeleeSavedData.GetArray(iIndex, data, sizeof(data)); + + if (data.flMeleeBounds > 0.0) + TF2Attrib_SetByName(iWeapon, "melee bounds multiplier", data.flMeleeBounds); + + if (data.flMeleeRange > 0.0) + TF2Attrib_SetByName(iWeapon, "melee range multiplier", data.flMeleeRange); + + g_aMeleeSavedData.Erase(iIndex); +} \ No newline at end of file diff --git a/addons/sourcemod/scripting/vsh/event.sp b/addons/sourcemod/scripting/vsh/event.sp index b9e70c4e..293ed22f 100644 --- a/addons/sourcemod/scripting/vsh/event.sp +++ b/addons/sourcemod/scripting/vsh/event.sp @@ -206,12 +206,14 @@ public void Event_RoundArenaStart(Event event, const char[] sName, bool bDontBro } } - //Refresh boss health from player count for (int iClient = 1; iClient <= MaxClients; iClient++) { SaxtonHaleBase boss = SaxtonHaleBase(iClient); if (boss.bValid) { + boss.CallFunction("OnArenaRoundStart"); + + //Refresh boss health from player count int iHealth = boss.CallFunction("CalculateMaxHealth"); boss.iMaxHealth = iHealth; boss.iHealth = iHealth; diff --git a/addons/sourcemod/scripting/vsh/menu.sp b/addons/sourcemod/scripting/vsh/menu.sp index 18d90cb6..02e629f1 100644 --- a/addons/sourcemod/scripting/vsh/menu.sp +++ b/addons/sourcemod/scripting/vsh/menu.sp @@ -37,10 +37,11 @@ void Menu_Init() Format(buffer, sizeof(buffer), "%s \nKirillian - Several boss model addition", buffer); Format(buffer, sizeof(buffer), "%s \nSediSocks - Announcer model", buffer); Format(buffer, sizeof(buffer), "%s \nArtvin & Crusty - Modified Saxton Hale model", buffer); + Format(buffer, sizeof(buffer), "%s \nArtvin - Melee hitsound desync fix", buffer); Format(buffer, sizeof(buffer), "%s \nsarysa - Modified Yeti concept", buffer); Format(buffer, sizeof(buffer), "%s \nAlex Turtle & Chillax - Original Rewrite test subjects", buffer); Format(buffer, sizeof(buffer), "%s \nwo - Test subject", buffer); - Format(buffer, sizeof(buffer), "%s \nRedSun - Host community!", buffer); + Format(buffer, sizeof(buffer), "%s \nRed Sun - Host community!", buffer); g_hMenuCredits.SetTitle(buffer); g_hMenuCredits.AddItem("back", "<- Back"); diff --git a/addons/sourcemod/scripting/vsh/sdk.sp b/addons/sourcemod/scripting/vsh/sdk.sp index 8e5649f2..a43b2fcf 100644 --- a/addons/sourcemod/scripting/vsh/sdk.sp +++ b/addons/sourcemod/scripting/vsh/sdk.sp @@ -191,6 +191,18 @@ void SDK_Init() else DHookEnableDetour(hHook, true, Hook_CouldHealTarget); + hHook = DHookCreateFromConf(hGameData, "CTFWeaponBaseMelee::DoSwingTraceInternal"); + if (hHook == null) + { + LogMessage("Failed to create hook: CTFWeaponBaseMelee::DoSwingTraceInternal!"); + } + else + { + DHookEnableDetour(hHook, false, Hook_DoSwingTraceInternalPre); + DHookEnableDetour(hHook, true, Hook_DoSwingTraceInternalPost); + } + + delete hHook; delete hGameData; @@ -402,6 +414,18 @@ public MRESReturn Hook_CouldHealTarget(int iDispenser, Handle hReturn, Handle hP return MRES_Ignored; } +MRESReturn Hook_DoSwingTraceInternalPre(int iEntity, DHookReturn ret, Handle hParams) +{ + CustomMelee_DoSwingTracePre(iEntity); + return MRES_Ignored; +} + +MRESReturn Hook_DoSwingTraceInternalPost(int iEntity, DHookReturn ret, Handle hParams) +{ + CustomMelee_DoSwingTracePost(iEntity, ret.Value); + return MRES_Ignored; +} + void SDK_FVisible(int iEntity) { if (g_hHookFVisible != null)