diff --git a/plugins/L4D2Tools.smx b/plugins/L4D2Tools.smx index be511a4..a0bf223 100644 Binary files a/plugins/L4D2Tools.smx and b/plugins/L4D2Tools.smx differ diff --git a/plugins/l4d_survivor_identity_fix.smx b/plugins/l4d_survivor_identity_fix.smx new file mode 100644 index 0000000..3d76a02 Binary files /dev/null and b/plugins/l4d_survivor_identity_fix.smx differ diff --git a/scripting/L4D2Tools.sp b/scripting/L4D2Tools.sp index 2197574..1d5cb58 100644 --- a/scripting/L4D2Tools.sp +++ b/scripting/L4D2Tools.sp @@ -11,12 +11,14 @@ #include #include "jutils.inc" -static bool bLasersUsed[2048]; +static bool bLasersUsed[2048], waitingForPlayers; static ConVar hLaserNotice, hFinaleTimer, hFFNotice, hMPGamemode; -static int iFinaleStartTime, botDropMeleeWeapon[MAXPLAYERS+1]; +static int iFinaleStartTime, botDropMeleeWeapon[MAXPLAYERS+1], extraKitsAmount; +static Handle waitTimer = INVALID_HANDLE; static float OUT_OF_BOUNDS[3] = {0.0, -1000.0, 0.0}; -static int extraKitsAmount = 0; + +native int IdentityFix_SetPlayerModel(int client, int args); //TODO: Remove the Plugin_Stop on pickup, and give item back instead. keep reference to dropped weapon to delete. public Plugin myinfo = { @@ -27,6 +29,12 @@ public Plugin myinfo = { url = "" }; +public APLRes AskPluginLoad2(Handle myself, bool late, char[] error, int err_max) +{ + MarkNativeAsOptional("IdentityFix_SetPlayerModel"); + return APLRes_Success; +} + //TODO: Implement automatic extra kits public void OnPluginStart() { EngineVersion g_Game = GetEngineVersion(); @@ -38,7 +46,7 @@ public void OnPluginStart() { hLaserNotice = CreateConVar("sm_laser_use_notice", "1.0", "Enable notification of a laser box being used", FCVAR_NONE, true, 0.0, true, 1.0); hFinaleTimer = CreateConVar("sm_time_finale", "0.0", "Record the time it takes to complete finale. 0 -> OFF, 1 -> Gauntlets Only, 2 -> All finales", FCVAR_NONE, true, 0.0, true, 2.0); hFFNotice = CreateConVar("sm_ff_notice", "0.0", "Notify players if a FF occurs. 0 -> Disabled, 1 -> In chat, 2 -> In Hint text", FCVAR_NONE, true, 0.0, true, 2.0); - hMPGamemode = FindConVar("mp_gamemode"); + hMPGamemode = FindConVar("mp_gamemode"); HookEvent("player_use", Event_PlayerUse); HookEvent("player_hurt", Event_PlayerHurt); @@ -50,38 +58,91 @@ public void OnPluginStart() { HookEvent("player_bot_replace", Event_BotPlayerSwap); HookEvent("bot_player_replace", Event_BotPlayerSwap); HookEvent("map_transition", Event_MapTransition); + HookEvent("player_spawn", Event_PlayerSpawn); AutoExecConfig(true, "l4d2_tools"); for(int client = 1; client < MaxClients; client++) { - if(IsClientConnected(client) && IsClientInGame(client) && GetClientTeam(client) == 2) { - if(IsFakeClient(client)) - SDKHook(client, SDKHook_WeaponDrop, Event_OnWeaponDrop); + if(IsClientConnected(client) && IsClientInGame(client) && GetClientTeam(client) == 2 && IsFakeClient(client)) { + SDKHook(client, SDKHook_WeaponDrop, Event_OnWeaponDrop); } } RegAdminCmd("sm_model", Command_SetClientModel, ADMFLAG_ROOT); } //TODO: Give kits on fresh start as well, need to set extraKitsAmount -public void OnMapStart() { - if(L4D_IsFirstMapInScenario()) { - extraKitsAmount = GetClientCount(true) - 4; - if(extraKitsAmount < 0) extraKitsAmount = 0; - PrintToServer("New map has started"); - } +public Action Event_PlayerSpawn(Event event, const char[] name, bool dontBroadcast) { + int client = GetClientOfUserId(event.GetInt("userid")); if(extraKitsAmount > 0) { - for(int i = 1; i < MaxClients + 1; i++) { - if(IsClientConnected(i) && IsClientInGame(i) && IsPlayerAlive(i) && GetClientTeam(i) == 2) { - PrintToServer("Found a client to spawn %d extra kits: %N", extraKitsAmount, i); - while(extraKitsAmount > 0) { - CheatCommand(i, "give", "first_aid_kit", ""); - extraKitsAmount--; - } - break; + char wpn[32]; + if(GetClientWeaponName(client, 3, wpn, sizeof(wpn))) { + if(!StrEqual(wpn, "weapon_first_aid_kit")) { + CheatCommand(client, "give", "first_aid_kit", ""); + extraKitsAmount--; } } } } +public void OnMapStart() { + if(L4D_IsFirstMapInScenario()) { + extraKitsAmount = GetSurvivorCount() - 4; + if(extraKitsAmount < 0) extraKitsAmount = 0; + waitingForPlayers = true; + PrintToServer("New map has started"); + } + if(extraKitsAmount > 0 && !waitingForPlayers) { + int lastClient; + for(int i = 1; i < MaxClients + 1; i++) { + if(IsClientConnected(i) && IsClientInGame(i) && IsPlayerAlive(i) && GetClientTeam(i) == 2) { + PrintToServer("Found a client to spawn %d extra kits: %N", extraKitsAmount, i); + char wpn[32]; + if(GetClientWeaponName(i, 3, wpn, sizeof(wpn))) { + if(!StrEqual(wpn, "weapon_first_aid_kit")) { + lastClient = GetClientOfUserId(i); + CreateTimer(5.0, Timer_SpawnKits, lastClient); + extraKitsAmount--; + } + } + } + + } + if(extraKitsAmount > 0) { + CreateTimer(0.1, Timer_SpawnKits, lastClient); + } + } + int survivorCount = GetSurvivorCount(); + if(survivorCount > 4) + CreateTimer(60.0, Timer_AddExtraCounts, survivorCount); +} +public Action Timer_AddExtraCounts(Handle hd, int players) { + float percentage = 0.042 * players; + PrintToServer("Populating extra items based on player count (%d)", players); + char classname[32]; + for(int i = MaxClients + 1; i < 2048; i++) { + if(IsValidEntity(i)) { + GetEntityClassname(i, classname, sizeof(classname)); + if(StrContains(classname, "_spawn", true) > -1 && !StrEqual(classname, "info_zombie_spawn", true)) { + int count = GetEntProp(i, Prop_Data, "m_itemCount"); + if(GetRandomFloat() < percentage) { + PrintToServer("Debug: Incrementing spawn count for %s from %d", classname, count); + SetEntProp(i, Prop_Data, "m_itemCount", ++count); + } + PrintToServer("%s %d", classname, count); + } + } + } +} +public Action Timer_SpawnKits(Handle timer, int user) { + //After kits given, re-set number to same incase a round restarts. + int prevAmount = extraKitsAmount; + int client = GetClientOfUserId(user); + while(extraKitsAmount > 0) { + CheatCommand(client, "give", "first_aid_kit", ""); + extraKitsAmount--; + } + extraKitsAmount = prevAmount; + return Plugin_Handled; +} public Action Command_SetClientModel(int client, int args) { if(args < 1) { @@ -116,6 +177,7 @@ public Action Command_SetClientModel(int client, int args) { ReplyToTargetError(client, target_count); return Plugin_Handled; } + bool identityFixAvailable = GetFeatureStatus(FeatureType_Native, "IdentityFix_SetPlayerModel") == FeatureStatus_Available; for (int i = 0; i < target_count; i++) { if(IsClientConnected(target_list[i]) && IsClientInGame(target_list[i]) && IsPlayerAlive(target_list[i]) && GetClientTeam(target_list[i]) == 2) { SetEntProp(target_list[i], Prop_Send, "m_survivorCharacter", modelID); @@ -125,8 +187,10 @@ public Action Command_SetClientModel(int client, int args) { GetSurvivorName(target_list[i], name, sizeof(name)); SetClientInfo(target_list[i], "name", name); } + if(identityFixAvailable) + IdentityFix_SetPlayerModel(target_list[i], modelID); - int primaryWeapon = GetPlayerWeaponSlot(client, 0); + int primaryWeapon = GetPlayerWeaponSlot(target_list[i], 0); if(primaryWeapon > -1) { SDKHooks_DropWeapon(target_list[i], primaryWeapon, NULL_VECTOR, NULL_VECTOR); @@ -162,12 +226,27 @@ public Action Event_BotPlayerSwap(Event event, const char[] name, bool dontBroad EquipPlayerWeapon(client, botDropMeleeWeapon[bot]); botDropMeleeWeapon[bot] = -1; }else{ - PrintToConsole(client, "Could not give back your melee weapon, %N has it instead.", meleeOwnerEnt); + PrintToChat(client, "Could not give back your melee weapon, %N has it instead.", meleeOwnerEnt); } } SDKUnhook(bot, SDKHook_WeaponDrop, Event_OnWeaponDrop); } } +public bool OnClientConnect(int client) { + if(waitingForPlayers) { + if(waitTimer != INVALID_HANDLE) { + CloseHandle(waitTimer); + } + waitTimer = CreateTimer(2.0, Timer_Wait, client); + } + return true; +} +public Action Timer_Wait(Handle hdl, int client) { + waitingForPlayers = false; + extraKitsAmount = GetSurvivorCount(); + CreateTimer(5.0, Timer_SpawnKits, GetClientOfUserId(client)); + PrintToServer("Debug: No more players joining in 2.0s, spawning kits."); +} //TODO: Might have to actually check for the bot they control, or possibly the bot will call this itself. public void OnClientDisconnect(int client) { if(botDropMeleeWeapon[client] > -1) { @@ -198,7 +277,8 @@ public void Frame_HideEntity(int entity) { public void Event_EnterSaferoom(Event event, const char[] name, bool dontBroadcast) { int user = GetClientOfUserId(event.GetInt("userid")); if(user == 0) return; - if(botDropMeleeWeapon[user] > -1) { + if(botDropMeleeWeapon[user] > 0) { + PrintToServer("Giving melee weapon back to %N", user); float pos[3]; GetClientAbsOrigin(user, pos); TeleportEntity(botDropMeleeWeapon[user], pos, NULL_VECTOR, NULL_VECTOR); @@ -295,7 +375,7 @@ public void Event_CarAlarmTriggered(Event event, const char[] name, bool dontBro PrintToChatAll("%N activated a car alarm!", userID); } public Action Event_MapTransition(Event event, const char[] name, bool dontBroadcast) { - extraKitsAmount = GetClientCount(true) - 4; + extraKitsAmount = GetSurvivorCount() - 4; if(extraKitsAmount < 0) extraKitsAmount = 0; PrintToServer("Will spawn an extra %d kits", extraKitsAmount); } @@ -328,4 +408,14 @@ stock int GetAnyValidClient() { } } return -1; +} + +stock int GetSurvivorCount() { + int count = 0; + for(int i = 1; i < MaxClients + 1; i++) { + if(IsClientConnected(i) && IsClientInGame(i) && GetClientTeam(i) == 2) { + count++; + } + } + return count; } \ No newline at end of file diff --git a/scripting/l4d_survivor_identity_fix.sp b/scripting/l4d_survivor_identity_fix.sp new file mode 100644 index 0000000..c38be2d --- /dev/null +++ b/scripting/l4d_survivor_identity_fix.sp @@ -0,0 +1,233 @@ +#pragma semicolon 1 +#pragma newdecls required + +#define PLUGIN_NAME "[L4D1/2] Survivor Identity Fix for 5+ Survivors" +#define PLUGIN_AUTHOR "Merudo, Shadowysn" +#define PLUGIN_DESC "Fix bug where a survivor will change identity when a player connects/disconnects if there are 5+ survivors" +#define PLUGIN_VERSION "1.6" +#define PLUGIN_URL "https://forums.alliedmods.net/showthread.php?p=2403731#post2403731" +#define PLUGIN_NAME_SHORT "5+ Survivor Identity Fix" +#define PLUGIN_NAME_TECH "survivor_identity_fix" + +#include +#include +#include + +#define TEAM_SURVIVOR 2 +#define TEAM_PASSING 4 + +char g_Models[MAXPLAYERS+1][128]; + +#define GAMEDATA "l4d_survivor_identity_fix" + +Handle hConf = null; +#define NAME_SetModel "CBasePlayer::SetModel" +static Handle hDHookSetModel = null; + +#define SIG_SetModel_LINUX "@_ZN11CBasePlayer8SetModelEPKc" +#define SIG_SetModel_WINDOWS "\\x55\\x8B\\x2A\\x8B\\x2A\\x2A\\x56\\x57\\x50\\x8B\\x2A\\xE8\\x2A\\x2A\\x2A\\x2A\\x8B\\x2A\\x2A\\x2A\\x2A\\x2A\\x8B\\x2A\\x8B\\x2A\\x2A\\x8B" + +#define SIG_L4D1SetModel_WINDOWS "\\x8B\\x2A\\x2A\\x2A\\x56\\x57\\x50\\x8B\\x2A\\xE8\\x2A\\x2A\\x2A\\x2A\\x8B\\x3D" + +public Plugin myinfo = +{ + name = PLUGIN_NAME, + author = PLUGIN_AUTHOR, + description = PLUGIN_DESC, + version = PLUGIN_VERSION, + url = PLUGIN_URL +} + +public APLRes AskPluginLoad2(Handle myself, bool late, char[] error, int err_max) { + CreateNative("IdentityFix_SetPlayerModel", Native_SetPlayerModel); + return APLRes_Success; +} + +public void OnPluginStart() +{ + GetGamedata(); + + CreateConVar("l4d_survivor_identity_fix_version", PLUGIN_VERSION, "Survivor Change Fix Version", FCVAR_SPONLY|FCVAR_NOTIFY|FCVAR_DONTRECORD); + + HookEvent("player_bot_replace", Event_PlayerToBot, EventHookMode_Post); + HookEvent("bot_player_replace", Event_BotToPlayer, EventHookMode_Post); +} + +// ------------------------------------------------------------------------ +// Stores the client of each survivor each time it is changed +// Needed because when Event_PlayerToBot fires, it's hunter model instead +// ------------------------------------------------------------------------ +public MRESReturn SetModel_Pre(int client, Handle hParams) +{ } // We need this pre hook even though it's empty, or else the post hook will crash the game. + +public MRESReturn SetModel(int client, Handle hParams) +{ + if (!IsValidClient(client)) return; + if (!IsSurvivor(client)) + { + g_Models[client][0] = '\0'; + return; + } + + char model[128]; + DHookGetParamString(hParams, 1, model, sizeof(model)); + if (StrContains(model, "models/infected", false) < 0) + { + strcopy(g_Models[client], 128, model); + } +} + +// ------------------------------------------------------------------------ +// Models & survivor names so bots can be renamed +// ------------------------------------------------------------------------ +char survivor_names[8][] = { "Nick", "Rochelle", "Coach", "Ellis", "Bill", "Zoey", "Francis", "Louis"}; +char survivor_models[8][] = +{ + "models/survivors/survivor_gambler.mdl", + "models/survivors/survivor_producer.mdl", + "models/survivors/survivor_coach.mdl", + "models/survivors/survivor_mechanic.mdl", + "models/survivors/survivor_namvet.mdl", + "models/survivors/survivor_teenangst.mdl", + "models/survivors/survivor_biker.mdl", + "models/survivors/survivor_manager.mdl" +}; + +// -------------------------------------- +// Bot replaced by player +// -------------------------------------- +public Action Event_BotToPlayer(Handle event, const char[] name, bool dontBroadcast) +{ + int player = GetClientOfUserId(GetEventInt(event, "player")); + int bot = GetClientOfUserId(GetEventInt(event, "bot")); + + if (!IsValidClient(player) || !IsSurvivor(player) || IsFakeClient(player)) return; // ignore fake players (side product of creating bots) + + char model[128]; + GetClientModel(bot, model, sizeof(model)); + SetEntityModel(player, model); + SetEntProp(player, Prop_Send, "m_survivorCharacter", GetEntProp(bot, Prop_Send, "m_survivorCharacter")); +} + +// -------------------------------------- +// Player -> Bot +// -------------------------------------- +public Action Event_PlayerToBot(Handle event, char[] name, bool dontBroadcast) +{ + int player = GetClientOfUserId(GetEventInt(event, "player")); + int bot = GetClientOfUserId(GetEventInt(event, "bot")); + + if (!IsValidClient(player) || !IsSurvivor(player) || IsFakeClient(player)) return; // ignore fake players (side product of creating bots) + + if (g_Models[player][0] != '\0') + { + SetEntProp(bot, Prop_Send, "m_survivorCharacter", GetEntProp(player, Prop_Send, "m_survivorCharacter")); + SetEntityModel(bot, g_Models[player]); // Restore saved model. Player model is hunter at this point + for (int i = 0; i < 8; i++) + { + if (StrEqual(g_Models[player], survivor_models[i])) SetClientInfo(bot, "name", survivor_names[i]); + } + } +} + +void GetGamedata() +{ + char filePath[PLATFORM_MAX_PATH]; + BuildPath(Path_SM, filePath, sizeof(filePath), "gamedata/%s.txt", GAMEDATA); + if( FileExists(filePath) ) + { + hConf = LoadGameConfigFile(GAMEDATA); // For some reason this doesn't return null even for invalid files, so check they exist first. + } + else + { + PrintToServer("[SM] %s plugin unable to get %i.txt gamedata file. Generating...", PLUGIN_NAME_SHORT, GAMEDATA); + + Handle fileHandle = OpenFile(filePath, "a+"); + if (fileHandle == null) + { SetFailState("[SM] Couldn't generate gamedata file!"); } + + WriteFileLine(fileHandle, "\"Games\""); + WriteFileLine(fileHandle, "{"); + WriteFileLine(fileHandle, " \"left4dead\""); + WriteFileLine(fileHandle, " {"); + WriteFileLine(fileHandle, " \"Signatures\""); + WriteFileLine(fileHandle, " {"); + WriteFileLine(fileHandle, " \"%s\"", NAME_SetModel); + WriteFileLine(fileHandle, " {"); + WriteFileLine(fileHandle, " \"library\" \"server\""); + WriteFileLine(fileHandle, " \"linux\" \"%s\"", SIG_SetModel_LINUX); + WriteFileLine(fileHandle, " \"windows\" \"%s\"", SIG_L4D1SetModel_WINDOWS); + WriteFileLine(fileHandle, " \"mac\" \"%s\"", SIG_SetModel_LINUX); + WriteFileLine(fileHandle, " }"); + WriteFileLine(fileHandle, " }"); + WriteFileLine(fileHandle, " }"); + WriteFileLine(fileHandle, " \"left4dead2\""); + WriteFileLine(fileHandle, " {"); + WriteFileLine(fileHandle, " \"Signatures\""); + WriteFileLine(fileHandle, " {"); + WriteFileLine(fileHandle, " \"%s\"", NAME_SetModel); + WriteFileLine(fileHandle, " {"); + WriteFileLine(fileHandle, " \"library\" \"server\""); + WriteFileLine(fileHandle, " \"linux\" \"%s\"", SIG_SetModel_LINUX); + WriteFileLine(fileHandle, " \"windows\" \"%s\"", SIG_SetModel_WINDOWS); + WriteFileLine(fileHandle, " \"mac\" \"%s\"", SIG_SetModel_LINUX); + WriteFileLine(fileHandle, " }"); + WriteFileLine(fileHandle, " }"); + WriteFileLine(fileHandle, " }"); + WriteFileLine(fileHandle, "}"); + + CloseHandle(fileHandle); + hConf = LoadGameConfigFile(GAMEDATA); + if (hConf == null) + { SetFailState("[SM] Failed to load auto-generated gamedata file!"); } + + PrintToServer("[SM] %s successfully generated %s.txt gamedata file!", PLUGIN_NAME_SHORT, GAMEDATA); + } + PrepDHooks(); +} + +void PrepDHooks() +{ + if (hConf == null) + { + SetFailState("Error: Gamedata not found"); + } + + hDHookSetModel = DHookCreateDetour(Address_Null, CallConv_THISCALL, ReturnType_Void, ThisPointer_CBaseEntity); + DHookSetFromConf(hDHookSetModel, hConf, SDKConf_Signature, NAME_SetModel); + DHookAddParam(hDHookSetModel, HookParamType_CharPtr); + DHookEnableDetour(hDHookSetModel, false, SetModel_Pre); + DHookEnableDetour(hDHookSetModel, true, SetModel); +} + +bool IsValidClient(int client, bool replaycheck = true) +{ + if (client <= 0 || client > MaxClients) return false; + if (!IsClientInGame(client)) return false; + if (replaycheck) + { + if (IsClientSourceTV(client) || IsClientReplay(client)) return false; + } + return true; +} + +bool IsSurvivor(int client) +{ + if (GetClientTeam(client) != TEAM_SURVIVOR && GetClientTeam(client) != TEAM_PASSING) return false; + return true; +} + +public int Native_SetPlayerModel(Handle plugin, int numParams) { + int client = GetNativeCell(1); + int character = GetNativeCell(2); + if(numParams != 2) { + ThrowNativeError(SP_ERROR_NATIVE, "Incorrect amount of parameters passed"); + }else if(client < 1 || client > MaxClients || !IsClientInGame(client)) { + ThrowNativeError(SP_ERROR_INDEX, "Client index %d is not valid or is not in game", client); + } else if(character < 0 || character > 7) { + ThrowNativeError(SP_ERROR_INDEX, "Character ID (%d) is not in range (0-7)", character); + } else { + strcopy(g_Models[client], 64, survivor_models[character]); + } + return 0; +} \ No newline at end of file