Java版热血传奇2之资源文件与地图



Java版热血传奇2之资源文件与地图

我虽然是90后,但是也很喜欢热血传奇2(以下简称“传奇”)这款游戏。

进入程序员行业后自己也对传奇客户端实现有所研究,现在将我的一些研究结果展示出来,如果大家有兴趣的话不妨与我交流。

项目我托管到codeplex上了,使用GPLv2开源协议。大家可以checkout代码出来看。

我现在将地图加载出来了,算是达到了里程碑1吧。

如果要将传奇的地图和资源文件详细解析可能我得写上几万字,不过我现在越来越懒了,就只将读取wix、wil、map文件的方法和它们的解析贴出来吧。

准备工作:

热血传奇十周年客户端

JDK7

Eclipse

注意:

阅读此篇文章后您将不需要再到网络上搜索传奇资源文件和地图文件解析,因为我的随笔绝对是最全最完整最详细的!但这可能需要您花费一些耐心。

  第一部分——地图:

    第一节——描述:

Q: Tile是什么?

A: Tile在中文是“瓷砖”、“块”的意思,具体到传奇地图中就是48*32屏幕像素大小的矩形区域。单个传奇地图就是由多个Tile构成的。

Q: map格式文件究竟存放了哪些信息?

A: map格式文件保存了一个完成地图的所有信息,但是对于当前Tile的图片只是保存了一个索引而不是把图片色彩数据保存下来。

Q: map格式文件怎样读取?

A: 对于文件读取以及对应到Java语言中的数据类型和数据结构我们要从两方面考虑。

一是map的数据内容:

map文件分为两部分。一个文件头标识了当前地图的高度、宽度等重要信息;剩余部分则是多个Tile的详细信息

二是map格式文件是由Object-Pascal(以下简称Delphi)语言序列化而成的,我们首先需要了解从Delphi序列化的数据到Java反序列化需要进行的操作。

以上内容表明了地图的信息,热血传奇中地图由Tile构成,每个Tile对应48*32屏幕像素大小。

.map文件则保存了地图的宽度、高度以及每个Tile的详细信息。

    第二节——对应:

.map文件如果对应到编程语言中数据结构的话在Delphi中如下(文件头):

1 TMapHeader = packed record 
2  wWidth: Word; 
3  wHeight :Word; 
4  sTitle :String[16]; 
5  UpdateDate :TDateTime; 
6  Reserved :array[0..22] of Char;

(Tile,两种都可以):

 MapTile

每个.map文件如果在Delphi中就成了一个TMapHeader加wWidth*wHeight个MapTile。

(对于每个字段占用的字节数请查看下面Java代码中注释)

由于我们是使用Java语言描述热血传奇地图,所以我针对上述两个数据结构使用Java语言进行了描述:

 MapHeader

(Tile我使用了两种描述方式,后一种用于生产环境更加优秀):

 MapTile
 MapTileInfo


对于读取物理文件到产生对象,我使用一些工具方法,这里主要是高低位的问题,还有就是Delphi中字符串和时间日期到Java的不同处理。

    第三节——读取:

我对.map文件读取数据生成对象的过程如下(工具方法请查阅源码,就不在此贴出了):

复制代码
 1 /**
 2      * 通过字节数组反序列化地图文件头数据
 3      * 
 4      * @param bytes
 5      *     数据(文件中直接读取,未经过任何处理的字节数组)
 6      * @return
 7      *     地图文件头信息
 8      */
 9     public static MapHeader readMapHeader(byte[] bytes) {
10         MapHeader res = new MapHeader();
11         res.setWidth(Common.readShort(bytes, 0, true));
12         res.setHeight(Common.readShort(bytes, 2, true));
13         res.setTitle(readStaticSingleString(bytes, 4));
14         res.setUpdateDate(readDate(bytes, 21, true));
15         res.setReserved(readChars(bytes, 29, 23));
16         return res;
17     }
复制代码

 

