☰
石器时代老游戏源码复现:服务端编译、数据库配置与联调避坑指南
2026/10/16 19:21:52 网站建设 项目流程

简介:全套石器时代项目源代码,适合想研究经典 C 语言游戏/应用架构的开发者,也适合需要参考完整工程组织方式的初学者。包内共 378 个文件,以 174 个 .h 头文件和 170 个 .c 源文件为主体,配合 makefile、VC6 工程文件(.dsp/.dsw)及 Visual Studio 解决方案(.sln/.vcproj),可以直观看到一套老式 C 项目的模块划分与构建方式;整体仅 1.32MB,便于快速下载比对。从节选内容看,源码覆盖 battle、char、item、magic、npc 等核心模块,适合围绕游戏逻辑、角色状态、战斗流程与消息协议做断点调试和二次开发。已有 2016 人学习下载,对想深入理解 C 语言工程实践、还原早期网络游戏服务端实现的人来说,是一份难得的完整参考资料。

1. 这份“完整”的石器时代源代码,到底能拿来做什么

在怀旧游戏源码交流这个圈子里,被问得最多的一句话不是“能不能跑”,而是“我下载的这份完整石器时代源代码,到底少了什么”。压缩包解压出来几百兆,目录树看着很全,一编译红字一片,一启动进程秒退,折腾两天连登录界面都见不到。其实“完整”从来不是以体积或文件数衡量的,它等于服务端、客户端、数据库脚本、依赖环境四样东西能相互咬合。这篇笔记就是给所有打算认真复现老游戏服务端的人看的:拿到一份自称完整的石器时代源码之后,按什么顺序去盘它、编译它、启动它,以及撞墙之后怎么排查。它的价值不在商业运营,而在让你看清十年前 MMORPG 服务端是怎么把账号、场景、角色、封包串起来的,看完你会觉得这波折腾很值。

2. 拿到源码包先别急着编译:先盘出这份包裹的边界

2.1 完整性判断:服务端、客户端、数据库脚本三件套缺一不可

“完整”这两个字,在不同人嘴里含义完全不同。有人说完整等于服务端能启动,有人说完整等于能进地图打怪,还有人说得自带 GM 命令。按我带别人看源码的经验,真正值得花时间的包,至少要同时具备三块:服务端源码或二进制、对应版本的客户端、数据库初始化脚本。地图资源、封包定义、GM 工具表反而是次要的,因为很多能从客户端里解出来,数据库脚本如果缺失,你连账号表都建不出来。

拿到包以后,先按下面这张表找对应目录,不要被一堆 .cpp 带偏。

组成部分常见目录名缺失时的典型表现
服务端源码Server / LoginServer / GameServer编译不出登录服或游戏服进程
客户端程序与资源Client / bin / res没有验证封包与地图表现的对象
数据库脚本db / database / sql建不了账号、角色、物品表
地图与场景资源map / data / scene进游戏卡在加载或黑屏
封包定义文档protocol / packet / doc客户端与服务器消息对不上

如果数据库脚本目录是空的,别急着编译,先想办法把库建起来,否则后面登录服一启动就报数据库连接失败。很多所谓完整包,其实只缺了脚本,自己补一套基础表并不是难事。核心原则:完整性的定义是“能跑成最小闭环”,不是“文件覆盖有多全”。把“完整”重新理解为“能在本地跑通”,比纠结压缩包体积要实际得多。

2.2 老 Visual C++ 项目的依赖陷阱:运行库、DirectX SDK、第三方库缺失

石器时代这一代游戏源码,最常见的工程环境是 Visual C++ 6.0 或早期 Visual Studio。如果你现在用新版 VS 打开工程,第一关是版本转换向导,第二关是平台工具集,第三关是旧的 WINSOCK 库和 DirectX 库找不到。很多编译错误根本不是语法错误,而是编译器不知道去哪里找 windows.h、winsock.h、ddraw.h。我见过有人把几千个 C2065 报错当成代码问题,实际上把 SDK 路径加上就全好了。

