木须柄的时光工坊

PSP 可以使用外接的 GPS 模块,实现定位和导航功能。比如官方的地图软件,包括 Go!Explore (欧洲),Maplus (日本),还有自制地图软件,包括 MapThis,PSP-Maps。
不过截止到目前为止(2026年1月)所有软件都已经停止更新超过15年以上了,如果希望能获得国内较新的导航数据,并适配 PSP 的 GPS 模块是非常困难的。
经过一段时间的测试,我发现目前只有 PSP-Maps 可以相对方便的获取新的地图信息。因此打算深入研究一下,让 PSP 实现地图定位功能。

| 功能 | PSP 按键 | 键盘 |
|---|---|---|
| 选择 / 移动地图 | 十字键 | 上/下/左/右 |
| 确定 | X 或⚪ | Space |
| 放大 | R | PgUp |
| 缩小 | L | PgDn |
| 菜单 | Start | Esc |

| 菜单 | 说明 |
|---|---|
| 当前地图 | 可以左右键切换地图类型 |
| 输入地址 | 地址查询(已废弃) |
| 路线规划 | 输入起始地和目的地,生成路线 |
| 显示详细信息 | 打开后会显示经纬度、缩放和地图类型 |
| 显示 KML | 显示导航信息(已废弃) |
| 缓存缩放层数 | 批量获取地图缓存,建议一次不要超过5层 |
| 缓存大小 | 一般选择 512MB ~ 1GB 足够 |
| 启用天文 / 星球模式 | 谷歌天文模式地图(已废弃) |
PSP-Maps 使用的是 curl 标准库实现的网络下载功能,这在 PSP 上是无法兼容的,因此软件只能在电脑端下载地图瓦片图的离线缓存,然后把缓存和数据文件拷贝到 PSP 端使用。
缓存地图有两种方式,一种是直接在地图上移动缩放,就会自动下载对应缩放的瓦片图,这样手动下载虽然直观,但是比较低效。
另一种缓存方式是使用菜单功能,这样可以批量下载 1-9 层地图瓦片(当前显示的层是 1,每一层有 4^n 张图片),建议一次不要下载超过 5 层图片。

由于外网提供的 PSP-Maps 软件包年代太过久远(2009年),其中很多地图源已经失效,比如 Google, Yahoo 地图已经无法访问。同时较好的地图源,比如 OpenStreetMap 的接口需要更新,否则不能正常访问。因此我希望重新编译 PSP-Maps 源码,改进使用体验。
构建 PSP 自制软件需要首先一套交叉编译环境,我这里使用的是 Windows 自带的 wsl 下的 Ubuntu 24.04 环境,使用 PSPDEV 工具链构建编译环境(具体方法可以参考这里)。然而这套工具可以“方便”的构建 PSP 交叉编译环境,但是由于 PSP-Maps 的源码很久没有更新了(截止到2013年),所以其代码依赖的工具链版本已经不适应现代编译环境,很多基础工具的调用方式都发生了很大变化。即使我尝试 Debug 其代码适应了现代环境,并使其能正常通过 Ubuntu 和 Win 11 下的编译并正常运行, 但是生成的程序却始终没办法在 PSP 实机上正常运行。


