简介:WzComparerR2是一款面向冒险岛玩家、MOD制作者与游戏数据分析爱好者的免费WZ文件读取与比较工具,用于解析客户端base.wz中的地图、装备、技能、怪物属性等核心数据,解决游戏数据难以直观查看与版本差异对比的问题。资源包共32个文件,以28个dll动态链接库为主,配合1个exe主程序、1个lua脚本、1个xml配置与1个config文件,整体约4.78MB,涵盖WzLib解析库、MapRender地图渲染、Avatar角色模拟、MonsterCard怪物卡片、LuaConsole脚本控制台及Plugin插件等多个功能模块,结构完整。目前已有3634人学习下载。借助直观的图形界面,用户可导入base.wz浏览数据、搜索关键词、比较不同版本差异并导出文本或图片,既能帮助玩家理解游戏机制,也为MOD创作与数据分析提供实用支撑。
1. WzComparerR2 到底解决什么问题:从冒险岛 WZ 文件结构说起
冒险岛客户端目录下那一堆.wz文件,本质上是 Nexon 自研的二进制打包格式,把图片、音频、字符串、技能数据、地图信息全部塞进一个树形结构里。你直接拿十六进制编辑器打开,看到的是加密后的偏移表和压缩块,根本读不出东西。WzComparerR2 就是干这个的:它是一个开源的 WZ 文件读取与可视化工具,能把这棵资源树还原成可浏览的目录,导出 PNG、MP3、XML,甚至直接对比两个版本之间的差异。做冒险岛怀旧服搭建、单机版资源替换、职业代码提取、技能数据校对的人,基本绕不开它。我第一次接触是因为要查 079 版本某个职业的 attackType 字段,用别的工具翻半天翻不到,换 WzComparerR2 之后直接在属性面板里看到了原始值。这篇笔记就按「它怎么读 WZ → 怎么配环境跑起来 → 怎么导出和对比 → 踩过哪些坑」的顺序讲清楚,新手能照着复现,熟手能直接跳到参数和边界部分。
2. WZ 文件格式与 WzComparerR2 的读取链路
2.1 WZ 的树形结构与加密方式
WZ 文件不是简单的压缩包。它内部是一棵节点树,每个节点有 tag 标识类型:0x01是图片,0x02是音频,0x03是数字,0x04是字符串,0x05是浮点,0x08是子目录,0x09是属性列表。文件头之后是加密的偏移表,Nexon 用了一套基于版本号的异或密钥来混淆偏移量。不同版本的密钥不同,这就是为什么你用旧版工具打开新版 WZ 会报错或者读出乱码。
WzComparerR2 的处理链路分四步:先读文件头拿到版本号和加密偏移表,用内置的密钥表解密偏移;然后按偏移定位到根节点,递归解析每个节点的 tag 和长度;接着对压缩过的数据块做解压(通常是 zlib);最后把解析结果映射成内存中的树形对象,交给 UI 层渲染。整个过程是只读的,不会修改原文件,这点很重要——你不用担心操作失误把客户端搞坏。
提示:WZ 的密钥和版本号绑定,怀旧服常见的 079、092、113 版本密钥各不相同。WzComparerR2 内置了多个版本的密钥,但如果你拿到的是私服改过的 WZ,密钥可能被替换,这时候需要手动指定。
2.2 环境准备与最小可运行配置
WzComparerR2 是 C# 写的,基于 .NET Framework。你不需要装 Visual Studio 全家桶,但需要 .NET Framework 4.6 或以上运行时。如果你要自己编译源码,那就得装 Visual Studio 2019 或 2022,选「.NET 桌面开发」工作负载。
最小可运行配置清单:
| 组件 | 要求 | 说明 |
|---|---|---|
| 操作系统 | Windows 7 SP1 及以上 | 官方只在 Windows 上测试,Linux 需走 Wine |
| .NET Framework | 4.6+ | 运行时,不是 SDK |
| 内存 | 4GB 以上 | 大版本 WZ 解压后内存占用可能到 1.5GB |
| 磁盘 | 预留 2GB | 导出资源和缓存需要空间 |
| 源码编译(可选) | VS2019+ | 需要 .NET 桌面开发工作负载 |
如果你只是用,直接拿编译好的 release 包解压就能跑。如果要改代码或者加自定义导出逻辑,就 clone 源码用 VS 打开.sln编译。编译时注意把平台目标设成 x64,x86 在大 WZ 上会内存溢出。
2.3 第一次打开 WZ:从加载到浏览的完整步骤
打开 WzComparerR2 之后,界面分三栏:左边是 WZ 文件树,中间是节点属性,右边是预览区。加载一个 WZ 的流程如下:
第一步,菜单栏选File → Open,定位到客户端目录下的Data文件夹,选中Base.wz或Skill.wz。工具会自动识别版本号并解密偏移表。如果弹窗提示「Unknown version」,说明这个 WZ 的版本号不在内置表里,需要手动输入。
第二步,加载完成后左侧树会展开根节点。Base.wz下面通常是Character、Effect、Item等目录;Skill.wz下面是各职业的技能节点。点击任意节点,中间面板会显示它的属性列表,包括_inlink、_outlink、_canvas这些特殊字段。
第三步,右键节点可以导出。图片节点导出 PNG,音频节点导出 MP3,整个目录可以导出成 XML 或者 JSON。导出 XML 的时候注意编码选 UTF-8,否则中文技能名会乱码。
# 如果你用命令行批量导出(部分分支支持 CLI 模式) WzComparerR2.CLI.exe --input "C:\MapleStory\Data\Skill.wz" \ --output "C:\export\skill" \ --format xml \ --encoding utf-8 \ --recursive这段命令的意思是:读取Skill.wz,把整棵树递归导出成 XML 到C:\export\skill,编码用 UTF-8。--recursive不加的话只导出根节点一层。--format还可以换成png或mp3,但那样只对对应类型的节点生效。实际用的时候,我一般先导出 XML 做数据分析,需要图片素材再单独导出 PNG。
3. 用 WzComparerR2 做资源导出与版本对比
3.1 导出 PNG 与 XML 的参数怎么调
导出图片时,WzComparerR2 默认会把_canvas节点下的所有帧按顺序导出,命名规则是节点名_帧号.png。如果你只要第一帧,在导出对话框里把「导出所有帧」取消勾选。PNG 的背景透明度默认保留,但有些老版本 WZ 的图片没有 alpha 通道,导出后会带黑底,这时候需要在设置里勾选「移除黑色背景」。
导出 XML 的参数更关键。_inlink和_outlink是 WZ 的引用机制,一个节点可以引用另一个节点的数据。导出时如果勾选「解析链接」,工具会把引用展开成实际内容;不勾选的话,XML 里只保留链接路径。做版本对比时我建议勾选解析,否则你对比两个版本会发现链接路径没变但实际内容变了,容易漏掉改动。
# 用 Python 解析导出的 XML,提取技能冷却和伤害字段 import xml.etree.ElementTree as ET tree = ET.parse(r"C:\export\skill\Skill_000.xml") root = tree.getroot() for skill in root.iter("imgdir"): name = skill.get("name") if name and name.startswith("skill"): for child in skill: if child.tag == "int" and child.get("name") == "cooltime": print(f"{name} 冷却: {child.get('value')} 秒") if child.tag == "int" and child.get("name") == "damage": print(f"{name} 伤害: {child.get('value')}%")这段脚本遍历 XML 里所有imgdir节点,找到名字以skill开头的,然后读取cooltime和damage两个整型属性。child.get('value')拿的是属性值,child.get('name')拿的是属性名。实际用的时候你会发现有些技能伤害是数组而不是单值,那就得判断child.tag是不是vector或者canvas,再做额外处理。
3.2 两个版本 WZ 的差异对比怎么做
版本对比是 WzComparerR2 的强项。菜单栏Tools → Compare打开对比窗口,左边选旧版本 WZ,右边选新版本 WZ,点开始之后工具会逐节点比对,把新增、删除、修改的节点用不同颜色标出来。
对比结果分三类:Added是新增节点,Removed是删除节点,Modified是属性值有变化的节点。对于Modified,双击可以看具体哪个字段变了,旧值和新值并排显示。我做怀旧服更新的时候,习惯先把Skill.wz和Character.wz对比一遍,看看哪些职业技能被调整了,然后再决定要不要同步到服务端。
注意:对比大文件时内存占用会翻倍,因为两个 WZ 要同时加载。如果机器内存不够,可以先把 WZ 导出成 XML,再用文本对比工具(比如 Beyond Compare)比 XML,虽然精度差一点但省内存。
3.3 批量导出与自动化脚本
手动一个个导出太慢,WzComparerR2 支持脚本扩展。它在Scripts目录下放.csx脚本文件,用 C# 脚本引擎执行。你可以写一个脚本遍历整棵树,按条件导出指定类型的节点。
// 导出所有技能图标,按职业分类存放 foreach (var node in PluginContext.WzFileTree.Nodes) { if (node.Name == "Skill") { foreach (var jobNode in node.Nodes) { string jobName = jobNode.Name; string outputDir = @"C:\export\icons\" + jobName; System.IO.Directory.CreateDirectory(outputDir); foreach (var skillNode in jobNode.Nodes) { var icon = skillNode.FindNodeByPath("icon"); if (icon != null && icon.Value is WzCanvas) { string fileName = outputDir + "\\" + skillNode.Name + ".png"; ((WzCanvas)icon.Value).SaveAsPng(fileName); } } } } }这段脚本的逻辑是:从根节点找到Skill目录,遍历下面的职业节点,为每个职业建一个文件夹,再遍历职业技能节点,找到icon子节点,如果是画布类型就存成 PNG。PluginContext.WzFileTree是脚本引擎注入的当前 WZ 树对象,FindNodeByPath按路径查找子节点,SaveAsPng是画布对象的导出方法。跑之前记得把outputDir改成你自己的路径,否则会往 C 盘根目录写。
4. 避坑与排查:WzComparerR2 使用中的 5 个血泪教训
4.1 打开 WZ 报「Invalid version」或直接闪退
现象:双击 WZ 文件后工具无响应,或者弹窗提示版本号无效,然后进程消失。
原因:WZ 文件头的版本号被私服修改过,或者文件本身被加密了第二层。有些怀旧服为了防解包,会在标准 WZ 外面再套一层自定义加密。
解决:先用十六进制编辑器看文件头前 4 个字节,标准 WZ 是PKG1开头。如果不是,说明被二次加密了,需要先脱壳。如果是PKG1但版本号异常,在 WzComparerR2 的设置里手动指定版本号,或者用--force-version参数强制读取。
4.2 导出的 PNG 全是黑底或者透明通道丢失
现象:导出的图片背景是黑色,放到 PS 里看 alpha 通道是满的。
原因:老版本 WZ 的图片数据没有独立的 alpha 通道,透明信息藏在调色板里。WzComparerR2 默认按 ARGB 解析,遇到这种图就会把透明区域填成黑色。
解决:在导出设置里勾选「使用调色板透明度」或者「移除黑色背景」。如果已经导出了,可以用 ImageMagick 批量处理:magick mogrify -transparent black *.png,把黑色替换成透明。
4.3 对比功能卡死或者结果不完整
现象:点开始对比后进度条卡在某个百分比不动,或者对比完了发现漏了很多节点。
原因:对比时工具会递归加载两个 WZ 的所有节点,如果某个节点有循环引用(_inlink指向自身),递归就会死循环。另外,如果两个 WZ 的版本号不同,偏移表解密方式不一样,对比结果会错乱。
解决:对比前先确认两个 WZ 的版本号一致。如果有循环引用,在设置里把「最大递归深度」调到 10 以内。实在不行就导出 XML 用文本工具比,虽然麻烦但不会卡死。
4.4 中文技能名导出后变成乱码
现象:XML 里的中文全是中这种实体,或者直接显示问号。
原因:WZ 里的字符串是 UTF-16 编码,导出时如果选了 ANSI 或者 GBK 就会乱码。另外有些私服把字符串加密了,需要先解密。
解决:导出 XML 时编码选 UTF-8,并且勾选「保留 Unicode 实体」。如果还是乱码,说明字符串被加密了,需要在脚本里调用解密函数。常见做法是用 WzComparerR2 的WzString类先解密再导出。
4.5 内存溢出导致导出中断
现象:导出到一半工具报OutOfMemoryException,然后崩溃。
原因:WzComparerR2 默认把整个 WZ 加载到内存,大版本(比如 176 以上)的Skill.wz解压后可能超过 2GB,32 位进程直接爆掉。
解决:把编译目标改成 x64,或者用--low-memory模式分批加载。导出的时候不要一次性导出整棵树,按目录分批导出,导完一批清一次缓存。我一般会写个脚本,每导出一个职业就调用GC.Collect()强制回收。
5. 进阶:用脚本扩展 WzComparerR2 做自定义数据提取
5.1 脚本引擎的注入对象与常用 API
WzComparerR2 的脚本引擎基于 Roslyn,支持 C# 脚本语法。脚本运行时,引擎会注入几个关键对象:PluginContext提供当前 WZ 树和插件入口,WzFileTree是树形结构,WzNode是节点对象。每个WzNode有Name、Value、Nodes三个核心属性,Value可能是WzInt、WzString、WzCanvas等类型,用is判断后再强转。
常用 API 清单:
| API | 作用 | 返回类型 |
|---|---|---|
FindNodeByPath(string) | 按路径查找子节点 | WzNode |
GetValue<T>() | 获取节点值并转型 | T |
SaveAsPng(string) | 画布存为 PNG | void |
SaveAsMp3(string) | 音频存为 MP3 | void |
ToXml() | 节点转 XML 字符串 | string |
5.2 写一个提取全职业技能数据的脚本
下面这个脚本把Skill.wz里所有职业的主动技能提取出来,输出成 CSV,方便导入 Excel 做数值分析。
using System.IO; using System.Text; var sb = new StringBuilder(); sb.AppendLine("职业,技能ID,技能名,最大等级,冷却时间,伤害"); foreach (var jobNode in PluginContext.WzFileTree.FindNodeByPath("Skill").Nodes) { string jobName = jobNode.Name; foreach (var skillNode in jobNode.Nodes) { string skillId = skillNode.Name; string skillName = ""; int maxLevel = 0; int cooltime = 0; int damage = 0; var nameNode = skillNode.FindNodeByPath("name"); if (nameNode != null) skillName = nameNode.GetValue<string>(); var levelNode = skillNode.FindNodeByPath("maxLevel"); if (levelNode != null) maxLevel = levelNode.GetValue<int>(); var coolNode = skillNode.FindNodeByPath("cooltime"); if (coolNode != null) cooltime = coolNode.GetValue<int>(); var dmgNode = skillNode.FindNodeByPath("damage"); if (dmgNode != null) damage = dmgNode.GetValue<int>(); sb.AppendLine($"{jobName},{skillId},{skillName},{maxLevel},{cooltime},{damage}"); } } File.WriteAllText(@"C:\export\skill_data.csv", sb.ToString(), Encoding.UTF8);脚本逻辑很直白:遍历Skill下的每个职业节点,再遍历职业下的每个技能节点,依次读取name、maxLevel、cooltime、damage四个字段,拼成 CSV 行。GetValue<string>()和GetValue<int>()是泛型方法,会根据节点实际类型做转换。如果某个字段不存在,FindNodeByPath返回 null,脚本里做了判空处理,不会崩。跑完之后 CSV 用 Excel 打开,按职业筛选就能看到每个职业的技能数值分布。
5.3 验证脚本输出与常见调试手段
脚本跑完先别急着信结果。我一般会做三步验证:第一步,随机抽三个技能,手动在 WzComparerR2 界面里找到对应节点,核对cooltime和damage是否和 CSV 一致;第二步,统计 CSV 行数,和界面里技能节点总数对比,看有没有漏;第三步,检查有没有空值或者异常值,比如maxLevel是 0 或者damage是负数。
如果发现数据不对,调试手段有两个:一是在脚本里加Console.WriteLine打印中间变量,WzComparerR2 的脚本控制台会输出;二是把skillNode.ToXml()打到日志里,看原始 XML 结构长什么样。常见问题是字段名拼错,比如cooltime写成coolTime,WZ 里字段名是大小写敏感的。
提示:脚本执行时间可能比较长,大版本
Skill.wz遍历一遍要几十秒。建议先在小的 WZ 文件上测试脚本逻辑,确认没问题再跑全量。
5.4 一个我常用的习惯
每次改完脚本,我不会直接覆盖原来的导出目录,而是新建一个带时间戳的文件夹,比如export_20250115_1430。这样万一新脚本有 bug,旧数据还在,不用重新跑一遍。另外,CSV 导出后我会用git diff和上一版对比,看看哪些技能数值变了——这比在 WzComparerR2 界面里一个个点快得多。希望帮到你。
本文还有配套的精品资源,点击获取