解决套路是:优先在 Windows 虚拟机里装一个老版本 VC++,别用新 VS 强行转换。如果因为兼容性不得不用新 VS,就手工把工程转成 CMake 或新格式,但大把的 include 路径和预处理宏需要重新指。DirectX SDK 的版本也要对上,老游戏客户端常用 DirectDraw,缺了 DX 头文件,所有图形相关文件全挂。运行库方面,要装 VC6 对应的可再发行组件,有些包还会依赖特定版本的 STLport、zlib 或 libpng。遇到这类问题,通常不是代码错,是环境错。

常见依赖清单我一般照着查:VC6 运行库、DirectX 8/9 SDK、Platform SDK、MySQL 客户端库、zlib、STLport。缺哪个,就把哪个加入项目的 include 和 lib 路径,再编译。如果看到第三方库的版本和源码里的注释不一致,尽量以源码为准,老代码对版本很敏感,随便换个新版库可能局部正常、整体崩溃。

2.3 用目录扫描代替人肉翻包:先列清单再动手

一个几百 MB 的源码包,靠资源管理器一层层点开效率太低。我习惯先列目录树和统计各目录体积,看体量分布再决定先研究哪块。比如服务端源码只有几十 MB,却有一个 GB 的地图资源,那关键路径很可能在资源加载和地图格式解析上,而不是逻辑代码;反过来,如果源码有几百 MB 而数据库脚本只有几 KB,就要重点看角色、物品是不是硬编码在 CPP 里,改起来会麻烦得多。

# 列出两层目录,先看顶层结构 find . -maxdepth 2 -type d | sort # 统计每个子目录的体积,找出哪里占了大头 du -sh ./*/ | sort -h # 优先找出所有数据库脚本和启动说明 find . -iname "*.sql" -o -iname "readme*" | sort

这三条命令能在大约 10 秒内把一份源码包的骨架摸清。第一条看目录分层,第二条看资源体量分布,第三条找数据库脚本和说明文件。du 的 -h 输出人类可读体积,sort -h 让最大的目录排在最后,方便一眼看出大头。find 的 -iname 不区分大小写,老代码里 .SQL、.sql、README 都可能存在。

在 Windows 环境没有 find 和 du,可以直接用 Git Bash 或 WSL 跑;如果你把源码丢到 Linux 服务器上研究,原样能跑。拿到新包先做这一步,后面所有排查路径都会清晰很多。我自己的经验是:先盘点,再动手,能省掉至少一个通宵。

3. 把服务端跑起来:编译、配置、启动的最小闭环

3.1 先定环境:Windows 虚拟机 + VC6/VS 编译的常规选择

老古董源码有一个共同特点:对环境极度敏感。同样是 VC6 工程,在 Windows XP 上编译可能顺滑,在 Windows 10 上就报一堆地址和链接错误。所以我一般建议先搭一台干净的 Windows 虚拟机,装好老版 VC++,再配数据库,别在主力开发机上硬刚。虚拟机用桥接网络,方便后面让另一台机器上的客户端连进来测试,多机联调是避免“本机能跑、别人连不上”的最直接手段。

判断用什么编译器的依据是目录树:有 .sln/.vcproj/CMakeLists.txt 就按对应工程走;只有 Makefile 和 GCC 风格源码,说明作者已经搬到 Linux 上了,可以直接用 Linux 环境。遇到只有老式 .dsp/.dsw 文件的包,先去找 VC6 或 VS2008,别拿 VS2022 硬转。语言和运行库也要统一,服务端和客户端如果一边用 ANSI 字符串、一边用 Unicode,日志会全是乱码。

选型还有一个隐藏点:数据库版本。老源码多数配 MySQL 5.x,连接库是 libmysql.dll 的老版本。如果装 MySQL 8,客户端连接库不兼容,即使配置写对了也可能报“无法加载认证插件”。我常备一个 MySQL 5.7 的绿色版,专门用来跑老游戏服务端。这一步定好了,后面 SQL 脚本导入才不会被版本卡住。

3.2 数据库初始化:建库、建表、导入基础数据的 SQL 套路

