本文大部分使用ai生成,但是每个坑我都踩过

📋 准备工作

  1. 服务器环境:Docker (Ubuntu/Debian 容器) 或 Linux VPS。
  2. 数据库:MySQL 5.7
  3. 本地电脑:用于修改源码和编译插件 (Windows)。 无需
    插件下载链接在下方

第一步:数据库准备 (MySQL)

这是最基础的一步。为了避免 Unknown column 报错,我们直接建立一个“全功能”的数据库。

  1. 创建数据库与用户
    在宝塔面板或命令行中:

    • 数据库名:l4d2
    • 用户名:l4d2
    • 密码:(你自己设定的)
    • 权限:必须设置为 所有人 (即 %),否则 Docker 连不上。
  2. 修改 MySQL 配置 (解决连接超时)
    修改 MySQL 配置文件 (my.cnf/my.ini):

    • [mysqld] 下添加 skip-name-resolve
    • 重启 MySQL 服务
  3. 导入“完美版”表结构
    打开 phpMyAdmin -> l4d2 -> SQL,执行以下语句(这包含了所有必需字段和触发器):

    SET NAMES utf8mb4;
    
    -- 1. 创建核心玩家表 (包含所有 Anne 版特有字段)
    CREATE TABLE IF NOT EXISTS `players` (
      `steamid` varchar(64) NOT NULL,
      `name` varchar(128) NOT NULL DEFAULT '',
      `ip` varchar(32) NOT NULL DEFAULT '',
      `lastontime` int(11) NOT NULL DEFAULT '0',
      `playtime` int(11) NOT NULL DEFAULT '0',
      `points` int(11) NOT NULL DEFAULT '0',
      `kills` int(11) NOT NULL DEFAULT '0',
      `headshots` int(11) NOT NULL DEFAULT '0',
      `playtime_versus` int(11) NOT NULL DEFAULT '0',
      `playtime_realism` int(11) NOT NULL DEFAULT '0',
      `playtime_survival` int(11) NOT NULL DEFAULT '0',
      `playtime_scavenge` int(11) NOT NULL DEFAULT '0',
      `playtime_realismversus` int(11) NOT NULL DEFAULT '0',
      `playtime_mutations` int(11) NOT NULL DEFAULT '0',
      `points_survivors` int(11) NOT NULL DEFAULT '0',
      `points_infected` int(11) NOT NULL DEFAULT '0',
      `points_realism` int(11) NOT NULL DEFAULT '0',
      `points_survival` int(11) NOT NULL DEFAULT '0',
      `points_scavenge_survivors` int(11) NOT NULL DEFAULT '0',
      `points_scavenge_infected` int(11) NOT NULL DEFAULT '0',
      `points_realism_survivors` int(11) NOT NULL DEFAULT '0',
      `points_realism_infected` int(11) NOT NULL DEFAULT '0',
      `points_mutations` int(11) NOT NULL DEFAULT '0',
      `versus_kills_survivors` int(11) NOT NULL DEFAULT '0',
      `scavenge_kills_survivors` int(11) NOT NULL DEFAULT '0',
      `realism_kills_survivors` int(11) NOT NULL DEFAULT '0',
      `mutations_kills_survivors` int(11) NOT NULL DEFAULT '0',
      `kill_infected` int(11) NOT NULL DEFAULT '0',
      `kill_hunter` int(11) NOT NULL DEFAULT '0',
      `kill_smoker` int(11) NOT NULL DEFAULT '0',
      `kill_boomer` int(11) NOT NULL DEFAULT '0',
      `kill_spitter` int(11) NOT NULL DEFAULT '0',
      `kill_jockey` int(11) NOT NULL DEFAULT '0',
      `kill_charger` int(11) NOT NULL DEFAULT '0',
      `melee_kills` int(11) NOT NULL DEFAULT '0',
      `infected_spawn_1` int(11) NOT NULL DEFAULT '0',
      `infected_spawn_2` int(11) NOT NULL DEFAULT '0',
      `infected_spawn_3` int(11) NOT NULL DEFAULT '0',
      `infected_spawn_4` int(11) NOT NULL DEFAULT '0',
      `infected_spawn_5` int(11) NOT NULL DEFAULT '0',
      `infected_spawn_6` int(11) NOT NULL DEFAULT '0',
      `infected_spawn_7` int(11) NOT NULL DEFAULT '0',
      `infected_spawn_8` int(11) NOT NULL DEFAULT '0',
      `infected_boomer_blinded` int(11) NOT NULL DEFAULT '0',
      `infected_boomer_vomits` int(11) NOT NULL DEFAULT '0',
      `infected_smoker_damage` int(11) NOT NULL DEFAULT '0',
      `infected_jockey_damage` int(11) NOT NULL DEFAULT '0',
      `infected_jockey_ridetime` float NOT NULL DEFAULT '0',
      `infected_charger_damage` int(11) NOT NULL DEFAULT '0',
      `infected_tank_damage` int(11) NOT NULL DEFAULT '0',
      `infected_hunter_pounce_counter` int(11) NOT NULL DEFAULT '0',
      `infected_hunter_pounce_dmg` int(11) NOT NULL DEFAULT '0',
      `infected_tanksniper` int(11) NOT NULL DEFAULT '0',
      `charger_impacts` int(11) NOT NULL DEFAULT '0',
      `award_pills` int(11) NOT NULL DEFAULT '0',
      `award_medkit` int(11) NOT NULL DEFAULT '0',
      `award_defib` int(11) NOT NULL DEFAULT '0',
      `award_revive` int(11) NOT NULL DEFAULT '0',
      `award_rescue` int(11) NOT NULL DEFAULT '0',
      `award_tankkill` int(11) NOT NULL DEFAULT '0',
      `award_tankkillnodeaths` int(11) NOT NULL DEFAULT '0',
      `award_witchcrowned` int(11) NOT NULL DEFAULT '0',
      `award_witchdisturb` int(11) NOT NULL DEFAULT '0',
      `award_friendlyfire` int(11) NOT NULL DEFAULT '0',
      `award_fincap` int(11) NOT NULL DEFAULT '0',
      `award_teamkill` int(11) NOT NULL DEFAULT '0',
      `award_left4dead` int(11) NOT NULL DEFAULT '0',
      `award_letinsafehouse` int(11) NOT NULL DEFAULT '0',
      `award_allinsafehouse` int(11) NOT NULL DEFAULT '0',
      `award_survivor_down` int(11) NOT NULL DEFAULT '0',
      `award_infected_win` int(11) NOT NULL DEFAULT '0',
      `award_scavenge_infected_win` int(11) NOT NULL DEFAULT '0',
      `award_jockey` int(11) NOT NULL DEFAULT '0',
      `award_charger` int(11) NOT NULL DEFAULT '0',
      `award_gascans_poured` int(11) NOT NULL DEFAULT '0',
      `award_upgrades_added` int(11) NOT NULL DEFAULT '0',
      `award_matador` int(11) NOT NULL DEFAULT '0',
      `award_ledgegrab` int(11) NOT NULL DEFAULT '0',
      `award_scatteringram` int(11) NOT NULL DEFAULT '0',
      `award_bulldozer` int(11) NOT NULL DEFAULT '0',
      `award_perfect_blindness` int(11) NOT NULL DEFAULT '0',
      `award_pounce_nice` int(11) NOT NULL DEFAULT '0',
      `award_pounce_perfect` int(11) NOT NULL DEFAULT '0',
      `award_campaigns` int(11) NOT NULL DEFAULT '0',
      `award_protect` int(11) NOT NULL DEFAULT '0',
      `award_smoker` int(11) NOT NULL DEFAULT '0',
      `award_hunter` int(11) NOT NULL DEFAULT '0',
      `award_adrenaline` int(11) NOT NULL DEFAULT '0',
      `lastannemode` int(11) NOT NULL DEFAULT '0',
      `lastgamemode` int(11) NOT NULL DEFAULT '0',
      PRIMARY KEY (`steamid`)
    ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
    
    -- 2. 创建辅助表
    CREATE TABLE IF NOT EXISTS `settings` (
      `steamid` varchar(64) NOT NULL,
      `mute` int(11) NOT NULL DEFAULT '0',
      PRIMARY KEY (`steamid`)
    ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
    
    CREATE TABLE IF NOT EXISTS `server_settings` (
      `sname` varchar(64) NOT NULL,
      `svalue` varchar(1024) NOT NULL DEFAULT '',
      PRIMARY KEY (`sname`)
    ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
    
    CREATE TABLE IF NOT EXISTS `maps` (
      `name` varchar(64) NOT NULL,
      `gamemode` int(11) NOT NULL DEFAULT '0',
      `custom` int(11) NOT NULL DEFAULT '0',
      `mutation` varchar(64) NOT NULL DEFAULT '',
      `playtime_nor` int(11) NOT NULL DEFAULT '0',
      `playtime_adv` int(11) NOT NULL DEFAULT '0',
      `playtime_exp` int(11) NOT NULL DEFAULT '0',
      `kills_nor` int(11) NOT NULL DEFAULT '0',
      `kills_adv` int(11) NOT NULL DEFAULT '0',
      `kills_exp` int(11) NOT NULL DEFAULT '0',
      `points_nor` int(11) NOT NULL DEFAULT '0',
      `points_adv` int(11) NOT NULL DEFAULT '0',
      `points_exp` int(11) NOT NULL DEFAULT '0',
      `restarts_nor` int(11) NOT NULL DEFAULT '0',
      `restarts_adv` int(11) NOT NULL DEFAULT '0',
      `restarts_exp` int(11) NOT NULL DEFAULT '0',
      `caralarm_nor` int(11) NOT NULL DEFAULT '0',
      `caralarm_adv` int(11) NOT NULL DEFAULT '0',
      `caralarm_exp` int(11) NOT NULL DEFAULT '0',
      PRIMARY KEY (`name`,`gamemode`)
    ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
    
    CREATE TABLE IF NOT EXISTS `timedmaps` (
      `id` int(11) NOT NULL AUTO_INCREMENT,
      `map` varchar(64) NOT NULL,
      `gamemode` int(11) NOT NULL,
      `difficulty` int(11) NOT NULL,
      `mutation` varchar(64) NOT NULL,
      `steamid` varchar(64) NOT NULL,
      `time` float NOT NULL,
      `players` int(11) NOT NULL,
      `mode` int(11) NOT NULL DEFAULT '0',
      `plays` int(11) NOT NULL DEFAULT '1',
      `modified` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
      `created` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
      `sinum` int(11) NOT NULL DEFAULT '0',
      `sitime` int(11) NOT NULL DEFAULT '0',
      `anneversion` varchar(32) NOT NULL DEFAULT '',
      `usebuy` int(11) NOT NULL DEFAULT '0',
      `auto` int(11) NOT NULL DEFAULT '0',
      PRIMARY KEY (`id`)
    ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
    
    -- 3. 创建防火墙触发器 (拦截幽灵账号)
    DELIMITER //
    CREATE TRIGGER `block_bad_id_insert` BEFORE INSERT ON `l4dstats_players` FOR EACH ROW BEGIN
        IF NEW.steamid LIKE '%STOP_IGNORING%' THEN SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT = 'Blocked invalid SteamID'; END IF;
    END;
    //
    DELIMITER ;

第二步:服务器环境准备 (Docker/Linux)

  1. 安装 32位 运行库

    • 如果不装这个,SourceMod 加载 MySQL 驱动会失败。
    • 命令

      sudo dpkg --add-architecture i386
      sudo apt-get update
      sudo apt-get install lib32z1
    • Docker用户:如果重启后失效,参考之前的教程把宿主机的 /usr/lib32/libz.so.1 复制 (docker cp) 到容器的 /游戏目录/bin/ 下。
  2. 升级 SourceMod

    • 务必确保服务器使用的是 SourceMod 1.12。旧版 (1.11) 无法运行此插件。

第三步:修改与编译插件 (Windows)

这是解决“不记录时间”、“人数不足不记录”的关键。

  1. 准备文件

    • l4d_stats.sp 放入 scripting/
    • 下载 left4dhooks.inccolors.inc 放入 scripting/include/
  2. 修改源码 (l4d_stats.sp)

    • 解除无法统计时间限制
      搜索 stock int IsFunGame(),把 return 3; 改为 return 0;
    • 解除人数限制 (可选,方便单人测试):
      搜索 CheckSurvivorsHumans,把判断条件里的 human < 3 改为 human < 1
  3. 编译

    • 把修改后的 .sp 文件拖到 spcomp.exe 上。
    • 得到 l4d_stats.smx

无需编译,我已经编译过了
https://wwbou.lanzouq.com/i0zxZ3d30m8f
密码:fosv


第四步:安装与配置

  1. 上传插件

    • 把编译好的 l4d_stats.smx 放入 addons/sourcemod/plugins/
    • Left4DHooks.smx (依赖插件) 也放入同目录。
    • Left4DHooks.ext.2.l4d2.so (扩展文件) 放入 addons/sourcemod/extensions/
  2. 配置 databases.cfg
    编辑 addons/sourcemod/configs/databases.cfg,加入:

    "l4dstats"  // 名字必须是这个
    {
        "driver"    "default"
        "host"      "172.17.0.1" //你自己的数据库地址
        "database"  "l4d2" //你的数据库名称
        "user"      "l4d2" //你的数据库用户名
        "pass"      "你的密码"
        "port"      "3306"
        "charset"   "utf8mb4"
    }

第五步:启动与验证

  1. 重启服务器
  2. 检查插件状态
    控制台输入 sm plugins list,确保 L4D2 Player StatsLeft4DHooks 都是运行状态。
  3. 检查报错
    查看 addons/sourcemod/logs/errors_xxxx.log。如果按照上述步骤操作,应该只有 Blocked invalid SteamID 这种正常的拦截日志,不应再有 Unknown columnTable doesn't exist
  4. 进服测试

    • 输入 !rank
    • 在服务器里挂机 2分钟
    • 去 phpMyAdmin 刷新 players 表,你会发现 playtime 变成了 2,points 也有了数据。

至此,部署完成!