← Back to Blog

《旅行青蛙·中国之旅》数据保存与游戏逆向

蛙蛙复活计划:项目成果、关键技术与开发日志

项目成果概览

本项目围绕《旅行青蛙·中国之旅》的 Android 客户端、静态资源和个人历史存档,搭建了可在本地运行的游戏保存环境。开发从登录快照回放开始,逐步扩展到可写存档、日常玩法、相册与地图,并形成普通版和图鉴版两套独立运行的版本。

已完成的主要内容

  • 资源与存档保存:备份 APK、应用私有数据和热更新资源,抓取正式服初始化状态,并区分原始记录与可分享的脱敏数据。
  • 本地服务器与持久化:使用 Python WebSocket 服务器接入原客户端,将静态快照回放扩展为可读写的本地状态,保存历史备份并处理旧存档迁移。
  • 普通版日常循环:接入旅行、背包与餐桌、商店、家具、花圃、庭院旅友、日历及活动奖励等流程;后续持续修正状态同步与结算时机。
  • 照片与相册:恢复历史照片图层,支持照片保存、删除、回收站、恢复与 Android 相册导出,并继续校准旅行照片、动态照片和构图坐标。
  • 地图与旅行日记:将依赖在线 H5 的旅行地图改为本地 Egret 静态地图,保留 35 个目的地、城市相册和打卡进度;将漂流瓶入口改为可新建、编辑、删除和持久化的个人旅行日记。
  • 独立图鉴版:使用独立应用包、端口和存档,预置全收集内容,并保留庭院、旅行、制作和活动交互,处理无限资源与重复奖励之间的边界。

关键技术与验证方式

开发涉及 Python、WebSocket、JavaScript / Egret / EUI、ADB,以及客户端补丁与 JSON 状态管理。工作重点包括协议分析、状态持久化、存档迁移、照片图层恢复和在线功能的本地替代。

日志中的验收沿着“客户端操作 → 协议请求与响应 → 状态落盘 → 重启后恢复”展开,而不仅检查界面能否显示。旅行日记保存、相册分页、奖励同步和旅友回礼等问题,都通过这条链路定位和修正。

还原范围与当前边界

客户端源码和静态配置、正式服实际观测,以及为了离线运行补充的本地规则,分别记录。无法从现有证据确认的服务端概率与参数放入 offline_policy.json;单次抓包结果不作为完整官方算法的证明。

普通版已具备日志中记录的日常循环,图鉴版也已形成独立运行方案,文末继续记录后续修复。这是个人离线保存项目,部分依赖在线服务的功能采用本地替代;由于版权原因,项目内容暂不公开。

下文保留项目背景、开发总结和按日期记录的实现过程。“进行中”“未完成”等标注反映当时的状态,后续进展见较晚日期的日志;启动命令和目录结构集中在文末。

项目背景

2026.9.8得知《旅行青蛙·中国之旅》在六周年前的一周要停服了。

受到小红书用户@呱命由我不由服这篇备份游戏数据的文章的启发,决定开启《旅行青蛙·中国之旅》的离线保存项目,希望可以保留原客户端、资源和自己的存档,在2026.12.8停服后,让原来的客户端可以在本地服务器上运行。

该日志将持续更新该项目的进度与技术实现。由于版权原因,项目内容暂不公开。

开发总结

从开启这个项目到完成每一个细节总共花了十天、两次codex重置(也就是Chatgpt plus的三个周额度)。此前从来没有接触过游戏逆向,甚至于说是第一次尝试完成一个游戏工程。将一个相对来说很简单的游戏作为第一个小练手,一是为了情怀,二是觉得很好玩。plus的额度少得可怜,于是一直和时笨时聪明的5.6Terra斗智斗勇,一周里同时做后端和测试和一点点前端(地图系统的开发)。

一开始做的时候没想过能不能成功,只是怀着忐忑的心情去官方包里抓代码,去分析,然后一步一步考察把游戏逆向出来的可能性。幸运的是大部分逻辑可以直接爬出来,于是就进行下去了。开发到第三天其实已经有其他人把离线版做出来了(尽管细节与功能尚不齐全),也会感慨自己的水平仍需精进。在做的过程中所有需要收集的东西其实都已经显现出来了,对于一个主要玩法为收集的放置游戏来说,游玩的意义还存在吗?我继续做这个项目的意义是什么呢?

做了几天之后目标已经不再是把这个游戏留下来(或许也是因为第二天的时候这个游戏就已经留下来了),而是完成我的第一个游戏项目。本项目由于版权原因无法完全公开,但是在大学期间为数不多的宝贵的清闲时间中,还能找到一点激起自己热情的事情,也是幸运的。

最开始玩旅行青蛙的日服,功能十分简单,没有会砸家具台的青蛙,没有各种传统节气的活动。后来玩国服,再到现在做逆向,才意识到游戏本身是开发者爱的体现。项目完成的过程中尽可能忠实于原游戏的表现,但与其他玩家互动的功能不得不删除。于是突发奇想,在搓完地图系统(原地图系统使用H5,由于权限原因难以复刻)之后将漂流瓶功能改成了旅行日记。也许某日再次点开,看到几年前的明信片与文字,仿若拾起曾经向大海诉说的心愿——以及漂泊多年之后,终于回到岸边,然后再次出发。

2026.9.10

开启项目。电脑上安装了雷电模拟器14,可以模拟安卓环境。虽然应用商店已经将游戏下架,但是可以在官网上找到安装包apk。

1. 备份 APK 、应用私有数据、热更新资源

在真正开始写本地服务器之前,需要先尽可能把原始客户端、资源和自己的游戏数据完整留下来。

最先保存的是当前能够正常运行的 APK。《旅行青蛙·中国之旅》的 Android 客户端并不是把所有内容全部打包在 APK 里面。运行以后,它还会在应用私有目录中产生大量 SDK 数据、缓存、配置文件和后来下载的游戏资源。因此下一步是在有 root 权限的 Android 模拟器中,将整个应用私有目录打包保存。

大致流程是:

/data/data/com.aligames.lxqw.hhb
        ↓
tar 打包
        ↓
复制到 /sdcard
        ↓
通过 adb pull 保存到电脑

最终备份出来的私有数据接近 100 MB。 里面除了常见的

shared_prefs
files
code_cache
数据库
WebView 数据
SDK 数据

之外,还有两个后来非常重要的目录:

files/games
files/jsdata

最开始我还怀疑 shared_prefs 或 SQLite 数据库中可能直接保存了完整游戏存档,例如应用中有一个体积比较大的shared_prefs/localhost.xml.但进一步检查后发现,它主要是 Egret 的 localStorage 数据,其中保存了资源版本记录、三叶草显示位置、本地公告以及一些客户端状态,并不是完整的角色存档。几个 SQLite 数据库也主要来自支付、日志、推送、下载器等 SDK,没有找到明显的核心游戏角色数据库。因而也可以看出,角色的主要状态并不完整地保存在 Android 本地,真正的存档必须继续从服务器端保存。

继续检查 files/games 后,发现这里其实保存了游戏运行时下载下来的大量热更新资源。 客户端访问过的资源 URL 被转换成了本地缓存路径。例如可以看到来自游戏热更新服务器的:

release/lingxi/android/1083/
release/lingxi/android/1085/

其中包含:

js/default.thm.js
js/main.min.js
resource/China/default.res.json
resource/China/config/gameConfig.json
各种图片、配置和 EAB 资源

files/games 中大约有几十 MB 的内容,这部分对于游戏保存非常关键。

也就是说,当前真正运行的客户端并不完全等于最初安装的 APK,而更接近:

APK
+
运行时下载的热更新 JS
+
运行时下载的配置
+
运行时下载的资源文件

如果只保存 APK,将来重新安装以后,官方热更新服务器一旦关闭,客户端很可能无法再次获得这些内容。 因此我把当前已经下载到本机的热更新目录也作为项目的重要备份保存了下来。

2. 提取客户端逻辑 main.min.js

热更新资源中最关键的文件之一是main.min.js. 这个文件有一百多万字节,实际上包含了游戏绝大部分 Egret/TypeScript 编译后的 JavaScript 客户端逻辑。

通过阅读和搜索 main.min.js,逐渐能够找到:

RoleModel
ItemModel
TravelModel
UserModel
FurnitureModel
协议列表 ProtocolList
WebSocket 管理代码

以及诸如:

clover_harvest
item_buy
item_putin_bag
item_putin_desk
client_load_role
client_load_events

这样的具体游戏协议。

因此我保存了一份完全不修改的 main.min.js,然后所有实验都在它的副本上进行。

目前大致有:

main.min.js
    原始基线

main.capture.js
    连接官方服务器
    额外输出协议日志

main.offline.js
    强制连接本地服务器

main.offline.stage5.js
    在离线版本基础上增加当前需要的本地协议扩展

3.EAB 配置包与正式规则恢复

客户端的 config.eab 并不是普通压缩包。资源头会标识 EAB 格式和加密状态;main.min.js 中保留了完整的资源解析器,包括:

EjoyAssetBundle
→ EAB header
→ xxtea decrypt
→ 资源目录 JSON
→ 每份配置 payload

因此恢复配置的方式不是去猜文件内容,而是顺着原客户端自己的加载链读回来:

1. 从正式 JS 找到 EAB parser
2. 确认加密标记与 xxtea 解密调用
3. 还原客户端用于资源包的 key 生成方式
4. 解析前置目录长度与资源索引
5. 按原索引读取 tumbler / compost / pocket / bench 等 JSON

这套方法主要用于读取静态规则,不改写原始资源包。它让后续工作可以直接对照正式配置中的 ID、图层、图纸和分类,而不是只依赖攻略截图。

例如工作台相关配置可以重新整理成:

tumblerData_json
→ 12 种不倒翁底座
→ 每种 3 个正式成品变体

compostData_json / pocketData_json
→ 堆肥箱与挂兜的正式款式列表

benchData_json
→ 三类特殊家具的工作台类别

furnitureCommon_json
→ 素风格、石作、迎春、庆典、彩陶、夕拾、许愿等主题名称

攻略仍然有价值:正式配置告诉我“有哪些东西”,攻略补上“哪些材料能一起放进工作台”。两者合起来,才适合写入本地规则。

4. 找到客户端自带的本地测试模式

阅读 main.min.js 时还发现了一件很有意思的事情:游戏本身其实保留了一个 TestChannel。代码中的默认配置甚至包含ws://127.0.0.1:8080以及一套不依赖正式渠道 SDK 的测试登录逻辑。正式运行时,这些配置会被线上配置覆盖,所以普通客户端最终还是连接官方服务器。但这意味着,我并不需要从零重新写一个 Android 客户端,也不必自己伪造正式渠道 SDK。只需要在自己的离线副本中,在最终配置阶段强制恢复:

ChannelType.Test
ws://127.0.0.1:8080

原来的游戏客户端就能够主动连接一个本地 WebSocket 服务器。 这一发现后来成为整个离线方案的基础。

最终结构是:

Android 模拟器中的原游戏客户端
        |
        | ws://127.0.0.1:8080
        |
     adb reverse
        |
        v
Windows 上的 Python WebSocket Server

5. 给客户端增加协议日志

接下来需要解决的问题是: 客户端代码虽然告诉我它会发送哪些协议,但是不知道官方服务器到底会返回什么。

因此我制作了一个main.capture.js. 这个版本仍然正常连接官方服务器,只是在原本的 Socket 日志代码旁边额外加入了自己的 FROG 日志标记。 因为 Android logcat 对单行日志长度有限制,所以一条较大的 JSON 协议不能直接完整打印。 最终采用的是分块方式:

FROG|消息编号|分块编号|总分块数|内容

例如一条很长的服务器响应会变成:

FROG|31|0|4|...
FROG|31|1|4|...
FROG|31|2|4|...
FROG|31|3|4|...

电脑端再通过 Python 脚本按照消息编号和分块编号重新拼接。

第一次给压缩后的 JavaScript 插入日志代码时,因为原代码处于一个逗号表达式中,我插入了不合适的分号,导致整个 main.min.js 出现 JavaScript SyntaxError,游戏直接无法运行,后来重新寻找更安全的插入位置才修好。

6. 第一次完整抓取登录后的初始化状态、数据脱敏

日志系统工作以后,下一步就是做一次最重要的抓取: 从启动游戏开始,一直到完全进入庭院,记录官方服务器发送的整个初始化过程。

最终这一次抓取成功重组出了几十条完整协议,而且没有出现丢失分块或 JSON 解析失败。