服务端跑起来前,必须先把数据库准备好。哪怕包里带了脚本,我也建议先手动建一个库,确保字符集和表结构符合预期。老源码有很多把中文内容硬编码在脚本里,字符集选错,后面进游戏全是乱码。常见做法是新建一个独立库,表名前缀和源码里写死的前缀保持一致。

-- 建库,字符集按源码要求选,拿不准先用 utf8mb4 CREATE DATABASE IF NOT EXISTS sa_db CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; USE sa_db; -- 账号表:存登录凭据和 GM 权限 CREATE TABLE `account` ( `id` INT UNSIGNED NOT NULL AUTO_INCREMENT, `username` VARCHAR(32) NOT NULL, `pass_hash` CHAR(32) NOT NULL DEFAULT '', `gm_level` TINYINT UNSIGNED NOT NULL DEFAULT 0 COMMENT '0普通玩家,1普通GM,9超级GM', `create_time` TIMESTAMP NULL DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (`id`), UNIQUE KEY `uk_username` (`username`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; -- 角色表:最核心字段就是所在地图和坐标 CREATE TABLE `role` ( `id` INT UNSIGNED NOT NULL AUTO_INCREMENT, `account_id` INT UNSIGNED NOT NULL, `name` VARCHAR(32) NOT NULL, `map_id` INT UNSIGNED NOT NULL DEFAULT 1000, `x` SMALLINT UNSIGNED NOT NULL DEFAULT 100, `y` SMALLINT UNSIGNED NOT NULL DEFAULT 100, `level` SMALLINT UNSIGNED NOT NULL DEFAULT 1, PRIMARY KEY (`id`), UNIQUE KEY `uk_name` (`name`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

这两张表只是老式 MMORPG 最基本的轮廓,实际源码包里的字段会多得多,有任务状态、宠物信息、装备栏等。拿到完整脚本时优先用原脚本,这里只解决“没有任何 sql 文件”时怎么起步。核心参数:gm_level 决定 GM 命令是否理会你;map_id、x、y 是角色登录后的落点,建角色时必须给初始值,否则会出生在 (0,0) 导致客户端卡死。账号表的主键和唯一键也要提前想好,登录服会按 username 查库。

注意:导入脚本前先看表前缀,有些源码要求sa_account、login_account之类,别把表名写死成自己的。如果源码里用了存储过程,MySQL 版本要支持,老存储过程在 5.7 上通常没问题。导入完成后,用一条 SELECT 验证账号表能查到数据,再启动服务端。

3.3 服务端启动顺序和三个必调参数:监听端口、数据库连接、GM 开关

服务端不是只有一个进程,老游戏常见会拆成登录服和游戏服。启动顺序基本固定:先数据库,再登录服,等登录服监听端口出来,再开游戏服。顺序错了,游戏服连不上登录服,日志里会一直刷连接失败。排查启动问题时,第一步永远是看进程在不在,第二步是看端口有没有起来,第三步才是翻日志。

[login] listen_ip = 0.0.0.0 listen_port = 9001 max_connections = 200 [game] listen_ip = 0.0.0.0 listen_port = 9002 login_host = 127.0.0.1 login_port = 9001 db_host = 127.0.0.1 db_port = 3306 db_user = sa_user db_pass = sa_pass db_name = sa_db

这份配置是我按老端习惯写的示例,重点是三个参数。第一个是 listen_ip,必须设成 0.0.0.0,设成 127.0.0.1 就只能本机连;第二个是 db_host/db_user/db_pass,数据库连接串写错,登录服启动直接退;第三个是 GM 开关,一般在 game 段里单独开,没开的话游戏里敲 GM 命令没有反应。老源码有很多配置项是编译期常量,改 ini 不一定生效,查不到时就去源码里搜端口号和数据库名。

启动后验证端口:

# Windows 下确认端口监听 netstat -ano | findstr :9001 # Linux 下确认 ss -ltnp | grep 9001

看到监听说明登录服已经起来,接下来用客户端连。如果端口没出现,多半是配置里的数据库连接失败,进程起来又自杀。日志文件比控制台输出可靠,老代码喜欢把错误写进 log 目录,按时间戳找最新日志。启动闭环跑通后,才算真正进入客户端调试环节。

4. 客户端连不上?从登录封包到地图资源,把链路拆开看

4.1 客户端和服务端的握手流程:登录验证、角色列表、进入地图

客户端不是一上来就发封包的。老游戏的做法通常是:客户端先请求服务器列表,拿到登录服地址端口,然后 TCP 连上登录服,发账号密码,登录服查库后返回结果,客户端再拿着登录票据去连游戏服,拉角色列表,选择角色后进入地图。这个链路上任何一环断开,表现都只是“连接失败”,很难看出是哪一步挂了。

所以遇到连不上,我会按顺序打点:服务器列表能不能拉到,登录服端口通不通,账号密码能不能通过,游戏服端口通不通,地图资源能不能加载。日志和抓包工具是两大利器。用 Wireshark 的过滤器tcp.port == 9001直接看三包交互,先观察有没有数据回复,再分析内容。如果没有抓包条件,就看服务端日志里在哪个阶段断掉,日志是源码给你的免费调试信息。

老游戏封包不是简单的“长度 + 协议号”,很多会在包头上再做一层加密、压缩或混淆。第一次抓包看到前几个字节不是明文别慌,可能只是这一代端的加密方式,先确认链路通不通,再研究怎么解码。加密层的调试要放到后面,别一上来就卡在解密上。

4.2 用 Python 写一个最小封包观察器,看请求与响应的链路

我习惯用 Python 快速验证服务端端口的行为,比反复启动客户端工程省时间。下面这段代码就是一个最小的封包观察器:连接登录端口,发送一个按长度前缀封装的请求包,读取服务端返回的原始字节并打印十六进制。它不做解密,只确认链路和数据层有没有响应。

import socket import struct def recv_exact(sock, n): """读满 n 个字节,缺一个字节都不返回""" buf = b"" while len(buf) < n: chunk = sock.recv(n - len(buf)) if not chunk: raise ConnectionError("连接被对端关闭") buf += chunk return buf def read_packet(sock): """读取一个标准长度前缀封包,前2字节等于总长度""" head = recv_exact(sock, 2) (total,) = struct.unpack("<H", head) # 小端无符号短整数 if total < 2: raise ValueError(f"非法包长: {total}") body = recv_exact(sock, total - 2) return body sock = socket.create_connection(("127.0.0.1", 9001), timeout=3) # 构造一个最小请求包:协议号 + 定长账号 + 定长密码 # 真实协议差异很大,这里只是演示,跑通前先看有没有回包 proto = b"\x01\x00" username = b"A" * 8 password = b"B" * 8 body = proto + username + password packet = struct.pack("<H", len(body) + 2) + body sock.sendall(packet) try: resp = read_packet(sock) except TimeoutError: print("超时:服务端没回包,检查端口和监听IP") else: print("响应字节数:", len(resp), "前16字节:", resp[:16].hex()) sock.close()

代码里的 read_packet 是通用长度前缀封包读取,适合所有“前两字节为长度”的协议。struct.unpack("<H", head) 按小端序解出无符号短整数,total 是整个包的长度,所以 body 要再读 total-2 字节。如果服务端用大端序,就要把<改成>。这个观察器的价值在于:它把网络调试从“客户端 + 服务端 + 资源包”的大系统里剥离出来,只留下最原始的 TCP 数据,能立刻判断是服务端没回包,还是客户端处理不了。超时并不代表协议不对,也可能只是服务端收包后主动断开,要多打印 recv 的原始异常。

把这套脚本扩展成循环抓包,再配合数据库日志,通常能很快定位登录阶段的问题。脚本里端口、协议号、账号字段长度都要按实际源码改,别拿示例数据直接套生产包。

4.3 地图和战斗资源缺失的典型表现:客户端卡在“载入中”

登录验证过了,角色列表也能看到,一进游戏就卡在载入中,这是最典型的资源缺失症状。原因通常有三个:客户端资源包里没有对应地图 ID 的地图文件;地图文件在但格式不被客户端识别;服务端下发的初始地图 ID 和客户端资源目录对不上。表现都一样,但排查方向不同。

先查角色表里的初始 map_id 是不是存在的地图编号。很多老端出生点是 1000,如果你把角色表里的默认地图设成 1001,而客户端没有 1001 的资源,就会一直转圈。把 map_id 改回一个确定可用的编号,比如 1000,再重新登录测试,这是最快的一招。如果是地图格式问题,就得找地图编辑器或服务端源码里的地图导出工具,检查资源和客户端的版本是否配套。战斗资源缺失一般是战斗动画文件缺,客户端会在进入战斗时闪退或黑屏,体现得比地图更晚。

地图这一块是很多源码包“完整”承诺里最脆弱的环节。有些包为了减小体积会把 res 目录里的高材质清掉,只留低清版,看起来还能跑,但一换地图就报错。遇到这种问题要尽量先找回配套客户端,别急着改代码。地图 ID、地图文件格式、客户端资源版本三个条件都对齐,才能安然进图。

5. 避坑:编译、启动、连线的五个常见问题

5.1 编译报错 [C2065]:未声明标识符,实际是平台工具集和依赖没对齐

现象:一编译就是几百条 error C2065,报的还都是系统 API,比如 CreateWindow、WSAStartup、DDraw 相关的函数未声明。看起来像源码缺文件,实际上大多是编译器找不到 Windows SDK 的 include 路径和库路径。VC6 的默认 SDK 路径和后来的 Visual Studio 不一样,老工程在 VS2022 里打开,平台工具集会变成 v143,头文件路径全部偏移。

原因:代码没变,是依赖没对齐。解决方法是把平台工具集改回 v100 或 v120 再编译,或者手动在工程属性的“VC++ 目录”里补上 Windows SDK 的 include 和 lib 路径。如果还不行,确认是否装了 DirectX SDK,并把 DX 的 include/lib 放到搜索路径的最前面。记住一条:C2065 大爆发时,先怀疑环境,再怀疑代码。

5.2 启动后进程秒退,先查配置不是先查代码

现象:登录服或游戏服进程双击后窗口一闪就没了,没有任何弹窗报错。新手最容易一头扎进源码里找 main 函数,其实老服务端的 main 通常很短,真正决定生死的是配置加载。配置里数据库连不上、必要文件不存在、监听端口被占用,都会导致进程自己退出,而窗口一闪就看不到日志。

原因:日志被写到 log 目录,控制台窗口关闭了,不代表没记录。解决:先看 log 目录下最新日志文件,按时间戳排序,找最后几行。典型报错是 “connect database failed”、“open profile failed”、“bind port failed”。然后回配置改,而不是改代码。我通常会把进程从命令行启动而不是双击,这样退出前打印的堆栈还能留在终端里。

5.3 本机能登录、局域网连不上:监听地址写死了 127.0.0.1

现象:同一台机器上客户端能登录,放到另一台电脑上就提示“无法连接服务器”。原因非常典型:服务端配置里的 listen_ip 是 127.0.0.1,服务只绑了回环地址,外面的机器当然连不进来。有些源码还会在代码里写死 socket bind 的地址,配置文件根本没生效。

解决:把配置里 listen_ip 改成 0.0.0.0,再检查服务端防火墙有没有放行对应端口。如果改配置没用,就在源码里搜 “127.0.0.1” 和 “INADDR_LOOPBACK”,很可能有个常量写死在 init 函数里。改完重新编译,用 netstat 确认监听地址变成 0.0.0.0 而不是 127.0.0.1。这个问题是联调中最高频的,优先级排很高。

5.4 账号能建、进不去地图:数据库字符集把中文地图名搞成乱码

现象:账号能创建,角色也能选,但一进地图就断开或卡住;服务端日志里地图名是一串问号。原因多半是数据库字符集不一致。老脚本用 latin1 写入的中文,你在建库时用了 utf8mb4,读取时按 utf8 转码变成乱码,地图查不到,角色自然进不去。或者反过来,源码连接串里没指定 charset,MySQL 连接默认用了错误字符集。

解决:先看脚本开头有没有 SET NAMES 或者 DEFAULT CHARSET 声明,保持建库、脚本、连接串三者字符集一致。最简单的方法是重建库,严格按脚本里的 charset 来,如果脚本没有明确声明,先用 latin1 试,再用 utf8mb4 试。登录服连接串里加上 charset=utf8mb4 或 charset=latin1,和库一致。改完清掉角色表重来,别在脏数据上反复试。

5.5 GM 命令无效:不是功能没写,是权限号没赋

现象:游戏里敲 GM 命令没反应,或者提示“无权限”,源码里明明看到 GM 函数实现。原因通常是账号表里 gm_level 是 0,服务端校验权限不通过。有的端还分普通 GM 和超级 GM,命令要求 9,你给 1 它照样不理。

解决:直接在数据库里把账号 gm_level 改成源码要求的数值,改完重启登录服让权限缓存刷新。如果改了还是无效,去源码里搜 GM 命令的权限常量,确认服务端读哪张表哪个字段,重点看它读的是 account.gm_level 还是角色表里一个独立字段。权限字段赋值后,要让客户端也重新登录,很多权限是在登录时写入内存的,不是每发一次命令查一次库。

6. 收尾:进阶玩法——改经验倍率、加物品、换地图名,以及一条保命原则

6.1 改经验倍率:在数据库参数表里做乘法,不在代码里做加法

跑通之后,大多数人第一件事是改经验倍率让自己爽一下。常见做法是找到经验费率表或全局配置表,比如 exp_rate 或者 rate,把杀怪经验值做乘法。我一般建议在数据库层做,因为老源码结构不熟,动代码容易把公式改崩。

-- 假设有全局参数表,先查一下当前倍率 SELECT * FROM exp_rate WHERE map_id = 0; -- 把经验倍率从 1 改成 10 UPDATE exp_rate SET exp_rate = 10 WHERE map_id = 0;

注意有些端是“基础经验 × 倍率 × 组队加成”,改完倍率后数值变化很大,别人可能无法复现。所以先在单人地图验证,确认角色的经验数值符合预期,再决定是否全局改。改完要重启游戏服,很多倍率参数只在启动时加载一次。

6.2 新增物品:从模板表到掉落表的三个地方要同步

新增一个物品不是只往 item 表插一行,老端通常要同时处理三处:物品模板表、掉落表、客户端资源里的图标和说明。三处缺一处,要么物品看不见,要么捡起来是问号。

-- 1. 物品模板表,定义 ID、名称、类型、堆叠上限和售价 INSERT INTO item_template (id, name, type, max_stack, sell_price) VALUES (90001, '测试纪念品', 1, 20, 100); -- 2. 掉落表,把新物品挂到某个 NPC 上,drop_rate 是权重 INSERT INTO npc_drop (npc_id, item_id, drop_rate) VALUES (10101, 90001, 5); -- 3. 记得更新物品描述表,否则客户端显示空白 INSERT INTO item_desc (item_id, desc_text) VALUES (90001, '本地学习用的测试物品');

这里的表名和字段是按通用结构举例,实际端可能用 item_data、drop_list、item_extra 之类的名字,先在源码里确认。加物品最怕的是 ID 冲突,建议从 90000 开始加,避免覆盖原版 ID。如果在客户端看不到图标,则是资源表的问题,别在数据库里反复折腾。

6.3 版权红线:学习、复原、写技术文章可以,但别拿它做运营

最后一条是保命原则。这类老游戏源代码和历史资源绝大多数有著作权归属,你可以用它做本地学习、做服务端架构复盘、写技术文档,但不要拿去开服运营,也不要公开传播完整客户端和服务端资源。公开的源码包本身来源模糊,我自己的习惯是:只保留少量必要代码做笔记,完整包放在不联网的虚拟机里研究,文章里只贴原理和关键片段,不打包分享。

折腾老源码的过程,让我养成了拿到任何项目先盘目录、再配环境、后跑闭环的习惯。这个习惯帮我少走很多弯路。这篇笔记如果能让你少掉几根头发,那我也算没白写,希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询