经过 killme 的提醒,我觉得应该转变思路,重新构建一个适配源码的旧版编译环境。一开始我选择 Ubuntu 12.04 + Minimalist-PSPSDK (minpspw),结果这个环境又太过老旧,一是 Ubuntu 12.04 的 glibc 3.15 低于 psp-gcc 的最低编译要求,二是 minpspw 的编译工具链也并不完整,无法独立完成 PSP 端的编译和打包 eboot.pbp。
经过多次尝试,最终我选择在虚拟机环境下运行 Ubuntu 14.04 作为基础,使用 v20200623 历史版本的 PSPDEV 作为交叉编译环境。终于成功让生成的程序正常运行在 PSP 实机上。后来经过工具链和环境的交叉对比,我发现 Ubuntu 环境并不需要有什么限制,用 Ubuntu 24.04 也是可以的(少量代码需要更新一下),问题主要出在 PSVDEV 的版本上,如果使用最新版本(v20260101)生成的 eboot.pbp 就会导致我的 PSP 实机 (2000型号) 死机。
编译环境:Ubuntu 14.04(新版系统环境存在兼容性问题)
安装基础依赖:
1sudo apt-get update
2sudo apt-get install build-essential cmake pkgconf libreadline8 libusb-0.1 libgpgme11 libarchive-tools fakeroot
安装 PSPDEV 编译环境:
1cd /opt
2
3# 下载 pspdev, 直接下编译好的包, 不需要从源码编译
4wget https://github.com/pspdev/pspdev/releases/download/v20200623/pspdev-ubuntu-latest.tar.gz
5tar zxvf pspdev-ubuntu-latest.tar.gz
6cd pspdev
7
8# 添加环境变量, 增加以下两行内容
9vim ~/.bashrc
10
11export PSPSDK="/opt/pspsdk"
12export PATH="$PATH:$PSPSDK/bin"
13
14source ~/.bashrc
15
16# 测试验证
17psp-config --pspdev-path
18/opt/pspdev
19
20psp-gcc --version
21psp-gcc (GCC) 4.3.5
22Copyright (C) 2008 Free Software Foundation, Inc.
23This is free software; see the source for copying conditions. There is NO
24warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
在直接去编译复杂项目之前,可以先试试编译一个 Hello World 并放入 PSP 验证
创建这三个文件:
hellopsp.c
1#include <pspkernel.h>
2#include <pspdebug.h>
3
4PSP_MODULE_INFO("HELLO", 0, 1, 0);
5PSP_MAIN_THREAD_ATTR(THREAD_ATTR_USER | THREAD_ATTR_VFPU);
6
7int main() {
8 pspDebugScreenInit();
9 pspDebugScreenPrintf("Hello PSP!\n");
10 sceKernelSleepThread();
11 return 0;
12}
Makefile
1TARGET = hellopsp
2OBJS = hellopsp.o
3
4PSPSDK = $(shell psp-config --pspsdk-path)
5include $(PSPSDK)/lib/build.mak
build.sh
1#!/bin/bash
2set -euo pipefail
3
4# ===== 可配置项 =====
5APP_NAME="Hello PSP" # XMB 显示名
6ELF="hellopsp.elf" # make 产物
7OUT_DIR="EBOOT"
8PBP_NAME="EBOOT.PBP"
9
10# ===== 构建 =====
11echo "[*] make..."
12make
13
14# ===== 校验产物 =====
15if [[ ! -f "$ELF" ]]; then
16 echo "[!] ERROR: $ELF not found. Check your Makefile TARGET output."
17 exit 1
18fi
19
20# ===== 生成 PARAM.SFO =====
21echo "[*] mksfoex..."
22mksfoex "$APP_NAME" PARAM.SFO
23
24# ===== 打包 PBP =====
25echo "[*] pack-pbp..."
26mkdir -p "$OUT_DIR"
27pack-pbp \
28 "$OUT_DIR/$PBP_NAME" \
29 PARAM.SFO \
30 NULL \
31 NULL \
32 NULL \
33 NULL \
34 NULL \
35 "$ELF" \
36 NULL
37
38echo "[✓] Done: $OUT_DIR/$PBP_NAME"
然后编译程序,获得 eboot.pbp:
1# 编译
2chmod +x build.h
3./build.h
4
5├── build.sh
6├── EBOOT
7│ └── EBOOT.PBP
8├── hellopsp.c
9├── hellopsp.elf
10├── hellopsp.o
11├── Makefile
12└── PARAM.SFO
将 eboot.pbp 拷贝到 PSP 的 ms0:/PSP/GAME/Hello 中,打开程序后显示
1Hello PSP!

