├── .gitattributes ├── .gitignore ├── LICENSE ├── LinGe_Library ├── README.md ├── plugins │ └── LinGe_Library.smx └── scripting │ ├── LinGe_Library.sp │ └── include │ ├── LinGe_Function.inc │ └── LinGe_Library.inc ├── README.md ├── hostname ├── data │ └── hostname.txt ├── plugins │ └── hostname.smx └── scripting │ └── hostname.sp ├── l4d2_multislots ├── README.md ├── l4d2_multislots.cfg ├── plugins │ └── l4d2_multislots.smx ├── scripting │ └── l4d2_multislots.sp └── 物品代码.txt ├── l4d2_votes ├── README.md ├── plugins │ └── l4d2_votes.smx └── scripting │ └── l4d2_votes.sp ├── l4d2_wpgive ├── plugins │ └── l4d2_wpgive.smx └── scripting │ └── l4d2_wpgive.sp ├── l4d_mapvote ├── README.md ├── data │ └── l4d_mapvote.cfg ├── plugins │ └── l4d_mapvote.smx └── scripting │ └── l4d_mapvote.sp ├── l4d_server_manager ├── l4d_server_manager.smx └── l4d_server_manager.sp ├── l4d_template.sp └── l4d_votemode ├── README.md ├── data ├── l4d_votemode.cfg └── l4d_votemode_all.cfg ├── plugins └── l4d_votemode.smx ├── scripting └── l4d_votemode.sp └── translations ├── chi └── votemode.phrases.txt └── votemode.phrases.txt /.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files and perform LF normalization 2 | * text=auto 3 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .vscode -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Lin515 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /LinGe_Library/README.md: -------------------------------------------------------------------------------- 1 | 一个简单的自用库,缓慢更新中…… -------------------------------------------------------------------------------- /LinGe_Library/plugins/LinGe_Library.smx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Lin515/L4D_LinGe_Plugins/3abfac98dd1922e9853441904d80509c01f9d064/LinGe_Library/plugins/LinGe_Library.smx -------------------------------------------------------------------------------- /LinGe_Library/scripting/LinGe_Library.sp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | 5 | public Plugin myinfo = { 6 | name = "[L4D] LinGe Library", 7 | author = "LinGe", 8 | description = "求生之路 一个简单的自用库", 9 | version = "0.1", 10 | url = "https://github.com/Lin515/L4D_LinGe_Plugins" 11 | }; 12 | 13 | public APLRes AskPluginLoad2(Handle myself, bool late, char[] error, int err_max) 14 | { 15 | EngineVersion game = GetEngineVersion(); 16 | if (game!=Engine_Left4Dead && game != Engine_Left4Dead2) 17 | { 18 | strcopy(error, err_max, "本插件只支持 Left 4 Dead 1&2 ."); 19 | return APLRes_SilentFailure; 20 | } 21 | 22 | CreateNative("GetBaseMode", Native_GetBaseMode); 23 | CreateNative("IsOnVersus", Native_IsOnVersus); 24 | RegPluginLibrary("LinGe_Library"); 25 | 26 | return APLRes_Success; 27 | } 28 | 29 | BaseMode g_iCurrentMode = INVALID; 30 | public any Native_GetBaseMode(Handle plugin, int numParams) 31 | { 32 | g_iCurrentMode = INVALID; 33 | int entity = CreateEntityByName("info_gamemode"); 34 | if (IsValidEntity(entity)) 35 | { 36 | DispatchSpawn(entity); 37 | HookSingleEntityOutput(entity, "OnCoop", OnGamemode, true); 38 | HookSingleEntityOutput(entity, "OnSurvival", OnGamemode, true); 39 | HookSingleEntityOutput(entity, "OnVersus", OnGamemode, true); 40 | HookSingleEntityOutput(entity, "OnScavenge", OnGamemode, true); 41 | ActivateEntity(entity); 42 | AcceptEntityInput(entity, "PostSpawnActivate"); 43 | if (IsValidEntity(entity)) 44 | RemoveEdict(entity); 45 | } 46 | return g_iCurrentMode; 47 | } 48 | public void OnGamemode(const char[] output, int caller, int activator, float delay) 49 | { 50 | if (strcmp(output, "OnCoop") == 0) 51 | g_iCurrentMode = OnCoop; 52 | else if (strcmp(output, "OnSurvival") == 0) 53 | g_iCurrentMode = OnSurvival; 54 | else if (strcmp(output, "OnVersus") == 0) 55 | g_iCurrentMode = OnVersus; 56 | else if (strcmp(output, "OnScavenge") == 0) 57 | g_iCurrentMode = OnScavenge; 58 | else 59 | g_iCurrentMode = INVALID; 60 | } 61 | 62 | // 是否是基于对抗的模式(生还者对抗以及清道夫模式也视为对抗) 63 | public any Native_IsOnVersus(Handle plugin, int numParams) 64 | { 65 | char gamemode[30]; 66 | GetConVarString(FindConVar("mp_gamemode"), gamemode, sizeof(gamemode)); 67 | if (strcmp(gamemode, "mutation15") == 0) // 生还者对抗 68 | return true; 69 | 70 | BaseMode baseMode = Native_GetBaseMode(null, 0); 71 | if (OnVersus == baseMode) 72 | return true; 73 | if (OnScavenge == baseMode) 74 | return true; 75 | 76 | return false; 77 | } -------------------------------------------------------------------------------- /LinGe_Library/scripting/include/LinGe_Function.inc: -------------------------------------------------------------------------------- 1 | #if defined _LinGe_Function_included_ 2 | #endinput 3 | #endif 4 | #define _LinGe_Function_included_ 5 | #include 6 | 7 | // 一些常用函数 8 | 9 | // L4D2_RunScript 来源 Silver https://forums.alliedmods.net/showthread.php?p=2657025 10 | // 用logic_script实体来执行一段vscript脚本代码 11 | // 控制台指令script具有相同的功能 不过script是cheats指令 12 | // 并且据Silvers所说script指令似乎存在内存泄漏 所以通过实体来执行代码更优一点 13 | stock void L4D2_RunScript(const char[] sCode) 14 | { 15 | // 获取 logic_script 实体 16 | static int iScriptLogic = INVALID_ENT_REFERENCE; 17 | if ( iScriptLogic == INVALID_ENT_REFERENCE || !IsValidEntity(iScriptLogic) ) 18 | { 19 | iScriptLogic = FindEntityByClassname(-1, "logic_script"); 20 | // 如果查找不到则创建 21 | if ( iScriptLogic == INVALID_ENT_REFERENCE || !IsValidEntity(iScriptLogic) ) 22 | { 23 | iScriptLogic = EntIndexToEntRef(CreateEntityByName("logic_script")); 24 | if ( iScriptLogic == INVALID_ENT_REFERENCE || !IsValidEntity(iScriptLogic) ) 25 | { 26 | LogError("Could not create 'logic_script'"); 27 | return; 28 | } 29 | DispatchSpawn(iScriptLogic); 30 | } 31 | } 32 | 33 | SetVariantString(sCode); 34 | AcceptEntityInput(iScriptLogic, "RunScriptCode"); 35 | } 36 | stock void _L4D2_RunScript(char[] sCode, any ...) 37 | { 38 | static char sBuffer[8192]; 39 | VFormat(sBuffer, sizeof(sBuffer), sCode, 2); 40 | L4D2_RunScript(sBuffer); 41 | } 42 | 43 | // 将运行结果存入指定ConVar 44 | // 这种利用控制台变量获取返回值的方法参考自left4dhooks 45 | stock void Ret_L4D2_RunScript(const char[] code, const char[] convar) 46 | { 47 | _L4D2_RunScript("Convars.SetValue(\"%s\", %s)", convar, code); 48 | } 49 | 50 | // 传送 51 | stock bool Teleport(int client, int target) 52 | { 53 | float vOrigin[3] = 0.0; 54 | float vAngles[3] = 0.0; 55 | GetClientAbsOrigin(target, vOrigin); 56 | GetClientAbsAngles(target, vAngles); 57 | TeleportEntity(client, vOrigin, vAngles, NULL_VECTOR); 58 | } 59 | 60 | // 在char[][] arr中查找指定字符串 61 | stock int IndexString(const char[] str, const char[][] arr, int size, int srtidx=0, bool reverse=false, bool caseSensitive=true, bool equal=true) 62 | { 63 | if (srtidx >= size || srtidx < 0) 64 | { 65 | LogError("索引溢出 srtidx=%d size=%d", srtidx, size); 66 | return -2; 67 | } 68 | if (reverse) 69 | { 70 | for (int i=srtidx; i>=0; i--) 71 | { 72 | if (equal) 73 | { 74 | if (strcmp(str, arr[i], caseSensitive) == 0) 75 | return i; 76 | } 77 | else 78 | { 79 | if (StrContains(str, arr[i], caseSensitive) != -1) 80 | return i; 81 | } 82 | } 83 | } 84 | else 85 | { 86 | for (int i=srtidx; i= size || srtidx < 0) 106 | { 107 | LogError("索引溢出 srtidx=%d size=%d", srtidx, size); 108 | return -2; 109 | } 110 | if (reverse) 111 | { 112 | for (int i=srtidx; i>=0; i--) 113 | { 114 | if (data == arr[i]) 115 | return i; 116 | } 117 | } 118 | else 119 | { 120 | for (int i=srtidx; i MaxClients) 256 | return false; 257 | if (!IsClientConnected(client)) 258 | return false; 259 | if (!noNeedInGame && !IsClientInGame(client)) 260 | return false; 261 | return true; 262 | } 263 | 264 | // 所有玩家是否已载入到游戏 主要用于每回合开始阶段检测 265 | stock bool IsAllHumanInGame() 266 | { 267 | for (int i=1; i<=MaxClients; i++) 268 | { 269 | // 若存在已连接且不是BOT,但尚未在游戏中的玩家 270 | if (IsClientConnected(i) && !IsClientInGame(i)) 271 | { 272 | if (!IsFakeClient(i)) 273 | return false; 274 | } 275 | } 276 | return true; 277 | } 278 | 279 | // 从BOT生还者中获取其有效真实玩家的client 280 | // 在执行该函数前你应先确认该client是有效生还者BOT 281 | // 若返回0,则说明该BOT没有真实玩家就位 282 | // 若返回>0,则说明有真实玩家就位,并且返回值为其真实玩家client 283 | stock int GetHumanClient(int bot) 284 | { 285 | if (IsAlive(bot) && HasEntProp(bot, Prop_Send, "m_humanSpectatorUserID")) 286 | { 287 | int human = GetClientOfUserId(GetEntProp(bot, Prop_Send, "m_humanSpectatorUserID")); 288 | if (human > 0) 289 | { 290 | if (IsClientInGame(human)) 291 | { 292 | if (!IsFakeClient(human) && GetClientTeam(human)==1) 293 | return human; 294 | } 295 | } 296 | } 297 | return 0; 298 | } 299 | 300 | // 玩家是否处于闲置(旁观不算) 301 | stock bool IsClientIdle(int client) 302 | { 303 | if (GetClientTeam(client) != 1) 304 | return false; 305 | 306 | for(int i=1; i<=MaxClients; i++) 307 | { 308 | if (IsClientInGame(i)) 309 | { 310 | if (GetClientTeam(i) == 2 && IsFakeClient(i)) 311 | { 312 | if (GetHumanClient(i) == client) 313 | return true; 314 | } 315 | } 316 | } 317 | return false; 318 | } 319 | 320 | // 当前在线的全部Client 321 | stock int GetClients(bool noNeedInGame=false) 322 | { 323 | int num = 0; 324 | for (int i=1; i<=MaxClients; i++) 325 | { 326 | if (IsClientConnected(i)) 327 | { 328 | if (IsClientInGame(i) || noNeedInGame) 329 | { 330 | num++; 331 | } 332 | } 333 | } 334 | return num; 335 | } 336 | 337 | // 当前在线的全部真实玩家数 338 | stock int GetHumans(bool noNeedInGame=false) 339 | { 340 | int num = 0; 341 | for (int i=1; i<=MaxClients; i++) 342 | { 343 | if (IsClientConnected(i) && !IsFakeClient(i)) 344 | { 345 | if (IsClientInGame(i) || noNeedInGame) 346 | { 347 | num++; 348 | } 349 | } 350 | } 351 | return num; 352 | } 353 | 354 | #define TEAM_UNKNOWN 0 355 | #define TEAM_SPECTATOR 1 356 | #define TEAM_SURVIVOR 2 357 | #define TEAM_INFECTED 3 358 | /** 359 | * 获得指定类型的玩家数量 360 | * 361 | * @param team 玩家队伍,若为TEAM_UNKNOWN则不检查队伍 362 | * @param bot 是否可以是BOT 363 | * @param alive 是否必须是存活状态 364 | * @return 玩家数量 365 | */ 366 | stock int GetPlayers(int team=TEAM_UNKNOWN, bool bot=true, bool alive=false) 367 | { 368 | int num = 0; 369 | for (int i=1; i<=MaxClients; i++) 370 | { 371 | if (IsClientInGame(i)) 372 | { 373 | if (TEAM_UNKNOWN != team && GetClientTeam(i) != team) 374 | continue; 375 | if (!bot && IsFakeClient(i)) 376 | continue; 377 | if (alive && !IsAlive(i)) 378 | continue; 379 | num++; 380 | } 381 | } 382 | return num; 383 | } 384 | 385 | // BOT生还数(不含闲置) 386 | stock int GetBotSurvivors() 387 | { 388 | int num = 0; 389 | for (int i=1; i<=MaxClients; i++) 390 | { 391 | if (IsClientInGame(i)) 392 | { 393 | if (GetClientTeam(i) == 2 && IsFakeClient(i)) 394 | { 395 | if (GetHumanClient(i) == 0) 396 | num++; 397 | } 398 | } 399 | } 400 | return num; 401 | } 402 | 403 | // 存活BOT数(不含闲置) 404 | stock int GetAliveBotSurvivors() 405 | { 406 | int num = 0; 407 | for (int i=1; i<=MaxClients; i++) 408 | { 409 | if (IsClientInGame(i)) 410 | { 411 | if (GetClientTeam(i) == 2 && IsFakeClient(i)) 412 | { 413 | if (IsAlive(i) && GetHumanClient(i) == 0) 414 | num++; 415 | } 416 | } 417 | } 418 | return num; 419 | } 420 | 421 | // 闲置数 422 | stock int GetIdleSurvivors() 423 | { 424 | int num = 0; 425 | for (int i=1; i<=MaxClients; i++) 426 | { 427 | if (IsClientInGame(i)) 428 | { 429 | if (GetClientTeam(i) == 2 && IsFakeClient(i)) 430 | { 431 | if (GetHumanClient(i) > 0) 432 | num++; 433 | } 434 | } 435 | } 436 | return num; 437 | } 438 | 439 | // 旁观数 440 | // idle=true 返回真实旁观数+闲置 441 | // idle=false 返回真实旁观数 442 | stock int GetSpectators(bool idle=false) 443 | { 444 | int num = 0; 445 | for (int i=1; i<=MaxClients; i++) 446 | { 447 | if (IsClientInGame(i)) 448 | { 449 | if (GetClientTeam(i) == 1 && !IsFakeClient(i)) // 不知道有没有出现BOT在旁观的可能,总之加了这个判断不会错 450 | num++; 451 | } 452 | } 453 | if (idle) 454 | return num; 455 | else 456 | return num-GetIdleSurvivors(); 457 | } -------------------------------------------------------------------------------- /LinGe_Library/scripting/include/LinGe_Library.inc: -------------------------------------------------------------------------------- 1 | #if defined _LinGe_Library_included_ 2 | #endinput 3 | #endif 4 | #define _LinGe_Library_included_ 5 | #include 6 | 7 | public SharedPlugin __pl_LinGe_Library = 8 | { 9 | name = "LinGe_Library", 10 | file = "LinGe_Library.smx", 11 | #if defined REQUIRE_PLUGIN 12 | required = 1, 13 | #else 14 | required = 0, 15 | #endif 16 | } 17 | 18 | // 游戏基础模式 19 | enum BaseMode { 20 | INVALID = 0, 21 | OnCoop, 22 | OnVersus, 23 | OnSurvival, 24 | OnScavenge 25 | }; 26 | native BaseMode GetBaseMode(); // 获取当前游戏的基础模式 27 | native bool IsOnVersus(); // 当前游戏模式是否是基于对抗类模式(生还者对抗与清道夫模式也视为对抗类模式) 28 | 29 | #if !defined REQUIRE_PLUGIN 30 | public __pl_LinGe_Library_SetNTVOptional() 31 | { 32 | MarkNativeAsOptional("GetBaseMode"); 33 | MarkNativeAsOptional("IsOnVersus"); 34 | } 35 | #endif -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 自己写的一些插件 2 | 3 | - **LinGe_Library** 4 | 一个简单的自用库,我的插件都需要用到这个库,请安装它。 5 | - **l4d2_multislots** 6 | 适用于战役的多人控制插件,也可用于药役,但不保证所有类型的药役都能正常用,有需求请自己修改它。依赖[left4dhooks&dhooks](https://forums.alliedmods.net/showthread.php?t=321696)。 7 | - **l4d2_votes** 8 | 多功能投票插件,包含多倍弹药、自动红外、友伤设置、服务器人数设置、特感击杀回血等功能。依赖[builtinvotes](https://github.com/A1mDev/builtinvotes)扩展。 9 | - **l4d_mapvote** 10 | 投票换图插件,在配置文件可添加三方图。默认使用游戏内置投票。依赖[builtinvotes](https://github.com/A1mDev/builtinvotes)扩展。 11 | - **l4d_server_manager** 12 | 一个简单的服务器管理插件,让服务器动态管理大厅和自动休眠。依赖[left4dhooks&dhooks](https://forums.alliedmods.net/showthread.php?t=321696)。 13 | - **l4d_votemode** 14 | 基于Silver的l4d_votemode修改,依赖[builtinvotes](https://github.com/A1mDev/builtinvotes)扩展来发起游戏内置投票。 15 | - **hostname** 16 | 控制服务器名。 17 | - **l4d2_wpgive** 18 | 武器获取菜单。 19 | -------------------------------------------------------------------------------- /hostname/data/hostname.txt: -------------------------------------------------------------------------------- 1 | "hostname" 2 | { 3 | "27015" 4 | { 5 | "hostname" "服务器 1" 6 | } 7 | "27016" 8 | { 9 | "hostname" "服务器 2" 10 | } 11 | "这里是服务器端口号" 12 | { 13 | "hostname" "这里是服务器名" 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /hostname/plugins/hostname.smx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Lin515/L4D_LinGe_Plugins/3abfac98dd1922e9853441904d80509c01f9d064/hostname/plugins/hostname.smx -------------------------------------------------------------------------------- /hostname/scripting/hostname.sp: -------------------------------------------------------------------------------- 1 | #pragma semicolon 1 2 | #pragma newdecls required 3 | #include 4 | 5 | public Plugin myinfo = { 6 | name = "hostname", 7 | author = "LinGe", 8 | description = "服务器名", 9 | version = "0.1", 10 | url = "https://github.com/Lin515/L4D_LinGe_Plugins" 11 | }; 12 | 13 | public APLRes AskPluginLoad2(Handle myself, bool late, char[] error, int err_max) 14 | { 15 | EngineVersion game = GetEngineVersion(); 16 | if (game!=Engine_Left4Dead && game!=Engine_Left4Dead2) 17 | { 18 | strcopy(error, err_max, "本插件只支持 Left 4 Dead 1&2."); 19 | return APLRes_SilentFailure; 20 | } 21 | return APLRes_Success; 22 | } 23 | 24 | #define HOSTNAME_CONFIG "data/hostname.txt" 25 | ConVar cv_hostname; 26 | char g_hostname[200]; 27 | 28 | public void OnPluginStart() 29 | { 30 | KeyValues kv_hostname = new KeyValues("hostname"); 31 | ConVar cv_hostport = FindConVar("hostport"); 32 | cv_hostname = FindConVar("hostname"); 33 | 34 | char filePath[PLATFORM_MAX_PATH]; 35 | BuildPath(Path_SM, filePath, sizeof(filePath), HOSTNAME_CONFIG); 36 | if (FileExists(filePath)) 37 | { 38 | if (!kv_hostname.ImportFromFile(filePath)) 39 | { 40 | SetFailState("导入 %s 失败!", filePath); 41 | } 42 | } 43 | 44 | char port[20]; 45 | FormatEx(port, sizeof(port), "%d", cv_hostport.IntValue); 46 | kv_hostname.JumpToKey(port); 47 | kv_hostname.GetString("hostname", g_hostname, sizeof(g_hostname), "Left 4 Dead 2"); 48 | cv_hostname.SetString(g_hostname); 49 | } 50 | 51 | public void OnConfigsExecuted() 52 | { 53 | cv_hostname.SetString(g_hostname); 54 | } -------------------------------------------------------------------------------- /l4d2_multislots/README.md: -------------------------------------------------------------------------------- 1 | # 求生之路2 适用于战役、药役的4+玩家控制 2 | 3 | ## 简述 4 | 5 | 关于4+玩家控制的插件很多,但是都或多或少有些小问题不是让我很满意,而且它们都不能正常用在我服务器上的药役,所以自己重写了一个。 6 | 请注意,这个插件只适用于战役或药役,有些药役是基于对抗修改的也可以用,但是正常对抗模式应禁用本插件。 7 | 8 | **支持游戏:求生之路2** 9 | **建议插件平台:SourceMod 1.10.0** 10 | **依赖:[left4dhooks&dhooks](https://forums.alliedmods.net/showthread.php?t=321696) [LinGe_Library](https://github.com/LinGe515/L4D_LinGe_Plugins/tree/main/LinGe_Library)** 11 | 12 | ## 功能说明 13 | 14 | - **!jg !join !joingame** 15 | 加入生还者,若当前无存活BOT空位,则会自动增加一个BOT。 16 | - **!away !s !spec !spectate** 17 | 进入旁观。 18 | - **!afk** 19 | 快速闲置。 20 | 本插件不修复多人房间下的闲置BUG,有需求建议安装闲置修复插件:[survivor_afk_fix](https://github.com/LuxLuma/Left-4-fix/tree/master/left%204%20fix/survivors/survivor_afk_fix)。 21 | - **!tp** 22 | 传送指令,可以将自己快速传送到别的生还者位置上。主要方便掉队或卡住的玩家。 23 | 默认为所有人可用,0CD,cfg文件中可对该指令进行设置。 24 | - **!ab !addbot** 25 | 手动增加一个BOT。仅管理员可用。 26 | - **!kb** 27 | 踢出所有BOT,接管闲置玩家的BOT不会被踢出。仅管理员可用。 28 | - **!sset** 29 | 设置服务器最大人数。仅管理员可用。 30 | - **!mmn** 31 | 查看当前是否开启自动多倍物资补给。物资倍数=向上取整(玩家数/4)。 32 | - **!mmn on** 33 | 打开自动多倍物资。 34 | - **!mmn off** 35 | 关闭自动多倍物资。 36 | - **服务器控制台下执行 sm_mmn** 37 | 查看当前设置的多倍物资列表。若未设置哪些物资多倍,则默认自动多倍医疗包。 38 | - **服务器控制台下执行 sm_mmn xxx** 39 | 将xxx加入到多倍物资列表。sm_mmn clear 可清除当前物资设置 40 | - **!autogive** 41 | 查看当前是否开启自动给予物资。设置这个功能的指令开关是为了适应一些固定武器的突变模式。 42 | - **!autogive on** 43 | 打开自动给予物资。 44 | - **!autogive off** 45 | 关闭自动给予物资。 46 | - **服务器控制台下执行 sm_autogive** 47 | 查看当前设置的自动给予物资列表。若未设置自动给予哪些物资,则默认给予MP5。 48 | - **服务器控制台下执行 sm_autogive xxx** 49 | 将xxx加入到自动给予物资列表。sm_autogive clear 可清除当前物资设置。 50 | 51 | 设置自动给予的物资和自动多倍的物资只能在服务器控制台设置,这是为了防止玩家游戏中误输入。 52 | 推荐在cfg配置文件中写入相关指令。例如: 53 | 54 | ``` 55 | // 医疗包与近战自动多倍 56 | sm_mmn weapon_first_aid_kit_spawn 57 | sm_mmn weapon_melee_spawn 58 | // 自动给予马格南与M16 59 | sm_autogive pistol_magnum 60 | sm_autogive rifle 61 | ``` 62 | 63 | 注意,插件不会对你设置的物资代码做任何检查,你应该自己确认物资代码有效性和合法性。 64 | 物资代码请查看[物品代码.txt](物品代码.txt)。 -------------------------------------------------------------------------------- /l4d2_multislots/l4d2_multislots.cfg: -------------------------------------------------------------------------------- 1 | // This file was auto-generated by SourceMod (v1.10.0.6502) 2 | // ConVars for plugin "l4d2_multislots.smx" 3 | 4 | // 玩家连接完毕后是否自动使其加入游戏 5 | // - 6 | // Default: "1" 7 | // Minimum: "0.000000" 8 | // Maximum: "1.000000" 9 | l4d2_multislots_auto_join "1" 10 | 11 | // 自动给予离开安全区以后新出生的生还者武器与物品 -1:完全禁用(游戏中也无法使用指令开启) 0:关闭 1:开启 12 | // - 13 | // Default: "1" 14 | // Minimum: "-1.000000" 15 | // Maximum: "1.000000" 16 | l4d2_multislots_auto_give "1" 17 | // 自动给予马格南与M16 18 | sm_autogive pistol_magnum 19 | sm_autogive rifle 20 | 21 | // 当前回合结束是否自动踢出多余BOT 22 | // - 23 | // Default: "1" 24 | // Minimum: "0.000000" 25 | // Maximum: "1.000000" 26 | l4d2_multislots_auto_kickbot "1" 27 | 28 | // 根据人数自动设置物资倍数 -1:完全禁用(游戏中也无法使用指令开启) 0:关闭 1:开启 29 | // - 30 | // Default: "1" 31 | // Minimum: "-1.000000" 32 | // Maximum: "1.000000" 33 | l4d2_multislots_auto_supply "1" 34 | // 医疗包与近战自动多倍 若不设置则默认医疗包多倍 35 | sm_mmn weapon_first_aid_kit_spawn 36 | sm_mmn weapon_melee_spawn 37 | 38 | // 允许插件控制服务器最大人数?0:不允许 1:允许且允许其它方式修改最大人数 2:只允许本插件控制最大人数 39 | // - 40 | // Default: "1" 41 | // Minimum: "0.000000" 42 | // Maximum: "2.000000" 43 | l4d2_multislots_allow_sset "1" 44 | 45 | // 服务器默认最大人数,不允许插件控制人数时本参数无效 46 | // - 47 | // Default: "8" 48 | // Minimum: "1.000000" 49 | // Maximum: "32.000000" 50 | l4d2_multislots_maxs "10" 51 | 52 | // 只允许在安全区内增加BOT 53 | // - 54 | // Default: "0" 55 | // Minimum: "0.000000" 56 | // Maximum: "1.000000" 57 | l4d2_multislots_onlysafe_addbot "0" 58 | 59 | // 生还者初始数量(添加多了服务器会爆卡喔,要是满了32个会刷不出特感) 60 | // - 61 | // Default: "4" 62 | // Minimum: "1.000000" 63 | // Maximum: "32.000000" 64 | l4d2_multislots_survivor_limit "4" 65 | 66 | // 哪些人可以使用传送指令?0:完全禁用 1:仅管理员可用 2:所有人可用 67 | // - 68 | // Default: "2" 69 | // Minimum: "0.000000" 70 | // Maximum: "2.000000" 71 | l4d2_multislots_tp_permission "2" 72 | 73 | // 限制玩家使用传送指令的时间间隔,单位为秒 74 | // - 75 | // Default: "0" 76 | // Minimum: "0.000000" 77 | l4d2_multislots_tp_limit "0" 78 | 79 | // 玩家死亡后多少秒可以选择自主复活?若设置为0则禁用复活。 80 | // - 81 | // Default: "30" 82 | // Minimum: "0.000000" 83 | l4d2_multislots_respawn_limit "30" 84 | 85 | // 玩家复活后拥有多少血量? 86 | // - 87 | // Default: "100" 88 | // Minimum: "1.000000" 89 | l4d2_multislots_respawn_health "100" -------------------------------------------------------------------------------- /l4d2_multislots/plugins/l4d2_multislots.smx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Lin515/L4D_LinGe_Plugins/3abfac98dd1922e9853441904d80509c01f9d064/l4d2_multislots/plugins/l4d2_multislots.smx -------------------------------------------------------------------------------- /l4d2_multislots/scripting/l4d2_multislots.sp: -------------------------------------------------------------------------------- 1 | // 适用于战役模式的多人控制 主要是自用 2 | // 代码大量参考(复制~)了望夜多人插件(R_smc)与豆瓣多人插件(l4d2_multislots SwiftReal, MI 5, 豆瓣) 3 | #pragma semicolon 1 4 | #pragma newdecls required 5 | #include 6 | #include 7 | #include 8 | #include 9 | 10 | public Plugin myinfo = { 11 | name = "多人控制", 12 | author = "LinGe", 13 | description = "L4D2多人控制", 14 | version = "2.18", 15 | url = "https://github.com/Lin515/L4D_LinGe_Plugins" 16 | }; 17 | 18 | ConVar cv_l4dSurvivorLimit; 19 | ConVar cv_svmaxplayers; 20 | ConVar cv_survivorLimit; 21 | ConVar cv_maxs; 22 | int g_maxs; 23 | ConVar cv_autoGive; 24 | int g_autoGive; 25 | ConVar cv_autoSupply; 26 | int g_autoSupply; 27 | ConVar cv_allowSset; 28 | ConVar cv_autoJoin; 29 | ConVar cv_onlySafeAddBot; 30 | ConVar cv_autoKickBot; 31 | ConVar cv_tpPermission; 32 | ConVar cv_tpLimit; 33 | ConVar cv_respawnLimit; 34 | int g_respawnLimit; 35 | ConVar cv_respawnHealth; 36 | // 玩家死亡时,进行倒计时,倒计时完毕可以自主复活 37 | int g_deathCountDown[MAXPLAYERS+1]; 38 | 39 | bool g_isOnVersus = true; // 本插件不应该用在对抗中,但是可以用在基于对抗的药役中 40 | ArrayList g_autoGiveWeapen; // 自动给予哪些物品 41 | ArrayList g_supply; // 哪些启用多倍物资补给 42 | int g_nowMultiple = 1; // 当前物资倍数 43 | bool g_allHumanInGame = true; // 所有玩家是否已经载入 默认为true是为了在游戏中途加载插件时能正常工作 44 | 45 | int g_clientSerial[MAXPLAYERS+1]; 46 | bool g_autoJoin[MAXPLAYERS+1]; // 哪些玩家自动加入生还者 47 | int g_lastTpTime[MAXPLAYERS+1]; // 玩家上次使用tp时间 48 | 49 | bool g_hasMapTransitioned = false; // 是否发生地图过渡 50 | 51 | public APLRes AskPluginLoad2(Handle myself, bool late, char[] error, int err_max) 52 | { 53 | EngineVersion game = GetEngineVersion(); 54 | if (game != Engine_Left4Dead2) 55 | { 56 | strcopy(error, err_max, "本插件只支持 Left 4 Dead 2."); 57 | return APLRes_SilentFailure; 58 | } 59 | return APLRes_Success; 60 | } 61 | 62 | public void OnPluginStart() 63 | { 64 | cv_l4dSurvivorLimit = FindConVar("survivor_limit"); 65 | cv_svmaxplayers = FindConVar("sv_maxplayers"); 66 | cv_survivorLimit = CreateConVar("l4d2_multislots_survivor_limit", "4", "生还者初始数量(添加多了服务器会爆卡喔,要是满了32个会刷不出特感)", _, true, 1.0, true, 32.0); 67 | cv_maxs = CreateConVar("l4d2_multislots_maxs", "8", "服务器默认最大人数。不允许插件控制人数时本参数无效。", _, true, 1.0, true, 32.0); 68 | cv_autoGive = CreateConVar("l4d2_multislots_auto_give", "1", "自动给予离开安全区以后新出生的生还者武器与物品 -1:完全禁用(游戏中也无法使用指令开启) 0:关闭 1:开启", _, true, -1.0, true, 1.0); 69 | cv_autoSupply = CreateConVar("l4d2_multislots_auto_supply", "1", "根据人数自动设置物资倍数 -1:完全禁用(游戏中也无法使用指令开启) 0:关闭 1:开启", _, true, -1.0, true, 1.0); 70 | cv_allowSset = CreateConVar("l4d2_multislots_allow_sset", "1", "允许插件控制服务器最大人数?0:不允许 1:允许且允许其它方式修改最大人数 2:只允许本插件控制最大人数", _, true, 0.0, true, 2.0); 71 | cv_autoJoin = CreateConVar("l4d2_multislots_auto_join", "1", "玩家连接完毕后是否自动使其加入游戏", _, true, 0.0, true, 1.0); 72 | cv_onlySafeAddBot = CreateConVar("l4d2_multislots_onlysafe_addbot", "0", "只允许在安全区内增加BOT", _, true, 0.0, true, 1.0); 73 | cv_autoKickBot = CreateConVar("l4d2_multislots_auto_kickbot", "1", "当前回合结束是否自动踢出多余BOT", _, true, 0.0, true, 1.0); 74 | cv_tpPermission = CreateConVar("l4d2_multislots_tp_permission", "2", "哪些人可以使用传送指令?0:完全禁用 1:仅管理员可用 2:所有人可用", _, true, 0.0, true, 2.0); 75 | cv_tpLimit = CreateConVar("l4d2_multislots_tp_limit", "0", "限制玩家使用传送指令的时间间隔,单位为秒", _, true, 0.0); 76 | cv_respawnLimit = CreateConVar("l4d2_multislots_respawn_limit", "30", "玩家死亡后多少秒可以选择自主复活?若设置为0则禁用复活。", _, true, 0.0); 77 | cv_respawnHealth = CreateConVar("l4d2_multislots_respawn_health", "100", "玩家复活后拥有多少血量?", _, true, 1.0, true, 100.0); 78 | cv_l4dSurvivorLimit.SetBounds(ConVarBound_Upper, true, 32.0); 79 | cv_l4dSurvivorLimit.AddChangeHook(SurvivorLimitChanged); 80 | cv_survivorLimit.AddChangeHook(SurvivorLimitChanged); 81 | cv_autoSupply.AddChangeHook(AutoMultipleChanged); 82 | if (null != cv_svmaxplayers) 83 | cv_svmaxplayers.AddChangeHook(MaxplayersChanged); 84 | AutoExecConfig(true, "l4d2_multislots"); 85 | 86 | RegAdminCmd("sm_forceaddbot", Cmd_forceaddbot, ADMFLAG_ROOT, "强制增加一个BOT,无视条件限制"); 87 | RegAdminCmd("sm_addbot", Cmd_addbot, ADMFLAG_KICK, "增加一个BOT"); 88 | RegAdminCmd("sm_ab", Cmd_addbot, ADMFLAG_KICK, "增加一个BOT"); 89 | RegAdminCmd("sm_kb", Cmd_kb, ADMFLAG_KICK, "踢出所有电脑BOT"); 90 | RegAdminCmd("sm_sset", Cmd_sset, ADMFLAG_ROOT, "设置服务器最大人数"); 91 | RegAdminCmd("sm_mmn", Cmd_mmn, ADMFLAG_ROOT, "自动多倍物资设置"); 92 | RegAdminCmd("sm_autogive", Cmd_autogive, ADMFLAG_ROOT, "自动给予物品设置"); 93 | RegAdminCmd("sm_respawn", Cmd_respawn, ADMFLAG_ROOT, "设置 respawn_limit"); 94 | 95 | AddCommandListener(Command_Jointeam, "jointeam"); 96 | AddCommandListener(Command_Vote, "Vote"); 97 | RegConsoleCmd("sm_jg", Cmd_joingame, "玩家加入生还者"); 98 | RegConsoleCmd("sm_join", Cmd_joingame, "玩家加入生还者"); 99 | RegConsoleCmd("sm_joingame", Cmd_joingame, "玩家加入生还者"); 100 | RegConsoleCmd("sm_away", Cmd_away, "玩家进入旁观"); 101 | RegConsoleCmd("sm_s", Cmd_away, "玩家进入旁观"); 102 | RegConsoleCmd("sm_spec", Cmd_away, "玩家进入旁观"); 103 | RegConsoleCmd("sm_spectate", Cmd_away, "玩家进入旁观"); 104 | RegConsoleCmd("sm_afk", Cmd_afk, "快速闲置"); 105 | RegConsoleCmd("sm_tp", Cmd_tp, "玩家自主传送指令"); 106 | RegConsoleCmd("sm_re1", Cmd_re1, "死亡处复活"); 107 | RegConsoleCmd("sm_re2", Cmd_re2, "队友处复活"); 108 | 109 | HookEvent("round_start", Event_round_start, EventHookMode_PostNoCopy); 110 | HookEvent("map_transition", Event_map_transition, EventHookMode_Pre); 111 | HookEvent("finale_win", Event_round_end, EventHookMode_Pre); 112 | HookEvent("round_end", Event_round_end, EventHookMode_Pre); 113 | HookEvent("player_team", Event_player_team, EventHookMode_Post); 114 | HookEvent("player_death", Event_player_death, EventHookMode_PostNoCopy); 115 | HookEvent("defibrillator_used", Event_defibrillator_used, EventHookMode_Post); 116 | 117 | g_supply = CreateArray(40); 118 | g_autoGiveWeapen = CreateArray(40); 119 | for (int i=0; i 0 && null != cv_svmaxplayers) 151 | cv_svmaxplayers.IntValue = cv_maxs.IntValue; 152 | g_isOnVersus = IsOnVersus(); 153 | } 154 | 155 | public Action Cmd_forceaddbot(int client, int agrs) 156 | { 157 | if (0 != client && AddBot(true) == 0) 158 | PrintToChat(client, "\x04已强制添加一个BOT"); 159 | return Plugin_Handled; 160 | } 161 | 162 | public Action Cmd_addbot(int client, int agrs) 163 | { 164 | if (0 == client) 165 | return Plugin_Handled; 166 | switch (AddBot()) 167 | { 168 | case 0: 169 | PrintToChat(client, "\x04已成功添加一个BOT"); 170 | case -1: 171 | PrintToChat(client, "\x04服务器只允许未出安全区时增加BOT"); 172 | case -2: 173 | PrintToChat(client, "\x04当前无需增加BOT"); 174 | case -3: 175 | PrintToChat(client, "\x04创建BOT失败"); 176 | case -4: 177 | PrintToChat(client, "\x04生成生还者BOT失败"); 178 | case -5: 179 | PrintToChat(client, "\x04无法复活BOT,请尝试在回合开始时再添加"); 180 | } 181 | return Plugin_Handled; 182 | } 183 | 184 | public Action Cmd_away(int client, int args) 185 | { 186 | if (0 == client) 187 | return Plugin_Handled; 188 | if (GetClientTeam(client) == TEAM_SPECTATOR) 189 | { 190 | if (IsClientIdle(client)) 191 | PrintToChat(client, "\x04你当前已经是闲置状态"); 192 | else 193 | PrintToChat(client, "\x04你已经是旁观者了"); 194 | return Plugin_Handled; 195 | } 196 | if (GetClientTeam(client) == TEAM_SURVIVOR) 197 | { 198 | if (IsAlive(client)) 199 | g_autoJoin[client] = false; 200 | else 201 | { 202 | PrintToChat(client, "\x04请等待复活"); 203 | return Plugin_Handled; 204 | } 205 | } 206 | ChangeClientTeam(client, 1); 207 | return Plugin_Handled; 208 | } 209 | 210 | public Action Cmd_afk(int client, int args) 211 | { 212 | if (0 == client) 213 | return Plugin_Handled; 214 | 215 | if (GetClientTeam(client) == TEAM_SPECTATOR) 216 | { 217 | if (IsClientIdle(client)) 218 | PrintToChat(client, "\x04你当前已经是闲置状态"); 219 | else 220 | PrintToChat(client, "\x04你已经是旁观者了"); 221 | } 222 | else if (GetClientTeam(client) != TEAM_SURVIVOR) 223 | PrintToChat(client, "\x04闲置指令只限生还者使用"); 224 | else if (!IsAlive(client)) 225 | PrintToChat(client, "\x04死亡状态无权使用闲置"); 226 | else if (GetEntProp(client, Prop_Send, "m_isIncapacitated")) 227 | PrintToChat(client, "\x04倒地时无法使用闲置"); 228 | else if (GetPlayers(TEAM_SURVIVOR, false, true) == 1) 229 | PrintToChat(client, "\x04只有一名玩家存活时无法使用闲置"); 230 | else 231 | FakeClientCommand(client, "go_away_from_keyboard"); 232 | return Plugin_Handled; 233 | } 234 | public Action Cmd_tp(int client, int args) 235 | { 236 | if (0 == client) 237 | return Plugin_Handled; 238 | 239 | if (cv_tpPermission.IntValue == 0) 240 | { 241 | PrintToChat(client, "\x04服务器未启用传送指令"); 242 | return Plugin_Handled; 243 | } 244 | else if (cv_tpPermission.IntValue == 1) 245 | { 246 | if (0 == GetUserFlagBits(client)) 247 | { 248 | PrintToChat(client, "\x04服务器只允许管理员使用传送指令"); 249 | return Plugin_Handled; 250 | } 251 | } 252 | 253 | if (GetClientTeam(client) != TEAM_SURVIVOR) 254 | { 255 | PrintToChat(client, "\x04只有生还者可以使用传送指令"); 256 | return Plugin_Handled; 257 | } 258 | else if (!IsAlive(client)) 259 | { 260 | PrintToChat(client, "\x04死亡状态无法使用传送"); 261 | return Plugin_Handled; 262 | } 263 | else if (GetPlayers(TEAM_SURVIVOR, true, true) == 1) 264 | { 265 | PrintToChat(client, "\x04只有一名存活生还者时无法使用传送"); 266 | return Plugin_Handled; 267 | } 268 | 269 | int diff = GetTime() - g_lastTpTime[client]; 270 | if (diff < cv_tpLimit.IntValue) 271 | { 272 | PrintToChat(client, "\x04你需要\x03 %d \x04秒后才能再次使用传送指令", cv_tpLimit.IntValue-diff); 273 | return Plugin_Handled; 274 | } 275 | 276 | DisplayTpMenu(client); 277 | return Plugin_Handled; 278 | } 279 | 280 | public Action Cmd_joingame(int client, int args) 281 | { 282 | if (0 == client) 283 | return Plugin_Handled; 284 | switch (JoinSurvivor(client)) 285 | { 286 | case -1: 287 | PrintToChat(client, "\x04请等待本回合结束后再加入游戏"); 288 | case -2, -5: // 按本插件的机制,应该不会返回-2 -5为无法复活BOT 289 | PrintToChat(client, "\x04当前生还者空位不足,暂时无法加入"); 290 | case 1: 291 | PrintToChat(client, "\x04你已经是生还者了"); 292 | case 2: 293 | PrintToChat(client, "\x04你当前是闲置状态,请点击鼠标左键加入游戏"); 294 | case 3: 295 | PrintToChat(client, "\x04有玩家尚未载入完毕,当所有玩家载入完毕时你将自动加入生还者"); 296 | } 297 | return Plugin_Handled; 298 | } 299 | 300 | public Action Cmd_kb(int client, int args) 301 | { 302 | KickAllBot(); 303 | PrintToChatAll("\x04踢除所有bot"); 304 | 305 | return Plugin_Handled; 306 | } 307 | 308 | public Action Cmd_sset(int client, int args) 309 | { 310 | if (client > 0) 311 | { 312 | if (null == cv_svmaxplayers) 313 | PrintToChat(client, "\x04未能捕捉到\x03 sv_maxplayers"); 314 | else if (cv_allowSset.IntValue > 0) 315 | SsetMenuDisplay(client); 316 | else 317 | PrintToChat(client, "\x04服务器人数控制未开启"); 318 | } 319 | return Plugin_Handled; 320 | } 321 | 322 | public Action Cmd_mmn(int client, int args) 323 | { 324 | if (cv_autoSupply.IntValue == -1 && client > 0) 325 | { 326 | PrintToChat(client, "\x04自动多倍物资功能当前是完全禁用的"); 327 | return Plugin_Handled; 328 | } 329 | 330 | char buffer[40]; 331 | if (0 == args) 332 | { 333 | if (0 == client) 334 | { 335 | int len = g_supply.Length; 336 | if (0 == len) 337 | { 338 | PrintToServer("未设置自定义多倍的物资,将默认启用医疗包多倍"); 339 | } 340 | else 341 | { 342 | for (int i=0; i 0) 390 | { 391 | PrintToChat(client, "\x04自动给予物资功能当前是完全禁用的"); 392 | return Plugin_Handled; 393 | } 394 | 395 | char buffer[40]; 396 | if (0 == args) 397 | { 398 | // 如果是在服务器执行,则列出当前自动给予物品列表 399 | if (0 == client) 400 | { 401 | int len = g_autoGiveWeapen.Length; 402 | if (0 == len) 403 | PrintToServer("未设置自动给予的物品,将默认给予MP5"); 404 | else 405 | { 406 | for (int i=0; i 0) 458 | PrintToChatAll("\x04死亡 \x03%d \x04秒后可以自主复活", cv_respawnLimit.IntValue); 459 | else 460 | PrintToChatAll("\x04自主复活\x03 已关闭"); 461 | return Plugin_Handled; 462 | } 463 | 464 | public void AutoMultipleChanged(ConVar convar, const char[] oldValue, const char[] newValue) 465 | { 466 | SetMultiple(); 467 | } 468 | 469 | public void SurvivorLimitChanged(ConVar convar, const char[] oldValue, const char[] newValue) 470 | { 471 | cv_l4dSurvivorLimit.SetInt(cv_survivorLimit.IntValue); 472 | } 473 | public void MaxplayersChanged(ConVar convar, const char[] oldValue, const char[] newValue) 474 | { 475 | if (cv_allowSset.IntValue == 2) 476 | cv_svmaxplayers.IntValue = cv_maxs.IntValue; 477 | else 478 | cv_maxs.IntValue = cv_svmaxplayers.IntValue; 479 | } 480 | 481 | public bool OnClientConnect(int client) 482 | { 483 | // 重置一些数据 484 | int serial = GetClientSerial(client); 485 | if (g_clientSerial[client] != serial) 486 | { 487 | g_clientSerial[client] = serial; 488 | g_autoJoin[client] = true; 489 | } 490 | g_lastTpTime[client] = 0; 491 | return true; 492 | } 493 | 494 | public void OnMapEnd() 495 | { 496 | g_allHumanInGame = false; 497 | } 498 | // round_start事件在OnMapStart之前触发,且OnMapStart只在地图变更时会触发 499 | public Action Event_round_start(Event event, const char[] name, bool dontBroadcast) 500 | { 501 | g_allHumanInGame = false; 502 | CreateTimer(1.0, Timer_HasClient, _, TIMER_REPEAT); 503 | } 504 | public Action Timer_HasClient(Handle timer, any multiple) 505 | { 506 | if (GetClients() == 0) 507 | return Plugin_Continue; 508 | 509 | // 开局以之前的倍数设置一次多倍物资 510 | // 只有当游戏中有至少一个玩家存在时,物资实体才会存在 511 | // 此时设置多倍物资才是有效的 512 | int nowMultiple = g_nowMultiple; 513 | g_nowMultiple = 1; 514 | if (cv_autoSupply.IntValue == 1) 515 | SetMultiple(nowMultiple); 516 | CreateTimer(1.0, Timer_CheckAllHumanInGame, _, TIMER_REPEAT); 517 | return Plugin_Stop; 518 | } 519 | public Action Timer_CheckAllHumanInGame(Handle timer) 520 | { 521 | if (!IsAllHumanInGame()) 522 | return Plugin_Continue; 523 | // 所有玩家载入完毕之后再校准一次物资倍数 524 | if (cv_autoSupply.IntValue == 1) 525 | SetMultiple(); 526 | g_allHumanInGame = true; 527 | // 让没有加入游戏的玩家自动加入 528 | if (GetPlayers(TEAM_SURVIVOR) > 0) 529 | { 530 | for (int i=1; i<=MaxClients; i++) 531 | { 532 | if (g_autoJoin[i]) 533 | JoinSurvivor(i); // 无需判断client有效性 JoinSurvivor自带判断 534 | } 535 | } 536 | return Plugin_Stop; 537 | } 538 | 539 | public Action Event_round_end(Event event, const char[] name, bool dontBroadcast) 540 | { 541 | if (cv_autoKickBot.IntValue == 1) 542 | KickAllBot(false); 543 | } 544 | 545 | public Action Event_defibrillator_used(Event event, const char[] name, bool dontBroadcast) 546 | { 547 | if (!event) 548 | return Plugin_Continue; 549 | if (cv_respawnLimit.IntValue == 0 || cv_respawnHealth.IntValue <= 50) 550 | return Plugin_Continue; 551 | 552 | int client = GetClientOfUserId(event.GetInt("subject")); 553 | SetEntProp(client, Prop_Send, "m_iHealth", cv_respawnHealth.IntValue); 554 | return Plugin_Continue; 555 | } 556 | 557 | public Action Event_player_team(Event event, const char[] name, bool dontBroadcast) 558 | { 559 | int client = GetClientOfUserId(event.GetInt("userid")); 560 | int oldteam = event.GetInt("oldteam"); 561 | int team = event.GetInt("team"); 562 | 563 | if (IsValidClient(client, true)) 564 | { 565 | // 自动让玩家加入生还者 566 | if (!IsFakeClient(client) && oldteam == 0) 567 | { 568 | CreateTimer(1.0, Timer_AutoJoinSurvivor, client, TIMER_REPEAT); 569 | } 570 | // 自动更改物资倍数需所有玩家已完成载入 571 | if ( g_allHumanInGame && cv_autoSupply.IntValue == 1) 572 | { 573 | if ( (0 == oldteam && 2 == team) 574 | || (2 == oldteam && 0 == team) ) 575 | CreateTimer(1.2, Timer_SetMultiple); 576 | } 577 | } 578 | } 579 | public Action Timer_AutoJoinSurvivor(Handle timer, any client) 580 | { 581 | if (!IsClientConnected(client)) 582 | return Plugin_Stop; 583 | // 等待玩家完全进入游戏 584 | // 过早的使玩家复活或加入存活的BOT,会导致开局生还者数量错误 585 | if (g_allHumanInGame && IsClientInGame(client)) 586 | { 587 | // 如果玩家数少于4人,则玩家可能自动会加入到生还者 588 | if (GetClientTeam(client) == TEAM_SURVIVOR) 589 | { 590 | if (!IsAlive(client)) 591 | RespawnTeleportGiveSupply(client); 592 | } 593 | else if (GetClientTeam(client) == TEAM_SPECTATOR 594 | && g_autoJoin[client] && cv_autoJoin.IntValue==1) 595 | { 596 | JoinSurvivor(client); 597 | } 598 | return Plugin_Stop; 599 | } 600 | return Plugin_Continue; 601 | } 602 | public Action Timer_SetMultiple(Handle timer) 603 | { 604 | SetMultiple(); 605 | } 606 | 607 | // 玩家死亡事件 608 | public Action Event_player_death(Event event, const char[] name, bool dontBroadcast) 609 | { 610 | if (cv_respawnLimit.IntValue == 0) 611 | return Plugin_Continue; 612 | if (!event) 613 | return Plugin_Continue; 614 | int client = GetClientOfUserId(event.GetInt("userid")); 615 | if (!IsAllowRespawn(client, false)) 616 | return Plugin_Continue; 617 | 618 | g_deathCountDown[client] = cv_respawnLimit.IntValue; 619 | PrintHintText(client, "在 %d 秒后你可以复活自己", g_deathCountDown[client]); 620 | CreateTimer(1.0, Timer_RespawnPlayer, client, TIMER_REPEAT); 621 | return Plugin_Continue; 622 | } 623 | // 倒计时复活 624 | public Action Timer_RespawnPlayer(Handle timer, any client) 625 | { 626 | g_deathCountDown[client]--; 627 | if (!IsAllowRespawn(client, false)) 628 | { 629 | if (IsValidClient(client) && GetClientTeam(client) == TEAM_SPECTATOR) 630 | PrintHintText(client, "我的战场不在这里"); 631 | g_deathCountDown[client] = cv_respawnLimit.IntValue; 632 | return Plugin_Stop; 633 | } 634 | 635 | if (g_deathCountDown[client] > 0) 636 | { 637 | PrintHintText(client, "在 %d 秒后你可以复活自己", g_deathCountDown[client]); 638 | } 639 | else 640 | { 641 | PrintHintText(client, "F1键或!re1:死亡处复活\nF2键或!re2:队友处复活"); 642 | } 643 | return Plugin_Continue; 644 | } 645 | void RevivePlayer(int client, bool teleport=true) 646 | { 647 | L4D2_VScriptWrapper_ReviveByDefib(client); 648 | if (!IsAlive(client)) 649 | { 650 | if (!teleport) 651 | PrintToChat(client, "\x04在死亡处复活失败,你将传送到队友身边"); 652 | L4D_RespawnPlayer(client); 653 | teleport = true; 654 | } 655 | GivePlayerSupply(client); 656 | SetHealth(client, cv_respawnHealth.IntValue); 657 | if (teleport) 658 | TeleportToAliveSurvivor(client); 659 | } 660 | 661 | // 监测F1/F2 662 | public Action Command_Vote(int client, const char[] command, int args) 663 | { 664 | if (IsAllowRespawn(client)) 665 | { 666 | char sArg[8]; 667 | GetCmdArg(1, sArg, sizeof(sArg)); 668 | if (strcmp(sArg, "Yes", false) == 0 || strcmp(sArg, "No", false) == 0 ) 669 | RevivePlayer(client, strcmp(sArg, "No", false) == 0); 670 | } 671 | return Plugin_Continue; 672 | } 673 | public Action Cmd_re1(int client, int args) 674 | { 675 | if (IsAllowRespawn(client)) 676 | RevivePlayer(client, false); 677 | } 678 | public Action Cmd_re2(int client, int args) 679 | { 680 | if (IsAllowRespawn(client)) 681 | RevivePlayer(client, true); 682 | } 683 | 684 | void SsetMenuDisplay(int client) 685 | { 686 | char namelist[128]; 687 | char nameno[16]; 688 | Menu menu = new Menu(SsetMenuHandler); 689 | menu.SetTitle("设置服务器人数:"); 690 | int i = 1; 691 | while (i <= 32) 692 | { 693 | Format(namelist, 32, "%d", i); 694 | Format(nameno, 4, "%i", i); 695 | AddMenuItem(menu, nameno, namelist, 0); 696 | i++; 697 | } 698 | SetMenuExitButton(menu, true); 699 | DisplayMenu(menu, client, 0); 700 | } 701 | public int SsetMenuHandler(Handle menu, MenuAction action, int client, int itemNum) 702 | { 703 | switch (action) 704 | { 705 | case MenuAction_Select: 706 | { 707 | char clientinfos[20]; 708 | GetMenuItem(menu, itemNum, clientinfos, sizeof(clientinfos)); 709 | cv_maxs.IntValue = StringToInt(clientinfos); 710 | cv_svmaxplayers.IntValue = cv_maxs.IntValue; 711 | PrintToChatAll("\x04更改服务器的最大人数为\x04 \x03%i \x05人", cv_svmaxplayers.IntValue); 712 | } 713 | case MenuAction_End: 714 | delete menu; 715 | } 716 | return 0; 717 | } 718 | 719 | int DisplayTpMenu(int client) 720 | { 721 | char name[MAX_NAME_LENGTH]; 722 | char rec[10]; 723 | Menu menu = new Menu(TpMenuHandler); 724 | menu.SetTitle("你想传送到谁那里?"); 725 | for (int i=1; i<=MaxClients; i++) 726 | { 727 | if (IsClientInGame(i)) 728 | { 729 | if (GetClientTeam(i) == TEAM_SURVIVOR && IsAlive(i) && i != client) 730 | { 731 | GetClientName(i, name, sizeof(name)); 732 | IntToString(i, rec, sizeof(rec)); 733 | menu.AddItem(rec, name); 734 | } 735 | } 736 | } 737 | menu.ExitBackButton = false; 738 | menu.Display(client, MENU_TIME_FOREVER); 739 | } 740 | public int TpMenuHandler(Menu menu, MenuAction action, int client, int curSel) 741 | { 742 | switch(action) 743 | { 744 | case MenuAction_End: 745 | { 746 | delete menu; 747 | } 748 | case MenuAction_Select: 749 | { 750 | char rec[10]; 751 | if ( menu.GetItem(curSel, rec, sizeof(rec)) ) 752 | { 753 | int target = StringToInt(rec); 754 | if ( IsValidClient(target) && GetClientTeam(target)==TEAM_SURVIVOR 755 | && IsValidClient(client) && GetClientTeam(client)==TEAM_SURVIVOR ) 756 | { 757 | Teleport(client, target); 758 | g_lastTpTime[client] = GetTime(); 759 | } 760 | } 761 | } 762 | } 763 | } 764 | 765 | // 设置物资补给倍数,参数为0则根据当前生还者数量自动设置,若未开启多倍物资则重置为1 766 | void SetMultiple(int num=0) 767 | { 768 | if (0 == num) 769 | { 770 | if (cv_autoSupply.IntValue == 1) 771 | { 772 | int survivors = GetPlayers(TEAM_SURVIVOR); 773 | num = survivors / 4; 774 | // 向上取整且num最小为1 775 | if (survivors%4 != 0 || 0 == num) 776 | num++; 777 | } 778 | else 779 | num = 1; 780 | } 781 | 782 | if (num != g_nowMultiple) 783 | { 784 | int len = g_supply.Length; 785 | char buffer[40]; 786 | char numstr[10]; 787 | IntToString(num, numstr, sizeof(numstr)); 788 | // 如果未自定义物资补给,则只设置医疗包多倍 789 | if (0 == len) 790 | { 791 | SetKeyValueByClassname("weapon_first_aid_kit_spawn", "count", numstr); 792 | } 793 | else 794 | { 795 | for (int i=0; i0; i++) 815 | { 816 | if (IsClientInGame(i) && IsFakeClient(i)) 817 | { 818 | if (GetClientTeam(i) == TEAM_SURVIVOR) 819 | { 820 | if (GetHumanClient(i) == 0) 821 | { 822 | KickClient(i, ""); 823 | kickCount--; 824 | } 825 | } 826 | } 827 | } 828 | } 829 | 830 | // 使client加入生还者 会自动判断client有效性 831 | int JoinSurvivor(int client) 832 | { 833 | if (!IsValidClient(client)) // 判断client有效性 834 | return 4; 835 | if (IsFakeClient(client)) // 不允许BOT通过此函数加入生还 836 | return 5; 837 | g_autoJoin[client] = true; 838 | if (GetClientTeam(client) == TEAM_SURVIVOR) // client已经是生还者 839 | return 1; 840 | if (IsClientIdle(client)) // client处于闲置 841 | return 2; 842 | if (!g_allHumanInGame) 843 | return 3; 844 | 845 | // 搜索可接管BOT,若没有则添加一个 846 | int ret = 0; 847 | int bot = FindBotToTakeOver(); 848 | if (bot > 0) 849 | { 850 | TakeOverBot(client, bot); 851 | ret = 0; 852 | } 853 | else 854 | { 855 | ret = AddBot(); 856 | } 857 | if (0 == ret) // 0.5秒后检查其状态 858 | CreateTimer(0.5, Delay_JoinSurvivor, client); 859 | return ret; 860 | } 861 | public Action Delay_JoinSurvivor(Handle timer, any client) 862 | { 863 | JoinSurvivor(client); 864 | } 865 | 866 | // AddBot 返回0表示成功增加BOT 返回-1表示当前不允许增加BOT 返回-2表示无需增加BOT 867 | int AddBot(bool force=false) 868 | { 869 | if (!force && cv_onlySafeAddBot.IntValue == 1 870 | && L4D_HasAnySurvivorLeftSafeArea() ) 871 | return -1; 872 | if (!force && GetAliveBotSurvivors() >= GetSpectators() 873 | && GetPlayers(TEAM_SURVIVOR) >= cv_survivorLimit.IntValue ) 874 | return -2; 875 | 876 | int bot = CreateFakeClient("survivor bot"); 877 | if (bot > 0) 878 | { 879 | ChangeClientTeam(bot, 2); 880 | if (DispatchKeyValue(bot, "classname", "SurvivorBot") && DispatchSpawn(bot)) 881 | { 882 | RespawnTeleportGiveSupply(bot); 883 | KickClient(bot, ""); 884 | return 0; 885 | } 886 | KickClient(bot, ""); 887 | LogError("生成生还者BOT失败"); 888 | return -4; 889 | } 890 | else 891 | { 892 | LogError("BOT创建失败"); 893 | return -3; 894 | } 895 | } 896 | 897 | // 复活传送并给予物品 898 | void RespawnTeleportGiveSupply(int client) 899 | { 900 | // 如果client是死亡的则复活它 901 | if (!IsAlive(client)) 902 | L4D_RespawnPlayer(client); 903 | // 如果已经有人离开安全区,则传送并给予物资 904 | if (L4D_HasAnySurvivorLeftSafeArea()) 905 | { 906 | GivePlayerSupply(client); 907 | TeleportToAliveSurvivor(client); 908 | } 909 | } 910 | 911 | // 传送到一个活着的生还者附近 912 | bool TeleportToAliveSurvivor(int client) 913 | { 914 | // 传送 915 | for (int i=1; i<=MaxClients; i++) 916 | { 917 | if (IsClientInGame(i)) 918 | { 919 | if (GetClientTeam(i) == TEAM_SURVIVOR && client!=i && IsAlive(i)) 920 | { 921 | Teleport(client, i); 922 | return true; 923 | } 924 | } 925 | } 926 | return false; 927 | } 928 | 929 | // 给予玩家物资 930 | void GivePlayerSupply(int client, bool checkConVar=true) 931 | { 932 | if (checkConVar && cv_autoGive.IntValue == 0) 933 | return; 934 | if (!IsValidClient(client)) 935 | return; 936 | int len = g_autoGiveWeapen.Length; 937 | char buffer[40]; 938 | if (len > 0) 939 | { 940 | for (int i=0; i 0) 971 | { 972 | char buffer[MAX_NAME_LENGTH]; 973 | GetCmdArg(1, buffer, sizeof(buffer)); 974 | if ( strcmp(buffer, "2") == 0 975 | || strcmp(buffer, "survivor", false) == 0 ) 976 | { 977 | JoinSurvivor(client); 978 | return Plugin_Handled; 979 | } 980 | } 981 | return Plugin_Continue; 982 | } 983 | 984 | // 让玩家接管一个生还者BOT 985 | void TakeOverBot(int client, int bot) 986 | { 987 | // 完全接管适用于基于对抗模式的药役 988 | // 战役模式应不完全接管 989 | if (g_isOnVersus) 990 | { 991 | L4D_SetHumanSpec(bot, client); 992 | L4D_TakeOverBot(client); 993 | } 994 | else 995 | { 996 | L4D_SetHumanSpec(bot, client); 997 | SetEntProp(client, Prop_Send, "m_iObserverMode", 5); 998 | WriteTakeoverPanel(client, bot); 999 | } 1000 | } 1001 | 1002 | // WriteTakeoverPanel 来源于 [Lux]survivor_afk_fix.sp 1003 | //Thanks Leonardo for helping me with the vgui keyvalue layout 1004 | //This is for rare case sometimes the takeover panel don't show. 1005 | void WriteTakeoverPanel(int client, int bot) 1006 | { 1007 | char buf[2]; 1008 | int character = GetEntProp(bot, Prop_Send, "m_survivorCharacter", 1); 1009 | IntToString(character, buf, sizeof(buf)); 1010 | BfWrite msg = view_as(StartMessageOne("VGUIMenu", client)); 1011 | msg.WriteString("takeover_survivor_bar"); //type 1012 | msg.WriteByte(true); //hide or show panel type 1013 | msg.WriteByte(1); //amount of keys 1014 | msg.WriteString("character"); //key name 1015 | msg.WriteString(buf); //key value 1016 | EndMessage(); 1017 | } 1018 | 1019 | // 所有已死亡的玩家数 1020 | stock int GetDeathHumanSurvivors() 1021 | { 1022 | int num = 0; 1023 | for (int i=1; i<=MaxClients; i++) 1024 | { 1025 | if (IsClientInGame(i)) 1026 | { 1027 | if (GetClientTeam(i) == TEAM_SURVIVOR && !IsFakeClient(i)) 1028 | { 1029 | if (!IsAlive(i)) 1030 | num++; 1031 | } 1032 | } 1033 | } 1034 | return num; 1035 | } 1036 | 1037 | // 是否符合复活条件:必须是已阵亡的真人生还者玩家 1038 | bool IsAllowRespawn(int client, bool checkCountDown=true) 1039 | { 1040 | if (client < 1 || client > MaxClients) 1041 | return false; 1042 | if (checkCountDown && g_deathCountDown[client] > 0) 1043 | return false; 1044 | if (!IsClientInGame(client)) // 必须在游戏中 1045 | return false; 1046 | if (IsFakeClient(client)) // 必须是真人玩家 1047 | return false; 1048 | if (GetClientTeam(client) != TEAM_SURVIVOR) // 必须是生还者 1049 | return false; 1050 | if (IsAlive(client)) // 必须已经阵亡 1051 | return false; 1052 | return true; 1053 | } -------------------------------------------------------------------------------- /l4d2_multislots/物品代码.txt: -------------------------------------------------------------------------------- 1 | give指令 给予物品指令可用武器代码 可用来设置自动给予物品的参数 2 | 主武器: 3 | grenade_lancher 榴弹发射器 4 | shotgun_chrome 铁喷 5 | pampshotgun 木喷 6 | smg UZI冲锋枪 7 | smg_mp5 MP5冲锋枪 8 | smg_silenced 消音冲锋枪 9 | autoshotgun 一代经典连喷 10 | shotgun_spas SPAS12散弹枪(二代连喷) 11 | rifle M16步枪 12 | rifle_ak47 AK47步枪 13 | rifle_desert SCAR三连发 14 | rifle_sg552 SG552步枪 15 | rifle_m60 M60 16 | hunting_rifle 15连木狙 17 | sniper_military 30连军狙 18 | sniper_scout cs小鸟枪 19 | sniper_awp cs大狙 20 | 副武器: 21 | pistol 普通手枪 22 | pistol_magnum 马格南 23 | crowbar 圣剑 24 | fireaxe 斧头 25 | tonfa 警棍 26 | knife 小刀 27 | machete 砍刀 28 | katana 武士刀 29 | shovel 铲子 30 | pitchfork 干草叉 31 | golfclub 高尔夫球棍 32 | baseball_bat 棒球棒 33 | cricket_bat 板球棒 34 | chainsaw 电锯 35 | electric_guitar 吉他 36 | frying_pan 平底锅 37 | riotshield 防爆盾 38 | 投掷: 39 | molotov 燃烧瓶 40 | pipe_bomb 土制炸弹 41 | vomitjar 胆汁 42 | 工具包: 43 | upgradepack_explosive 高爆弹药包 44 | upgradepack_incendiary 燃烧弹包 45 | defibrillator 电震器 46 | first_aid_kit 血包 47 | 药物: 48 | pain_pills 止痛药 49 | adrenaline 肾上腺素 50 | 特殊道具: 51 | gascan 油箱 52 | oxygentank 氧气罐 53 | propanetank 煤气罐 54 | fireworkcrate 烟花 55 | gnome 侏儒 56 | 57 | 物品生成实体代码 可用来设置哪些物品自动多倍 更多请查阅https://developer.valvesoftware.com/wiki/List_of_L4D2_Entities 58 | 具体含义不一一说明了,与上述give指令的代码基本差不多,请自行理解 59 | weapon_adrenaline_spawn 60 | weapon_ammo_spawn 61 | weapon_autoshotgun_spawn 62 | weapon_chainsaw_spawn 63 | weapon_defibrillator_spawn 64 | weapon_first_aid_kit_spawn 65 | weapon_gascan_spawn 66 | weapon_grenade_launcher_spawn 67 | weapon_hunting_rifle_spawn 68 | weapon_item_spawn 69 | weapon_melee_spawn 70 | weapon_molotov_spawn 71 | weapon_pain_pills_spawn 72 | weapon_pipe_bomb_spawn 73 | weapon_pistol_magnum_spawn 74 | weapon_pistol_spawn 75 | weapon_pumpshotgun_spawn 76 | weapon_rifle_ak47_spawn 77 | weapon_rifle_desert_spawn 78 | weapon_rifle_m60_spawn 79 | weapon_rifle_sg552_spawn 80 | weapon_rifle_spawn 81 | weapon_scavenge_item_spawn 82 | weapon_shotgun_chrome_spawn 83 | weapon_shotgun_spas_spawn 84 | weapon_smg_mp5_spawn 85 | weapon_smg_silenced_spawn 86 | weapon_smg_spawn 87 | weapon_sniper_awp_spawn 88 | weapon_sniper_military_spawn 89 | weapon_sniper_scout_spawn 90 | weapon_spawn 91 | weapon_upgradepack_explosive_spawn 92 | weapon_upgradepack_incendiary_spawn 93 | weapon_vomitjar_spawn -------------------------------------------------------------------------------- /l4d2_votes/README.md: -------------------------------------------------------------------------------- 1 | # 求生之路2 多功能投票 2 | 3 | 发送 !votes 打开投票菜单,所有玩家可用。 4 | 包含多倍弹药、自动红外、友伤设置、服务器人数设置、特感击杀回血等功能。 5 | 另外可以添加自定义选项。 6 | 7 | **支持游戏:求生之路2** 8 | **建议插件平台:SourceMod 1.10.0** 9 | **依赖:[builtinvotes](https://github.com/A1mDev/builtinvotes)** 10 | 11 | ## 服务器指令 12 | 13 | - **l4d2_votes_additem** 14 | 给投票菜单添加额外选项。 15 | 指令格式:l4d2_votes_additem 是否是服务器指令[0或1] 选项名 指令 16 | 如果指定为服务器指令,则会发起投票,通过才会执行。 17 | 若不是服务器指令,则在客户端直接执行该指令。 18 | 19 | ``` 20 | // 设置服务器指令 21 | l4d2_votes_additem 1 "Coop" "sm_cvar mp_gamemode coop" 22 | // 设置客户端指令 23 | l4d2_votes_additem 0 "MapVote" "sm_mapvote" 24 | ``` 25 | 26 | - **l4d2_votes_removeitem** 27 | 删除额外选项。 28 | 指令格式:l4d2_votes_removeitem 选项名 29 | 30 | ``` 31 | l4d2_votes_removeitem "MapVote" 32 | ``` 33 | 34 | ### 关于添加额外的中文选项名 35 | 36 | 服务端对中文的支持不是很好,如果要添加额外的中文选项,例如在 cfg 文件中写入: 37 | 38 | ``` 39 | l4d2_votes_additem 0 "更换地图" "sm_mapvote" 40 | ``` 41 | 42 | 服务器会无法识别出中文字符而以致指令输入无效。 43 | 44 | 对此可以使用 VScripts 脚本来代替 cfg 文件完成指令添加,下面是一个简单的例子: 45 | 46 | ``` squirrel 47 | // scripts/vscripts/director_base_addon.nut 48 | switch (Convars.GetStr("hostport")) 49 | { 50 | case "20001": 51 | SendToServerConsole("l4d2_votes_additem 0 \"更换模式\" \"sm_modevote\""); 52 | SendToServerConsole("l4d2_votes_additem 0 \"更换地图\" \"sm_mapvote\""); 53 | break; 54 | default: 55 | break; 56 | } 57 | ``` 58 | 59 | 60 | 61 | ## 控制台变量 62 | 63 | 插件会自动生成 cfg 文件。每个变量后面为其默认值。 64 | 65 | - **l4d2_votes_admin_pass** 1 66 | 管理员是否无需经过投票即可直接执行指令 0:否 1:是 67 | - **l4d2_votes_time** 20 68 | 投票应在多少秒内完成? 69 | - **l4d2_votes_delay** 10 70 | 玩家需间隔多少秒才能再次发起投票? 71 | 插件会锁定 sm_vote_delay 一直为本变量的值。 72 | - **l4d2_votes_ammomode** 1 73 | 多倍弹药模式 74 | - -1完全禁用 75 | - 0 禁用多倍但允许投票补满所有人弹药 76 | - 1 一倍且允许开启多倍弹药 77 | - 2 双倍 78 | - 3 三倍 79 | - 4 无限(需换弹) 80 | - 5 无限(无需换弹) 81 | - **l4d2_votes_autolaser** 0 82 | 自动获得红外 -1:完全禁用 0:关闭 1:开启 83 | - **l4d2_votes_teamhurt** 0 84 | 是否允许投票改变友伤系数 -1:不允许 0:允许 85 | - **l4d2_votes_restartchapter** 0 86 | 是否允许投票重启当前章节 -1:不允许 0:允许 87 | - **l4d2_votes_players** 8 88 | 服务器人数,若为0则不改变人数。游戏时改变本参数是无效的。 89 | - **l4d2_votes_players_lower** 4 90 | 投票更改服务器人数的下限 91 | - **l4d2_votes_players_upper** 12 92 | 投票更改服务器人数的上限。若下限>=上限,则不允许投票更改服务器人数(不影响本插件更改默认人数)。 93 | - **ReturnBlood** 0 94 | 特感击杀回血总开关 -1:完全禁用 0:关闭 1:开启 95 | - **l4d2_votes_returnblood_special** 2 96 | 击杀一只特感回多少血 97 | - **l4d2_votes_returnblood_witch** 10 98 | 击杀一只Witch回多少血 99 | - **l4d2_votes_returnblood_limit** 100 100 | 最高回血上限。 101 | 仅影响回血时的上限,不影响其它情况下的血量上限。 -------------------------------------------------------------------------------- /l4d2_votes/plugins/l4d2_votes.smx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Lin515/L4D_LinGe_Plugins/3abfac98dd1922e9853441904d80509c01f9d064/l4d2_votes/plugins/l4d2_votes.smx -------------------------------------------------------------------------------- /l4d2_votes/scripting/l4d2_votes.sp: -------------------------------------------------------------------------------- 1 | #pragma semicolon 1 2 | #pragma newdecls required 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | 9 | public Plugin myinfo = { 10 | name = "l4d2 votes", 11 | author = "LinGe", 12 | description = "多功能投票:弹药、自动红外、友伤、服务器人数设置、特感击杀回血等", 13 | version = "1.8", 14 | url = "https://github.com/Lin515/L4D_LinGe_Plugins" 15 | }; 16 | 17 | ConVar cv_svmaxplayers; // sv_maxplayers 18 | ConVar cv_smVoteDelay; // sm_vote_delay 19 | ConVar cv_adminPass; // 管理员是否不需要经过投票即可直接执行设置 20 | ConVar cv_voteDelay; 21 | ConVar cv_voteTime; // 投票应在多少秒内完成 22 | ConVar cv_ammoMode; // 弹药模式 23 | int g_ammoMode; 24 | ConVar cv_autoLaser; // 自动红外 25 | int g_autoLaser; 26 | ConVar cv_teamHurt; // 是否允许投票改变友伤系数 27 | ConVar cv_restartChapter; // 是否允许投票重启当前章节 28 | ConVar cv_players; // 服务器默认人数 29 | int g_players; 30 | ConVar cv_playersLower; // 投票改变服务器人数下限 31 | ConVar cv_playersUpper; // 投票改变服务器人数上限 32 | ConVar cv_returnBlood; // 击杀回血 33 | int g_returnBlood; 34 | ConVar cv_specialReturn; // 特感击杀回血量 35 | ConVar cv_witchReturn; // witch击杀回血量 36 | ConVar cv_healthLimit; // 回血上限 37 | ConVar cv_restore; 38 | int g_zombieClassOffset; // 感染者类型的网络属性偏移量 39 | bool g_hasMapTransitioned = false; // 是否发生地图过渡 40 | 41 | // votes 42 | enum struct VoteAction { 43 | char action[64]; 44 | char params[64]; 45 | } 46 | enum struct ExtraItem { 47 | bool isServerCommand; // 如果是服务器指令,则会发起投票执行 48 | char display[64]; 49 | char command[128]; 50 | } 51 | ArrayList g_extraItem; 52 | 53 | // 友伤系数 54 | ConVar cv_thFactor[4]; 55 | // 武器备弹量设置 56 | ConVar cv_ammoInfinite; // 无限弹药无需换弹 57 | ConVar cv_ammoMax[7]; // 最大弹药量 58 | char cvar_ammoMax[][] = { // 控制台变量名 59 | "ammo_shotgun_max", // 单喷 60 | "ammo_autoshotgun_max", // 连喷 61 | "ammo_assaultrifle_max", // 步枪 62 | "ammo_grenadelauncher_max", // 榴弹 63 | "ammo_huntingrifle_max", // 连狙 64 | "ammo_sniperrifle_max", // 狙击 65 | "ammo_smg_max" // 冲锋枪 66 | }; 67 | 68 | public void OnPluginStart() 69 | { 70 | cv_svmaxplayers = FindConVar("sv_maxplayers"); 71 | cv_smVoteDelay = FindConVar("sm_vote_delay"); 72 | cv_ammoInfinite = FindConVar("sv_infinite_ammo"); // 该变量为1时 主副武器开枪均不消耗子弹 73 | for (int i=0; i<7; i++) 74 | { 75 | cv_ammoMax[i] = FindConVar(cvar_ammoMax[i]); 76 | cv_ammoMax[i].SetBounds(ConVarBound_Upper, true, 9999.0); 77 | } 78 | cv_thFactor[0] = FindConVar("survivor_friendly_fire_factor_easy"); 79 | cv_thFactor[1] = FindConVar("survivor_friendly_fire_factor_normal"); 80 | cv_thFactor[2] = FindConVar("survivor_friendly_fire_factor_hard"); 81 | cv_thFactor[3] = FindConVar("survivor_friendly_fire_factor_expert"); 82 | cv_adminPass = CreateConVar("l4d2_votes_admin_pass", "1", "管理员发起的指令无需进行投票", _, true, 0.0, true, 1.0); 83 | cv_voteTime = CreateConVar("l4d2_votes_time", "20", "投票应在多少秒内完成?", _, true, 10.0, true, 60.0); 84 | cv_voteDelay = CreateConVar("l4d2_votes_delay", "10", "玩家需要等待多久才能再次发起投票?将一直锁定sm_vote_delay为本变量的值。", _, true, 0.0); 85 | cv_ammoMode = CreateConVar("l4d2_votes_ammomode", "1", "多倍弹药模式 -1:完全禁用 0:禁用多倍但允许投票补满所有人弹药 1:一倍且允许开启多倍弹药 2:双倍 3:三倍 4:无限(需换弹) 5:无限(无需换弹)", _, true, -1.0, true, 5.0); 86 | cv_autoLaser = CreateConVar("l4d2_votes_autolaser", "0", "自动获得红外 -1:完全禁用 0:关闭 1:开启", _, true, -1.0, true, 1.0); 87 | cv_teamHurt = CreateConVar("l4d2_votes_teamhurt", "0", "是否允许投票改变友伤系数 -1:不允许 0:允许", _, true, -1.0, true, 0.0); 88 | cv_restartChapter = CreateConVar("l4d2_votes_restartchapter", "0", "是否允许投票重启当前章节 -1:不允许 0:允许", _, true, -1.0, true, 0.0); 89 | cv_players = CreateConVar("l4d2_votes_players", "8", "服务器人数,若为0则不改变人数。游戏时改变本参数是无效的。", _, true, 0.0, true, 32.0); 90 | cv_playersLower = CreateConVar("l4d2_votes_players_lower", "4", "投票更改服务器人数的下限", _, true, 1.0, true, 32.0); 91 | cv_playersUpper = CreateConVar("l4d2_votes_players_upper", "12", "投票更改服务器人数的上限。若下限>=上限,则不允许投票更改服务器人数。(这不影响本插件更改默认人数)", _, true, 1.0, true, 32.0); 92 | cv_returnBlood = CreateConVar("ReturnBlood", "0", "特感击杀回血总开关 -1:完全禁用 0:关闭 1:开启", _, true, -1.0, true, 1.0); 93 | cv_specialReturn = CreateConVar("l4d2_votes_returnblood_special", "2", "击杀一只特感回多少血", _, true, 0.0, true, 100.0); 94 | cv_witchReturn = CreateConVar("l4d2_votes_returnblood_witch", "10", "击杀一只Witch回多少血", _, true, 0.0, true, 100.0); 95 | cv_healthLimit = CreateConVar("l4d2_votes_returnblood_limit", "100", "最高回血上限。(仅影响回血时的上限,不影响其它情况下的血量上限)", _, true, 40.0, true, 500.0); 96 | cv_restore = CreateConVar("l4d2_votes_changelevel_restore", "1", "更换地图后重置多倍弹药、自动红外、玩家人数、击杀回血、友伤系数(正常过关不会重置)", _, true, 0.0, true, 1.0); 97 | AutoExecConfig(true, "l4d2_votes"); 98 | cv_ammoMode.AddChangeHook(AmmoModeChanged); 99 | cv_autoLaser.AddChangeHook(AutoLaserChanged); 100 | cv_voteDelay.AddChangeHook(VoteDelayChanged); 101 | cv_smVoteDelay.AddChangeHook(VoteDelayChanged); 102 | HookEvent("player_death", Event_player_death, EventHookMode_Post); 103 | HookEvent("weapon_reload", Event_weapon_reload, EventHookMode_Post); // 玩家换弹 104 | HookEvent("map_transition", Event_map_transition, EventHookMode_PostNoCopy); 105 | AddNormalSoundHook(OnNormalSound); // 挂钩声音 106 | 107 | RegServerCmd("l4d2_votes_additem", Cmd_additem, "给投票菜单增加选项"); 108 | RegServerCmd("l4d2_votes_removeitem", Cmd_removeitem, "移除额外的添加项"); 109 | RegConsoleCmd("sm_votes", Cmd_votes, "多功能投票菜单"); 110 | 111 | g_zombieClassOffset = FindSendPropInfo("CTerrorPlayer", "m_zombieClass"); 112 | g_extraItem = new ArrayList(sizeof(ExtraItem)); 113 | // 防止游戏中途加载插件时无法正常触发自动红外 114 | for (int i=1; i<=MaxClients; i++) 115 | { 116 | if (IsClientInGame(i)) 117 | { 118 | SDKHook(i, SDKHook_WeaponEquipPost, OnWeaponEquipPost); 119 | SDKHook(i, SDKHook_WeaponDropPost, OnWeaponDropPost); 120 | } 121 | } 122 | } 123 | 124 | public Action Event_map_transition(Event event, const char[] name, bool dontBroadcast) 125 | { 126 | g_hasMapTransitioned = true; 127 | } 128 | public void OnMapStart() 129 | { 130 | g_ammoMode = cv_ammoMode.IntValue; 131 | g_autoLaser = cv_autoLaser.IntValue; 132 | g_players = cv_players.IntValue; 133 | g_returnBlood = cv_returnBlood.IntValue; 134 | } 135 | 136 | bool g_isValidConVar = false; 137 | public void OnConfigsExecuted() 138 | { 139 | if ((g_hasMapTransitioned || !cv_restore.BoolValue) 140 | && g_isValidConVar) 141 | { 142 | // 如果是正常过关,或者设定更换地图不重置功能开关,则还原为之前的功能设置 143 | cv_ammoMode.IntValue = g_ammoMode; 144 | cv_autoLaser.IntValue = g_autoLaser; 145 | cv_players.IntValue = g_players; 146 | cv_returnBlood.IntValue = g_returnBlood; 147 | } 148 | else 149 | { 150 | // 否则,则不保留之前的设置 151 | for (int i=0; i<4; i++) 152 | cv_thFactor[i].RestoreDefault(); 153 | } 154 | g_hasMapTransitioned = false; 155 | g_isValidConVar = true; 156 | 157 | if (cv_players.IntValue > 0 && null != cv_svmaxplayers) 158 | cv_svmaxplayers.IntValue = cv_players.IntValue; 159 | cv_smVoteDelay.IntValue = cv_voteDelay.IntValue; 160 | } 161 | 162 | public void VoteDelayChanged(ConVar convar, const char[] oldValue, const char[] newValue) 163 | { 164 | cv_smVoteDelay.IntValue = cv_voteDelay.IntValue; 165 | } 166 | 167 | public Action Event_player_death(Event event, const char[] name, bool dontBroadcast) 168 | { 169 | // 特感击杀回血 170 | if (1 == cv_returnBlood.IntValue) 171 | { 172 | int victim = GetClientOfUserId(event.GetInt("userid")); 173 | int attacker = GetClientOfUserId(event.GetInt("attacker")); 174 | // 被攻击者必须是有效特感 攻击者必须是有效生还者并且没有倒地 175 | if (IsValidClient(victim) && GetClientTeam(victim) == 3 176 | && IsValidClient(attacker) && GetClientTeam(attacker) == 2 177 | && !IsIncapacitated(attacker) ) 178 | { 179 | int surHealth = GetHealth(attacker); 180 | int zombieClass = GetEntData(victim, g_zombieClassOffset); // 获取特感类型 181 | if (zombieClass >= 1 && zombieClass <= 6 182 | && cv_specialReturn.IntValue > 0) // 如果是普通特感 183 | surHealth += cv_specialReturn.IntValue; 184 | else if (7 == zombieClass 185 | && cv_witchReturn.IntValue > 0) // 如果是witch 186 | surHealth += cv_witchReturn.IntValue; 187 | else 188 | return Plugin_Continue; 189 | if (surHealth > cv_healthLimit.IntValue) 190 | surHealth = cv_healthLimit.IntValue; 191 | SetHealth(attacker, surHealth); 192 | } 193 | } 194 | return Plugin_Continue; 195 | } 196 | 197 | // 添加额外的选项 198 | public Action Cmd_additem(int args) 199 | { 200 | if (args != 3) 201 | { 202 | PrintToServer("l4d2_votes_additem 是否是服务器指令[0或1] 选项名 指令"); 203 | return Plugin_Handled; 204 | } 205 | char buffer[5]; 206 | ExtraItem item; 207 | 208 | GetCmdArg(1, buffer, sizeof(buffer)); 209 | if (strcmp(buffer, "0") == 0) 210 | item.isServerCommand = false; 211 | else 212 | item.isServerCommand = true; 213 | GetCmdArg(2, item.display, sizeof(item.display)); 214 | GetCmdArg(3, item.command, sizeof(item.command)); 215 | int idx = FindItem(item.display); 216 | if (-1 == idx) 217 | g_extraItem.PushArray(item); 218 | else 219 | g_extraItem.SetArray(idx, item); 220 | 221 | return Plugin_Handled; 222 | } 223 | // 移除额外的添加项 224 | public Action Cmd_removeitem(int args) 225 | { 226 | if (args != 1) 227 | { 228 | PrintToServer("l4d2_votes_removeitem 选项名"); 229 | return Plugin_Handled; 230 | } 231 | char display[64]; 232 | GetCmdArg(1, display, sizeof(display)); 233 | int idx = FindItem(display); 234 | if (-1 == idx) 235 | PrintToServer("l4d2_votes_removeitem 未找到选项:%s", display); 236 | else 237 | g_extraItem.Erase(idx); 238 | return Plugin_Handled; 239 | } 240 | int FindItem(const char[] itemDisplay) 241 | { 242 | ExtraItem item; 243 | int len = g_extraItem.Length; 244 | for (int i=0; i cv_playersLower.IntValue 262 | && cv_svmaxplayers != null) 263 | menu.AddItem("Menu_MaxPlayers", "服务器人数"); 264 | if (0 == cv_teamHurt.IntValue) 265 | menu.AddItem("Menu_TeamHurt", "友伤设置"); 266 | 267 | if (0 == cv_ammoMode.IntValue) 268 | menu.AddItem("Vote_GiveAmmo", "补满弹药"); 269 | else if (1 <= cv_ammoMode.IntValue) 270 | menu.AddItem("Menu_Ammo", "弹药设置"); 271 | 272 | if (0 == cv_autoLaser.IntValue) 273 | menu.AddItem("Vote_AutoLaser_1", "开启自动红外"); 274 | else if (1 == cv_autoLaser.IntValue) 275 | menu.AddItem("Vote_AutoLaser_0", "关闭自动红外"); 276 | 277 | if (0 == cv_returnBlood.IntValue) 278 | menu.AddItem("Vote_ReturnBlood_1", "开启击杀回血"); 279 | else if (1 == cv_returnBlood.IntValue) 280 | menu.AddItem("Vote_ReturnBlood_0", "关闭击杀回血"); 281 | 282 | if (0 == cv_restartChapter.IntValue) 283 | menu.AddItem("Vote_Restart", "重启当前章节"); 284 | 285 | // 额外添加项 286 | int len = g_extraItem.Length; 287 | ExtraItem item; 288 | char buffer[20]; 289 | for (int i=0; i=2 && mode<=3) // 2倍、3倍弹药 613 | { 614 | char buffer[10]; 615 | // 设置的新弹药量 616 | for (int i=0; i<7; i++) 617 | { 618 | cv_ammoMax[i].GetDefault(buffer, sizeof(buffer)); 619 | cv_ammoMax[i].IntValue = StringToInt(buffer) * mode; 620 | } 621 | } 622 | 623 | if (mode > 0) 624 | GiveSurvivorsAmmo(); 625 | } 626 | // 补满生还者弹药 627 | void GiveSurvivorsAmmo() 628 | { 629 | for (int i=1; i<=MaxClients; i++) 630 | { 631 | if (IsClientInGame(i) && GetClientTeam(i) == 2) 632 | CheatCommand(i, "give", "ammo"); 633 | } 634 | } 635 | 636 | // 玩家换弹 自动补充弹药 637 | bool g_mute = false; 638 | public Action Event_weapon_reload(Event event, const char[] name, bool dontBroadcast) 639 | { 640 | if (4 == cv_ammoMode.IntValue) 641 | { 642 | int client = GetClientOfUserId(event.GetInt("userid")); 643 | if (IsValidClient(client) && GetClientTeam(client) == 2) 644 | { 645 | // 每次给予弹药都会出现拾物音效 会比较吵 所以在给予弹药时屏蔽下一次的拾物音效 646 | g_mute = true; 647 | CheatCommand(client, "give", "ammo"); 648 | CreateTimer(0.01, Delay_SetMute); // 防止备弹已满,给予弹药时未触发拾物音效而导致g_mute一直滞留为true 649 | } 650 | } 651 | } 652 | public Action OnNormalSound(int clients[65], int &numClients, char sample[256], int &entity, int &channel, float &volume, int &level, int &pitch, int &flags, char soundEntry[256], int &seed) 653 | { 654 | // 屏蔽自动给弹药时的拾物音效 655 | // 不检查产生拾物音效的动作,一次屏蔽是针对所有人的屏蔽,一般不会出现什么问题 656 | // 因为很难出现同一tick中 一人换弹一人刚好在拾取物品 657 | // 就算出现了也不过是听不到这次的声音 也没啥大不了的 ╮(╯▽╰)╭ 658 | if (g_mute && StrContains(sample, "items/itempickup.wav")!=-1) 659 | { 660 | g_mute = false; 661 | return Plugin_Handled; 662 | } 663 | return Plugin_Continue; 664 | } 665 | public Action Delay_SetMute(Handle timer) 666 | { 667 | g_mute = false; 668 | } 669 | 670 | // 自动获得红外 671 | #define UPGRADE_LASER (1 << 2) // 100b 672 | public void AutoLaserChanged(ConVar convar, const char[] oldValue, const char[] newValue) 673 | { 674 | if (1 == cv_autoLaser.IntValue) 675 | { 676 | for (int i=1; i<=MaxClients; i++) 677 | { 678 | if (IsClientInGame(i) && GetClientTeam(i) == 2) 679 | SetWeaponLaser(GetPlayerWeaponSlot(i, 0)); 680 | } 681 | } 682 | } 683 | 684 | public void OnClientPutInServer(int client) 685 | { 686 | if (IsValidClient(client)) 687 | { 688 | SDKHook(client, SDKHook_WeaponEquipPost, OnWeaponEquipPost); 689 | SDKHook(client, SDKHook_WeaponDropPost, OnWeaponDropPost); 690 | } 691 | } 692 | public void OnClientDisconnect(int client) 693 | { 694 | if (IsValidClient(client)) 695 | { 696 | SDKUnhook(client, SDKHook_WeaponEquipPost, OnWeaponEquipPost); 697 | SDKUnhook(client, SDKHook_WeaponDropPost, OnWeaponDropPost); 698 | } 699 | } 700 | 701 | public Action OnWeaponEquipPost(int client, int weapon) 702 | { 703 | if (1 == cv_autoLaser.IntValue) 704 | { 705 | if (IsValidClient(client) && GetClientTeam(client) == 2) 706 | SetWeaponLaser(weapon); 707 | } 708 | } 709 | 710 | // 武器丢掉时去除红外,如果不去除武器多了满地都是红外 711 | public Action OnWeaponDropPost(int client, int weapon) 712 | { 713 | if (1 == cv_autoLaser.IntValue) 714 | SetWeaponLaser(weapon, true); 715 | } 716 | 717 | void SetWeaponLaser(int weapon, bool remove=false) 718 | { 719 | if (IsValidEntity(weapon) && HasEntProp(weapon, Prop_Send, "m_upgradeBitVec")) 720 | { 721 | int flags = GetEntProp(weapon, Prop_Send, "m_upgradeBitVec"); 722 | if (!(flags & UPGRADE_LASER) && !remove) // 如果没有红外,且要求获得 723 | SetEntProp(weapon, Prop_Send, "m_upgradeBitVec", flags | UPGRADE_LASER); 724 | else if ((flags & UPGRADE_LASER) && remove) // 如果有红外,且要求去除 725 | SetEntProp(weapon, Prop_Send, "m_upgradeBitVec", flags & (~UPGRADE_LASER)); 726 | } 727 | } -------------------------------------------------------------------------------- /l4d2_wpgive/plugins/l4d2_wpgive.smx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Lin515/L4D_LinGe_Plugins/3abfac98dd1922e9853441904d80509c01f9d064/l4d2_wpgive/plugins/l4d2_wpgive.smx -------------------------------------------------------------------------------- /l4d2_wpgive/scripting/l4d2_wpgive.sp: -------------------------------------------------------------------------------- 1 | #pragma semicolon 1 2 | #pragma newdecls required 3 | #include 4 | #include 5 | 6 | public Plugin myinfo = { 7 | name = "!wpgive 获取武器", 8 | author = "LinGe", 9 | description = "!wpgive 获取武器", 10 | version = "0.1", 11 | url = "https://github.com/Lin515/L4D_LinGe_Plugins" 12 | }; 13 | 14 | public APLRes AskPluginLoad2(Handle myself, bool late, char[] error, int err_max) 15 | { 16 | EngineVersion game = GetEngineVersion(); 17 | if (game!=Engine_Left4Dead2) 18 | { 19 | strcopy(error, err_max, "本插件只支持 Left 4 Dead 2."); 20 | return APLRes_SilentFailure; 21 | } 22 | return APLRes_Success; 23 | } 24 | 25 | public void OnPluginStart() 26 | { 27 | RegConsoleCmd("sm_wpgive", Cmd_wpgive, "物品获取菜单"); 28 | } 29 | 30 | public Action Cmd_wpgive(int client, int args) 31 | { 32 | if (0 == client) 33 | return Plugin_Handled; 34 | 35 | Menu menu = new Menu(GiveMenu_Selected); 36 | menu.SetTitle("武器获取"); 37 | menu.AddItem("give_melee", "近战"); 38 | menu.AddItem("give_shotgun", "霰弹枪"); 39 | menu.AddItem("give_smg", "冲锋枪"); 40 | menu.AddItem("give_rifle", "步枪"); 41 | menu.AddItem("give_sniper", "狙击枪"); 42 | menu.AddItem("give_other", "其它武器"); 43 | menu.Display(client, MENU_TIME_FOREVER); 44 | 45 | return Plugin_Handled; 46 | } 47 | 48 | public int GiveMenu_Selected(Menu menu, MenuAction action, int client, int curSel) 49 | { 50 | switch (action) 51 | { 52 | case MenuAction_End: 53 | { 54 | delete menu; 55 | } 56 | case MenuAction_Select: 57 | { 58 | char info[128]; 59 | menu.GetItem(curSel, info, sizeof(info)); 60 | Menu_Exec(client, info); 61 | } 62 | } 63 | } 64 | 65 | void Menu_Exec(int client, const char[] str) 66 | { 67 | Menu menu = new Menu(GiveTowMenu_Selected); 68 | if (strcmp(str, "give_melee") == 0) 69 | { 70 | menu.AddItem("knife", "小刀"); 71 | menu.AddItem("machete", "砍刀"); 72 | menu.AddItem("katana", "武士刀"); 73 | menu.AddItem("baseball_bat", "棒球棍"); 74 | menu.AddItem("cricket_bat", "板球棍"); 75 | menu.AddItem("fireaxe", "斧头"); 76 | menu.AddItem("frying_pan", "平底锅"); 77 | menu.AddItem("crowbar", "撬棍"); 78 | menu.AddItem("electric_guitar", "吉他"); 79 | menu.AddItem("tonfa", "警棍"); 80 | menu.AddItem("pitchfork", "草叉"); 81 | menu.AddItem("shovel", "铁锹"); 82 | menu.AddItem("weapon_chainsaw", "电锯"); 83 | menu.SetTitle("近战"); 84 | } 85 | else if (strcmp(str, "give_shotgun") == 0) 86 | { 87 | menu.AddItem("pumpshotgun", "M870"); 88 | menu.AddItem("shotgun_chrome", "Chrome"); 89 | menu.AddItem("autoshotgun", "M1014"); 90 | menu.AddItem("shotgun_spas", "SPAS"); 91 | menu.SetTitle("霰弹枪"); 92 | } 93 | else if (strcmp(str, "give_smg") == 0) 94 | { 95 | menu.AddItem("smg", "UZI"); 96 | menu.AddItem("smg_silenced", "MAC"); 97 | menu.AddItem("weapon_smg_mp5", "MP5"); 98 | menu.SetTitle("冲锋枪"); 99 | } 100 | else if (strcmp(str, "give_rifle") == 0) 101 | { 102 | menu.AddItem("rifle_ak47", "AK47"); 103 | menu.AddItem("rifle", "M16"); 104 | menu.AddItem("rifle_desert", "SCAR"); 105 | menu.AddItem("weapon_rifle_sg552", "SG552"); 106 | menu.SetTitle("步枪"); 107 | } 108 | else if (strcmp(str, "give_sniper") == 0) 109 | { 110 | menu.AddItem("hunting_rifle", "M14"); 111 | menu.AddItem("sniper_military", "G3SG1"); 112 | menu.AddItem("weapon_sniper_scout", "Scout"); 113 | menu.AddItem("weapon_sniper_awp", "AWP"); 114 | menu.SetTitle("狙击枪"); 115 | } 116 | else if (strcmp(str, "give_other") == 0) 117 | { 118 | menu.AddItem("pistol", "普通手枪"); 119 | menu.AddItem("pistol_magnum", "马格南"); 120 | menu.AddItem("weapon_grenade_launcher", "榴弹"); 121 | menu.AddItem("rifle_m60", "M60"); 122 | menu.SetTitle("其它武器"); 123 | } 124 | else 125 | { 126 | delete menu; 127 | return; 128 | } 129 | menu.ExitBackButton = true; 130 | menu.Display(client, MENU_TIME_FOREVER); 131 | } 132 | 133 | public int GiveTowMenu_Selected(Menu menu, MenuAction action, int client, int curSel) 134 | { 135 | switch (action) 136 | { 137 | case MenuAction_End: 138 | { 139 | delete menu; 140 | } 141 | case MenuAction_Cancel: 142 | { 143 | if( curSel == MenuCancel_ExitBack ) 144 | Cmd_wpgive(client, 0); 145 | } 146 | case MenuAction_Select: 147 | { 148 | char info[128]; 149 | menu.GetItem(curSel, info, sizeof(info)); 150 | CheatCommand(client, "give", info); 151 | } 152 | } 153 | } -------------------------------------------------------------------------------- /l4d_mapvote/README.md: -------------------------------------------------------------------------------- 1 | # 求生之路1&2 投票换图插件 2 | 3 | 适用于服务端的投票换图插件,可以用来更换三方图,地图需自己在**data/l4d_mapvote.cfg**中添加。 4 | 投票模式有菜单投票和游戏内置投票两种模式。 5 | 插件编写参考了SilverShot的l4d_votemode。 6 | 7 | **支持游戏:求生之路1&2**(实际上游戏1代我没测试过,应该也是支持的) 8 | **建议插件平台:SourceMod 1.10.0** 9 | **依赖:[builtinvotes](https://github.com/A1mDev/builtinvotes)** 如果你不使用游戏内置投票则可以不安装这个扩展。 10 | 11 | ## 指令与功能 12 | 13 | - **!mapvote** 14 | 打开投票地图菜单。非管理员也可以使用。 15 | - **!mapveto** 16 | 直接否定当前的投票。仅具有ROOT权限管理员可用。 17 | - **!mappass** 18 | 直接通过当前的投票。仅具有ROOT权限管理员可用。 19 | - **!mapchange** 20 | 打开更换地图菜单,使用此指令换图无需经过投票。仅具有ROOT权限管理员可用。 21 | 22 | 配置文件cfg/sourcemod/l4d_mapvote.cfg中可更改投票持续时间、更换地图延时、地图检查开关、投票模式。 23 | 24 | ## 游戏模式检测 25 | 26 | 本插件可以根据当前的基础游戏模式来完成一些特殊的动作。 27 | 28 | ### 定义地图列表游戏模式 29 | 30 | 在data/l4d_mapvote.cfg写入地图时,在一组地图列表的第一项写入: 31 | 32 | > "\_BASEMODE\_" "1" 33 | 34 | 这样在游戏中,当前游戏模式若是基于战役模式,打开地图列表就会直接显示该组地图。 35 | \_BASEMODE\_可以设置为1~4,具体含义如下: 36 | 37 | 1. 基于战役模式 38 | 2. 基于对抗模式 39 | 3. 基于生还者模式 40 | 4. 基于清道夫模式 41 | 42 | 多组地图列表中设置\_BASEMODE\_为同一值,只有最后一个有效。 43 | 44 | ### 更改游戏模式 45 | 46 | 如果你允许插件更改游戏模式,那么当更换指定地图的模式与当前游戏模式不符时,插件会同时更改游戏模式和地图。 47 | 比如在基于战役的模式下更换为基于对抗地图列表中的地图,插件会将游戏模式修改为普通对抗模式。 -------------------------------------------------------------------------------- /l4d_mapvote/data/l4d_mapvote.cfg: -------------------------------------------------------------------------------- 1 | "定义地图列表适用的游戏基础模式,请在第一项设置key=_BASEMODE_ value=1:基于战役 2:基于对抗 3:基于生还者 4:基于清道夫" 2 | { 3 | "战役地图" 4 | { 5 | "_BASEMODE_" "1" 6 | "7小时以后" "l4d2_7hours_later_01" 7 | "84警区" "p84m1_apartment" 8 | "2019 经典版" "2019_M1o" 9 | "Chernobyl:Chapter One" "ch01_jupiter" 10 | "ONE 4 NINE" "l4d_149_1" 11 | "Warcelona" "srocchurch" 12 | "Z-PTZ" "appartments" 13 | "白森林" "wfp1_track" 14 | "暴毙峡谷" "ddg1_tower_v2_1" 15 | "别被丢下" "bhm1_outskirts" 16 | "颤栗森林" "hf01_theforest" 17 | "城市17" "l4d2_city17_01" 18 | "城市航班" "uf1_boulevard" 19 | "赤潮" "redtide_01_alleyways" 20 | "大坝洪水" "l4d_damit01_orchard" 21 | "逮虾户:战术间谍行动" "r_concert" 22 | "地狱之路" "highway01_apt_20130613" 23 | "喋血蜃楼" "l4d2_diescraper1_apartment_361" 24 | "堕落2019" "l4d_fallen01_approach" 25 | "返校" "l4d2_bts01_forest" 26 | "飞溅山之旅" "splash1" 27 | "封锁" "bt1" 28 | "古惑狼 2013" "CrashBandicootMap1_classic" 29 | "古惑狼 2020" "CrashBandicootMap1" 30 | "广州增城" "1_m1" 31 | "黑暗森林" "dw_woods" 32 | "户外坟墓" "l4d_grave_city" 33 | "灰怆" "city" 34 | "火星基地" "l4d_arrival" 35 | "寂静岭" "l4d_sh01_oldsh" 36 | "寂静岭:另一边的生活" "sh_01" 37 | "寂静岭3:重回" "silenthill3mall" 38 | "交通大拥堵" "gridlockfinal2" 39 | "救赎2" "redemptionii-deadstop" 40 | "绝对零度" "l4d_zero01_south" 41 | "军事工厂2" "l4d_MIC2_TrapmentD" 42 | "尻名山" "cnm1_city1" 43 | "恐怖之旅" "eu01_residential_b16" 44 | "黎明" "l4d2_daybreak01_hotel" 45 | "连续死亡" "Dead_Series1" 46 | "伦理问题" "qe_1_cliche" 47 | "伦理问题:阿尔法测试" "qe2_ep1" 48 | "马里奥" "c1_mario1_1" 49 | "摩耶山危机" "l4d_yama_1" 50 | "能源危机" "ec01_outlets" 51 | "去年夏天" "campanar_coop_vs" 52 | "诺瓦矿场" "nova_first" 53 | "闪电突袭2" "l4d2_stadium1_apartment" 54 | "深埋" "bdp_bunker01" 55 | "生还之锋" "fos01_roadonhill" 56 | "死城2" "l4d2_deadcity01_riverside" 57 | "死期将至" "l4d2_timemachine_01" 58 | "死亡度假" "hotel01_market_two" 59 | "死亡回声2" "de01_sewers" 60 | "死亡陷阱" "ddntr1_01urban" 61 | "死亡宣判" "death_sentence_1" 62 | "泰坦尼克" "rmstitanic_m1" 63 | "逃离瓦伦西亚" "quart" 64 | "天堂可待2" "aircrash" 65 | "万里长城 方氏" "ch_map1_city" 66 | "万圣节主题公园" "pk_loony" 67 | "维也纳的呼唤" "l4d_viennacalling_city" 68 | "维也纳的呼唤2" "l4d_viennacalling2_1" 69 | "瘟疫传说" "pt_home1" 70 | "我的世界" "l4d2_deathcraft_01_town" 71 | "我恨山2" "l4d_ihm01_forest" 72 | "邪恶双眸" "2ee_01" 73 | "血腥荒野" "l4d_tbm_1" 74 | "血之轨迹" "bloodtracks_01" 75 | "盐井地狱公园" "saltwell_1_d" 76 | "夜惊" "nt01_mansion" 77 | "阴霾将至" "bjrtc2_lvl1" 78 | "幽闭恐惧症" "claustrophobia1" 79 | "幽灵船2" "l4d_deathaboard01_prison" 80 | "迂回前进" "cdta_01detour" 81 | "再见了晨茗" "msd1_town" 82 | "治愈2" "Cure2_001" 83 | "致命货运站" "l4d2_ff01_woods" 84 | "坠入死亡" "l4d2_fallindeath01" 85 | } 86 | "对抗地图" 87 | { 88 | "_BASEMODE_" "2" 89 | "84警区" "p84m1_apartment" 90 | "2019 经典版" "2019_M1o" 91 | "ONE 4 NINE" "l4d_149_1" 92 | "Warcelona" "srocchurch" 93 | "白森林" "wfp1_track" 94 | "暴毙峡谷" "ddg1_tower_v2_1" 95 | "别被丢下" "bhm1_outskirts" 96 | "颤栗森林" "hf01_theforest" 97 | "城市17" "l4d2_city17_01" 98 | "城市航班" "uf1_boulevard" 99 | "赤潮" "redtide_01_alleyways" 100 | "大坝洪水" "l4d_damit01_orchard" 101 | "地狱之路" "highway01_apt_20130613" 102 | "喋血蜃楼" "l4d2_diescraper1_apartment_361" 103 | "返校" "l4d2_bts01_forest" 104 | "古惑狼 2013" "CrashBandicootMap1_classic" 105 | "古惑狼 2020" "CrashBandicootMap1" 106 | "黑暗森林" "dw_woods" 107 | "交通大拥堵" "gridlockfinal2" 108 | "绝对零度" "l4d_zero01_south" 109 | "恐怖之旅" "eu01_residential_b16" 110 | "黎明" "l4d2_daybreak01_hotel" 111 | "连续死亡" "Dead_Series1" 112 | "马里奥" "c1_mario1_1" 113 | "摩耶山危机" "l4d_yama_1" 114 | "能源危机" "ec01_outlets" 115 | "去年夏天" "campanar_coop_vs" 116 | "诺瓦矿场" "nova_first" 117 | "闪电突袭2" "l4d2_stadium1_apartment" 118 | "深埋" "bdp_bunker01" 119 | "死城2" "l4d2_deadcity01_riverside" 120 | "死亡度假" "hotel01_market_two" 121 | "死亡回声2" "de01_sewers" 122 | "死亡宣判" "death_sentence_1" 123 | "泰坦尼克" "rmstitanic_m1" 124 | "逃离瓦伦西亚" "quart" 125 | "天堂可待2" "aircrash" 126 | "万里长城 方氏" "ch_map1_city" 127 | "我恨山2" "l4d_ihm01_forest" 128 | "邪恶双眸" "2ee_01" 129 | "血腥荒野" "l4d_tbm_1" 130 | "血之轨迹" "bloodtracks_01" 131 | "幽灵船2" "l4d_deathaboard01_prison" 132 | "迂回前进" "cdta_01detour" 133 | "致命货运站" "l4d2_ff01_woods" 134 | "坠入死亡" "l4d2_fallindeath01" 135 | } 136 | } -------------------------------------------------------------------------------- /l4d_mapvote/plugins/l4d_mapvote.smx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Lin515/L4D_LinGe_Plugins/3abfac98dd1922e9853441904d80509c01f9d064/l4d_mapvote/plugins/l4d_mapvote.smx -------------------------------------------------------------------------------- /l4d_mapvote/scripting/l4d_mapvote.sp: -------------------------------------------------------------------------------- 1 | // 本插件编写参考 SilverShot l4d_votemode.sp 2 | 3 | #pragma semicolon 1 4 | #pragma newdecls required 5 | 6 | #include 7 | #include 8 | #include 9 | #undef REQUIRE_EXTENSIONS 10 | #include 11 | #define REQUIRE_EXTENSIONS 12 | 13 | #define CONFIG_MAPVOTE "data/l4d_mapvote.cfg" 14 | 15 | public Plugin myinfo = { 16 | name = "[L4D & L4D2] Map Vote", 17 | author = "LinGe, SilverShot", 18 | description = "投票换图", 19 | version = "2.2", 20 | url = "https://github.com/Lin515/L4D_LinGe_Plugins" 21 | }; 22 | 23 | TopMenu g_hCvarMenuMenu; 24 | 25 | ConVar cv_l4dGamemode; 26 | ConVar cv_allowChangeMode; // 是否允许插件改变游戏模式 27 | ConVar cv_useBuiltinvotes; // 是否使用扩展发起原版投票开关 28 | ConVar cv_mapCheck; // 地图检查开关 29 | ConVar cv_voteTime; // 投票持续时间 30 | ConVar cv_mapchangeDelay; // 地图更换延时 31 | 32 | bool g_isBuiltinvotesLoaded = false; // Builtinvotes扩展是否加载成功 33 | 34 | int g_isMapCheck = -1; 35 | bool g_useBuiltinvotes = false; // 当前插件是否使用原版投票 36 | bool g_allowMapChange = true; // 当前是否允许插件发起地图更改 37 | bool g_isMapChange[MAXPLAYERS+1]; // 记录本次指令是否是mapchange 38 | 39 | BaseMode g_baseMode = INVALID; 40 | ArrayList g_modeClass; // 存储对应模式的地图列表索引 41 | int g_selected[MAXPLAYERS+1]; 42 | char g_mapCodes[1024][64], g_mapNames[1024][64]; 43 | int g_mapCount; 44 | char g_mapClass[64][64]; 45 | int g_classCount, g_mapIndex[64]; 46 | 47 | bool g_isAdminPass; // 在提前取消投票的时候判断是否是管理员直接通过了本次投票 48 | 49 | public APLRes AskPluginLoad2(Handle myself, bool late, char[] error, int err_max) 50 | { 51 | EngineVersion game = GetEngineVersion(); 52 | if (game!=Engine_Left4Dead && game!=Engine_Left4Dead2) 53 | { 54 | strcopy(error, err_max, "本插件只支持 Left 4 Dead 1&2 "); 55 | // 在1代没经过测试,懒得测试,应该也是支持的 56 | return APLRes_SilentFailure; 57 | } 58 | __ext_builtinvotes_SetNTVOptional(); 59 | MarkNativeAsOptional("CancelBuiltinVote"); 60 | return APLRes_Success; 61 | } 62 | 63 | public void OnPluginStart() 64 | { 65 | RegAdminCmd("sm_mapveto", CommandVeto, ADMFLAG_ROOT, "管理员否决本次投票"); 66 | RegAdminCmd("sm_mappass", CommandPass, ADMFLAG_ROOT, "管理员通过本次投票"); 67 | RegAdminCmd("sm_mapchange", CommandChange, ADMFLAG_ROOT, "管理员直接更换地图无需经过投票"); 68 | RegConsoleCmd("sm_mapvote", CommandVote, "打开投票换图菜单"); 69 | 70 | cv_l4dGamemode = FindConVar("mp_gamemode"); 71 | cv_allowChangeMode = CreateConVar("l4d_mapvote_allow_changemode", "1", "当要更换的地图指定模式与当前游戏模式不符时,是否允许插件改变为指定游戏模式", _, true, 0.0, true, 1.0); 72 | cv_useBuiltinvotes = CreateConVar("l4d_mapvote_use_builtinvotes", "1", "是否使用builtinvotes扩展发起原版投票?开启情况下,若扩展未正常加载仍会使用sourcemod平台菜单投票。游戏中直接修改这个参数不会生效。", _, true, 0.0, true, 1.0); 73 | cv_mapCheck = CreateConVar("l4d_mapvote_map_check", "1", "地图检查,若开启则不会显示data/l4d_mapvote.cfg中的无效地图。游戏中直接修改这个参数不会生效。(注意:只有服务端才会自动读取addons下的全部三方图,客户端不会。)", _, true, 0.0, true, 1.0); 74 | cv_voteTime = CreateConVar("l4d_mapvote_vote_time", "20", "投票应在多少秒内完成?", _, true, 10.0, true, 60.0); 75 | cv_mapchangeDelay = CreateConVar("l4d_mapvote_mapchange_delay", "5", "地图更换延时", _, true, 0.0, true, 60.0); 76 | AutoExecConfig(true, "l4d_mapvote"); 77 | 78 | Handle topmenu = GetAdminTopMenu(); 79 | if( LibraryExists("adminmenu") && (topmenu != null) ) 80 | OnAdminMenuReady(topmenu); 81 | 82 | if ( GetExtensionFileStatus("builtinvotes.ext") == 1 ) 83 | g_isBuiltinvotesLoaded = true; 84 | 85 | g_modeClass = CreateArray(); 86 | } 87 | 88 | // 不编写游戏中途重新读取地图列表的功能,因为读取量太大时会造成短暂的卡顿 89 | public void OnConfigsExecuted() 90 | { 91 | // 只有当扩展加载成功时才使用原版投票 92 | if (cv_useBuiltinvotes.IntValue == 1 && g_isBuiltinvotesLoaded) 93 | g_useBuiltinvotes = true; 94 | else 95 | g_useBuiltinvotes = false; 96 | g_baseMode = GetBaseMode(); 97 | if (g_isMapCheck != cv_mapCheck.IntValue) 98 | { 99 | g_isMapCheck = cv_mapCheck.IntValue; 100 | LoadConfig(); 101 | } 102 | g_allowMapChange = true; 103 | } 104 | 105 | // ==================================================================================================== 106 | // ADD TO ADMIN MENU 107 | // ==================================================================================================== 108 | public void OnLibraryRemoved(const char[] name) 109 | { 110 | if( strcmp(name, "adminmenu") == 0 ) 111 | g_hCvarMenuMenu = null; 112 | } 113 | 114 | public void OnAdminMenuReady(Handle topmenu) 115 | { 116 | if( topmenu == g_hCvarMenuMenu) 117 | return; 118 | 119 | g_hCvarMenuMenu = view_as(topmenu); 120 | 121 | TopMenuObject player_commands = FindTopMenuCategory(g_hCvarMenuMenu, ADMINMENU_SERVERCOMMANDS); 122 | if( player_commands == INVALID_TOPMENUOBJECT ) return; 123 | 124 | AddToTopMenu(g_hCvarMenuMenu, "sm_changemap_menu", TopMenuObject_Item, Handle_Category, player_commands, "sm_changemap_menu", ADMFLAG_GENERIC); 125 | } 126 | 127 | public int Handle_Category(Handle topmenu, TopMenuAction action, TopMenuObject object_id, int client, char[] buffer, int maxlength) 128 | { 129 | switch( action ) 130 | { 131 | case TopMenuAction_DisplayTitle: 132 | Format(buffer, maxlength, "更换地图"); 133 | case TopMenuAction_DisplayOption: 134 | Format(buffer, maxlength, "更换地图"); 135 | case TopMenuAction_SelectOption: 136 | { 137 | g_isMapChange[client] = true; 138 | VoteMenu_Select(client); 139 | } 140 | } 141 | } 142 | 143 | // ==================================================================================================== 144 | // LOAD CONFIG 145 | // ==================================================================================================== 146 | void LoadConfig() 147 | { 148 | // 获取到配置文件的绝对路径 149 | char sPath[PLATFORM_MAX_PATH]; 150 | BuildPath(Path_SM, sPath, sizeof(sPath), CONFIG_MAPVOTE); 151 | 152 | g_mapCount = 0; 153 | g_classCount = -1; 154 | g_modeClass.Clear(); 155 | for (int i=0; i<5; i++) 156 | g_modeClass.Push(-1); 157 | if (FileExists(sPath)) 158 | ParseConfigFile(sPath); // 读取data/l4d_mapvote.cfg中的地图 159 | else 160 | SetFailState("文件不存在 : %s", sPath); 161 | } 162 | 163 | // 读取配置文件 164 | bool ParseConfigFile(const char[] file) 165 | { 166 | SMCParser parser = new SMCParser(); 167 | SMC_SetReaders(parser, Config_NewSection, Config_KeyValue, Config_EndSection); 168 | parser.OnEnd = Config_End; 169 | 170 | char error[128]; 171 | int line = 0, col = 0; 172 | SMCError result = parser.ParseFile(file, line, col); 173 | delete parser; 174 | 175 | if ( result != SMCError_Okay ) 176 | { 177 | SMC_GetErrorString(result, error, sizeof(error)); 178 | SetFailState("%s on line %d, col %d of %s [%d]", error, line, col, file, result); 179 | } 180 | 181 | return (result == SMCError_Okay); 182 | } 183 | 184 | public SMCResult Config_NewSection(Handle parser, const char[] section, bool quotes) 185 | { 186 | g_classCount++; 187 | if( g_classCount > 0 ) 188 | { 189 | strcopy(g_mapClass[g_classCount-1], 64, section); 190 | g_mapIndex[g_classCount-1] = g_mapCount; 191 | } 192 | return SMCParse_Continue; 193 | } 194 | 195 | public SMCResult Config_KeyValue(Handle parser, const char[] key, const char[] value, bool key_quotes, bool value_quotes) 196 | { 197 | if (StrEqual(key, "_BASEMODE_") 198 | && g_mapIndex[g_classCount-1] == g_mapCount ) 199 | { 200 | int val = StringToInt(value); 201 | if (val >= 1 && val <= 4) 202 | g_modeClass.Set(val, g_classCount-1); 203 | return SMCParse_Continue; 204 | } 205 | if (!IsMapValid(value) && g_isMapCheck == 1) 206 | return SMCParse_Continue; 207 | strcopy(g_mapNames[g_mapCount], 64, key); 208 | strcopy(g_mapCodes[g_mapCount], 64, value); 209 | g_mapCount++; 210 | return SMCParse_Continue; 211 | } 212 | public SMCResult Config_EndSection(Handle parser) 213 | { 214 | g_mapIndex[g_classCount] = g_mapCount; 215 | return SMCParse_Continue; 216 | } 217 | public void Config_End(Handle parser, bool halted, bool failed) 218 | { 219 | if( failed ) 220 | SetFailState("Error: Cannot load the mapvote config"); 221 | } 222 | 223 | 224 | // --------------------------------CMD--------------------------------------- 225 | public Action CommandVeto(int client, int args) 226 | { 227 | if (_IsVoteInProgress()) 228 | { 229 | g_isAdminPass = false; 230 | PrintToChatAll("\x04管理员否决了本次投票"); 231 | _CancelVote(); 232 | } 233 | return Plugin_Handled; 234 | } 235 | 236 | public Action CommandPass(int client, int args) 237 | { 238 | if(_IsVoteInProgress()) 239 | { 240 | g_isAdminPass = true; 241 | PrintToChatAll("\x04管理员通过了本次投票"); 242 | _CancelVote(); 243 | } 244 | return Plugin_Handled; 245 | } 246 | 247 | public Action CommandVote(int client, int args) 248 | { 249 | if (0 == client || args > 0) 250 | return Plugin_Handled; 251 | 252 | if (GetClientTeam(client) == 1) 253 | { 254 | PrintToChat(client, "\x04旁观者不能发起投票"); 255 | return Plugin_Handled; 256 | } 257 | 258 | // 判断是否能发起投票 259 | if(!_IsNewVoteAllowed()) 260 | { 261 | PrintToChat(client, "\x04暂时还不能发起新投票"); 262 | return Plugin_Handled; 263 | } 264 | 265 | g_isMapChange[client] = false; 266 | VoteMenu_Select(client); 267 | return Plugin_Handled; 268 | } 269 | 270 | public Action CommandChange(int client, int args) 271 | { 272 | if (0 == client || args > 0) 273 | return Plugin_Handled; 274 | 275 | g_isMapChange[client] = true; 276 | VoteMenu_Select(client); 277 | return Plugin_Handled; 278 | } 279 | 280 | void VoteMenu_Select(int client, bool all=false) 281 | { 282 | if (g_mapCount == 0) 283 | { 284 | PrintToChat(client, "\x04未读取到地图,请确认\x03 data/l4d_mapvote.cfg \x04配置正确"); 285 | return; 286 | } 287 | if (!g_allowMapChange) 288 | { 289 | PrintToChat(client, "\x04当前不能发起地图更改"); 290 | return; 291 | } 292 | 293 | int val = g_modeClass.Get(view_as(g_baseMode)); 294 | if (val!=-1 && !all) 295 | { 296 | g_selected[client] = val; 297 | VoteTwoMenu_Select(client, g_selected[client]); 298 | } 299 | else 300 | { 301 | Menu menu = new Menu(VoteMenuHandler_Select); 302 | if (g_isMapChange[client]) 303 | menu.SetTitle("更换地图"); 304 | else 305 | menu.SetTitle("投票换图"); 306 | 307 | // Build menu 308 | for (int i=0; i(idx); 392 | // 如果指定模式与当前模式一致则无需更换模式 393 | if (g_newMode == g_baseMode) 394 | g_newMode = INVALID; 395 | } 396 | } 397 | 398 | if ( g_isMapChange[client] ) 399 | { 400 | if (_IsVoteInProgress()) 401 | { 402 | g_isAdminPass = false; 403 | PrintToChatAll("\x04管理员已选择地图,本次投票取消"); 404 | _CancelVote(); 405 | } 406 | ChangeMap(); 407 | } 408 | else 409 | StartVote(client); 410 | } 411 | 412 | // 投票 413 | Handle g_voteExt; 414 | int g_iNumPlayers = 0; 415 | int g_iPlayers[MAXPLAYERS]; 416 | void StartVote(int client) 417 | { 418 | if (GetClientTeam(client) == 1) 419 | { 420 | PrintToChat(client, "\x04旁观者不能发起投票"); 421 | return; 422 | } 423 | if (!_IsNewVoteAllowed()) 424 | { 425 | PrintToChat(client, "\x04暂时还不能发起新投票"); 426 | return; 427 | } 428 | 429 | // 开始发起投票 430 | g_isAdminPass = false; 431 | g_iNumPlayers = 0; 432 | char sBuffer[128]; 433 | Format(sBuffer, sizeof(sBuffer), "是否同意更换地图为 %s%s ?", g_mapNames[g_newMap], g_modeName[g_newMode]); 434 | 435 | for (int i=1; i<=MaxClients; i++) 436 | { 437 | if (!IsClientInGame(i) || IsFakeClient(i) || (GetClientTeam(i) == 1)) 438 | continue; 439 | g_iPlayers[g_iNumPlayers++] = i; 440 | } 441 | 442 | if (g_useBuiltinvotes) 443 | { 444 | g_voteExt = CreateBuiltinVote(Vote_ActionHandler_Ext, BuiltinVoteType_Custom_YesNo, BuiltinVoteAction_Cancel | BuiltinVoteAction_VoteEnd | BuiltinVoteAction_End); 445 | SetBuiltinVoteArgument(g_voteExt, sBuffer); 446 | SetBuiltinVoteInitiator(g_voteExt, client); 447 | DisplayBuiltinVote(g_voteExt, g_iPlayers, g_iNumPlayers, cv_voteTime.IntValue); 448 | FakeClientCommand(client, "Vote Yes"); // 发起投票的人默认同意 449 | } 450 | else 451 | { 452 | Menu vote = new Menu(Vote_ActionHandler_Menu); 453 | vote.SetTitle(sBuffer); 454 | vote.AddItem("", "是"); 455 | vote.AddItem("", "否"); 456 | vote.DisplayVote(g_iPlayers, g_iNumPlayers, cv_voteTime.IntValue); 457 | } 458 | } 459 | 460 | public int Vote_ActionHandler_Ext(Handle vote, BuiltinVoteAction action, int param1, int param2) 461 | { 462 | char sBuffer[128]; 463 | Format(sBuffer, sizeof(sBuffer), "即将更换地图为 %s%s .", g_mapNames[g_newMap], g_modeName[g_newMode]); 464 | switch (action) 465 | { 466 | // 已完成投票 467 | case BuiltinVoteAction_VoteEnd: 468 | { 469 | if (param1 == BUILTINVOTES_VOTE_YES) 470 | { 471 | DisplayBuiltinVotePass(vote, sBuffer); 472 | // 延时3秒再发起换图指令,因为投票通过的显示具有延迟 473 | CreateTimer(3.0, delayChangeMap); 474 | } 475 | else if (param1 == BUILTINVOTES_VOTE_NO) 476 | { 477 | DisplayBuiltinVoteFail(vote, BuiltinVoteFail_Loses); 478 | } 479 | else 480 | { 481 | // Should never happen, but is here as a diagnostic 482 | DisplayBuiltinVoteFail(vote, BuiltinVoteFail_Generic); 483 | LogMessage("Vote failure. winner = %d", param1); 484 | } 485 | } 486 | // 投票被取消 487 | case BuiltinVoteAction_Cancel: 488 | { 489 | if (g_isAdminPass) 490 | { 491 | DisplayBuiltinVotePass(vote, sBuffer); 492 | CreateTimer(3.0, delayChangeMap); 493 | } 494 | else 495 | DisplayBuiltinVoteFail(vote, view_as(param1)); 496 | } 497 | // 投票动作结束 498 | case BuiltinVoteAction_End: 499 | { 500 | g_voteExt = INVALID_HANDLE; 501 | CloseHandle(vote); 502 | } 503 | } 504 | } 505 | 506 | public int Vote_ActionHandler_Menu(Menu menu, MenuAction action, int param1, int param2) 507 | { 508 | switch( action ) 509 | { 510 | // 投票结束 511 | case MenuAction_VoteEnd: 512 | { 513 | int yes = 0, winningVotes = 0, totalVotes = 0; 514 | GetMenuVoteInfo(param2, winningVotes, totalVotes); 515 | if (0 == param1) // 0为 是 选项 516 | yes = winningVotes; 517 | else 518 | yes = totalVotes - winningVotes; 519 | if (yes > g_iNumPlayers-yes) 520 | { 521 | PrintToChatAll("\x04同意\x03 %d\x04,否定\x03 %d\x04,本次投票通过", yes, g_iNumPlayers-yes); 522 | ChangeMap(); 523 | } 524 | else 525 | { 526 | PrintToChatAll("\x04同意\x03 %d\x04,否定\x03 %d\x04,本次投票未通过", yes, g_iNumPlayers-yes); 527 | } 528 | } 529 | // 投票被取消 530 | case MenuAction_VoteCancel: 531 | { 532 | // 所有人弃权 533 | if (VoteCancel_NoVotes == param1) 534 | { 535 | PrintToChatAll("\x04所有人弃权投票,本次投票未通过"); 536 | } 537 | else if (VoteCancel_Generic == param1 && g_isAdminPass) 538 | ChangeMap(); 539 | } 540 | case MenuAction_End: 541 | { 542 | delete menu; 543 | } 544 | } 545 | } 546 | 547 | 548 | // 延时更换地图 549 | public Action delayChangeMap(Handle timer) 550 | { 551 | g_allowMapChange = false; 552 | ChangeMap(); 553 | } 554 | 555 | int g_time; 556 | void ChangeMap() 557 | { 558 | g_allowMapChange = false; 559 | g_time = cv_mapchangeDelay.IntValue; 560 | if (g_time < 1) 561 | { 562 | if (g_newMode != INVALID) 563 | cv_l4dGamemode.SetString(g_modeCode[g_newMode]); 564 | ServerCommand("changelevel %s", g_mapCodes[g_newMap]); 565 | g_allowMapChange = true; 566 | } 567 | else 568 | { 569 | PrintToChatAll("\x04将在 \x03%i \x04秒后更换地图为\x03 %s%s \x04...", g_time--, g_mapNames[g_newMap], g_modeName[g_newMode]); 570 | CreateTimer(1.0, tmrChangeMap, _, TIMER_REPEAT); 571 | } 572 | } 573 | public Action tmrChangeMap(Handle timer) 574 | { 575 | if (g_time > 0) 576 | { 577 | PrintToChatAll("\x04将在 \x03%i \x04秒后更换地图为\x03 %s%s \x04...", g_time--, g_mapNames[g_newMap], g_modeName[g_newMode]); 578 | return Plugin_Continue; 579 | } 580 | if (g_newMode != INVALID) 581 | cv_l4dGamemode.SetString(g_modeCode[g_newMode]); 582 | ServerCommand("changelevel %s", g_mapCodes[g_newMap]); 583 | g_allowMapChange = true; 584 | return Plugin_Stop; 585 | } 586 | 587 | 588 | 589 | bool _IsNewVoteAllowed() 590 | { 591 | if (g_useBuiltinvotes) 592 | return IsNewBuiltinVoteAllowed(); 593 | else 594 | return IsNewVoteAllowed(); 595 | } 596 | 597 | bool _IsVoteInProgress() 598 | { 599 | if (g_useBuiltinvotes) 600 | return IsBuiltinVoteInProgress(); 601 | else 602 | return IsVoteInProgress(); 603 | } 604 | 605 | void _CancelVote() 606 | { 607 | if (g_useBuiltinvotes) 608 | CancelBuiltinVote(); 609 | else 610 | CancelVote(); 611 | } -------------------------------------------------------------------------------- /l4d_server_manager/l4d_server_manager.smx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Lin515/L4D_LinGe_Plugins/3abfac98dd1922e9853441904d80509c01f9d064/l4d_server_manager/l4d_server_manager.smx -------------------------------------------------------------------------------- /l4d_server_manager/l4d_server_manager.sp: -------------------------------------------------------------------------------- 1 | #pragma semicolon 1 2 | #include 3 | #include 4 | #include 5 | #include 6 | 7 | // 这个插件主要是自用 就懒得说太多具体作用了 8 | public Plugin myinfo = { 9 | name = "[L4D] LinGe Server Manager", 10 | author = "LinGe", 11 | description = "求生之路 简单管理服务器", 12 | version = "0.4", 13 | url = "https://github.com/Lin515/L4D_LinGe_Plugins" 14 | }; 15 | 16 | ConVar cv_hostingLobby; 17 | ConVar cv_onlyLobby; 18 | ConVar cv_allowBotGame; 19 | ConVar cv_allowHibernate; 20 | ConVar cv_autoLobby; 21 | ConVar cv_autoHibernate; 22 | ConVar cv_exclusive; 23 | ConVar cv_exclusiveLock; 24 | ConVar cv_autoCrash; 25 | ConVar cv_cheats; 26 | int g_crashCountDown = 0; 27 | 28 | public APLRes AskPluginLoad2(Handle myself, bool late, char[] error, int err_max) 29 | { 30 | EngineVersion game = GetEngineVersion(); 31 | if (game!=Engine_Left4Dead && game!=Engine_Left4Dead2) 32 | { 33 | strcopy(error, err_max, "本插件只支持 Left 4 Dead 1&2 ."); 34 | return APLRes_SilentFailure; 35 | } 36 | return APLRes_Success; 37 | } 38 | 39 | public void OnPluginStart() 40 | { 41 | cv_hostingLobby = FindConVar("sv_hosting_lobby"); 42 | cv_onlyLobby = FindConVar("sv_allow_lobby_connect_only"); 43 | cv_allowBotGame = FindConVar("sb_all_bot_game"); 44 | cv_allowHibernate = FindConVar("sv_hibernate_when_empty"); 45 | cv_exclusive = FindConVar("sv_steamgroup_exclusive"); 46 | cv_cheats = FindConVar("sv_cheats"); 47 | 48 | cv_autoLobby = CreateConVar("l4d_server_manager_auto_lobby", "1", "自动管理服务器大厅(第一个人连入时使其创建大厅,然后再将大厅移除)", _, true, 0.0, true, 1.0); 49 | cv_autoHibernate = CreateConVar("l4d_server_manager_auto_hibernate", "1", "自动管理服务器休眠", _, true, 0.0, true, 1.0); 50 | cv_exclusiveLock = CreateConVar("sv_steamgroup_exclusive_lock", "-1", "当该值>=0时,sv_steamgroup_exclusive 将被锁定为这个变量的值", _, true, -1.0, true, 1.0); 51 | cv_autoCrash = CreateConVar("l4d_auto_crash", "0", "当服务器不存在真人玩家多少秒后,自动将服务器Crash重启,若为0则不自动重启。(仅可用于Linux服务端,Windows服务端Crash后需要手动启动)", _, true, 0.0); 52 | 53 | cv_exclusive.AddChangeHook(OnExclusiveChanged); 54 | cv_exclusiveLock.AddChangeHook(OnExclusiveChanged); 55 | } 56 | 57 | public void OnExclusiveChanged(ConVar convar, const char[] oldValue, const char[] newValue) 58 | { 59 | if (cv_exclusiveLock.IntValue >= 0 && cv_exclusiveLock.IntValue != cv_exclusive.IntValue) 60 | cv_exclusive.IntValue = cv_exclusiveLock.IntValue; 61 | } 62 | 63 | public void OnMapStart() 64 | { 65 | if (GetHumans(true) == 0) 66 | { 67 | if (cv_autoLobby.IntValue == 1) 68 | { 69 | cv_onlyLobby.IntValue = 1; 70 | } 71 | if (cv_autoHibernate.IntValue == 1 && g_crashCountDown == 0) 72 | { 73 | cv_allowBotGame.IntValue = 0; 74 | cv_allowHibernate.IntValue = 1; 75 | } 76 | } 77 | } 78 | 79 | public bool OnClientConnect(int client) 80 | { 81 | if (!IsFakeClient(client)) 82 | { 83 | if (cv_autoLobby.IntValue == 1) 84 | { 85 | cv_onlyLobby.SetInt(0); 86 | if (cv_hostingLobby.IntValue == 1) 87 | L4D_LobbyUnreserve(); 88 | } 89 | if ((cv_autoHibernate.IntValue == 1 || cv_autoCrash.IntValue > 0) && cv_allowBotGame.IntValue == 0) 90 | cv_allowBotGame.IntValue = 1; 91 | } 92 | return true; 93 | } 94 | 95 | public void OnClientDisconnect(int client) 96 | { 97 | if (IsFakeClient(client)) 98 | return; 99 | CreateTimer(1.0, Timer_CheckHasHuman); 100 | } 101 | 102 | public Action Timer_CheckHasHuman(Handle timer) 103 | { 104 | if (GetHumans(true) == 0) 105 | { 106 | if (cv_autoCrash.IntValue > 0) 107 | { 108 | if (0 == g_crashCountDown) 109 | { 110 | g_crashCountDown = cv_autoCrash.IntValue; 111 | CreateTimer(1.0, Timer_AutoCrash, 0, TIMER_REPEAT); 112 | } 113 | } 114 | else if (cv_autoHibernate.IntValue == 1) 115 | { 116 | if (cv_autoLobby.IntValue == 1) 117 | { 118 | cv_onlyLobby.IntValue = 1; 119 | } 120 | cv_allowBotGame.IntValue = 0; 121 | cv_allowHibernate.IntValue = 1; 122 | } 123 | } 124 | } 125 | 126 | public Action Timer_AutoCrash(Handle timer, any data) 127 | { 128 | g_crashCountDown--; 129 | if (GetHumans(true) > 0) 130 | { 131 | g_crashCountDown = 0; 132 | return Plugin_Stop; 133 | } 134 | if (g_crashCountDown <= 0) 135 | { 136 | g_crashCountDown = 0; 137 | cv_cheats.IntValue = 1; 138 | ServerCommand("sv_crash"); 139 | return Plugin_Stop; 140 | } 141 | return Plugin_Continue; 142 | } -------------------------------------------------------------------------------- /l4d_template.sp: -------------------------------------------------------------------------------- 1 | #pragma semicolon 1 2 | #pragma newdecls required 3 | #include 4 | #include 5 | 6 | public Plugin myinfo = { 7 | name = "插件名", 8 | author = "LinGe", 9 | description = "描述", 10 | version = "0.1", 11 | url = "https://github.com/Lin515/L4D_LinGe_Plugins" 12 | }; 13 | 14 | public APLRes AskPluginLoad2(Handle myself, bool late, char[] error, int err_max) 15 | { 16 | EngineVersion game = GetEngineVersion(); 17 | if (game!=Engine_Left4Dead && game!=Engine_Left4Dead2) 18 | { 19 | strcopy(error, err_max, "本插件只支持 Left 4 Dead 1&2."); 20 | return APLRes_SilentFailure; 21 | } 22 | return APLRes_Success; 23 | } 24 | 25 | public void OnPluginStart() 26 | { 27 | 28 | } -------------------------------------------------------------------------------- /l4d_votemode/README.md: -------------------------------------------------------------------------------- 1 | # 求生之路1&2 投票更换模式 2 | 3 | 投票更换游戏模式插件,是用Silver的l4d_votemode修改的。 4 | 我主要是在其原版的基础上增加了使用builtinvotes扩展来发起游戏内置投票的功能。 5 | 保留了原版的多语言功能,不过经过修改之后,有一些小地方只能统一使用投票发起者的语言。因为需要新增一些提示语句,但我的能力有限,所以其原版的几个翻译文件中我只保留了英语的。 6 | 7 | **支持游戏:求生之路1&2**(实际上游戏1代我没测试过,应该也是支持的) 8 | **建议插件平台:SourceMod 1.10.0** 9 | **依赖:[builtinvotes](https://github.com/LinGe515/L4D_LinGe_Plugins/tree/main/依赖的扩展与插件/builtinvotes)** 10 | **原版 l4d_votemode:** [[L4D & L4D2] Vote Mode](https://forums.alliedmods.net/showthread.php?t=179279) 11 | 12 | ## 指令与功能 13 | 14 | - **!modevote** 15 | 打开投票更换模式菜单。非管理员也可以使用。 16 | - **!modeveto** 17 | 直接否定当前的投票。仅具有ROOT权限管理员可用。 18 | - **!modepass** 19 | 直接通过当前的投票。仅具有ROOT权限管理员可用。 20 | - **!modeforce** 21 | 打开更换模式菜单,使用此指令更换模式无需经过投票。仅具有ROOT权限管理员可用。 22 | 23 | 插件使用方法与我的另一个插件[l4d_mapvote](https://github.com/LinGe515/L4D_LinGe_Plugins/tree/main/l4d_mapvote)基本一样,它们都是基于原版的l4d_votemode修改的。 24 | -------------------------------------------------------------------------------- /l4d_votemode/data/l4d_votemode.cfg: -------------------------------------------------------------------------------- 1 | "gamemodes" 2 | { 3 | "基于战役" 4 | { 5 | "普通战役" "coop" 6 | "写实战役" "realism" 7 | "绝境求生" "mutation4" 8 | "特感速递" "community1" 9 | "血流不止" "mutation3" 10 | "四剑客" "mutation5" 11 | "无法近身" "mutation14" 12 | "侏儒治愈" "mutation20" 13 | "猎头者" "mutation2" 14 | "狩猎盛宴" "mutation16" 15 | "钢铁侠" "mutation8" 16 | "侏儒卫队" "mutation9" 17 | "单间" "mutation10" 18 | "死亡之门" "community5" 19 | "感染季节" "community2" 20 | } 21 | "基于对抗" 22 | { 23 | "原版包抗" "versus" 24 | "突变药抗" "community6" 25 | "团队对抗" "teamversus" 26 | "写实对抗" "mutation12" 27 | "无包无药" "mutation11" 28 | "全TANK" "mutation19" 29 | "生存对抗" "mutation15" 30 | "虚血对抗" "mutation18" 31 | "骑师派对" "community3" 32 | } 33 | "基于清道夫" 34 | { 35 | "清道夫模式" "scavenge" 36 | "清道夫对抗" "mutation13" 37 | "团队清道夫" "teamscavenge" 38 | } 39 | "基于生还者" 40 | { 41 | "生还者模式" "survival" 42 | "生还者对抗" "mutation15" 43 | "噩梦经历" "community4" 44 | } 45 | } -------------------------------------------------------------------------------- /l4d_votemode/data/l4d_votemode_all.cfg: -------------------------------------------------------------------------------- 1 | "gamemodes" 2 | { 3 | "Valve Coop" 4 | { 5 | "Coop" "coop" 6 | "Realism" "realism" 7 | "Bleed Out" "mutation3" 8 | "Chainsaw Massacre" "mutation7" 9 | "Four Swordsmen" "mutation5" 10 | "Gib Fest" "mutation14" 11 | "Hard Eight" "mutation4" 12 | "Healing Gnome" "mutation20" 13 | "Headshot" "mutation2" 14 | "Hunting Party" "mutation16" 15 | "Ironman" "mutation8" 16 | "Last Gnome On Earth" "mutation9" 17 | "Room For One" "mutation10" 18 | "Death's Door" "community5" 19 | "Special Delivery" "community1" 20 | "Flu Season" "community2" 21 | "Versus Survival" "mutation15" 22 | "Survival" "survival" 23 | "Nightmare" "community4" 24 | } 25 | "Valve Versus/Scavenge" 26 | { 27 | "Versus" "versus" 28 | "Team Versus" "teamversus" 29 | "Confogl" "community6" 30 | "Healthpackalypse!" "mutation11" 31 | "Realism Versus" "mutation12" 32 | "Taaannnkk!" "mutation19" 33 | "Versus Survival" "mutation15" 34 | "Versus Bleed Out" "mutation18" 35 | "Riding My Survivor" "community3" 36 | "Scavenge" "scavenge" 37 | "Team Scavenge" "teamscavenge" 38 | "Follow the liter" "mutation13" 39 | } 40 | "Custom Coop 1" 41 | { 42 | "Air Raid" "airraid" 43 | "Big Daddy CO-OP" "bigdaddycoop" 44 | "Biohazard" "biohazard" 45 | "Blast Zone" "blastzone" 46 | "Bleed Out" "mutation3" 47 | "Boomer BBQ" "boomerbbq" 48 | "Boomer Shooter" "boomershooter" 49 | "Boomer Shooter Extreme" "boomershooterex" 50 | "Chainsaw Massacre" "mutation7" 51 | "Classic Campaign" "cmutation3" 52 | "Classic Mode" "classicmode" 53 | "Common Problems" "commonproblems" 54 | "Coulrophobia" "coulrophobia" 55 | "Deadliest Countdown" "dead" 56 | "Death's Door" "community5" 57 | "Deconstruction" "deconstruction" 58 | "Dirt Nap" "dirtnap" 59 | "Elite Death's Door" "deathsdoorel" 60 | "Elite Special Delivery" "specialdeliveryel" 61 | "Flame War" "flamewar" 62 | "Four Swordsmen" "mutation5" 63 | "Fire Storm" "firestorm" 64 | "Flu Season" "community2" 65 | "Fully Charged" "fullycharged" 66 | "Gib Fest" "mutation14" 67 | "Hard Eight" "mutation4" 68 | "Headshot!" "mutation2" 69 | "Healing Gnome" "mutation20" 70 | "Horror For Two" "horror42" 71 | "Hunter Barrage" "hunterbarrage" 72 | "Hunting Party" "mutation16" 73 | "Infestation" "infestation" 74 | "Iron Man" "mutation8" 75 | "Jimpocalypse" "jimpocalypse" 76 | "Jockeys!" "jockeys" 77 | "Karma Bros" "karmabros" 78 | "Last Gnome On Earth" "mutation9" 79 | } 80 | "Custom Coop 2" 81 | { 82 | "Last Man On Earth" "mutation1" 83 | "Left 2 Die" "left2die" 84 | "Limited Resources" "cmutation38" 85 | "Lone Gunman" "mutation17" 86 | "Madness" "madness" 87 | "Nemesis" "nemesis" 88 | "One Man Army" "onemanarmy" 89 | "Pyromania" "pyromania" 90 | "Riot Control" "riotcontrol" 91 | "Room For One" "mutation10" 92 | "Silent But Deadly" "silentbutdeadly" 93 | "Single Player Realism" "singleplayerrealism" 94 | "Snipe Fest" "snipefest" 95 | "Solo Bombing Run" "grenades" 96 | "Special Delivery" "community1" 97 | "Spitters!" "spitters" 98 | "Suicide Boombers" "suicideboombers" 99 | "Tank Attack" "tankattack" 100 | "Tank Bake" "tankbake" 101 | "Tank Rush" "tankrush" 102 | "Taste of Your Own Medicine" "taste" 103 | "Uncommon Ground" "uncommonground" 104 | "Witch Hunt" "witchhunt" 105 | "Monday Morning M60 Massacre!!" "zslmondaymn" 106 | "Monday Night Bile Combat!!" "zslmonday" 107 | "Tuesday Morning Spitter Rage!!" "zsltuesdaymn" 108 | "Tuesday Night Tank Fight!!" "zsltuesday" 109 | "Wednesday Morning Riot!!" "zslwednesdaymn" 110 | "Wednesday Night Gnome Patrol!!" "zslwednesday" 111 | "Thursday Morning Claw Hammer!!" "zslthursdaymn" 112 | "Thursday Night Jockey Trouble!!" "zslthursday" 113 | "Friday Morning Explosion!!" "zslfridaymn" 114 | "Friday Night Friendly Fire!!" "zslfriday" 115 | "Saturday Morning Special Edition!!" "zslsaturdaymn" 116 | "Saturday Night Tankball!!" "zslsaturday" 117 | "Sunday Morning Smoker Roast!!" "zslsundaymn" 118 | "Sunday Night Suicide Run!!" "zslsunday" 119 | } 120 | "Custom Survival" 121 | { 122 | "Bleed Out Survival" "bleedoutsurvival" 123 | "Bombardment" "bombardment" 124 | "Classic Survival" "cmutation30" 125 | "Cocktail Party" "cocktailparty" 126 | "Death's Door Survival" "deathsdoorsv" 127 | "Healing Gnome Survival" "healinggnomesurvival" 128 | "Hordes" "hordes" 129 | "Jimmy Gibbs Only!" "cmutation29" 130 | "Last Survival On Earth" "cmutation7" 131 | "Nightmare" "community4" 132 | "Nightmare - Original" "nightmare" 133 | "Onslaught" "onslaught" 134 | "Realism Survival" "realismsurvival" 135 | "Special Delivery Survival" "specialdeliverysv" 136 | "Tank Assault" "tankassault" 137 | "United We Stand" "united" 138 | "Versus Survival" "mutation15" 139 | } 140 | "Custom Versus" 141 | { 142 | "Big Daddy" "bigdaddy" 143 | "Bleed Out Versus" "mutation18" 144 | "Bleed Out Versus CO-OP" "bleedoutvscoop" 145 | "Classic Versus" "cmutation55" 146 | "Death's Bleeding Tank" "dbt" 147 | "Death's Door Versus" "deathsdoorvs" 148 | "Healing Gnome Versus" "healinggnomeversus" 149 | "Healthpackalypse!" "mutation11" 150 | "Hunting Party Versus" "huntingpartyvs" 151 | "Low Gravity" "lowgravity" 152 | "Realism Versus" "mutation12" 153 | "Realism Versus CO-OP" "realismvscoop" 154 | "Riding My Survivor" "community3" 155 | "Special Delivery Versus" "specialdeliveryvs" 156 | "Taaannnk!!" "mutation19" 157 | "Tankball Versus" "tankassault" 158 | "Tank Busters" "tankbusters" 159 | "Twisted Tongue's" "twistedtongues" 160 | "Ultra Realism" "cmutation26" 161 | "Versus CO-OP" "versuscoop" 162 | "Versus CO-OP - No Kits" "versuscoopnokit" 163 | } 164 | "Custom Scavenge" 165 | { 166 | "Big Daddy Scavenge" "bigdaddyscav" 167 | "Bleed Out Scavenge" "bleedoutscavenge" 168 | "Follow The Lighter" "mutation13" 169 | "Healing Gnome Scavenge" "healinggnomescavenge" 170 | "Realism Scavenge" "realismscavenge" 171 | "Scavenge CO-OP" "scavengecoop" 172 | "Special Delivery Scavenge" "specialdeliverysc" 173 | } 174 | } -------------------------------------------------------------------------------- /l4d_votemode/plugins/l4d_votemode.smx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Lin515/L4D_LinGe_Plugins/3abfac98dd1922e9853441904d80509c01f9d064/l4d_votemode/plugins/l4d_votemode.smx -------------------------------------------------------------------------------- /l4d_votemode/scripting/l4d_votemode.sp: -------------------------------------------------------------------------------- 1 | #define PLUGIN_VERSION "2.1" 2 | 3 | /*====================================================================================== 4 | Plugin Info: 5 | 6 | * Name : [L4D & L4D2] Vote Mode 7 | * Author : SilverShot 8 | * Modify : LinGe - https://github.com/Lin515/L4D_LinGe_Plugins 9 | * Descrp : Allows players to vote change the game mode. Admins can force change the game mode. 10 | * Link : https://forums.alliedmods.net/showthread.php?t=179279 11 | * Plugins : https://sourcemod.net/plugins.php?exact=exact&sortby=title&search=1&author=Silvers 12 | 13 | ======================================================================================== 14 | Change Log: 15 | 2.1 (26-Oct-2021) by LinGe 16 | - 发起投票的人默认同意本次投票 17 | 18 | 2.0 (19-Jun-2021) by LinGe 19 | - 增加功能:可以使用builtinvotes扩展发起游戏内置投票 20 | - 原菜单投票功能的实现改为SourceMod的API函数实现 21 | 22 | 1.5 (16-Jun-2020) 23 | - Added Hungarian translations to the "translations.zip", thanks to "KasperH" for providing. 24 | - Now sets Normal difficulty anytime the plugin changes gamemode. Thanks to "Alex101192" for reporting. 25 | 26 | 1.4 (10-May-2020) 27 | - Fixed potential issues with some translations not being displayed in the right language. 28 | - Various changes to tidy up code. 29 | 30 | 1.3 (30-Apr-2020) 31 | - Optionally uses Info Editor (requires version 1.8 or newer) to detect and change to valid Survival/Scavenge maps. 32 | - This method will also set the difficulty to Normal when switching to Survival/Scavenge maps. 33 | - This method only works when l4d_votemode_restart is set to 1. 34 | - Thanks to "Alex101192" for testing. 35 | 36 | 1.2 (05-May-2018) 37 | - Converted plugin source to the latest syntax utilizing methodmaps. Requires SourceMod 1.8 or newer. 38 | 39 | 1.1 (10-May-2012) 40 | - Fixed votes potentially not displaying to everyone. 41 | 42 | 1.0 (28-Feb-2012) 43 | - Initial release. 44 | 45 | ======================================================================================== 46 | 47 | This plugin was made using source code from the following plugins. 48 | If I have used your code and not credited you, please let me know. 49 | 50 | * Thanks to "N3wton" for "[L4D2] Pause" - Used to make the voting system. 51 | https://forums.alliedmods.net/showthread.php?t=137765 52 | 53 | * Thanks to "chundo" for "Custom Votes" - Used to load the config via SMC Parser. 54 | https://forums.alliedmods.net/showthread.php?p=633808 55 | 56 | * Thanks to "Rayman1103" for the "All Mutations Unlocked" addon. 57 | https://forums.steampowered.com/forums/showthread.php?t=1529433 58 | 59 | ======================================================================================*/ 60 | 61 | #pragma semicolon 1 62 | #pragma newdecls required 63 | 64 | #include 65 | #include 66 | #include 67 | 68 | #define CVAR_FLAGS FCVAR_NOTIFY 69 | #define CHAT_TAG "\x04" 70 | #define CONFIG_VOTEMODE "data/l4d_votemode.cfg" 71 | 72 | 73 | // Cvar handles and variables 74 | ConVar g_hCvarAdmin, g_hCvarMenu, g_hCvarRestart, g_hCvarTimeout; 75 | int g_iCvarAdmin, g_iCvarRestart; 76 | int g_fCvarTimeout; 77 | 78 | // Other handles 79 | ConVar g_hMPGameMode, g_hRestartGame; 80 | TopMenu g_hCvarMenuMenu; 81 | 82 | // Distinguishes mode selected and if admin forced 83 | int g_iChangeModeTo, g_iSelected[MAXPLAYERS+1]; 84 | 85 | // Strings to hold the gamemodes and titles 86 | char g_sModeCommands[256][64], g_sModeNames[256][64], g_sModeTitles[256][64]; 87 | 88 | // Store where the different titles are within the commands list 89 | int g_iConfigCount, g_iConfigLevel, g_iModeIndex[64], g_iTitleIndex[64], g_iTitleCount; 90 | 91 | // Survival/Scavenge map detection 92 | native void InfoEditor_GetString(int pThis, const char[] keyname, char[] dest, int destLen); 93 | bool g_bInfoEditor; 94 | 95 | 96 | ConVar cv_useBuiltinvotes; // 是否使用扩展发起原版投票开关 97 | bool g_useBuiltinvotes = false; // 当前插件是否使用原版投票 98 | bool g_allowChangeMode = true; // 当前是否允许插件发起模式更改 99 | bool g_isForceMode[MAXPLAYERS+1]; // 记录本次指令是否是forcemode 100 | bool g_isAdminPass; // 在提前取消投票的时候判断是否是管理员直接通过了本次投票 101 | 102 | 103 | // ==================================================================================================== 104 | // PLUGIN INFO / START 105 | // ==================================================================================================== 106 | public Plugin myinfo = 107 | { 108 | name = "[L4D & L4D2] Vote Mode", 109 | author = "SilverShot, LinGe", 110 | description = "Allows players to vote change the game mode. Admins can force change the game mode.", 111 | version = PLUGIN_VERSION, 112 | url = "https://github.com/Lin515/L4D_LinGe_Plugins/L4D_LinGe_Plugins" 113 | // 原版 url = "https://forums.alliedmods.net/showthread.php?t=179279" 114 | } 115 | 116 | public APLRes AskPluginLoad2(Handle myself, bool late, char[] error, int err_max) 117 | { 118 | EngineVersion test = GetEngineVersion(); 119 | if( test != Engine_Left4Dead && test != Engine_Left4Dead2 ) 120 | { 121 | strcopy(error, err_max, "Plugin only supports Left 4 Dead 2."); 122 | return APLRes_SilentFailure; 123 | } 124 | 125 | MarkNativeAsOptional("InfoEditor_GetString"); 126 | 127 | return APLRes_Success; 128 | } 129 | 130 | public void OnLibraryAdded(const char[] name) 131 | { 132 | if( strcmp(name, "info_editor") == 0 ) 133 | g_bInfoEditor = true; 134 | } 135 | 136 | public void OnLibraryRemoved(const char[] name) 137 | { 138 | if( strcmp(name, "info_editor") == 0 ) 139 | g_bInfoEditor = false; 140 | else if( strcmp(name, "adminmenu") == 0 ) 141 | g_hCvarMenuMenu = null; 142 | } 143 | 144 | public void OnPluginStart() 145 | { 146 | if( (g_hMPGameMode = FindConVar("mp_gamemode")) == null ) 147 | SetFailState("Failed to find convar handle 'mp_gamemode'. Cannot load plugin."); 148 | 149 | if( (g_hRestartGame = FindConVar("mp_restartgame")) == null ) 150 | SetFailState("Failed to find convar handle 'mp_restartgame'. Cannot load plugin."); 151 | 152 | LoadTranslations("votemode.phrases"); 153 | LoadTranslations("common.phrases"); 154 | LoadTranslations("core.phrases"); 155 | 156 | g_hCvarMenu = CreateConVar( "l4d_votemode_admin_menu", "1", "0=No, 1=Display in the Server Commands of admin menu.", CVAR_FLAGS ); 157 | g_hCvarAdmin = CreateConVar( "l4d_votemode_admin_flag", "", "Players with these flags can vote to change the game mode.", CVAR_FLAGS ); 158 | g_hCvarRestart = CreateConVar( "l4d_votemode_restart", "1", "0=No restart, 1=With 'changelevel' command, 2=Restart map with 'mp_restartgame' cvar.", CVAR_FLAGS ); 159 | g_hCvarTimeout = CreateConVar( "l4d_votemode_timeout", "20.0", "How long the vote should be visible.", CVAR_FLAGS, true, 5.0, true, 60.0 ); 160 | cv_useBuiltinvotes = CreateConVar( "l4d_votemode_use_builtinvotes", "1", "Use the Builtinvotes extension to initiate a vote? If enabled, you must install the Builtinvotes extension.", CVAR_FLAGS, true, 0.0, true, 1.0); 161 | CreateConVar("l4d_votemode_version", PLUGIN_VERSION, "Vote Mode plugin version.", FCVAR_NOTIFY|FCVAR_DONTRECORD); 162 | AutoExecConfig(true, "l4d_votemode"); 163 | 164 | GetCvars(); 165 | g_hCvarAdmin.AddChangeHook(ConVarChanged_Cvars); 166 | g_hCvarRestart.AddChangeHook(ConVarChanged_Cvars); 167 | g_hCvarTimeout.AddChangeHook(ConVarChanged_Cvars); 168 | 169 | RegAdminCmd( "sm_modeveto", CommandVeto, ADMFLAG_ROOT, "Allows admins to veto a current vote."); 170 | RegAdminCmd( "sm_modepass", CommandPass, ADMFLAG_ROOT, "Allows admins to pass a current vote."); 171 | RegAdminCmd( "sm_modeforce", CommandForce, ADMFLAG_ROOT, "Allows admins to force the game into a different mode."); 172 | RegConsoleCmd( "sm_modevote", CommandVote, "Displays a menu to vote the game into a different mode."); 173 | RegAdminCmd( "sm_vetomode", CommandVeto, ADMFLAG_ROOT, "Allows admins to veto a current vote."); 174 | RegAdminCmd( "sm_passmode", CommandPass, ADMFLAG_ROOT, "Allows admins to pass a current vote."); 175 | RegAdminCmd( "sm_forcemode", CommandForce, ADMFLAG_ROOT, "Allows admins to force the game into a different mode."); 176 | RegConsoleCmd( "sm_votemode", CommandVote, "Displays a menu to vote the game into a different mode."); 177 | 178 | Handle topmenu = GetAdminTopMenu(); 179 | if( LibraryExists("adminmenu") && (topmenu != null) ) 180 | OnAdminMenuReady(topmenu); 181 | 182 | LoadConfig(); 183 | } 184 | 185 | public void OnConfigsExecuted() 186 | { 187 | if (cv_useBuiltinvotes.IntValue == 1) 188 | g_useBuiltinvotes = true; 189 | else 190 | g_useBuiltinvotes = false; 191 | g_allowChangeMode = true; 192 | } 193 | 194 | 195 | // ==================================================================================================== 196 | // ADD TO ADMIN MENU 197 | // ==================================================================================================== 198 | public void OnAdminMenuReady(Handle topmenu) 199 | { 200 | if( topmenu == g_hCvarMenuMenu || g_hCvarMenu.BoolValue == false ) 201 | return; 202 | 203 | g_hCvarMenuMenu = view_as(topmenu); 204 | 205 | TopMenuObject player_commands = FindTopMenuCategory(g_hCvarMenuMenu, ADMINMENU_SERVERCOMMANDS); 206 | if( player_commands == INVALID_TOPMENUOBJECT ) return; 207 | 208 | AddToTopMenu(g_hCvarMenuMenu, "sm_forcemode_menu", TopMenuObject_Item, Handle_Category, player_commands, "sm_forcemode_menu", ADMFLAG_GENERIC); 209 | } 210 | 211 | public int Handle_Category(Handle topmenu, TopMenuAction action, TopMenuObject object_id, int param, char[] buffer, int maxlength) 212 | { 213 | switch( action ) 214 | { 215 | case TopMenuAction_DisplayTitle: 216 | Format(buffer, maxlength, "%T", "VoteMode_Force", param); 217 | case TopMenuAction_DisplayOption: 218 | Format(buffer, maxlength, "%T", "VoteMode_Force", param); 219 | case TopMenuAction_SelectOption: 220 | { 221 | g_isForceMode[param] = true; 222 | VoteMenu_Select(param); 223 | } 224 | } 225 | } 226 | 227 | 228 | 229 | // ==================================================================================================== 230 | // CVARS 231 | // ==================================================================================================== 232 | public void ConVarChanged_Cvars(Handle convar, const char[] oldValue, const char[] newValue) 233 | { 234 | GetCvars(); 235 | } 236 | 237 | void GetCvars() 238 | { 239 | char sTemp[16]; 240 | g_hCvarAdmin.GetString(sTemp, sizeof(sTemp)); 241 | g_iCvarAdmin = ReadFlagString(sTemp); 242 | g_iCvarRestart = g_hCvarRestart.IntValue; 243 | g_fCvarTimeout = g_hCvarTimeout.IntValue; 244 | } 245 | 246 | 247 | 248 | // ==================================================================================================== 249 | // LOAD CONFIG 250 | // ==================================================================================================== 251 | void LoadConfig() 252 | { 253 | char sPath[PLATFORM_MAX_PATH]; 254 | BuildPath(Path_SM, sPath, sizeof(sPath), CONFIG_VOTEMODE); 255 | 256 | if( !FileExists(sPath) ) 257 | { 258 | SetFailState("Error: Cannot find the Votemode config '%s'", sPath); 259 | return; 260 | } 261 | 262 | ParseConfigFile(sPath); 263 | } 264 | 265 | bool ParseConfigFile(const char[] file) 266 | { 267 | // Load parser and set hook functions 268 | SMCParser parser = new SMCParser(); 269 | SMC_SetReaders(parser, Config_NewSection, Config_KeyValue, Config_EndSection); 270 | parser.OnEnd = Config_End; 271 | 272 | // Log errors detected in config 273 | char error[128]; 274 | int line = 0, col = 0; 275 | SMCError result = parser.ParseFile(file, line, col); 276 | 277 | if( result != SMCError_Okay ) 278 | { 279 | parser.GetErrorString(result, error, sizeof(error)); 280 | SetFailState("%s on line %d, col %d of %s [%d]", error, line, col, file, result); 281 | } 282 | 283 | delete parser; 284 | return (result == SMCError_Okay); 285 | } 286 | 287 | public SMCResult Config_NewSection(Handle parser, const char[] section, bool quotes) 288 | { 289 | // Section strings, used for the first menu ModeTitles 290 | g_iConfigLevel++; 291 | if( g_iConfigLevel > 1 ) 292 | { 293 | strcopy(g_sModeTitles[g_iConfigLevel -2], sizeof(g_sModeTitles[]), section); 294 | g_iModeIndex[g_iConfigLevel -2] = g_iConfigCount; 295 | g_iTitleIndex[g_iTitleCount++] = g_iConfigCount + 1; 296 | } 297 | return SMCParse_Continue; 298 | } 299 | 300 | public SMCResult Config_KeyValue(Handle parser, const char[] key, const char[] value, bool key_quotes, bool value_quotes) 301 | { 302 | // Key and value strings, used for the ModeNames and ModeCommands 303 | strcopy(g_sModeNames[g_iConfigCount], sizeof(g_sModeNames[]), key); 304 | strcopy(g_sModeCommands[g_iConfigCount], sizeof(g_sModeCommands[]), value); 305 | g_iConfigCount++; 306 | return SMCParse_Continue; 307 | } 308 | 309 | public SMCResult Config_EndSection(Handle parser) 310 | { 311 | // Config finished loading 312 | g_iModeIndex[g_iConfigLevel -1] = g_iConfigCount; 313 | return SMCParse_Continue; 314 | } 315 | 316 | public void Config_End(Handle parser, bool halted, bool failed) 317 | { 318 | if( failed ) 319 | SetFailState("Error: Cannot load the Votemode config."); 320 | } 321 | 322 | 323 | 324 | // ==================================================================================================== 325 | // COMMANDS 326 | // ==================================================================================================== 327 | public Action CommandVeto(int client, int args) 328 | { 329 | if( _IsVoteInProgress() ) 330 | { 331 | g_isAdminPass = false; 332 | PrintToChatAll("%s%t", CHAT_TAG, "VoteMode_Veto"); 333 | _CancelVote(); 334 | } 335 | return Plugin_Handled; 336 | } 337 | 338 | public Action CommandPass(int client, int args) 339 | { 340 | if( _IsVoteInProgress() ) 341 | { 342 | g_isAdminPass = true; 343 | PrintToChatAll("%s%t", CHAT_TAG, "VoteMode_Pass"); 344 | _CancelVote(); 345 | } 346 | return Plugin_Handled; 347 | } 348 | 349 | public Action CommandVote(int client, int args) 350 | { 351 | if (0 == client) 352 | return Plugin_Handled; 353 | 354 | // Admins only 355 | if( CheckCommandAccess(client, "", g_iCvarAdmin) == false ) 356 | { 357 | PrintToChat(client, "%s%t", CHAT_TAG, "No Access"); 358 | return Plugin_Handled; 359 | } 360 | 361 | if (GetClientTeam(client) == 1) 362 | { 363 | PrintToChat(client, "%s%t", CHAT_TAG, "VoteMode_Spec"); 364 | return Plugin_Handled; 365 | } 366 | 367 | if (!g_allowChangeMode) 368 | { 369 | PrintToChat(client, "%s%t", CHAT_TAG, "VoteMode_NoChangeMode"); 370 | return Plugin_Handled; 371 | } 372 | 373 | if (!_IsNewVoteAllowed()) 374 | { 375 | PrintToChat(client, "%s%t", CHAT_TAG, "VoteMode_InProgress"); 376 | return Plugin_Handled; 377 | } 378 | 379 | if( args == 1 ) 380 | { 381 | char sTemp[64]; 382 | GetCmdArg(1, sTemp, sizeof(sTemp)); 383 | 384 | for( int i = 0; i < g_iConfigCount; i++ ) 385 | { 386 | if( strcmp(g_sModeCommands[i], sTemp, false) == 0 ) 387 | { 388 | StartVote(client, i); 389 | return Plugin_Handled; 390 | } 391 | } 392 | } 393 | 394 | g_isForceMode[client] = false; 395 | VoteMenu_Select(client); 396 | return Plugin_Handled; 397 | } 398 | 399 | public Action CommandForce(int client, int args) 400 | { 401 | if (!g_allowChangeMode) 402 | { 403 | PrintToChat(client, "%s%t", CHAT_TAG, "VoteMode_NoChangeMode"); 404 | return Plugin_Handled; 405 | } 406 | 407 | if( args == 1 ) 408 | { 409 | char sTemp[64]; 410 | GetCmdArg(1, sTemp, sizeof(sTemp)); 411 | 412 | for( int i = 0; i < g_iConfigCount; i++ ) 413 | { 414 | if( strcmp(g_sModeCommands[i], sTemp, false) == 0 ) 415 | { 416 | if (_IsVoteInProgress()) 417 | { 418 | g_isAdminPass = false; 419 | PrintToChatAll("%s%t", CHAT_TAG, "VoteMode_AdminForce"); 420 | _CancelVote(); 421 | } 422 | ChangeGameModeTo(i); 423 | return Plugin_Handled; 424 | } 425 | } 426 | } 427 | 428 | g_isForceMode[client] = true; 429 | VoteMenu_Select(client); 430 | return Plugin_Handled; 431 | } 432 | 433 | // ==================================================================================================== 434 | // DISPLAY MENU 435 | // ==================================================================================================== 436 | void VoteMenu_Select(int client) 437 | { 438 | Menu menu = new Menu(VoteMenuHandler_Select); 439 | if( g_isForceMode[client] ) 440 | menu.SetTitle("%T", "VoteMode_Force", client); 441 | else 442 | menu.SetTitle("%T", "VoteMode_Vote", client); 443 | 444 | // Build menu 445 | for( int i = 0; i < g_iConfigLevel -1; i++ ) 446 | menu.AddItem("", g_sModeTitles[i]); 447 | 448 | // Display menu 449 | menu.Display(client, MENU_TIME_FOREVER); 450 | } 451 | 452 | public int VoteMenuHandler_Select(Menu menu, MenuAction action, int client, int param2) 453 | { 454 | switch( action ) 455 | { 456 | case MenuAction_End: 457 | { 458 | delete menu; 459 | } 460 | // case MenuAction_Cancel: 461 | // { 462 | // if( param2 == MenuCancel_ExitBack && g_isForceMode[client] && g_hCvarMenuMenu != null ) 463 | // g_hCvarMenuMenu.Display(client, TopMenuPosition_LastCategory); //TopMenuPosition_Start 464 | // } 465 | case MenuAction_Select: 466 | { 467 | g_iSelected[client] = param2; 468 | VoteTwoMenu_Select(client, param2); 469 | } 470 | } 471 | } 472 | 473 | void VoteTwoMenu_Select(int client, int param2) 474 | { 475 | Menu menu = new Menu(VoteMenuTwoMenur_Select); 476 | if( g_isForceMode[client] ) 477 | menu.SetTitle("%T", "VoteMode_Force", client); 478 | else 479 | menu.SetTitle("%T", "VoteMode_Vote", client); 480 | 481 | // Build menu 482 | int param1 = g_iModeIndex[param2]; 483 | param2 = g_iModeIndex[param2 +1]; 484 | 485 | for( int i = param1; i < param2; i++ ) 486 | menu.AddItem("", g_sModeNames[i]); 487 | 488 | // Display menu 489 | menu.ExitBackButton = true; 490 | menu.Display(client, MENU_TIME_FOREVER); 491 | } 492 | 493 | public int VoteMenuTwoMenur_Select(Menu menu, MenuAction action, int client, int param2) 494 | { 495 | switch( action ) 496 | { 497 | case MenuAction_End: 498 | { 499 | delete menu; 500 | } 501 | case MenuAction_Cancel: 502 | { 503 | if( param2 == MenuCancel_ExitBack ) 504 | VoteMenu_Select(client); 505 | } 506 | case MenuAction_Select: 507 | { 508 | // Work out the mode command index 509 | int iSelected = g_iSelected[client]; 510 | iSelected = g_iModeIndex[iSelected]; 511 | iSelected += param2; 512 | ModeSelect(client, iSelected); 513 | } 514 | } 515 | } 516 | 517 | void ModeSelect(int client, int iSelected) 518 | { 519 | if ( g_isForceMode[client] ) 520 | { 521 | if (_IsVoteInProgress()) 522 | { 523 | g_isAdminPass = false; 524 | PrintToChatAll("%s%t", CHAT_TAG, "VoteMode_AdminForce"); 525 | _CancelVote(); 526 | } 527 | ChangeGameModeTo(iSelected); 528 | } 529 | else 530 | StartVote(client, iSelected); 531 | } 532 | 533 | 534 | 535 | // ==================================================================================================== 536 | // VOTING STUFF 537 | // ==================================================================================================== 538 | Handle g_voteExt; 539 | int g_iNumPlayers = 0; 540 | int g_iPlayers[MAXPLAYERS]; 541 | int g_voteInitiator = -1; 542 | void StartVote(int client, int iMode) 543 | { 544 | if (!g_allowChangeMode) 545 | { 546 | PrintToChat(client, "%s%t", CHAT_TAG, "VoteMode_NoChangeMode"); 547 | return; 548 | } 549 | if (GetClientTeam(client) == 1) 550 | { 551 | PrintToChat(client, "%s%t", CHAT_TAG, "VoteMode_Spec"); 552 | return; 553 | } 554 | // Don't allow multiple votes 555 | if( !_IsNewVoteAllowed() ) 556 | { 557 | PrintToChat(client, "%s%t", CHAT_TAG, "VoteMode_InProgress"); 558 | return; 559 | } 560 | 561 | // Setup vote 562 | g_isAdminPass = false; 563 | g_iNumPlayers = 0; 564 | g_iChangeModeTo = iMode; 565 | g_voteInitiator = client; 566 | char sBuffer[128]; 567 | SetGlobalTransTarget(client); 568 | Format(sBuffer, sizeof(sBuffer), "%t", "VoteMode_Change", g_sModeNames[iMode]); 569 | 570 | // Display vote 571 | for (int i=1; i<=MaxClients; i++) 572 | { 573 | if (!IsClientInGame(i) || IsFakeClient(i) || (GetClientTeam(i) == 1)) 574 | continue; 575 | g_iPlayers[g_iNumPlayers++] = i; 576 | } 577 | 578 | if (g_useBuiltinvotes) 579 | { 580 | g_voteExt = CreateBuiltinVote(Vote_ActionHandler_Ext, BuiltinVoteType_Custom_YesNo, BuiltinVoteAction_Cancel | BuiltinVoteAction_VoteEnd | BuiltinVoteAction_End); 581 | SetBuiltinVoteArgument(g_voteExt, sBuffer); 582 | SetBuiltinVoteInitiator(g_voteExt, client); 583 | DisplayBuiltinVote(g_voteExt, g_iPlayers, g_iNumPlayers, g_fCvarTimeout); 584 | FakeClientCommand(client, "Vote Yes"); // 发起投票的人默认同意 585 | } 586 | else 587 | { 588 | Menu vote = new Menu(Vote_ActionHandler_Menu); 589 | vote.SetTitle(sBuffer); 590 | Format(sBuffer, sizeof(sBuffer), "%t", "Yes"); 591 | vote.AddItem("", sBuffer); 592 | Format(sBuffer, sizeof(sBuffer), "%t", "No"); 593 | vote.AddItem("", sBuffer); 594 | vote.DisplayVote(g_iPlayers, g_iNumPlayers, g_fCvarTimeout); 595 | } 596 | } 597 | 598 | 599 | public int Vote_ActionHandler_Ext(Handle vote, BuiltinVoteAction action, int param1, int param2) 600 | { 601 | char sBuffer[128]; 602 | Format(sBuffer, sizeof(sBuffer), "%T", "VoteMode_Changing", g_voteInitiator, g_sModeNames[g_iChangeModeTo]); 603 | switch (action) 604 | { 605 | // 已完成投票 606 | case BuiltinVoteAction_VoteEnd: 607 | { 608 | if (param1 == BUILTINVOTES_VOTE_YES) 609 | { 610 | DisplayBuiltinVotePass(vote, sBuffer); 611 | ChangeGameModeTo(g_iChangeModeTo); 612 | } 613 | else if (param1 == BUILTINVOTES_VOTE_NO) 614 | { 615 | DisplayBuiltinVoteFail(vote, BuiltinVoteFail_Loses); 616 | } 617 | else 618 | { 619 | // Should never happen, but is here as a diagnostic 620 | DisplayBuiltinVoteFail(vote, BuiltinVoteFail_Generic); 621 | LogMessage("Vote failure. winner = %d", param1); 622 | } 623 | } 624 | // 投票被取消 625 | case BuiltinVoteAction_Cancel: 626 | { 627 | if (g_isAdminPass) 628 | { 629 | DisplayBuiltinVotePass(vote, sBuffer); 630 | ChangeGameModeTo(g_iChangeModeTo); 631 | } 632 | else 633 | DisplayBuiltinVoteFail(vote, view_as(param1)); 634 | } 635 | // 投票动作结束 636 | case BuiltinVoteAction_End: 637 | { 638 | g_voteExt = INVALID_HANDLE; 639 | CloseHandle(vote); 640 | } 641 | } 642 | } 643 | 644 | public int Vote_ActionHandler_Menu(Menu menu, MenuAction action, int param1, int param2) 645 | { 646 | switch( action ) 647 | { 648 | // 投票结束 649 | case MenuAction_VoteEnd: 650 | { 651 | int yes = 0, winningVotes = 0, totalVotes = 0; 652 | GetMenuVoteInfo(param2, winningVotes, totalVotes); 653 | if (0 == param1) // 0为 是 选项 654 | yes = winningVotes; 655 | else 656 | yes = totalVotes - winningVotes; 657 | if (yes > g_iNumPlayers-yes) 658 | { 659 | PrintToChatAll("%s%t", CHAT_TAG, "VoteMode_VotePass", yes, g_iNumPlayers-yes); 660 | ChangeGameModeTo(g_iChangeModeTo); 661 | } 662 | else 663 | { 664 | PrintToChatAll("%s%t", CHAT_TAG, "VoteMode_VoteFail", yes, g_iNumPlayers-yes); 665 | } 666 | } 667 | // 投票被取消 668 | case MenuAction_VoteCancel: 669 | { 670 | // 所有人弃权 671 | if (VoteCancel_NoVotes == param1) 672 | { 673 | PrintToChatAll("%s%t", CHAT_TAG, "VoteMode_VoteInvalid"); 674 | } 675 | else if (VoteCancel_Generic == param1 && g_isAdminPass) 676 | ChangeGameModeTo(g_iChangeModeTo); 677 | } 678 | case MenuAction_End: 679 | { 680 | delete menu; 681 | } 682 | } 683 | } 684 | 685 | 686 | // ==================================================================================================== 687 | // SET GAME MODE 688 | // ==================================================================================================== 689 | 690 | void ChangeGameModeTo(int iMode) 691 | { 692 | g_allowChangeMode = false; 693 | CreateTimer(3.0, tmrChangeMode, iMode); 694 | } 695 | 696 | public Action tmrChangeMode(Handle timer, any index) 697 | { 698 | // Change map 699 | if( g_iCvarRestart == 1 ) 700 | { 701 | // Current map 702 | int done; 703 | bool change = true; 704 | char sMap[64]; 705 | GetCurrentMap(sMap, sizeof(sMap)); 706 | 707 | // Info Editor detected? 708 | if( g_bInfoEditor ) 709 | { 710 | int indexed; 711 | 712 | // Get title from selected index 713 | for( int i = 0; i < sizeof(g_iTitleIndex); i++ ) 714 | { 715 | if( g_iTitleIndex[i] && index + 1 >= g_iTitleIndex[i] ) 716 | { 717 | indexed = i; 718 | } else { 719 | break; 720 | } 721 | } 722 | 723 | // Change to Survival/Scavenge mode? 724 | if( strcmp(g_sModeTitles[indexed], "Survival") == 0 || strcmp(g_sModeTitles[indexed], "Scavenge") == 0 ) 725 | { 726 | char sTemp[64]; 727 | ArrayList hTemp; 728 | hTemp = new ArrayList(ByteCountToCells(64)); 729 | 730 | // Loop valid Survival/Scavenge maps from mission file 731 | for( int i = 1; i < 15; i++ ) 732 | { 733 | Format(sTemp, sizeof(sTemp), "modes/%s/%d/map", g_sModeTitles[indexed], i); 734 | InfoEditor_GetString(0, sTemp, sTemp, sizeof(sTemp)); 735 | 736 | if( strcmp(sTemp, "N/A") == 0 ) // Doesn't exist 737 | { 738 | break; 739 | } 740 | else if( strcmp(sTemp, sMap) == 0 ) // Same as current map 741 | { 742 | done = 1; 743 | break; 744 | } 745 | else 746 | { 747 | hTemp.PushString(sTemp); // Store valid maps 748 | } 749 | } 750 | 751 | // Not same map 752 | if( !done ) 753 | { 754 | // Get random valid map 755 | done = hTemp.Length; 756 | if( done ) 757 | { 758 | hTemp.GetString(GetRandomInt(0, done-1), sMap, sizeof(sMap)); 759 | } 760 | else 761 | { 762 | change = false; 763 | } 764 | } 765 | 766 | delete hTemp; 767 | } 768 | } 769 | 770 | if( change ) 771 | { 772 | g_hMPGameMode.SetString(g_sModeCommands[index]); 773 | // ServerCommand("z_difficulty normal; changelevel %s", sMap); 774 | ServerCommand("changelevel %s", sMap); 775 | } 776 | else 777 | { 778 | PrintToChatAll("%sFailed to change gamemode, no valid map", CHAT_TAG); 779 | LogAction(-1, -1, "Failed to change gamemode, no valid map"); 780 | } 781 | g_allowChangeMode = true; 782 | } 783 | else if( g_iCvarRestart == 2 ) 784 | { 785 | g_hRestartGame.IntValue = 1; 786 | CreateTimer(0.1, tmrRestartGame); 787 | } 788 | } 789 | 790 | public Action tmrRestartGame(Handle timer) 791 | { 792 | g_hRestartGame.IntValue = 1; 793 | g_allowChangeMode = true; 794 | } 795 | 796 | bool _IsNewVoteAllowed() 797 | { 798 | if (g_useBuiltinvotes) 799 | return IsNewBuiltinVoteAllowed(); 800 | else 801 | return IsNewVoteAllowed(); 802 | } 803 | 804 | bool _IsVoteInProgress() 805 | { 806 | if (g_useBuiltinvotes) 807 | return IsBuiltinVoteInProgress(); 808 | else 809 | return IsVoteInProgress(); 810 | } 811 | 812 | void _CancelVote() 813 | { 814 | if (g_useBuiltinvotes) 815 | CancelBuiltinVote(); 816 | else 817 | CancelVote(); 818 | } -------------------------------------------------------------------------------- /l4d_votemode/translations/chi/votemode.phrases.txt: -------------------------------------------------------------------------------- 1 | "Phrases" 2 | { 3 | "VoteMode_Force" 4 | { 5 | "chi" "更换模式" 6 | } 7 | "VoteMode_Vote" 8 | { 9 | "chi" "投票更换模式" 10 | } 11 | "VoteMode_Pass" 12 | { 13 | "chi" "管理员通过了本次投票" 14 | } 15 | "VoteMode_Veto" 16 | { 17 | "chi" "管理员否决了本次投票" 18 | } 19 | "VoteMode_AdminForce" 20 | { 21 | "chi" "管理员已选择模式,本次投票取消" 22 | } 23 | "VoteMode_Spec" 24 | { 25 | "chi" "旁观者不能发起投票" 26 | } 27 | "VoteMode_InProgress" 28 | { 29 | "chi" "暂时还不能发起新投票" 30 | } 31 | "VoteMode_NoChangeMode" 32 | { 33 | "chi" "当前不能发起模式变更" 34 | } 35 | "VoteMode_Change" 36 | { 37 | "chi" "是否同意更换游戏模式为 {1} ?" 38 | } 39 | "VoteMode_Changing" 40 | { 41 | "chi" "即将更换游戏模式为 {1} ..." 42 | } 43 | "VoteMode_VotePass" 44 | { 45 | "chi" "同意 {1},否定 {2},本次投票通过" 46 | } 47 | "VoteMode_VoteFail" 48 | { 49 | "chi" "同意 {1},否定 {2},本次投票未通过" 50 | } 51 | "VoteMode_VoteInvalid" 52 | { 53 | "chi" "无人参与投票,本次投票无效" 54 | } 55 | } -------------------------------------------------------------------------------- /l4d_votemode/translations/votemode.phrases.txt: -------------------------------------------------------------------------------- 1 | "Phrases" 2 | { 3 | "VoteMode_Force" 4 | { 5 | "en" "Force Game Mode" 6 | } 7 | "VoteMode_Vote" 8 | { 9 | "en" "Vote Game Mode" 10 | } 11 | "VoteMode_Pass" 12 | { 13 | "en" "An admin has passed the current vote." 14 | } 15 | "VoteMode_Veto" 16 | { 17 | "en" "An admin has ended the current vote." 18 | } 19 | "VoteMode_AdminForce" 20 | { 21 | "en" "An admin has select a game mode." 22 | } 23 | "VoteMode_Spec" 24 | { 25 | "en" "Spectator cannot call a vote." 26 | } 27 | "VoteMode_InProgress" 28 | { 29 | "en" "Cannot start vote!" 30 | } 31 | "VoteMode_NoChangeMode" 32 | { 33 | "en" "Cannot change Game Mode." 34 | } 35 | "VoteMode_Change" 36 | { 37 | "#format" "{1:s}" 38 | "en" "Change Game Mode to {1}, Yes or No?" 39 | } 40 | "VoteMode_Changing" 41 | { 42 | "#format" "{1:s}" 43 | "en" "Changing to {1} ..." 44 | } 45 | "VoteMode_VotePass" 46 | { 47 | "#format" "{1:d},{2:d}" 48 | "en" "Yes {1}, No {2}, Vote successful." 49 | } 50 | "VoteMode_VoteFail" 51 | { 52 | "#format" "{1:d},{2:d}" 53 | "en" "Yes {1}, No {2}, Vote failed." 54 | } 55 | "VoteMode_VoteInvalid" 56 | { 57 | "en" "No one voted, Vote failed." 58 | } 59 | } --------------------------------------------------------------------------------