客户端登录完成后会发送:

hall.login
hall.enter_game
annual.load
client.load_all_info

随后服务器连续主动推送大量初始化状态。 这部分数据包括:

client.load_role
clover.load_clovers
item.load_items
item.load_shop_info
mail.load
travel.load_note
client.load_events
album.load_all
guest.load
story.load
furniture.load_furniture
weather.load
calendar.load

以及大量活动和其他系统的数据,总共获得了四十多类初始化 push。

其中最重要的几个是:

client.load_role包含角色基本信息、青蛙状态、三叶草数量、设置等;

clover.load_clovers包含草地中每一株三叶草的状态;

item.load_items包含仓库、背包、餐桌;

item.load_shop_info包含商店购买记录;

client.load_events包含当前尚未处理的旅行事件;

以及album.load_all、mail.load、travel.load_note等长期存档数据。

到这里,第一次真正获得了一份服务器端存档快照。

抓协议时还有一个必须注意的问题:登录阶段不可避免会经过正式服务器的账号和认证数据。所以后来写解析工具时,我把输出明确分成两类:

LOCAL_ONLY只保存在自己的电脑上,用于最大程度保存原始数据;

SHARE_SAFE用于分析、分享和后续记录。

安全版本会自动删除或替换:

token
account
device id
IMEI
OAID
IP
正式登录数据
设备环境数据

等敏感字段。

之后分析单个动作时,也尽量只使用 SHARE_SAFE 文件,而不直接处理或者传播完整原始登录日志。

7. 本地服务器

Stage 1.能够登录

拿到初始化快照后,才开始写第一版 Python WebSocket 服务器。

Stage 1 所完成的工作:

客户端连接
↓
本地测试账号登录
↓
模拟 hall.login / hall.enter_game
↓
客户端发送 client.load_all_info
↓
把之前官方服务器的初始化 push 原样 replay

第一次看到原游戏客户端在完全不连接官方服务器的情况下正常进入庭院,是整个项目的第一个重要节点。

但这时它还只是一个“冻结的存档”。服务器每次启动都只会重新播放原来的快照,所以任何新的操作都无法永久保存。

也正是在这个基础上,后面才开始把 Stage 1 的静态 replay,一步一步改造成现在能够真实修改 state.json 的可写本地服务器。

Stage2.开启备份

将初始化抓取的数据转换为了本地 state.json,之后服务器不再每次从原始抓包重新恢复,而是从这份本地状态文件加载。客户端的一些设置修改,例如client.set_client,已经可以直接写入本地状态。保存时还会自动保留历史备份,避免调试过程中破坏唯一存档。

Stage3.收三叶草☘

在这个基础上,今天第一个真正实现的游戏操作是收三叶草。

为了确认官方服务器的真实行为,我在官方连接模式下只操作了一次普通三叶草,并单独记录这次动作的 WebSocket 协议。抓到的结果:客户端发送 clover.harvest,参数中包含 clover_id;服务器随后先发送 clover.update 更新三叶草总量,再返回包含 clover_id 的响应。

这也证明了一个很重要的思路:客户端源码可以告诉我“客户端会发什么”和“收到数据以后如何处理”,但服务器真正返回什么,仍然最好用实际协议来校准。

根据这次抓包,实现了 Stage 3。现在在本地服中收一棵普通三叶草后,服务器会修改三叶草总数,更新对应草的 last_harvest,保存到 state.json,再按照官方协议顺序向客户端发送 clover.update 和收获响应。

实际测试成功。收获前本地存档中的三叶草数量与官方快照一致,收一棵之后数量正确加一。关闭游戏和服务器后重新启动,新的数量仍然存在,说明这一操作已经不只是客户端 UI 上的临时变化,而是真正进入了本地存档。

Stage4.背包和餐桌

这部分不需要再次抓官方协议,因为客户端代码已经把协议结构暴露得非常完整:item.putin_bag
item.takeout_bag
item.putin_desk
item.takeout_desk

放入物品时发送位置和 item_id,取出时只发送位置。服务器同时维护 house、bag 和 desk 三份状态,并通过 item.update 向客户端同步仓库中物品的绝对数量。

测试中,物品可以正常从仓库放进背包或餐桌,再取回仓库;退出、重启服务器和重新进入游戏之后,位置和数量仍然正确。

Stage5.普通商店购买

这里遇到了一个比较有意思的问题。官方客户端发送 item.buy 时,实际上只把 shop_id 发送给服务器。商品对应的 item_id 和价格都来自客户端自己的 ShopDataDB。

官方服务器当然知道每个 shop_id 对应什么商品,但本地服务器并没有官方服务端的配置数据库。因此我对“本地离线版客户端”的协议做了一个很小的扩展:本地模式下,item.buy 除了原来的 shop_id,还会把客户端已经知道的 item_id 和 price 一并发送给本地服务器。

这样服务器就能完整执行一次购买事务:三叶草扣除商品价格,仓库对应物品数量增加,商店购买次数增加,然后整体写入 state.json,最后通过 clover.update 和 item.update 同步给客户端。

实际测试同样成功。购买了 shop_id=0 的商品,价格为 10,服务器日志显示三叶草正确扣除,物品进入仓库;随后将该物品放进餐桌再取出,库存数量也始终正确。

Stage6.旅行系统(进行中)

今天最后开始研究的是旅行系统。

目前已经确认,客户端把青蛙是否在家定义为:

frog.status == 0:在家

frog.status == 1:外出

我当前保存下来的状态中,frog.status=1,游戏内确实只能看到餐桌,看不到背包。

还发现了一个很有意思的细节:青蛙外出以后,存档中的 bag 数组并不会立即清空,之前准备的物品仍然保留在那里,但 bag_completed 已经恢复为 False。客户端是否显示背包,主要由 frog.status 决定,而不是由 bag 是否为空决定。

背包点击“准备完成”时,客户端只会发送item.set_bag_completed(true),但在完整协议列表里,没有找到类似 travel.start 或 frog.depart 的主动“出发”请求。 结合实际游戏机制来看,这意味着青蛙什么时候离开,很可能完全由服务器根据时间、背包和其他条件决定,而不是客户端按下某个按钮之后立即触发。

旅行过程中同样主要由服务器驱动。客户端会接收:client.load_events、notify.new_event并维护自己的旅行事件列表;处理完某个事件后,再通过 client.confirm_event 告诉服务器已经读取。

源码中还能看到一个明确的 BackHome 旅行事件类型,因此“青蛙回家”本身很可能也是服务器生成的一条时间事件。

现在青蛙正好处于外出状态。下一步准备切回官方协议抓取模式,在餐桌上放好食物之后长时间保持游戏运行,同时持续记录 FROG WebSocket 日志。因为无法准确预测青蛙什么时候回来,所以让日志持续写入电脑文件,直到观察到青蛙已经回家。

如果顺利,这一次长时间抓取应该能够获得整个旅行系统目前最关键的一批数据:外出期间的旅行事件、notify.new_event、可能产生的明信片或邮件,以及最重要的 BackHome 事件结构。


现在已经能够在完全本地的环境中收三叶草、购买商品、管理仓库、背包和餐桌,而且所有变化都会写入独立的本地存档并在重启后恢复。

接下来真正困难的部分,是还原原服务器中那些带有时间、随机性和状态机的逻辑,尤其是旅行、明信片和回家机制。

2026.9.11

质疑图恒宇->理解图恒宇->成为图恒宇。

前面的工作已经解决了本地 WebSocket、登录流程、可写存档、三叶草收割、背包桌面和商店等基础功能。今天真正开始进入游戏最核心的部分——旅行,以及围绕旅行延伸出来的照片、访客、花圃、礼品盒、故事和日历系统。

Stage6.旅行系统

最开始实现旅行时,我面临的一个问题是:客户端虽然保存了大量关于目的地、食物、幸运物、工具、照片和特产的静态配置,但真正“服务器怎样抽出一次旅行结果”的算法并不完整存在于客户端。

因此目前采用的原则还是,能从客户端源码和配置确定的行为严格照原版;服务器内部看不到的概率和权重,单独作为本地策略保存。

旅行系统首先恢复了:

准备背包
→ 点击“准备好”
→ 等待出发
→ frog.status = 1
→ 生成目的地
→ 生成照片、特产、物品、三叶草和抽奖券
→ 等待返程
→ BackHome
→ frog.status = 0
→ 展示返家奖励

为了测试方便,一开始把旅行时间临时设成了 60 秒。

实机测试中,这套流程最终完整跑通了一次:

手动准备背包
→ 点击准备完成
→ 重新登录后青蛙出发
→ 等待 60 秒
→ 再次登录
→ 青蛙返家
→ 得到 75 个三叶草
→ 得到 2 张抽奖券
→ 得到旅行特产
→ 带回 4 张照片

随后我实际保存了其中两张照片、删除两张,再进入相册,服务器能够正确返回现有照片。这说明最核心的一条链已经真正闭环:

旅行
→ 新照片
→ 保存 / 删除
→ 总相册
→ 持久化

解决“青蛙一进游戏就不在家”的存档迁移问题

旅行系统刚加入时出现了一个很麻烦的问题:启动离线服后,青蛙直接不在家,导致根本无法继续准备行李。原因是原始正式服快照本身是在青蛙旅行途中保存的。

正式服的frog.status = 1只说明“它正在旅行”。但离线服务器并没有这趟正式服旅行对应的目的地、开始时间、返回时间、奖励计划、照片计划。于是产生了一个“悬空旅行”:客户端认为青蛙不在家,但离线服务器永远不知道它什么时候回来。最后加入了一次性迁移规则:

status != 0
+
不存在本地生成的 activity / plan
→ 判断为正式服遗留的悬空外出
→ 自动收尾并恢复 HOME

并且不是简单修改 status=0,而是根据之前真实抓到的正式服返家过程恢复物品。

之前抓到的一次正式返家中:

旅行中 bag:
[-1,-1,2007,2005]

desk:
[3,19,1001,1000,2001,2009,2010,-1]

返家后变为:

bag:
[3,1001,2010,2009]

desk:
[-1,19,-1,1000,2001,-1,-1,-1]

而 2005 / 2007 归还仓库。

离线迁移现在也按照这套已经验证过的正式服物品流执行。

这也是今天一个很重要的经验:不能只保存一个状态值,还必须区分“正式服遗留状态”和“离线服务器自己创建、能够继续结算的状态”。

明信片生成已经能工作,但构图还需要继续校准

旅行照片现在不是简单回放以前抓到的照片,而是根据保存下来的

Picture
resources
layers
pose

重新生成。

对于较新的照片资源,这种方法已经能正确恢复背景和青蛙图层。例如之前真实抓到的青海湖照片,可以从配置重新计算出接近正式服的青蛙坐标。

但今天实机测试时也发现了一个视觉上的问题。有些比较普通的“路过照片”,背景中的固定美术元素是固定的,而青蛙、雨伞等元素应该在一定范围内随机摆放。当前离线算法使用的随机位置范围过大,导致某张雨景照片里青蛙和伞明显偏到画面右下角,视觉上很不自然。这不影响照片存档和旅行逻辑,但后面需要按不同的Picture.type、frogPose、照片模板分别设置安全随机区域,而不能所有照片都使用同一套坐标随机算法。

Stage7.特殊植物和庭院网兜三叶草

测试旅行期间顺便收了一轮庭院三叶草,碰到了一个以前没有实现的情况:收到了狗尾草。日志中这一株是:

element = 3
sprite = 2001001

之前服务器把所有 element != 0 都当成“不支持”,于是客户端触发:

clover.harvest_resend

而 resend 处理代码又错误地把一个 list 当成 dict,导致服务器直接异常。

后续通过源码和配置确认:

element = 0 → 普通三叶草
element = 1 → 四叶草
element = 2 → 幸运花
element = 3 → 特殊野生植物
element = 4 → 花盆植物

其中五种野草包括:

狗尾草
知风草
蓼
蒲公英
兔尾草

狗尾草对应:

decoration id = 1001

因此现在特殊植物收获后会进入 client.load_decorate 的花材/装饰收藏,而不是增加三叶草数量。

同时重新实现了 clover.harvest_resend,保证客户端因为网络或协议原因重发请求时:

不会再次加奖励
不会再次加入收藏
不会报错

也就是幂等处理。

“网兜”属于家具系统

今天还确认了一个之前理解不准确的地方。

庭院旁边那个会慢慢积累三叶草的“网兜”,客户端真正对应的是:

furniture.load_pocket.clover

也就是说它不是:

草成熟
→ 自动增加玩家三叶草余额