复制代码
 1 /**
 2      * 通过字节数组反序列化地图逻辑坐标块儿数据
 3      * 
 4      * @param bytes
 5      *     数据(文件中直接读取,未经过任何处理的字节数组)
 6      * @return
 7      *     地图逻辑坐标块儿信息
 8      */
 9     public static MapTile readMapTile(byte[] bytes) {
10         MapTile res = new MapTile();
11         res.setBngImgIdx(Common.readShort(bytes, 0, true));
12         res.setMidImgIdx(Common.readShort(bytes, 2, true));
13         res.setObjImgIdx(Common.readShort(bytes, 4, true));
14         res.setDoorIdx(bytes[6]);
15         res.setDoorOffset(bytes[7]);
16         res.setAniFrame(bytes[8]);
17         res.setAniTick(bytes[9]);
18         res.setObjFileIdx(bytes[10]);
19         res.setLight(bytes[11]);
20         return res;
21     }
22     
23     /**
24      * 通过字节数组反序列化地图逻辑坐标块信息
25      * 
26      * @param bytes
27      *     数据(文件中直接读取,未经过任何处理的字节数组)
28      * @return
29      *     地图逻辑坐标块儿信息
30      */
31     public static MapTileInfo readMapTileInfo(byte[] bytes) {
32         MapTileInfo res = new MapTileInfo();
33         // 读取背景
34         short bng = Common.readShort(bytes, 0, true);
35         // 读取中间层
36         short mid = Common.readShort(bytes, 2, true);
37         // 读取对象层
38         short obj = Common.readShort(bytes, 4, true);
39         // 设置背景
40         if((bng & 0b0111_1111_1111_1111) > 0) {
41             res.setBngImgIdx((short) (bng & 0b0111_1111_1111_1111));
42             res.setHasBng(true);
43         }
44         // 设置中间层
45         if((mid & 0b0111_1111_1111_1111) > 0) {
46             res.setMidImgIdx((short) (mid & 0b0111_1111_1111_1111));
47             res.setHasMid(true);
48         }
49         // 设置对象层
50         if((obj & 0b0111_1111_1111_1111) > 0) {
51             res.setObjImgIdx((short) (obj & 0b0111_1111_1111_1111));
52             res.setHasObj(true);
53         }
54         // 设置是否可站立
55         res.setCanWalk(!Common.is1AtTopDigit(bng) && !Common.is1AtTopDigit(obj));
56         // 设置是否可飞行
57         res.setCanFly(!Common.is1AtTopDigit(obj));
58                 
59         res.setDoorOffset(bytes[7]);
60         if(Common.is1AtTopDigit(bytes[7])) res.setDoorOpen(true);
61         res.setAniFrame(bytes[8]);
62         if(Common.is1AtTopDigit(bytes[8])) {
63             res.setAniFrame((byte) (bytes[8] & 0x7F));
64             res.setHasAni(true);
65         }
66         res.setAniTick(bytes[9]);
67         res.setObjFileIdx(bytes[10]);
68         res.setLight(bytes[11]);
69         return res;
70     }
复制代码

  第二部分——资源文件:

    第一节——描述:

Q: wix和wil文件分别是什么?

A: wix全名为“Wemade Image Index”,顾名思义是图片库索引;wil全名为“Wemade Image Lib”,意为图片库。wix文件中存储了对应图片库的基本信息,包括图片数量、每个图片色彩数据起始位置;wil文件则存储了包括图片调色板、图片色彩数据在内的所有图片信息。

Q: wix和wil需要对应起来用吗?

A: 其实在生产环境中只需要使用wil就可以了,它包含了程序所需所有信息,网络上有人说必须使用wix来寻找每个图片色彩数据起始位置的说法是错误的,不过结合起来使用能最大限度避免错误,如果不服,请联系我!

Q: wix文件结构和wil文件结构?

A: wix文件由标题、图片数和图片色彩数据起始位置(对应wil中索引)的数组构成;wil文件由文件头和多个图片数据构成,文件头内容相对固定,每个图片色彩数据长度都不尽相同。

    第二节——对应:

wix文件:

Delphi语言描述(起始位置数组没有加上):

1 type 
2  TIndexHeader = record 
3  sTitle :String[40]; 
4  iIndexCount :Integer;

Java语言描述:

 Wix

wil文件:

Delphi语言描述(调色板未加上,调色板说起来比较麻烦):

1 type 
2  TLibHeader = record 
3  sTitle :String[40]; 
4  iImageCount :Integer; 
5  iColorCount :Integer; 
6  iPaletteSize :Integer;
复制代码
1 type 
2  TImageInfo = record 
3  siWidth :SmallInt; 
4  siHeight :SmallInt; 
5  siPx :SmallInt; 
6  siPy :SmallInt; 
7  Bits :PByte;
复制代码

Java语言描述:

 Wil
 LibHeader
 ImageInfo

    第三节——读取:

我们的目的在于使用wil中的图片,在上图其实可以看到我们只差一个每个图片色彩数据大小(??处),这个大小可以自己计算得到(涉及到Delphi位图处理,信息量较大,不在此赘述),但我们也可以从wix中拿到,这样比较方便,而且不会出错。

 readWil

    第四节——图片处理:

图片数据需要经过转换才能在界面上展示(针对8位的图片):

首先在读取LibHeader时就需要做透明色处理:

 readLibHeader

在显示时要记住图片颜色数据的存储是从右向左,从上往下的:

 readImage

 

说了这么久,我自己都糊涂了。大家如果不清楚请下载源码或基于Eclipse和JDK的项目进行查看。

 

编辑于2015-01-25 21:06:33

项目没有继续下去,不过我用lwjgl重写了部分,实现了地图加载和纹理读取。大家可以去新的项目地址checkout代码。