文章目錄
- 前言
- 一、實作思路
- 二、專案準備
- 1. 創建maven工程
- 2. 匯入依賴
- ①. JSON依賴
- ②. Lombok依賴
- ③. Junit4單元測驗
- 三、核心代碼
- 1. 使用的物件
- 2. 讀取資料檔案
- 2. 修改關卡資訊
- 3. 修改金幣資訊
- 四、代碼測驗
- 1. 讀取資料檔案
- 2. 修改關卡位置
- 3. 修改金幣數量
- 4. 退出修改器
- 5. 輸入引數錯誤情況
- 五、原始碼
- 1. 專案結構
- 2. 專案代碼
- 總結
前言
植物大戰僵尸的資料檔案是存盤在本地的dat檔案當中,修改在本地的dat檔案就可以修改到游戲中的資料,之前使用二進制編碼工具Hex Editor 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集合
*/
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>
總結
以上便是使用Java修改植物大戰僵尸資料檔案的程序,還記得當時在使用工具去修改游戲存檔資料時都感覺到不可思議與不敢相信!而此時通過Java代碼進行修改檔案資料時都沒有一絲的感覺自己做不成功,對于自己來說,不僅僅是技術上的提高,更重要的是心態上的變化,當自己獨立寫過一定數量的代碼時內心就會變得很有底氣,變得更加相信自己,可能就像很多人說的那樣,真正的技術一定是相當數量的代碼堆疊起來的!
自然界沒有風風雨雨,大地就不會春華秋實,若不嘗試著做些本事之外的事,就永遠不會成長!人生的價值并不在于成功后的榮光,而在于追求的本身,在于信念的樹立與堅持的程序,堅守信念,猶如在內心撒下一顆種子,只要在適宜的條件下,種子自會生根發芽破土而出,總會有識訓果實的期望,有時需要外力輔助才可取得成果,但最侄訓要靠自我去完成,因為任何人也不可能把信念深植于你的心中,所以,我們要堅守自我的信念,播下期望的種子,做一名自信者,牢牢把住自我生命的羅盤!

轉載請註明出處,本文鏈接:https://www.uj5u.com/ruanti/340485.html
標籤:其他
