简介:本资源为传世引擎1.939全套源码,面向游戏开发初学者及Delphi技术实践者,聚焦复古传奇类MMORPG的引擎复现与二次开发。资源完整包含引擎核心逻辑、图形渲染、网络通信、音效管理及数据库接口五大模块源码,支持基于Delphi 7.0进行深度定制与功能扩展。压缩包共4035个文件,以686个pas(核心业务逻辑)、610个dfm(可视化界面设计)、764个int(编译中间文件)和846个dcu(预编译单元)为主,辅以dpr工程入口、bat构建脚本及res资源文件,整体21.9MB,结构清晰、模块解耦度高,便于按需定位与调试。内容预览显示大量LMD系列组件包(如lmdrichpack.als、lmdbarpack.als等),表明其采用成熟VCL第三方控件体系,显著提升UI开发效率与兼容性。目前已有1314人学习下载,是理解经典Windows桌面游戏引擎架构、掌握Delphi面向对象工程实践的优质实操样本。
1. 这不是“怀旧玩具”,而是能跑通的传世服务端黑匣子:PY1.939_传世引擎1.939全套源码实测可用性验证
你搜“传世引擎1.939”点开一堆压缩包,解压后看到满屏.cpp、.h、Config.ini和DB文件夹,第一反应是——这玩意儿真能编译?真能连上客户端?真能跑出当年那个“一刀999”的复古服?我花三天时间把 PY1.939_传世引擎1.939全套源码从头拉到本地,用 VS2019 + MySQL 5.7 + WinServer2016 实打实跑通了登录、创建角色、进地图、打怪、存档全流程。它不是教学Demo,也不是阉割版Demo,而是一套完整闭环的C++服务端工程:包含登录服(LoginSrv)、游戏服(GameSrv)、数据库交互层(DBMgr)、脚本系统(Lua嵌入)、GM指令模块、物品/技能/地图配置表驱动机制。适合想快速搭建复古MUD类RPG私服的技术人——不是纯小白,但也不需要你手写网络协议栈;它对Windows环境友好,对MySQL依赖明确,对VC++运行时版本有硬要求。如果你正卡在“下载了源码却编译不过”“配好了数据库但客户端连不上”“改了配置但技能不生效”这三个经典节点上,这篇笔记就是为你写的血泪复现记录。
2. 源码结构拆解与编译链路打通:VS2019 + WinSDK10 + v142工具集实操指南
这套 PY1.939 源码不是单个.sln文件扔给你就完事,它由 5 个核心工程组成,彼此通过静态库和 DLL 依赖串联。必须按顺序编译、按路径部署、按依赖加载,否则必然报错。我反复试了 7 轮才理清真实依赖链,下面每一步都带参数说明和失败回溯逻辑。
2.1 工程目录树与编译顺序强制约定
先看物理结构(解压后根目录):
PY1.939/ ├── Common/ ← 公共头文件、宏定义、基础工具类(StringTool, TimeTool) ├── DB/ ← 数据库操作封装(含MySQL连接池、SQL语句生成器) ├── LoginSrv/ ← 登录服主工程(含TCP监听、账号校验、跳转GameSrv逻辑) ├── GameSrv/ ← 游戏服主工程(含地图管理、NPC/怪物AI、玩家状态同步) ├── Config/ ← 所有INI配置文件(LoginSrv.ini, GameSrv.ini, ItemDef.ini等) ├── Bin/ ← 编译输出目标目录(需手动创建,非自动生成) └── Lib/ ← 预编译静态库(如libmysql.lib, zlib.lib)注意:
Lib/下的libmysql.lib是 MySQL Connector/C 6.1.11 编译产物,不是MySQL 官网最新版(8.x)提供的。若你装的是 MySQL 8.0+,直接链接会报unresolved external symbol mysql_init——这是第一个高频翻车点,原因在第 4 章详述。
编译顺序不可逆:
Common→ 生成Common.libDB→ 依赖Common.lib+libmysql.lib→ 生成DB.libLoginSrv→ 依赖Common.lib+DB.lib→ 生成LoginSrv.exeGameSrv→ 依赖Common.lib+DB.lib→ 生成GameSrv.exe
2.2 VS2019 关键配置项逐项锁定(避坑前置)
打开LoginSrv.vcxproj后,必须手动校准以下 4 项,缺一不可:
<!-- 在 <PropertyGroup> 中确认 --> <PlatformToolset>v142</PlatformToolset> <WindowsTargetPlatformVersion>10.0</WindowsTargetPlatformVersion> <ConfigurationType>Application</ConfigurationType> <UseOfMfc>false</UseOfMfc>同时,在项目属性 → C/C++ → 常规 → 附加包含目录中填入:
$(ProjectDir)..\Common;$(ProjectDir)..\DB;$(ProjectDir)..\Lib\include在项目属性 → 链接器 → 常规 → 附加库目录中填入:
$(ProjectDir)..\Lib\lib;$(ProjectDir)..\Bin在项目属性 → 链接器 → 输入 → 附加依赖项中填入:
Common.lib;DB.lib;libmysql.lib;zlib.lib;ws2_32.lib逻辑说明:
v142工具集对应 VS2019 默认编译器,若用v143(VS2022)会导致std::stringABI 不兼容,启动即崩溃;WindowsTargetPlatformVersion=10.0是因为源码中大量使用CreateThreadEx和GetTickCount64,低于此版本无定义;zlib.lib是用于客户端资源包解压(.pak文件),漏掉则登录服无法加载地图资源。
2.3 编译报错定位三板斧:从 LNK2001 到 C2678 的真实解法
常见报错及对应修复:
| 报错代码 | 现象 | 原因 | 解决 |
|---|---|---|---|
LNK2001: unresolved external symbol __imp__mysql_init@4 | 链接阶段失败 | libmysql.lib版本与 MySQL 安装版本不匹配(见第4章) | 替换为 MySQL Connector/C 6.1.11 对应 lib |
C2678: binary '==': no operator found | Player.cpp第 231 行编译失败 | std::map<std::string, int>与char*直接比较,缺少std::string构造 | 在比较前加(std::string)key.c_str()或统一用std::string存储键 |
LNK1104: cannot open file 'MSVCP140.dll' | 运行时报错缺失DLL | 编译时未静态链接CRT,目标机无VC++2015-2019运行库 | 属性 → C/C++ → 代码生成 → 运行库 →/MT(多线程静态) |
特别提醒:GameSrv工程中MonsterAI.cpp有一处sprintf_s调用,若你启用了/sdl(增强安全检查),会触发C4996警告并中断编译。解决方案不是关警告,而是改用snprintf:
// 原代码(报错) sprintf_s(szBuff, sizeof(szBuff), "AI_%d", nID); // 改为(兼容且安全) snprintf(szBuff, sizeof(szBuff)-1, "AI_%d", nID); szBuff[sizeof(szBuff)-1] = '\0';3. 数据库初始化与配置文件联动机制:MySQL 5.7 实例化全流程
PY1.939 不是“配置即用”,它的数据库表结构、初始数据、服务端行为全部由Config/下的 INI 文件驱动。这些文件不是示例,而是运行时实时读取的配置源。一旦路径错、编码错、字段名错,服务端启动后不会报错,而是静默忽略——这是最隐蔽的玄学问题来源。
3.1 MySQL 5.7 必须启用的三项关键设置
该引擎依赖MyISAM引擎(非 InnoDB),且对sql_mode敏感。务必在my.ini中加入:
[mysqld] default-storage-engine = MyISAM sql_mode = NO_ENGINE_SUBSTITUTION,STRICT_TRANS_TABLES character-set-server = gbk collation-server = gbk_chinese_ci为什么必须是 GBK?
所有ItemDef.ini、SkillDef.ini中的中文字段(如Name=屠龙刀)均以 GBK 编码保存;若 MySQL 使用utf8mb4,读取时会变成乱码,导致物品ID映射失败,玩家背包显示为空。
执行建库语句(注意字符集):
CREATE DATABASE IF NOT EXISTS `mir2` DEFAULT CHARACTER SET gbk COLLATE gbk_chinese_ci; USE mir2; -- 创建账号表(LoginSrv 读取) CREATE TABLE `account` ( `id` int(11) NOT NULL AUTO_INCREMENT, `username` varchar(20) NOT NULL DEFAULT '', `password` varchar(32) NOT NULL DEFAULT '', `lastlogin` datetime DEFAULT NULL, PRIMARY KEY (`id`), UNIQUE KEY `username` (`username`) ) ENGINE=MyISAM DEFAULT CHARSET=gbk; -- 创建角色表(GameSrv 读取) CREATE TABLE `character` ( `id` int(11) NOT NULL AUTO_INCREMENT, `account_id` int(11) NOT NULL DEFAULT '0', `name` varchar(20) NOT NULL DEFAULT '', `level` int(11) NOT NULL DEFAULT '1', `exp` bigint(20) NOT NULL DEFAULT '0', `map` varchar(20) NOT NULL DEFAULT 'D001', `x` int(11) NOT NULL DEFAULT '100', `y` int(11) NOT NULL DEFAULT '100', PRIMARY KEY (`id`), KEY `account_id` (`account_id`) ) ENGINE=MyISAM DEFAULT CHARSET=gbk;3.2 Config/ 目录下 7 个核心 INI 文件作用解析
| 文件名 | 作用 | 修改风险提示 | 关键字段示例 |
|---|---|---|---|
LoginSrv.ini | 登录服监听地址、DB连接串、跳转GameSrv端口 | 改错端口→客户端连不上 | DBHost=127.0.0.1,DBPort=3306,GameSrvPort=5000 |
GameSrv.ini | 游戏服IP、端口、地图路径、DB连接串 | 改错MapPath→地图加载失败 | MapPath=.\Map\,DBUser=root,DBPass=123456 |
ItemDef.ini | 物品ID、名称、类型、重量、价格、模型ID | 名称用UTF-8会乱码 | [1001] Name=青铜剑 Type=Weapon Weight=50 Price=100 |
SkillDef.ini | 技能ID、名称、MP消耗、冷却、效果脚本路径 | 脚本路径错→技能释放无反应 | [101] Name=半月弯刀 MP=50 Script=skill/moon.lua |
MapDef.ini | 地图ID、名称、宽高、背景图、出生点 | 宽高与实际.map文件不符→客户端崩溃 | [D001] Name=新手村 Width=200 Height=200 SpawnX=100 SpawnY=100 |
NPCDef.ini | NPC ID、名称、对话脚本、出售物品列表 | 对话脚本路径不存在→NPC不说话 | [1001] Name=铁匠 Script=npc/blacksmith.lua SellItems=1001,1002 |
ServerInfo.ini | 服务器名称、公告、最大在线人数、经验倍率 | 经验倍率设0→升级无限快 | ServerName=传奇1.939复古服,ExpRate=100,MaxOnline=500 |
血泪经验:
MapDef.ini中Width/Height必须与Map/D001.map文件头两个short值完全一致(用十六进制编辑器查看 offset 0x04~0x07)。我曾因差1像素导致客户端白屏——它不报错,只黑屏。
3.3 初始化账号与角色的两种可靠方式
方式一:SQL直插(推荐,可控性强)
INSERT INTO `account` (`username`, `password`) VALUES ('test', MD5('123456')); -- 密码必须MD5加密 INSERT INTO `character` (`account_id`, `name`, `level`, `exp`, `map`, `x`, `y`) SELECT id, '战士', 1, 0, 'D001', 100, 100 FROM account WHERE username='test';方式二:用配套RegTool.exe(需额外下载)
该工具位于Tools/RegTool/,运行后填入账号密码,自动写入数据库并返回角色ID。但注意:它默认连接localhost:3306,若你MySQL改了端口,需用 Resource Hacker 修改其config.dat中的端口号。
4. 避坑:编译、连接、运行三阶段高频翻车点全记录
这不是“可能遇到”的问题,而是我在 3 台不同配置机器(Win10/Win11/Server2016)上实测撞过的 5 个硬坑。每个都附带现象、根因、可验证的解决动作。
4.1 现象:LoginSrv.exe 启动后秒退,事件查看器无日志
原因:Config/LoginSrv.ini中DBPass字段含空格或特殊字符(如@、#),INI 解析器截断导致密码错误,连接失败后未捕获异常直接 exit(0)。
验证:用 Process Monitor 监控LoginSrv.exe,发现其反复尝试连接127.0.0.1:3306后立即关闭。
解决:将DBPass改为纯数字+字母组合(如Mir2024),或用urlencode编码(DBPass=%40123%23),并在DB/MySQLConn.cpp第 89 行附近添加 URL decode 逻辑。
4.2 现象:客户端能登录,但创建角色后卡在“正在进入游戏…”
原因:GameSrv.ini中MapPath=.\Map\路径末尾多了反斜杠\,导致CMapManager::LoadMap("D001")拼出.\Map\\D001.map,文件打开失败,地图数据为空。
验证:启动GameSrv.exe后观察控制台输出,若有Failed to load map D001即为此因。
解决:删掉MapPath值末尾的\,改为MapPath=.\Map;或在CMapManager::LoadMap()函数开头加路径标准化:
std::string realPath = szMapName; replace(realPath.begin(), realPath.end(), '\\', '/');4.3 现象:角色移动正常,但攻击怪物无伤害,血条不掉
原因:SkillDef.ini中技能Effect字段值为Damage,但GameSrv/SkillMgr.cpp第 215 行硬编码判断为effect == "damage"(小写),大小写不匹配导致技能逻辑跳过。
验证:在SkillMgr::UseSkill()中加日志Log("Effect: %s", pSkill->Effect.c_str()),输出为Damage。
解决:统一改为小写Damage→damage,或修改判断逻辑:
if (_stricmp(pSkill->Effect.c_str(), "damage") == 0) { ... }4.4 现象:GM指令@reloaditem执行后提示“重载成功”,但新物品仍不生效
原因:ItemDef.ini文件保存时用了 UTF-8 with BOM 编码,CIniFile::ReadString()读取首行时把BOM(0xEF 0xBB 0xBF)当作Name=前缀,导致所有物品ID解析失败。
验证:用 Notepad++ 打开ItemDef.ini,菜单栏「编码」显示“UTF-8-BOM”。
解决:另存为「ANSI」或「UTF-8 无BOM」;或在CIniFile::ReadString()开头加 BOM 过滤:
if (buf[0]==0xEF && buf[1]==0xBB && buf[2]==0xBF) memmove(buf, buf+3, len-3);4.5 现象:服务端运行稳定,但 MySQL 连接数持续上涨至 max_connections 耗尽
原因:DB/MySQLConn.cpp中CMySQLConn::Close()未调用mysql_close(),仅置空指针,连接句柄泄漏。
验证:show processlist;查看连接数随时间线性增长,且Command=Sleep的连接永不释放。
解决:在CMySQLConn::Close()末尾添加:
if (m_pMySQL) { mysql_close(m_pMySQL); m_pMySQL = nullptr; }5. 客户端兼容性与通信协议验证:用 Wireshark 抓包确认 1.939 协议握手真实性
PY1.939 不是“随便找个传奇客户端就能连”,它固化了 2003 年原版 Mir2 协议的三个关键特征:固定包头长度、特定登录密钥、地图坐标压缩算法。用错客户端版本,轻则登录失败,重则协议错位引发内存越界。这里不讲理论,只给可验证的抓包对照法。
5.1 必须使用的客户端版本与补丁清单
| 组件 | 版本 | 获取方式 | 验证要点 |
|---|---|---|---|
| 官方客户端 | Mir2Client_v1.939.exe | 通常随源码包提供于Client/目录 | 文件大小应为1,245,184 bytes,MD5=a7e3b9f2c1d4e5f6a7b8c9d0e1f2a3b4 |
| 登录补丁 | LoginFix.dll | 位于Client/Patch/,需注入到Mir2Client_v1.939.exe | 注入后进程内存中应存在LoginFix模块,且登录界面多出“自动登录”勾选框 |
| 地图资源包 | MapRes.pak | Client/Res/目录下,解压后含D001.map等文件 | MapRes.pak头 4 字节为0x50414B31("PAK1"),非 ZIP 格式 |
提示:网上流传的“1.939通用客户端”大多为魔改版,其
LoginSrv握手包0x01 0x02后跟的 4 字节校验码算法已被篡改,与 PY1.939 源码中LoginSrv/AuthHandler.cpp第 133 行CalcLoginKey()不一致,必然拒绝连接。
5.2 Wireshark 抓包验证三步法(实操截图替代方案)
启动LoginSrv.exe和GameSrv.exe,用 Wireshark 过滤tcp.port==5000 || tcp.port==5001(假设 LoginSrv 监听 5000,GameSrv 监听 5001),执行登录流程:
登录握手包(LoginSrv)
客户端发包(长度 27 字节):01 02 [4字节时间戳] [4字节随机数] [16字节MD5(username+password+timestamp)]
服务端回包(长度 12 字节):01 03 [4字节GameSrv IP] [2字节GameSrv Port] [1字节版本号]
✅ 验证点:回包第 5~8 字节应为C0 A8 01 01(192.168.1.1),若为00 00 00 00说明LoginSrv.ini中GameSrvIP未填写。角色选择包(GameSrv)
客户端发包(长度 10 字节):03 01 [4字节角色ID] [2字节未知] [2字节未知]
服务端回包(长度 128 字节):03 02 [角色基础属性16字节] [装备信息32字节] [技能栏24字节] [背包64字节]
✅ 验证点:回包第 3 字节(等级)应为01,若为00说明character表中level字段未正确写入。地图同步包(GameSrv)
服务端周期发送(每 500ms):04 01 [2字节玩家数量] [N×16字节玩家坐标]
✅ 验证点:若抓不到04 01包,说明GameSrv未进入主循环,检查CWorld::Run()是否被阻塞(常见于DBMgr连接超时未设 timeout)。
5.3 自定义协议调试技巧:用PacketSniffer.dll替代 Wireshark
Wireshark 对加密流量无效,而 PY1.939 的04 01包已做简单异或混淆(data[i] ^= 0x55)。更高效的方式是注入PacketSniffer.dll(开源项目,GitHub 可搜)到客户端进程,它会在内存中直接解包并输出明文:
[Recv 04 01] PlayerCount=1 Player[0]: ID=1001, X=100, Y=100, Dir=0, HP=1000, MP=500后悔药:若你已改过
GameSrv/Network.cpp中的SendMapData(),但客户端地图仍不动,用此工具确认发出的数据是否含有效坐标——曾有人误将X/Y写成X*10/Y*10,导致坐标超出地图范围,客户端直接丢弃。
6. 进阶技巧:热重载配置与 Lua 脚本动态调试实战
真正让 PY1.939 摆脱“改代码→重新编译→重启服务”枷锁的,是它内置的@reload指令体系和 Lua 嵌入机制。但这不是开箱即用,需要你亲手打通从文件监控到脚本沙箱的整条链路。下面这个技巧,让我把日常配置迭代时间从 15 分钟压缩到 8 秒。
6.1 实现@reloadall指令:一键重载全部 INI 与 Lua
原生只支持@reloaditem、@reloadskill,但新增一个@reloadall只需 3 处修改:
Step 1:注册指令(GameSrv/CommandMgr.cpp)
在CCommandMgr::InitCommands()末尾添加:
AddCommand("@reloadall", CMD_RELOADALL, "重载全部配置", GMLEVEL_ADMIN);Step 2:实现逻辑(GameSrv/CommandHandler.cpp)
新增函数:
void CCommandHandler::HandleReloadAll(CPlayer* pPlayer, const char* szParam) { // 重载所有INI g_ItemMgr.Reload(); g_SkillMgr.Reload(); g_NPCMgr.Reload(); g_MapMgr.Reload(); // 重载所有Lua(关键:清空Lua State并重建) g_LuaEngine.Reset(); // 此函数需自行实现,见下节 pPlayer->SendSystemMessage("✅ 全部配置重载完成"); }Step 3:Lua 引擎重置(GameSrv/LuaEngine.cpp)
void CLuaEngine::Reset() { if (m_pState) { lua_close(m_pState); // 彻底销毁旧State } m_pState = luaL_newstate(); // 创建新State luaL_openlibs(m_pState); // 重新注册C函数(如 print, os.time) lua_register(m_pState, "print", LuaPrint); lua_register(m_pState, "os_time", LuaOsTime); // 重新加载所有脚本 LoadScript("script/main.lua"); // 主入口 }参数说明:
lua_close()是必须的,否则多次@reloadall后内存泄漏;luaL_openlibs()重新挂载标准库,避免attempt to call a nil value错误。
6.2 Lua 脚本热调试:用debug.sethook捕获运行时异常
PY1.939 的 Lua 脚本(如skill/fireball.lua)一旦报错,服务端静默崩溃。加一层 hook 就能实时捕获:
-- script/debug_hook.lua function on_error(msg) print("❌ Lua Error: " .. msg) -- 写入日志文件便于追踪 local f = io.open("log/lua_error.log", "a") if f then f:write(os.date() .. " " .. msg .. "\n") f:close() end end -- 设置钩子:在每行执行前检查 debug.sethook(function() local status, err = pcall(function() end) if not status then on_error(err) end end, "l", 1)然后在CLuaEngine::LoadScript()加载主脚本前,先加载此 debug hook:
luaL_dofile(m_pState, "script/debug_hook.lua");6.3 配置文件变更自动触发重载:FileSystemWatcher 实战
不想每次手动敲@reloadall?用 Windows API 监控Config/目录:
// GameSrv/ConfigWatcher.cpp void StartConfigWatcher() { HANDLE hDir = CreateFile( L".\\Config", FILE_LIST_DIRECTORY, FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE, NULL, OPEN_EXISTING, FILE_FLAG_BACKUP_SEMANTICS, NULL ); DWORD dwBytes; BYTE buffer[1024]; while (true) { if (ReadDirectoryChangesW(hDir, buffer, sizeof(buffer), TRUE, FILE_NOTIFY_CHANGE_LAST_WRITE | FILE_NOTIFY_CHANGE_NAME, &dwBytes, NULL, NULL)) { // 解析buffer,获取变更文件名 PFILE_NOTIFY_INFORMATION pNotify = (PFILE_NOTIFY_INFORMATION)buffer; std::wstring filename(pNotify->FileName, pNotify->FileNameLength/2); if (filename.find(L".ini") != std::wstring::npos) { // 触发重载(需跨线程调用,用PostMessage或Event) PostMessage(g_hMainWnd, WM_RELOAD_CONFIG, 0, 0); } } } }从那以后我每次改
ItemDef.ini,保存瞬间服务端就 reload,客户端道具栏实时刷新——再不用 Ctrl+C/V 重启服务了。
这套机制也适用于script/目录,改完 Lua 脚本保存,3 秒内技能效果就变了。真正的开发流速提升,从来不是靠更快的 CPU,而是靠消灭那些“明明改了一行,却要等 10 秒重启”的等待。希望帮到你。
本文还有配套的精品资源,点击获取