而应该是:

草成熟后继续经过生长周期
→ 多出来的三叶草进入挂兜
→ 玩家点击挂兜
→ furniture.pocket_get
→ 才正式加入三叶草钱包

所以之前做的“离线期间自动加三叶草”被改成了真正的挂兜库存。

第一次启用新算法时不会追溯几个月历史数据,否则会突然产生大量三叶草;只从离线版本启用之后开始累计。

Stage8.花圃系统与花瓶系统

花圃系统

在三叶草系统之后,开始完整梳理庭院的花圃。

这一块原本以为只是简单种植物,实际牵涉的系统很多:

种子
肥料
特产
堆肥箱
访客
成长值
花盆
蔬果
观赏花
室内花瓶
旅行纪念花
嘟嘟商店

客户端静态配置意外地保存得非常完整。

目前确认有:

11 类种子
6 种肥料
每类植物 3 个成长阶段
每类种子对应多个具体品种

并且嘟嘟商店的商品也保留着官方配置:

6001–6006 → 肥料
6007–6016 / 6019 → 各类种子

肥料还能区分:

水溶肥
缓释肥

客户端文字明确描述了不同的肥力强度和持续方式,但服务器内部精确“加多少成长值、持续多久”没有静态数字,因此这些仍作为离线策略参数保存。

花圃系统现在实现了:

购买 / 获得种子
→ 花盆播种
→ 访客离开时促进生长
→ 被动成长
→ 堆肥提高成长速度
→ stage 1
→ stage 2
→ stage 3
→ 成熟
→ 收获

成熟植物分两类。

一类是蔬果,直接成为旅行特产,例如:

拇指萝卜
樱桃萝卜
樱桃番茄
草莓
迷你南瓜

另一类是观赏花,进入室内花瓶使用的 decoration 收藏。

花瓶系统

顺着花圃继续检查室内花瓶,最终发现三种来源的植物实际上汇入同一个系统:

庭院随机野草
花盆种出的观赏花
旅行带回来的纪念花

例如旅行花材包括:

蜡梅
木槿
桂花
丁香
建兰
水仙
君子兰

花盆种出来的具体品种也有对应的 decoration ID。

客户端花瓶的消耗规则也有一个比较反直觉的地方:

插入一株新花
→ 新花暂时不扣数量

下一次换花
→ 被换下来的旧花消耗 1 个
→ 新花继续保留到下一次更换

现在离线服务器按照客户端原有语义实现,而不是简单“插进去立即扣 1”。

Stage9.访客喂食

之前已经完成三位伙伴:

困困
胖胖
跳跳

的官方喜爱度矩阵。

客户端 Character.taste[] 明确保存了每位伙伴对所有特产的固定喜爱值,因此这部分完全不需要猜。

反馈档位也是客户端直接写死的:

>= 80
两眼放光,高兴地扑了上来

60–79
吃得津津有味,嘴巴吧唧个不停

20–59
一声不吭,顽强填饱了肚子

< 20
看了一眼,没有什么胃口

实机测试中:

胖胖 + item 4014
taste = 60

也成功落入正确反馈档。

今天还修正了回礼出现的时序。

正式服并不是:

喂完
→ 马上收到邮件

而是:

喂特产
→ 访客继续留在庭院
→ 退出游戏
→ 再次登录
→ 访客离开
→ 回礼或聚会邀请出现

离线服务器现在也按照这一流程工作,并且奖励/邀请在喂食时就提前写入存档,因此不能靠不断退出重进刷新结果。

Stage10.礼品盒

语义混淆

今天围绕礼品盒有一次非常值得记录的错误,主要是由于我和gpt的交流出现了一些用语上的差错,导致了理解上的错误,把伙伴礼品盒与回礼、旅途遇到用户青蛙触发的故事,以及其他用户来串门的三个系统混起来了。

最开始,我根据“其他青蛙串门”把礼品盒触发逻辑错误地接到了visit.load,并模拟了一个“匿名旅蛙”,甚至进一步关联到了省花系统。实机测试后发现客户端只弹出了“其他青蛙会串门”的教程,却没有出现预期回礼;同时“故事”页面也没有变化。 继续检查源码和原始存档后才发现,游戏里至少存在三套看起来很像、实际上完全不同的社交系统:

Guest
→ 困困 / 胖胖 / 跳跳
→ 庭院访客、喂食、回礼、聚会

Visit
→ 其他玩家青蛙在庭院串门
→ 省花收藏

Story
→ 自己的蛙旅行途中遇到其他玩家的蛙
→ 收到对方特产
→ 可以回赠
→ 长期故事记录

另外礼品盒自己还有一套:

TravelFriends
→ 壁虎
→ 刺猬
→ 萤火虫

所以一开始把:

GiftBox
Story
Visit

当成同一套系统,是错误的。

这也成为今天最重要的架构经验之一:在逆向一个长期运营游戏时,UI 语义相似不代表服务端模型相同。必须以 Model、协议和存档字段为准。

礼品盒最终改成壁虎 / 刺猬 / 萤火虫,并成功实机闭环

修正之后,礼品盒不再制造“匿名玩家蛙”。

现在流程变成:

相册照片 / 特产
→ 放入礼品盒
→ 等待
→ 壁虎 / 刺猬 / 萤火虫之一出现
→ 带回奖励
→ VisitFriend 事件
→ 原客户端回礼界面
→ client.confirm_event

今天已经实机验证成功。

日志中实际产生:

GIFTBOX FRIEND RETURN
friend_id=1

随后客户端成功弹出了回礼,并最终发送:

client.confirm_event

因此礼品盒系统现在已经完成了一次真实客户端闭环。

Stage11.故事系统

进一步检查正式服抓包时发现,story.load 保存的数据非常明显就是过去与其他玩家青蛙相遇的记录:

id
partner
name
gift
feedback

存档里已经有Story 1–25,而客户端静态 Story 配置也正好只有 25 个故事。

离线版本现在计划这样处理:

已有 25 个故事
→ 原样保存故事进度
→ 去掉真实玩家 ID / 蛙名的依赖
→ 显示统一匿名名称

未集齐故事的其他存档
→ 旅行中按官方静态参数
   friend_choice_percent = 25
→ 尝试解锁尚未获得的 Story

同时不再要求给一个实际上已经不存在的真人玩家返还特产。

这比之前简单做“匿名玩家串门”更加符合游戏自身的系统结构。

Stage12.省花(未完成)

在误接 Visit 系统的过程中还顺带确认了省花收藏和旅行地图是完全独立的。

正式服存档虽然已经去过青海,也有青海照片、字条、旅行记录,但visit.load.acquire = {}仍然为空。因此去过青海≠获得青海省花。省花来源于其他玩家青蛙的庭院串门系统,而不是自己的旅行。

旅行地图使用

encytravel.load
unlock_list
unlock_desc
show_sub

省花则使用

visit.load
acquire

当前 2026 客户端甚至把省花图鉴入口隐藏了,因此这一部分暂时降低开发优先级,以后再恢复隐藏收藏页。

Stage13.日历(进行中)

今天还实际测试了日历。当天(实则为12日凌晨)出现一个可以领取的幸运礼物,客户端请求calendar.get_luck_reward。

服务器正确处理,实际获得:

item_id = 10108
count = 1

并写入存档。因此当日日历幸运礼物这一条已经闭环。

但之前已经错过日期的节气食品仍然不能领取,这说明“幸运日礼物”和“节气料理”仍是两套不同流程。

节气食品系统之前已经确认有三个任务条件:

观看广告 / 分享
累计获得 30 三叶草
抽奖中奖

离线版本计划继续保持:

广告 / 分享 → 自动完成
30 三叶草 → 必须实际完成
抽奖 → 必须实际完成

旧节气食品不能领取的问题留到下一阶段处理。

Stage14.周末帮助小伙伴 / 答谢

今晚最后又发现了一套之前没有实现的周循环玩法。

周末会出现“小伙伴的疑问”,玩家需要从候选列表中选择 5 个物品,到下一周周末,会根据其中猜对的数量获得不同档位的答谢礼物。

今天先领取了上一周小伙伴的答谢,然后尝试开始这一周的帮助,但服务器出现

lottery.confirm_reward
BLOCK 未实现写操作

说明这个玩法使用的是另一套 lottery.* 协议,和商店里item.gacha完全不是一回事。

之前正式服抓包其实已经保存过它的状态:

phase
last_phase
state
select_list
answer
right_flag
reward
extra_item

因此下一阶段会优先完整逆向这一套周循环:

本周生成问题
→ 玩家选 5 个
→ 保存答案
→ 到下一周
→ 生成正确答案
→ 计算 right_flag
→ 根据猜对数量选择 reward 档
→ 领取答谢
→ 开启下一 phase

具体答案是否纯随机、候选是否有权重、不同正确数对应什么奖励,都先从完整客户端资源中确认,不先自行设计。


截至今晚,可以把功能状态整理为:

【已实机验证闭环】

✓ 本地 WebSocket 登录
✓ 可写 state.json
✓ 普通三叶草收割
✓ 特殊野草收获
✓ clover.harvest_resend
✓ 背包 / 桌面
✓ 普通商店
✓ 博物馆已有收藏读取

✓ 普通旅行出发
✓ 旅行计时
✓ 旅行返家
✓ BackHome 奖励
✓ 特产 / 三叶草 / 抽奖券
✓ 新旅行照片
✓ 保存照片
✓ 删除照片
✓ 相册持久化

✓ 三伙伴官方喜爱值
✓ 喂食反馈
✓ 延迟回礼
✓ 聚会邀请前半段

✓ 礼品盒照片移动
✓ 壁虎 / 刺猬 / 萤火虫回礼
✓ VisitFriend
✓ client.confirm_event

✓ 当日日历幸运礼物


【已经实现,等待更多实机验证】

△ 花圃
△ 种子
△ 肥料
△ 堆肥箱
△ 花盆成长
△ 观赏花 / 蔬果
△ 挂兜
△ 花瓶
△ 嘟嘟园艺商品


【结构已经确认,但还没闭环】

△ Story 离线玩家交互
△ 聚会 PartyResult
△ 博物馆旅行奖励
△ 旅行地图 / 城市照片
△ 家具交互
△ 旅行事件历史


【下一阶段】

○ 周末“帮助小伙伴 / 答谢” lottery
○ 旧节气食品领取
○ PartyResult 聚会返程
○ 屋内地图
○ 屋内相册入口
○ 旅行事件回看
○ 照片构图校准
○ 花圃完整实机验收
○ 家具制作与旅行商人

几个技术结论

    1. 客户端资源负责告诉我们“游戏有什么”,真实协议负责告诉我们“服务器什么时候让它发生”。两者缺一不可。
    1. 状态相似并不代表系统相同。Guest、Visit、Story、TravelFriends 看起来都是“其他小伙伴”,实际上属于四套独立的数据模型。
    1. 离线复刻不能只追求“按钮能点”。一次功能真正完成的标准应该是:状态生成 → 客户端展示 → 用户操作 → 协议确认 → 存档持久化 → 重启后仍正确。
    1. 对服务器内部看不到的随机概率,不应该假装成官方值。所有推测参数都应该集中到 policy 中,未来找到证据后可以替换,而不用重构存档。
    1. 今天最重要的成果,是普通旅行从一个静态存档查看器真正变成了一个可以再次出发、再次回来、再次产生新照片和新物品的循环。

从这个节点开始,这个项目已经不再只是“保存一份关服前的数据”,而是在逐步把原来依赖远端服务器的游戏逻辑重新搬回本地。

2026.9.12

今天的主要工作:一方面继续补齐原服务器曾经负责的周循环、日历、相册和客户端状态;另一方面第一次真正碰到了“官方功能本身依赖已经失效的网页服务”的情况——旅行地图。

最后又在旅行地图的基础上,把原来的社交漂流瓶改造成了完全本地的“旅行日记”。

这一阶段的重点已经不只是“让协议不报错”,而是开始处理三个更难的问题:

  1. 如何区分真正的官方事实和离线版自己补的策略;

  2. 如何保存已经下线、依赖网页或社交服务的旧功能;

  3. 如何保证客户端看起来成功的操作,真的写进了本地存档。


Stage14. 周末“帮助小伙伴 / 答谢”(进行中)

前一天已经发现,周末“小伙伴的疑问”使用的是一套独立的 lottery.* 协议。它和商店里的普通抽奖item.gacha完全不是一回事。

正式服留下的 lottery.load 状态中可以看到:

phase
last_phase
state
select_list
answer
right_flag
reward
extra_item
egg_num

其中保存过的 phase 171 状态里,候选列表有 15 个物品,而玩家需要从里面选 5 个。

因此这套玩法本质上更像一个跨周状态机:

本周打开玩法
↓
得到候选列表
↓
选择 5 个物品
↓
保存本周选择
↓
进入下一 phase
↓
出现 answer / right_flag
↓
根据猜中数量得到答谢
↓
确认奖励
↓
进入下一轮

这里最重要的一点,是没有因为“看起来像随机竞猜”就直接自己写一套随机算法。

我们手里至少保存了两组正式服候选池:

phase 170
phase 171

所以对于已经观测过的 phase,优先复用真实数据;对于未来没有抓到的周循环,才允许进入本地 policy。

目前 offline_policy.json 里明确保留:

candidate_count = 15
selection_count = 5

同时,正式服 phase 171 还留下了两个非常有价值的事实:

打开新一轮时得到 10 个三叶草
extra_item = item 101 × 1

这两个事实可以直接复现;但未来 phase 的候选池、答案生成和奖励细节,如果没有进一步证据,就不能假装成“官方概率”。

一次很典型的存档迁移问题

实现这一套状态机时,还遇到了一个旧状态与新代码不一致的问题。

正式服最后留下的存档已经处在 phase 171,但离线服早期曾把部分状态按 phase 170 理解,导致:

phase
last_phase
state
奖励领取状态

互相不完全匹配。

最终没有选择“重置玩法”,而是加入一次性 migration,把正式服遗留状态校准到当前离线逻辑。

日志中可以明确看到:

migrate.stage6f1.lottery_phase171

之后又发现 extra_item 不应该悄悄直接进库存。为了保持原游戏“奖励通过邮件到达”的感觉,又增加了一次修复迁移:

LOTTERY EXTRA MAIL CREATED
migrate.stage6f2.lottery_extra_mail

这和后面年终总结的分享奖励一起提醒了我:一个奖励“数值上到账”并不代表行为已经复刻正确。奖励从哪里出现、是否经过邮箱、什么时候出现红点,本身也是游戏体验的一部分。

Stage13. 日历系统收口

前一天已经跑通calendar.get_luck_reward,当天实际领取到了一个幸运礼物,并写入本地存档。

但旧节气食品仍然无法领取。这说明日历里至少存在两条不同链路:幸运日礼物≠节气料理任务。

继续梳理后,节气料理的三个任务条件可以整理成:

task1:广告 / 分享
task2:累计获得 30 三叶草
task3:完成一次抽奖

离线版对它们采用了不同处理:

广告 / 分享
→ 在线服务已经没有保存价值
→ 离线自动完成

累计获得 30 三叶草
→ 仍要求真实游戏行为

普通抽奖
→ 仍要求真实抽奖行为

这样既避免因为停服后无法播放广告导致任务永久卡死,也没有把整个任务系统改成“打开页面就自动领奖”。

旧节气奖励最终也恢复了历史领取能力。目前确认的旧奖励包括:

day 7  → item 43
day 23 → item 28

并且状态会写入 state.json,重启后不会反复领取。

Stage15.普通商店抽奖

为了让日历第三个任务能够真正完成,普通商店抽奖也一起实现。协议是item.gacha,每次消耗5 张抽奖券。

客户端静态配置中能确认奖品 rank 的权重:

White   40
Blue    25
Purple  22
Green    9
Red      3
Gold     1

这一部分和周末 lottery.* 的区别必须一直保持清楚:

item.gacha
→ 商店普通抽奖

lottery.*
→ 周末帮助小伙伴 / 下周答谢

两者 UI 都带“抽取 / 猜测 / 奖励”的感觉,但数据模型完全不同。

到这里,Stage 6G 可以真正算 CLOSED:

旧节气食品
普通 Gacha
日历任务进度
重启持久化

Stage17.聚会返程

伙伴喂食之后的聚会系统,客户端已经能确认几个关键状态:

frog.status = 3
→ 聚会中

TimerEvent.PartyGo     = 23
TimerEvent.PartyResult = 24

本地服务器也已经具备 Party 自动返程和奖励结算框架。

但是和普通旅行类似,真正的 2026 服务端概率并不都存在于客户端。当前本地 policy 中的:

duration_seconds
clover_min / clover_max
ticket_percent
page_percent
collection_percent

都明确属于离线保存策略,而不是“从客户端逆向出的官方值”。

这一点现在已经成为项目的固定规则:客户端能证明结构,抓包能证明某一次真实行为;两者都证明不了的服务端概率,只能叫 local policy。

聚会返程虽然已有本地逻辑,但我仍把它和已经多次实机闭环的普通旅行区分开,不把“代码存在”直接等同于“完全还原官方算法”。

Stage18.相册系统(进行中)

恢复存档照片

旅行系统恢复以后,一个很现实的问题出现了:未来可以继续生成照片,但过去几年已经拍到的照片才是最值得保存的东西。

正式服抓取中实际保存了 200 条历史相册记录。最初生成 compact index 后,却只有199 条.做集合对比后发现缺的是:

album id = 1312
pic_id   = 2137

于是没有继续凭肉眼翻相册,而是直接做数据完整性检查:

official rows
local rows
unique ids
missing
extra
mismatch
duplicate

修复后结果变成:

local rows        = 200
unique ids        = 200
official rows     = 200
missing           = 0
mismatch          = 0
extra             = 0
duplicate         = 0

随后在原客户端中逐页查看,200 张历史照片的完整图层都能正常显示。

这一阶段非常值得记录,因为“有 200 个缩略图”远远不够。

照片真正需要保存的是:

pic_id
+
layers
+
每层 resource
+
坐标
+
相册 id
+
时间与其他元数据

如果只保存一个 pic_id,未来官方服务器关闭后,某些历史照片的随机人物、伙伴、前景和位置就可能永远丢失。

相册回收站与 Android 系统相册

历史照片恢复之后,又继续把原客户端的相册操作补齐:

保存旅行照片
删除旅行照片
进入回收站
恢复照片
真正删除
最近 6 张回收记录
重启持久化

还确认了一条容易遗漏的规则:旅行回来后没有选择保存的照片也应该进入和手动删除照片相同的回收站体系。

一开始点击“保存到系统相册”没有任何效果。

这并不是 Android 权限本身的问题,而是因为离线版启用了客户端自带的TestChannel.TestChannel 继承的基础实现中:save_texture_to_album实际上是空的。正式渠道使用的 Ejoy Native bridge 才真正把纹理保存到 Android 相册。

所以最终不是自己重新写一套 Android 文件导出,而是让离线 TestChannel 重新调用原客户端已经存在的 Native 能力。

修复后:

游戏内照片
→ Egret texture
→ Ejoy Native bridge
→ Android 系统相册

成功工作。

这也是一个很有代表性的经验:离线化时不要默认“Test 模式 = 功能完整”。测试渠道经常会绕开登录 SDK,同时也可能把支付、分享、系统相册等原生能力一起空实现。

Stage19.百科、旅行百科与客户端设置持久化

Stage 6I.1(备注:工作及代码编号体系,本文为方便阅读有所修改) 主要处理了几类看起来不大、但重启后很容易露馅的状态,包括encyclopedia.set_show_sub``encytravel.set_show_sub.以前这些操作在当前会话里看起来正常,但服务器没有真正保存,所以重启后会恢复旧状态。

修复后:

客户端修改
→ 本地服务器收到协议
→ 修改对应 push
→ state.json
→ 下次 client.load_all_info replay

能够完整闭环。

同时还修正了旅行百科中的 long_id 映射,以及 Item 静态数据在:dict``list两种格式下的兼容问题。这类 bug 没有旅行和相册那么显眼,却很能检验本地服务器到底是“能演示”,还是已经开始变成一份真正可长期使用的存档。

Stage6.旅行系统修复

普通旅行已经能出发和返家之后,又发现一个体验上的缺口:青蛙确实离家了,但原版应该出现的“xxx出去旅行了”并没有弹出来。原因是客户端并不只观察frog.status,源码里真正负责这条提示的是TimerEvent.Type.GoTravel.也就是说,服务器需要同时表达两个层次:

状态:
frog.status = 1

事件:
GoTravel

前者决定“青蛙现在在不在家”,后者负责“刚刚发生了什么”。

这和之前 BackHome、VisitFriend 的逻辑是一致的。

从这之后,本地服务器对 TimerEvent 的理解也更清楚了:

GoTravel       = 1
BackHome       = 2
Picture        = 3
Gift           = 7
VisitFriend    = 16
FurniturePut   = 22
PartyGo        = 23
PartyResult    = 24

保存一个游戏,不能只保存最终 state;很多“生活感”其实来自事件流。

Stage20.家具系统(更换家具)

原游戏中,青蛙外出以后玩家可以整理房间家具。离线版一度出现青蛙已经外出, 但换家具入口仍然打不开的情况。服务端状态没有问题,真正卡住的是客户端自己的显示条件FurnitureModel.isOpen(),它还受商店 start_time 一类门槛影响。

最后只在本地 TestChannel 下放宽入口:

decorate_open
+
frog 不在家
→ 允许进入换家具

而服务端仍然保留frog 在家时拒绝玩家手动替换,避免为了修 UI 把原游戏规则一起破坏。

这个修复也体现了一个很常见的逆向误区:按钮不出现,不一定是协议没实现;有时服务器早就准备好了,真正拦住它的是客户端本地的 feature gate。

Stage21.2023 年终总结

这一阶段还决定保留一个很有纪念意义的入口:2023 年终总结。

原来的活动有时间限制,但对于个人离线保存版本,我希望它可以永久打开,也可以作为“现在连接的是本地服”的辨识标志。处理了三个问题:入口长期开放、永久红点消除、分享奖励可领取一次。红点的原因来自AnnualReviewModel.updateRedot(),当is_share == false时会一直显示红点。

本地版关闭了这条无意义的长期提醒,并实现annual.share第一次分享后发放item 57(庆典蛋糕)且只能领取一次。

这里也留下了一个以后必须遵守的修正:这次蛋糕已经直接进入库存,作为历史结果不再回滚;但原游戏更合理的分享奖励流程应该是:

分享成功
→ 奖励邮件
→ 邮箱红点
→ mail.open
→ 进入库存

以后恢复其他历史活动时,不再跳过邮箱链路。

Stage22.旅行地图

一个回归 bug 揭开了真正的 TravelMap

开始时只是发现一个小问题:房间里点击“地图”家具→ 居然进入旅行百科。继续查源码后确认,原版正确调用链是:

MainInView.on_enterTravelMap_tap
↓
ActivityModel.getActivity("travelmap")
↓
TravelMapController

它根本不是 EncyTravelViewControl。

继续向下追以后,才发现 TravelMap 并不是普通的 Egret 页面,而是一套外部 H5。

原页面地址被恢复为:

https://act.lingxigames.com/prism-kpf75rez

并找到了对应的:

main.a6253edc.js
main.ba4cb5db.css

原 H5 使用高德地图,并通过 Native WebView bridge 从游戏客户端读取:

getPictureList
getPictureData
bbsDetail
moment_token

这解释了为什么“地图”在原客户端里看起来和其他页面很不一样。

35 个真实目的地

从原 H5 中还提取出了全部地图点。

一共 35 个:

北京、天津、宁夏、青岛、成都、云南、拉萨、上海、苏州、杭州、
海南、桂林、广州、香港、垦丁、十分、哈尔滨、辽宁、张家界、
武汉、洛阳、安徽、西安、酒泉、重庆、贵州、福建、江西、山西、
吉林、澳门、青海、河北、内蒙古、新疆

这些点是原 TravelMap H5 自己保存的经纬度。

原高德地图配置也找到了:

zoom = 4
zooms = [4, 8]
center = [111.380283, 32.743444]

放弃额外 WebView 服务,改成原生静态地图

第一版思路是尽量忠实恢复 H5:

游戏
→ WebView
→ 本地 HTTP server
→ 原 TravelMap HTML / JS / CSS

甚至一度额外开了8081 → 52353来提供 H5。

但这个方案的问题也很明显:

需要第二个本地服务
需要 WebView 网络栈
需要 bridge
还依赖原 H5 对地图 SDK 的假设

对于一个希望多年以后仍然可以直接启动的保存版本,这反而增加了脆弱点。最后决定保存原 H5 作为历史证据,但实际离线版本使用 Egret 原生静态地图。于是 TravelMap 被重新画成游戏内部页面。

地图几何

