diff --git a/2024/07/07/hello-world/index.html b/2024/07/07/hello-world/index.html deleted file mode 100644 index 75d09cd..0000000 --- a/2024/07/07/hello-world/index.html +++ /dev/null @@ -1,205 +0,0 @@ - - - - - - - Hello World | Creeper Blog - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
-
- - -
-
- -
- - -
- - -

- Hello World -

- - -
- -
- -

Welcome to Hexo! This is your very first post. Check documentation for more info. If you get any problems when using Hexo, you can find the answer in troubleshooting or you can ask me on GitHub.

-

Quick Start

Create a new post

1
$ hexo new "My New Post"
- -

More info: Writing

-

Run server

1
$ hexo server
- -

More info: Server

-

Generate static files

1
$ hexo generate
- -

More info: Generating

-

Deploy to remote sites

1
$ hexo deploy
- -

More info: Deployment

- - -
- -
- - - -
- - -
- - - -
-
- -
- -
-
- -
- - - - - - - - - - - - - - - - - - - - -
- - \ No newline at end of file diff --git "a/2024/07/07/\346\265\213\350\257\225\345\217\221\345\270\203\346\226\207\347\253\240/index.html" "b/2024/07/07/\346\265\213\350\257\225\345\217\221\345\270\203\346\226\207\347\253\240/index.html" new file mode 100644 index 0000000..0539512 --- /dev/null +++ "b/2024/07/07/\346\265\213\350\257\225\345\217\221\345\270\203\346\226\207\347\253\240/index.html" @@ -0,0 +1,223 @@ + + + + + + + 测试发布文章 | Creeper Blog + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+ + +
+
+ + +
+ + + +
+
+ +
+ +
+
+ +
+ + + + + + + + + + + + + + + + + + + + +
+ + \ No newline at end of file diff --git "a/2024/07/26/\344\275\277\347\224\250Spring-Cloud-Feign\346\236\204\345\273\272\345\243\260\346\230\216\345\274\217\345\256\242\346\210\267\347\253\257/index.html" "b/2024/07/26/\344\275\277\347\224\250Spring-Cloud-Feign\346\236\204\345\273\272\345\243\260\346\230\216\345\274\217\345\256\242\346\210\267\347\253\257/index.html" new file mode 100644 index 0000000..c741fd2 --- /dev/null +++ "b/2024/07/26/\344\275\277\347\224\250Spring-Cloud-Feign\346\236\204\345\273\272\345\243\260\346\230\216\345\274\217\345\256\242\346\210\267\347\253\257/index.html" @@ -0,0 +1,235 @@ + + + + + + + 使用Spring Cloud Feign构建声明式客户端 | Creeper Blog + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+ + +
+
+ +
+ + +
+ + +

+ 使用Spring Cloud Feign构建声明式客户端 +

+ + +
+ +
+ +

在使用Spring Boot开发微服务后端时,有时需要向其他微服务发起HTTP请求。如果使用传统的RestTemplate方法,先构造HTTP请求,再解析返回的HTTP响应,会产生很多冗余代码,降低项目的可读性。Spring Cloud Feign可以很好地解决这个问题。Feign是一种声明式、模板化的HTTP客户端,开发者只需要定义接口,便可以向调用本地方法一样调用远程接口,非常简洁方便。
此处Spring Boot项目使用maven构建,在pom.xml中加入依赖并安装:

+
1
2
3
4
5
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
<version>4.1.1</version>
</dependency>
+

在Spring Boot的主文件中加入@EnableFeignClients注解:

+
1
2
3
4
5
6
7
8
9
@SpringBootApplication
@EnableFeignClients
public class UniGptBotServiceApplication {

public static void main(String[] args) {
SpringApplication.run(UniGptBotServiceApplication.class, args);
}

}
+

创建一个client目录,用于放置所有Feign客户端类,例如,我需要向微服务user-service发送远程请求,则创建一个UserServiceClient类:

+
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
package com.unigpt.bot.client;

import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.stereotype.Component;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestHeader;

@Component
@FeignClient(name = "user-service", url = "http://localhost:8082/internal")
public interface UserServiceClient {

@DeleteMapping("/users/used-bots/{botId}")
public void deleteBotFromUsedList(
@PathVariable Integer botId,
@RequestHeader(name = "X-User-Id") Integer userId);
}
+