源码地址:https://github.com/GameMaker2k/PSP-Maps
安装 PSP-Maps 依赖
1sudo apt update
2sudo apt-get install libsdl1.2-dev libcurl4-openssl-dev libxml2-dev libsdl-image1.2-dev libsdl-gfx1.2-dev libsdl-ttf2.0-dev libsdl-mixer1.2-dev
3
4# 64 位系统需要额外安装
5sudo apt-get install libc6-dev-i386
编译 PSP-Maps (基本步骤)
1# 拉取源码
2sudo git clone https://github.com/GameMaker2k/PSP-Maps.git
3cd PSP-Maps
4
5# 电脑端编译
6make
7
8# PSP 端编译
9make -f Makefile.psp
源码包中包含 Makefile, Makefile.psp,CMakelist.txt,可以使用 make, cmake 构建,其中 Makefile.psp 是用于构建 PSP 版包体的。
至于一些零零碎碎的模块、motion_driver 之类的小组件就不一一赘述了,缺啥补啥。
特别注意:对于 PSP 端的编译依赖,需要明确使用 /pspdev 目录下的 psp-gcc 和工具链文件,不要使用系统端的标准库工具,否则会出现兼容性问题。其中某些模块,比如 curl, xml2 等在 PSP 端是无法编译的,需要写分支屏蔽掉。以下是代码的改动:
pspmaps.c
1@@ -35,7 +35,10 @@
2 #include <SDL_gfxPrimitives.h>
3 #include <SDL_ttf.h>
4 #include <SDL_mixer.h>
5-#include <curl/curl.h>
6+
7+#ifndef _PSP
8+ #include <curl/curl.h>
9+#endif
10
11 #define DEFAULT_MAP 0
12 #define DEFAULT_CHEAT_MAP 18
13@@ -82,7 +85,11 @@ SDL_Surface *screen, *prev, *next;
14 SDL_Surface *logo, *na, *zoom;
15 SDL_Joystick *joystick;
16 TTF_Font *font;
17+
18+#ifndef _PSP
19 CURL *curl;
20+#endif
tile.c
1@@ -115,6 +115,7 @@ void savedisk(int x, int y, int z, int s, SDL_RWops *rw, int
2 disk_idx = (disk_idx + 1) % config.cache_size;
3 }
4
5+#ifndef _PSP
6 /* curl callback to save in memory */
7 size_t curl_write(void *ptr, size_t size, size_t nb, void *stream)
8 {
9@@ -123,10 +124,16 @@ size_t curl_write(void *ptr, size_t size, size_t nb, void
10 rw->write(rw, ptr, size, nb);
11 return t;
12 }
13+#endif
14
15 /* get the image on internet and return a buffer */
16 SDL_RWops *getnet(int x, int y, int z, int s)
17 {
18+#ifdef _PSP
19+ /* PSP: no online download, offline mode */
20+ return NULL;
kml.c
1@@ -7,12 +7,15 @@
2 #include <SDL_image.h>
3 #include <SDL_gfxPrimitives.h>
4
5+#ifndef _PSP
6 #include <libxml/parser.h>
7 #include <libxml/tree.h>
8+#endif
9
10 SDL_Surface *marker;
11 Placemark *places = NULL;
12
13+#ifndef _PSP
14 void placemark_parse(xmlNode *node, Placemark *place)
15 {
16 xmlNode *cur, *cur2;
17@@ -95,9 +98,16 @@ void kml_parse(char *file)
18
19 xmlFreeDoc(doc);
20 }
对于构建文件某些细节根据所在环境的工具链的安装位置,可能存在细微差异,这里贴出我的修改版本仅供参考。
Makefile
1CC ?= gcc
2
3CFLAGS += -O2 -g -Wall `sdl-config --cflags` `curl-config --cflags` `xml2-config --cflags`
4
5LIBS += -lSDL_image -lSDL_gfx -lSDL_ttf -lSDL_mixer `sdl-config --libs` `curl-config --libs` `xml2-config --libs` -lm $(LDFLAGS)
6
7PREFIX ?= /usr/local
8DESTDIR ?=
9
10.PHONY: all install uninstall clean
11
12all: pspmaps
13
14pspmaps: pspmaps.c $(ICON) global.o kml.o tile.c io.c
15 $(CC) $(CFLAGS) -o pspmaps$(EXEEXT) pspmaps.c $(ICON) global.o kml.o $(LIBS)
16
17global.o: global.c global.h
18 $(CC) $(CFLAGS) -c global.c
19
20kml.o: kml.c kml.h
21 $(CC) $(CFLAGS) -c kml.c
22
23tile.o: tile.c tile.h
24 $(CC) $(CFLAGS) -c tile.c
25
26io.o: io.c io.h
27 $(CC) $(CFLAGS) -c io.c
28
29icon.o: icon.rc
30 $(WINDRES) -i icon.rc -o icon.o
31
32install: pspmaps
33 install -v -m 0755 -d $(DESTDIR)$(PREFIX)/bin
34 install -v -m 0755 ./pspmaps$(EXEEXT) $(DESTDIR)$(PREFIX)/bin
35
36uninstall: pspmaps
37 rm -rfv $(DESTDIR)$(PREFIX)/bin/pspmaps$(EXEEXT)
38
39clean:
40 rm -rfv pspmaps pspmaps.exe *.o PSP-Maps.prx PSP-Maps.elf PARAM.SFO EBOOT.PBP pspmaps.gpu cache/ data/*.dat kml/
Makefile.psp
1TARGET = PSP-Maps
2OBJS = pspmaps.o global.o kml.o sceUsbGps.o
3
4PSP_FW_VERSION = 371
5BUILD_PRX = 1
6
7INCDIR =
8CFLAGS = -O2 -G0 -Wall -g
9CXXFLAGS = $(CFLAGS) -fno-exceptions -fno-rtti
10ASFLAGS = $(CFLAGS)
11
12LIBDIR =
13
14EXTRA_TARGETS = EBOOT.PBP
15PSP_EBOOT_TITLE = PSP-Maps
16PSP_EBOOT_ICON = icon.png
17PSP_EBOOT_PIC1 = screenshot.png
18
19PSPSDK=$(shell psp-config --pspsdk-path)
20PSPBIN = $(PSPSDK)/../bin
21
22# PSP build: 禁止使用宿主机 /usr/include 与宿主机的 *-config 输出
23# 仅保留项目自身 include
24CFLAGS += -I. -I./motion/
25CFLAGS += -I/opt/pspdev/psp/include/
26CFLAGS += -I/opt/pspdev/psp/include/SDL/
27# CFLAGS += -I/opt/pspsdk/psp/include/ $(shell $(PSPBIN)/curl-config --cflags) $(shell $(PSPBIN)/xml2-config --cflags)
28
29# PSP libs:只链接 PSP 侧可用的库
30# 下面 SDL_* 前提是你安装的是 PSP 版 SDL/SDL_image/SDL_ttf/SDL_gfx/SDL_mixer(通常在 $PSPDEV/psp/lib)
31LIBDIR += ./motion
32LIBS = -lmotion_driver -lSDL_image -lSDL_gfx -lSDL_ttf -lSDL_mixer -lpng -ljpeg -lSDL -lfreetype -lmikmod -lvorbisfile -lvorbis -logg -lz -lpspwlan -lpsputility -lpspgum -lpspgu -lpspusb -lm
33
34LIBS += $(shell $(PSPBIN)/sdl-config --libs)
35# LIBS += $(shell $(PSPBIN)/curl-config --libs)
36# LIBS += $(shell $(PSPBIN)/xml2-config --libs)
37
38include $(PSPSDK)/lib/build.mak
39
如果编译没问题,可以正常生成 eboot.pbp 文件。
拷贝 eboot.pbp 文件到 PSP 中,目录位置是 ms0:/PSP/GAME/PSP-Maps,如果一切没问题,就能看到正常运行了

Windows 版本的编译使用 MSYS2 处理
1# 安装依赖
2pacman -S --needed \
3 mingw-w64-x86_64-gcc \
4 mingw-w64-x86_64-cmake \
5 mingw-w64-x86_64-pkg-config \
6 mingw-w64-x86_64-SDL \
7 mingw-w64-x86_64-SDL_image \
8 mingw-w64-x86_64-SDL_gfx \
9 mingw-w64-x86_64-SDL_ttf \
10 mingw-w64-x86_64-SDL_mixer \
11 mingw-w64-x86_64-libxml2 \
12 mingw-w64-x86_64-curl
13
14# 编译源码
15mkdir build
16cd build
17
18cmake .. -G "MinGW Makefiles" \
19 -DSDLGFX_INCLUDE_DIR=/mingw64/include \
20 -DSDLGFX_LIBRARY=/mingw64/lib/libSDL_gfx.dll.a \
21 -DCURL_INCLUDE_DIR=/mingw64/include \
22 -DCURL_LIBRARY=/mingw64/lib/libcurl.dll.a \
23 -DCURL_LIBRARIES=/mingw64/lib/libcurl.dll.a
24
25mingw32-make
26
27# 拷贝 dll 依赖
28ldd pspmaps.exe | grep mingw64 | awk '{print $3}' | xargs -I{} cp -u {} .
由于 PSP 运行环境内存不大,可以把字体裁剪,只保留软件用到的汉字
menu_chars.txt
1 !"#$%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\]^_`abcdefghijklmnopqrstuvwxyz{|}~°±×→←
2汉化版本功能改进修复设置默认地图删除失效必应道路航拍混合标准云自行车交通影像谷歌月球阿波罗任务克莱门汀黑白形高程火卫星可见光红外天空微历史当前输入地址路线规划加载保存收藏恢复视图显示详细信息跟随过渡效果键盘类型缓存缩放层数启用天文星模式大小返回退出纬度经度速度方向定位信号状态移动静止偏移精度更新连接断开卫星数量强度高度时间间隔来源数据模块设备正在加载中完成失败成功错误警告重试初始化检测等待处理中请目的清理上下切换写确认发
然后使用 pyftsubset 裁剪字体,可以把楷体从 13mb 缩减到 61kb,非常适合 PSP 加载
1# 安装 pyftsubset
2apt update
3apt install python3-fonttools
4
5# 裁剪字体
6pyftsubset stkaiti.ttf \
7 --text-file=menu_chars.txt \
8 --output-file=stkaiti_psp.ttf \
9 --drop-tables+=GSUB,GPOS,GDEF \
10 --no-layout-closure \
11 --no-hinting \
12 --recommended-glyphs \
13 --name-IDs='*' \
14 --name-languages='*'
PSP-Maps 的文本内容不错,主要就是菜单界面和零星的一些文本。基本上都集中在 pspmaps.c 文件中,对应修改一下即可。
另外需要修改 io.c 文件中关于文字的渲染模式,改为 TTF_RenderUTF8_Blended 模式
1/* prints a message using the bitmap font */
2void print(SDL_Surface *dst, int x, int y, char *text)
3{
4 SDL_Rect pos;
5 SDL_Surface *src;
6 SDL_Color color = {255, 255, 255};
7 if (font == NULL) return;
8 pos.x = x;
9 pos.y = y;
10 // src = TTF_RenderText_Blended(font, text, color);
11 src = TTF_RenderUTF8_Blended(font, text, color);
12 SDL_BlitSurface(src, NULL, dst, &pos);
13 SDL_FreeSurface(src);
14}
初始世界地图全图,缩放 0%,每次缩放 +/- 5%,每一层的显示由 4 张瓦片图组成,所以每层的瓦片总数为 4^n 张图片
地图瓦片缓存存储在 cache 目录下,第一层目录为 000-999 编号,每层目录下又包含 000.dat ~ 999.dat,每个 .dat 文件即一个 瓦片图,可以修改后缀 jpg 查看。
| 等级 | 缩放 | 图片数量 |
|---|---|---|
| 1 | 0% | 4 |
| 2 | 5% | 16 |
| 3 | 10% | 64 |
| 4 | 15% | 256 |
| 5 | 20% | 1024 |
| 6 | 25% | 4096 |
| 7 | 30% | 16384 |
| 8 | 35% | 65536 |
| 9 | 40% | 262144 |
| 10 | 45% | 1048576 |
| 11 | 50% | 4194304 |
| 12 | 55% | 16777216 |
| 13 | 60% | 67108864 |
| 14 | 65% | 268435456 |
| 15 | 70% | 1073741824 |
| 16 | 75% | 4294967296 |
| 17 | 80% | 17179869184 |
| 18 | 85% | 68719476736 |
| 19 | 90% | 274877906944 |
| 20 | 95% | 1099511627776 |
| 总计 | 1466015503700 | |
程序默认的 cache 生成机制有 bug,当一次性生成最大 9 层缓存时(当前层为 0 层),后面会无法新建缓存文件,导致从 000/000.dat 开始覆盖,所以建议使用 generate_cache.bat 脚本首先生成 dat 文件缓存占位,然后再去程序中下载地图缓存。
经过测试,程序中将缓存大小的字节数,同时作为了缓存编号的上限。因此实际缓存并不会达到指定大小,就会回到开始,覆写 000/000.dat。比如,如果设置缓存为 4GB,那么一次性获取的缓存文件上限是 409600 条。
为了修正 BUG,这里修改了缓存的保存机制,改为 cache/<s>/<z>/<bx>/<by>/x_y.dat 的保存形式。其中 s 代表地图源,z 代表缩放层级,bx 代表地图 x 轴的分桶,by 代表 y 轴的分桶,缓存文件用地图坐标 <x, y> 表示。每桶 256 个文件上限,充分照顾了 PSP 存储卡的 fat32 的文件检索能力。
修改源码如下,更新了三个函数,涉及缓存的读取、保存:
tile.c
1/* return the disk file name for cache entry
2 * maximum of 1000 entries per folder to improve access speed */
3/* void diskname(char *buf, int n)
4{
5 sprintf(buf, "cache/%.3d", n/1000);
6 mkdir(buf, 0755);
7 sprintf(buf, "cache/%.3d/%.3d.dat", n/1000, n%1000);
8}
9*/
10
11static void mkdir_safe(const char *path)
12{
13 mkdir(path, 0755);
14}
15
16#define TILE_BUCKET_SHIFT 4 /* 2^4 = 16 */
17
18static void diskname(char *buf, int s, int z, int x, int y)
19{
20 char tmp[256];
21 int xb = x >> TILE_BUCKET_SHIFT;
22 int yb = y >> TILE_BUCKET_SHIFT;
23
24 mkdir_safe("cache");
25
26 snprintf(tmp, sizeof(tmp), "cache/%d", s);
27 mkdir_safe(tmp);
28
29 snprintf(tmp, sizeof(tmp), "cache/%d/%d", s, z);
30 mkdir_safe(tmp);
31
32 snprintf(tmp, sizeof(tmp), "cache/%d/%d/%d", s, z, xb);
33 mkdir_safe(tmp);
34
35 snprintf(tmp, sizeof(tmp), "cache/%d/%d/%d/%d", s, z, xb, yb);
36 mkdir_safe(tmp);
37
38 snprintf(buf, 256, "cache/%d/%d/%d/%d/%d_%d.dat", s, z, xb, yb, x, y);
39}
40
41/* save tile in disk cache */
42/*
43void savedisk(int x, int y, int z, int s, SDL_RWops *rw, int n)
44{
45 FILE *f;
46 char name[50];
47 char buffer[BUFFER_SIZE];
48
49 if (!config.cache_size) return;
50
51 DEBUG("savedisk(%d, %d, %d, %d)\n", x, y, z, s);
52
53 if (rw == NULL)
54 {
55 printf("warning: savedisk(NULL)!\n");
56 return;
57 }
58
59 disk[disk_idx].x = x;
60 disk[disk_idx].y = y;
61 disk[disk_idx].z = z;
62 disk[disk_idx].s = s;
63
64 SDL_RWseek(rw, 0, SEEK_SET);
65 diskname(name, disk_idx);
66 if ((f = fopen(name, "wb")) != NULL)
67 {
68 SDL_RWread(rw, buffer, 1, n);
69 fwrite(buffer, 1, n, f);
70 fclose(f);
71 }
72
73 disk_idx = (disk_idx + 1) % config.cache_size;
74}
75*/
76
77void savedisk(int x, int y, int z, int s, SDL_RWops *rw, int n)
78{
79 FILE *f;
80 char filename[256];
81 unsigned char buffer[BUFFER_SIZE];
82
83 if (!rw || n <= 0)
84 return;
85
86 /* 新 diskname:基于 s,z,x,y(含分桶) */
87 diskname(filename, s, z, x, y);
88
89 /* 已存在就不写(无限增长 + 去重) */
90 f = fopen(filename, "rb");
91 if (f) {
92 fclose(f);
93 return;
94 }
95
96 f = fopen(filename, "wb");
97 if (!f)
98 return;
99
100 SDL_RWseek(rw, 0, SEEK_SET);
101
102 while (n > 0) {
103 int chunk = n > BUFFER_SIZE ? BUFFER_SIZE : n;
104 if (SDL_RWread(rw, buffer, 1, chunk) != (size_t)chunk)
105 break;
106 fwrite(buffer, 1, chunk, f);
107 n -= chunk;
108 }
109
110 fclose(f);
111}
112
113
114/* return the tile from disk if available, or NULL */
115/*
116SDL_Surface *getdisk(int x, int y, int z, int s)
117{
118 int i;
119 char name[50];
120 DEBUG("getdisk(%d, %d, %d, %d)\n", x, y, z, s);
121 for (i = 0; i < config.cache_size; i++)
122 if (disk[i].x == x && disk[i].y == y && disk[i].z == z && disk[i].s == s)
123 {
124 diskname(name, i);
125 return IMG_Load(name);
126 }
127 return NULL;
128}
129*/
130
131SDL_Surface *getdisk(int x, int y, int z, int s)
132{
133 char filename[256];
134 SDL_RWops *rw;
135 SDL_Surface *img;
136
137 diskname(filename, s, z, x, y);
138
139 rw = SDL_RWFromFile(filename, "rb");
140 if (!rw)
141 return NULL;
142
143 img = IMG_Load_RW(rw, 1); /* 1 = 自动 close RWops */
144 if (!img) {
145 /* 文件损坏,直接删掉,避免反复失败 */
146 remove(filename);
147 return NULL;
148 }
149
150 return img;
151}
地图缓存经过这一轮的改进,解决了之前文件结构无序,缓存数量存在隐形上限的 BUG。但是还有一个致命缺陷,即每张地图瓦片的 dat 文件太小,生成缓存时,会有大量的 1~10kb 左右的小文件。而 PSP 存储卡的分区方式为 fat32,簇大小默认为 16kb。使用这种存储方式,会造成巨大的空间浪费。极端情况下,一份不到 1GB 的地图缓存,在 PSP 上会占用超过 10GB 的存储空间。因此需要彻底改进缓存的构造方式。
经过几轮的实验,我这里使用 cache/<s>/<z>/<bx>_<by>.cache 的缓存结构进行存储,这种结构生成的单个 .cache 文件在 500kb ~500mb 之间,大小适宜,非常适合 PSP 实机存取。
tile.c
1#define CACHE_MAGIC 0x50434143u /* 'C''A''C''P' little-endian 近似标识 */
2#define CACHE_VER 1u
3#define CACHE_SLOTS_POW2 18
4#define TILE_MAX_BYTES (512 * 1024)
5#define CACHE_BLOCK_SHIFT 6 /* 64x64 tiles per cache file */
6
7typedef struct CacheHeader {
8 uint32_t magic;
9 uint32_t version;
10 uint32_t slots_pow2; /* N = 1<<slots_pow2 */
11 uint32_t slot_count; /* = 1<<slots_pow2 */
12 uint32_t reserved0;
13 uint32_t reserved1;
14 uint32_t reserved2;
15 uint32_t reserved3;
16} CacheHeader;
17
18typedef struct CacheSlot {
19 int32_t x;
20 int32_t y;
21 uint32_t offset; /* record 在文件内的偏移;0=空 */
22 uint32_t size; /* 数据长度 */
23} CacheSlot;
24
25typedef struct CacheRecordHdr {
26 uint32_t tag; /* 'TILE' */
27 int32_t x;
28 int32_t y;
29 uint32_t size; /* data size */
30} CacheRecordHdr;
31
32static void mkdir_safe(const char *path)
33{
34 mkdir(path, 0755);
35}
36
37static uint32_t hash_xy(int32_t x, int32_t y)
38{
39 /* 一个简单的 32-bit mix */
40 uint32_t h = (uint32_t)x * 0x9E3779B1u;
41 h ^= (uint32_t)y + 0x7F4A7C15u + (h<<6) + (h>>2);
42 h ^= (h >> 16);
43 return h;
44}
45
46/* cache/<s>/<z>/<bx>_<by>.cache 路径 */
47static void cachepack_path(char *buf, int s, int z, int x, int y)
48{
49 char tmp[256];
50 int bx = x >> CACHE_BLOCK_SHIFT;
51 int by = y >> CACHE_BLOCK_SHIFT;
52
53 mkdir_safe("cache");
54
55 snprintf(tmp, sizeof(tmp), "cache/%d", s);
56 mkdir_safe(tmp);
57
58 snprintf(tmp, sizeof(tmp), "cache/%d/%d", s, z);
59 mkdir_safe(tmp);
60
61 snprintf(buf, 256, "cache/%d/%d/%d_%d.cache", s, z, bx, by);
62}
63
64static int cachepack_open_or_create(FILE **out, int s, int z, int x, int y, uint32_t slots_pow2)
65{
66 char path[256];
67 CacheHeader hdr;
68
69 cachepack_path(path, s, z, x, y);
70
71 FILE *f = fopen(path, "rb+");
72 if (!f) {
73 /* create */
74 f = fopen(path, "wb+");
75 if (!f) return 0;
76
77 memset(&hdr, 0, sizeof(hdr));
78 hdr.magic = CACHE_MAGIC;
79 hdr.version = CACHE_VER;
80 hdr.slots_pow2 = slots_pow2;
81 hdr.slot_count = (1u << slots_pow2);
82
83 if (fwrite(&hdr, 1, sizeof(hdr), f) != sizeof(hdr)) { fclose(f); return 0; }
84
85 /* 快速预分配索引区:header + slot_count*slot_size */
86 long end = (long)sizeof(CacheHeader) + (long)hdr.slot_count * (long)sizeof(CacheSlot);
87 if (fseek(f, end - 1, SEEK_SET) != 0) { fclose(f); return 0; }
88 fputc(0, f);
89 fflush(f);
90 } else {
91 /* validate */
92 if (fread(&hdr, 1, sizeof(hdr), f) != sizeof(hdr)) { fclose(f); return 0; }
93 if (hdr.magic != CACHE_MAGIC || hdr.version != CACHE_VER || hdr.slot_count == 0) {
94 fclose(f);
95 return 0;
96 }
97 }
98
99 *out = f;
100 return 1;
101}
102
103static int cachepack_open_readonly(FILE **out, int s, int z, int x, int y)
104{
105 char path[256];
106 cachepack_path(path, s, z, x, y);
107
108 FILE *f = fopen(path, "rb");
109 if (!f) return 0;
110
111 CacheHeader hdr;
112 if (fread(&hdr, 1, sizeof(hdr), f) != sizeof(hdr)) { fclose(f); return 0; }
113 if (hdr.magic != CACHE_MAGIC || hdr.version != CACHE_VER || hdr.slot_count == 0) { fclose(f); return 0; }
114
115 *out = f;
116 return 1;
117}
118
119static int read_exact(FILE *f, void *buf, size_t n)
120{
121 return fread(buf, 1, n, f) == n;
122}
123
124static int read_header(FILE *f, CacheHeader *hdr)
125{
126 unsigned char b[32];
127 if (fseek(f, 0, SEEK_SET) != 0) return 0;
128 if (!read_exact(f, b, sizeof(b))) return 0;
129 memcpy(hdr, b, sizeof(b));
130
131 if (hdr->magic != CACHE_MAGIC || hdr->version != CACHE_VER) return 0;
132 if (hdr->slot_count == 0) return 0;
133 return 1;
134}
135
136static int read_slot_at(FILE *f, uint32_t slot_index, CacheSlot *slot)
137{
138 unsigned char b[16];
139 long off = (long)sizeof(CacheHeader) + (long)slot_index * (long)sizeof(CacheSlot);
140 if (fseek(f, off, SEEK_SET) != 0) return 0;
141 if (!read_exact(f, b, sizeof(b))) return 0;
142 memcpy(slot, b, sizeof(b));
143 return 1;
144}
145
146static int read_record_at(FILE *f, uint32_t offset, CacheRecordHdr *rh)
147{
148 unsigned char b[16];
149 if (fseek(f, (long)offset, SEEK_SET) != 0) return 0;
150 if (!read_exact(f, b, sizeof(b))) return 0;
151 memcpy(rh, b, sizeof(b));
152 return 1;
153}
154
155static long cachepack_slot_offset(const CacheHeader *hdr, uint32_t slot_index)
156{
157 return (long)sizeof(CacheHeader) + (long)slot_index * (long)sizeof(CacheSlot);
158}
159
160static int cachepack_slot_write(FILE *f, const CacheHeader *hdr, uint32_t slot_index, const CacheSlot *slot)
161{
162 if (fseek(f, cachepack_slot_offset(hdr, slot_index), SEEK_SET) != 0) return 0;
163 if (fwrite(slot, 1, sizeof(*slot), f) != sizeof(*slot)) return 0;
164 return 1;
165}
166
167/* 在 cachepack 中查找 (x,y),找到则返回 1 并给出 offset/size;否则 0 */
168static int cachepack_find(FILE *f, const CacheHeader *hdr, int32_t x, int32_t y, uint32_t *out_off, uint32_t *out_size)
169{
170 uint32_t mask = hdr->slot_count - 1;
171 uint32_t i = hash_xy(x, y) & mask;
172
173 for (uint32_t probe = 0; probe < hdr->slot_count; probe++) {
174 CacheSlot slot;
175 if (!read_slot_at(f, i, &slot)) return 0;
176
177 if (slot.offset == 0) {
178 return 0; /* empty slot => not found */
179 }
180 if (slot.x == x && slot.y == y) {
181 *out_off = slot.offset;
182 *out_size = slot.size;
183 return 1;
184 }
185
186 i = (i + 1) & mask;
187 }
188 return 0;
189}
190
191/* 写入:若已存在则不写;不存在则追加 record 并写 slot。返回 1=写入成功或已存在,0=失败 */
192static int cachepack_put(FILE *f, const CacheHeader *hdr, int32_t x, int32_t y, const void *data, uint32_t size)
193{
194 uint32_t off=0, sz=0;
195 if (cachepack_find(f, hdr, x, y, &off, &sz)) {
196 return 1; /* already exists */
197 }
198
199 uint32_t mask = hdr->slot_count - 1;
200 uint32_t i = hash_xy(x, y) & mask;
201
202 /* 找空槽 */
203 for (uint32_t probe = 0; probe < hdr->slot_count; probe++) {
204 CacheSlot slot;
205 if (!read_slot_at(f, i, &slot)) return 0;
206
207 if (slot.offset == 0) {
208 /* append record at EOF */
209 if (fseek(f, 0, SEEK_END) != 0) return 0;
210 long rec_off = ftell(f);
211 if (rec_off <= 0) return 0;
212
213 CacheRecordHdr rh;
214 rh.tag = 0x454C4954u; /* 'TILE' */
215 rh.x = x;
216 rh.y = y;
217 rh.size = size;
218
219 if (fwrite(&rh, 1, sizeof(rh), f) != sizeof(rh)) return 0;
220 if (size > 0 && fwrite(data, 1, size, f) != size) return 0;
221
222 /* write slot */
223 CacheSlot ns;
224 ns.x = x;
225 ns.y = y;
226 ns.offset = (uint32_t)rec_off;
227 ns.size = size;
228
229 if (!cachepack_slot_write(f, hdr, i, &ns)) return 0;
230 fflush(f);
231 return 1;
232 }
233
234 i = (i + 1) & mask;
235 }
236
237 /* 表满:这里不做扩容,返回失败。你如果后面需要,我可以给你加“自动重建扩容”的代码。 */
238 return 0;
239}
240
241/* 从 cachepack 读取数据到 out_buf(需足够大),返回实际 size;失败返回 0 */
242static uint32_t cachepack_get(FILE *f, uint32_t offset, int32_t x, int32_t y, void *out_buf, uint32_t buf_cap)
243{
244 if (fseek(f, (long)offset, SEEK_SET) != 0) return 0;
245
246 CacheRecordHdr rh;
247 if (!read_record_at(f, offset, &rh)) return 0;
248
249 if (rh.tag != 0x454C4954u || rh.x != x || rh.y != y) return 0;
250 if (rh.size > buf_cap) return 0;
251
252 if (rh.size > 0 && fread(out_buf, 1, rh.size, f) != rh.size) return 0;
253 return rh.size;
254}
255
256#define TILE_BUCKET_SHIFT 4 /* 2^4 = 16 */
257
258/* save tile in disk cache */
259void savedisk(int x, int y, int z, int s, SDL_RWops *rw, int n)
260{
261#ifdef _PSP
262 /* PSP: 禁止写缓存,完全只读 */
263 return;
264#endif
265
266 // FILE *f;
267 // char filename[256];
268
269 if (!rw || n <= 0)
270 return;
271
272 // 先把数据读到内存(既要写旧文件,也要写 cachepack)
273 unsigned char *all = (unsigned char*)malloc((size_t)n);
274 if (!all) return;
275
276 SDL_RWseek(rw, 0, SEEK_SET);
277 if (SDL_RWread(rw, all, 1, n) != (size_t)n) {
278 free(all);
279 return;
280 }
281
282 // 新格式:写入 cache/<s>/<z>/<dx>_<dy>.cache(存在则跳过)
283 // .cache 格式以一个缩放层为单位存储, 可以避免大量小缓存文件对磁盘空间的浪费
284 {
285 FILE *cf = NULL;
286 CacheHeader hdr;
287 if (cachepack_open_or_create(&cf, s, z, x, y, CACHE_SLOTS_POW2)) {
288 if (read_header(cf, &hdr)) {
289 cachepack_put(cf, &hdr, (int32_t)x, (int32_t)y, all, (uint32_t)n);
290 }
291 fclose(cf);
292 }
293 }
294
295 free(all);
296}
297
298/* return the tile from disk if available, or NULL */
299SDL_Surface *getdisk(int x, int y, int z, int s)
300{
301 // 新格式:cache/<s>/<z>.cache
302 FILE *cf = NULL;
303 CacheHeader hdr;
304
305#ifdef _PSP
306 // PSP:只读打开,不创建
307 if (!cachepack_open_readonly(&cf, s, z, x, y))
308 return NULL;
309#else
310 // PC:允许创建
311 // slots_pow2 = 18 => 262144 slots(约 4MB 索引表)
312 // slots_pow2 = 12 => 4096 slots(约 64KB 索引表)
313 if (!cachepack_open_or_create(&cf, s, z, x, y, CACHE_SLOTS_POW2))
314 return NULL;
315#endif
316
317 if (!read_header(cf, &hdr)) { fclose(cf); return NULL; }
318
319 uint32_t off=0, sz=0;
320 if (!cachepack_find(cf, &hdr, (int32_t)x, (int32_t)y, &off, &sz)) {
321 fclose(cf);
322 return NULL;
323 }
324
325 if (sz == 0 || sz > TILE_MAX_BYTES) { fclose(cf); return NULL; }
326
327 unsigned char *tmp = (unsigned char*)malloc(sz);
328 if (!tmp) { fclose(cf); return NULL; }
329
330 uint32_t got = cachepack_get(cf, off, (int32_t)x, (int32_t)y, tmp, sz);
331 fclose(cf);
332
333 if (got != sz) { free(tmp); return NULL; }
334
335 SDL_RWops *rw = SDL_RWFromMem(tmp, (int)sz);
336 SDL_Surface *img = rw ? IMG_Load_RW(rw, 1) : NULL;
337 free(tmp);
338 if (img) return img;
339
340 return NULL;
341}
342
经过多轮的优化和改造,现在这个自制软件终于可以相对方便的使用了。我把离线地图批量下载好,导入 PSP 后就可以愉快玩耍了。最后附上汉化版和离线地图的下载地址。