中国行政区边界使用保存下来的 GeoJSON,在补丁阶段转换为可以直接写进客户端的线段数据。

最初几个版本先后出现:

省份填充形成大块灰色区域
地图只能横向拖
经纬度缩放不一致
城市点击热区出现黑块
文字挡住城市点

最终在 v6/v7 中改成:

1900 × 1200 的大地图画布
Mercator 投影
只画行政区 stroke,不填充 polygon
横向 + 纵向 EUI Scroller
透明点击热区
标签碰撞检测
标签主动避让所有城市点

城市照片与地图点之间的映射也没有硬编码照片编号,而是沿用游戏自己的静态数据链:

TravelModel picture
↓
pic_id
↓
PictureDB
↓
place
↓
GoalNumberDB
↓
tag
↓
g_beijing / g_qinghai / ...

因此以后新旅行产生照片时,只要它属于已有的 35 个目的地,重新打开地图就会自动更新:

已打卡城市数
城市点状态
城市照片数量
城市相册

不需要重新改地图代码。

从地图进入城市相册再回来

早期还有一个页面栈问题:地图 → 城市相册 → 返回会把地图一起销毁,最终回到房间。修复后使用core.RemoveViewType.Retain保留 TravelMap 页面,再叠加相册。这样返回才符合原来的交互。

顶部打卡进度

最后又根据原版截图恢复了顶部的城市打卡进度提示,包括蛙蛙已打卡多少个城市、距离 35 城成就还差多少、完成后显示称号。

到这里,原本依赖线上 H5、高德地图和社交后端的 TravelMap,已经变成完全由本地游戏资源和相册状态驱动的静态页面。

原 H5 仍然被保存下来,但运行时不再依赖它。

Stage23.从“漂流瓶”改造成个人旅行日记

TravelMap 原本还连着一个已经明显不适合离线保存的系统:漂流瓶。

原版漂流瓶本质上是社交功能。收到 Drift 邮件后,客户端会显示mail_bottle_png,并根据发件人的城市打开 TravelMap 中的 bbsDetail 页面。

原 H5 里还能看到:

收到的漂流瓶
城市留言
发送
删除
点赞 / 评论一类社交信息

在停服后的个人离线版中,我决定把这个位置重新设计成旅行日记,原因在本文开头的小作文中也有所提及,发送的第一个也是唯一一个漂流瓶虽然没有人点赞,但是对我来说意义非凡。

保留它最适合个人保存的部分:

照片
文字
城市
日期

去掉:

其他玩家昵称
IP
点赞
评论
分享
在线发送

日记数据没有新造服务器协议,而是复用 client settings

旅行日记首先要解决的问题不是 UI,而是“放在哪里”。

如果专门增加:

diary.load
diary.save
diary.delete

虽然也能做,但会让本地服务器多一整套原游戏不存在的协议。

最终选择复用客户端已经有的:

UserModel.clientSettings

增加:

{
  "travelDiary": {
    "version": 1,
    "next_id": 3,
    "entries": [
      {
        "id": 1,
        "picture_id": 168,
        "city": "g_beijing",
        "city_name": "北京",
        "text": "...",
        "created_at": 1712203200,
        "updated_at": 1712203200
      }
    ]
  }
}

这样日记跟其他客户端设置一起走现成的:

client.set_client

服务器无需增加新的业务协议。

照片也不复制一份图片,只保存:

picture_id

显示时再从 TravelModel 找到真正的相册记录。

城市信息同样不是手填:

album picture
→ pic_id
→ PictureDB.place
→ GoalNumberDB
→ city tag / city name

这让日记和旅行地图使用同一套地点语义。

第一个崩溃:有 layers 还不等于能直接拿 texture

旅行日记第一版一点击“新建”,游戏直接重启。

服务器日志没有收到新的业务请求,所以很快就可以判断:不是 server.py 崩了,不是 WebSocket 断言,而是客户端渲染路径出了问题。

继续对比原 TravelMap / 明信片代码,才发现原客户端完整的照片渲染流程是:

getPictureById
↓
如果 layers == null
    checkPictureByIds
↓
重新取 picture
↓
Tabikaeru.loadPicture(picture)
↓
Tabikaeru.getPictureTexture(picture)

第一版只做了:

checkPictureByIds
→ getPictureTexture

少了:

Tabikaeru.loadPicture

于是即使服务端已经通过:

album.load_by_id_list

返回完整图层,客户端资源仍然没有完成加载。

补上这一层之后,照片可以正常显示。

这个问题很好地说明:“服务器已经返回数据”和“客户端已经能渲染资源”是两件事。数据层、资源加载层、纹理生成层缺一不可。

第二个坑:eui.TextInput 看得见但点不进去

第一版日记编辑器使用动态创建的eui.TextInput,输入框能画出来,但在当前客户端运行环境里点击后没有键盘,也无法输入文字。

最后改成更底层的:

var input = new egret.TextField();
input.type = egret.TextFieldType.INPUT;
input.multiline = true;
input.wordWrap = true;
input.maxChars = 140;

并单独放一个 hint Label 模拟 placeholder。

这样 Android 输入法终于能正常弹出,也可以输入中文。

这也是 Egret 老项目里一个很典型的问题:skin 驱动的 EUI 控件在动态构造时,不一定拥有和 EXML 页面中相同的完整行为;需要输入能力时,原生 egret.TextFieldType.INPUT 反而更可靠。

日记页面完成的功能

目前旅行日记已经支持:

新建
编辑
删除
上一篇
下一篇
切换照片
140 字文字
自动显示城市
自动显示日期
重启后恢复

同时做了几处 UI 微调:

地图右上角“日记”入口
关闭按钮位置
两按钮整体居中
编辑页原生输入框
照片缺失提示

为了验证日记是否可以承载历史记录,还手工向本地存档加入了一篇指定:

日期 = 2024-04-04
照片 = 相册 #168(北京长城)
城市 = 北京

的旧日记,原客户端能够正常读取和显示。

这说明这套结构既能记录今后的旅行,也能用于整理过去已经存在的相册。

最后一个持久化 bug:UI 说“已保存”,服务器却什么都没收到

旅行日记最隐蔽的问题出现在“编辑”。

第一次新建日记可以保存,但再次进入编辑、修改文字以后当前页面看起来改好了,关闭再打开也可能暂时还在,但重启游戏以后修改消失

最开始怀疑是 state.json replay 覆盖了新内容。但服务器日志给出了最直接的证据:正常保存应该出现:

RECV cmd=client.set_client session=None
STATE SAVED reason=client.set_client

而出问题时,这两行根本没有出现。也就是说不是服务器保存错了,而是客户端根本没发保存请求。

根因:对象引用与 setClientSettings

TravelDiaryStore.get() 返回的是clientSettings.travelDiary本身。编辑时直接修改这个对象,然后再调用setClientSettings("travelDiary", t)。

客户端内部有类似:

this.clientSettings[key] != value

的变化判断。但是此时this.clientSettings.travelDiary和t本来就是同一个对象。因此哪怕里面的 text 已经变了,引用比较仍然认为还是同一个值,于是没有发送 client.set_client。

最终修复

最后不再依赖这一层变化检测。

保存时先深拷贝:

var copy = JSON.parse(JSON.stringify(t));

再明确更新:

userModel.clientSettings.travelDiary = copy;

然后直接走原客户端本来就使用的协议:

core.SocketManage.getInstance().send(
    "client_set_client",
    null,
    JSON.stringify(userModel.clientSettings)
);

实机日志终于出现:

RECV cmd=client.set_client session=None
STATE SAVED reason=client.set_client
已持久化 client.set_client

重新启动游戏以后,刚才修改的日记仍然存在。

这次可以确认:

UI 编辑
→ client_set_client
→ state.json
→ 重新登录 replay
→ 修改仍存在

真正闭环。

删除为什么也一起修好了

日记的删除代码并没有单独写另一套持久化逻辑。

它的流程是:

确认删除
→ 从 entries 中 splice
→ TravelDiaryStore.save()
→ 调整 currentIndex
→ refresh()

而现在 TravelDiaryStore.save() 已经统一改成强制发送 client_set_client。

因此:

新建
编辑
删除

最终都会经过同一条持久化出口。

从代码路径看,删除不会再存在之前那种“当前会话看起来删了、重启又回来”的对象引用问题。

漂流瓶的处理

为了避免未来又出现在线社交漂流瓶,客户端还加入了两层拦截。

已有 Drift 邮件加载时:

revice_mails
→ 找到 Mail.EvtId.Drift
→ 不加入普通邮件展示
→ 自动 mail_open

运行过程中收到新的 Drift 时:

notify_new_mail
→ 如果是 Drift
→ 直接处理,不显示社交漂流瓶

因此在当前离线客户端中,用户看到的是个人“旅行日记”,而不是一个伪造的在线社区。

不过这里仍然留一个以后可以单独做的审计项:目前已经在客户端消费/隐藏 Drift;后续还可以继续检查本地服务器是否存在任何主动生成 Drift 的旧逻辑,从源头完全删除。

这不影响旅行日记 CRUD 本身的闭环。


这一阶段最值得记住的技术经验

1.“能显示”不是完成,“重启还在”才是完成

这两天最明显的例子就是旅行日记。界面已经显示旅行日记已保存并不意味着服务器真的保存了。最终确认问题靠的不是 UI,而是:

客户端动作
↓
WebSocket 日志
↓
STATE SAVED
↓
重启
↓
重新 replay

以后每一个有写操作的功能,都继续用这一标准验收:

状态生成
→ 客户端显示
→ 用户操作
→ 协议确认
→ 状态落盘
→ 重启仍正确

2.区分三种“事实”

整个项目现在越来越依赖一个简单但非常重要的分类。

  • 第一类:客户端 / 静态资源明确写着的

例如:

Gacha rank 权重
35 个 TravelMap 城市坐标
TimerEvent 类型
Character.taste
Picture.place

这类可以视为高置信度原版事实。

  • 第二类:正式服抓包实际观测到的

例如:

phase 171 的 select_list
打开 phase 171 得到 10 三叶草
某一次旅行返家获得 75 三叶草、2 张券
正式相册 200 条

这类证明的是:

“官方服务器至少真实发生过这一次”

但不应该从单次样本反推出完整概率。

  • 第三类:服务器内部算法看不到,只能为了离线运行补上

例如:

未来周末候选池
聚会奖励概率
旅行部分权重
花圃成长参数

全部集中放进:

offline_policy.json

并明确写注释:

local preservation policy
not verified official server value

这样以后如果找到新的正式证据,只需要替换 policy,而不需要推翻整个存档格式。

4.原功能依赖在线服务时,不一定要机械复制架构

TravelMap 是最典型的例子。

原版架构是:

Egret
→ Native WebView
→ 线上 H5
→ AMap
→ 社交后端

如果完全照搬,就必须长期维护:

本地 HTTP server
WebView 兼容性
地图 SDK
JS bridge
H5 构建产物

但地图真正值得保存的是:

35 个地点
已去过哪些城市
每个城市有哪些照片
点击城市进入相册
原版大致视觉语义

所以最终保留:

原 H5 / JS / CSS 作为证据与归档

运行时则改成:

Egret 原生静态地图

这比“为了架构一样而继续依赖一堆已经没有意义的在线组件”更符合长期保存的目标。

5.给压缩 JS 打补丁时,必须做结构化和幂等

这两天还有一个纯工程上的教训。

最早一些 patch 脚本会要求:

必须恰好存在上一版 marker
必须恰好匹配某一整段压缩字符串

结果只要前面做过一次很小的 UI 调整,下一份 patch 就可能直接报:

expected exactly one ... found 0

文件实际上没有坏,只是补丁写得太脆弱。

后面统一改成更稳的策略:

1. 用稳定函数边界定位
2. marker 只做辅助诊断
3. 不强依赖“必须从上一小版本升级”
4. patch 重复运行应幂等
5. 修改前自动备份
6. 修改后重新读取实际文件验证关键特征

例如日记持久化最后一版,不再要求必须先是 A5,而是寻找:

TravelDiaryStore
↓
e.save=function(...)
↓
e.pictureMeta=function

只替换这一段。

对于长期维护一个已经被多次 patch 的百万字节压缩 JS,这种方式可靠得多。


已经完成或基本收口的部分包括:

✓ 周末 lottery 当前已知 phase 状态迁移与奖励处理
✓ 旧节气食品
✓ 普通 Gacha
✓ 日历任务进度与持久化

✓ 历史相册 200 / 200 full layers
✓ 照片保存 / 删除 / 回收站 / 恢复
✓ Android 系统相册导出