此处,我们定义了UserServiceClient接口,@FeignClient注解定义了服务的名称url,接口的函数定义与controller类似,可以使用注解规定路径参数、请求参数、请求头、请求体信息。Feign会根据定义的接口和注解自动生成实现。

+

定义完客户端接口类后,依赖注入UserServiceClient类,即可使用deleteBotFromUsedList方法,像调用本地方法一样调用远程服务。

+ + +
+ +
+ + + + + +
+ + +
+ + + +
+
+ +
+ +
+
+ +
+ + + + + + + + + + + + + + + + + + + + +
+ + \ No newline at end of file diff --git "a/2024/08/18/Linux\345\210\207\346\215\242java\347\211\210\346\234\254/index.html" "b/2024/08/18/Linux\345\210\207\346\215\242java\347\211\210\346\234\254/index.html" new file mode 100644 index 0000000..1cd0c39 --- /dev/null +++ "b/2024/08/18/Linux\345\210\207\346\215\242java\347\211\210\346\234\254/index.html" @@ -0,0 +1,231 @@ + + + + + + + Linux切换java版本 | Creeper Blog + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+ + +
+
+ +
+ + +
+ + +

+ Linux切换java版本 +

+ + +
+ +
+ +

Linux系统中同时安装了多个版本的Java,可以使用update-alternatives命令来切换默认的Java版本。以下是具体的步骤:

+

打开终端,输入:

+
1
sudo update-alternatives --config java
+

这将列出系统中所有可用的Java版本。
在提示选择版本的列表中,输入想要设为默认的Java版本对应的数字,然后按回车,即可切换默认的java版本。

+ + +
+ +
+ + + + + +
+ + +
+ + + +
+
+ +
+ +
+
+ +
+ + + + + + + + + + + + + + + + + + + + +
+ + \ No newline at end of file diff --git "a/2024/08/25/\344\275\277\347\224\250libGDX\347\247\273\346\244\21520\345\271\264\345\211\215\347\232\204J2ME\346\270\270\346\210\217\343\200\212\344\270\255\345\233\275\350\267\263\346\243\213\343\200\213/index.html" "b/2024/08/25/\344\275\277\347\224\250libGDX\347\247\273\346\244\21520\345\271\264\345\211\215\347\232\204J2ME\346\270\270\346\210\217\343\200\212\344\270\255\345\233\275\350\267\263\346\243\213\343\200\213/index.html" new file mode 100644 index 0000000..632e7bc --- /dev/null +++ "b/2024/08/25/\344\275\277\347\224\250libGDX\347\247\273\346\244\21520\345\271\264\345\211\215\347\232\204J2ME\346\270\270\346\210\217\343\200\212\344\270\255\345\233\275\350\267\263\346\243\213\343\200\213/index.html" @@ -0,0 +1,291 @@ + + + + + + + 使用libGDX移植20年前的J2ME游戏《中国跳棋》 | Creeper Blog + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+ + +
+
+ +
+ + +
+ + +

+ 使用libGDX移植20年前的J2ME游戏《中国跳棋》 +

+ + +
+ +
+ + +

背景介绍

暑假闲来无事,我在家中无意翻找到了自己使用的第一部触屏手机,是一个Samsung Anycall手机,由于电池老化的原因,我无法再次打开这个手机了。十年前我还是个小学生,智能手机并不普及,对于这第一个触屏手机也是非常有新鲜感,什么都想玩一下。其中最为印象深刻的就是一个跳棋游戏,主人公需要和6个npc对战中国跳棋,最终通关游戏。由于是将近二十年前开发的老游戏,在互联网上完全无法查到游戏开发商的任何信息,这个游戏也不可能在现代的机器上运行了,想到这个,我有些心血来潮,想让这个童年吸引我的小游戏重新焕发一次生命力。

+

查找游戏资源

我只记得这个游戏叫做《中国跳棋》,并且是十年前的Samsung手机内置的,于是在bilibili和抖音都搜索了一下,果然搜出了几个视频,都是展示在十几年前的手机上玩这个游戏的。从视频中我回忆起了一些关于这个游戏的具体信息,在互联网上进行了查找。不久我就在一个论坛(DOSPY论坛)上找到了这个游戏的相关资源:【稀有资源】中 国 跳 棋,游戏的形式是一个jar包。

+

使用模拟器运行J2ME游戏

十几年前按键手机和部分触屏手机上的很多游戏都是使用J2ME开发的,维基百科对于J2ME的介绍为:

