前言
植物大战僵尸的数据文件是存储在本地的dat文件当中,修改在本地的dat文件就可以修改到游戏中的数据。之前使用二进制编码工具HexEditor Neo实现了修改植物大战僵尸的本地游戏数据,现在尝试不使用Hex Editor Neo二进制工具编辑游戏存档,使用Java程序来编辑游戏在本地存储的数据。在经历了几次失败以后成功的实现了在Java程序中修改植物大战僵尸的本地数据,在这里将实现的过程以及思路和错误记录下来,便于以后返回温习。
使用Hex Editor Neo修改游戏数据博客链接:C1任务01-修改游戏存档
一、实现思路
不论是做大型的项目或者只是实现一个小的功能,都要先明确实现的思路,哪一步要做什么要事先明确,不然就会像无头苍蝇一样不知所措。
Java版本:JDK1.8
使用工具:IntelliJ IDEA 2021.1.2
项目管理:maven
实现思路相对简单,因为植物大战僵尸游戏的数据文件存储在本地的存储位置是已知的,因此我们可以将实现过程拆分为以下三个步骤:
- 将.dat数据文件抽象为File对象,使用IO流将数据读取到Java程序当中
- 将相应位置的数据修改为用户输入的数据
- 最后将Java程序中存储的数据通过IO流写回到本地的dat数据文件中
这里可以覆盖回数据文件中也可以修改指定位置的数据,在这里我采用的方法是覆盖原文件的数据。
二、项目准备
在正式编写代码之前要先做一些准备工作
1. 创建maven工程
因为本身并不需要在浏览器端展示数据,因此创建一个空的maven工程即可
到这里一个maven工程就创建完毕
2. 导入依赖
①. JSON依赖
在这个Java项目中如果出现异常或其它错误情况,我是以JSON形式输出到控制台,因此在这里我导入了阿里巴巴开发的fastjson
依赖
<!--导入alibaba的Json依赖--><dependency><groupId>com.alibaba</groupId><artifactId>fastjson</artifactId><version>1.2.62</version></dependency>
②. Lombok依赖
导入lombok
依赖的原因是为了减少实体类中的代码量,使代码更简洁,可读性更高
<!--导入lombok工具--><dependency><groupId>org.projectlombok</groupId><artifactId>lombok</artifactId><version>1.18.20</version></dependency>
③. Junit4单元测试
在写代码时确保所写方法没有问题的一种方式就是使用单元测试,在这里我导入了Junit4
单元测试框架
<!--导入单元测试依赖--><dependency><groupId>junit</groupId><artifactId>junit</artifactId><version>4.13</version></dependency>
至此maven工程中的依赖全部导入完成
三、核心代码
1. 使用的对象
在读取dat数据文件中要使用到以下几个Java对象,在此进行简单的介绍
- InputStream: 该抽象类是所有的类表示字节输入流的父类
- FileInputStream:从文件系统中的文件中获得输入的字节
- DataOutputStream:将数据写入到指定的基本输出流中
2. 读取数据文件
我在读取数据文件时将文件的存储路径定义成了全局变量,便于在每个方法中进行调用
因为存储植物大战僵尸的数据文件user1.dat中数据是以二进制的方式进行存储,因此我们在读取文件内容时也要使用二进制的方式进行读取。
如果使用字符的方式进行读取的话会出现读取出的数据只有几个字符的情况,用记事本打开dat文件就会发现在二进制数据文件中的内容只有一行并且很多字符都是以空格形式存在的,因此使用字符读入的方式就只能读取到一行数据,并且空格数据会被当成null进行处理,所以显示的结果就只有几个字符。
将读取到的整数数据存储到泛型约束为Integer
类型的List
集合当中,进行存储
/*** 读取文件内容并将读取到的内容以List集合的格式返回** @return 数据的List集合*/publicstaticList<Integer>readFile() { try { // 声明文件对象Filefile=newFile(filePath); // 将文件内容读取到文件读取流当中InputStreamin; // 将读取的流进行封装in=newFileInputStream(file); // 定义整数对象用于存储读取到的内容intcontent; // 一次读取一行,直到读入的内容为null时读取文件的过程结束while ((content=in.read()) !=-1) { // 将读取到的内容存储到List集合中nums.add(content); } // 关闭流in.close(); } catch (Exceptione) { e.printStackTrace(); } returnnums; }
2. 修改关卡信息
在之前修改游戏存档数据时就明白,在user1.dat
文件中,第4列中的十六进制内容代表着关卡信息,因此在修改游戏的关卡信息时就要指定List
集合中下标为4的集合数据值为用户输入的值。在这里用户输入的数据虽然是十进制的数据,但是在将数据写入user1.dat
文件时不需要再进行十进制到十六进制的转换了,因为最后在文件中存储的形式都是二进制的0和1的形式进行存储的。
/*** 修改关卡数据** @param result 要修改的关卡(十六进制)*/publicvoidwriteFileCheckPoint(Stringresult) { // 进行文件的读取List<Integer>dataList=ReadUtil.readFile(); // 将修改关卡列上的数据dataList.set(4, Integer.valueOf(result, 16)); ReadUtil.writeFile(dataList); dataList.removeAll(dataList); System.out.println("关卡数据写入完成!"); }
将数据输出到数据文件的方法
/*** 将文件内容写入到user1.dat文件中,可以进行修改关卡和修改金币数量** @param dataList 传来的整型数组*/publicstaticvoidwriteFile(List<Integer>dataList) { // 声明要输出到的文件对象Filefile=newFile(filePath); try { // 定义数据输出流DataOutputStreamout=newDataOutputStream(newFileOutputStream(file)); // 遍历传来的List集合for (Integerinteger : dataList) { // 将List集合中的数据写入到user1.dat文件中out.write(integer); // 刷新输出流out.flush(); } // 关闭输出流out.close(); } catch (Exceptione) { e.printStackTrace(); } }
3. 修改金币信息
在这里还要注意到,虽然第八列和第九列的内容代表着金币信息,但是在这里的第九列的数据为高位,并不是按照惯性思维从第八列开始依次排列,因此在存储金币信息时要进行单独的处理。
具体的处理方法就是,将十进制数转换为十六进制数据时如果转换后的十六进制数的长度为3位(在Java中十进制数转换为十六进制数时,十六进制数是以String类型进行存储和显示的),则在转换后的字符串的起始位置加0,这样做的原因是要进行截取两位,让高位的数据在高位存储,低位的数据在低位存储。
例如:
十进制数据转换为十六进制数据的转换方法如下:
/*** 将十进制整数转换为16进制的字符串** @param num 传来的十进制整数* @return 转换为16进制后的字符串*/publicstaticStringintToHex(intnum) { // 如果传来的整数为0则直接返回if (num==0) { return"0"; } // 使用到StringBuilder效率会更高StringBuilderbuilder=newStringBuilder(); // 定义16进制下的所有数字char[] hexChar= {'0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f'}; // 如果传来的数不为0则一直进行除法运算while (num!=0) { builder=builder.append(hexChar[num%16]); num=num/16; } if (builder.length() ==1||builder.length() %2!=0) { return"0"+builder.reverse(); } // 最后将builder反转并返回returnbuilder.reverse().toString(); }
修改金币数量的方法:
/*** 修改金币数据** @param result 要修改的金币数量(十六进制)*/publicvoidwriteFileMoney(Stringresult) { // 进行文件的读取List<Integer>dataList=ReadUtil.readFile(); // 将传来的字符长度进行除2运算intcount=result.length() >>1; if (count>1) { // 以两位为单位长度进行截取,一共有两个数据StringfirstStr=result.substring(0, 2); // 低位数据StringsecondStr=result.substring(2, 4); // 高位数据// 设置低位的数据dataList.set(8, Integer.valueOf(secondStr, 16)); // 设置高位的数据dataList.set(9, Integer.valueOf(firstStr, 16)); // 将修改后的金币数据数据写入文件ReadUtil.writeFile(dataList); System.out.println("金币数据写入完成!"); // 清空集合中的数据dataList.removeAll(dataList); } else { // 将修改关卡列上的数据dataList.set(8, Integer.valueOf(result, 16)); // 当进入这里时第九位一定为0,当从很多的金币修改到很少的金币时要确保第九位为0dataList.set(9, 0); // 将修改后的整型List集合写入到dat文件中ReadUtil.writeFile(dataList); System.out.println("金币数据写入完成!"); // 清空集合中的数据dataList.removeAll(dataList); } }