木须柄的时光工坊

探索技术与游戏乐趣的奇妙之旅

目录
2026年如何在 PSP 上实现地图导航?PSP-Maps 编译日志
/        

2026年如何在 PSP 上实现地图导航?PSP-Maps 编译日志

PSP 可以使用外接的 GPS 模块,实现定位和导航功能。比如官方的地图软件,包括 Go!Explore (欧洲),Maplus (日本),还有自制地图软件,包括 MapThis,PSP-Maps。

不过截止到目前为止(2026年1月)所有软件都已经停止更新超过15年以上了,如果希望能获得国内较新的导航数据,并适配 PSP 的 GPS 模块是非常困难的。

经过一段时间的测试,我发现目前只有 PSP-Maps 可以相对方便的获取新的地图信息。因此打算深入研究一下,让 PSP 实现地图定位功能。

一、使用方法

1.1 按键功能映射

image-20260131201345854.png

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

1.2 菜单功能

image-20260131201520635.png

菜单 说明
当前地图 可以左右键切换地图类型
输入地址 地址查询(已废弃)
路线规划 输入起始地和目的地,生成路线
显示详细信息 打开后会显示经纬度、缩放和地图类型
显示 KML 显示导航信息(已废弃)
缓存缩放层数 批量获取地图缓存,建议一次不要超过5层
缓存大小 一般选择 512MB ~ 1GB 足够
启用天文 / 星球模式 谷歌天文模式地图(已废弃)

二、离线地图缓存

PSP-Maps 使用的是 curl 标准库实现的网络下载功能,这在 PSP 上是无法兼容的,因此软件只能在电脑端下载地图瓦片图的离线缓存,然后把缓存和数据文件拷贝到 PSP 端使用。

2.1 下载地图

缓存地图有两种方式,一种是直接在地图上移动缩放,就会自动下载对应缩放的瓦片图,这样手动下载虽然直观,但是比较低效。

另一种缓存方式是使用菜单功能,这样可以批量下载 1-9 层地图瓦片(当前显示的层是 1,每一层有 4^n 张图片),建议一次不要下载超过 5 层图片。

image-20260131203233649.png

三、编译源码

由于外网提供的 PSP-Maps 软件包年代太过久远(2009年),其中很多地图源已经失效,比如 Google, Yahoo 地图已经无法访问。同时较好的地图源,比如 OpenStreetMap 的接口需要更新,否则不能正常访问。因此我希望重新编译 PSP-Maps 源码,改进使用体验。

3.1 踩坑前言

构建 PSP 自制软件需要首先一套交叉编译环境,我这里使用的是 Windows 自带的 wsl 下的 Ubuntu 24.04 环境,使用 PSPDEV 工具链构建编译环境(具体方法可以参考这里)。然而这套工具可以“方便”的构建 PSP 交叉编译环境,但是由于 PSP-Maps 的源码很久没有更新了(截止到2013年),所以其代码依赖的工具链版本已经不适应现代编译环境,很多基础工具的调用方式都发生了很大变化。即使我尝试 Debug 其代码适应了现代环境,并使其能正常通过 Ubuntu 和 Win 11 下的编译并正常运行, 但是生成的程序却始终没办法在 PSP 实机上正常运行。

MVIMG_20260129_023929.jpg

image-20260131210600858.png

经过 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型号) 死机。

3.2 编译准备工作

编译环境:Ubuntu 14.04(新版系统环境存在兼容性问题)

安装基础依赖:

1sudo apt-get update
2sudo apt-get install build-essential cmake pkgconf libreadline8 libusb-0.1 libgpgme11 libarchive-tools fakeroot

安装 PSPDEV 编译环境:

github 地址

 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.

3.3 Hello World

在直接去编译复杂项目之前,可以先试试编译一个 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!

image-20260131204819922.png

3.4 编译 PSP-Maps

源码地址: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,如果一切没问题,就能看到正常运行了

image-20260131205336658.png

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 {} .

3.5 源代码改进和汉化

由于 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}

3.6 源码改进

瓦片计算方法

初始世界地图全图,缩放 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

地图缓存 BUG

程序默认的 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 后就可以愉快玩耍了。最后附上汉化版和离线地图的下载地址

评论
取消