+
1
Java ME以往称作J2ME(Java Platform, Micro Edition)是为机顶盒、移动电话和PDA之类嵌入式消费电子设备提供的Java语言平台,包括虚拟机和一系列标准化的Java API。它和Java SE、Java EE一起构成Java技术的三大版本,并且同样是通过JCP(Java Community Process)制订的。
+

J2ME是一个过时的框架,使用其开发的游戏自然无法在现代的操作系统上运行。因此,需要使用模拟器运行游戏的jar包。手机端可以选择J2ME Loader,电脑端可以选择microemulator
我使用microemulator运行原版游戏,部分界面如下:

+
+ alt text + alt text +
+ +

解包游戏并反编译

从DOSPY论坛上下载的jar包其实就是一个zip压缩文件,在Linux中,可通过unzip命令解压缩到指定目录extracted:

+
1
unzip your-file.jar -d extracted/
+

进入extracted目录,可看到包含Java的.class文件和资源文件(图片、音频)
alt text
与C/C++程序不同的是,Java编译出的.class字节码非常容易被反编译为Java代码,所以通常开发者会对代码进行混淆,使得其可读性下降,难以被破解。
我使用CFR工具反编译字节码,当然也有很多其他的工具。
CFR工具本身也是一个jar文件,反编译单个字节码文件:

+
1
java -jar cfr.jar target.jar > target.java
+

在Linux中,使用find命令,将extracted目录下所有的.class文件都反编译为.java文件:

+
1
find extracted/ -name "*.class" -exec java -jar cfr.jar {} --outputdir extracted \;
+ +

阅读源代码

幸运的是,本游戏的代码并没有过度混淆,不幸的是,即使反编译为Java源代码,我也不可能在我的机器上运行这些代码,只能阅读代码,同时借助Copilot加速理解代码。
虽然代码的结构清晰,但是由于代码量较大,我花了整整两天时间理解整个游戏的代码逻辑,由于篇幅限制,只能说几个较为重要的发现。

+

整型常量被大量使用

代码中整型常量被大量使用,例如:

+
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public class BoardView
implements XTimerListener,
Viewable {
public static final int GAMEREADY = 0;
public static final int SELECTDIA = 1;
public static final int MOVEDIA = 2;
public static final int MOVINGDIA = 3;
public static final int COMTHINK = 4;
public static final int COMBO_MSG = 5;
public static final int TIMEOUT_MSG = 6;
public static final int CHANGETURN = 7;
public static final int GAMEOVER = 8;
public static final int VIEWRESULT = 9;
public static final int NEXTROUND = 10;
public static final int NEXTSTAGE = 11;
public static final int READYTALK = 12;
public static final int RETURNVSMENU = 13;
public static final int GAMEFAILED = 14;
public static final int COMMOVEDIA = 15;
public static final int HOMEIN = 16;

//...
}
+

Java的枚举类型是在Java5引入的特性,在当时的Java2中还不存在。开发者具有良好的编程习惯,将这些枚举值都使用常量代替,避免了Magic Number。然而,Java编译器直接将这些常量编译成了整数,反编译后,并未保留常量的变量名,使得代码难以理解,例如:

+
1
2
3
4
5
6
7
if (this.state == 12) {
// do something
} else if (this.state == 0) {
// do something
} else if (this.state == 1) {
// do something
}
+

如果没有联系上述常量的定义,确实难以理解这些分支的语义,因此,我在阅读这些代码时,还原了这些常量,使得代码更易于理解。

+

适配不同的屏幕大小

即使是二十年前的代码,也对不同型号的手机屏幕进行了适配:

+
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public static void checkScreenSize() {
if (totalWidth > 118 && totalWidth < 122) {
lcdSize = 3;
} else if (totalWidth > 174 && totalWidth < 178) {
lcdSize = 2;
} else if (totalWidth > 238 && totalWidth < 242) {
lcdSize = totalHeight > 180 ? 1 : 2;
} else if (totalWidth > 319 && totalWidth < 321) {
lcdSize = 4;
}
}

public static boolean isQVGA() {
return lcdSize == 1 || lcdSize == 4;
}
+ +

我专门查了一下QVGA屏幕的含义:

+
1
QVGA images or videos are 320 pixels wide and 240 pixels tall (320 x 240 pixels). The name Quarter VGA is written as QVGA and the resolution is four times smaller than VGA resolution (640 x 480 pixels).
+ +