✓ 百科与旅行百科 show_sub 持久化
✓ GoTravel 出发事件
✓ 青蛙外出时家具替换
✓ 2023 年终总结长期保留
✓ annual.share 一次性庆典蛋糕

✓ TravelMap 原 H5 / bridge / 35 城市结构逆向
✓ 房间地图入口恢复
✓ 完全本地 Egret 静态旅行地图
✓ 城市照片自动统计与城市相册
✓ 双向拖动、标签避让、页面 Retain、打卡进度

✓ 本地旅行日记
✓ 新建 / 编辑 / 删除 / 翻页
✓ 照片完整加载
✓ Android 中文输入
✓ client.set_client 真正持久化
✓ 历史日期 + 指定旧照片日记

目前仍值得继续做的主要部分有:

○ 新旅行照片的构图校准
  - 过路照仍可能偏移
  - 风景照也有只剩背景的情况

○ animpicture 动态照片完整状态机

○ 花圃完整实机验收

○ Story / Visit 等社交系统最终离线化

○ 漂流瓶服务端生成源审计

○ 博物馆、手工、旅行商人

○ 更多历史限时活动常驻化

○ 聚会返程继续补更多实机证据

○ 旅行目的地 / route 的静态配置进一步还原

现在项目已经越来越不像“做一个假的服务器让旧 APK 打开”,而更像是在一点点重建一个个人游戏保存环境:

原客户端
+
原静态资源
+
自己的历史存档
+
保存下来的正式服事实
+
明确标记的本地替代策略

对我来说,这也是这个项目现在最重要的边界:不是重新设计《旅行青蛙》,而是在原服务消失之后,尽量让过去的存档、原来的规则和以后还能继续发生的新旅行共存在同一个本地世界里。

2026.9.13

看到小红书上面已经有人把离线版做出来了,感觉自己确实还是开发效率慢了点。不过第一次做游戏相关的内容,也当是积累经验了,而且也可以加入自己喜欢的功能和设置。顺便把工作区从网页版ChatGPT换成了Codex(前面两天一直在当命令行纺织工……),用luna试试看。

二编:luna实在太笨了,只是因为便宜才用。气死我了。


Stage18.相册系统

今天比较重要的一步,是把静态照片的构图方式改了一层。

原来的照片生成逻辑能够生成背景、青蛙和尾部图层,但角色位置有一部分是根据尺寸估算出来的。背景尺寸、角色尺寸或缩放比例稍有区别,最终照片里的青蛙就可能偏高、偏低,或者和官方截图中的位置对不上。

现在 server.py 会优先检查官方静态照片资源。如果背景和角色资源都存在,并且尺寸符合官方照片的 500×350 画布,就直接使用官方资源中记录的坐标生成照片图层,不再重新猜测角色应该放在哪里。

目前已经确认可以使用这套精确构图的场景共有 13 组:

哈尔滨       g_haerbin4
内蒙古       g_neimenggu1
内蒙古       g_neimenggu2
内蒙古       g_neimenggu3
新疆         g_xinjiang1
新疆         g_xinjiang2
新疆         g_xinjiang3
重庆         g_chongqing3
青岛         g_qingdao3
贵州         g_guizhou7
西安         g_xian4
郑州         g_zhengzhou1
北京         g_beijing6

照片图层现在按照下面的优先级处理:

官方完整图层模板
    ↓
官方静态背景 + 官方角色资源 + 官方坐标
    ↓
静态 Picture/resources 资源回退
    ↓
旧的角色尺寸估算逻辑

这样处理的好处是,能直接使用官方资源的场景不会再受到本地估算误差影响;没有完整官方资源的照片仍然可以保留原来的生成能力,不会因为某一张图片缺失而影响整个旅行回程流程。

目前仍有 7 个官方场景没有在现有资源中找到完整背景:

g_xianggang3
g_xianggang5
g_changbaishan1
g_changbaishan2
g_changbaishan3
g_hainan3
g_hainan4

攻略中有对应图片,直接替换掉了。目前所有照片都有了。

2026.9.14

Stage19.旅行路线、照片与目的地重新校准

这一天主要把“带什么出门”和“会去哪里、带回什么照片”之间的关系重新梳理了一遍。

此前本地旅行已经能正常出发、返程、带回照片和特产,但不少目的地更像是一个泛化的随机选择:食物、幸运物、博物馆门票、工具和照片之间还没有尽可能贴近客户端静态数据的对应关系。

现在旅行服务端把这些因素统一接入地点选择:

食物
幸运物
工具
博物馆门票
    ↓
旅行目的地
    ↓
目的地照片 / 特产 / 收藏品 / 旅行笔记

其中博物馆门票属于最明确的一类定向条件,会优先指向对应博物馆所在地;普通幸运物仍然主要体现方向或目的地偏置,而不是凭空制造一个完全确定的官方路线。

凤梨酥也补进了地图。由于现有资源和攻略不足以证明它只对应唯一地点,本地离线版采用:

109 凤梨酥
→ 垦丁 / 十分
→ 各 50%

这只是为了让离线旅行保留两种合理去向的本地代表映射,不是官方服务端概率。

Stage20.照片资源与构图补全

前面已经恢复了照片背景、角色、前景和图层数据,但真正逐张对照后发现,部分静态场景缺少本地官方完整图层,导致照片可能只有背景、角色位置不准,或者退回到比较粗糙的估算构图。

这次继续整理了保存下来的画集资源、客户端资源和攻略截图。对于已经能确认地点、人物和场景对应关系的照片,直接接入实际图片和对应位置,不再把它们回退成通用背景。

目前旅行照片会优先使用:

官方完整图层模板
    ↓
已确认的静态场景图片与坐标
    ↓
原客户端 Picture / resources 图层
    ↓
普通构图回退

照片不再只是“能生成一张图”,而是尽量恢复成每个地点原本应当出现的场景。普通过路照、目标照、工具照、特殊照也重新进入同一套生成与返程链路。

Stage21.旅行带回照片与动态照片

之后又继续检查了“照片能否获得”本身,而不只检查照片能否显示。

普通旅行现在会按路线生成应有的过路照和地点照片;庆典蛋糕、节气食品、特殊食物、博物馆票与纪念品也接入各自对应的照片池。周年庆蛋糕保留三张不同照片的随机返回方式。

动态照片也补齐了从材料到成品的流程:

照片
+ 显影液
+ 制作进度
    ↓
动态照片
    ↓
相册显示 / 位置图层 / 存档保存

显影液、照片放入与取出、制作过程、成品生成和重新登录后的状态都由本地存档维护。这样动态照片不再只是页面上的历史展示,而是可以继续制作的系统。

2026.9.15

这一天从照片系统继续向外扩展,重点变成了“原客户端每个入口是否真的能完成它对应的动作”。

Stage22.旅行商人、伴蛙前行与相机时刻

旅行商人、伴蛙前行、Camera / Moment 等入口原本能够在客户端显示,但部分操作没有本地服务器接收方,点击后会停在灰色状态,或者只完成客户端临时展示。

现在补上了这些入口所需要的本地协议和状态:

旅行商人商品
→ 购买 / 扣除三叶草 / 入库

伴蛙前行任务
→ 读取进度
→ 领取奖励
→ 保存已领取状态

Camera / Moment
→ 识别庭院当前动作
→ 解锁对应时刻
→ 写入本地状态

伴蛙前行里“旅行指南”“笔记指南”等条目也不再只是灰色入口;客户端已有的新手引导页面可以正常打开,任务进度和奖励会跟随存档保存。

Stage23.旅行中的伙伴与庭院状态

旅行邂逅 Story 保留官方静态故事内容,但不再把已经失效的其他玩家身份带进离线版。

离线版现在使用统一的匿名旅途伙伴身份:

官方故事文本
+ 官方故事编号
+ 本地赠礼 / 回赠状态
- 真实昵称
- 真实玩家标识
- 在线社交关系

已有 25 条故事的存档不会继续获得;未集齐时才会按本地规则补齐缺失故事。

庭院中的青蛙也不再一直固定在一个位置。它会根据白天、傍晚和夜晚切换原客户端已有的动作与位置;重新登录时会刷新,但不会破坏旅行中的青蛙状态。

Stage24.Cont-照片系统的一个边界

这两天最明显的感受是,照片系统不能只靠“看起来差不多”。

一张照片是否真正恢复,需要同时满足:

旅行条件正确
→ 能被带回
→ 有对应图片和图层
→ 客户端位置正确
→ 相册可以保存
→ 重启后仍在

因此后续继续开发时,也会优先沿着这条链检查,而不是只给缺失场景补一张看起来相似的背景图。

到 9 月 14 日,离线版已经不只是保存旧相册和旧存档:新的旅行能够根据携带物品前往不同地点,带回对应照片、特产和收藏;照片既能进入普通相册,也能参与动态照片制作;原本依赖在线玩家的故事则被保留为个人旅行记录。

Stage26.日历、称号与个人资料的长期状态

日历原先更接近一个固定年份的展示。现在日期和星期会跟随系统年份重新排布,保留原有节日、物品获取与 UI 结构;年份数字则继续使用客户端已有的手写数字资源,而不是浏览器默认字体。

这部分也顺带重新检查了个人资料的保存链:

青蛙名字 / 称号修改
→ client.set_client
→ sync_pushes[client.load_role]
→ state.json
→ 重启后恢复

称号的条件、解锁提示和更换状态也继续由本地存档维护,不再只停留在当前页面的临时显示。

2026.9.16

Stage27.BugFix-相册、礼品盒与回收站

相册里一张照片的“位置”不是单纯的客户端筛选条件。它会在:

相册
礼品盒
回收站

三个列表之间移动,而且每次移动都必须保持照片本体、元数据和数量一致。

现在“放入礼品盒”“放回相册”“移入回收站”都走同一份服务端状态转换;照片不会再因为从礼品盒操作而脱离三个列表。回收站恢复、彻底删除和重新登录后的列表状态也与这套状态一起保存。

Story 的匿名旅伴文案统一为“旅途中的朋友”。故事不是一次性被全部塞进存档:已经全收集时不再重复获得;换一个尚未集齐故事的存档时,仍会继续补齐缺失条目。

Stage28.花瓶、百科与庭院动作

花瓶中的植物现在有明确的生命周期:

插入
→ 新鲜状态
→ 枯萎显示
→ 消失

不同植物共享客户端原有的花瓶展示方式,但状态和时间由本地存档记录。青蛙外出时可以继续插入、丢弃或替换花瓶植物;青蛙在家时则保留原本受限的交互语义。

百科也不再只是历史静态页。一次旅行、一次获得或一次重新放入物品箱,都可以让此前未收录的物品进入对应百科记录。

庭院动作同时重新按条件收束:砍竹子、制作、阅读、休息等动作会检查工作台是否真的有制作任务、青蛙是否在家以及当前时间段。这样制作动作不再在空工作台旁边无条件出现。

Stage29.花圃与手工品的完整流程

花圃现在覆盖了从播种到回收的连续状态:

买种子 / 旅友播种
→ 生长进度
→ 堆肥提升土地肥力
→ 花朵与作物收获
→ 花瓶或库存中的后续使用

堆肥放入后会立即消耗物品,不能再把已经投入的材料拿回库存;堆肥箱外观、土地肥力档位和花圃中对应的花朵显示都会随状态更新。挂兜则按独立容量累计额外三叶草,领取时再进入钱包。

手工品也恢复成一条实际制作链:

朽木 / 旅行材料
→ 三种护符木片
→ 三拼护符
→ 木质护符、祈愿物、印章

不同护符会在旅行结算时应用各自的本地效果;制作工具、材料消耗、制作时间和屋内制作动作一起保存。祈愿物与印章也不再只是图鉴中的静态图片。

Stage30.家具制作与特殊材料

普通家具现在补齐到全部 14 个正式主题、380 件:

素风格
石作 / 博古 / 露营 / 雨林 / 夏凉 / 海洋 / 秋收
三套迎春
三套庆典

素风格不需要主题主材料:放好工具、持有图纸和普通材料即可制作。其余主题仍由对应特殊材料决定;一份材料对应多种主题时,工作台会从该材料允许的主题池随机选择。

织彩带现在对应三套庆典:

庆典·欢聚
庆典·田园
庆典·彩陶

三类特殊家具则使用正式成品 ID 列表,而不是只保留最早几款:

植物果实 → 不倒翁 36 个成品变体
手工合页 → 堆肥箱 15 个可制作款
彩色编绳 → 挂兜 6 个可制作款

