news 2026/10/1 8:58:07

PY1.939传世引擎源码实测编译与运行全指南

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
PY1.939传世引擎源码实测编译与运行全指南

简介:本资源为传世引擎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 章详述。

编译顺序不可逆:

  1. Common→ 生成Common.lib
  2. DB→ 依赖Common.lib+libmysql.lib→ 生成DB.lib
  3. LoginSrv→ 依赖Common.lib+DB.lib→ 生成LoginSrv.exe
  4. GameSrv→ 依赖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 foundPlayer.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.iniNPC 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.pakClient/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),执行登录流程:

  1. 登录握手包(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未填写。

  2. 角色选择包(GameSrv)
    客户端发包(长度 10 字节):
    03 01 [4字节角色ID] [2字节未知] [2字节未知]
    服务端回包(长度 128 字节):
    03 02 [角色基础属性16字节] [装备信息32字节] [技能栏24字节] [背包64字节]
    ✅ 验证点:回包第 3 字节(等级)应为01,若为00说明character表中level字段未正确写入。

  3. 地图同步包(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 秒重启”的等待。希望帮到你。

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

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/1 8:56:49

ORCA多智能体避碰算法:从速度障碍到RVO2实战全解

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/10/1 8:56:12

浪涌测试实战:1.2/50μs、共模差模、台面搭建与整改

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/10/1 8:55:00

ROS坐标系与TF树实战:map、odom、base_link、laser到底怎么用

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/10/1 8:54:37

MATLAB手写数字识别系统实战:从MNIST到图像预处理与模型调优

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/10/1 8:54:18

HikariCP底层原理与生产故障排查指南

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华