存在性能优化的代码写法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
int computeMoveGuide(int n) {
byte by = this.dia[n].posx;
byte by2 = this.dia[n].posy;
int n2 = 0;
if (this.moveCnt > 0 && this.jumpMove == 0) {
this.possibleDirCnt = 0;
for (int i = 0; i < 6; ++i) {
this.moveGuide[i] = 0;
}
return 0;
}
for (int i = 0; i < 6; ++i) {
this.moveGuide[i] = 0;
this.moveGuide[i] = this.diaBoard.checkBoard(this.index, by + Resource.hInc[i], by2 + Resource.vInc[i]);
if (this.moveGuide[i] == 2) {
this.moveGuide[i] = this.diaBoard.checkBoard(this.index, by + Resource.hInc[i] * 2, by2 + Resource.vInc[i] * 2);
this.moveGuide[i] = this.moveGuide[i] == 1 ? 2 : 0;
}
if (this.jumpMove > 0 && this.moveGuide[i] == 1) {
this.moveGuide[i] = 0;
}
if (this.moveGuide[i] == 0) continue;
++n2;
}
this.possibleDirCnt = n2;
return n2;
}
+

computeMoveGuide的for循环中,使用n2这个局部变量进行累加计算,最终将n2的值赋值给this.possibleDirCnt,而并没有将this.possibleDirCnt直接进行累加。this.possibleDirCnt是一个类的成员变量,存储在堆内存内,而n2是一个局部变量,可以被放在寄存器中,操作n2只需要读写寄存器,比读写堆内存快得多,这对于十几年前的手机来说应该是有性能的提升。

+

使用libGDX重写游戏代码

经过一个上午的了解,我选择了libGDX框架,对J2ME游戏进行移植,主要原因有几点:

+
    +
  • 编程语言相同:libGDX和J2ME都是使用Java开发,游戏核心算法部分不需要任何改动,只需要修改各种调用的库即可。
  • +
  • 跨平台:不仅Java语言本身跨平台,libGDX框架也是跨平台的,支持桌面端(Windows, MacOS, Linux)和移动端(Android, iOS)
  • +
  • 运行高效:libGDX本身对性能做了一定的优化。
  • +
+

具体的游戏移植过程技术难度并不高,只是比较繁琐,需要同时关注移植前后接口的变化,主要包括:

+
    +
  • 绘制场景的框架不同:J2ME使用javax.microedition.lcdui.Graphics绘制图像,而libGDX使用SpriteBatchStage绘制图像和UI,同时,二者的坐标系统完全不同,需要进行坐标变换。
  • +
  • 动画播放的框架不同:J2ME使用逐帧绘制的方式绘制动画,每个动画需要使用多个状态成员来记录动画播放的阶段,代码量较大且难以理解,而libGDX使用Action来定义和绘制动画,内置了很多基本动画,编码较为简单可读。
  • +
  • 响应事件的逻辑不通:J2ME使用了大量嵌套分支语句对于用户的按键编写对于用户按键的响应逻辑,而libGDX可以使用类似回调函数的方式定义事件的响应逻辑。
  • +
  • 播放音效的框架不同。
  • +
+

移植代码仓库

现阶段已经完成了故事模式6关的核心部分,已经可以和20年前的AI完跳棋了,与原版游戏对比如图:

+

alt text

+

其中左侧为原版,右侧为移植版(可以看到较为简陋)

+

视频演示在Bilibili:

+

https://www.bilibili.com/video/BV1KDWZetEg4/?share_source=copy_web&vd_source=70024a5eb82a8b6b82eac10977ce41b8

+

我将代码开源在了Github和Gitee,持续更新中:

+

https://github.com/creeper12356/ChineseCheckerPorted

+

https://gitee.com/creeper12356/ChineseCheckerPorted

+

欢迎共同交流学习!

+ + +
+ +
+ + + + + +
+ + +
+ + + +
+
+ +
+ +
+
+ +
+ + + + + + + + + + + + + + + + + + + + +
+ + \ No newline at end of file diff --git "a/2024/10/27/\350\247\243\345\206\263Windows\344\270\213zip\345\216\213\347\274\251\345\214\205\345\234\250Linux\344\270\213\350\247\243\345\216\213\344\271\261\347\240\201\351\227\256\351\242\230/index.html" "b/2024/10/27/\350\247\243\345\206\263Windows\344\270\213zip\345\216\213\347\274\251\345\214\205\345\234\250Linux\344\270\213\350\247\243\345\216\213\344\271\261\347\240\201\351\227\256\351\242\230/index.html" new file mode 100644 index 0000000..917b532 --- /dev/null +++ "b/2024/10/27/\350\247\243\345\206\263Windows\344\270\213zip\345\216\213\347\274\251\345\214\205\345\234\250Linux\344\270\213\350\247\243\345\216\213\344\271\261\347\240\201\351\227\256\351\242\230/index.html" @@ -0,0 +1,226 @@ + + + + + + + 解决Windows下zip压缩包在Linux下解压乱码问题 | Creeper Blog + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+ + +
+
+ +
+ + +
+ + +