不倒翁会优先补齐当前未拥有的成品,收集完成后才回到可重复制作的随机池。每次制作都会经历:工作台锁定、普通材料消耗、青蛙制作动作、成品写入家具收藏、重新登录后恢复。

2026.9.17

Stage31-fix.旅行明信片概率调整

旅行结算继续从“能出发”推进到“带什么会带回什么”。食物、幸运物、博物馆票、工具和手工护符共同影响目的地、照片、特产、收藏品与旅行时长。

博物馆门票保持明确的定向效果;路标、星星灯、猫猫信纸、幸运花与其他幸运物则按照各自的地点偏置或照片池生效。节气食品、月饼、周年庆蛋糕和其他特殊食物也都连接到对应的照片组,不再只作为普通食物消耗。

旅行明信片恢复为通常随机带回;彩叶幸运草是例外,会优先补齐尚未获得的照片。这样既保留了原本旅行的随机性,也让彩草仍有明确的收集价值。

Stage32-fix.嘟嘟商人、聚会与夕拾家具

嘟嘟不再常驻庭院。它会在中午到午夜的窗口内选择一段随机停留时间,到点离开;小摊商品也改为有限格位,而不是把全部物品同时堆进商店。

设计图、联动家具和幸运物都补进了各自的购买规则:

未拥有的设计图
→ 可购买一次

已经拥有的设计图
→ 不再上架

重复触发的限量家具
→ 保留数量并返还对应三叶草

森之国度联动家具、古琴展博古家具、夕拾相关家具与常规种子、肥料一起进入嘟嘟可用商品池。旅友聚会返程也补回了三叶草、抽奖券、友情绘本新页和未获得夕拾家具等结果;已经拥有的图鉴页和家具不会重复作为新收集奖励出现。

Stage33-fix.庭院、花圃与青蛙动作条件

花圃和堆肥不再只是数值面板。投入堆肥箱后的材料会先经历可取回阶段,随后锁定并开始发酵;锁定材料在花圃列表中显示为不可点击,完成吸收后才转化为土地肥力。不同肥力档位、花朵显示、播种、好友播种、收获和挂兜领取都使用同一份庭院状态。

家具工作台也重新与青蛙动作绑定:持有材料和工具并不意味着立刻制作,青蛙需要在家,等待随机开工时间,再经过一段随机制作时间才会完成家具。砍竹子、制作、用餐、阅读、睡眠和夏季吃西瓜等动作同样增加了时间、季节、餐桌食物或工作台任务等条件;青蛙外出时不会再在庭院错误地执行室内制作动作。

Stage34.家具、手工品与限时称号

家具系统继续补齐特殊材料的产出和全部可制作款式。正丹纸等可对应多种主题的材料会在允许的正式主题中随机选择;彩带可以生成不同庆典家具;植物果实、手工合页和彩色编绳分别覆盖不倒翁、堆肥箱和挂兜的全部款式。

护符木片、木质护符、祈愿物和印章也保留材料获得、工具制作、制作动作与旅行效果的完整链路。称号方面,原活动期才能看到的称号改为离线版常驻内容:已满足条件会解锁、弹出提示并可更换;“蛙选之人”等限时称号有稳定的本地获得状态,不再随着活动结束而消失。

- 蛙选之人:完成周年蛋糕
- 明星小蛙:集齐四座博物馆
- 蛙界艺术家:首次完成家具制作

Stage35.博物馆大冒险、中秋贺卡、春节贺卡、扭蛋机与周年庆蛋糕等限时活动

原本依赖活动服务的页面已经改成离线版可长期进入的内容:博物馆大冒险、中秋贺卡、春节贺卡、扭蛋机与周年庆蛋糕都各自保留活动本身的状态机、奖励和每日进度。

这些入口不再只靠客户端临时显示,而是统一写入本地存档:

进入活动
→ 读取当天状态
→ 完成任务 / 消耗材料 / 领取奖励
→ 更新三叶草、道具或活动货币
→ 重启后保持结果

博物馆大冒险的探索次数、每日登录和分享增加次数会按活动进度保存;扭蛋机则保留每日两项任务、待领取扭蛋币、快速完成、抽取记录和阶段奖励。蛋糕活动中的分享、制作、领取材料、分层制作、问答和点蜡烛继续沿用原有的顺序,而不是一次性跳到完成状态。

节日贺卡的购买与刷新

中秋和春节贺卡都补回了可持续使用的材料循环。

中秋背景的“首次免费”现在是一份独立的购买历史,不再因为背景已经被放到贺卡上而重新变成免费;第一次购买后,后续背景购买固定消耗 100 三叶草。贴纸、背景、当前贺卡、已寄出记录和寄出后的奖励都使用同一份本地数据。

春节贺卡的小铺则按本地日期刷新每日福袋额度:

新的自然日
→ 小铺购买次数归零
→ 重新显示可购买贴纸
→ 当天首包免费,后续福袋消耗三叶草

这样第二天不会停留在前一天的灰色售罄状态,分享、兑换和任务贴纸的数量也仍会在卡片制作与礼盒奖励中连续保存。

分享任务与活动奖励同步

活动中的分享按钮现在会将实际分享动作回传到对应任务,而不是只关闭客户端弹窗。

分享
→ 对应活动任务完成
→ 待领取活动货币增加
→ 页面刷新后领取
→ 抽取或兑换继续使用

扭蛋机的分享任务、每日任务列表和待领取扭蛋币使用同一份活动状态。完成任务后,扭蛋币会先进入待领取队列,打开扭蛋机即可自动收取并用于扭蛋;任务、扭蛋币和抽取记录都会随本地存档保存。

活动入口与本地服务状态

这两天还逐个补齐了活动入口与服务端状态之间的连接。大冒险转盘、春节和中秋贺卡小铺、扭蛋机任务、周年蛋糕、许愿池和新手引导都不再依赖已经下线的活动服务。

每个按钮都会走到明确的本地状态转换,而不是只在客户端播放一次效果:

按钮操作
→ 本地服务器确认条件
→ 扣除或发放物品
→ 推送页面所需数据
→ state.json 保存

因此重新启动游戏后,旅行、照片、庭院、家具、称号和活动页面都会从同一份个人存档继续运行。

Stage32.许愿池与空白存档的新手流程

许愿池补成了可以从零开始使用的离线入口。初始许愿币、可许愿的庭院物品、获得次数和领取结果都放在活动状态中;每个可获得物品保留自己的数量上限,获得后会直接进入家具或庭院库存。

全新空白存档的新手流程也补上了开门、步骤确认和完成奖励:

打开房门
→ 完成新手引导步骤
→ 领取三叶草、抽奖券与起始物品
→ 写入本地角色状态

新手奖励只能领取一次,已有存档不会因为重新进入游戏而重复得到。

Stage36-fix.过路照构图与旅行照片校准

普通照片不是一张固定背景。它由远景、道路或水面、近景、青蛙、随身工具和可能出现的旅伴组成;其中同一地点还会根据工具、同行对象和随机组选择不同构图。

这次继续按客户端图层、正式图片尺寸和攻略截图校准普通过路照。海边、道路、山地和城镇等场景重新按正确的前后层关系放置,不再把道路、沙滩和水面拆到错误的上下区域。青蛙与工具也会落在各自场景的正确活动区域,而不是统一从左下角开始估算。

地点与场景类型
→ 正式背景 / 前景图层
→ 场景坐标组
→ 青蛙、工具与旅伴变体
→ 保存为可带回的过路照

静态场景优先使用已经确认的正式模板;普通随机场景保留多个可选位置与构图组,使同一条旅行路线不会每次都生成完全相同的照片。

2026.9.19

Stage37.官服周末抓包与旅行、聚会回礼闭环

周末补齐了正式服的旅行返程、聚会返程与“周末的小插曲”抓包,并据此把离线服中原先只具备入口的回礼流程接回实际状态。旅行不再只结算固定的照片和特产:返程时还可以出现意外收获;照片的分享奖励也改成点击即可领取三叶草,不再跳转外部分享。

广告相关的任务入口保留在原位置,但离线版点击后直接完成奖励条件。这样活动页面仍保持原来的操作路径,同时不会依赖广告平台或第三方页面。

旅行 / 聚会 / 周末事件完成
→ 本地结算回礼
→ 写入照片、家具、绘本、三叶草或抽奖券
→ 推送活动与邮件记录
→ 下次登录继续显示

Stage38.节日周期、春节宝箱与活动货架

限时活动重新分成“按自然日期出现”和“离线常驻”两类。中秋贺卡以每年中秋前后十天开放;春节贺卡以农历正月初一前后十五天开放;周年蛋糕固定在 12 月 14 日至次年 1 月 12 日。六一当天登录会获得棒棒糖,儿童节称号只解锁一次。

春节活动期间,嘟嘟每天会带来半价正丹纸;春节贺卡的金、红宝箱不再只按寄送次数决定,而是读取卡片背景和三张贴纸的组合。正式配置中的五组必出金箱搭配已经接入,其余组合仍会按卡片组合产生不同结果。

背景 + 三张贴纸
→ 贺卡组合
→ 金箱 / 红箱
→ 祝福、贺卡、贴纸与物品奖励

大冒险、扭蛋机和许愿池则作为离线版常驻内容保存。它们不再因原活动日期结束而关闭,面板和庭院入口也删除了“截止日期”“常驻”等提示,保持与普通长期功能一致的显示。

Stage9-fix.庭院旅友的停留与回礼时机

旅友到访的间隔继续使用冷却时间加随机出现检查;到达庭院后,未喂食的旅友会随机停留 2 至 6 小时,而不是固定六小时。

喂食不会立即把回礼塞进邮箱。服务端会先记录旅友的口味、回礼或聚会邀请结果;旅友会再随机停留 2 至 4 小时,到时才离开并结算礼物、友情绘本或聚会邀请。花圃中的旅友播种、成长协助也在离开结算时一并完成。

旅友到访
→ 随机停留 2–6 小时
→ 喂食并记录回礼结果
→ 再随机停留 2–4 小时
→ 回礼、邀请与花圃协助结算

完结撒花🎉

普通离线版已经具备完整的日常循环。后续工作转入存档体验修正,以及面向收集、查看和测试的图鉴版。

2026.9.20

🐸图鉴版:独立端口、独立存档与全收集初始状态

图鉴版不是在普通存档上直接改数值,而是使用独立应用包、独立本地服务器端口和独立存档:

普通版应用
→ 52352
→ state.json

图鉴版《旅行青蛙·中国之旅图鉴版》
→ 52353
→ gallery_state.json

这样两套游戏可以同时安装、切换和测试,普通版的旅行进度、庭院状态和活动记录不会被图鉴版覆盖。

图鉴版的定位是“可查看、可试用、可继续互动的完整图鉴”,而不是把普通版的每一次获取流程都强行跳过。一次性收集内容在进入图鉴版时已解锁:旅行装备、家具、故事、称号、友情绘本、手工品、百科、地图、纪念品、特产、博物馆典藏和固定旅行照片都能直接查看。

1.图鉴版的无限资源与不重复产出

图鉴版保留原有商店价格、制作入口和旅行界面,但资源逻辑改为稳定的展示与试用状态:

三叶草
→ 固定为 9999

食品、幸运物、花朵、花苗、种子、特产与常规材料
→ 各保有 99 个

购买、使用、插花、周年蛋糕制作
→ 不扣减数量

重复获得
→ 不再叠加无意义的库存

相册页数已经扩展到最大可用容量,固定照片各保留一份;动态照片所需的六类材料照片也一并准备好,可以直接进入制作流程。花瓶中的可插花材、周年蛋糕材料和手工品材料保持 99 个,活动页面不显示结束日期,中秋、新年和周年庆可以随时查看和操作。

图鉴版仍允许青蛙制作家具、制作手工品并出现对应动作。区别在于:已经拥有的家具、祈愿物和印章不会重复写入收藏,也不会重复发放额外奖励。这样既能保留庭院的生活感,也不会破坏全图鉴状态。

2.一次性收集内容全图鉴

图鉴版进入游戏时已经补齐下列内容;它们仍会按照客户端原有分类显示,而不是单独做一套新的展示页:

  • 旅行装备 → 食物、幸运物、工具、坐垫、杯子与旅行用具

  • 家具与图纸 → 14 个正式主题、普通家具、全部不倒翁、堆肥箱、挂兜及相关设计图

  • 故事与旅友 → 全部旅行邂逅故事、友情绘本页与收藏页

  • 手工品 → 木质护符、祈愿物、印章、制作工具与可用材料

  • 称号与百科 → 全部称号、物品百科、旅行百科、纪念品、特产、藏品与博物馆典藏

  • 地图与旅行记录 → 全部地图区域、旅行笔记与可查看的目的地内容

  • 伴蛙前行 → 30 个任务、所有阶段进度和全部奖励均标记为完成并领取

  • 美好瞬间 → 30 个彩蛋

