我虽然是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中如下(文件头):
TMapHeader = packed record
wWidth: Word;
wHeight :Word;
sTitle :String[];
UpdateDate :TDateTime;
Reserved :array[..] of Char;
(Tile,两种都可以):
type
TMapInfo = packed record
wBkImg :Word;
wMidImg :Word;
wFrImg :Word;
btDoorIndex :Byte;
btDoorOffset :Byte;
btAniFrame :Byte;
btAniTick :Byte;
btArea :Byte;
btLight :Byte; type
TMapInfo = packed record
wBigTileImg :Word;
wSmTileImg :Word;
wObjImg :Word;
btDoorIndex :Byte;
btDoorOffset :Byte;
btAniFrame :Byte;
btAniTick :Byte;
btObjFile :Byte;
btLight :Byte;
MapTile
每个.map文件如果在Delphi中就成了一个TMapHeader加wWidth*wHeight个MapTile。
(对于每个字段占用的字节数请查看下面Java代码中注释)
由于我们是使用Java语言描述热血传奇地图,所以我针对上述两个数据结构使用Java语言进行了描述:
package org.coderecord.jmir.entt.internal; import java.util.Date; /**
* 热血传奇2地图文件头
* <p>
* 针对*.map文件的数据结构使用Java语言描述
* <br>
* 地图文件头为52字节,在Pascal中定义为
* <br>
* TMapHeader = packed record
* <br>
*  wWidth: Word;
* <br>
*  wHeight :Word;
* <br>
*  sTitle :String[16];
* <br>
*  UpdateDate :TDateTime;
* <br>
*  Reserved :array[0..22] of Char;
* </p>
* <p>
* <b>wWidth</b> 表示地图宽度(占用两个字节,相当于Java语言short;一般不超过1000)
* <br>
* <b>wHeight</b> 表示地图高度(占用两个字节,相当于Java于洋short;一般不超过1000)
* <br>
* <b>sTitle</b> 标题,静态单字符串(占用17个字节,首字节为字符串已使用的长度即已存放的字符数,一般为“Legend of mir”)
* <br>
* <b>UpdateDate</b> 地图最后更新时间(占用8个字节,为TDateTime类型,可使用{@link org.coderecord.jmir.kits.Pascal#readDate(byte[], int, boolean) readDate} 转换为java.util.Date)
* <br>
* <b>Reserved</b> 保留字符,固定为23字节
* </p>
*
* @author ShawRyan
*
*/
public class MapHeader { /** 地图宽度(横向长度) */
private short width;
/** 地图高度(纵向长度) */
private short height;
/** 标题 */
private String title;
/** 更新日期 */
private Date updateDate;
/** 保留字符 */
private char[] reserved; /** 默认构造函数 */
public MapHeader() {}
/** 带全部参数的构造函数 */
public MapHeader(short width, short height, String title, Date updateDate, char[] reserved) {
this.width = width;
this.height = height;
this.title = title;
this.updateDate = updateDate;
this.reserved = reserved;
}
/** 使用已有对象构造实例 */
public MapHeader(MapHeader mapHeader) {
this.width = mapHeader.getWidth();
this.height = mapHeader.getHeight();
this.title = mapHeader.getTitle();
this.updateDate = mapHeader.getUpdateDate();
this.reserved = mapHeader.getReserved();
} /** 获取地图宽度(横向长度) */
public short getWidth() {
return width;
}
/** 设置地图宽度(横向长度) */
public void setWidth(short width) {
this.width = width;
}
/** 获取地图高度(纵向长度) */
public short getHeight() {
return height;
}
/** 设置地图高度(纵向长度) */
public void setHeight(short height) {
this.height = height;
}
/** 获取标题 */
public String getTitle() {
return title;
}
/** 设置标题 */
public void setTitle(String title) {
this.title = title;
}
/** 获取更新时间 */
public Date getUpdateDate() {
return updateDate;
}
/** 设置更新时间 */
public void setUpdateDate(Date updateDate) {
this.updateDate = updateDate;
}
/** 获取保留字符 */
public char[] getReserved() {
return reserved;
}
/** 设置保留字符 */
public void setReserved(char[] reserved) {
this.reserved = reserved;
}
}
MapHeader
(Tile我使用了两种描述方式,后一种用于生产环境更加优秀):
package org.coderecord.jmir.entt.internal; /**
* 热血传奇2地图“块”
* <br>
* 即 “<b>逻辑坐标</b>”点(人物/NPC等放置需要占用一个逻辑坐标点)
* <br>
* 需要注意的是逻辑坐标和屏幕坐标是不一样的,屏幕坐标一般为像素值,根据显示器分辨率设置而有所不同
* <br>
* 热血传奇2中一个逻辑坐标点(地图块)需要占用 48 * 32 屏幕坐标大小
* <br>
* 每个地图块为2层结构,包括‘地’和‘空’
* 例如树叶投影下的地图块就是2层,包括地表及物体(如有突起石头的地面或有水流的地面)和树叶
* <p>
* 在Pascal语言中使用以下数据结构对地图块进行描述和存储(两种)
* <br>
* type
* <br>
* TMapInfo = packed record
* <br>
*  wBkImg :Word;
* <br>
*  wMidImg :Word;
* <br>
*  wFrImg :Word;
* <br>
*  btDoorIndex :Byte;
* <br>
*  btDoorOffset :Byte;
* <br>
*  btAniFrame :Byte;
* <br>
*  btAniTick :Byte;
* <br>
*  btArea :Byte;
* <br>
*  btLight :Byte;
* </p>
* <p>
* type
* <br>
* TMapInfo = packed record
* <br>
*  wBigTileImg :Word;
* <br>
*  wSmTileImg :Word;
* <br>
*  wObjImg :Word;
* <br>
*  btDoorIndex :Byte;
* <br>
*  btDoorOffset :Byte;
* <br>
*  btAniFrame :Byte;
* <br>
*  btAniTick :Byte;
* <br>
*  btObjFile :Byte;
* <br>
*  btLight :Byte;
* </p>
* <p>
* <b>wBkImg</b>或<b>wBigTileImg</b> 表示地图地表图片,如果最高位为1则表示不能通过(或站立),如河水型地表等。在判断是否可以飞过(从空中通过)时则不需要考虑
* <br>
* <b>wMidImg</b>或<b>wSmTileImg</b> 表示地图可视物体图片(有时被称为可视数据/中间层/小地图块/地图补充背景等等),如果wBkImg(或wBigTileImg)没有铺满则使用此地图块进行铺垫。最高位不作为判断依据,不过图片索引一般小于0x8000,即最高位一般为0。例如在某地图中第一个地图块的wBkImg(或wBigTileImg)大小为96 * 64,则代表该地图左上角4个块儿的地表都不为空,此时紧邻的三个地图块都可以不用设置wBkImg(或wBigTileImg)和wMidImg(或wSmTileImg);如果某个地图块的没有被其他块儿的wBkImg(或wBigTileImg)铺满,自己也没有wBkImg(或wBigTileImg),那么它就需要一个wMidImg(或wSmTileImg)进行铺垫。值得一提的是并不一定在有了wMidImg(或wBigTileImg)后就不需要绘制此层图片了
* <br>
* <b>wFrImg</b>或<b>wObjImg</b> 表示表层图片(对象),即空中遮挡物,如植物或建筑物,如果最高位为1则表示不能通过(或站立)。在判断是否可飞过(从空中通过)时需要作为唯一条件判断,在判断是否可以徒步通过或站立时需要联合wBkImg进行判断
* <br>
* <b>总的来说,地图一般为两层(只是针对上面的三个属性,下方的也属于地图部分,不过先不纳入考虑),包括背景层与对象层,背景层为wBkImg(或wBigTileImg)和wMidImg(或wSmTileImg)的集合,一般来说wBkImg就能搞定,也有时候需要两者都有;Spirit(人物/怪物/NPC/掉落物品等)在两层中间;索引从1开始,所以在从资源中真正取图片时应该减1(适用于所有资源索引);索引一般最高位为0,为1一般表示特殊情况(在Java语言中可以理解为大于0,因为首位为1表示负数)</b>
* <br>
* <b>btDoorIndex</b> 门索引,最高位为1表示有门,为0表示没有门。
* <br>
* <b>btDoorOffset</b> 门偏移,最高位为1表示门打开了,为0表示门为关闭状态
* <br>
* <b>btAniFrame</b> 帧数,指示当前地图块动态内容由多少张静态图片轮询播放,需要和btAniTick一起起作用;如果最高位为1(即值大于0x80,或者在Java中为小于0的数值)则表示有动态内容
* <br>
* <b>btAniTick</b> 跳帧数,指示当前地图块动态内容应该每隔多少帧变换当前显示的静态图片,需要和btAniFrame一起作用
* <br>
* <b>btAniFrame和btAniTick作用时表达式如下index = (gAniCount % (btAniFrame * (1 + btAniTick))) / (1 + btAniTick)
* <br>
*  其中gAniCount是当前画面帧是第几帧,它会在每次绘制游戏界面时累加,它可以有最大值,超过可以置0;index是相对当前objImgIdx的偏移量,比如当前对象层图片索引为1,而AniFrame为10,则表示从1到11这10副图片应该作为一动态内容播放(有待考证)
* </b>
* <br>
* <b>btArea</b>或<b>btObjFile</b> 表示当前wFrImg(或wObjImg)和动态内容构成图片来自哪个Object资源文件,具体为Object{btArea}.wil中,如果btArea为0则是Objects.wil
* <br>
* <b>btLight</b> 亮度,一般为0/1/4
* </p>
* @author ShawRyan
*
*/
public class MapTile { /** 背景图索引 */
private short bngImgIdx;
/** 补充背景图索引 */
private short midImgIdx;
/** 对象图索引 */
private short objImgIdx;
/** 门索引 */
private byte doorIdx;
/** 门偏移 */
private byte doorOffset;
/** 动画帧数 */
private byte aniFrame;
/** 动画跳帧数 */
private byte aniTick;
/** 资源文件索引 */
private byte objFileIdx;
/** 亮度 */
private byte light; /** 默认构造函数 */
public MapTile() { }
/** 使用已有对象构造实例 */
public MapTile(MapTile mapTile) {
this.bngImgIdx = mapTile.bngImgIdx;
this.midImgIdx = mapTile.midImgIdx;
this.objImgIdx = mapTile.objImgIdx;
this.doorIdx = mapTile.doorIdx;
this.doorOffset = mapTile.doorOffset;
this.aniFrame = mapTile.aniFrame;
this.aniTick = mapTile.aniTick;
this.objFileIdx = mapTile.objFileIdx;
this.light = mapTile.light;
}
/** 带全部参数的构造函数 */
public MapTile(short bngImgIdx, short midImgIdx, short objImgIdx, byte doorIdx, byte doorOffset, byte aniFrame, byte aniTick, byte objFileIdx, byte light) {
this.bngImgIdx = bngImgIdx;
this.midImgIdx = midImgIdx;
this.objImgIdx = objImgIdx;
this.doorIdx = doorIdx;
this.doorOffset = doorOffset;
this.aniFrame = aniFrame;
this.aniTick = aniTick;
this.objFileIdx = objFileIdx;
this.light = light;
} /** 获取背景图索引 */
public short getBngImgIdx() {
return bngImgIdx;
}
/** 设置背景图索引 */
public void setBngImgIdx(short bngImgIdx) {
this.bngImgIdx = bngImgIdx;
}
/** 获取补充图索引 */
public short getMidImgIdx() {
return midImgIdx;
}
/** 设置补充图索引 */
public void setMidImgIdx(short midImgIdx) {
this.midImgIdx = midImgIdx;
}
/** 获取对象图索引 */
public short getObjImgIdx() {
return objImgIdx;
}
/** 设置对象图索引 */
public void setObjImgIdx(short objImgIdx) {
this.objImgIdx = objImgIdx;
}
/** 获取门索引 */
public byte getDoorIdx() {
return doorIdx;
}
/** 设置门索引 */
public void setDoorIdx(byte doorIdx) {
this.doorIdx = doorIdx;
}
/** 获取门偏移 */
public byte getDoorOffset() {
return doorOffset;
}
/** 设置门偏移 */
public void setDoorOffset(byte doorOffset) {
this.doorOffset = doorOffset;
}
/** 获取动画帧数 */
public byte getAniFrame() {
return aniFrame;
}
/** 设置动画帧数 */
public void setAniFrame(byte aniFrame) {
this.aniFrame = aniFrame;
}
/** 获取动画跳帧数 */
public byte getAniTick() {
return aniTick;
}
/** 设置动画跳帧数 */
public void setAniTick(byte aniTick) {
this.aniTick = aniTick;
}
/** 获取资源文件索引 */
public byte getObjFileIdx() {
return objFileIdx;
}
/** 设置资源文件索引 */
public void setObjFileIdx(byte objFileIdx) {
this.objFileIdx = objFileIdx;
}
/** 获取亮度 */
public byte getLight() {
return light;
}
/** 设置亮度 */
public void setLight(byte light) {
this.light = light;
}
}
MapTile
package org.coderecord.jmir.entt.internal; import org.coderecord.jmir.scn.DrawSupport; /**
* MapTile方便程序逻辑的另类解读方式
*
* @author ShawRyan
*
*/
public class MapTileInfo {
/** 背景图索引 */
private short bngImgIdx;
/** 是否有背景图(在热血传奇2地图中,背景图大小为4个地图块,具体到绘制地图时则表现在只有横纵坐标都为双数时才绘制),见{@link DrawSupport#drawMap(int, int, org.coderecord.jmir.entt.Map, int, int) drawMap} */
private boolean hasBng;
/** 是否可行走(站立) */
private boolean canWalk;
/** 补充背景图索引 */
private short midImgIdx;
/** 是否有补充图 */
private boolean hasMid;
/** 对象图索引 */
private short objImgIdx;
/** 是否有对象图 */
private boolean hasObj;
/** 是否可以飞越 */
private boolean canFly;
/** 门索引 */
private byte doorIdx;
/** 是否有门 */
private boolean hasDoor;
/** 门偏移 */
private byte doorOffset;
/** 门是否开启 */
private boolean doorOpen;
/** 动画帧数 */
private byte aniFrame;
/** 是否有动画 */
private boolean hasAni;
/** 动画跳帧数 */
private byte aniTick;
/** 资源文件索引 */
private byte objFileIdx;
/** 亮度 */
private byte light; /** 无参构造函数 */
public MapTileInfo() { }
/** 带全部参数的构造函数 */
public MapTileInfo(short bngImgIdx, boolean hasBng, boolean canWalk, short midImgIdx, boolean hasMid, short objImgIdx, boolean hasObj, boolean canFly, byte doorIdx, boolean hasDoor, byte doorOffset, boolean doorOpen, byte aniFrame, boolean hasAni, byte aniTick, byte objFileIdx, byte light) {
this.bngImgIdx = bngImgIdx;
this.hasBng = hasBng;
this.canWalk = canWalk;
this.midImgIdx = midImgIdx;
this.hasMid = hasMid;
this.objImgIdx = objImgIdx;
this.hasObj = hasObj;
this.canFly = canFly;
this.doorIdx = doorIdx;
this.hasDoor = hasDoor;
this.doorOffset = doorOffset;
this.doorOpen = doorOpen;
this.aniFrame = aniFrame;
this.hasAni = hasAni;
this.aniTick = aniTick;
this.objFileIdx = objFileIdx;
this.light = light;
}
/** 基于已有实体构造对象 */
public MapTileInfo(MapTileInfo mapTileInfo) {
this.bngImgIdx = mapTileInfo.bngImgIdx;
this.hasBng = mapTileInfo.hasBng;
this.canWalk = mapTileInfo.canWalk;
this.midImgIdx = mapTileInfo.midImgIdx;
this.hasMid = mapTileInfo.hasMid;
this.objImgIdx = mapTileInfo.objImgIdx;
this.hasObj = mapTileInfo.hasObj;
this.canFly = mapTileInfo.canFly;
this.doorIdx = mapTileInfo.doorIdx;
this.hasDoor = mapTileInfo.hasDoor;
this.doorOffset = mapTileInfo.doorOffset;
this.doorOpen = mapTileInfo.doorOpen;
this.aniFrame = mapTileInfo.aniFrame;
this.hasAni = mapTileInfo.hasAni;
this.aniTick = mapTileInfo.aniTick;
this.objFileIdx = mapTileInfo.objFileIdx;
this.light = mapTileInfo.light;
} /** 获取背景图索引 */
public short getBngImgIdx() {
return bngImgIdx;
}
/** 设置背景图索引 */
public void setBngImgIdx(short bngImgIdx) {
this.bngImgIdx = bngImgIdx;
}
/** 获取该地图块是否有背景图 */
public boolean isHasBng() {
return hasBng;
}
/** 设置该地图块是否有背景图 */
public void setHasBng(boolean hasBng) {
this.hasBng = hasBng;
}
/** 获取该地图块是否可以站立或走过 */
public boolean isCanWalk() {
return canWalk;
}
/** 设置该地图块是否可以站立或走过 */
public void setCanWalk(boolean canWalk) {
this.canWalk = canWalk;
}
/** 获取补充图索引 */
public short getMidImgIdx() {
return midImgIdx;
}
/** 设置补充图索引 */
public void setMidImgIdx(short midImgIdx) {
this.midImgIdx = midImgIdx;
}
/** 获取该地图块是否有补充图 */
public boolean isHasMid() {
return hasMid;
}
/** 设置该地图块是否有补充图 */
public void setHasMid(boolean hasMid) {
this.hasMid = hasMid;
}
/** 获取对象图索引 */
public short getObjImgIdx() {
return objImgIdx;
}
/** 设置对象图索引 */
public void setObjImgIdx(short objImgIdx) {
this.objImgIdx = objImgIdx;
}
/** 获取该地图块是否有对象图 */
public boolean isHasObj() {
return hasObj;
}
/** 设置该地图块是否有对象图 */
public void setHasObj(boolean hasObj) {
this.hasObj = hasObj;
}
/** 获取该地图块是否可以飞越 */
public boolean isCanFly() {
return canFly;
}
/** 设置该地图块是否可以飞越 */
public void setCanFly(boolean canFly) {
this.canFly = canFly;
}
/** 获取门索引 */
public byte getDoorIdx() {
return doorIdx;
}
/** 设置门索引 */
public void setDoorIdx(byte doorIdx) {
this.doorIdx = doorIdx;
}
/** 获取该地图块是否有门 */
public boolean isHasDoor() {
return hasDoor;
}
/** 设置该地图块是否有门 */
public void setHasDoor(boolean hasDoor) {
this.hasDoor = hasDoor;
}
/** 获取门偏移 */
public byte getDoorOffset() {
return doorOffset;
}
/** 设置门偏移 */
public void setDoorOffset(byte doorOffset) {
this.doorOffset = doorOffset;
}
/** 获取该地图块门是否打开 */
public boolean isDoorOpen() {
return doorOpen;
}
/** 设置该地图块门是否打开 */
public void setDoorOpen(boolean doorOpen) {
this.doorOpen = doorOpen;
}
/** 获取动画帧数 */
public byte getAniFrame() {
return aniFrame;
}
/** 设置动画帧数 */
public void setAniFrame(byte aniFrame) {
this.aniFrame = aniFrame;
}
/** 获取该地图块是否有动画 */
public boolean isHasAni() {
return hasAni;
}
/** 设置该地图块是否有动画 */
public void setHasAni(boolean hasAni) {
this.hasAni = hasAni;
}
/** 获取动画跳帧数 */
public byte getAniTick() {
return aniTick;
}
/** 设置动画跳帧数 */
public void setAniTick(byte aniTick) {
this.aniTick = aniTick;
}
/** 获取资源文件索引 */
public byte getObjFileIdx() {
return objFileIdx;
}
/** 设置资源文件索引 */
public void setObjFileIdx(byte objFileIdx) {
this.objFileIdx = objFileIdx;
}
/** 获取亮度 */
public byte getLight() {
return light;
}
/** 设置亮度 */
public void setLight(byte light) {
this.light = light;
}
}
MapTileInfo
对于读取物理文件到产生对象,我使用一些工具方法,这里主要是高低位的问题,还有就是Delphi中字符串和时间日期到Java的不同处理。
第三节——读取:
我对.map文件读取数据生成对象的过程如下(工具方法请查阅源码,就不在此贴出了):
/**
* 通过字节数组反序列化地图文件头数据
*
* @param bytes
* 数据(文件中直接读取,未经过任何处理的字节数组)
* @return
* 地图文件头信息
*/
public static MapHeader readMapHeader(byte[] bytes) {
MapHeader res = new MapHeader();
res.setWidth(Common.readShort(bytes, 0, true));
res.setHeight(Common.readShort(bytes, 2, true));
res.setTitle(readStaticSingleString(bytes, 4));
res.setUpdateDate(readDate(bytes, 21, true));
res.setReserved(readChars(bytes, 29, 23));
return res;
}
/**
* 通过字节数组反序列化地图逻辑坐标块儿数据
*
* @param bytes
* 数据(文件中直接读取,未经过任何处理的字节数组)
* @return
* 地图逻辑坐标块儿信息
*/
public static MapTile readMapTile(byte[] bytes) {
MapTile res = new MapTile();
res.setBngImgIdx(Common.readShort(bytes, 0, true));
res.setMidImgIdx(Common.readShort(bytes, 2, true));
res.setObjImgIdx(Common.readShort(bytes, 4, true));
res.setDoorIdx(bytes[6]);
res.setDoorOffset(bytes[7]);
res.setAniFrame(bytes[8]);
res.setAniTick(bytes[9]);
res.setObjFileIdx(bytes[10]);
res.setLight(bytes[11]);
return res;
} /**
* 通过字节数组反序列化地图逻辑坐标块信息
*
* @param bytes
* 数据(文件中直接读取,未经过任何处理的字节数组)
* @return
* 地图逻辑坐标块儿信息
*/
public static MapTileInfo readMapTileInfo(byte[] bytes) {
MapTileInfo res = new MapTileInfo();
// 读取背景
short bng = Common.readShort(bytes, 0, true);
// 读取中间层
short mid = Common.readShort(bytes, 2, true);
// 读取对象层
short obj = Common.readShort(bytes, 4, true);
// 设置背景
if((bng & 0b0111_1111_1111_1111) > 0) {
res.setBngImgIdx((short) (bng & 0b0111_1111_1111_1111));
res.setHasBng(true);
}
// 设置中间层
if((mid & 0b0111_1111_1111_1111) > 0) {
res.setMidImgIdx((short) (mid & 0b0111_1111_1111_1111));
res.setHasMid(true);
}
// 设置对象层
if((obj & 0b0111_1111_1111_1111) > 0) {
res.setObjImgIdx((short) (obj & 0b0111_1111_1111_1111));
res.setHasObj(true);
}
// 设置是否可站立
res.setCanWalk(!Common.is1AtTopDigit(bng) && !Common.is1AtTopDigit(obj));
// 设置是否可飞行
res.setCanFly(!Common.is1AtTopDigit(obj)); res.setDoorOffset(bytes[7]);
if(Common.is1AtTopDigit(bytes[7])) res.setDoorOpen(true);
res.setAniFrame(bytes[8]);
if(Common.is1AtTopDigit(bytes[8])) {
res.setAniFrame((byte) (bytes[8] & 0x7F));
res.setHasAni(true);
}
res.setAniTick(bytes[9]);
res.setObjFileIdx(bytes[10]);
res.setLight(bytes[11]);
return res;
}
第二部分——资源文件:
第一节——描述:
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语言描述(起始位置数组没有加上):
type
TIndexHeader = record
sTitle :String[];
iIndexCount :Integer;
Java语言描述:
package org.coderecord.jmir.entt; /**
* WIX即“WEMADE IMAGE INDEX”
* <br>
* 意味图片索引
* <br>
* 在热血传奇2中,图片资源存储方式一般为一个wix文件和一个wil文件形成一组,负责保存一个内容或功能的图片资源
* <br>
* wix文件为索引文件,通过它从wil文件中解析出图片数据
* <br>
* 对于wix文件(头)可使用如下数据结构进行描述
* <br>
* type
* <br>
* TIndexHeader = record
* <br>
*  sTitle :String[40];
* <br>
*  iIndexCount :Integer;
* <br>
* <b>sTitle一般为“INDX v1.0-WEMADE Entertainment inc.”,表示标题;iIndexCount表示对应wil(lib库文件)中存储的图片数量;其实重点是此文件中前44个字节用处不大,从45字节开始才是我们关心的。</b>
* <p>
* <b>注意:</b>为什么sTitle是String[40]而非String[43],如果是后者,则sTitle会占用44字节,刚好是我们所说的前44字节?其实Pascal中对record的packed修饰符有特殊处理,<b>不带</b>packed表示“对齐”(对齐意味着能被4整除),现在sTitle为String[40],加上其头部修饰的一个字节占共用41字节,并不能被4整除,所以编译器会在后面加上3个字节来<b>“对齐”</b>,所以在这里sTitle一共占用44字节
* </p>
* 从第49字节开始则是iIndexCount个Integer类型数据,标识在对应wil文件中各个图片数据对应位置(array of Integer),这里的位置索引从0开始,指示在文件二进制数据的索引
*
* @author ShawRyan
*
*/
public class WIX {
/** 标题 */
private String title;
/** 资源数量 */
private int indexCount;
/** 资源数据起始位置 */
private int[] indexArray; /** 默认构造函数 */
public WIX() {}
/** 基于已有对象构造实例 */
public WIX(WIX wix) {
this.title = wix.title;
this.indexCount = wix.indexCount;
this.indexArray = wix.indexArray;
}
/** 使用全部参数构造实例 */
public WIX(String title, int indexCount, int[] indexArray) {
this.title = title;
this.indexCount = indexCount;
this.indexArray = indexArray;
} /** 获取标题 */
public String getTitle() {
return title;
}
/** 设置标题 */
public void setTitle(String title) {
this.title = title;
}
/** 获取资源数量 */
public int getIndexCount() {
return indexCount;
}
/** 设置资源数量 */
public void setIndexCount(int indexCount) {
this.indexCount = indexCount;
}
/** 获取资源索引 */
public int[] getIndexArray() {
return indexArray;
}
/** 设置资源索引 */
public void setIndexArray(int[] indexArray) {
this.indexArray = indexArray;
}
}
Wix
wil文件:
Delphi语言描述(调色板未加上,调色板说起来比较麻烦):
type
TLibHeader = record
sTitle :String[];
iImageCount :Integer;
iColorCount :Integer;
iPaletteSize :Integer;
type
TImageInfo = record
siWidth :SmallInt;
siHeight :SmallInt;
siPx :SmallInt;
siPy :SmallInt;
Bits :PByte;
Java语言描述:
package org.coderecord.jmir.entt; import org.coderecord.jmir.entt.internal.ImageInfo;
import org.coderecord.jmir.entt.internal.LibHeader; /**
* WIL即“WEMADE IMAGE LIBRARY”
* <br>
* 意为图片库
* <br>
* 存放多个图片数据(包括像素点色彩)
* <br>
* WIL文件由头部和多个图片信息组成
* <br>
* 头部可以使用如下类型进行描述
* <br>
* type
* <br>
* TLibHeader = record
* <br>
*  sTitle :String[40];
* <br>
*  iImageCount :Integer;
* <br>
*  iColorCount :Integer;
* <br>
*  iPaletteSize :Integer;
* <br>
* <b>头部共占用56字节,sTitle占用44字节,原因参见{@link WIX}。其中sTitle为标题,一般为“ILIB v1.0-WEMADE Entertainment inc.”;iImageCount为图片数量;iColorCount表示色彩位深度,如256表示8bit位图,65536表示16bit位图(2的幂数);iPaletteSize表示调色板字节数</b>
* <br>
* 头部后则是调色板(调色板请参照{@link org.coderecord.jmir.entt.internal.LibHeader#pallette LibHeader})和多个图片数据,包括图片宽高/坐标和像素色彩,可以使用下面的结构进行描述(不包括调色板,调色板其实也可放入header中,类型为 array of Byte)
* <br>
* type
* <br>
* TImageInfo = record
* <br>
*  siWidth :SmallInt;
* <br>
*  siHeight :SmallInt;
* <br>
*  siPx :SmallInt;
* <br>
*  siPy :SmallInt;
* <br>
*  Bits :PByte;
* <br>
* <b>siWidth</b> 图片宽度
* <br>
* <b>siHeight</b> 图片高度
* <br>
* <b>siPx</b> 图片横向偏移量
* <br>
* <b>siPy</b> 图片纵向偏移量
* <br>
* <b>Bits</b> 图片像素色彩值数组
*
* @author ShawRyan
*
*/
public class WIL { /** 文件头 */
private LibHeader header;
/** 图片数组 */
private ImageInfo[] images; /** 无参构造函数 */
public WIL() {}
/** 基于已有实例构造对象 */
public WIL(WIL wil) {
this.header = wil.header;
this.images = wil.images;
}
/** 全参构造函数 */
public WIL(LibHeader header, ImageInfo[] images) {
this.header = header;
this.images = images;
} /** 获取头部 */
public LibHeader getHeader() {
return header;
}
/** 设置头部 */
public void setHeader(LibHeader header) {
this.header = header;
}
/** 获取图片 */
public ImageInfo[] getImages() {
return images;
}
/** 设置图片 */
public void setImages(ImageInfo[] images) {
this.images = images;
}
}
Wil
package org.coderecord.jmir.entt.internal; /** WIL文件头 */
public class LibHeader {
/** 长度(字节数),不包括调色板大小 */
public static final int BIT_LENGTH_WITHOUT_PATTELE = 56; /** 标题 */
private String title;
/** 图片数量 */
private int imageCount;
/** 色深度 */
private int colorCount;
/** 调色板字节数 */
private int paletteSize;
/**
* 调色板
* <br>
* 调色板使用是一组字节数组即二维字节数组,第二维总为4字节,依次存储蓝、绿、红、Alpha色彩值
* <br>
* <br>
* 对于任意色彩而言,都应该使用24位来存储,比如FF0000表示红色
* <br>
* 24位色彩值也被称为真彩
* <br>
* Windows 中位图有两种格式:
* <br>
*  设备相关位图 Device Depend Bitmap DDB
* <br>
*  设备无关位图 Device Independ Bitmap DIB
* <br>
* 热血传奇使用DIB对图片进行存储
* <br>
* 色彩深度有时少于24位,有时是因为精度要求并不会那么高,例如使用5或6个字节存储的R/G/B值就已经够用,此时可使用16位颜色值存储一个像素点的颜色
* <br>
*  有时甚至一幅图的色彩不超过256种,此时就可以将这256中颜色提取出来作为一个调色板,然后像素点色彩值则存储色彩值在这个调色板中的索引,这就是8位颜色值
* <br>
*  对于单色或16色(即使用1bit或4bit存储色彩值的情况不与考虑)
*/
private int[] palette; /** 无参构造函数 */
public LibHeader() {}
/** 基于已有对象构造实例 */
public LibHeader(LibHeader header) {
this.title = header.title;
this.imageCount = header.imageCount;
this.colorCount = header.colorCount;
this.paletteSize = header.paletteSize;
this.palette = header.palette;
}
/** 带全部参数的构造函数 */
public LibHeader(String title, int imageCount, int colorCount, int paletteSize, int[] pallette) {
this.title = title;
this.imageCount = imageCount;
this.colorCount = colorCount;
this.paletteSize = paletteSize;
this.palette = pallette;
} /** 获取标题 */
public String getTitle() {
return title;
}
/** 设置标题 */
public void setTitle(String title) {
this.title = title;
}
/** 获取图片数量 */
public int getImageCount() {
return imageCount;
}
/** 设置图片数量 */
public void setImageCount(int imageCount) {
this.imageCount = imageCount;
}
/** 获取色深 */
public int getColorCount() {
return colorCount;
}
/** 设置色深 */
public void setColorCount(int colorCount) {
this.colorCount = colorCount;
}
/** 获取调色板大小 */
public int getPaletteSize() {
return paletteSize;
}
/** 设置调色板大小 */
public void setPaletteSize(int paletteSize) {
this.paletteSize = paletteSize;
}
/** 获取调色板 */
public int[] getPalette() {
return palette;
}
/** 设置调色板 */
public void setPalette(int[] pallette) {
this.palette = pallette;
}
}
LibHeader
package org.coderecord.jmir.entt.internal; /** 图片信息 */
public class ImageInfo {
/** 图片宽度 */
private short width;
/** 图片高度 */
private short height;
/** 图片横向偏移量 */
private short offsetX;
/** 图片纵向偏移量 */
private short offsetY;
/**
* 图片数据
* <br>
* 逐列存储像素色彩值,对于4 * 4的图片而言,其色彩数据如下(以字节为单位)
* <br>
* <b>注意:</b>如果图片宽度字节数不是4的倍数会有填充字节数,如果是读取真正的图片数据可以不用考虑,但如果需要一次性读取多张图片则需要跳过填充的字节,参见{@link org.coderecord.jmir.kits.Pascal#fillPByteLineWidth(int, int) fillPByteLineWidth}
* <br>
* 8位
* <br>
* 12 8 4 0 
* <br>
* 13 9 5 1 
* <br>
* 14 10 6 2 
* <br>
* 15 11 7 3 
* <br>
* 16位
* <br>
* 24&25 16&17 8&9 0&1 
* <br>
* 26&27 18&19 10&11 2&3 
* <br>
* 28&29 20&21 12&13 4&5 
* <br>
* 30&31 22&23 14&15 6&7 
*/
private byte[] pixels; /** 无参构造函数 */
public ImageInfo() {}
/** 基于已有对象构造实例 */
public ImageInfo(ImageInfo imageInfo) {
this.height = imageInfo.height;
this.offsetX = imageInfo.offsetX;
this.offsetY = imageInfo.offsetY;
this.pixels = imageInfo.pixels;
this.width = imageInfo.width;
}
/** 带全部参数的构造函数 */
public ImageInfo(short width, short height, short offsetX, short offsetY, byte[] pixels) {
this.width = width;
this.height = height;
this.offsetX = offsetX;
this.offsetY = offsetY;
this.pixels = pixels;
} /** 获取图片宽度 */
public short getWidth() {
return width;
}
/** 设置图片高度 */
public void setWidth(short width) {
this.width = width;
}
/** 获取图片高度 */
public short getHeight() {
return height;
}
/** 设置图片高度 */
public void setHeight(short height) {
this.height = height;
}
/** 获取图片横线偏移量 */
public short getOffsetX() {
return offsetX;
}
/** 设置图片横向偏移量 */
public void setOffsetX(short offsetX) {
this.offsetX = offsetX;
}
/** 获取图片纵向偏移量 */
public short getOffsetY() {
return offsetY;
}
/** 设置图片纵向偏移量 */
public void setOffsetY(short offsetY) {
this.offsetY = offsetY;
}
/** 获取图片二进制数据 */
public byte[] getPixels() {
return pixels;
}
/** 设置图片二进制数据 */
public void setPixels(byte[] pixels) {
this.pixels = pixels;
}
}
ImageInfo
第三节——读取:
我们的目的在于使用wil中的图片,在上图其实可以看到我们只差一个每个图片色彩数据大小(??处),这个大小可以自己计算得到(涉及到Delphi位图处理,信息量较大,不在此赘述),但我们也可以从wix中拿到,这样比较方便,而且不会出错。
/**
* 根据库文件路径和索引文件路径读取图片库
*
* @param wilPath
* 图片库文件路径
* @param wixPath
* 图片索引文件路径
* @return
* 得到的图片库文件路径
*/
public static WIL readWILFromFile(String wilPath, String wixPath) {
WIX wix = new WIX();
try(FileInputStream fis = new FileInputStream(wixPath)) {
wix.setTitle(Pascal.readStaticSingleString(fis, 0, 44));
wix.setIndexCount(Common.readInt(fis, 0, true));
int[] imageIndexs = new int[wix.getIndexCount()];
for(int i = 0; i < imageIndexs.length; ++i)
imageIndexs[i] = Common.readInt(fis, 0, true);
wix.setIndexArray(imageIndexs);
} catch (FileNotFoundException e) {
throw new RuntimeException(e);
} catch (IOException e) {
throw new RuntimeException(e);
}
WIL res = new WIL();
try(FileInputStream fis = new FileInputStream(wilPath)) {
res.setHeader(Pascal.readLibHeader(fis));
} catch (FileNotFoundException e) {
throw new RuntimeException(e);
} catch (IOException e) {
throw new RuntimeException(e);
}
try(RandomAccessFile raf = new RandomAccessFile(wilPath, "r")){
ImageInfo[] images = new ImageInfo[res.getHeader().getImageCount()];
for(int i = 0; i < images.length; ++i)
images[i] = Pascal.readImageInfo(raf, wix.getIndexArray()[i], Pascal.colorCountToBitCount(res.getHeader().getColorCount()));
res.setImages(images);
} catch (FileNotFoundException e) {
throw new RuntimeException(e);
} catch (IOException e) {
throw new RuntimeException(e);
}
return res;
}
readWil
第四节——图片处理:
图片数据需要经过转换才能在界面上展示(针对8位的图片):
首先在读取LibHeader时就需要做透明色处理:
/**
* 从流中读取图片库文件头
*
* @param is
* 数据流
* @return
* 文件头
* @throws IOException
* 可能的流异常
*/
public static LibHeader readLibHeader(InputStream is) throws IOException {
LibHeader res = new LibHeader();
res.setTitle(Pascal.readStaticSingleString(is, 0, 44));
res.setImageCount(Common.readInt(is, 0, true));
res.setColorCount(Common.readInt(is, 0, true));
res.setPaletteSize(Common.readInt(is, 0, true));
int[] palette = new int[res.getPaletteSize() / 4];
for(int i = 0; i < palette.length; ++i) {
byte[] byteArgb = new byte[4];
is.read(byteArgb);
// 最重要的一步,重设Alpha值
// 调色板的Alpha值都为0,而实际上只有0x00000000(一般在调色板第一个色彩)才是透明色,即热血传奇2图片库中8位色彩没有不透明的纯黑色
if(byteArgb[2] == 0 && byteArgb[1] == 0 && byteArgb[0] == 0)
byteArgb[3] = 0;
else
byteArgb[3] = (byte) 255; palette[i] = Common.readInt(byteArgb, 0, true);
} res.setPalette(palette);
return res;
}
readLibHeader
在显示时要记住图片颜色数据的存储是从右向左,从上往下的:
/**
* 从ImageInfo对象及调色板和色彩深度读取图片数据
*
* @param imageInfo
* 图片原数据信息
* @param palette
* 调色板
* @param bitCount
* 色彩深度
* @return
*/
public static BufferedImage readImage(ImageInfo imageInfo, int[] palette, int bitCount) {
BufferedImage res = null;
if(bitCount == 8) {
res = new BufferedImage(imageInfo.getWidth(), imageInfo.getHeight(), BufferedImage.TYPE_INT_ARGB); int index = 0;
for(int h = 0; h < imageInfo.getHeight(); ++h)
for(int w = 0; w < imageInfo.getWidth(); ++w)
res.setRGB(w, res.getHeight() - 1 - h, palette[imageInfo.getPixels()[index++] & 0xff]);
} else if(bitCount == 16) {
// FIXME
res = new BufferedImage(imageInfo.getWidth(), imageInfo.getHeight(), BufferedImage.TYPE_USHORT_565_RGB);
int index = 0;
for(int h = 0; h < imageInfo.getHeight(); ++h)
for(int w = 0; w < imageInfo.getWidth(); ++w, index += 2)
res.setRGB(w, res.getHeight() - 1 - h, Common.readShort(imageInfo.getPixels(), index, true));
} else if(bitCount == 24) {
// FIXME
//res = new BufferedImage(imageInfo.getWidth(), imageInfo.getHeight(), BufferedImage.TYPE_INT_RGB);
//int index = 0;
//for(int h = 0; h < imageInfo.getHeight(); ++h)
// for(int w = 0; w < imageInfo.getWidth(); ++w)
// res.setRGB(w, h, Common.readInt(imageInfo.getPixels(), index++, true));
} else {
// FIXME
//res = new BufferedImage(imageInfo.getWidth(), imageInfo.getHeight(), BufferedImage.TYPE_INT_RGB);
//int index = 0;
//for(int h = 0; h < imageInfo.getHeight(); ++h)
// for(int w = 0; w < imageInfo.getWidth(); ++w)
// res.setRGB(w, h, Common.readInt(imageInfo.getPixels(), index++, true));
}
return res;
}
readImage
说了这么久,我自己都糊涂了。大家如果不清楚请下载源码或基于Eclipse和JDK的项目进行查看。
编辑于2015-01-25 21:06:33
项目没有继续下去,不过我用lwjgl重写了部分,实现了地图加载和纹理读取。大家可以去新的项目地址checkout代码。
编辑于2017-08-16 13:53:40
github上我上传了一个轻便的库,可以一看https://github.com/jootnet/mir2.core
欢迎您移步我们的交流群,无聊的时候大家一起打发时间:
或者通过QQ与我联系:
(最后编辑时间2014-03-16 15:43:23)