+ 解决Windows下zip压缩包在Linux下解压乱码问题 +

+ + +
+ +
+ +

由于编码格式的不同,在Windows下压缩的zip文件在Linux下解压后,其中中文的文件名会出现乱码。解决方式:

+

安装7z和convmv

+
1
sudo apt install 7zip convmv
+ +

执行压缩后将编码从GBK转为UTF-8

+
1
2
LANG=C 7za x your-zip-file.zip
convmv -f GBK -t utf8 --notest -r .
+ +

参考:
https://blog.csdn.net/suiyueruge1314/article/details/90766185

+ + +
+ +
+ + + + + +
+ + +
+ + + +
+
+ +
+ +
+
+ +
+ + + + + + + + + + + + + + + + + + + + +
+ + \ No newline at end of file diff --git a/archives/2024/07/index.html b/archives/2024/07/index.html index d8a52f0..d96662c 100644 --- a/archives/2024/07/index.html +++ b/archives/2024/07/index.html @@ -83,13 +83,32 @@

-

- Hello World + 使用Spring Cloud Feign构建声明式客户端 +

+ + +
+
+
+ + + +
+
+
+ + + +

+ 测试发布文章

@@ -118,7 +137,7 @@

@@ -131,7 +150,23 @@

letzter Beitrag

diff --git a/archives/2024/08/index.html b/archives/2024/08/index.html new file mode 100644 index 0000000..37a8f12 --- /dev/null +++ b/archives/2024/08/index.html @@ -0,0 +1,220 @@ + + + + + + + Archiv: 2024/8 | Creeper Blog + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+ + +
+
+ + + + + + +
+
+ 2024 +
+
+ + + + + + + + +
+ + + +
+ + + +
+
+ +
+ +
+
+ +
+ + + + + + + + + + + + + + + + + + + + +
+ + \ No newline at end of file diff --git a/archives/2024/10/index.html b/archives/2024/10/index.html new file mode 100644 index 0000000..88ad09e --- /dev/null +++ b/archives/2024/10/index.html @@ -0,0 +1,201 @@ + + + + + + + Archiv: 2024/10 | Creeper Blog + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+ + +
+
+ + + + + + +
+
+ 2024 +
+
+ + + +
+ + + +
+
+ +
+ +
+
+ +
+ + + + + + + + + + + + + + + + + + + + +
+ + \ No newline at end of file diff --git a/archives/2024/index.html b/archives/2024/index.html index f374f00..4120e61 100644 --- a/archives/2024/index.html +++ b/archives/2024/index.html @@ -83,13 +83,89 @@

+ + + + + + + + + + + + + + + +
+
+
+ + + +

+ 测试发布文章

@@ -118,7 +194,7 @@

@@ -131,7 +207,23 @@

letzter Beitrag

diff --git a/archives/index.html b/archives/index.html index 28e2a22..69829e6 100644 --- a/archives/index.html +++ b/archives/index.html @@ -83,13 +83,89 @@

+ + + + + + + + + + + + + + + +
+
+
+ + + +

+ 测试发布文章

@@ -118,7 +194,7 @@

@@ -131,7 +207,23 @@

letzter Beitrag

diff --git a/index.html b/index.html index 61eb931..922d273 100644 --- a/index.html +++ b/index.html @@ -69,10 +69,10 @@

-
+
@@ -83,7 +83,7 @@

- Hello World + 解决Windows下zip压缩包在Linux下解压乱码问题

@@ -91,24 +91,243 @@

-

Welcome to Hexo! This is your very first post. Check documentation for more info. If you get any problems when using Hexo, you can find the answer in troubleshooting or you can ask me on GitHub.

-

Quick Start

Create a new post

1
$ hexo new "My New Post"
+

由于编码格式的不同,在Windows下压缩的zip文件在Linux下解压后,其中中文的文件名会出现乱码。解决方式:

+

安装7z和convmv

+
1
sudo apt install 7zip convmv
-

More info: Writing

-

Run server

1
$ hexo server
+

执行压缩后将编码从GBK转为UTF-8

+
1
2
LANG=C 7za x your-zip-file.zip
convmv -f GBK -t utf8 --notest -r .
-

More info: Server

-

Generate static files

1
$ hexo generate
+

参考:
https://blog.csdn.net/suiyueruge1314/article/details/90766185

-

More info: Generating

-

Deploy to remote sites

1
$ hexo deploy
+ +
+ +

+ +

+ + + + +
+ +
+ + +
+ + +

+ 使用libGDX移植20年前的J2ME游戏《中国跳棋》 +

+ + +
+ +
+ + +

背景介绍

暑假闲来无事,我在家中无意翻找到了自己使用的第一部触屏手机,是一个Samsung Anycall手机,由于电池老化的原因,我无法再次打开这个手机了。十年前我还是个小学生,智能手机并不普及,对于这第一个触屏手机也是非常有新鲜感,什么都想玩一下。其中最为印象深刻的就是一个跳棋游戏,主人公需要和6个npc对战中国跳棋,最终通关游戏。由于是将近二十年前开发的老游戏,在互联网上完全无法查到游戏开发商的任何信息,这个游戏也不可能在现代的机器上运行了,想到这个,我有些心血来潮,想让这个童年吸引我的小游戏重新焕发一次生命力。

+

查找游戏资源

我只记得这个游戏叫做《中国跳棋》,并且是十年前的Samsung手机内置的,于是在bilibili和抖音都搜索了一下,果然搜出了几个视频,都是展示在十几年前的手机上玩这个游戏的。从视频中我回忆起了一些关于这个游戏的具体信息,在互联网上进行了查找。不久我就在一个论坛(DOSPY论坛)上找到了这个游戏的相关资源:【稀有资源】中 国 跳 棋,游戏的形式是一个jar包。

+

使用模拟器运行J2ME游戏

十几年前按键手机和部分触屏手机上的很多游戏都是使用J2ME开发的,维基百科对于J2ME的介绍为:

+
1
Java ME以往称作J2ME(Java Platform, Micro Edition)是为机顶盒、移动电话和PDA之类嵌入式消费电子设备提供的Java语言平台,包括虚拟机和一系列标准化的Java API。它和Java SE、Java EE一起构成Java技术的三大版本,并且同样是通过JCP(Java Community Process)制订的。
+

J2ME是一个过时的框架,使用其开发的游戏自然无法在现代的操作系统上运行。因此,需要使用模拟器运行游戏的jar包。手机端可以选择J2ME Loader,电脑端可以选择microemulator
我使用microemulator运行原版游戏,部分界面如下:

+
+ alt text + alt text +
+ +

解包游戏并反编译

从DOSPY论坛上下载的jar包其实就是一个zip压缩文件,在Linux中,可通过unzip命令解压缩到指定目录extracted:

+
1
unzip your-file.jar -d extracted/
+

进入extracted目录,可看到包含Java的.class文件和资源文件(图片、音频)
alt text
与C/C++程序不同的是,Java编译出的.class字节码非常容易被反编译为Java代码,所以通常开发者会对代码进行混淆,使得其可读性下降,难以被破解。
我使用CFR工具反编译字节码,当然也有很多其他的工具。
CFR工具本身也是一个jar文件,反编译单个字节码文件:

+
1
java -jar cfr.jar target.jar > target.java
+

在Linux中,使用find命令,将extracted目录下所有的.class文件都反编译为.java文件:

+
1
find extracted/ -name "*.class" -exec java -jar cfr.jar {} --outputdir extracted \;
+ +

阅读源代码

幸运的是,本游戏的代码并没有过度混淆,不幸的是,即使反编译为Java源代码,我也不可能在我的机器上运行这些代码,只能阅读代码,同时借助Copilot加速理解代码。
虽然代码的结构清晰,但是由于代码量较大,我花了整整两天时间理解整个游戏的代码逻辑,由于篇幅限制,只能说几个较为重要的发现。

+

整型常量被大量使用

代码中整型常量被大量使用,例如:

+
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public class BoardView
implements XTimerListener,
Viewable {
public static final int GAMEREADY = 0;
public static final int SELECTDIA = 1;
public static final int MOVEDIA = 2;
public static final int MOVINGDIA = 3;
public static final int COMTHINK = 4;
public static final int COMBO_MSG = 5;
public static final int TIMEOUT_MSG = 6;
public static final int CHANGETURN = 7;
public static final int GAMEOVER = 8;
public static final int VIEWRESULT = 9;
public static final int NEXTROUND = 10;
public static final int NEXTSTAGE = 11;
public static final int READYTALK = 12;
public static final int RETURNVSMENU = 13;
public static final int GAMEFAILED = 14;
public static final int COMMOVEDIA = 15;
public static final int HOMEIN = 16;

//...
}
+

Java的枚举类型是在Java5引入的特性,在当时的Java2中还不存在。开发者具有良好的编程习惯,将这些枚举值都使用常量代替,避免了Magic Number。然而,Java编译器直接将这些常量编译成了整数,反编译后,并未保留常量的变量名,使得代码难以理解,例如:

+
1
2
3
4
5
6
7
if (this.state == 12) {
// do something
} else if (this.state == 0) {
// do something
} else if (this.state == 1) {
// do something
}
+

如果没有联系上述常量的定义,确实难以理解这些分支的语义,因此,我在阅读这些代码时,还原了这些常量,使得代码更易于理解。

+

适配不同的屏幕大小

即使是二十年前的代码,也对不同型号的手机屏幕进行了适配:

+
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public static void checkScreenSize() {
if (totalWidth > 118 && totalWidth < 122) {
lcdSize = 3;
} else if (totalWidth > 174 && totalWidth < 178) {
lcdSize = 2;
} else if (totalWidth > 238 && totalWidth < 242) {
lcdSize = totalHeight > 180 ? 1 : 2;
} else if (totalWidth > 319 && totalWidth < 321) {
lcdSize = 4;
}
}

public static boolean isQVGA() {
return lcdSize == 1 || lcdSize == 4;
}
+ +

我专门查了一下QVGA屏幕的含义:

+
1
QVGA images or videos are 320 pixels wide and 240 pixels tall (320 x 240 pixels). The name Quarter VGA is written as QVGA and the resolution is four times smaller than VGA resolution (640 x 480 pixels).
+ +

存在性能优化的代码写法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
int computeMoveGuide(int n) {
byte by = this.dia[n].posx;
byte by2 = this.dia[n].posy;
int n2 = 0;
if (this.moveCnt > 0 && this.jumpMove == 0) {
this.possibleDirCnt = 0;
for (int i = 0; i < 6; ++i) {
this.moveGuide[i] = 0;
}
return 0;
}
for (int i = 0; i < 6; ++i) {
this.moveGuide[i] = 0;
this.moveGuide[i] = this.diaBoard.checkBoard(this.index, by + Resource.hInc[i], by2 + Resource.vInc[i]);
if (this.moveGuide[i] == 2) {
this.moveGuide[i] = this.diaBoard.checkBoard(this.index, by + Resource.hInc[i] * 2, by2 + Resource.vInc[i] * 2);
this.moveGuide[i] = this.moveGuide[i] == 1 ? 2 : 0;
}
if (this.jumpMove > 0 && this.moveGuide[i] == 1) {
this.moveGuide[i] = 0;
}
if (this.moveGuide[i] == 0) continue;
++n2;
}
this.possibleDirCnt = n2;
return n2;
}
+

computeMoveGuide的for循环中,使用n2这个局部变量进行累加计算,最终将n2的值赋值给this.possibleDirCnt,而并没有将this.possibleDirCnt直接进行累加。this.possibleDirCnt是一个类的成员变量,存储在堆内存内,而n2是一个局部变量,可以被放在寄存器中,操作n2只需要读写寄存器,比读写堆内存快得多,这对于十几年前的手机来说应该是有性能的提升。

+

使用libGDX重写游戏代码

经过一个上午的了解,我选择了libGDX框架,对J2ME游戏进行移植,主要原因有几点:

+
    +
  • 编程语言相同:libGDX和J2ME都是使用Java开发,游戏核心算法部分不需要任何改动,只需要修改各种调用的库即可。
  • +
  • 跨平台:不仅Java语言本身跨平台,libGDX框架也是跨平台的,支持桌面端(Windows, MacOS, Linux)和移动端(Android, iOS)
  • +
  • 运行高效:libGDX本身对性能做了一定的优化。
  • +
+

具体的游戏移植过程技术难度并不高,只是比较繁琐,需要同时关注移植前后接口的变化,主要包括:

