通过Java修改游戏存档的实现思路
作者:Dream_飞翔 时间:2023-07-30 20:10:55
前言
植物大战僵尸的数据文件是存储在本地的dat文件当中,修改在本地的dat文件就可以修改到游戏中的数据。之前使用二进制编码工具Hex Editor Neo实现了修改植物大战僵尸的本地游戏数据,现在尝试不使用Hex Editor Neo二进制工具编辑游戏存档,使用Java程序来编辑游戏在本地存储的数据。在经历了几次失败以后成功的实现了在Java程序中修改植物大战僵尸的本地数据,在这里将实现的过程以及思路和错误记录下来,便于以后返回温习。
使用Hex Editor Neo修改游戏数据博客链接:C1任务01-修改游戏存档
提示:以下是本篇文章正文内容,下面案例可供参考
一、实现思路
不论是做大型的项目或者只是实现一个小的功能,都要先明确实现的思路,哪一步要做什么要事先明确,不然就会像无头苍蝇一样不知所措。
Java版本:JDK1.8
使用工具:IntelliJ IDEA 2021.1.2
项目管理:maven
idea2021最新激活码:
https://www.jb51.net/article/196349.htm
https://www.yuque.com/docs/share/b996d27e-c888-45f2-bb1e-f6db5efe2485?#
实现思路相对简单,因为植物大战僵尸游戏的数据文件存储在本地的存储位置是已知的,因此我们可以将实现过程拆分为以下三个步骤:
将.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集合
*/
public static List<Integer> readFile() {
try {
// 声明文件对象
File file = new File(filePath);
// 将文件内容读取到文件读取流当中
InputStream in;
// 将读取的流进行封装
in = new FileInputStream(file);
// 定义整数对象用于存储读取到的内容
int content;
// 一次读取一行,直到读入的内容为null时读取文件的过程结束
while ((content = in.read()) != -1) {
// 将读取到的内容存储到List集合中
nums.add(content);
}
// 关闭流
in.close();
} catch (Exception e) {
e.printStackTrace();
}
return nums;
}
2. 修改关卡信息
在之前修改游戏存档数据时就明白,在user1.dat
文件中,第4列中的十六进制内容代表着关卡信息,因此在修改游戏的关卡信息时就要指定List
集合中下标为4的集合数据值为用户输入的值。在这里用户输入的数据虽然是十进制的数据,但是在将数据写入user1.dat
文件时不需要再进行十进制到十六进制的转换了,因为最后在文件中存储的形式都是二进制的0和1的形式进行存储的。
/**
* 修改关卡数据
*
* @param result 要修改的关卡(十六进制)
*/
public void writeFileCheckPoint(String result) {
// 进行文件的读取
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 传来的整型数组
*/
public static void writeFile(List<Integer> dataList) {
// 声明要输出到的文件对象
File file = new File(filePath);
try {
// 定义数据输出流
DataOutputStream out = new DataOutputStream(new FileOutputStream(file));
// 遍历传来的List集合
for (Integer integer : dataList) {
// 将List集合中的数据写入到user1.dat文件中
out.write(integer);
// 刷新输出流
out.flush();
}
// 关闭输出流
out.close();
} catch (Exception e) {
e.printStackTrace();
}
}
3. 修改金币信息
在这里还要注意到,虽然第八列和第九列的内容代表着金币信息,但是在这里的第九列的数据为高位,并不是按照惯性思维从第八列开始依次排列,因此在存储金币信息时要进行单独的处理。
具体的处理方法就是,将十进制数转换为十六进制数据时如果转换后的十六进制数的长度为3位(在Java中十进制数转换为十六进制数时,十六进制数是以String类型进行存储和显示的),则在转换后的字符串的起始位置加0,这样做的原因是要进行截取两位,让高位的数据在高位存储,低位的数据在低位存储。
例如:
十进制数据转换为十六进制数据的转换方法如下:
/**
* 将十进制整数转换为16进制的字符串
*
* @param num 传来的十进制整数
* @return 转换为16进制后的字符串
*/
public static String intToHex(int num) {
// 如果传来的整数为0则直接返回
if (num == 0) {
return "0";
}
// 使用到StringBuilder效率会更高
StringBuilder builder = new StringBuilder();
// 定义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反转并返回
return builder.reverse().toString();
}
修改金币数量的方法:
/**
* 修改金币数据
*
* @param result 要修改的金币数量(十六进制)
*/
public void writeFileMoney(String result) {
// 进行文件的读取
List<Integer> dataList = ReadUtil.readFile();
// 将传来的字符长度进行除2运算
int count = result.length() >> 1;
if (count > 1) {
// 以两位为单位长度进行截取,一共有两个数据
String firstStr = result.substring(0, 2); // 低位数据
String secondStr = 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,当从很多的金币修改到很少的金币时要确保第九位为0
dataList.set(9, 0);
// 将修改后的整型List集合写入到dat文件中
ReadUtil.writeFile(dataList);
System.out.println("金币数据写入完成!");
// 清空集合中的数据
dataList.removeAll(dataList);
}
}
四、代码测试
接下来对所写的Java项目进行测试,首先是一个金币为0且关卡数为0的空白存档,在修改文件时先将植物大战僵尸关闭,因为在修改数据文件时如果植物大战僵尸游戏开着,虽然将数据文件的内容做了修改,但是在关闭植物大战僵尸后,游戏仍然会将当前游戏内的数据信息覆盖到dat文件中,因此就相当于没有进行任何修改。
现在关闭植物大战僵尸游戏并且在IntelliJ IDEA中将主类启动
1. 读取数据文件
首先读取数据文件,查看第4列的数据是否为01(默认第一关)与第8列的数据是否为0(默认金币为0)
读取到的数据文件内容正确
2. 修改关卡位置
现在将关卡修改到第42关,即5-2关
再读取数据文件,查看第四列的值是否为2a
现在进入到游戏中查看关卡是否改变
关卡的数据和我们修改的内容一样,现在再查看商店中的金币数量是否为0
金币数量也为0,说明只修改了关卡信息
3. 修改金币数量
此时进行修改金币的数量
再读取数据文件,查看第八列的数据是否为e8,第九列的数据是否为03
进入游戏中查看金币是否发生了变化
此时金币数量修改为了10000
4. 退出修改器
5. 输入参数错误情况
在项目启动时输入的内容不是修改器的功能选项时
关卡位置和金币数量越界情况
成功以JSON格式输出,至此Java项目测试完毕
五、源码
1. 项目结构
2. 项目代码
①. ResultInfo类,位于pojo包
package com.shijimo.game.pojo;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.experimental.Accessors;
/**
* @author Dream_飞翔
* @date 2021/10/26
* @time 18:32
* @email 1072876976@qq.com
*
* 输出提示信息
*/
@Data
@Accessors(chain = true)
@NoArgsConstructor
@AllArgsConstructor
public class ResultInfo {
private Integer code; // 状态码
private String msg; // 提示信息
}
②. EditorService类,位于service包
package com.shijimo.game.service;
import com.shijimo.game.util.ReadUtil;
import java.util.List;
/**
* @author Dream_飞翔
* @date 2021/10/26
* @time 18:29
* @email 1072876976@qq.com
*/
public class EditorService {
/**
* 修改关卡数据
*
* @param result 要修改的关卡(十六进制)
*/
public void writeFileCheckPoint(String result) {
// 进行文件的读取
List<Integer> dataList = ReadUtil.readFile();
// 将修改关卡列上的数据
dataList.set(4, Integer.valueOf(result, 16));
ReadUtil.writeFile(dataList);
dataList.removeAll(dataList);
System.out.println("关卡数据写入完成!");
}
/**
* 修改金币数据
*
* @param result 要修改的金币数量(十六进制)
*/
public void writeFileMoney(String result) {
// 进行文件的读取
List<Integer> dataList = ReadUtil.readFile();
// 将传来的字符长度进行除2运算
int count = result.length() >> 1;
if (count > 1) {
// 以两位为单位长度进行截取,一共有两个数据
String firstStr = result.substring(0, 2);
String secondStr = 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,如果从高位改到低位的话要确保第九位为0
dataList.set(9, 0);
ReadUtil.writeFile(dataList);
System.out.println("金币数据写入完成!");
dataList.removeAll(dataList);
}
}
}
③. NumUtil类,位于util包
package com.shijimo.game.util;
/**
* @author Dream_飞翔
* @date 2021/10/26
* @time 16:32
* @email 1072876976@qq.com
* <p>
* 本类用于将整数进行进制的转换
*/
public class NumUtil {
/**
* 将十进制整数转换为16进制的字符串
*
* @param num 传来的整数
* @return 转换为16进制后的字符串
*/
public static String intToHex(int num) {
// 如果传来的整数为0则直接返回
if (num == 0) {
return "0";
}
// 使用到StringBuilder效率会更高
StringBuilder builder = new StringBuilder();
// 定义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();
}
// else if (builder.length() >= 1 && builder.length() % 2 != 0) {
// return builder.reverse() + "0";
// }
// 最后将builder反转并返回
return builder.reverse().toString();
}
}
⑥. ReadUtil类,位于util包
package com.shijimo.game.util;
import java.io.*;
import java.util.ArrayList;
import java.util.List;
/**
* @author Dream_飞翔
* @date 2021/10/26
* @time 10:11
* @email 1072876976@qq.com
* <p>
* 该类用于读取二进制文件中的数据,植物大战僵尸的本地数据文件中只有一行数据
* 1. 通过InputStream进行二进制方式读取
* 2. 对每一行的内容进行特殊的处理
*/
public class ReadUtil {
// 定义文件的路径
static String filePath = "C:\\ProgramData\\PopCap Games\\PlantsVsZombies\\userdata\\user1.dat";
// 定义整型集合类用于存储
static List<Integer> nums = new ArrayList<>();
/**
* 读取文件内容并将读取到的内容以List集合的格式返回
*
* @return 数据的List集合
*/
public static List<Integer> readFile() {
try {
// 声明文件对象
File file = new File(filePath);
// 将文件内容读取到文件读取流当中
InputStream in;
// 将读取的流进行封装
in = new FileInputStream(file);
// 定义整数对象用于存储读取到的内容
int content;
// 一次读取一行,直到读入的内容为null时读取文件的过程结束
while ((content = in.read()) != -1) {
// 将读取到的内容存储到List集合中
nums.add(content);
}
// 关闭流
in.close();
} catch (Exception e) {
e.printStackTrace();
}
return nums;
}
/**
* 将文件内容写入到user1.dat文件中,可以进行修改关卡和修改金币数量
*
* @param dataList 传来的整型数组
*/
public static void writeFile(List<Integer> dataList) {
// 声明要输出到的文件对象
File file = new File(filePath);
try {
// 定义数据输出流
DataOutputStream out = new DataOutputStream(new FileOutputStream(file));
// 遍历传来的List集合
for (Integer integer : dataList) {
// 将List集合中的数据写入到user1.dat文件中
out.write(integer);
// 刷新输出流
out.flush();
}
// 刷新输出流
out.flush();
// 关闭输出流
out.close();
} catch (Exception e) {
e.printStackTrace();
}
}
/**
* 将指定的文件内容输出到控制台上
*/
public static void printFile() {
// 定义整型集合类用于存储
List<Integer> dataList = new ArrayList<>();
try {
// 声明文件对象
File file = new File(filePath);
// 将文件内容读取到文件读取流当中
InputStream in;
// 将读取的流进行封装
in = new FileInputStream(file);
// 定义整数对象用于存储读取到的内容
int content;
// 一次读取一行,直到读入的内容为null时读取文件的过程结束
while ((content = in.read()) != -1) {
// 将读取到的内容存储到List集合中
dataList.add(content);
}
// 关闭流
in.close();
} catch (Exception e) {
e.printStackTrace();
}
// 定义标志变量的初值为0
int count = 0;
// 将读取到的内容输出
System.out.println("00\t01\t02\t03\t04\t05\t06\t07\t08\t09\t0a\t0b\t0c\t0d\t0e\t0f");
// 遍历读取到的整数集合
for (Integer data : dataList) {
// 如果标志变量的值对16做取余运算为0的话则进行换行操作
if (count % 16 == 0)
System.out.println();
System.out.print(NumUtil.intToHex(data) + "\t");
count++;
}
}
}
⑦. EditorApplication主启动类,位于项目的最外层包中
package com.shijimo.game;
import com.alibaba.fastjson.JSONObject;
import com.shijimo.game.pojo.ResultInfo;
import com.shijimo.game.service.EditorService;
import com.shijimo.game.util.NumUtil;
import com.shijimo.game.util.ReadUtil;
import java.util.Scanner;
/**
* @author Dream_飞翔
* @date 2021/10/26
* @time 17:01
* @email 1072876976@qq.com
*/
public class EditorApplication {
public static void main(String[] args) {
// 将业务处理对象实例化
EditorService editorService = new EditorService();
System.out.println("**********************************************************");
System.out.println(" ,---._ \n" +
" .-- -.' \\ \n" +
" | | : \n" +
" : ; | \n" +
" : | .---. \n" +
" | : : ,--.--. /. ./| ,--.--. \n" +
" : / \\ .-' . ' | / \\ \n" +
" | ; | .--. .-. | /___/ \\: | .--. .-. | \n" +
" ___ l \\__\\/: . . . \\ ' . \\__\\/: . . \n" +
" / /\\ J : ,\" .--.; | \\ \\ ' ,\" .--.; | \n" +
"/ ../ `..- , / / ,. | \\ \\ / / ,. | \n" +
"\\ \\ ; ; : .' \\ \\ \\ | ; : .' \\ \n" +
" \\ \\ ,' | , .-./ '---\" | , .-./ \n" +
" \"---....--' `--`---' `--`---' ");
System.out.println("\n 老张写的植物大战僵尸修改器 version: 1.0");
while (true) {
System.out.println("**********************************************************");
System.out.println("* =======> 1. 修改关卡位置 <======= *");
System.out.println("* =======> 2. 修改金币数量 <======= *");
System.out.println("* =======> 3. 读取数据文件 <======= *");
System.out.println("* =======> 4. 退出此修改器 <======= *");
System.out.println("**********************************************************");
System.out.print("请输入您的选择:");
// 定义Scanner对象用于接收从键盘上输入的数字
Scanner scanner = new Scanner(System.in);
int choose = scanner.nextInt();
switch (choose) {
case 1: {
Scanner editor = new Scanner(System.in);
System.out.println("您的选择是 => 选择修改关卡的位置");
System.out.print("请输入您想要跳到的关卡位置(最高50关):");
// 接收关卡的位置数据
int checkPoint = editor.nextInt();
// 进行越界判断
if (checkPoint <= 0 || checkPoint > 50) {
System.out.println(JSONObject.toJSONString(new ResultInfo(500, "输入数据有误")));
break;
}
// 将提示信息输出
System.out.println("正在修改关卡数据...");
// 如果输入的数据合法,将其转换为十六进制数据
String result = NumUtil.intToHex(checkPoint);
// 调用业务层的方法进行修改内容
editorService.writeFileCheckPoint(result);
System.out.println("关卡数据修改成功!");
}
break;
case 2: {
System.out.println("您的选择是 => 修改游戏的金币数量");
System.out.print("请输入您想要修改的金币数量(最高655350个):");
// 声明输入对象
Scanner editor = new Scanner(System.in);
// 接收输入的金币数量
int money = editor.nextInt();
// 进行越界判断
if (money > 655350 || money < 0) {
System.out.println(JSONObject.toJSONString(new ResultInfo(500, "输入数据有误")));
break;
}
// 如果输入的数据合法,将其转换为十六进制数据
String result = NumUtil.intToHex(money / 10);
// 调用业务层的方法进行修改数据并返回修改结果
editorService.writeFileMoney(result);
// 如果金币数量修改成功
System.out.println("金币数据修改成功!");
}
break;
case 3: {
System.out.println("您的选择是 => 读取游戏的数据文件");
System.out.println("开始读取数据文件...");
// 读取数据文件并打印
ReadUtil.printFile();
// 换行
System.out.println();
// 输出提示信息
System.out.println("数据文件读取成功!");
} break;
case 4: {
System.out.println("感谢您的使用,期待下次再见!");
System.exit(0);
}
default:
System.out.println(JSONObject.toJSONString(new ResultInfo(500, "输入指令有误")));
}
}
}
}
⑧. pom.xml文件
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>org.example</groupId>
<artifactId>GameEditor</artifactId>
<version>1.0-SNAPSHOT</version>
<properties>
<maven.compiler.source>8</maven.compiler.source>
<maven.compiler.target>8</maven.compiler.target>
</properties>
<dependencies>
<!-- 导入alibaba的Json依赖 -->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.62</version>
</dependency>
<!-- 导入lombok工具 -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.20</version>
</dependency>
<!-- 导入单元测试依赖 -->
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.13</version>
</dependency>
</dependencies>
</project>
来源:https://blog.csdn.net/qq_48455576/article/details/121002191