值得注意的是手工品部分:护符本体、祈愿物和印章都已纳入图鉴;三种护符碎片从零开始,仍可通过旅行获得并完成三拼。如果护符碎片也设成99且使用不消耗,那么就永远拼不完,也退不出拼护符的界面了。

3.照片、相册与动态照片

图鉴版的相册扩容了999页,并预先放入每一张非 Normal 类型的固定照片,包括旅行地点、食物、幸运物、活动和特殊事件照片。普通过路照属于随装备、旅伴与随机位置生成的照片,不作为固定样张预置,仍可以通过旅行继续产生。

动态照片的每一种来源照片都会预置六张,正好覆盖一次动态照片制作所需的材料数量;动态照片本体、相框入口和相册保存流程仍按原来的步骤运行。相册扩容道具保持最大容量,但当前可翻页数只取决于已经保存的真实照片,因此不会再出现“容量有一千页、后面全是空白”的页面。

4.活动与长期入口

普通版和图鉴版都将博物馆大冒险、扭蛋机、许愿池作为离线长期入口;它们不会因为原活动结束而消失。广告、分享类任务在两版中都保留按钮和奖励路径,离线环境下点击即可完成对应条件。

节日内容的差异则保持明确:

普通版
→ 中秋前后十天
→ 农历新年前后十五天
→ 周年庆 12 月 14 日至次年 1 月 12 日
→ 六一当天登录奖励与称号

图鉴版
→ 中秋贺卡、春节贺卡、周年庆蛋糕始终可进入
→ 不显示结束日期
→ 周年蛋糕所有层级同时可做

春节贺卡的首包免费、背景首次免费、贴纸组合、红箱与金箱奖励规则仍然存在;中秋贺卡的寄送、购买和奖励流程也未被删去。图鉴版只是解除日历窗口,让这些页面可以随时查看和操作。嘟嘟商人的普通商品、联动家具、幸运物、半价正丹纸和重复购买返还逻辑仍按各自物品规则处理。

5.庭院、制作与不重复奖励

图鉴版没有把青蛙变成静态陈列。青蛙仍会回家、外出、睡眠、阅读、进食、制作家具和制作手工品;花圃、花瓶、堆肥、挂兜、旅友到访、聚会、旅行返程和周末小插曲也仍能继续体验。

区别是全收集后的产出处理:

家具制作 / 手工品制作
→ 保留等待、工作台与青蛙动作
→ 已拥有成品不再重复加入收藏

花瓶插花 / 周年蛋糕 / 旅行消耗
→ 可以继续操作
→ 对应资源维持 99 个

旅行、聚会与活动回礼
→ 保留页面、邮件与结算反馈
→ 唯一收藏不重复发放
折叠区小作文

回想起高中时每周末上线收集明信片、装好背包;节气与节日的特殊食物与照片;以及在第一次收到北京的明信片时,发出的漂流瓶,“希望一年半之后 我也能考到这个城市”。时间过得太快太快。高考完,我如愿考到了北京,把蛙蛙寄过来的照片全部印出来,收纳在一本相册,然后不怎么再上线。继续奔波辛劳地活着……几乎不想起以前。前一天半夜睡不着,打开小程序,看到蛙蛙回来了,在庭院里闭着眼睛淋雨——玩了几年,第一次解锁这个彩蛋。独在异乡,面对着日益内卷与异化的环境,我也不再感受雨,而只是被淋湿。曾经玩的时候心想总有一天会遇到这个场景,会集齐所有彩蛋……当时只道是寻常。

其实小青蛙本会走散在2023年的春天

远行与回归……而回归的路更长。

后续Bug修复(持续更新)

1.日历、相册与周年蛋糕

日历的红点不再仅仅因为节气任务全部完成就常亮。它会同时检查当天是否真的存在可领取的节气奖励;未来日期的奖励仍会留在日历中,但不会提前提示。幸运日奖励则继续按实际未领取状态显示红点。

相册扩容也重新回到它应有的职责:扩容只增加可存放照片的容量,而不制造没有照片的可翻页面。此前动态照片选择页和普通相册都会把容量当作实际页数,因此在已有照片页之后还能翻到空白或占位页。现在两种入口都只按真实照片数量分页:

相册容量增加
→ 可以继续保存更多照片

当前真实照片数量
→ 决定相册与动态照片选择页的最大页数

动态照片1号位的过路照也进行了坐标修复。

周年蛋糕的小问答同时核对了客户端实际提交的答案字段。问答现在会正确识别所选项、发放奖励并推进下一步;普通版仍保留材料消耗、分阶段等待、制作、分享和领取奖励的顺序。

2.青蛙动作刷新与写字台入口

原版本青蛙动作刷新时长为固定2小时,现在设定为1-3小时随机。写字台原本没有对应的功能,现在点开写字台可以进入笔记界面。

3.嘟嘟商店、节日趣闻与新手旅行

嘟嘟的货架继续收紧到真正可购买的内容。实用锯子、毛刷、锤子、棒针、小刀等制作工具都是单次获得物品:已经拥有后不会再次进入当天货架。旧存档里遗留的同类货位也会在刷新时清掉;如果客户端保留了旧货位并继续点购,则保持原价返还,不会重复增加工具。

“故事”中的节日趣闻恢复为原存档中的两张明信片。它们作为过去与其他玩家互动留下的静态记录继续显示,不再由离线服务器虚构新的陌生玩家来信或改写这两张内容。

新账号的首次旅行也补上了固定的开场:

第一次完成旅行准备
→ 固定前往北京
→ 正常带回旅行记录与奖励
→ 后续旅行恢复食物、幸运物和路线映射

已有旅行历史的普通版与图鉴版不会被这条规则改写。

4.归家插花与木质护符碎片

青蛙旅行归家后,如果花瓶为空且仓库中有可插花材,会有35%的概率自己插上一朵花。它不会替换玩家已经插好的花,也不额外消耗花材;花朵仍按花瓶原有的新鲜、枯萎与消失周期推进。

木质护符的三种碎片不再按“缺哪种补哪种”的方式掉落。旅行触发碎片奖励后,三种碎片各占三分之一概率;即使某一种已经很多,仍有可能继续获得它。图鉴版保留护符本体99个、使用不消耗的设定,碎片则仍从旅行中随机得到,能够正常完成三拼。

5.周年蛋糕完成态与图鉴版抽奖券

周年蛋糕点燃蜡烛并领取最终奖励后,服务端会把蛋糕推进到完整的第六阶段。顶部五个进度图标因此都会显示为已点亮,最后的蜡烛不再停留在灰色。旧的已完成蛋糕记录也会在加载时自动校正到这一状态。

图鉴版的抽奖券补齐为与三叶草一致的固定资源:数量始终显示999,商店抽奖不扣减,白球返券或其他获得途径也不会继续增加。普通版仍使用实际抽奖券数量与消耗规则。

6.dry 过路照的滴水叶片坐标

过路照中 dry、dry1、dry2、dry3 共用的滴水叶片前景曾回退到左上角 (0,0),导致叶片和水滴压住青蛙。该前景资源宽242,而照片画布宽500;现在按右侧贴边的位置放到 (258,0):

dry_back
→ 画布背景

青蛙 / 旅伴 / 工具
→ 场景活动区域

dry_fw @ (258,0)
→ 右上角滴水叶片前景

普通版与图鉴版已有的同类照片均已同步迁移,新旅行生成的 dry 场景也会直接使用正确坐标。

7.花圃堆肥与土地肥力

检查两个版本的存档后发现,六格堆肥虽然会依次发酵,土地肥力却只由正在发酵的单格材料临时决定。材料吸收后,效果没有累计到土地上;长时间不登录时也只结算一格,玩家很难看到“土地肥力”增加。

现在每份材料完成发酵后都会累加肥力和持续时间。六格按照放置时间依次吸收;即使隔了一整天才进入游戏,服务端也会把期间已完成的材料逐项结算。花朵的生长按每段实际生效的肥力计算,不会把刚得到的加成追溯到之前的时间。肥力花朵沿用客户端原有的三档显示。服务端持续时间没有在客户端资源中给出,离线版采用本地规则:普通肥料与特产每份延长一天,长效肥料每份延长三天,避免隔天登录时材料已消耗、肥力却已归零。

8.图鉴版奖励数量同步

图鉴版任务奖励曾出现“抽奖券实际保持999张,客户端却收到1000张”的数字不一致。现在任务领奖结果和抽奖券推送都使用图鉴版的固定数量;重复物品与日历奖励的返回结果也读取最终库存值。普通版继续按实际获得数量增加。

9.青蛙返家后的家具进度

青蛙旅行回家会推进被砸工作台的修复次数,也会累计可能自行换家具的返家次数。此前只有真正修好工作台或换上家具时才保存这两个计数;如果这一趟没有可见变化,重启游戏可能丢失进度。现在每次返家都会保存计数,工作台在达到设定的旅行次数后可以稳定恢复。

附录:运行备忘与项目目录

本地运行备忘

自用备忘

在 CMD 中运行。普通版和图鉴版可独立启动,只玩其中一个时只开对应服务器即可。

先设置项目目录和 ADB 路径;以下为通用示例,请替换为自己的安装位置:

set "FROG_HOME=C:\path\to\frog"
set "ADB=C:\path\to\adb.exe"

启动普通版:

start "旅行青蛙·中国之旅" /D "%FROG_HOME%\server" cmd /k python server.py --port 52352
"%ADB%" -s emulator-5554 reverse tcp:8080 tcp:52352

启动图鉴版:

start "旅行青蛙·中国之旅图鉴版" /D "%FROG_HOME%\server" cmd /k python server.py --gallery --port 52353
"%ADB%" -s emulator-5554 reverse tcp:8081 tcp:52353

关闭时,优先在对应服务器窗口按 Ctrl+C。若窗口找不到,可在 CMD 强制关闭:

for /f "tokens=5" %a in ('netstat -ano ^| findstr :52352 ^| findstr LISTENING') do taskkill /PID %a /F

图鉴版则是:

for /f "tokens=5" %a in ('netstat -ano ^| findstr :52353 ^| findstr LISTENING') do taskkill /PID %a /F

模拟器重启后需要重新执行一次对应的 adb reverse;服务器重启本身不需要重新安装游戏或清理任何数据。新开 CMD 窗口时,需要先重新设置上述两个变量。

访问模拟器

"%ADB%" shell
su

项目结构

frog\
├─ analysis\                 分析资料
├─ apk\
│  ├─ base.apk
│  └─ gallery-unsigned.apk
├─ backup\                   本地备份与热更新镜像
├─ cache\
├─ capture\
│  ├─ initial_sync\
│  ├─ weekend_party_travel_20260919\
│  ├─ album_anim_20260912\
│  ├─ travelmap\
│  └─ raw\
├─ client\
│  ├─ config\
│  └─ js\
│     ├─ main.offline.stage6i2.js       普通版客户端补丁
│     ├─ main.gallery.js                图鉴版客户端补丁
│     ├─ main.min.js
│     └─ calendar.offline.js
├─ hotfix\
│  └─ c1_client\
├─ picture\
│  └─ photo_reference_104\
├─ server\
│  ├─ server.py                         两版本共用服务端
│  ├─ offline_policy.json
│  ├─ config\
│  │  ├─ Picture_json.json
│  │  ├─ picture_official_variants.json
│  │  ├─ picture_resource_positions_json.json
│  │  ├─ furnitureData_json.json
│  │  ├─ Item_json.json
│  │  ├─ animpictureData_json.json
│  │  └─ ...
│  ├─ data\
│  │  ├─ state.json                     普通版存档
│  │  ├─ gallery_state.json             图鉴版存档
│  │  └─ backups\
│  ├─ archive\
│  └─ travelmap\
├─ tools\
│  ├─ build_gallery_client.py
│  ├─ build_gallery_state.py
│  ├─ complete_gallery_state.py
│  ├─ patch_album_pages.py
│  └─ prepare_gallery_apk.py
├─ tmp\
├─ AGENTS.md
└─ frog_desktop_handoff_2026-09-13.md

DISCUSSION

Comments

Sign in with GitHub to join the conversation.