+
    +
  • 绘制场景的框架不同:J2ME使用javax.microedition.lcdui.Graphics绘制图像,而libGDX使用SpriteBatchStage绘制图像和UI,同时,二者的坐标系统完全不同,需要进行坐标变换。
  • +
  • 动画播放的框架不同:J2ME使用逐帧绘制的方式绘制动画,每个动画需要使用多个状态成员来记录动画播放的阶段,代码量较大且难以理解,而libGDX使用Action来定义和绘制动画,内置了很多基本动画,编码较为简单可读。
  • +
  • 响应事件的逻辑不通:J2ME使用了大量嵌套分支语句对于用户的按键编写对于用户按键的响应逻辑,而libGDX可以使用类似回调函数的方式定义事件的响应逻辑。
  • +
  • 播放音效的框架不同。
  • +
+

移植代码仓库

现阶段已经完成了故事模式6关的核心部分,已经可以和20年前的AI完跳棋了,与原版游戏对比如图:

+

alt text

+

其中左侧为原版,右侧为移植版(可以看到较为简陋)

+

视频演示在Bilibili:

+

https://www.bilibili.com/video/BV1KDWZetEg4/?share_source=copy_web&vd_source=70024a5eb82a8b6b82eac10977ce41b8

+

我将代码开源在了Github和Gitee,持续更新中:

+

https://github.com/creeper12356/ChineseCheckerPorted

+

https://gitee.com/creeper12356/ChineseCheckerPorted

+

欢迎共同交流学习!

+ + +
+ +
+ +
+ + + + +
+ +
+ + +
+ + +

+ Linux切换java版本 +

+ + +
+ +
+ +

Linux系统中同时安装了多个版本的Java,可以使用update-alternatives命令来切换默认的Java版本。以下是具体的步骤:

+

打开终端,输入:

+
1
sudo update-alternatives --config java
+

这将列出系统中所有可用的Java版本。
在提示选择版本的列表中,输入想要设为默认的Java版本对应的数字,然后按回车,即可切换默认的java版本。

+ + +
+ +
+ +
+ + + + +
+ +
+ + +
+ + +

+ 使用Spring Cloud Feign构建声明式客户端 +

+ -

More info: Deployment

+
+ +
+ +

在使用Spring Boot开发微服务后端时,有时需要向其他微服务发起HTTP请求。如果使用传统的RestTemplate方法,先构造HTTP请求,再解析返回的HTTP响应,会产生很多冗余代码,降低项目的可读性。Spring Cloud Feign可以很好地解决这个问题。Feign是一种声明式、模板化的HTTP客户端,开发者只需要定义接口,便可以向调用本地方法一样调用远程接口,非常简洁方便。
此处Spring Boot项目使用maven构建,在pom.xml中加入依赖并安装:

+
1
2
3
4
5
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
<version>4.1.1</version>
</dependency>
+

在Spring Boot的主文件中加入@EnableFeignClients注解:

+
1
2
3
4
5
6
7
8
9
@SpringBootApplication
@EnableFeignClients
public class UniGptBotServiceApplication {

public static void main(String[] args) {
SpringApplication.run(UniGptBotServiceApplication.class, args);
}

}
+

创建一个client目录,用于放置所有Feign客户端类,例如,我需要向微服务user-service发送远程请求,则创建一个UserServiceClient类:

+
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
package com.unigpt.bot.client;

import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.stereotype.Component;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestHeader;

@Component
@FeignClient(name = "user-service", url = "http://localhost:8082/internal")
public interface UserServiceClient {

@DeleteMapping("/users/used-bots/{botId}")
public void deleteBotFromUsedList(
@PathVariable Integer botId,
@RequestHeader(name = "X-User-Id") Integer userId);
}
+

此处,我们定义了UserServiceClient接口,@FeignClient注解定义了服务的名称url,接口的函数定义与controller类似,可以使用注解规定路径参数、请求参数、请求头、请求体信息。Feign会根据定义的接口和注解自动生成实现。

+

定义完客户端接口类后,依赖注入UserServiceClient类,即可使用deleteBotFromUsedList方法,像调用本地方法一样调用远程服务。

+
+ +
+ + + + +
+ +
+ + +
+ + +

+ 测试发布文章 +

+ + +
+ +
+ +

Creeper Blog 测试发布文章

测试内容

+ + +
+
@@ -151,7 +370,23 @@

letzter Beitrag