diff --git a/.comet/config.yaml b/.comet/config.yaml new file mode 100644 index 0000000..4431858 --- /dev/null +++ b/.comet/config.yaml @@ -0,0 +1,38 @@ +# Configuration schema used by Comet. Do not edit this value. +schema: comet.project.v1 +# Default workflow entered by /comet. Must also appear in workflows. +default_workflow: native +# Workflows enabled in this project: native, classic, or both. +workflows: + - native +# Enables automatic recovery through the read-only Ambient Resume probe for both Native and Classic. Set false to disable it. +# ambient_resume: true | false +ambient_resume: true +# Project policy for automatic personal-memory learning and retrieval. +memory: + # Allows workflow events in this project to form new personal memories automatically. + # learning: true | false + learning: true + # Allows personal memories to be injected into Agent context for this project. + # retrieval: true | false + retrieval: true +# Project knowledge retrieval provider used by ordinary Comet tasks. +knowledge: + # Provider for project knowledge. + # provider: local | remote + provider: local +# Native workflow settings. They do not change Classic state or behavior. +native: + # Root directory where Native stores Comet specs and changes. Runtime data stays under .comet. + artifact_root: docs + # Artifact language used by Native workflow documents. + # language: en | zh-CN + language: en + # Controls how Native asks clarifying questions: batch asks every currently answerable question per round (default), sequential asks one at a time. + # clarification_mode: batch | sequential + clarification_mode: batch + # Controls whether Native archives automatically after a successful preview or waits for explicit user confirmation. + # archive_confirmation: automatic | required + archive_confirmation: automatic + # Maximum failed Verify outcomes allowed for one confirmed acceptance target before Native stops the completion loop. + max_verify_failures: 5 diff --git a/.gitignore b/.gitignore index cf3447c..5cc057a 100644 --- a/.gitignore +++ b/.gitignore @@ -109,5 +109,9 @@ Desktop.ini /.agents/ /.codex/ /.codebase-memory/ + +# >>> Comet managed project state >>> +!/.comet/ /.comet/* !/.comet/config.yaml +# <<< Comet managed project state <<< diff --git a/AGENTS.md b/AGENTS.md index b5204b4..77c21eb 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -14,6 +14,10 @@ - .release-workspaces 不进入主仓库。本轮用户要求停止拉取,已删除本地克隆;不得自行重新克隆、fetch、pull 或推送。 - docs 中的框架接入和发布流水线属于设计,未实现部分不得报告为已完成。 + + + + ## Comet Ambient Resume 在这个仓库中,开始处理需要改动或调查的任务前,如果可能存在活跃 Comet workflow,把当前用户请求传入只读探针:`comet resume-probe . --stdin --json`。 diff --git a/README.md b/README.md index f27bec9..d0173c9 100644 --- a/README.md +++ b/README.md @@ -1,40 +1,83 @@ # Farm -一个源码主仓库、多框架发布仓库的农场插件翻新项目。 +统一维护农场核心,面向真寻、普通 NoneBot 和 AstrBot 分包发布。 +主仓库:https://github.com/Shu-Ying/main_plugin_farm.git。 -当前阶段:初始化、旧代码保全与设计评审。新的跨框架农场尚未实现, -本目录不能直接作为 NoneBot、AstrBot 或真寻插件安装。 +## 当前可用范围 -## 当前已有内容 +第一批框架无关基础核心已实现,位于 src/farm_core: -- docs/:长期规划、架构、业务约束、发布设计、迁移与源码审查。 -- legacy/zhenxun_plugin_farm/:旧 Beta 分支代码与资源的迁移基线。 -- src/、integrations/、tests/、release/:职责说明和未来实施位置。 -- scripts/check_baseline.py:离线校验迁移清单、Python 语法及静态数据库。 -- Git 已本地初始化为 main;没有配置主仓库远程,也未提交或推送。 +- 离线迁移旧版 202 条作物配置。 +- 注册、账户/库存/地块查询、购买、播种、收获、铲除。 +- 作物锁定/解锁、指定出售与全部出售、基础文本展示。 +- SQLite 事务、资产流水、幂等重放及并发保护。 +- 初始 500 农场币、0 点券、0 经验、3 块普通地。 +- 可选共享图片层:旧布局与素材、目标尺寸绘制、WebP/JPEG/PNG、 + 有界缓存和受控后台执行,参见 [绘图说明](docs/10-rendering.md)。 -先读 [文档索引](docs/README.md),再读 -[待确认决策](docs/07-decisions.md)。 +本目录目前不是可直接放入真寻、NoneBot 或 AstrBot 插件目录的成品。 +integrations 中仍是职责说明,框架接入与发布流水线尚未实现。 +签到、偷菜、升级、兑换和真实玩家数据迁移不在本轮范围。 -## 校验 +## 安装与本地演示 -使用 Python 3.10 或更高版本: +Python 语法基线为 3.10;本轮在 Windows、Python 3.12.14 上验证。 +推荐在独立虚拟环境中使用。构建需要 setuptools>=68; +运行核心本身没有第三方依赖。 + +```text +python -m pip install --no-deps --no-build-isolation . +python -m farm_core --database runtime/demo.sqlite3 +``` + +演示使用独立的 demo 身份及可控时钟,不需要真实等待作物成熟。 +指定目录内生成数据库及同名 .log;请使用开发目录,不指向生产数据。 +相同演示数据库重复运行会重放固定操作结果,不重复发放资产。 + +接入方调用示例: + +```python +from farm_core import Catalogue, FarmService, Identity, SQLiteStore + +store = SQLiteStore("runtime/farm.sqlite3") +await store.initialize() +service = FarmService(store, Catalogue.bundled()) +who = Identity("deployment-1", "realm-1", "qq", "user-1") +try: + receipt = await service.register(who, "我的农场", "event-register-1") + snapshot = await service.snapshot(who) +finally: + await store.close() +``` + +每个写操作传入稳定的 operation_id。调用方负责身份命名空间、权限、 +会话及发送消息;核心不会自动关联跨平台账号。 + +## 离线验证与迁移 ```text python scripts/check_baseline.py -git status --short +python scripts/export_catalogue.py --check +python scripts/verify_foundation.py ``` -脚本不启动机器人,不执行网络请求,不读真实玩家数据库。 -语法通过不等于插件加载或玩法正确。 +完整验证执行基线校验、可复现导出、规则/事务测试和离线 wheel 安装; +打包测试需要已安装的 pip、setuptools、wheel、Pillow和可加载字体, +禁止从网络补装;绘图测试可设置FARM_TEST_FONT为本机字体路径。 +只跑业务测试时,可安装核心后使用: + +```text +python -m unittest discover -s tests -p "test_transactions.py" -v +``` + +[文档索引](docs/README.md)、 +[基础核心说明](docs/09-foundation.md)、 +[待确认产品决策](docs/07-decisions.md)。 ## 来源与许可证 -旧代码来源为 Shu-Ying/zhenxun_plugin_farm 的 Beta 分支, -提交 b015c6c1997f146f651b677382980d338a306bb2。 -保留原 GPLv3 LICENSE、原作者信息和旧 README。 -素材权属尚需逐项核对,不因代码许可证而自动完成素材授权判断。 - -同步副本中的服务令牌已改为读取 FARM_SERVICE_TOKEN 环境变量, -其余源码与资源复制差异见 docs/legacy-manifest.json。 -不要直接部署含已知业务缺陷的迁移基线。 +legacy/zhenxun_plugin_farm 保存旧 Beta 基线,827 个文件保持校验。 +来源提交 b015c6c1997f146f651b677382980d338a306bb2,保留 GPLv3 LICENSE。 +基线副本的硬编码服务令牌已改为环境变量;原服务端轮换未完成。 +新核心不使用该令牌、不访问旧远程服务;可选渲染包独立保存所需旧素材。 +新核心 0.1.0 为本地基础包版本,不表示三个框架插件已发布。 diff --git a/docs/02-architecture.md b/docs/02-architecture.md index 1a825c4..ab71d61 100644 --- a/docs/02-architecture.md +++ b/docs/02-architecture.md @@ -2,7 +2,7 @@ ## 原则与当前状态 -下列为建议设计,尚未实现。当前旧代码位于 legacy,未把其全局变量、 +下列为远期目标设计;第一批核心已按职责实现为独立模块,见09-foundation.md。当前旧代码位于 legacy,未把其全局变量、 数据库管理器和服务定位器直接当作新核心继续复用。 采用一个共享 Python 包,内部按职责划分,避免先建多个互相依赖的 @@ -33,7 +33,7 @@ farm/ docs/ ``` -本轮只创建职责说明及迁移基线,不生成大量空模块或假实现。 +初始化时只创建职责说明;当前已实现第一批核心,不生成大量空模块。 当一个用例实施时,才创建所需模块。 ## 依赖方向 diff --git a/docs/04-release-design.md b/docs/04-release-design.md index 1bb06b9..462b322 100644 --- a/docs/04-release-design.md +++ b/docs/04-release-design.md @@ -4,7 +4,7 @@ | 角色 | 远程地址 | 建议职责 | |---|---|---| -| 开发主仓库 | 尚未提供;本地 D:/Work/farm | 业务、接入、测试、文档、构建的唯一开发源 | +| 开发主仓库 | https://github.com/Shu-Ying/main_plugin_farm.git;本地 D:/Work/farm | 业务、接入、测试、文档、构建的唯一开发源 | | 真寻发布仓库 | https://github.com/Shu-Ying/zhenxun_plugin_farm.git | 真寻完整插件 | | AstrBot 发布仓库 | https://github.com/Shu-Ying/astrbot_plugin_farm.git | AstrBot 完整插件 | | NoneBot 发布仓库 | https://github.com/Shu-Ying/nonebot_plugin_farm.git | NoneBot 包及 PyPI 构建资料 | diff --git a/docs/07-decisions.md b/docs/07-decisions.md index af9e271..f9435ce 100644 --- a/docs/07-decisions.md +++ b/docs/07-decisions.md @@ -17,7 +17,7 @@ | 兼容基线 | 保留旧指令清单;Python/宿主版本经矩阵实测确定 | 旧 README 的 3.10+ 不等于三框架共同最低版本 | | 发布顺序 | 真寻先稳定,之后普通 NoneBot、AstrBot | 利用现有宿主验证闭环,控制变更范围 | | 发布仓库 | 使用给出的三个现有仓库,接管前比较现有代码 | 三仓库不为空,不能直接覆盖 | -| 主仓库远程 | 用户后续提供独立 farm 地址 | 本轮只建本地 Git,不误用发布仓库 | +| 主仓库远程 | 已明确 Shu-Ying/main_plugin_farm | 本轮只建本地 Git,不误用发布仓库 | | 发布授权 | 首次候选 PR 审查,稳定后再议自动发布 | 本轮未授权远程覆盖、推送或发布 | | 跨平台 OS | 建议 Windows + Linux;macOS 有需求再验收 | 路径大小写、字体、浏览器和时区需分别测试 | | 素材许可证 | 核对图片、字体、HTML 和借用工具来源 | 代码 GPLv3 不自动证明所有素材可分发 | diff --git a/docs/08-validation.md b/docs/08-validation.md index 94eac6e..adc818f 100644 --- a/docs/08-validation.md +++ b/docs/08-validation.md @@ -1,4 +1,6 @@ -# 本轮验证与交付边界 +# 初始化阶段验证与交付边界 + +此页保留初始化阶段的历史记录;随后完成的基础核心与正式验证见 [09-foundation.md](09-foundation.md)。 日期:2026-10-15。执行 Python:3.12.14;未安装或启动三种宿主。 新工具还以 Python 3.10 语法规则解析旧代码;这不代表三框架共同支持 diff --git a/docs/09-foundation.md b/docs/09-foundation.md new file mode 100644 index 0000000..c3eff04 --- /dev/null +++ b/docs/09-foundation.md @@ -0,0 +1,104 @@ +# 第一批基础核心实现 + +日期:2026-10-15。对应 Comet change:farm-core-foundation。 +已确认规格位于 comet/changes/farm-core-foundation。 +本文件记录实现与开发期测试,不替代 Runtime 的独立验收报告。 + +## 模块与职责 + +| 模块 | 责任 | +|---|---| +| domain.py | 不可变身份/作物/账户/库存/地块/结果,整数校验及纯规则 | +| catalogue.py、data/plants.json | 本地配置加载与校验,202 条原始作物配置 | +| ports.py | 应用层所需事务仓储接口 | +| application.py | 注册、购买、播种、收获、铲除、锁定、出售 | +| repository.py | 单个事务连接上的 SQL,不自行提交 | +| storage.py | schema、连接、事务、幂等、流水、线程执行和关闭等待 | +| presentation.py | 结构化结果转普通文本,不修改数据库 | +| __main__.py | 显式本地演示及日志文件 | +| scripts/export_catalogue.py | 只读静态作物库的可复现导出 | +| scripts/verify_foundation.py | 正式离线检查入口 | + +按职责先采用小模块;随着用例增加再拆分目录, +不为了贴合远期目录树生成大量空包。 +核心只用标准库,不导入三框架,包内部相对导入。 +wheel 随包携带 JSON,不包含 legacy、图片、玩家数据库或其他框架入口。 + +## 已迁移与修复 + +- 沿用500初始币、3地块、等级门槛及源作物价格/产量/经验/成熟时间。 +- 旧作物库202条记录完整导出且保留源 SHA-256。 +- 多季再生长间隔明确保留去重 phase[1:] 求和,未重平衡数值。 +- F01/F02:余额和经验只增加本次增量。 +- F03:每块地单独加成、向下取整,再求和。 +- F04/F06:数据库错误传播;扣款、发货、流水与结果在一个事务中。 +- F05:不缓存不存在玩家,注册提交后即可读取。 +- F08/F11:锁定统一约束两种出售入口;文本直接渲染结构化结果。 +- F07签到与F09活动模块未迁移,不能算已修复。 +- 保留旧基线只读,迁移工具不读取真实玩家库。 + +## API 约定 + +所有操作为异步方法;服务接受显式 Store、Catalogue 和可选秒级时钟。 + +| 方法 | 说明 | +|---|---| +| register(identity, name, operation_id) | 首次注册;重复注册不再赠送 | +| snapshot(identity) | 同一读事务的账户、库存、地块快照 | +| buy(identity, name, count, operation_id) | 按币种和等级购买 | +| sow(identity, name, count, operation_id) | count=None 尽可能播种;显式数量不足则失败 | +| harvest(identity, operation_id) | 收获全部已成熟地块,一笔事务结算 | +| eradicate(identity, operation_id) | 只清理枯萎实例 | +| lock(identity, name, locked, operation_id) | 锁定或解锁产物 | +| sell(identity, name, count, operation_id) | name=None 且 count=None 批量出售;锁定物品跳过 | + +指定出售锁定物品返回 plant_locked,全部出售跳过锁定物品; +没有可出售物品返回 nothing_to_sell。指定数量不自动截断为可用库存。 +显式数量接受1至1,000,000整数,拒绝 bool、负数、0、浮点和超大值。 +库存/资产/时间不超 SQLite 有符号64位整数上界。 + +FarmError.code 表示业务失败;StorageError 表示数据库故障; +SchemaError 表示外来或不兼容库。接入层不能把后两者当作未注册。 +同一个身份和 operation_id 只能对应同一动作与参数; +相同请求返回原已提交 Receipt,冲突请求返回 operation_conflict。 +已提交的余额是该操作时的结果,重放收据不是账户的最新查询, +要显示当前状态需另调 snapshot。 + +## 持久化与取消语义 + +新库使用独立 application_id、schema_version=1: +accounts、inventory、plots、operations、ledger。 +未知表集、外来库或更高版本被拒绝,不把旧 farm.db 自动当新库打开。 + +每次写用例使用新连接与 BEGIN IMMEDIATE;提交前统一写幂等结果。 +数据库条件更新、非负整数约束和唯一键保护资产。 +SQLite 工作在线程执行,事件循环不直接执行 SQL。 +close 拒绝新请求并等待在途任务,无导入时线程或持久数据库连接。 + +异步调用取消不保证取消已进入线程的事务:事务可能提交, +但结果与 operation_id 一起保存;使用相同 ID 重试可确定结果。 +这避免平台重试产生第二笔扣款;不得仅因发送消息失败而换新 ID。 +并发测试在单进程 Windows 中完成,跨进程及 Linux 仍需后续矩阵验证。 + +播种冻结成熟时间、产量、经验与季数等规则;出售目前按服务加载的 +目录价格结算,价格变更策略应在后续版本化经济规则中明确。 +不支持核心对外随意充值;外部金币接入需独立对账和补偿设计。 +生产数据备份、迁移、归档及账本保留策略仍待实施。 + +## 开发期证据 + +- 7项领域测试通过,包括整数边界、身份、源目录及坏数据。 +- 22项事务测试通过,包括玩法、回滚、并发、取消与重启。 +- 3项包装测试通过,包括无框架导入、无导入副作用和离线安装。 +- 首次事务测试有2项因测试代码未关闭连接而在Windows清理失败; + 修复测试资源释放后通过,不隐藏该失败。 +- 最终结果以 Comet 管理的 verification.md 为准。 +- 真实框架、旧玩家库、联机、图片和远程发布没有测试或启用。 + +## 正式验收结果 + +Runtime已执行完整离线检查并通过。 +独立只读Verifier核对A1至A8后全部判定通过,结果已由Runtime接收; +正式报告为 comet/changes/farm-core-foundation/verification.md。 +通用Skill桥接不能机器认证独立执行身份,当前等待用户接受结果, +尚未归档、提交或推送。此状态不等于三框架插件已发布。 diff --git a/docs/10-rendering.md b/docs/10-rendering.md new file mode 100644 index 0000000..957e534 --- /dev/null +++ b/docs/10-rendering.md @@ -0,0 +1,145 @@ +# 共享图片工具与“我的农场” + +状态:共享渲染已实现并通过离线检查和独立只读验收;等待用户接受。 +真实宿主命令与上传仍未接入。 +Python 语法基线3.10;本机实测 Windows/Python3.12.14/Pillow12.3.0。 + +## 安装与职责 + +```text +python -m pip install ".[render]" +``` + +核心 farm_core 仍无第三方运行依赖。可选 farm_render 使用 Pillow; +NoneBot、AstrBot、真寻均可调用,不导入任何宿主对象。 + +| 文件 | 职责 | +|---|---| +| src/farm_render/canvas.py | 同步透明合成、裁剪、缩放、文字和编码 | +| src/farm_render/assets.py | 包内素材读取、按目标尺寸缓存与字节上限 | +| src/farm_render/models.py | 输入配置、展示模型和编码结果 | +| src/farm_render/layout.py | 一次快照与统一时刻转换为可见状态 | +| src/farm_render/renderer.py | 底图、作物、账户面板、成图缓存 | +| src/farm_render/service.py | 一次查询、后台执行、并发和取消管理 | + +字体由部署方提供可合法使用的中文 TrueType/OpenType 文件路径。 +示例使用本机已有微软雅黑;不会复制或发布 Windows 字体。Linux +应配置包含中文的字体;测试可通过 FARM_TEST_FONT 指定路径。当前 +实现校验字体可加载,不保证任意字体具备所有中文、Emoji 字形。 + +## 调用 + +```python +from farm_render import FarmRenderer, RenderOptions, RenderService + +renderer = FarmRenderer("/path/to/chinese-font.ttf") +rendering = RenderService(renderer, concurrency=1, max_pending=8) +try: + picture = await rendering.my_farm( + service, who, + options=RenderOptions(theme="spring"), + avatar=avatar_bytes_or_none, + supported=("WEBP", "JPEG", "PNG"), + ) + # 接入层按 picture.mime 发送 picture.content。 +finally: + await rendering.close() +``` + +应用生命周期内复用一个 renderer/service,勿在每次命令中重新创建。 +上面的 finally 仅演示生命周期结束时的清理;退出插件时还需关闭 +业务 store。my_farm 每次调用只请求一次 service.snapshot,不按地块 +访问数据库。若已有快照,可直接 await rendering.render(who, snapshot, +now, options),统一 now 使用UTC秒整数。正常未注册/存储故障仍由 +业务层抛出,接入层负责转换为用户提示,不伪装成空农场。 + +同步 FarmRenderer.render 仅供离线或已有后台任务的场景;机器人 +事件循环应调用 RenderService。实例绑定字体、静态配置和素材版本, +替换素材或字体后重建实例。RenderService 只供同一事件循环使用。 + +## 尺寸、格式和质量 + +- 普通:1024×624、quality75;RenderOptions.high() 为1536×936、85。 +- 编码格式独立:AUTO、WEBP、JPEG、PNG;WebP method默认2。 +- AUTO仅根据接入层声明的 supported 和本机编码能力选择,默认 + supported只有JPEG。确认平台支持后传入WEBP,才会优先使用它。 +- 显式 format=WEBP 可强制选择;本机不支持则明确报错。平台不支持 + 时应在调用前选JPEG,避免上传失败后再次合成和有损转码。 +- PNG为无损,不使用quality控制画质;WebP/JPEG的quality不能视为 + 同等视觉质量标尺。本实现不使用WebP无损或动画。 + +原素材保持原格式。所有底图和坐标按目标尺寸处理,不先创建3072宽 +的合成画布。图像解码仍可能产生原素材大小的临时缓冲,不能把 +画布缩小等同于全部内存按同一比例下降。 + +## 阶段与缓存 + +布局使用旧30块地坐标。首季由保存成熟时间减首熟时长推导起点; +再生长由下一次成熟时间减再生长时长推导。阶段阈值映射到本季 +时长,解决旧代码多季仍引用最初播种时间的问题;仅改变展示, +不修改成熟和收益规则。枯萎优先于成熟。原偏移字段为offsetX/Y/W/H, +新实现使用真实字段,不沿用旧绘图的offic拼写。 + +素材LRU默认32MiB,成图LRU默认8MiB;零预算禁用对应缓存,超限 +条目不入缓存。素材返回副本,缓存底图不被玩家绘制污染。预算按 +缓存图像像素/编码数据及成图元数据预留计算,不代表进程RSS上限; +还存在解码临时缓冲、Python对象、线程和字体缓存(最多32项)。 + +成图键含完整作用域摘要、可见账户、地块阶段、头像摘要和配置。 +阶段/成熟边界直接改变展示键,不用固定TTL拖延成熟显示;同阶段 +不绘制秒级倒计时,所以可以跨秒复用。账户、头像和主题变化也失效。 +头像至多2MiB、400万像素;过大或损坏降级默认图。不下载头像, +头像长缓存、请求超时和平台媒体ID复用属于接入层职责。 + +RenderService默认一个绘图任务,最多8个活跃/排队的不同请求; +超过容量明确失败,由接入层提示稍后重试。同一展示请求共享任务。 +调用方取消只取消自己的等待,后台仍占用名额直至结束;close等待 +所有已接受任务排空。合并的是绘制,不合并调用前的快照查询。 + +## 素材完整性与来源 + +scripts/export_render_assets.py将673张已有素材原样打包,附202种作物 +展示配置和旧布局。--check比对确定性输出,不修改旧基线。 +素材包约束为安装资源,不按用户输入路径访问任意文件、不解压到磁盘。 +保留GPL及原始归属;历史素材分发权仍需在公开发布前核实。 + +旧基线按当前阶段算法有455个缺失阶段引用;不是本次导出丢失。 +缺失时显示问号占位并记录静态警告,避免整个命令失败;尚未补齐 +缺失图片,也不推测相近作物素材替代。不会声称202种作物均有完整图片。 + +## 测量与验证 + +```text +python scripts/render_demo.py --font /path/to/chinese-font.ttf +python scripts/export_render_assets.py --check +python scripts/verify_foundation.py +``` + +本轮测量原始结果见 [render-benchmark.json](render-benchmark.json)。 +样例为构造的18块解锁地,包含空地、生长、成熟、枯萎与锁定地。 +输出和日志位于runtime/render-demo,不进入Git。 + +| 尺寸 | 编码 | 字节 | 冷请求毫秒 | 命中缓存毫秒 | +|---|---|---:|---:|---:| +| 1024×624 | WebP75/method2 | 85,820 | 192.195 | 0.559 | +| 1024×624 | JPEG75 | 112,309 | 119.514 | 0.319 | +| 1024×624 | PNG | 1,083,976 | 153.524 | 0.271 | +| 1536×936 | WebP85/method2 | 236,462 | 208.278 | 0.327 | +| 1536×936 | JPEG85 | 293,733 | 136.219 | 0.274 | +| 1536×936 | PNG | 2,168,696 | 208.414 | 0.285 | + +这是一台本机的单次观测,不是旧版性能对照,也不是等画质比较。 +每组使用新的渲染器测冷缓存,字体/操作系统文件缓存可能已有预热; +未测低配服务器、峰值RSS、Linux运行或任何真实平台上传。 +普通样例WebP比JPEG小约24%,但编码约60ms,JPEG约4.5ms; +优先级取决于部署CPU与带宽,应保留独立格式配置。 + +日志记录query_ms、queue_ms、compose_ms、encode_ms、尺寸、编码、 +字节与成图命中,不记录真实姓名、作用域、头像或完整快照。 +上传阶段由适配器单独记录,不能将发送接口返回当作客户端已显示。 + +正式检查结果:45项测试全部通过,827文件基线与两类可复现导出 +检查通过。独立只读Verifier核对完整核心/渲染规格和源码后,A1-A13 +全部通过。验收报告见 [Comet报告]( +comet/changes/farm-core-foundation/verification.md)。通用Skill桥接不提供 +机器独立身份认证,流程仍要求用户明确接受后才归档。 diff --git a/docs/README.md b/docs/README.md index 77026e4..07c3529 100644 --- a/docs/README.md +++ b/docs/README.md @@ -1,10 +1,13 @@ # 文档索引 记录日期:2026-10-15。状态:已形成设计草案,待关键产品决策确认; -业务重构、多框架接入、发布流水线和真实数据迁移尚未实施。 +第一批基础核心已实施;多框架接入、发布和真实数据迁移尚未实施。 | 文档 | 用途 | |---|---| +| [09-foundation.md](09-foundation.md) | 已实现的第一批核心、API、测试与限制 | +| [10-rendering.md](10-rendering.md) | 共享图片工具、农场绘制、缓存、编码与接入 | +| [render-benchmark.json](render-benchmark.json) | 本机合成数据的冷暖缓存与编码测量 | | [01-roadmap.md](01-roadmap.md) | 长期目标、实施顺序、里程碑与验收 | | [02-architecture.md](02-architecture.md) | 分层、目录、依赖、接入与接口设计 | | [03-business-rules.md](03-business-rules.md) | 身份、货币、库存、作物、事务与幂等 | @@ -26,6 +29,6 @@ - “已确认”必须有源码、命令或文档来源;“建议”不能当作已接受需求。 - 文档中的目标结构是设计,不表示对应代码已实现。 - 发布仓库是分发目标,不是 Git 子模块;各框架使用相互独立的插件包。 -- 主仓库远程地址尚未提供;不将任一插件发布仓库误设为主仓库 origin。 +- 主仓库远程为 Shu-Ying/main_plugin_farm;本轮未执行远程操作。 - 用户已撤回拉取要求,且要求删除本地克隆;三个本地检出已删除,不再访问远程。 - 本轮仅修正迁移副本中的硬编码凭据;业务缺陷记录在审查报告,没有隐式修复。 diff --git a/docs/comet/changes/farm-core-foundation/brief.md b/docs/comet/changes/farm-core-foundation/brief.md new file mode 100644 index 0000000..f9794c5 --- /dev/null +++ b/docs/comet/changes/farm-core-foundation/brief.md @@ -0,0 +1,126 @@ +# Outcome + +在 D:/Work/farm 当前目录完成旧版可用规则的第一批迁移,建立不依赖 +真寻、NoneBot 或 AstrBot 的可运行基础核心;使用 SQLite 验证完整交易 +闭环,为后续框架接入提供稳定调用入口。Comet 初始化已完成, +AGENTS.md 缺失开始标记已修复。用户已明确选择当前目录继续。 + +# Scope + +- 迁移基线只读。迁出旧作物目录 202 条唯一名称配置,保留必要数值、 + 购买币种、等级、成熟时间、产量、经验和多季次数;新核心运行不依赖 + legacy 目录,不复制旧框架依赖。共享渲染素材按下述新增范围迁出。 +- 建立明确的玩家作用域标识,由调用方提供部署、领域、消息平台与 + 用户 ID,不自行选择群共享或跨框架账号绑定政策。 +- SQLite 新 schema、显式事务、操作流水和幂等操作记录;只处理新建 + 测试数据库,不导入真实玩家库。余额与库存以数据库为权威。 +- 注册初始 500 农场币、0 点券、0 经验、3 块普通空地,沿用旧值。 +- 提供账户/库存/地块查询、购买种子、播种、收获、铲除枯萎作物、 + 锁定/解锁作物、指定出售/全部出售及基础文本结果。 +- 每级累计经验门槛沿用 200*k*(k+1)/2,以整数计算避免浮点边界。 + 普通币与点券库存独立;本轮不实现两者兑换或外部金币充值。 +- 多季作物首熟时间沿用 time*3600;再生长间隔沿用旧实现去重后 + phase[1:] 之和。该规则明确保留兼容,数值平衡后续另议。 +- 纯规则支持每块地独立计算产量/经验加成再求和;首轮创建普通地, + 不提供土地升级、星级自动升级或偷菜加成。 +- 核心应用 API 为异步可调用,SQLite 阻塞操作不在事件循环线程执行; + 不引入可变玩家余额缓存或导入时后台线程。日志记录事务操作标识、 + 操作类别与提交结果,不记录密钥或真实用户消息。 +- 使用本地可重复测试覆盖规则、原子性、失败传播、重试幂等和并发; + 更新 README、实现说明与现有路线状态,保留待确认决策。 +- 根据用户对已讨论方案的明确执行指令,追加 farm_render:Pillow 同步 + 图片工具、离线素材包、有界缓存、按目标尺寸绘制、后台受控执行、 + 普通/高清预览、WebP/JPEG/PNG 编码和分阶段日志。 +- 渲染接收一次核心快照和统一时间,不逐地块查库、不联网;头像字节 + 由接入方提供,失败使用默认头像。保留旧农场背景、地块和作物素材。 +- 按可见状态缓存成图并在生长阶段边界失效;并发相同请求合并。 + 提供离线生成、性能记录、安装包独立运行和中文字体配置说明。 + +## Directory structure + +### Created + +- src/farm_render/:画布、素材、展示模型、布局、渲染器及异步入口。 +- scripts/export_render_assets.py、scripts/render_demo.py。 +- tests/test_render*.py、docs/10-rendering.md。 + +### Modified + +- pyproject.toml:render 可选依赖及渲染素材打包。 +- scripts/verify_foundation.py:增加渲染资源可复现检查。 +- README.md、docs/README.md、docs/task_plan.md、docs/progress.md。 +- 当前 brief 与新增 farm-render 目标规格;原业务规格保留。 + +### Deleted + +无。 + +### Not created + +- 不创建真实平台插件、远端工作流、真实玩家库或字体再分发包。 +- 不修改 legacy 基线,不创建多个框架的业务副本。 + +# Non-goals + +本轮不实现真实宿主插件、消息命令、签到、偷菜、活动、 +外部金币兑换、点券兑换、土地升级、星级进阶、账号绑定或在线联机。 +不读取或迁移真实玩家数据,不重置旧资产,不改旧基线业务。 +不 clone/fetch/pull/push 任何远程,不创建 PR、Release 或发布工作流。 +不将设计占位目录报告为已支持三框架。 + +# Acceptance examples + +- A1: 旧基线 827 文件校验继续通过;新核心不导入三框架、无导入副作用, + 可从安装包或源码独立运行,核心不通过路径引用 legacy。 +- A2: 可复现导出 202 条作物配置,名称和必要数值对应源静态数据库; + 本地加载支持格式校验,坏数据明确失败,不访问网络。 +- A3: 注册立即可查询 500 农场币、0 点券、0 经验、3 块空地; + 重复注册不重复赠送;不同显式作用域不串号。 +- A4: 购买到播种全过程扣款、入库、耗种和地块改变正确;0、负数、 + 布尔值、超大数量、余额/库存不足、等级不足和不存在对象明确失败, + 不留下部分写入。 +- A5: 未成熟不能收获;成熟后只增加本次产量/经验;历史经验 100、 + 本次获得20时为120;多地加成逐块计算且交换顺序不改变总和; + 多季次数及枯萎/铲除遵守保存的规则与可注入时钟。 +- A6: 出售收益只增加一次,例如余额100、收益20后为120; + 锁定同时约束指定与全部出售;返回结构化真实余额及安全文本, + 不发生旧版模板参数缺失。 +- A7: 模拟交易中途异常时余额、库存、地块及流水整体回滚; + 同一操作标识重试返回原结果、参数冲突拒绝;并发写入不超卖、 + 不重复领取,不吞数据库故障伪装成功;重启后数据可读。 +- A8: 提供可执行的离线测试、运行入口和使用说明,保留真实宿主、 + 真实库迁移及远程发布未验证的说明;文档记录已实现范围和剩余项。 +- A9: 独立图片工具支持透明合成、缩放、裁剪、文本测量/省略和三格式编码; + 渲染不导入框架、不联网,核心保持无 Pillow 依赖;安装包含所需素材。 +- A10: 从一次快照绘制空地、生长、成熟、再生长、枯萎及锁定地块, + 使用统一时间、旧布局和正确偏移;输出普通/高清可解码图片。 +- A11: 小画布从开始按目标尺寸绘制;素材和结果缓存有内存上限, + 共享素材不被污染;阶段/成熟边界、账户、头像及作用域变化不复用旧图。 +- A12: 合成和编码在后台执行且限制并发,相同请求合并;取消和异常 + 不提前释放运行名额、不泄漏后台任务;记录耗时、尺寸和缓存状态。 +- A13: 离线生成农场样例、对比尺寸/编码与冷暖缓存,保存测量报告和 + 接入文档;验证旧基线及原有核心测试,明确尚未接入真实消息平台。 + +# Constraints and invariants + +源码使用 Python,4 空格、80 字符行长,明确说明数据模型字段。 +保留 GPLv3 许可与源归属。共享核心内部相对导入。 +运行数据和测试临时数据库不入 Git。新实现只使用必要依赖, +优先标准库 SQLite、异步线程执行和 unittest,不为框架引入依赖。 +新数据库 schema 与旧库区分,未知或更高 schema 明确拒绝。 +同一基础交易的存储和应用紧密耦合,使用单个 change,不拆分子任务。 + +# Decisions + +- 用户已授权初始化 Comet 并执行基础开发;工作区为当前目录 main。 +- 主仓库地址已明确:https://github.com/Shu-Ying/main_plugin_farm.git。 +- 现有文档是设计参考,旧代码是实现参考;本轮仅覆盖上述第一批核心, + 不把整份长期计划视为一次必须全部实施的产品需求。 +- 未决的农场共享、联机、存量资产、兑换与素材发布不由本轮擅自决定。 +- 用户随后明确要求“根据上述方案和之前优化的绘制我的农场,请执行”, + 授权图片层及绘图优化。沿用已选当前目录;该目录已有未提交核心, + Runtime 不允许第二个 current change,因此作为基础开发范围补充。 +- 用户讨论后明确“确认并进入开发”,包含 WebP 优先候选及 JPEG/PNG + 兼容输出;尺寸、质量、编码力度独立配置,平台能力由适配器声明。 +- 素材保留原 GPL 归属,不捆绑未确认再分发许可的字体;部署显式配置 + 中文字体路径。真实宿主头像缓存和上传由后续适配器实现。 diff --git a/docs/comet/changes/farm-core-foundation/comet-state.yaml b/docs/comet/changes/farm-core-foundation/comet-state.yaml new file mode 100644 index 0000000..2559175 --- /dev/null +++ b/docs/comet/changes/farm-core-foundation/comet-state.yaml @@ -0,0 +1,377 @@ +schema: comet.native.v4 +name: farm-core-foundation +language: en +phase: verify +status: await-user +state_version: 14 +brief: brief.md +document_constraints_version: 3 +shape_confirmation_hash: 49a2a3fb4be0159531d63bcdd52c5895f2586e309c33be91fe4e38c7a159461d +spec_changes: + - capability: farm-core + operation: create + source: specs/farm-core/spec.md + - capability: farm-render + operation: create + source: specs/farm-render/spec.md +workspace: + isolation: current + change_branch: main + target_branch: main + finish: null +loop: + stage: await-user + goal_cycle: 3 + iteration: 1 + attempt: 1 + retry_epoch: 0 + failed_iteration_count: 0 + no_progress_count: 0 + execution_failure_count: 0 + previous_unresolved_ids: [] + next_action: confirm-skill-coordinated-pass +acceptance: + - id: A1 + source: brief.md + text: "A1: 旧基线 827 文件校验继续通过;新核心不导入三框架、无导入副作用, 可从安装包或源码独立运行,核心不通过路径引用 legacy。" + result: passed + reason: + text: 827文件基线正式检查通过;检查包导入无线程/文件副作用及wheel独立安装,核心无框架和legacy运行依赖。 + truncated: false + - id: A2 + source: brief.md + text: "A2: 可复现导出 202 条作物配置,名称和必要数值对应源静态数据库; 本地加载支持格式校验,坏数据明确失败,不访问网络。" + result: passed + reason: + text: 只读源库导出按id排序并带源hash;202项导出检查通过,加载器校验版本、整数、阶段和重复定义。 + truncated: false + - id: A3 + source: brief.md + text: "A3: 注册立即可查询 500 农场币、0 点券、0 经验、3 块空地; 重复注册不重复赠送;不同显式作用域不串号。" + result: passed + reason: + text: 账户注册与初始500资产、三空地在事务内完成;重复注册及身份作用域隔离测试正式通过。 + truncated: false + - id: A4 + source: brief.md + text: "A4: 购买到播种全过程扣款、入库、耗种和地块改变正确;0、负数、 布尔值、超大数量、余额/库存不足、等级不足和不存在对象明确失败, + 不留下部分写入。" + result: passed + reason: + text: 购买和播种在完整事务内扣款、入库、耗种、存图;数量、等级、余额和空地边界测试正式通过。 + truncated: false + - id: A5 + source: brief.md + text: "A5: 未成熟不能收获;成熟后只增加本次产量/经验;历史经验 100、 本次获得20时为120;多地加成逐块计算且交换顺序不改变总和; + 多季次数及枯萎/铲除遵守保存的规则与可注入时钟。" + result: passed + reason: + text: 成熟边界、经验增量、多季再生长及枯萎清除源实现与规格一致,逐地块加成及顺序测试通过。 + truncated: false + - id: A6 + source: brief.md + text: "A6: 出售收益只增加一次,例如余额100、收益20后为120; 锁定同时约束指定与全部出售;返回结构化真实余额及安全文本, + 不发生旧版模板参数缺失。" + result: passed + reason: + text: 锁定状态对指定及全部出售生效,出售只增本次收入,Receipt和文本使用真实余额;正式测试通过。 + truncated: false + - id: A7 + source: brief.md + text: "A7: 模拟交易中途异常时余额、库存、地块及流水整体回滚; 同一操作标识重试返回原结果、参数冲突拒绝;并发写入不超卖、 + 不重复领取,不吞数据库故障伪装成功;重启后数据可读。" + result: passed + reason: + text: BEGIN IMMEDIATE、操作指纹与结果同事务保存;异常回滚、并发超卖/重复收获、取消重放、重启与schema测试通过。 + truncated: false + - id: A8 + source: brief.md + text: "A8: 提供可执行的离线测试、运行入口和使用说明,保留真实宿主、 真实库迁移及远程发布未验证的说明;文档记录已实现范围和剩余项。" + result: passed + reason: + text: README、核心说明、离线入口与测试可用,准确说明尚无真实宿主、真实库迁移和远程发布验证。 + truncated: false + - id: A9 + source: brief.md + text: "A9: 独立图片工具支持透明合成、缩放、裁剪、文本测量/省略和三格式编码; 渲染不导入框架、不联网,核心保持无 Pillow + 依赖;安装包含所需素材。" + result: passed + reason: + text: Pillow可选渲染模块支持透明合成、复制、缩放、裁剪、文字测量省略及三格式;核心无Pillow导入,wheel携带673素材并独立渲染通过。 + truncated: false + - id: A10 + source: brief.md + text: "A10: 从一次快照绘制空地、生长、成熟、再生长、枯萎及锁定地块, 使用统一时间、旧布局和正确偏移;输出普通/高清可解码图片。" + result: passed + reason: + text: 纯场景转换消费一次快照和统一now,首季/再季锚点与枯萎优先正确;旧30块布局及真实offset字段已迁出,普通高清解码与阶段测试通过,样例已视觉检查。 + truncated: false + - id: A11 + source: brief.md + text: "A11: 小画布从开始按目标尺寸绘制;素材和结果缓存有内存上限, 共享素材不被污染;阶段/成熟边界、账户、头像及作用域变化不复用旧图。" + result: passed + reason: + text: 目标尺寸直接合成;素材/底图与成图LRU有字节预算且返回副本;键含作用域、账户、阶段、头像、主题及编码配置,边界变化不复用旧图。 + truncated: false + - id: A12 + source: brief.md + text: "A12: 合成和编码在后台执行且限制并发,相同请求合并;取消和异常 不提前释放运行名额、不泄漏后台任务;记录耗时、尺寸和缓存状态。" + result: passed + reason: + text: 整体合成编码由to_thread执行,semaphore限制并发、pending限制容量,shield维持取消后的任务容量并在关闭排空;合并、异常重试、事件循环和并发测试通过,日志不含用户标识。 + truncated: false + - id: A13 + source: brief.md + text: "A13: 离线生成农场样例、对比尺寸/编码与冷暖缓存,保存测量报告和 接入文档;验证旧基线及原有核心测试,明确尚未接入真实消息平台。" + result: passed + reason: + text: 离线脚本生成两尺寸三格式冷暖测量及日志,报告与已查看样例一致;正式检查45项和基线/两类导出通过,文档明确本机单次、未测上传/低配/Linux及真实平台。 + truncated: false +builder_handoff: + candidate_id: e88969d9-32c0-4321-a5a3-658023a60414 + identity_provider: skill-coordinated + builder_execution_ref: skill-coordinated:builder:455faebd-84e1-49d0-8041-c6f44318a0ea + iteration: 1 + summary: + text: 保留原框架无关业务核心,追加共享Pillow图片工具与农场快照渲染。673原素材独立打包,目标尺寸合成、三种编码、阶段变化失效、有界缓存、受控后台任务、取消排空和日志。13项绘图定向测试及3项打包检查通过,样图已目视检查。尚未执行本轮完整核心回归,由Runtime正式执行;真实框架与上传未接入,旧素材455个阶段引用缺图。 + truncated: false + addressed_acceptance_ids: + - A1 + - A2 + - A3 + - A4 + - A5 + - A6 + - A7 + - A8 + - A9 + - A10 + - A11 + - A12 + - A13 + acceptance_review: + - id: A1 + status: implemented-with-evidence + evidence: + - scripts/check_baseline.py + - tests/test_packaging.py + - src/farm_core/__init__.py + note: 已有实现保留并检查关联源码,完整回归列入本轮Runtime计划。 + - id: A2 + status: implemented-with-evidence + evidence: + - scripts/export_catalogue.py + - tests/test_domain.py + note: 已有实现保留并检查关联源码,完整回归列入本轮Runtime计划。 + - id: A3 + status: implemented-with-evidence + evidence: + - tests/test_transactions.py + - src/farm_core/application.py + note: 已有实现保留并检查关联源码,完整回归列入本轮Runtime计划。 + - id: A4 + status: implemented-with-evidence + evidence: + - tests/test_transactions.py + - src/farm_core/application.py + note: 已有实现保留并检查关联源码,完整回归列入本轮Runtime计划。 + - id: A5 + status: implemented-with-evidence + evidence: + - tests/test_domain.py + - tests/test_transactions.py + note: 已有实现保留并检查关联源码,完整回归列入本轮Runtime计划。 + - id: A6 + status: implemented-with-evidence + evidence: + - tests/test_transactions.py + - src/farm_core/presentation.py + note: 已有实现保留并检查关联源码,完整回归列入本轮Runtime计划。 + - id: A7 + status: implemented-with-evidence + evidence: + - tests/test_transactions.py + - src/farm_core/storage.py + note: 已有实现保留并检查关联源码,完整回归列入本轮Runtime计划。 + - id: A8 + status: implemented-with-evidence + evidence: + - README.md + - docs/09-foundation.md + - scripts/verify_foundation.py + note: 已有实现保留并检查关联源码,完整回归列入本轮Runtime计划。 + - id: A9 + status: implemented-with-evidence + evidence: + - src/farm_render/canvas.py + - tests/test_render.py + - tests/test_packaging.py + note: 新实现对应已确认规格,定向测试和离线样例可复核。 + - id: A10 + status: implemented-with-evidence + evidence: + - src/farm_render/layout.py + - src/farm_render/renderer.py + - tests/test_render.py + note: 新实现对应已确认规格,定向测试和离线样例可复核。 + - id: A11 + status: implemented-with-evidence + evidence: + - src/farm_render/assets.py + - src/farm_render/renderer.py + - tests/test_render.py + note: 新实现对应已确认规格,定向测试和离线样例可复核。 + - id: A12 + status: implemented-with-evidence + evidence: + - src/farm_render/service.py + - tests/test_render.py + note: 新实现对应已确认规格,定向测试和离线样例可复核。 + - id: A13 + status: implemented-with-evidence + evidence: + - scripts/render_demo.py + - docs/10-rendering.md + - docs/render-benchmark.json + note: 新实现对应已确认规格,定向测试和离线样例可复核。 + checks: + - name: + text: render-behavior + truncated: false + result: passed + note: + text: 13 tests passed + truncated: false + - name: + text: wheel-packaging + truncated: false + result: passed + note: + text: 3 tests passed; installed renderer generated PNG + truncated: false + - name: + text: visual-demo + truncated: false + result: passed + note: + text: 1024 WebP sample reviewed; three formats measured + truncated: false + - name: + text: full-regression + truncated: false + result: not-run + note: + text: 本轮完整检查交给Runtime执行 + truncated: false + checks_truncated: false + known_limits: + - text: 真实框架、上传、Linux和低配主机尚未运行验证。 + truncated: false + - text: 中文字体需部署方合法提供;不捆绑字体。 + truncated: false + - text: 旧基线455个阶段引用缺图,已占位降级。 + truncated: false + - text: 测量为本机单次合成数据,不是旧版或等画质基准。 + truncated: false + - text: 公开发布前仍需核实历史素材分发权。 + truncated: false + known_limits_truncated: false + review: null + submitted_at: 2026-10-15T08:36:49.457Z +blockers: + - owner: user + reason: + text: The generic Skill bridge cannot prove an independent Verifier execution; + user confirmation is required before Archive. + truncated: false + acceptance_ids: [] + resolution_action: await-user +verification: + candidate_id: e88969d9-32c0-4321-a5a3-658023a60414 + identity_provider: skill-coordinated + verifier_execution_ref: skill-coordinated:verifier:853b70de-beda-44dd-b303-cf2b3b300676 + iteration: 1 + attempt: 1 + assurance: skill-coordinated + verdict: pass + checks: + - id: offline-foundation-render + name: + text: Offline core and renderer + truncated: false + argv_display: + - text: scripts/verify_foundation.py + truncated: false + argv_truncated: false + cwd_ref: . + status: passed + exit_code: 0 + duration_ms: 10608 + summary: + text: 独立只读核对全部13项、完整规格、核心与渲染源码、测试/打包/导出脚本、正式Runtime日志和图片样例;复用匹配候选的45项正式检查,全部验收通过。图索引新增渲染未覆盖且旧文件metadata_changed,已直接源文件补齐,无需重复完整套件。 + truncated: false + risks: + - text: 真实消息平台、上传链路、Linux及低配服务器尚未验证;性能报告仅本机单次构造样例,不等画质对照。 + truncated: false + - text: 旧素材存在455个阶段引用缺失,已按规格显示问号并记录警告;不能宣称202种作物图片完整。 + truncated: false + - text: 部署需自备有权使用且包含所需中文字形的字体;当前只验证字体可加载,任意Unicode/Emoji覆盖不保证。 + truncated: false + - text: 历史素材公开分发权仍需在发布前核实。 + truncated: false + - text: 缓存预算为像素/编码内容核算,含预留,不代表整个进程峰值RSS硬上限。 + truncated: false + risks_truncated: false + completed_at: 2026-10-15T08:40:00.608Z +history: + - goal_cycle: 1 + iteration: 1 + attempt: 1 + outcome: pass + unresolved_ids: [] + summary: + text: 独立只读验收完成:完整brief/Spec、实现、测试、配置和文档均已核对,最后参考Builder交接;绑定候选的Runtime + foundation-offline以退出码0完成,827基线文件、可复现目录导出与32项测试通过。A1-A8全部通过;未追加或自行重跑完整检查,未修改项目源文件。 + truncated: false + completed_at: 2026-10-15T07:00:29.324Z + - goal_cycle: 1 + iteration: 1 + attempt: 1 + outcome: recovery + unresolved_ids: [] + summary: + text: 用户明确要求在已确认的图片工具及绘图优化方案上执行开发;将共享农场渲染作为当前基础开发的范围补充,保留原业务验收项并追加图片验收。 + truncated: false + completed_at: 2026-10-15T07:55:29.688Z + - goal_cycle: 2 + iteration: 0 + attempt: 0 + outcome: recovery + unresolved_ids: [] + summary: + text: Native confirmed acceptance criteria changed + truncated: false + completed_at: 2026-10-15T08:23:15.134Z + - goal_cycle: 3 + iteration: 1 + attempt: 1 + outcome: pass + unresolved_ids: [] + summary: + text: 独立只读核对全部13项、完整规格、核心与渲染源码、测试/打包/导出脚本、正式Runtime日志和图片样例;复用匹配候选的45项正式检查,全部验收通过。图索引新增渲染未覆盖且旧文件metadata_changed,已直接源文件补齐,无需重复完整套件。 + truncated: false + completed_at: 2026-10-15T08:40:00.608Z +history_overflow: + dropped_entries: 0 + first_dropped_at: null + last_dropped_at: null + outcome_counts: + pass: 0 + fail: 0 + blocked: 0 + execution-error: 0 + recovery: 0 +verification_result: pass +verification_report: verification.md +archived: false +created_at: 2026-10-15T06:34:33.214Z diff --git a/docs/comet/changes/farm-core-foundation/specs/farm-core/spec.md b/docs/comet/changes/farm-core-foundation/specs/farm-core/spec.md new file mode 100644 index 0000000..43892b1 --- /dev/null +++ b/docs/comet/changes/farm-core-foundation/specs/farm-core/spec.md @@ -0,0 +1,102 @@ +# Farm Core Foundation + +本规格定义第一批框架无关基础核心的完整行为,覆盖注册、配置、 +资产库存、种植交易、文本展示和本地持久化。 +真实机器人入口、联机和旧玩家数据迁移不在本 capability 范围内。 + +## Runtime and migration boundary + +共享代码位于 src/farm_core,包内部相对导入;加载模块不执行网络、 +创建数据库或启动线程。可注入显式配置、时钟与存储生命周期。 +静态作物目录从保留的旧作物库离线导出,发布或安装后不依赖 legacy。 +完整静态目录与玩家数据库分开保存,旧基线保持只读。 +SQLite 工作在线程中执行且每个用例拥有完整事务,异步调用可等待结果。 + +Scenario: Independent core and preserved baseline +Acceptance: A1 +基线离线校验通过,包独立导入、安装、调用不需要三种宿主或 legacy。 + +## Catalogue + +源静态目录 202 条作物名称唯一;映射 id/name/level/buy/isVip/vipBuy/ +experience/harvest/price/time/crop/phase 等必要数据,规范整数数值, +异常的字段或版本错误明确拒绝。购买使用对应币种,按等级校验。 +不下载远程目录,不将素材路径或绘图偏移当作业务依赖。 +多季再生长间隔保持旧去重 phase[1:] 求和规则,不在首轮重新平衡。 + +Scenario: Reproducible catalogue migration +Acceptance: A2 +离线导出再生成内容一致;202 条与源值匹配;损坏输入被拒绝。 + +## Registration and account model + +调用方提供 deployment/realm/platform/user 字符串身份,核心不推断账号 +等价性或跨群共享。首次注册赠送500 point、0 vipPoint、0 exp、3空地。 +唯一身份只能领取一次初始资产。查询总是基于已提交数据库状态。 +经验等级门槛为 200*k*(k+1)//2,使用整数边界计算。 +货币、数量和时间范围必须适合 SQLite 整数及应用限制。 +bool 不作为合法整数参数,不允许负数归零后当作交易成功。 + +Scenario: Immediate registration and scope isolation +Acceptance: A3 +首次注册立即查询得到默认值;重复注册无增发;不同作用域相互独立。 + +## Purchase and sowing + +购买种子时在同一用例事务内检查注册、作物、数量、等级和余额, +扣款并增加库存,失败整体回滚。播种检查空地和库存,在同一事务内 +消耗实际种子、创建作物实例,保存结算所需目录数据避免后续版本改变 +已播种作物。显式数量超过可用资源返回明确结果,全部操作使用明确 +参数而不是允许任意负数。返回真实操作数、余额及剩余库存。 + +Scenario: Purchase and sow atomically with validated inputs +Acceptance: A4 +非法数量、余额不足、等级不足和库存不足不产生任何部分状态。 + +## Harvest and crop lifecycle + +首熟为播种时间加 time*3600;使用可注入时钟,当前时间等于成熟时可收。 +每块地独立计算 floor(base*(100+bonus)/100) 后相加,不对累计值加成。 +经验只增加本次增量。产物入库、经验变动、收获次数和下一季状态 +在同一事务提交。最后一季后枯萎;铲除只清理枯萎地块。 +再次重试不重复收获。展示结果不再次修改数据。 + +Scenario: Correct experience and multi-crop settlement +Acceptance: A5 +历史100加本次20等于120;地块排序不影响总收益;成熟边界及多季状态正确。 + +## Inventory locking and selling + +指定出售和全部出售均保护锁定物品。出售收入在事务内增加一次, +库存减少为实际成交数量。没有可出售物品时有明确结果。 +查询及返回结构化账户/库存/地块数据,基础文本仅渲染结果,避免模板 +占位不匹配;文本不依赖任何消息框架。 + +Scenario: Consistent locking and exact sale proceeds +Acceptance: A6 +100余额出售收入20变为120;锁定物品无法通过任一出售入口消耗。 + +## Persistence, idempotency and failures + +每个写用例有稳定 operation_id、动作和规范化参数指纹。 +同作用域同标识相同请求返回已提交结果,不再次扣款或发奖; +不同参数冲突拒绝。状态改变、资产流水和结果持久化在同一事务。 +数据库异常应回滚并传播为明确错误,不能转成不存在/无农场。 +支持并发调用,通过事务、条件更新及唯一约束避免超卖和重复结算。 +关闭后重开可读取已提交数据;未知/高版本 schema 拒绝。 +调用取消不能无记录重复交易,已提交结果可通过相同操作标识恢复。 + +Scenario: Rollback, replay and concurrent safety +Acceptance: A7 +故障注入无部分数据;重试幂等;冲突被拒绝;并发不透支;重启后状态一致。 + +## Verification and user documentation + +离线测试采用构造数据,覆盖上述全部验收示例以及源目录一致性。 +提供本地运行/演示入口,不宣称可放入三种框架插件目录加载。 +记录本轮新实现与旧缺陷对应关系,不把历史设计文档误标为全面完成。 +不安装或操作真实机器人,不访问远程仓库,不导入真实玩家数据库。 + +Scenario: Repeatable local verification and accurate status +Acceptance: A8 +安装及离线测试命令可复现,使用说明清楚标明未实现的接入与发布部分。 diff --git a/docs/comet/changes/farm-core-foundation/specs/farm-render/spec.md b/docs/comet/changes/farm-core-foundation/specs/farm-render/spec.md new file mode 100644 index 0000000..dfff486 --- /dev/null +++ b/docs/comet/changes/farm-core-foundation/specs/farm-render/spec.md @@ -0,0 +1,70 @@ +# Farm Render + +## Independent image layer + +farm_render 使用 Pillow 实现同步画布操作;farm_core 不依赖 Pillow。 +工具支持透明合成、复制、缩放、裁剪、文本测量与超长省略,以及 +WebP/JPEG/PNG 编码。JPEG 合并透明背景,输出携带 MIME、尺寸与字节。 +WebP 默认采用有损质量75、method=2;高清质量85。格式与尺寸独立, +auto 根据调用方声明的平台能力选择 WebP 或 JPEG,本机无 WebP 编码 +支持时 auto 回退 JPEG,显式 WebP 则清楚报错。PNG 保留无损选项。 +素材独立打入安装包,不在运行时引用 legacy 或宿主;字体显式配置, +不复制 Windows 商业字体。无字体时明确报错,不输出缺字占位图。 + +Scenario: Framework-independent packaged renderer +Acceptance: A9 +从 wheel 安装到临时目录,无 legacy 和框架仍能加载素材并生成图片。 + +## Snapshot and layout + +入口从业务服务取得一次一致快照;绘图纯消费快照与统一 now,不查库。 +复用旧30块地布局、季节静态背景、草地/普通地和作物阶段素材。 +首季阶段锚点由保存的 mature_at-growth_seconds 推导,再季使用 +mature_at-regrowth_seconds,按本季时长映射展示阶段,成熟优先、 +枯萎优先于成熟。阶段仅影响外观,不修改交易规则和数据库。 +读取实际 offsetX/Y/W/H 字段;缺失作物资源使用显式占位并记录警告。 +用户信息显示真实快照余额、点券和经验,头像仅接收字节,无头像时 +绘制默认图;拒绝超大或无效头像并降级。主题由调用方明确提供。 + +Scenario: Farm states and target profiles +Acceptance: A10 +空地、生长、成熟、再生长、枯萎和未解锁地可生成图片,普通与高清 +使用同一布局;统一时间下同一阶段稳定,边界切换正确。 + +## Resource and result cache + +普通图默认宽1024,高清宽1536;画布与坐标从开始按比例处理。 +解码后素材、底图和成图使用有界 LRU,超限条目不入缓存。 +素材缓存键含路径及目标尺寸,缓存图片只读或复制后修改。 +成图键含完整身份、可见地块阶段/账户/头像摘要、主题、质量与格式; +同阶段可复用,下一阶段或成熟时间到达必须改变键,静态素材版本 +由渲染器实例绑定;资源更新后重新创建实例。默认缓存不落盘。 + +Scenario: Bounded and correct caching +Acceptance: A11 +缓存总字节不超上限;跨账号不串图,阶段、余额、头像变化不命中旧图; +普通图绘制画布即为1024宽,无3072宽中间合成画布。 + +## Controlled asynchronous entry + +提供异步入口,将整张图的合成与编码交给后台线程;默认最多一个 +任务运行,等待队列有上限。相同身份/状态请求共享任务。调用取消 +不取消正在运行的共享绘制,也不提前释放容量。关闭等待任务排空。 +日志只记录查询/排队/合成/编码耗时、字节、尺寸和缓存结果,不记录 +真实用户标识、姓名、头像、凭据或完整快照。 + +Scenario: Concurrency, cancellation and diagnostics +Acceptance: A12 +慢绘图时事件循环仍可工作;同时运行数有界,重复请求共享任务, +失败可重试;取消后后台完成并正确释放容量,关闭无残留任务。 + +## Verification and usage + +提供明确中文字体路径的离线示例,使用构造数据和临时数据库, +报告冷暖耗时、尺寸、三种编码字节及缓存占用。测量仅代表本机样例, +不宣称低配服务器耗时或真实平台上传性能。保留核心离线验证与 +旧基线校验,不接入/重启真实机器人,不访问远端。 + +Scenario: Repeatable offline rendering +Acceptance: A13 +离线样例可解码,性能报告真实可复现,文档提供接入调用和未验证边界。 diff --git a/docs/comet/changes/farm-core-foundation/verification.md b/docs/comet/changes/farm-core-foundation/verification.md new file mode 100644 index 0000000..ba6cd92 --- /dev/null +++ b/docs/comet/changes/farm-core-foundation/verification.md @@ -0,0 +1,80 @@ +--- +generated_from_state_version: 14 +--- + +# Verification + +## Current result + +- Result: **Verification passed; your confirmation is required** +- Verification status: **Checks completed, but your confirmation is required** +- Goal cycle: 3 +- Iteration: 1 +- Verifier attempt: 1 +- Completed: 2026-10-15T08:40:00.608Z +- Summary: 独立只读核对全部13项、完整规格、核心与渲染源码、测试/打包/导出脚本、正式Runtime日志和图片样例;复用匹配候选的45项正式检查,全部验收通过。图索引新增渲染未覆盖且旧文件metadata_changed,已直接源文件补齐,无需重复完整套件。 + +## Acceptance + +| ID | Result | Source | Criterion | Reason | +| --- | --- | --- | --- | --- | +| A1 | passed | brief.md | A1: 旧基线 827 文件校验继续通过;新核心不导入三框架、无导入副作用, 可从安装包或源码独立运行,核心不通过路径引用 legacy。 | 827文件基线正式检查通过;检查包导入无线程/文件副作用及wheel独立安装,核心无框架和legacy运行依赖。 | +| A2 | passed | brief.md | A2: 可复现导出 202 条作物配置,名称和必要数值对应源静态数据库; 本地加载支持格式校验,坏数据明确失败,不访问网络。 | 只读源库导出按id排序并带源hash;202项导出检查通过,加载器校验版本、整数、阶段和重复定义。 | +| A3 | passed | brief.md | A3: 注册立即可查询 500 农场币、0 点券、0 经验、3 块空地; 重复注册不重复赠送;不同显式作用域不串号。 | 账户注册与初始500资产、三空地在事务内完成;重复注册及身份作用域隔离测试正式通过。 | +| A4 | passed | brief.md | A4: 购买到播种全过程扣款、入库、耗种和地块改变正确;0、负数、 布尔值、超大数量、余额/库存不足、等级不足和不存在对象明确失败, 不留下部分写入。 | 购买和播种在完整事务内扣款、入库、耗种、存图;数量、等级、余额和空地边界测试正式通过。 | +| A5 | passed | brief.md | A5: 未成熟不能收获;成熟后只增加本次产量/经验;历史经验 100、 本次获得20时为120;多地加成逐块计算且交换顺序不改变总和; 多季次数及枯萎/铲除遵守保存的规则与可注入时钟。 | 成熟边界、经验增量、多季再生长及枯萎清除源实现与规格一致,逐地块加成及顺序测试通过。 | +| A6 | passed | brief.md | A6: 出售收益只增加一次,例如余额100、收益20后为120; 锁定同时约束指定与全部出售;返回结构化真实余额及安全文本, 不发生旧版模板参数缺失。 | 锁定状态对指定及全部出售生效,出售只增本次收入,Receipt和文本使用真实余额;正式测试通过。 | +| A7 | passed | brief.md | A7: 模拟交易中途异常时余额、库存、地块及流水整体回滚; 同一操作标识重试返回原结果、参数冲突拒绝;并发写入不超卖、 不重复领取,不吞数据库故障伪装成功;重启后数据可读。 | BEGIN IMMEDIATE、操作指纹与结果同事务保存;异常回滚、并发超卖/重复收获、取消重放、重启与schema测试通过。 | +| A8 | passed | brief.md | A8: 提供可执行的离线测试、运行入口和使用说明,保留真实宿主、 真实库迁移及远程发布未验证的说明;文档记录已实现范围和剩余项。 | README、核心说明、离线入口与测试可用,准确说明尚无真实宿主、真实库迁移和远程发布验证。 | +| A9 | passed | brief.md | A9: 独立图片工具支持透明合成、缩放、裁剪、文本测量/省略和三格式编码; 渲染不导入框架、不联网,核心保持无 Pillow 依赖;安装包含所需素材。 | Pillow可选渲染模块支持透明合成、复制、缩放、裁剪、文字测量省略及三格式;核心无Pillow导入,wheel携带673素材并独立渲染通过。 | +| A10 | passed | brief.md | A10: 从一次快照绘制空地、生长、成熟、再生长、枯萎及锁定地块, 使用统一时间、旧布局和正确偏移;输出普通/高清可解码图片。 | 纯场景转换消费一次快照和统一now,首季/再季锚点与枯萎优先正确;旧30块布局及真实offset字段已迁出,普通高清解码与阶段测试通过,样例已视觉检查。 | +| A11 | passed | brief.md | A11: 小画布从开始按目标尺寸绘制;素材和结果缓存有内存上限, 共享素材不被污染;阶段/成熟边界、账户、头像及作用域变化不复用旧图。 | 目标尺寸直接合成;素材/底图与成图LRU有字节预算且返回副本;键含作用域、账户、阶段、头像、主题及编码配置,边界变化不复用旧图。 | +| A12 | passed | brief.md | A12: 合成和编码在后台执行且限制并发,相同请求合并;取消和异常 不提前释放运行名额、不泄漏后台任务;记录耗时、尺寸和缓存状态。 | 整体合成编码由to_thread执行,semaphore限制并发、pending限制容量,shield维持取消后的任务容量并在关闭排空;合并、异常重试、事件循环和并发测试通过,日志不含用户标识。 | +| A13 | passed | brief.md | A13: 离线生成农场样例、对比尺寸/编码与冷暖缓存,保存测量报告和 接入文档;验证旧基线及原有核心测试,明确尚未接入真实消息平台。 | 离线脚本生成两尺寸三格式冷暖测量及日志,报告与已查看样例一致;正式检查45项和基线/两类导出通过,文档明确本机单次、未测上传/低配/Linux及真实平台。 | + +## Checks + +| Check | Command | Working directory | Status | Exit | Duration | +| --- | --- | --- | --- | ---: | ---: | +| Offline core and renderer | scripts/verify_foundation.py | . | passed | 0 | 10608 ms | + +### Builder-reported evidence + +These are Builder reports, not Runtime check receipts or independent verification results. + +- render-behavior: passed — 13 tests passed +- wheel-packaging: passed — 3 tests passed; installed renderer generated PNG +- visual-demo: passed — 1024 WebP sample reviewed; three formats measured +- full-regression: not-run — 本轮完整检查交给Runtime执行 +- Known limitation: 真实框架、上传、Linux和低配主机尚未运行验证。 +- Known limitation: 中文字体需部署方合法提供;不捆绑字体。 +- Known limitation: 旧基线455个阶段引用缺图,已占位降级。 +- Known limitation: 测量为本机单次合成数据,不是旧版或等画质基准。 +- Known limitation: 公开发布前仍需核实历史素材分发权。 + +## Blockers + +- **user**: The generic Skill bridge cannot prove an independent Verifier execution; user confirmation is required before Archive. — next: `await-user` + +## Risks and skipped work + +- 真实消息平台、上传链路、Linux及低配服务器尚未验证;性能报告仅本机单次构造样例,不等画质对照。 +- 旧素材存在455个阶段引用缺失,已按规格显示问号并记录警告;不能宣称202种作物图片完整。 +- 部署需自备有权使用且包含所需中文字形的字体;当前只验证字体可加载,任意Unicode/Emoji覆盖不保证。 +- 历史素材公开分发权仍需在发布前核实。 +- 缓存预算为像素/编码内容核算,含预留,不代表整个进程峰值RSS硬上限。 + +## Previous iterations + +| Goal cycle | Iteration | Attempt | Outcome | Unresolved | Summary | Completed | +| ---: | ---: | ---: | --- | --- | --- | --- | +| 1 | 1 | 1 | pass | — | 独立只读验收完成:完整brief/Spec、实现、测试、配置和文档均已核对,最后参考Builder交接;绑定候选的Runtime foundation-offline以退出码0完成,827基线文件、可复现目录导出与32项测试通过。A1-A8全部通过;未追加或自行重跑完整检查,未修改项目源文件。 | 2026-10-15T07:00:29.324Z | +| 1 | 1 | 1 | recovery | — | 用户明确要求在已确认的图片工具及绘图优化方案上执行开发;将共享农场渲染作为当前基础开发的范围补充,保留原业务验收项并追加图片验收。 | 2026-10-15T07:55:29.688Z | +| 2 | 0 | 0 | recovery | — | Native confirmed acceptance criteria changed | 2026-10-15T08:23:15.134Z | +| 3 | 1 | 1 | pass | — | 独立只读核对全部13项、完整规格、核心与渲染源码、测试/打包/导出脚本、正式Runtime日志和图片样例;复用匹配候选的45项正式检查,全部验收通过。图索引新增渲染未覆盖且旧文件metadata_changed,已直接源文件补齐,无需重复完整套件。 | 2026-10-15T08:40:00.608Z | + + + +## Conclusion + +独立只读核对全部13项、完整规格、核心与渲染源码、测试/打包/导出脚本、正式Runtime日志和图片样例;复用匹配候选的45项正式检查,全部验收通过。图索引新增渲染未覆盖且旧文件metadata_changed,已直接源文件补齐,无需重复完整套件。 diff --git a/docs/progress.md b/docs/progress.md index 85028aa..9546b13 100644 --- a/docs/progress.md +++ b/docs/progress.md @@ -18,3 +18,37 @@ 13. 文档链接、差异和工具错误路径在最后交付核对中记录到验证结果。 未实现新业务、未提交或推送、未配置远程发布权限。 + +## 基础开发(用户确认后) + +- Comet change farm-core-foundation进入Build,当前目录main。 +- 迁出202条作物目录,核心代码独立于legacy和三种框架。 +- 7项领域、22项事务、3项打包测试分别通过。 +- 覆盖事务故障、60路并发购买、20路同标识重放、并发收获、 + 线程中操作被调用方取消、重启与schema拒绝。 +- 测试首轮出现2项Windows文件占用,已修复测试自己的连接关闭。 +- 根README和09-foundation记录运行方法、缺陷对应与未完成范围。 +- 候选即将提交Runtime检查,尚未宣称独立验收通过。 + +## 正式验证完成 + +- Runtime执行verify_foundation.py成功,基线、202条目录与32项测试通过。 +- 新的只读Verifier逐项核对完整规格与源码,A1至A8全部通过。 +- Runtime已接受结果,当前等待用户接受,不代用户归档。 +- 通用Skill桥接不能机器认证独立执行,因此保留用户确认边界。 +- 没有执行远程访问、提交、推送、发布或真实玩家数据迁移。 + +## 共享图片开发 + +- 用户明确确认进入开发,已有核心范围补充A9-A13并包含WebP。 +- 先观察10项缺少新模块的行为测试失败,再实现图片层;随后13项通过。 +- wheel在临时安装目录独立加载素材并生成PNG通过;格式检查发现一行 + 超80字符,已按定位修正,待正式检查复核。 +- 673原图片打入独立素材包;旧基线未修改。发现455个原阶段缺图引用, + 使用问号占位和静态警告,未声称素材已补齐。 +- 生成并目视检查1024宽WebP样例;三格式测量写入render-benchmark.json。 +- 当前候选待Comet完整检查和独立验收,真实框架及上传未验证。 +- Runtime完整检查通过:45项测试、827文件基线、作物目录和素材导出 + 可重复检查全部成功。已启动独立只读Verifier检查A1-A13,尚待结论。 +- 独立Verifier已完成完整规格/源码/测试和实际样图核对,A1-A13全部 + 通过,Runtime接受报告并等待用户接受;未执行归档、提交或发布。 diff --git a/docs/render-benchmark.json b/docs/render-benchmark.json new file mode 100644 index 0000000..2361971 --- /dev/null +++ b/docs/render-benchmark.json @@ -0,0 +1,101 @@ +{ + "python": "3.12.14", + "os": "Windows", + "pillow": "12.3.0", + "synthetic_data": true, + "scope": "本机单次样例;非旧版对照,未测上传或低配服务器", + "asset_count": 673, + "legacy_missing_stage_references": 455, + "measurements": [ + { + "width": 1024, + "height": 624, + "codec": "WEBP", + "quality": 75, + "method": 2, + "bytes": 85820, + "cold_ms": 192.195, + "compose_ms": 131.62, + "encode_ms": 59.955, + "warm_ms": 0.559, + "warm_cache_hit": true, + "asset_cache_bytes": 5248724, + "result_cache_bytes": 89916 + }, + { + "width": 1024, + "height": 624, + "codec": "JPEG", + "quality": 75, + "method": 2, + "bytes": 112309, + "cold_ms": 119.514, + "compose_ms": 114.538, + "encode_ms": 4.475, + "warm_ms": 0.319, + "warm_cache_hit": true, + "asset_cache_bytes": 5248724, + "result_cache_bytes": 116405 + }, + { + "width": 1024, + "height": 624, + "codec": "PNG", + "quality": 75, + "method": 2, + "bytes": 1083976, + "cold_ms": 153.524, + "compose_ms": 111.887, + "encode_ms": 41.111, + "warm_ms": 0.271, + "warm_cache_hit": true, + "asset_cache_bytes": 5248724, + "result_cache_bytes": 1088072 + }, + { + "width": 1536, + "height": 936, + "codec": "WEBP", + "quality": 85, + "method": 2, + "bytes": 236462, + "cold_ms": 208.278, + "compose_ms": 130.485, + "encode_ms": 76.9, + "warm_ms": 0.327, + "warm_cache_hit": true, + "asset_cache_bytes": 11809468, + "result_cache_bytes": 240558 + }, + { + "width": 1536, + "height": 936, + "codec": "JPEG", + "quality": 85, + "method": 2, + "bytes": 293733, + "cold_ms": 136.219, + "compose_ms": 124.821, + "encode_ms": 10.86, + "warm_ms": 0.274, + "warm_cache_hit": true, + "asset_cache_bytes": 11809468, + "result_cache_bytes": 297829 + }, + { + "width": 1536, + "height": 936, + "codec": "PNG", + "quality": 85, + "method": 2, + "bytes": 2168696, + "cold_ms": 208.414, + "compose_ms": 124.066, + "encode_ms": 83.863, + "warm_ms": 0.285, + "warm_cache_hit": true, + "asset_cache_bytes": 11809468, + "result_cache_bytes": 2172792 + } + ] +} diff --git a/docs/render-implementation-plan.md b/docs/render-implementation-plan.md new file mode 100644 index 0000000..5741dd5 --- /dev/null +++ b/docs/render-implementation-plan.md @@ -0,0 +1,34 @@ +# 共享农场绘图实施计划 + +目标:按已确认 farm-render 规格恢复图片展示并减少生成和传输开销。 +架构:核心快照 → 不可变展示数据 → Pillow 同步合成 → 受控异步入口。 +技术:Python 3.10+、Pillow、unittest;核心零第三方运行依赖不变。 +规格:docs/comet/changes/farm-core-foundation/specs/farm-render/spec.md。 + +## 约束与审查重点 + +- legacy 不改;运行时不访问 legacy、不联网、不复制未授权字体。 +- 缓存按字节限制;共享素材复制后使用,跨作用域不串图。 +- 关注成熟/再生长边界、极长昵称、坏头像、编码能力和调用取消。 +- 不接真实平台、不发布;真实上传能力仍待适配器确认。 + +## 实施顺序 + +- [x] 先写 tests/test_render.py:透明合成、三种编码、状态转换、 + 缓存隔离/失效/上限、无效头像、异步取消/排队/错误/关闭。 + 运行并确认缺失新功能导致失败。 +- [x] src/farm_render/canvas.py、models.py:图片原语和输入/输出模型。 +- [x] scripts/export_render_assets.py:可重复打包选定旧素材及展示元数据; + src/farm_render/assets.py:受限读取与 LRU,layout.py:纯阶段转换。 +- [x] renderer.py:目标尺寸底图、作物、真实账户面板、三格式编码。 +- [x] service.py:一次快照、受控并发、重复请求合并、取消和关闭排空。 +- [x] scripts/render_demo.py:生成普通/高清和编码对比;观察实际成图。 +- [x] pyproject.toml 增加 render extra、资源打包;测试 wheel 独立渲染。 +- [x] docs/10-rendering.md 保存用法、测量、边界,更新索引和进度。 +- [x] 定向测试通过后提交 Comet 检查,由独立只读 Verifier 验收 A1-A13。 + +不自动提交、归档或推送;最终验收结果交用户接受。 + +正式Runtime检查:45项测试、827文件基线和两类导出检查通过; +独立只读Verifier核对全部A1-A13并全部判定通过,Runtime已接受报告。 +当前等待用户接受结果,不自动归档。 diff --git a/docs/task_plan.md b/docs/task_plan.md index d3fc2be..945a227 100644 --- a/docs/task_plan.md +++ b/docs/task_plan.md @@ -45,3 +45,26 @@ | 用户要求停止拉取时三个 clone 已完成 | 如实说明,收到删除要求后逐个校验路径并删除 | 没有尝试绕过用户停止拉取要求,也没有执行远程发布。 + +## 第二轮:基础迁移开发 + +用户已确认范围并选择当前目录,Comet已进入Build。 +- [x] 修复Comet管理区并完成Native初始化。 +- [x] 202条配置离线迁移,建立独立核心包。 +- [x] 注册、查询、购买、播种、收获、铲除、锁定及出售。 +- [x] SQLite事务、流水、幂等、并发及取消恢复。 +- [x] 32项定向测试通过;文档与打包入口已完成。 +- [x] Runtime完整检查与独立Verifier验收:32项测试、8/8验收通过。 +- [ ] 用户接受验收结果后归档;本轮不推送发布。 + +开发期问题:测试连接未关闭导致Windows清理失败,已修复并复测。 +主仓库origin由用户设置为Shu-Ying/main_plugin_farm,未访问远程。 + +## 第三轮:共享农场图片 + +- [x] 用户确认方案并补充WebP,当前目录进入Build。 +- [x] 独立Pillow工具、快照展示、673素材原样打包、缓存和受控任务。 +- [x] 13项渲染定向测试通过,wheel独立图片生成通过。 +- [x] 生成普通/高清样例并检查文字布局,保存三格式测量和使用文档。 +- [x] 正式Runtime检查45项测试通过;独立Verifier判定全部A1-A13通过。 +- [ ] 用户接受结果后归档。 diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..bcf0aaa --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,27 @@ +[build-system] +requires = ["setuptools>=68"] +build-backend = "setuptools.build_meta" + +[project] +name = "farm-core" +version = "0.1.0" +description = "Framework-independent farm foundation" +readme = "README.md" +requires-python = ">=3.10" +license = {file = "LICENSE"} +dependencies = [] + +[project.optional-dependencies] +render = ["Pillow>=10.4,<13"] + +[tool.setuptools.packages.find] +where = ["src"] + +[tool.setuptools.package-data] +farm_core = ["data/*.json"] +farm_render = ["data/*.json", "data/*.zip", "data/*.md"] + +[tool.ruff] +line-length = 80 +target-version = "py310" +extend-exclude = ["legacy", ".agents", ".comet"] diff --git a/release/targets.json b/release/targets.json index d810897..9e0d0a2 100644 --- a/release/targets.json +++ b/release/targets.json @@ -1,7 +1,7 @@ { "schema": "farm.release-targets.v1", "status": "design-only", - "source_repository": null, + "source_repository": "https://github.com/Shu-Ying/main_plugin_farm.git", "network_actions_enabled": false, "targets": [ { diff --git a/scripts/export_catalogue.py b/scripts/export_catalogue.py new file mode 100644 index 0000000..4818fe8 --- /dev/null +++ b/scripts/export_catalogue.py @@ -0,0 +1,59 @@ +"""从只读旧作物库可复现导出独立目录,不访问玩家库或远程。""" + +import argparse +from contextlib import closing +import hashlib +import json +from pathlib import Path +import sqlite3 + + +def export(source: Path) -> bytes: + source = source.resolve() + with closing(sqlite3.connect( + source.as_uri() + "?mode=ro", uri=True, + )) as connection: + connection.row_factory = sqlite3.Row + rows = [ + dict(row) for row in connection.execute( + "SELECT * FROM plant ORDER BY id" + ) + ] + if not rows or len({row["name"] for row in rows}) != len(rows): + raise ValueError("Invalid or duplicate catalogue") + document = { + "schema": 1, + "source_sha256": hashlib.sha256(source.read_bytes()).hexdigest(), + "plants": rows, + } + return ( + json.dumps(document, ensure_ascii=False, sort_keys=True, indent=2) + + "\n" + ).encode("utf-8") + + +def main(): + root = Path(__file__).resolve().parents[1] + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument( + "--source", type=Path, + default=root / "legacy/zhenxun_plugin_farm/resource/db/plant.db", + ) + parser.add_argument( + "--output", type=Path, + default=root / "src/farm_core/data/plants.json", + ) + parser.add_argument("--check", action="store_true") + args = parser.parse_args() + content = export(args.source) + if args.check: + if not args.output.is_file() or args.output.read_bytes() != content: + raise SystemExit("Catalogue differs; regenerate it explicitly") + else: + args.output.parent.mkdir(parents=True, exist_ok=True) + args.output.write_bytes(content) + print("Catalogue checked" if args.check else "Catalogue exported") + + +if __name__ == "__main__": + main() diff --git a/scripts/export_render_assets.py b/scripts/export_render_assets.py new file mode 100644 index 0000000..7fd519c --- /dev/null +++ b/scripts/export_render_assets.py @@ -0,0 +1,74 @@ +"""可复现复制旧绘图素材;不修改图片、不包含字体或玩家数据。""" + +import argparse +import hashlib +from io import BytesIO +import json +from pathlib import Path +from zipfile import ZIP_STORED, ZipFile, ZipInfo + +from PIL import Image + + +def export(root): + legacy = root / "legacy/zhenxun_plugin_farm" + source = legacy / "resource" + paths = list((source / "plant").rglob("*.png")) + paths += [source / f"soil/{name}.png" for name in ( + "草土地", "普通土地", + )] + paths += [source / "background/expansion.png"] + paths += [source / "background/ripe.png"] + paths += [source / ( + f"background/decorateIcon/decorate_{theme}_1.jpg" + ) for theme in ("default", "spring", "summer", "autumn", "winter")] + layout = json.loads((legacy / "config/soil.json").read_bytes()) + catalogue = json.loads(( + root / "src/farm_core/data/plants.json" + ).read_bytes()) + metadata = { + "schema": 1, "canvas": [3072, 1872], "soil_size": layout["size"], + "positions": [layout["soil"][str(i)] for i in range(1, 31)], + "plants": {row["name"]: { + key: row[key] for key in ( + "phase", "general", "offsetX", "offsetY", "offsetW", "offsetH", + ) + } for row in catalogue["plants"]}, "images": {}, + } + output = BytesIO() + with ZipFile(output, "w", compression=ZIP_STORED) as bundle: + for path in sorted(paths): + name = path.relative_to(source).as_posix() + content = path.read_bytes() + with Image.open(BytesIO(content)) as image: + size = image.size + metadata["images"][name] = { + "size": size, "sha256": hashlib.sha256(content).hexdigest(), + } + bundle.writestr(ZipInfo(name, (2020, 1, 1, 0, 0, 0)), content) + return output.getvalue(), ( + json.dumps(metadata, ensure_ascii=False, sort_keys=True, indent=2) + + "\n" + ).encode("utf-8") + + +def main(): + root = Path(__file__).resolve().parents[1] + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("--check", action="store_true") + args = parser.parse_args() + files = zip(("assets.zip", "metadata.json"), export(root)) + destination = root / "src/farm_render/data" + for name, content in files: + path = destination / name + if args.check: + if not path.is_file() or path.read_bytes() != content: + raise SystemExit(f"Render asset differs: {name}") + else: + destination.mkdir(parents=True, exist_ok=True) + path.write_bytes(content) + print("Render assets checked" if args.check else "Render assets exported") + + +if __name__ == "__main__": + main() diff --git a/scripts/render_demo.py b/scripts/render_demo.py new file mode 100644 index 0000000..1400cd2 --- /dev/null +++ b/scripts/render_demo.py @@ -0,0 +1,108 @@ +"""离线样例与本机测量,不访问真实玩家库或平台。""" + +import argparse +from dataclasses import replace +import json +import logging +from pathlib import Path +import platform +import sys +from time import perf_counter + +from PIL import __version__ as pillow_version, features + +from farm_core import Catalogue, Identity +from farm_core.domain import Account, Plot, Snapshot +from farm_render import FarmRenderer, RenderOptions + + +def demonstration(): + catalogue = Catalogue.bundled() + plants = [p for p in catalogue if not p.name.startswith(( + "一星", "二星", "三星", "四星", "五星", + ))] + now = 36000 + plots = [] + for index in range(1, 19): + plant = plants[(index - 1) % min(len(plants), 20)] + if index % 6 == 0: + plots.append(Plot(index)) + continue + mature_at = now - 10 if index % 3 == 0 else ( + now + plant.growth_seconds // 3 + ) + plot = Plot(index, plant, mature_at) + if index == 11: + plot = replace(plot, harvest_count=plant.crop) + plots.append(plot) + return Snapshot( + Account("舒影的四季农场", 12580, 36, 3420), (), tuple(plots), + ), now + + +def main(): + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("--font", type=Path, required=True) + parser.add_argument( + "--output", type=Path, default=Path("runtime/render-demo"), + ) + parser.add_argument("--report", type=Path) + args = parser.parse_args() + args.output.mkdir(parents=True, exist_ok=True) + logging.basicConfig( + filename=args.output / "render.log", encoding="utf-8", + level=logging.INFO, + format="%(asctime)s %(levelname)s %(name)s %(message)s", + ) + who = Identity("demo", "local", "local", "synthetic-farmer") + snapshot, now = demonstration() + rows = [] + for width, quality in ((1024, 75), (1536, 85)): + for codec in ("WEBP", "JPEG", "PNG"): + if codec == "WEBP" and not features.check("webp"): + continue + renderer = FarmRenderer(args.font) + options = RenderOptions( + width=width, quality=quality, format=codec, theme="spring", + ) + started = perf_counter() + result = renderer.render(who, snapshot, now, options) + cold_ms = (perf_counter() - started) * 1000 + started = perf_counter() + warm = renderer.render(who, snapshot, now, options) + warm_ms = (perf_counter() - started) * 1000 + suffix = {"WEBP":"webp", "JPEG":"jpg", "PNG":"png"}[codec] + output = args.output / f"farm-{width}.{suffix}" + output.write_bytes(result.content) + rows.append({ + "width": width, "height": result.height, "codec": codec, + "quality": quality, "method": options.method, + "bytes": len(result.content), "cold_ms": round(cold_ms, 3), + "compose_ms": round(result.compose_ms, 3), + "encode_ms": round(result.encode_ms, 3), + "warm_ms": round(warm_ms, 3), "warm_cache_hit": warm.cache_hit, + "asset_cache_bytes": renderer.assets.cache.used_bytes, + "result_cache_bytes": renderer.results.used_bytes, + }) + metadata = renderer.assets.metadata + missing = sum( + f"plant/{name}/{stage}.png" not in metadata["images"] + for name, info in metadata["plants"].items() + for stage in range(1, len(dict.fromkeys(info["phase"].split(",")))+1) + ) + report = { + "python": platform.python_version(), "os": platform.system(), + "pillow": pillow_version, "synthetic_data": True, + "scope": "本机单次样例;非旧版对照,未测上传或低配服务器", + "asset_count": len(metadata["images"]), + "legacy_missing_stage_references": missing, "measurements": rows, + } + content = json.dumps(report, ensure_ascii=False, indent=2) + "\n" + (args.report or args.output / "benchmark.json").write_text( + content, encoding="utf-8", + ) + print(content) + + +if __name__ == "__main__": + main() diff --git a/scripts/verify_foundation.py b/scripts/verify_foundation.py new file mode 100644 index 0000000..b2509a3 --- /dev/null +++ b/scripts/verify_foundation.py @@ -0,0 +1,32 @@ +"""最终离线检查入口,由 Comet 执行并保存正式检查结果。""" + +import os +from pathlib import Path +import subprocess +import sys + + +def main(): + root = Path(__file__).resolve().parents[1] + environment = { + **os.environ, "PYTHONPATH": str(root / "src"), "PIP_NO_INDEX": "1", + "PYTHONIOENCODING": "utf-8", + } + commands = ( + [sys.executable, "scripts/check_baseline.py"], + [sys.executable, "scripts/export_catalogue.py", "--check"], + [sys.executable, "scripts/export_render_assets.py", "--check"], + [ + sys.executable, "-m", "unittest", "discover", + "-s", "tests", "-p", "test_*.py", "-v", + ], + ) + for command in commands: + result = subprocess.run(command, cwd=root, env=environment) + if result.returncode: + return result.returncode + return 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/src/farm_core/README.md b/src/farm_core/README.md index 6a9ac50..fd85553 100644 --- a/src/farm_core/README.md +++ b/src/farm_core/README.md @@ -1,6 +1,6 @@ -# 共享核心预留位置 +# 共享基础核心 -当前只有职责说明,未实现新核心。完整目标见 docs/02-architecture.md。 -实施时创建 domain、application、ports、infrastructure、presentation。 -禁止导入 nonebot、astrbot、zhenxun;内部模块采用相对导入。 -先实现注册、库存、资产和一个完整交易,不直接复制旧全局管理器。 +本轮已实现框架无关的作物目录、注册、交易及种植闭环。 +模块责任与限制见 docs/09-foundation.md,运行方式见根 README。 +运行无第三方依赖,导入不创建数据库、文件或后台线程。 +本包不是机器人插件入口;integrations 下接入尚未实现。 diff --git a/src/farm_core/__init__.py b/src/farm_core/__init__.py new file mode 100644 index 0000000..91ad3eb --- /dev/null +++ b/src/farm_core/__init__.py @@ -0,0 +1,11 @@ +"""框架无关的农场基础核心,导入时不启动服务。""" + +from .application import FarmService +from .catalogue import Catalogue +from .domain import Asset, FarmError, Identity +from .storage import SQLiteStore + +__all__ = [ + "Asset", "Catalogue", "FarmError", "FarmService", "Identity", + "SQLiteStore", +] diff --git a/src/farm_core/__main__.py b/src/farm_core/__main__.py new file mode 100644 index 0000000..c5c77f2 --- /dev/null +++ b/src/farm_core/__main__.py @@ -0,0 +1,55 @@ +"""离线演示;只有显式调用才创建指定数据库。""" + +import argparse +import asyncio +import logging +from pathlib import Path + +from . import Catalogue, FarmService, Identity, SQLiteStore +from .presentation import receipt_text, snapshot_text + + +async def demo(database): + clock = [0] + catalogue = Catalogue.bundled() + plant = min( + (p for p in catalogue if p.level == 0 and not p.is_vip), + key=lambda p: p.cost, + ) + store = SQLiteStore(database) + await store.initialize() + service = FarmService(store, catalogue, lambda: clock[0]) + identity = Identity("demo", "local", "local", "farmer") + try: + for receipt in ( + await service.register(identity, "演示农场", "demo-register"), + await service.buy(identity, plant.name, 1, "demo-buy"), + await service.sow(identity, plant.name, 1, "demo-sow"), + ): + print(receipt_text(receipt)) + clock[0] = plant.growth_seconds + print(receipt_text(await service.harvest(identity, "demo-harvest"))) + print(receipt_text(await service.sell( + identity, None, None, "demo-sell", + ))) + print(snapshot_text(await service.snapshot(identity))) + finally: + await store.close() + + +def main(): + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("--database", required=True) + args = parser.parse_args() + database = Path(args.database).resolve() + database.parent.mkdir(parents=True, exist_ok=True) + logging.basicConfig( + filename=database.with_suffix(".log"), encoding="utf-8", + level=logging.INFO, + format="%(asctime)s %(levelname)s %(name)s %(message)s", + ) + asyncio.run(demo(database)) + + +if __name__ == "__main__": + main() diff --git a/src/farm_core/application.py b/src/farm_core/application.py new file mode 100644 index 0000000..ee28393 --- /dev/null +++ b/src/farm_core/application.py @@ -0,0 +1,243 @@ +"""应用用例:一次业务动作对应一次完整存储事务。""" + +from dataclasses import asdict, replace +import time + +from .catalogue import Catalogue +from .domain import ( + Asset, FarmError, Identity, Plot, boosted, integer, quantity, text, +) +from .ports import Store + + +class FarmService: + def __init__(self, store: Store, catalogue: Catalogue, clock=None): + self.store = store + self.catalogue = catalogue + self.clock = clock if clock is not None else lambda: int(time.time()) + + @staticmethod + def _identity(identity): + if not isinstance(identity, Identity): + raise FarmError("invalid_identity") + + async def _execute( + self, identity, operation_id, action, parameters, operation, + ): + self._identity(identity) + text(operation_id) + now = integer(self.clock()) + return await self.store.transact( + identity, operation_id, action, parameters, now, + lambda repository: operation(repository, now), + ) + + @staticmethod + def _result(repository, **details): + account = repository.account() + return { + **details, "account": asdict(account), "level": account.level, + } + + async def snapshot(self, identity: Identity): + self._identity(identity) + return await self.store.snapshot(identity) + + async def register( + self, identity: Identity, name: str, operation_id: str, + ): + text(name, 64) + + def operation(repository, now): + created = repository.register(name) + return self._result(repository, created=created) + + return await self._execute( + identity, operation_id, "register", {"name": name}, operation, + ) + + async def buy( + self, identity: Identity, name: str, count: int, operation_id: str, + ): + text(name) + quantity(count) + + def operation(repository, now): + plant = self.catalogue.get(name) + if repository.account().level < plant.level: + raise FarmError("level_required") + cost = integer(plant.cost * count) + repository.asset(plant.asset, -cost) + repository.stock(name, seeds=count) + return self._result( + repository, name=name, quantity=count, cost=cost, + asset=plant.asset.value, + seeds=repository.item(name).seeds, + ) + + return await self._execute( + identity, operation_id, "buy", + {"name": name, "count": count}, operation, + ) + + async def sow( + self, identity: Identity, name: str, count: int | None, + operation_id: str, + ): + text(name) + if count is not None: + quantity(count) + + def operation(repository, now): + repository.account() + plant = self.catalogue.get(name) + available = repository.item(name).seeds + empty = [p for p in repository.plots() if p.plant is None] + amount = min(available, len(empty)) if count is None else count + if amount == 0 or amount > available: + raise FarmError("insufficient_stock") + if amount > len(empty): + raise FarmError("insufficient_empty_plots") + mature_at = integer(now + plant.growth_seconds) + repository.stock(name, seeds=-amount) + for plot in empty[:amount]: + repository.save_plot(replace( + plot, plant=plant, mature_at=mature_at, harvest_count=0, + )) + return self._result( + repository, name=name, quantity=amount, + slots=[p.slot for p in empty[:amount]], + seeds=repository.item(name).seeds, + ) + + return await self._execute( + identity, operation_id, "sow", + {"name": name, "count": count}, operation, + ) + + async def harvest(self, identity: Identity, operation_id: str): + def operation(repository, now): + repository.account() + harvested = [] + total_experience = 0 + for plot in repository.plots(): + plant = plot.plant + if ( + plant is None or plot.withered or plot.mature_at is None + or plot.mature_at > now + ): + continue + amount = boosted(plant.harvest, plot.yield_bonus) + experience = boosted( + plant.experience, plot.experience_bonus, + ) + repository.stock(plant.name, crops=amount) + total_experience = integer(total_experience + experience) + count = plot.harvest_count + 1 + mature_at = ( + integer(now + plant.regrowth_seconds) + if count < plant.crop else None + ) + repository.save_plot(replace( + plot, mature_at=mature_at, harvest_count=count, + )) + harvested.append({ + "slot": plot.slot, "name": plant.name, + "quantity": amount, "experience": experience, + }) + if not harvested: + raise FarmError("nothing_to_harvest") + repository.asset(Asset.EXPERIENCE, total_experience) + return self._result( + repository, harvested=harvested, + experience=total_experience, + quantity=sum(item["quantity"] for item in harvested), + ) + + return await self._execute( + identity, operation_id, "harvest", {}, operation, + ) + + async def eradicate(self, identity: Identity, operation_id: str): + def operation(repository, now): + repository.account() + withered = [p for p in repository.plots() if p.withered] + if not withered: + raise FarmError("nothing_to_eradicate") + for plot in withered: + repository.save_plot(replace( + plot, plant=None, mature_at=None, harvest_count=0, + )) + return self._result( + repository, slots=[p.slot for p in withered], + ) + + return await self._execute( + identity, operation_id, "eradicate", {}, operation, + ) + + async def lock( + self, identity: Identity, name: str, locked: bool, + operation_id: str, + ): + text(name) + if type(locked) is not bool: + raise FarmError("invalid_lock") + + def operation(repository, now): + repository.account() + self.catalogue.get(name) + repository.lock(name, locked) + return self._result(repository, name=name, locked=locked) + + return await self._execute( + identity, operation_id, "lock", + {"name": name, "locked": locked}, operation, + ) + + async def sell( + self, identity: Identity, name: str | None, count: int | None, + operation_id: str, + ): + if name is not None: + text(name) + if count is not None: + quantity(count) + if name is None: + raise FarmError("name_required_for_quantity") + + def operation(repository, now): + repository.account() + if name is not None: + item = repository.item(name) + if item.locked: + raise FarmError("plant_locked") + amount = item.crops if count is None else count + if amount <= 0 or amount > item.crops: + raise FarmError("insufficient_stock") + items = [(name, amount)] + else: + items = [ + (item.name, item.crops) + for item in repository.items() + if not item.locked and item.crops > 0 + ] + if not items: + raise FarmError("nothing_to_sell") + proceeds = 0 + sold = [] + for plant_name, amount in items: + plant = self.catalogue.get(plant_name) + proceeds = integer(proceeds + plant.price * amount) + repository.stock(plant_name, crops=-amount) + sold.append({"name": plant_name, "quantity": amount}) + repository.asset(Asset.POINT, proceeds) + return self._result( + repository, sold=sold, proceeds=proceeds, + quantity=sum(item["quantity"] for item in sold), + ) + + return await self._execute( + identity, operation_id, "sell", + {"name": name, "count": count}, operation, + ) diff --git a/src/farm_core/catalogue.py b/src/farm_core/catalogue.py new file mode 100644 index 0000000..c20fee5 --- /dev/null +++ b/src/farm_core/catalogue.py @@ -0,0 +1,52 @@ +"""只读、离线作物目录加载。""" + +from importlib import resources +import json +from types import MappingProxyType + +from .domain import FarmError, Plant, text + + +class Catalogue: + def __init__(self, plants): + items = tuple(plants) + if not items or any(not isinstance(p, Plant) for p in items): + raise FarmError("invalid_catalogue") + names = {plant.name: plant for plant in items} + if len(names) != len(items): + raise FarmError("duplicate_plant") + if len({p.plant_id for p in items}) != len(items): + raise FarmError("duplicate_plant") + self._plants = MappingProxyType(names) + + def get(self, name: str) -> Plant: + text(name) + try: + return self._plants[name] + except KeyError as exc: + raise FarmError("unknown_plant") from exc + + def __iter__(self): + return iter(self._plants.values()) + + def __len__(self): + return len(self._plants) + + @classmethod + def loads(cls, content: str | bytes) -> "Catalogue": + try: + data = json.loads(content) + if type(data["schema"]) is not int or data["schema"] != 1: + raise FarmError("invalid_catalogue") + if not isinstance(data["plants"], list): + raise FarmError("invalid_catalogue") + return cls(Plant.from_legacy(row) for row in data["plants"]) + except (KeyError, TypeError, ValueError, UnicodeError) as exc: + raise FarmError("invalid_catalogue") from exc + + @classmethod + def bundled(cls) -> "Catalogue": + content = resources.files(__package__).joinpath( + "data", "plants.json" + ).read_bytes() + return cls.loads(content) diff --git a/src/farm_core/data/plants.json b/src/farm_core/data/plants.json new file mode 100644 index 0000000..24d3892 --- /dev/null +++ b/src/farm_core/data/plants.json @@ -0,0 +1,4450 @@ +{ + "plants": [ + { + "buy": 163, + "crop": 1, + "experience": 18, + "general": 1, + "harvest": 17, + "id": 1, + "isBuy": 1, + "isVip": 0, + "level": 0, + "name": "胡萝卜", + "offsetH": 0, + "offsetW": 0, + "offsetX": 0, + "offsetY": 0, + "phase": "7200,18000,32400,46800,46800", + "price": 21, + "sell": 0, + "soil": 0, + "time": 13, + "vipBuy": 0 + }, + { + "buy": 125, + "crop": 1, + "experience": 15, + "general": 1, + "harvest": 16, + "id": 2, + "isBuy": 1, + "isVip": 0, + "level": 0, + "name": "白萝卜", + "offsetH": 0, + "offsetW": 0, + "offsetX": 0, + "offsetY": 0, + "phase": "7200,14400,25200,36000,36000", + "price": 17, + "sell": 0, + "soil": 0, + "time": 10, + "vipBuy": 0 + }, + { + "buy": 80, + "crop": 1, + "experience": 10, + "general": 1, + "harvest": 25, + "id": 3, + "isBuy": 1, + "isVip": 0, + "level": 0, + "name": "牧草", + "offsetH": 0, + "offsetW": 0, + "offsetX": 0, + "offsetY": 0, + "phase": "7200,14400,21600,28800,28800", + "price": 6, + "sell": 0, + "soil": 0, + "time": 8, + "vipBuy": 0 + }, + { + "buy": 168, + "crop": 1, + "experience": 19, + "general": 1, + "harvest": 17, + "id": 4, + "isBuy": 1, + "isVip": 0, + "level": 1, + "name": "大白菜", + "offsetH": 0, + "offsetW": 0, + "offsetX": 0, + "offsetY": 0, + "phase": "7200,14400,25200,36000,50400", + "price": 22, + "sell": 0, + "soil": 0, + "time": 14, + "vipBuy": 0 + }, + { + "buy": 169, + "crop": 1, + "experience": 19, + "general": 1, + "harvest": 17, + "id": 5, + "isBuy": 1, + "isVip": 0, + "level": 1, + "name": "大蒜", + "offsetH": 0, + "offsetW": 0, + "offsetX": 0, + "offsetY": 0, + "phase": "7200,18000,25200,36000,50400", + "price": 22, + "sell": 0, + "soil": 0, + "time": 14, + "vipBuy": 0 + }, + { + "buy": 168, + "crop": 1, + "experience": 19, + "general": 1, + "harvest": 18, + "id": 6, + "isBuy": 1, + "isVip": 0, + "level": 2, + "name": "水稻", + "offsetH": 0, + "offsetW": 0, + "offsetX": 0, + "offsetY": 0, + "phase": "7200,14400,25200,36000,50400", + "price": 21, + "sell": 0, + "soil": 0, + "time": 14, + "vipBuy": 0 + }, + { + "buy": 168, + "crop": 1, + "experience": 19, + "general": 1, + "harvest": 18, + "id": 7, + "isBuy": 1, + "isVip": 0, + "level": 2, + "name": "小麦", + "offsetH": 0, + "offsetW": 0, + "offsetX": 0, + "offsetY": 0, + "phase": "7200,14400,25200,36000,50400", + "price": 21, + "sell": 0, + "soil": 0, + "time": 14, + "vipBuy": 0 + }, + { + "buy": 175, + "crop": 1, + "experience": 19, + "general": 1, + "harvest": 17, + "id": 8, + "isBuy": 1, + "isVip": 0, + "level": 3, + "name": "玉米", + "offsetH": 0, + "offsetW": 0, + "offsetX": 0, + "offsetY": 0, + "phase": "7200,14400,25200,36000,50400", + "price": 23, + "sell": 0, + "soil": 0, + "time": 14, + "vipBuy": 0 + }, + { + "buy": 194, + "crop": 1, + "experience": 29, + "general": 1, + "harvest": 23, + "id": 9, + "isBuy": 1, + "isVip": 0, + "level": 4, + "name": "油菜", + "offsetH": 0, + "offsetW": 0, + "offsetX": 0, + "offsetY": 0, + "phase": "14400,28800,46800,61200,61200", + "price": 24, + "sell": 0, + "soil": 0, + "time": 17, + "vipBuy": 0 + }, + { + "buy": 195, + "crop": 1, + "experience": 25, + "general": 1, + "harvest": 21, + "id": 10, + "isBuy": 1, + "isVip": 0, + "level": 4, + "name": "生菜", + "offsetH": 0, + "offsetW": 0, + "offsetX": 0, + "offsetY": 0, + "phase": "10800,21600,36000,50400,68400", + "price": 24, + "sell": 0, + "soil": 0, + "time": 19, + "vipBuy": 0 + }, + { + "buy": 195, + "crop": 1, + "experience": 15, + "general": 1, + "harvest": 18, + "id": 11, + "isBuy": 1, + "isVip": 0, + "level": 4, + "name": "小白菜", + "offsetH": 0, + "offsetW": 0, + "offsetX": 0, + "offsetY": 0, + "phase": "7200,14400,25200,39600,39600", + "price": 21, + "sell": 0, + "soil": 0, + "time": 11, + "vipBuy": 0 + }, + { + "buy": 188, + "crop": 1, + "experience": 20, + "general": 1, + "harvest": 18, + "id": 12, + "isBuy": 1, + "isVip": 0, + "level": 4, + "name": "土豆", + "offsetH": 0, + "offsetW": 0, + "offsetX": 0, + "offsetY": 0, + "phase": "7200,14400,25200,39600,54000", + "price": 24, + "sell": 0, + "soil": 0, + "time": 15, + "vipBuy": 0 + }, + { + "buy": 237, + "crop": 1, + "experience": 21, + "general": 1, + "harvest": 20, + "id": 13, + "isBuy": 1, + "isVip": 0, + "level": 5, + "name": "红枣", + "offsetH": 0, + "offsetW": 0, + "offsetX": 0, + "offsetY": 0, + "phase": "7200,18000,28800,43200,57600", + "price": 25, + "sell": 0, + "soil": 0, + "time": 16, + "vipBuy": 0 + }, + { + "buy": 237, + "crop": 1, + "experience": 21, + "general": 1, + "harvest": 20, + "id": 14, + "isBuy": 1, + "isVip": 0, + "level": 5, + "name": "茄子", + "offsetH": 0, + "offsetW": 0, + "offsetX": 0, + "offsetY": 0, + "phase": "7200,18000,28800,43200,57600", + "price": 25, + "sell": 0, + "soil": 0, + "time": 16, + "vipBuy": 0 + }, + { + "buy": 258, + "crop": 1, + "experience": 37, + "general": 1, + "harvest": 28, + "id": 15, + "isBuy": 1, + "isVip": 0, + "level": 6, + "name": "花菜", + "offsetH": 0, + "offsetW": 0, + "offsetX": 0, + "offsetY": 0, + "phase": "21600,43200,64800,82800,100800", + "price": 26, + "sell": 0, + "soil": 0, + "time": 28, + "vipBuy": 0 + }, + { + "buy": 251, + "crop": 1, + "experience": 22, + "general": 1, + "harvest": 21, + "id": 16, + "isBuy": 1, + "isVip": 0, + "level": 6, + "name": "韭菜", + "offsetH": 0, + "offsetW": 0, + "offsetX": 0, + "offsetY": 0, + "phase": "7200,18000,32400,46800,61200", + "price": 26, + "sell": 0, + "soil": 0, + "time": 17, + "vipBuy": 0 + }, + { + "buy": 251, + "crop": 1, + "experience": 22, + "general": 1, + "harvest": 21, + "id": 17, + "isBuy": 1, + "isVip": 0, + "level": 6, + "name": "番茄", + "offsetH": 0, + "offsetW": 0, + "offsetX": 0, + "offsetY": 0, + "phase": "7200,18000,32400,46800,61200", + "price": 26, + "sell": 0, + "soil": 0, + "time": 17, + "vipBuy": 0 + }, + { + "buy": 99999, + "crop": 1, + "experience": 22, + "general": 1, + "harvest": 21, + "id": 18, + "isBuy": 1, + "isVip": 0, + "level": 6, + "name": "ATTomato", + "offsetH": 0, + "offsetW": 0, + "offsetX": 0, + "offsetY": 0, + "phase": "7200,18000,32400,46800,61200", + "price": 7000, + "sell": 0, + "soil": 0, + "time": 17, + "vipBuy": 0 + }, + { + "buy": 251, + "crop": 1, + "experience": 23, + "general": 1, + "harvest": 22, + "id": 19, + "isBuy": 1, + "isVip": 0, + "level": 7, + "name": "红玫瑰", + "offsetH": 0, + "offsetW": 0, + "offsetX": 0, + "offsetY": 0, + "phase": "10800,21600,32400,46800,64800", + "price": 27, + "sell": 0, + "soil": 0, + "time": 18, + "vipBuy": 0 + }, + { + "buy": 264, + "crop": 1, + "experience": 44, + "general": 1, + "harvest": 34, + "id": 20, + "isBuy": 1, + "isVip": 0, + "level": 7, + "name": "莲藕", + "offsetH": 0, + "offsetW": 0, + "offsetX": 0, + "offsetY": 0, + "phase": "21600,46800,75600,100800,122400", + "price": 26, + "sell": 0, + "soil": 0, + "time": 34, + "vipBuy": 0 + }, + { + "buy": 266, + "crop": 1, + "experience": 23, + "general": 1, + "harvest": 22, + "id": 21, + "isBuy": 1, + "isVip": 0, + "level": 7, + "name": "豌豆", + "offsetH": 0, + "offsetW": 0, + "offsetX": 0, + "offsetY": 0, + "phase": "10800,21600,32400,46800,64800", + "price": 27, + "sell": 0, + "soil": 0, + "time": 18, + "vipBuy": 0 + }, + { + "buy": 280, + "crop": 1, + "experience": 35, + "general": 1, + "harvest": 29, + "id": 41, + "isBuy": 1, + "isVip": 0, + "level": 8, + "name": "黄瓜", + "offsetH": 0, + "offsetW": 0, + "offsetX": 0, + "offsetY": 0, + "phase": "14400,32400,54000,79200,100800", + "price": 28, + "sell": 0, + "soil": 0, + "time": 28, + "vipBuy": 0 + }, + { + "buy": 296, + "crop": 1, + "experience": 25, + "general": 1, + "harvest": 24, + "id": 42, + "isBuy": 1, + "isVip": 0, + "level": 8, + "name": "辣椒", + "offsetH": 0, + "offsetW": 0, + "offsetX": 0, + "offsetY": 0, + "phase": "10800,25200,39600,54000,72000", + "price": 28, + "sell": 0, + "soil": 0, + "time": 20, + "vipBuy": 0 + }, + { + "buy": 296, + "crop": 1, + "experience": 25, + "general": 1, + "harvest": 24, + "id": 43, + "isBuy": 1, + "isVip": 0, + "level": 8, + "name": "芹菜", + "offsetH": 0, + "offsetW": 0, + "offsetX": 0, + "offsetY": 0, + "phase": "10800,25200,39600,54000,72000", + "price": 28, + "sell": 0, + "soil": 0, + "time": 20, + "vipBuy": 0 + }, + { + "buy": 325, + "crop": 1, + "experience": 27, + "general": 1, + "harvest": 25, + "id": 44, + "isBuy": 1, + "isVip": 0, + "level": 9, + "name": "南瓜", + "offsetH": 0, + "offsetW": 0, + "offsetX": 0, + "offsetY": 0, + "phase": "10800,25200,43200,61200,79200", + "price": 30, + "sell": 0, + "soil": 0, + "time": 22, + "vipBuy": 0 + }, + { + "buy": 362, + "crop": 1, + "experience": 21, + "general": 1, + "harvest": 24, + "id": 45, + "isBuy": 1, + "isVip": 0, + "level": 9, + "name": "核桃", + "offsetH": 0, + "offsetW": 0, + "offsetX": 0, + "offsetY": 0, + "phase": "10800,25200,43200,43200,61200", + "price": 29, + "sell": 0, + "soil": 0, + "time": 17, + "vipBuy": 0 + }, + { + "buy": 605, + "crop": 2, + "experience": 20, + "general": 1, + "harvest": 24, + "id": 46, + "isBuy": 1, + "isVip": 0, + "level": 10, + "name": "草莓", + "offsetH": 0, + "offsetW": 0, + "offsetX": 0, + "offsetY": 0, + "phase": "14400,28800,46800,64800,86400", + "price": 27, + "sell": 0, + "soil": 0, + "time": 24, + "vipBuy": 0 + }, + { + "buy": 578, + "crop": 2, + "experience": 18, + "general": 1, + "harvest": 23, + "id": 47, + "isBuy": 1, + "isVip": 0, + "level": 10, + "name": "苹果", + "offsetH": 0, + "offsetW": 0, + "offsetX": 0, + "offsetY": 0, + "phase": "14400,28800,43200,57600,75600", + "price": 24, + "sell": 0, + "soil": 0, + "time": 21, + "vipBuy": 0 + }, + { + "buy": 708, + "crop": 2, + "experience": 23, + "general": 1, + "harvest": 27, + "id": 48, + "isBuy": 1, + "isVip": 0, + "level": 11, + "name": "西瓜", + "offsetH": 0, + "offsetW": 0, + "offsetX": 0, + "offsetY": 0, + "phase": "14400,32400,54000,75600,100800", + "price": 29, + "sell": 0, + "soil": 0, + "time": 28, + "vipBuy": 0 + }, + { + "buy": 700, + "crop": 2, + "experience": 23, + "general": 1, + "harvest": 27, + "id": 49, + "isBuy": 1, + "isVip": 0, + "level": 11, + "name": "魔法神花", + "offsetH": 0, + "offsetW": 0, + "offsetX": 0, + "offsetY": 0, + "phase": "14400,32400,54000,75600,100800", + "price": 29, + "sell": 0, + "soil": 0, + "time": 28, + "vipBuy": 0 + }, + { + "buy": 708, + "crop": 2, + "experience": 23, + "general": 1, + "harvest": 27, + "id": 50, + "isBuy": 1, + "isVip": 0, + "level": 11, + "name": "黄豆", + "offsetH": 0, + "offsetW": 0, + "offsetX": 0, + "offsetY": 0, + "phase": "14400,32400,54000,75600,100800", + "price": 29, + "sell": 0, + "soil": 0, + "time": 28, + "vipBuy": 0 + }, + { + "buy": 900, + "crop": 2, + "experience": 25, + "general": 1, + "harvest": 29, + "id": 51, + "isBuy": 1, + "isVip": 0, + "level": 12, + "name": "香蕉", + "offsetH": 0, + "offsetW": 0, + "offsetX": 0, + "offsetY": 0, + "phase": "18000,39600,61200,86400,111600", + "price": 32, + "sell": 0, + "soil": 0, + "time": 31, + "vipBuy": 0 + }, + { + "buy": 800, + "crop": 2, + "experience": 20, + "general": 1, + "harvest": 28, + "id": 52, + "isBuy": 1, + "isVip": 0, + "level": 12, + "name": "竹笋", + "offsetH": 0, + "offsetW": 0, + "offsetX": 0, + "offsetY": 0, + "phase": "18000,36000,54000,72000,90000", + "price": 28, + "sell": 0, + "soil": 0, + "time": 25, + "vipBuy": 0 + }, + { + "buy": 1330, + "crop": 2, + "experience": 25, + "general": 1, + "harvest": 28, + "id": 53, + "isBuy": 1, + "isVip": 0, + "level": 13, + "name": "甘蔗", + "offsetH": 0, + "offsetW": 0, + "offsetX": 0, + "offsetY": 0, + "phase": "18000,39600,61200,86400,111600", + "price": 42, + "sell": 0, + "soil": 0, + "time": 31, + "vipBuy": 0 + }, + { + "buy": 1200, + "crop": 2, + "experience": 33, + "general": 1, + "harvest": 32, + "id": 54, + "isBuy": 1, + "isVip": 0, + "level": 13, + "name": "桃子", + "offsetH": 0, + "offsetW": 0, + "offsetX": 0, + "offsetY": 0, + "phase": "28800,57600,86400,118800,151200", + "price": 40, + "sell": 0, + "soil": 0, + "time": 42, + "vipBuy": 0 + }, + { + "buy": 1587, + "crop": 2, + "experience": 25, + "general": 1, + "harvest": 26, + "id": 55, + "isBuy": 1, + "isVip": 0, + "level": 14, + "name": "橙子", + "offsetH": 0, + "offsetW": 0, + "offsetX": 0, + "offsetY": 0, + "phase": "25200,50400,75600,104400,133200", + "price": 41, + "sell": 0, + "soil": 0, + "time": 37, + "vipBuy": 0 + }, + { + "buy": 6666, + "crop": 6, + "experience": 48, + "general": 1, + "harvest": 57, + "id": 56, + "isBuy": 0, + "isVip": 0, + "level": 63, + "name": "香粽龙舟", + "offsetH": 0, + "offsetW": 0, + "offsetX": 0, + "offsetY": 0, + "phase": "28800,61200,93600,129600,165600", + "price": 510, + "sell": 1, + "soil": 0, + "time": 46, + "vipBuy": 0 + }, + { + "buy": 2860, + "crop": 2, + "experience": 21, + "general": 1, + "harvest": 32, + "id": 57, + "isBuy": 0, + "isVip": 0, + "level": 28, + "name": "园艺世", + "offsetH": 0, + "offsetW": 0, + "offsetX": 0, + "offsetY": 0, + "phase": "10800,21600,32400,46800,68400", + "price": 118, + "sell": 0, + "soil": 0, + "time": 19, + "vipBuy": 0 + }, + { + "buy": 780, + "crop": 1, + "experience": 16, + "general": 1, + "harvest": 5, + "id": 58, + "isBuy": 0, + "isVip": 0, + "level": 5, + "name": "蜜思桃", + "offsetH": 0, + "offsetW": 0, + "offsetX": 0, + "offsetY": 0, + "phase": "14400,32400,54000,79200,108000", + "price": 168, + "sell": 0, + "soil": 0, + "time": 30, + "vipBuy": 0 + }, + { + "buy": 100, + "crop": 1, + "experience": 30, + "general": 1, + "harvest": 18, + "id": 59, + "isBuy": 1, + "isVip": 0, + "level": 15, + "name": "迎春花", + "offsetH": 0, + "offsetW": 0, + "offsetX": 0, + "offsetY": 0, + "phase": "10800,21600,39600,57600,86400", + "price": 30, + "sell": 0, + "soil": 0, + "time": 24, + "vipBuy": 0 + }, + { + "buy": 1978, + "crop": 3, + "experience": 30, + "general": 1, + "harvest": 29, + "id": 60, + "isBuy": 1, + "isVip": 0, + "level": 15, + "name": "葡萄", + "offsetH": 0, + "offsetW": 0, + "offsetX": 0, + "offsetY": 0, + "phase": "28800,61200,93600,129600,165600", + "price": 47, + "sell": 0, + "soil": 0, + "time": 46, + "vipBuy": 0 + }, + { + "buy": 2130, + "crop": 3, + "experience": 20, + "general": 1, + "harvest": 24, + "id": 61, + "isBuy": 1, + "isVip": 0, + "level": 15, + "name": "丝瓜", + "offsetH": 0, + "offsetW": 0, + "offsetX": 0, + "offsetY": 0, + "phase": "18000,39600,61200,86400,111600", + "price": 50, + "sell": 0, + "soil": 0, + "time": 31, + "vipBuy": 0 + }, + { + "buy": 1978, + "crop": 3, + "experience": 30, + "general": 1, + "harvest": 29, + "id": 62, + "isBuy": 1, + "isVip": 0, + "level": 15, + "name": "榛子", + "offsetH": 0, + "offsetW": 0, + "offsetX": 0, + "offsetY": 0, + "phase": "28800,61200,93600,129600,165600", + "price": 47, + "sell": 0, + "soil": 0, + "time": 46, + "vipBuy": 0 + }, + { + "buy": 1600, + "crop": 3, + "experience": 19, + "general": 1, + "harvest": 20, + "id": 63, + "isBuy": 1, + "isVip": 0, + "level": 16, + "name": "栗子", + "offsetH": 0, + "offsetW": 0, + "offsetX": 0, + "offsetY": 0, + "phase": "21600,43200,64800,86400,108000", + "price": 49, + "sell": 0, + "soil": 0, + "time": 30, + "vipBuy": 0 + }, + { + "buy": 2425, + "crop": 3, + "experience": 34, + "general": 1, + "harvest": 30, + "id": 64, + "isBuy": 1, + "isVip": 0, + "level": 16, + "name": "石榴", + "offsetH": 0, + "offsetW": 0, + "offsetX": 0, + "offsetY": 0, + "phase": "36000,72000,108000,147600,187200", + "price": 54, + "sell": 0, + "soil": 0, + "time": 52, + "vipBuy": 0 + }, + { + "buy": 2855, + "crop": 3, + "experience": 39, + "general": 1, + "harvest": 33, + "id": 65, + "isBuy": 1, + "isVip": 0, + "level": 17, + "name": "柚子", + "offsetH": 0, + "offsetW": 0, + "offsetX": 0, + "offsetY": 0, + "phase": "39600,82800,126000,172800,219600", + "price": 58, + "sell": 0, + "soil": 0, + "time": 61, + "vipBuy": 0 + }, + { + "buy": 2600, + "crop": 3, + "experience": 16, + "general": 1, + "harvest": 27, + "id": 66, + "isBuy": 1, + "isVip": 0, + "level": 17, + "name": "蘑菇", + "offsetH": 0, + "offsetW": 0, + "offsetX": 0, + "offsetY": 0, + "phase": "25200,50400,79200,108000,108000", + "price": 55, + "sell": 0, + "soil": 0, + "time": 30, + "vipBuy": 0 + }, + { + "buy": 1700, + "crop": 3, + "experience": 27, + "general": 1, + "harvest": 29, + "id": 67, + "isBuy": 1, + "isVip": 0, + "level": 18, + "name": "无花果", + "offsetH": 0, + "offsetW": 0, + "offsetX": 0, + "offsetY": 0, + "phase": "28800,57600,86400,118800,151200", + "price": 43, + "sell": 0, + "soil": 0, + "time": 42, + "vipBuy": 0 + }, + { + "buy": 1600, + "crop": 3, + "experience": 19, + "general": 1, + "harvest": 16, + "id": 68, + "isBuy": 1, + "isVip": 0, + "level": 18, + "name": "箬竹", + "offsetH": 0, + "offsetW": 0, + "offsetX": 0, + "offsetY": 0, + "phase": "21600,43200,64800,86400,108000", + "price": 64, + "sell": 0, + "soil": 0, + "time": 30, + "vipBuy": 0 + }, + { + "buy": 3480, + "crop": 3, + "experience": 40, + "general": 1, + "harvest": 35, + "id": 69, + "isBuy": 1, + "isVip": 0, + "level": 18, + "name": "菠萝", + "offsetH": 0, + "offsetW": 0, + "offsetX": 0, + "offsetY": 0, + "phase": "43200,90000,136800,183600,230400", + "price": 62, + "sell": 0, + "soil": 0, + "time": 64, + "vipBuy": 0 + }, + { + "buy": 1200, + "crop": 4, + "experience": 25, + "general": 1, + "harvest": 25, + "id": 70, + "isBuy": 1, + "isVip": 0, + "level": 19, + "name": "金针菇", + "offsetH": 0, + "offsetW": 0, + "offsetX": 0, + "offsetY": 0, + "phase": "28800,57600,86400,118800,151200", + "price": 38, + "sell": 0, + "soil": 0, + "time": 42, + "vipBuy": 0 + }, + { + "buy": 1200, + "crop": 4, + "experience": 25, + "general": 1, + "harvest": 25, + "id": 71, + "isBuy": 1, + "isVip": 0, + "level": 19, + "name": "花生", + "offsetH": 0, + "offsetW": 0, + "offsetX": 0, + "offsetY": 0, + "phase": "28800,57600,86400,118800,151200", + "price": 38, + "sell": 0, + "soil": 0, + "time": 42, + "vipBuy": 0 + }, + { + "buy": 3720, + "crop": 4, + "experience": 32, + "general": 1, + "harvest": 27, + "id": 72, + "isBuy": 1, + "isVip": 0, + "level": 19, + "name": "椰子", + "offsetH": 0, + "offsetW": 0, + "offsetX": 0, + "offsetY": 0, + "phase": "36000,75600,115200,154800,198000", + "price": 65, + "sell": 0, + "soil": 0, + "time": 55, + "vipBuy": 0 + }, + { + "buy": 4742, + "crop": 4, + "experience": 36, + "general": 1, + "harvest": 30, + "id": 73, + "isBuy": 1, + "isVip": 0, + "level": 20, + "name": "葫芦", + "offsetH": 0, + "offsetW": 0, + "offsetX": 0, + "offsetY": 0, + "phase": "39600,82800,126000,172800,219600", + "price": 71, + "sell": 0, + "soil": 0, + "time": 61, + "vipBuy": 0 + }, + { + "buy": 4742, + "crop": 4, + "experience": 36, + "general": 1, + "harvest": 30, + "id": 74, + "isBuy": 1, + "isVip": 0, + "level": 20, + "name": "睡莲", + "offsetH": 0, + "offsetW": 0, + "offsetX": 0, + "offsetY": 0, + "phase": "39600,82800,126000,172800,219600", + "price": 71, + "sell": 0, + "soil": 0, + "time": 61, + "vipBuy": 0 + }, + { + "buy": 1360, + "crop": 4, + "experience": 19, + "general": 1, + "harvest": 24, + "id": 75, + "isBuy": 1, + "isVip": 0, + "level": 20, + "name": "梨", + "offsetH": 0, + "offsetW": 0, + "offsetX": 0, + "offsetY": 0, + "phase": "21600,43200,64800,90000,115200", + "price": 35, + "sell": 0, + "soil": 0, + "time": 32, + "vipBuy": 0 + }, + { + "buy": 4700, + "crop": 4, + "experience": 27, + "general": 1, + "harvest": 27, + "id": 76, + "isBuy": 1, + "isVip": 0, + "level": 20, + "name": "猕猴桃", + "offsetH": 0, + "offsetW": 0, + "offsetX": 0, + "offsetY": 0, + "phase": "28800,61200,93600,129600,165600", + "price": 75, + "sell": 0, + "soil": 0, + "time": 46, + "vipBuy": 0 + }, + { + "buy": 4800, + "crop": 4, + "experience": 25, + "general": 1, + "harvest": 27, + "id": 77, + "isBuy": 1, + "isVip": 0, + "level": 21, + "name": "枇杷", + "offsetH": 0, + "offsetW": 0, + "offsetX": 0, + "offsetY": 0, + "phase": "28800,57600,86400,118800,151200", + "price": 77, + "sell": 0, + "soil": 0, + "time": 42, + "vipBuy": 0 + }, + { + "buy": 5356, + "crop": 4, + "experience": 41, + "general": 1, + "harvest": 32, + "id": 78, + "isBuy": 1, + "isVip": 0, + "level": 21, + "name": "火龙果", + "offsetH": 0, + "offsetW": 0, + "offsetX": 0, + "offsetY": 0, + "phase": "46800,97200,147600,198000,252000", + "price": 77, + "sell": 0, + "soil": 0, + "time": 70, + "vipBuy": 0 + }, + { + "buy": 5527, + "crop": 4, + "experience": 42, + "general": 1, + "harvest": 33, + "id": 79, + "isBuy": 1, + "isVip": 0, + "level": 22, + "name": "樱桃", + "offsetH": 0, + "offsetW": 0, + "offsetX": 0, + "offsetY": 0, + "phase": "50400,100800,151200,205200,259200", + "price": 78, + "sell": 0, + "soil": 0, + "time": 72, + "vipBuy": 0 + }, + { + "buy": 5527, + "crop": 4, + "experience": 42, + "general": 1, + "harvest": 33, + "id": 80, + "isBuy": 1, + "isVip": 0, + "level": 22, + "name": "李子", + "offsetH": 0, + "offsetW": 0, + "offsetX": 0, + "offsetY": 0, + "phase": "50400,100800,151200,205200,259200", + "price": 78, + "sell": 0, + "soil": 0, + "time": 72, + "vipBuy": 0 + }, + { + "buy": 1480, + "crop": 4, + "experience": 22, + "general": 1, + "harvest": 26, + "id": 81, + "isBuy": 1, + "isVip": 0, + "level": 23, + "name": "香瓜", + "offsetH": 0, + "offsetW": 0, + "offsetX": 0, + "offsetY": 0, + "phase": "21600,43200,68400,97200,126000", + "price": 38, + "sell": 0, + "soil": 0, + "time": 35, + "vipBuy": 0 + }, + { + "buy": 6588, + "crop": 4, + "experience": 44, + "general": 1, + "harvest": 34, + "id": 82, + "isBuy": 1, + "isVip": 0, + "level": 23, + "name": "荔枝", + "offsetH": 0, + "offsetW": 0, + "offsetX": 0, + "offsetY": 0, + "phase": "54000,108000,165600,219600,277200", + "price": 86, + "sell": 0, + "soil": 0, + "time": 77, + "vipBuy": 0 + }, + { + "buy": 2300, + "crop": 5, + "experience": 18, + "general": 1, + "harvest": 27, + "id": 83, + "isBuy": 1, + "isVip": 0, + "level": 24, + "name": "桂圆", + "offsetH": 0, + "offsetW": 0, + "offsetX": 0, + "offsetY": 0, + "phase": "18000,39600,61200,86400,111600", + "price": 38, + "sell": 0, + "soil": 0, + "time": 31, + "vipBuy": 0 + }, + { + "buy": 6590, + "crop": 5, + "experience": 26, + "general": 1, + "harvest": 28, + "id": 84, + "isBuy": 1, + "isVip": 0, + "level": 24, + "name": "木瓜", + "offsetH": 0, + "offsetW": 0, + "offsetX": 0, + "offsetY": 0, + "phase": "28800,61200,93600,129600,165600", + "price": 80, + "sell": 0, + "soil": 0, + "time": 46, + "vipBuy": 0 + }, + { + "buy": 2300, + "crop": 5, + "experience": 18, + "general": 1, + "harvest": 27, + "id": 85, + "isBuy": 1, + "isVip": 0, + "level": 24, + "name": "月柿", + "offsetH": 0, + "offsetW": 0, + "offsetX": 0, + "offsetY": 0, + "phase": "18000,39600,61200,86400,111600", + "price": 38, + "sell": 0, + "soil": 0, + "time": 31, + "vipBuy": 0 + }, + { + "buy": 2400, + "crop": 5, + "experience": 18, + "general": 1, + "harvest": 27, + "id": 86, + "isBuy": 1, + "isVip": 0, + "level": 25, + "name": "哈密瓜", + "offsetH": 0, + "offsetW": 0, + "offsetX": 0, + "offsetY": 0, + "phase": "18000,39600,61200,86400,111600", + "price": 39, + "sell": 0, + "soil": 0, + "time": 31, + "vipBuy": 0 + }, + { + "buy": 2400, + "crop": 5, + "experience": 18, + "general": 1, + "harvest": 27, + "id": 87, + "isBuy": 1, + "isVip": 0, + "level": 25, + "name": "桑葚", + "offsetH": 0, + "offsetW": 0, + "offsetX": 0, + "offsetY": 0, + "phase": "18000,39600,61200,86400,111600", + "price": 39, + "sell": 0, + "soil": 0, + "time": 31, + "vipBuy": 0 + }, + { + "buy": 7000, + "crop": 5, + "experience": 27, + "general": 1, + "harvest": 29, + "id": 88, + "isBuy": 1, + "isVip": 0, + "level": 25, + "name": "杨桃", + "offsetH": 0, + "offsetW": 0, + "offsetX": 0, + "offsetY": 0, + "phase": "28800,61200,93600,129600,165600", + "price": 82, + "sell": 0, + "soil": 0, + "time": 46, + "vipBuy": 0 + }, + { + "buy": 6300, + "crop": 5, + "experience": 18, + "general": 1, + "harvest": 26, + "id": 89, + "isBuy": 1, + "isVip": 0, + "level": 26, + "name": "柠檬", + "offsetH": 0, + "offsetW": 0, + "offsetX": 0, + "offsetY": 0, + "phase": "18000,39600,61200,86400,111600", + "price": 83, + "sell": 0, + "soil": 0, + "time": 31, + "vipBuy": 0 + }, + { + "buy": 2500, + "crop": 5, + "experience": 18, + "general": 1, + "harvest": 27, + "id": 90, + "isBuy": 1, + "isVip": 0, + "level": 26, + "name": "芒果", + "offsetH": 0, + "offsetW": 0, + "offsetX": 0, + "offsetY": 0, + "phase": "18000,39600,61200,86400,111600", + "price": 40, + "sell": 0, + "soil": 0, + "time": 31, + "vipBuy": 0 + }, + { + "buy": 2600, + "crop": 5, + "experience": 18, + "general": 1, + "harvest": 27, + "id": 91, + "isBuy": 1, + "isVip": 0, + "level": 27, + "name": "榴莲", + "offsetH": 0, + "offsetW": 0, + "offsetX": 0, + "offsetY": 0, + "phase": "18000,39600,61200,86400,111600", + "price": 41, + "sell": 0, + "soil": 0, + "time": 31, + "vipBuy": 0 + }, + { + "buy": 6600, + "crop": 5, + "experience": 18, + "general": 1, + "harvest": 27, + "id": 92, + "isBuy": 1, + "isVip": 0, + "level": 27, + "name": "杨梅", + "offsetH": 0, + "offsetW": 0, + "offsetX": 0, + "offsetY": 0, + "phase": "18000,39600,61200,86400,111600", + "price": 84, + "sell": 0, + "soil": 0, + "time": 31, + "vipBuy": 0 + }, + { + "buy": 2873, + "crop": 5, + "experience": 18, + "general": 1, + "harvest": 28, + "id": 93, + "isBuy": 1, + "isVip": 0, + "level": 28, + "name": "番石榴", + "offsetH": 0, + "offsetW": 0, + "offsetX": 0, + "offsetY": 0, + "phase": "18000,39600,61200,86400,111600", + "price": 42, + "sell": 0, + "soil": 0, + "time": 31, + "vipBuy": 0 + }, + { + "buy": 2976, + "crop": 5, + "experience": 18, + "general": 1, + "harvest": 28, + "id": 94, + "isBuy": 1, + "isVip": 0, + "level": 29, + "name": "蓝莓", + "offsetH": 0, + "offsetW": 0, + "offsetX": 0, + "offsetY": 0, + "phase": "18000,39600,61200,86400,111600", + "price": 43, + "sell": 0, + "soil": 0, + "time": 31, + "vipBuy": 0 + }, + { + "buy": 3110, + "crop": 5, + "experience": 18, + "general": 1, + "harvest": 28, + "id": 95, + "isBuy": 1, + "isVip": 0, + "level": 30, + "name": "山竹", + "offsetH": 0, + "offsetW": 0, + "offsetX": 0, + "offsetY": 0, + "phase": "18000,39600,61200,86400,111600", + "price": 44, + "sell": 0, + "soil": 0, + "time": 31, + "vipBuy": 0 + }, + { + "buy": 3210, + "crop": 5, + "experience": 18, + "general": 1, + "harvest": 28, + "id": 96, + "isBuy": 1, + "isVip": 0, + "level": 31, + "name": "苦瓜", + "offsetH": 0, + "offsetW": 0, + "offsetX": 0, + "offsetY": 0, + "phase": "18000,39600,61200,86400,111600", + "price": 45, + "sell": 0, + "soil": 0, + "time": 31, + "vipBuy": 0 + }, + { + "buy": 3300, + "crop": 5, + "experience": 18, + "general": 1, + "harvest": 28, + "id": 97, + "isBuy": 1, + "isVip": 0, + "level": 32, + "name": "冬瓜", + "offsetH": 0, + "offsetW": 0, + "offsetX": 0, + "offsetY": 0, + "phase": "18000,39600,61200,86400,111600", + "price": 46, + "sell": 0, + "soil": 0, + "time": 31, + "vipBuy": 0 + }, + { + "buy": 3376, + "crop": 5, + "experience": 18, + "general": 1, + "harvest": 29, + "id": 98, + "isBuy": 1, + "isVip": 0, + "level": 33, + "name": "杏子", + "offsetH": 0, + "offsetW": 0, + "offsetX": 0, + "offsetY": 0, + "phase": "18000,39600,61200,86400,111600", + "price": 45, + "sell": 0, + "soil": 0, + "time": 31, + "vipBuy": 0 + }, + { + "buy": 3470, + "crop": 5, + "experience": 18, + "general": 1, + "harvest": 29, + "id": 99, + "isBuy": 1, + "isVip": 0, + "level": 34, + "name": "金桔", + "offsetH": 0, + "offsetW": 0, + "offsetX": 0, + "offsetY": 0, + "phase": "18000,39600,61200,86400,111600", + "price": 46, + "sell": 0, + "soil": 0, + "time": 31, + "vipBuy": 0 + }, + { + "buy": 3520, + "crop": 5, + "experience": 19, + "general": 1, + "harvest": 30, + "id": 100, + "isBuy": 1, + "isVip": 0, + "level": 35, + "name": "红毛丹", + "offsetH": 0, + "offsetW": 0, + "offsetX": 0, + "offsetY": 0, + "phase": "21600,46800,64800,90000,118800", + "price": 47, + "sell": 0, + "soil": 0, + "time": 33, + "vipBuy": 0 + }, + { + "buy": 3630, + "crop": 5, + "experience": 16, + "general": 1, + "harvest": 28, + "id": 101, + "isBuy": 1, + "isVip": 0, + "level": 36, + "name": "芭蕉", + "offsetH": 0, + "offsetW": 0, + "offsetX": 0, + "offsetY": 0, + "phase": "21600,39600,57600,79200,104400", + "price": 48, + "sell": 0, + "soil": 0, + "time": 29, + "vipBuy": 0 + }, + { + "buy": 3840, + "crop": 6, + "experience": 16, + "general": 1, + "harvest": 26, + "id": 102, + "isBuy": 1, + "isVip": 0, + "level": 37, + "name": "番荔枝", + "offsetH": 0, + "offsetW": 0, + "offsetX": 0, + "offsetY": 0, + "phase": "18000,43200,64800,93600,111600", + "price": 49, + "sell": 0, + "soil": 0, + "time": 31, + "vipBuy": 0 + }, + { + "buy": 3970, + "crop": 6, + "experience": 16, + "general": 1, + "harvest": 26, + "id": 103, + "isBuy": 1, + "isVip": 0, + "level": 38, + "name": "橄榄", + "offsetH": 0, + "offsetW": 0, + "offsetX": 0, + "offsetY": 0, + "phase": "21600,43200,61200,82800,108000", + "price": 50, + "sell": 0, + "soil": 0, + "time": 30, + "vipBuy": 0 + }, + { + "buy": 4130, + "crop": 6, + "experience": 15, + "general": 1, + "harvest": 26, + "id": 104, + "isBuy": 1, + "isVip": 0, + "level": 39, + "name": "百香果", + "offsetH": 0, + "offsetW": 0, + "offsetX": 0, + "offsetY": 0, + "phase": "21600,43200,64800,86400,108000", + "price": 50, + "sell": 0, + "soil": 0, + "time": 30, + "vipBuy": 0 + }, + { + "buy": 4200, + "crop": 6, + "experience": 16, + "general": 1, + "harvest": 26, + "id": 105, + "isBuy": 1, + "isVip": 0, + "level": 40, + "name": "灯笼果", + "offsetH": 0, + "offsetW": 0, + "offsetX": 0, + "offsetY": 0, + "phase": "21600,39600,57600,79200,104400", + "price": 52, + "sell": 0, + "soil": 0, + "time": 29, + "vipBuy": 0 + }, + { + "buy": 4280, + "crop": 6, + "experience": 16, + "general": 0, + "harvest": 26, + "id": 106, + "isBuy": 1, + "isVip": 0, + "level": 41, + "name": "芦荟", + "offsetH": 0, + "offsetW": 0, + "offsetX": 0, + "offsetY": 0, + "phase": "21600,39600,57600,79200,104400", + "price": 53, + "sell": 0, + "soil": 0, + "time": 29, + "vipBuy": 0 + }, + { + "buy": 4380, + "crop": 6, + "experience": 17, + "general": 1, + "harvest": 27, + "id": 107, + "isBuy": 1, + "isVip": 0, + "level": 42, + "name": "薄荷", + "offsetH": 0, + "offsetW": 0, + "offsetX": 0, + "offsetY": 0, + "phase": "21600,39600,57600,82800,108000", + "price": 54, + "sell": 0, + "soil": 0, + "time": 30, + "vipBuy": 0 + }, + { + "buy": 4470, + "crop": 7, + "experience": 13, + "general": 1, + "harvest": 22, + "id": 108, + "isBuy": 1, + "isVip": 0, + "level": 43, + "name": "菠萝蜜", + "offsetH": 0, + "offsetW": 0, + "offsetX": 0, + "offsetY": 0, + "phase": "28800,46800,64800,86400,100800", + "price": 55, + "sell": 0, + "soil": 0, + "time": 28, + "vipBuy": 0 + }, + { + "buy": 4500, + "crop": 7, + "experience": 14, + "general": 1, + "harvest": 23, + "id": 109, + "isBuy": 1, + "isVip": 0, + "level": 44, + "name": "鳄梨", + "offsetH": 0, + "offsetW": 0, + "offsetX": 0, + "offsetY": 0, + "phase": "25200,54000,75600,97200,115200", + "price": 56, + "sell": 0, + "soil": 0, + "time": 32, + "vipBuy": 0 + }, + { + "buy": 4510, + "crop": 7, + "experience": 14, + "general": 1, + "harvest": 23, + "id": 110, + "isBuy": 1, + "isVip": 0, + "level": 45, + "name": "乌饭子", + "offsetH": 0, + "offsetW": 0, + "offsetX": 0, + "offsetY": 0, + "phase": "21600,39600,61200,82800,104400", + "price": 57, + "sell": 0, + "soil": 0, + "time": 29, + "vipBuy": 0 + }, + { + "buy": 4540, + "crop": 7, + "experience": 12, + "general": 1, + "harvest": 21, + "id": 111, + "isBuy": 1, + "isVip": 0, + "level": 46, + "name": "黑布仑", + "offsetH": 0, + "offsetW": 0, + "offsetX": 0, + "offsetY": 0, + "phase": "18000,36000,54000,72000,90000", + "price": 58, + "sell": 0, + "soil": 0, + "time": 25, + "vipBuy": 0 + }, + { + "buy": 4690, + "crop": 7, + "experience": 19, + "general": 1, + "harvest": 27, + "id": 112, + "isBuy": 1, + "isVip": 0, + "level": 47, + "name": "可可豆", + "offsetH": 0, + "offsetW": 0, + "offsetX": 0, + "offsetY": 0, + "phase": "28800,57600,86400,115200,144000", + "price": 59, + "sell": 0, + "soil": 0, + "time": 40, + "vipBuy": 0 + }, + { + "buy": 4720, + "crop": 7, + "experience": 19, + "general": 1, + "harvest": 27, + "id": 113, + "isBuy": 1, + "isVip": 0, + "level": 48, + "name": "莲雾", + "offsetH": 0, + "offsetW": 0, + "offsetX": 0, + "offsetY": 0, + "phase": "18000,39600,64800,93600,126000", + "price": 60, + "sell": 0, + "soil": 0, + "time": 35, + "vipBuy": 0 + }, + { + "buy": 4760, + "crop": 7, + "experience": 21, + "general": 1, + "harvest": 29, + "id": 114, + "isBuy": 1, + "isVip": 0, + "level": 49, + "name": "蛋黄果", + "offsetH": 0, + "offsetW": 0, + "offsetX": 0, + "offsetY": 0, + "phase": "32400,64800,97200,129600,162000", + "price": 61, + "sell": 0, + "soil": 0, + "time": 45, + "vipBuy": 0 + }, + { + "buy": 4790, + "crop": 7, + "experience": 21, + "general": 1, + "harvest": 29, + "id": 115, + "isBuy": 1, + "isVip": 0, + "level": 50, + "name": "沙棘", + "offsetH": 0, + "offsetW": 0, + "offsetX": 0, + "offsetY": 0, + "phase": "32400,68400,104400,136800,169200", + "price": 62, + "sell": 0, + "soil": 0, + "time": 47, + "vipBuy": 0 + }, + { + "buy": 4820, + "crop": 7, + "experience": 23, + "general": 1, + "harvest": 31, + "id": 116, + "isBuy": 1, + "isVip": 0, + "level": 51, + "name": "榅桲", + "offsetH": 0, + "offsetW": 0, + "offsetX": 0, + "offsetY": 0, + "phase": "36000,72000,108000,144000,180000", + "price": 63, + "sell": 0, + "soil": 0, + "time": 50, + "vipBuy": 0 + }, + { + "buy": 4830, + "crop": 7, + "experience": 22, + "general": 1, + "harvest": 31, + "id": 117, + "isBuy": 1, + "isVip": 0, + "level": 52, + "name": "黄金果", + "offsetH": 0, + "offsetW": 0, + "offsetX": 0, + "offsetY": 0, + "phase": "32400,64800,97200,133200,169200", + "price": 64, + "sell": 0, + "soil": 0, + "time": 47, + "vipBuy": 0 + }, + { + "buy": 4860, + "crop": 7, + "experience": 20, + "general": 1, + "harvest": 31, + "id": 118, + "isBuy": 1, + "isVip": 0, + "level": 53, + "name": "决明子", + "offsetH": 0, + "offsetW": 0, + "offsetX": 0, + "offsetY": 0, + "phase": "32400,64800,97200,129600,162000", + "price": 65, + "sell": 0, + "soil": 0, + "time": 45, + "vipBuy": 0 + }, + { + "buy": 4940, + "crop": 7, + "experience": 20, + "general": 1, + "harvest": 31, + "id": 119, + "isBuy": 1, + "isVip": 0, + "level": 54, + "name": "关苍术", + "offsetH": 0, + "offsetW": 0, + "offsetX": 0, + "offsetY": 0, + "phase": "32400,64800,100800,133200,165600", + "price": 66, + "sell": 0, + "soil": 0, + "time": 46, + "vipBuy": 0 + }, + { + "buy": 4990, + "crop": 7, + "experience": 20, + "general": 1, + "harvest": 31, + "id": 120, + "isBuy": 1, + "isVip": 0, + "level": 55, + "name": "冬凌草", + "offsetH": 0, + "offsetW": 0, + "offsetX": 0, + "offsetY": 0, + "phase": "36000,68400,100800,133200,165600", + "price": 67, + "sell": 0, + "soil": 0, + "time": 46, + "vipBuy": 0 + }, + { + "buy": 5000, + "crop": 7, + "experience": 21, + "general": 1, + "harvest": 32, + "id": 121, + "isBuy": 1, + "isVip": 0, + "level": 56, + "name": "关白附子", + "offsetH": 0, + "offsetW": 0, + "offsetX": 0, + "offsetY": 0, + "phase": "32400,64800,97200,129600,165600", + "price": 68, + "sell": 0, + "soil": 0, + "time": 46, + "vipBuy": 0 + }, + { + "buy": 5100, + "crop": 7, + "experience": 21, + "general": 1, + "harvest": 32, + "id": 122, + "isBuy": 1, + "isVip": 0, + "level": 57, + "name": "兔儿伞", + "offsetH": 0, + "offsetW": 0, + "offsetX": 0, + "offsetY": 0, + "phase": "32400,64800,100800,133200,165600", + "price": 69, + "sell": 0, + "soil": 0, + "time": 46, + "vipBuy": 0 + }, + { + "buy": 5150, + "crop": 7, + "experience": 21, + "general": 1, + "harvest": 33, + "id": 123, + "isBuy": 1, + "isVip": 0, + "level": 58, + "name": "益母草", + "offsetH": 0, + "offsetW": 0, + "offsetX": 0, + "offsetY": 0, + "phase": "32400,64800,97200,133200,169200", + "price": 69, + "sell": 0, + "soil": 0, + "time": 47, + "vipBuy": 0 + }, + { + "buy": 5180, + "crop": 8, + "experience": 20, + "general": 1, + "harvest": 33, + "id": 124, + "isBuy": 1, + "isVip": 0, + "level": 59, + "name": "一点红", + "offsetH": 0, + "offsetW": 0, + "offsetX": 0, + "offsetY": 0, + "phase": "32400,64800,97200,129600,162000", + "price": 69, + "sell": 0, + "soil": 0, + "time": 45, + "vipBuy": 0 + }, + { + "buy": 5300, + "crop": 7, + "experience": 21, + "general": 1, + "harvest": 34, + "id": 125, + "isBuy": 1, + "isVip": 0, + "level": 60, + "name": "三七", + "offsetH": 0, + "offsetW": 0, + "offsetX": 0, + "offsetY": 0, + "phase": "32400,64800,97200,133200,169200", + "price": 70, + "sell": 0, + "soil": 0, + "time": 47, + "vipBuy": 0 + }, + { + "buy": 6048, + "crop": 2, + "experience": 30, + "general": 1, + "harvest": 30, + "id": 126, + "isBuy": 1, + "isVip": 0, + "level": 88, + "name": "富贵竹", + "offsetH": 0, + "offsetW": 0, + "offsetX": 0, + "offsetY": 0, + "phase": "10800,21600,32400,46800,68400", + "price": 189, + "sell": 0, + "soil": 0, + "time": 19, + "vipBuy": 0 + }, + { + "buy": 6070, + "crop": 2, + "experience": 30, + "general": 1, + "harvest": 30, + "id": 127, + "isBuy": 1, + "isVip": 0, + "level": 92, + "name": "点地梅", + "offsetH": 0, + "offsetW": 0, + "offsetX": 0, + "offsetY": 0, + "phase": "10800,21600,32400,46800,68400", + "price": 199, + "sell": 0, + "soil": 0, + "time": 19, + "vipBuy": 0 + }, + { + "buy": 5400, + "crop": 2, + "experience": 30, + "general": 1, + "harvest": 39, + "id": 128, + "isBuy": 1, + "isVip": 0, + "level": 50, + "name": "双玉兰", + "offsetH": 0, + "offsetW": 0, + "offsetX": 0, + "offsetY": 0, + "phase": "10800,21600,32400,46800,68400", + "price": 152, + "sell": 0, + "soil": 0, + "time": 19, + "vipBuy": 0 + }, + { + "buy": 3888, + "crop": 2, + "experience": 45, + "general": 1, + "harvest": 35, + "id": 129, + "isBuy": 1, + "isVip": 0, + "level": 44, + "name": "红叶梅", + "offsetH": 0, + "offsetW": 0, + "offsetX": 0, + "offsetY": 0, + "phase": "10800,21600,32400,46800,68400", + "price": 148, + "sell": 0, + "soil": 0, + "time": 19, + "vipBuy": 0 + }, + { + "buy": 5200, + "crop": 2, + "experience": 28, + "general": 1, + "harvest": 34, + "id": 130, + "isBuy": 1, + "isVip": 0, + "level": 38, + "name": "鸾风玉", + "offsetH": 0, + "offsetW": 0, + "offsetX": 0, + "offsetY": 0, + "phase": "10800,21600,32400,46800,68400", + "price": 138, + "sell": 0, + "soil": 0, + "time": 19, + "vipBuy": 0 + }, + { + "buy": 0, + "crop": 1, + "experience": 8, + "general": 1, + "harvest": 27, + "id": 1001, + "isBuy": 1, + "isVip": 1, + "level": 5, + "name": "澳洲青苹果", + "offsetH": 0, + "offsetW": 0, + "offsetX": 0, + "offsetY": 0, + "phase": "14400,28800,46800,64800,86400", + "price": 1, + "sell": 0, + "soil": 0, + "time": 24, + "vipBuy": 23 + }, + { + "buy": 0, + "crop": 1, + "experience": 16, + "general": 1, + "harvest": 40, + "id": 1002, + "isBuy": 1, + "isVip": 1, + "level": 80, + "name": "椒蕉树", + "offsetH": 0, + "offsetW": 0, + "offsetX": 0, + "offsetY": 0, + "phase": "10800,21600,32400,46800,68400", + "price": 1, + "sell": 0, + "soil": 0, + "time": 19, + "vipBuy": 30 + }, + { + "buy": 0, + "crop": 1, + "experience": 11, + "general": 1, + "harvest": 30, + "id": 1003, + "isBuy": 1, + "isVip": 1, + "level": 90, + "name": "丁香水仙", + "offsetH": 0, + "offsetW": 0, + "offsetX": 0, + "offsetY": 0, + "phase": "14400,28800,46800,64800,86400", + "price": 1, + "sell": 0, + "soil": 0, + "time": 24, + "vipBuy": 23 + }, + { + "buy": 0, + "crop": 1, + "experience": 11, + "general": 1, + "harvest": 31, + "id": 1004, + "isBuy": 1, + "isVip": 1, + "level": 95, + "name": "围裙水仙", + "offsetH": 0, + "offsetW": 0, + "offsetX": 0, + "offsetY": 0, + "phase": "14400,28800,46800,64800,86400", + "price": 1, + "sell": 0, + "soil": 0, + "time": 24, + "vipBuy": 24 + }, + { + "buy": 0, + "crop": 1, + "experience": 11, + "general": 1, + "harvest": 33, + "id": 1005, + "isBuy": 1, + "isVip": 1, + "level": 100, + "name": "绿绒蒿", + "offsetH": 0, + "offsetW": 0, + "offsetX": 0, + "offsetY": 0, + "phase": "14400,28800,46800,64800,86400", + "price": 1, + "sell": 0, + "soil": 0, + "time": 24, + "vipBuy": 25 + }, + { + "buy": 0, + "crop": 1, + "experience": 12, + "general": 1, + "harvest": 34, + "id": 1006, + "isBuy": 1, + "isVip": 1, + "level": 105, + "name": "秦椒", + "offsetH": 0, + "offsetW": 0, + "offsetX": 0, + "offsetY": 0, + "phase": "14400,28800,46800,64800,86400", + "price": 1, + "sell": 0, + "soil": 0, + "time": 24, + "vipBuy": 26 + }, + { + "buy": 0, + "crop": 1, + "experience": 13, + "general": 1, + "harvest": 36, + "id": 1007, + "isBuy": 1, + "isVip": 1, + "level": 110, + "name": "五彩椒", + "offsetH": 0, + "offsetW": 0, + "offsetX": 0, + "offsetY": 0, + "phase": "14400,28800,46800,64800,86400", + "price": 1, + "sell": 0, + "soil": 0, + "time": 24, + "vipBuy": 26 + }, + { + "buy": 0, + "crop": 1, + "experience": 13, + "general": 1, + "harvest": 37, + "id": 1008, + "isBuy": 1, + "isVip": 1, + "level": 115, + "name": "香水百合", + "offsetH": 0, + "offsetW": 0, + "offsetX": 0, + "offsetY": 0, + "phase": "14400,28800,46800,64800,86400", + "price": 1, + "sell": 0, + "soil": 0, + "time": 24, + "vipBuy": 28 + }, + { + "buy": 0, + "crop": 1, + "experience": 15, + "general": 1, + "harvest": 39, + "id": 1009, + "isBuy": 1, + "isVip": 1, + "level": 120, + "name": "葛巾紫", + "offsetH": 0, + "offsetW": 0, + "offsetX": 0, + "offsetY": 0, + "phase": "14400,28800,46800,64800,86400", + "price": 1, + "sell": 0, + "soil": 0, + "time": 24, + "vipBuy": 29 + }, + { + "buy": 0, + "crop": 1, + "experience": 13, + "general": 1, + "harvest": 43, + "id": 1010, + "isBuy": 1, + "isVip": 1, + "level": 110, + "name": "一星五彩椒", + "offsetH": 0, + "offsetW": 0, + "offsetX": 0, + "offsetY": 0, + "phase": "14400,28800,46800,64800,86400", + "price": 2, + "sell": 0, + "soil": 0, + "time": 24, + "vipBuy": 32 + }, + { + "buy": 0, + "crop": 1, + "experience": 12, + "general": 1, + "harvest": 37, + "id": 1011, + "isBuy": 1, + "isVip": 1, + "level": 95, + "name": "一星围裙水仙", + "offsetH": 0, + "offsetW": 0, + "offsetX": 0, + "offsetY": 0, + "phase": "14400,28800,46800,64800,86400", + "price": 2, + "sell": 0, + "soil": 0, + "time": 24, + "vipBuy": 29 + }, + { + "buy": 0, + "crop": 1, + "experience": 12, + "general": 1, + "harvest": 41, + "id": 1012, + "isBuy": 1, + "isVip": 1, + "level": 105, + "name": "一星秦椒", + "offsetH": 0, + "offsetW": 0, + "offsetX": 0, + "offsetY": 0, + "phase": "14400,28800,46800,64800,86400", + "price": 2, + "sell": 0, + "soil": 0, + "time": 24, + "vipBuy": 31 + }, + { + "buy": 0, + "crop": 1, + "experience": 12, + "general": 1, + "harvest": 36, + "id": 1013, + "isBuy": 1, + "isVip": 1, + "level": 90, + "name": "一星丁香水仙", + "offsetH": 0, + "offsetW": 0, + "offsetX": 0, + "offsetY": 0, + "phase": "14400,28800,46800,64800,86400", + "price": 2, + "sell": 0, + "soil": 0, + "time": 24, + "vipBuy": 28 + }, + { + "buy": 0, + "crop": 1, + "experience": 12, + "general": 1, + "harvest": 40, + "id": 1014, + "isBuy": 1, + "isVip": 1, + "level": 100, + "name": "一星绿绒蒿", + "offsetH": 0, + "offsetW": 0, + "offsetX": 0, + "offsetY": 0, + "phase": "14400,28800,46800,64800,86400", + "price": 2, + "sell": 0, + "soil": 0, + "time": 24, + "vipBuy": 30 + }, + { + "buy": 0, + "crop": 1, + "experience": 15, + "general": 1, + "harvest": 47, + "id": 1015, + "isBuy": 1, + "isVip": 1, + "level": 120, + "name": "一星葛巾紫", + "offsetH": 0, + "offsetW": 0, + "offsetX": 0, + "offsetY": 0, + "phase": "14400,28800,46800,64800,86400", + "price": 2, + "sell": 0, + "soil": 0, + "time": 24, + "vipBuy": 35 + }, + { + "buy": 0, + "crop": 1, + "experience": 13, + "general": 1, + "harvest": 44, + "id": 1016, + "isBuy": 1, + "isVip": 1, + "level": 115, + "name": "一星香水百合", + "offsetH": 0, + "offsetW": 0, + "offsetX": 0, + "offsetY": 0, + "phase": "14400,28800,46800,64800,86400", + "price": 2, + "sell": 0, + "soil": 0, + "time": 24, + "vipBuy": 34 + }, + { + "buy": 100, + "crop": 3, + "experience": 12, + "general": 1, + "harvest": 28, + "id": 1017, + "isBuy": 1, + "isVip": 0, + "level": 81, + "name": "一星喇叭水仙", + "offsetH": 0, + "offsetW": 0, + "offsetX": 0, + "offsetY": 0, + "phase": "7200,14400,25200,36000,46800", + "price": 405, + "sell": 0, + "soil": 0, + "time": 13, + "vipBuy": 0 + }, + { + "buy": 100, + "crop": 3, + "experience": 12, + "general": 1, + "harvest": 28, + "id": 1018, + "isBuy": 1, + "isVip": 0, + "level": 76, + "name": "一星什样锦牡丹", + "offsetH": 0, + "offsetW": 0, + "offsetX": 0, + "offsetY": 0, + "phase": "7200,14400,25200,36000,46800", + "price": 400, + "sell": 0, + "soil": 0, + "time": 13, + "vipBuy": 0 + }, + { + "buy": 100, + "crop": 3, + "experience": 12, + "general": 1, + "harvest": 28, + "id": 1019, + "isBuy": 1, + "isVip": 0, + "level": 84, + "name": "一星卷丹百合", + "offsetH": 0, + "offsetW": 0, + "offsetX": 0, + "offsetY": 0, + "phase": "7200,14400,25200,36000,46800", + "price": 408, + "sell": 0, + "soil": 0, + "time": 13, + "vipBuy": 0 + }, + { + "buy": 100, + "crop": 3, + "experience": 12, + "general": 1, + "harvest": 28, + "id": 1020, + "isBuy": 1, + "isVip": 0, + "level": 70, + "name": "一星红富士", + "offsetH": 0, + "offsetW": 0, + "offsetX": 0, + "offsetY": 0, + "phase": "7200,14400,25200,36000,46800", + "price": 390, + "sell": 0, + "soil": 0, + "time": 13, + "vipBuy": 0 + }, + { + "buy": 100, + "crop": 3, + "experience": 12, + "general": 1, + "harvest": 32, + "id": 1021, + "isBuy": 1, + "isVip": 0, + "level": 64, + "name": "一星白玉霓", + "offsetH": 0, + "offsetW": 0, + "offsetX": 0, + "offsetY": 0, + "phase": "10800,21600,32400,46800,68400", + "price": 390, + "sell": 0, + "soil": 0, + "time": 19, + "vipBuy": 0 + }, + { + "buy": 100, + "crop": 3, + "experience": 12, + "general": 1, + "harvest": 28, + "id": 1022, + "isBuy": 1, + "isVip": 0, + "level": 49, + "name": "一星百叶蔷薇", + "offsetH": 0, + "offsetW": 0, + "offsetX": 0, + "offsetY": 0, + "phase": "3600,10800,21600,32400,43200", + "price": 400, + "sell": 0, + "soil": 0, + "time": 12, + "vipBuy": 0 + }, + { + "buy": 100, + "crop": 3, + "experience": 12, + "general": 1, + "harvest": 28, + "id": 1023, + "isBuy": 1, + "isVip": 0, + "level": 58, + "name": "一星巨峰葡萄", + "offsetH": 0, + "offsetW": 0, + "offsetX": 0, + "offsetY": 0, + "phase": "3600,10800,21600,32400,43200", + "price": 400, + "sell": 0, + "soil": 0, + "time": 12, + "vipBuy": 0 + }, + { + "buy": 100, + "crop": 3, + "experience": 12, + "general": 1, + "harvest": 22, + "id": 1024, + "isBuy": 1, + "isVip": 0, + "level": 40, + "name": "一星荷花蔷薇", + "offsetH": 0, + "offsetW": 0, + "offsetX": 0, + "offsetY": 0, + "phase": "3600,10800,21600,32400,43200", + "price": 324, + "sell": 0, + "soil": 0, + "time": 12, + "vipBuy": 0 + }, + { + "buy": 100, + "crop": 3, + "experience": 10, + "general": 1, + "harvest": 23, + "id": 1025, + "isBuy": 1, + "isVip": 0, + "level": 28, + "name": "一星书生捧墨", + "offsetH": 0, + "offsetW": 0, + "offsetX": 0, + "offsetY": 0, + "phase": "3600,10800,21600,32400,43200", + "price": 250, + "sell": 0, + "soil": 0, + "time": 12, + "vipBuy": 0 + }, + { + "buy": 0, + "crop": 1, + "experience": 14, + "general": 1, + "harvest": 50, + "id": 1026, + "isBuy": 1, + "isVip": 1, + "level": 110, + "name": "二星五彩椒", + "offsetH": 0, + "offsetW": 0, + "offsetX": 0, + "offsetY": 0, + "phase": "14400,28800,46800,64800,86400", + "price": 3, + "sell": 0, + "soil": 0, + "time": 24, + "vipBuy": 38 + }, + { + "buy": 0, + "crop": 1, + "experience": 14, + "general": 1, + "harvest": 43, + "id": 1027, + "isBuy": 1, + "isVip": 1, + "level": 95, + "name": "二星围裙水仙", + "offsetH": 0, + "offsetW": 0, + "offsetX": 0, + "offsetY": 0, + "phase": "14400,28800,46800,64800,86400", + "price": 3, + "sell": 0, + "soil": 0, + "time": 24, + "vipBuy": 34 + }, + { + "buy": 0, + "crop": 1, + "experience": 14, + "general": 1, + "harvest": 48, + "id": 1028, + "isBuy": 1, + "isVip": 1, + "level": 105, + "name": "二星秦椒", + "offsetH": 0, + "offsetW": 0, + "offsetX": 0, + "offsetY": 0, + "phase": "14400,28800,46800,64800,86400", + "price": 3, + "sell": 0, + "soil": 0, + "time": 24, + "vipBuy": 36 + }, + { + "buy": 0, + "crop": 1, + "experience": 12, + "general": 1, + "harvest": 42, + "id": 1029, + "isBuy": 1, + "isVip": 1, + "level": 90, + "name": "二星丁香水仙", + "offsetH": 0, + "offsetW": 0, + "offsetX": 0, + "offsetY": 0, + "phase": "14400,28800,46800,64800,86400", + "price": 3, + "sell": 0, + "soil": 0, + "time": 24, + "vipBuy": 32 + }, + { + "buy": 0, + "crop": 1, + "experience": 14, + "general": 1, + "harvest": 46, + "id": 1030, + "isBuy": 1, + "isVip": 1, + "level": 100, + "name": "二星绿绒蒿", + "offsetH": 0, + "offsetW": 0, + "offsetX": 0, + "offsetY": 0, + "phase": "14400,28800,46800,64800,86400", + "price": 3, + "sell": 0, + "soil": 0, + "time": 24, + "vipBuy": 35 + }, + { + "buy": 0, + "crop": 1, + "experience": 15, + "general": 1, + "harvest": 55, + "id": 1031, + "isBuy": 1, + "isVip": 1, + "level": 120, + "name": "二星葛巾紫", + "offsetH": 0, + "offsetW": 0, + "offsetX": 0, + "offsetY": 0, + "phase": "14400,28800,46800,64800,86400", + "price": 3, + "sell": 0, + "soil": 0, + "time": 24, + "vipBuy": 41 + }, + { + "buy": 0, + "crop": 1, + "experience": 14, + "general": 1, + "harvest": 52, + "id": 1032, + "isBuy": 1, + "isVip": 1, + "level": 115, + "name": "二星香水百合", + "offsetH": 0, + "offsetW": 0, + "offsetX": 0, + "offsetY": 0, + "phase": "14400,28800,46800,64800,86400", + "price": 3, + "sell": 0, + "soil": 0, + "time": 24, + "vipBuy": 39 + }, + { + "buy": 100, + "crop": 3, + "experience": 14, + "general": 1, + "harvest": 33, + "id": 1033, + "isBuy": 1, + "isVip": 0, + "level": 81, + "name": "二星喇叭水仙", + "offsetH": 0, + "offsetW": 0, + "offsetX": 0, + "offsetY": 0, + "phase": "7200,14400,25200,36000,46800", + "price": 410, + "sell": 0, + "soil": 0, + "time": 13, + "vipBuy": 0 + }, + { + "buy": 100, + "crop": 3, + "experience": 14, + "general": 1, + "harvest": 33, + "id": 1034, + "isBuy": 1, + "isVip": 0, + "level": 76, + "name": "二星什样锦牡丹", + "offsetH": 0, + "offsetW": 0, + "offsetX": 0, + "offsetY": 0, + "phase": "7200,14400,25200,36000,46800", + "price": 405, + "sell": 0, + "soil": 0, + "time": 13, + "vipBuy": 0 + }, + { + "buy": 100, + "crop": 3, + "experience": 14, + "general": 1, + "harvest": 33, + "id": 1035, + "isBuy": 1, + "isVip": 0, + "level": 84, + "name": "二星卷丹百合", + "offsetH": 0, + "offsetW": 0, + "offsetX": 0, + "offsetY": 0, + "phase": "7200,14400,25200,36000,46800", + "price": 412, + "sell": 0, + "soil": 0, + "time": 13, + "vipBuy": 0 + }, + { + "buy": 100, + "crop": 3, + "experience": 14, + "general": 1, + "harvest": 33, + "id": 1036, + "isBuy": 1, + "isVip": 0, + "level": 70, + "name": "二星红富士", + "offsetH": 0, + "offsetW": 0, + "offsetX": 0, + "offsetY": 0, + "phase": "7200,14400,25200,36000,46800", + "price": 392, + "sell": 0, + "soil": 0, + "time": 13, + "vipBuy": 0 + }, + { + "buy": 100, + "crop": 3, + "experience": 14, + "general": 1, + "harvest": 33, + "id": 1037, + "isBuy": 1, + "isVip": 0, + "level": 64, + "name": "二星白玉霓", + "offsetH": 0, + "offsetW": 0, + "offsetX": 0, + "offsetY": 0, + "phase": "10800,21600,32400,46800,68400", + "price": 390, + "sell": 0, + "soil": 0, + "time": 19, + "vipBuy": 0 + }, + { + "buy": 100, + "crop": 3, + "experience": 14, + "general": 1, + "harvest": 32, + "id": 1038, + "isBuy": 1, + "isVip": 0, + "level": 49, + "name": "二星百叶蔷薇", + "offsetH": 0, + "offsetW": 0, + "offsetX": 0, + "offsetY": 0, + "phase": "3600,10800,21600,32400,43200", + "price": 410, + "sell": 0, + "soil": 0, + "time": 12, + "vipBuy": 0 + }, + { + "buy": 100, + "crop": 3, + "experience": 14, + "general": 1, + "harvest": 32, + "id": 1039, + "isBuy": 1, + "isVip": 0, + "level": 58, + "name": "二星巨峰葡萄", + "offsetH": 0, + "offsetW": 0, + "offsetX": 0, + "offsetY": 0, + "phase": "3600,10800,21600,32400,43200", + "price": 420, + "sell": 0, + "soil": 0, + "time": 12, + "vipBuy": 0 + }, + { + "buy": 100, + "crop": 3, + "experience": 13, + "general": 1, + "harvest": 30, + "id": 1040, + "isBuy": 1, + "isVip": 0, + "level": 40, + "name": "二星荷花蔷薇", + "offsetH": 0, + "offsetW": 0, + "offsetX": 0, + "offsetY": 0, + "phase": "3600,10800,21600,32400,43200", + "price": 400, + "sell": 0, + "soil": 0, + "time": 12, + "vipBuy": 0 + }, + { + "buy": 100, + "crop": 3, + "experience": 11, + "general": 1, + "harvest": 35, + "id": 1041, + "isBuy": 1, + "isVip": 0, + "level": 28, + "name": "二星书生捧墨", + "offsetH": 0, + "offsetW": 0, + "offsetX": 0, + "offsetY": 0, + "phase": "3600,10800,21600,32400,43200", + "price": 300, + "sell": 0, + "soil": 0, + "time": 12, + "vipBuy": 0 + }, + { + "buy": 9438, + "crop": 2, + "experience": 45, + "general": 1, + "harvest": 35, + "id": 1042, + "isBuy": 1, + "isVip": 0, + "level": 86, + "name": "三星果藤", + "offsetH": 0, + "offsetW": 0, + "offsetX": 0, + "offsetY": 0, + "phase": "10800,21600,32400,46800,68400", + "price": 166, + "sell": 0, + "soil": 0, + "time": 19, + "vipBuy": 0 + }, + { + "buy": 0, + "crop": 1, + "experience": 14, + "general": 1, + "harvest": 58, + "id": 1043, + "isBuy": 1, + "isVip": 1, + "level": 110, + "name": "三星五彩椒", + "offsetH": 0, + "offsetW": 0, + "offsetX": 0, + "offsetY": 0, + "phase": "14400,28800,46800,64800,86400", + "price": 4, + "sell": 0, + "soil": 0, + "time": 24, + "vipBuy": 43 + }, + { + "buy": 0, + "crop": 1, + "experience": 14, + "general": 1, + "harvest": 50, + "id": 1044, + "isBuy": 1, + "isVip": 1, + "level": 95, + "name": "三星围裙水仙", + "offsetH": 0, + "offsetW": 0, + "offsetX": 0, + "offsetY": 0, + "phase": "14400,28800,46800,64800,86400", + "price": 4, + "sell": 0, + "soil": 0, + "time": 24, + "vipBuy": 38 + }, + { + "buy": 0, + "crop": 1, + "experience": 14, + "general": 1, + "harvest": 54, + "id": 1045, + "isBuy": 1, + "isVip": 1, + "level": 105, + "name": "三星秦椒", + "offsetH": 0, + "offsetW": 0, + "offsetX": 0, + "offsetY": 0, + "phase": "14400,28800,46800,64800,86400", + "price": 4, + "sell": 0, + "soil": 0, + "time": 24, + "vipBuy": 42 + }, + { + "buy": 0, + "crop": 1, + "experience": 14, + "general": 1, + "harvest": 39, + "id": 1046, + "isBuy": 1, + "isVip": 1, + "level": 90, + "name": "三星丁香水仙", + "offsetH": 0, + "offsetW": 0, + "offsetX": 0, + "offsetY": 0, + "phase": "14400,28800,46800,64800,86400", + "price": 4, + "sell": 0, + "soil": 0, + "time": 24, + "vipBuy": 37 + }, + { + "buy": 0, + "crop": 1, + "experience": 14, + "general": 1, + "harvest": 53, + "id": 1047, + "isBuy": 1, + "isVip": 1, + "level": 100, + "name": "三星绿绒蒿", + "offsetH": 0, + "offsetW": 0, + "offsetX": 0, + "offsetY": 0, + "phase": "14400,28800,46800,64800,86400", + "price": 4, + "sell": 0, + "soil": 0, + "time": 24, + "vipBuy": 40 + }, + { + "buy": 0, + "crop": 1, + "experience": 15, + "general": 1, + "harvest": 62, + "id": 1048, + "isBuy": 1, + "isVip": 1, + "level": 120, + "name": "三星葛巾紫", + "offsetH": 0, + "offsetW": 0, + "offsetX": 0, + "offsetY": 0, + "phase": "14400,28800,46800,64800,86400", + "price": 4, + "sell": 0, + "soil": 0, + "time": 24, + "vipBuy": 46 + }, + { + "buy": 0, + "crop": 1, + "experience": 14, + "general": 1, + "harvest": 59, + "id": 1049, + "isBuy": 1, + "isVip": 1, + "level": 115, + "name": "三星香水百合", + "offsetH": 0, + "offsetW": 0, + "offsetX": 0, + "offsetY": 0, + "phase": "14400,28800,46800,64800,86400", + "price": 4, + "sell": 0, + "soil": 0, + "time": 24, + "vipBuy": 45 + }, + { + "buy": 100, + "crop": 3, + "experience": 14, + "general": 1, + "harvest": 45, + "id": 1050, + "isBuy": 1, + "isVip": 0, + "level": 81, + "name": "三星喇叭水仙", + "offsetH": 0, + "offsetW": 0, + "offsetX": 0, + "offsetY": 0, + "phase": "7200,14400,25200,36000,46800", + "price": 568, + "sell": 0, + "soil": 0, + "time": 13, + "vipBuy": 0 + }, + { + "buy": 100, + "crop": 3, + "experience": 14, + "general": 1, + "harvest": 45, + "id": 1051, + "isBuy": 1, + "isVip": 0, + "level": 76, + "name": "三星什样锦牡丹", + "offsetH": 0, + "offsetW": 0, + "offsetX": 0, + "offsetY": 0, + "phase": "7200,14400,25200,36000,46800", + "price": 565, + "sell": 0, + "soil": 0, + "time": 13, + "vipBuy": 0 + }, + { + "buy": 100, + "crop": 3, + "experience": 14, + "general": 1, + "harvest": 45, + "id": 1052, + "isBuy": 1, + "isVip": 0, + "level": 84, + "name": "三星卷丹百合", + "offsetH": 0, + "offsetW": 0, + "offsetX": 0, + "offsetY": 0, + "phase": "7200,14400,25200,36000,46800", + "price": 570, + "sell": 0, + "soil": 0, + "time": 13, + "vipBuy": 0 + }, + { + "buy": 100, + "crop": 3, + "experience": 14, + "general": 1, + "harvest": 45, + "id": 1053, + "isBuy": 1, + "isVip": 0, + "level": 70, + "name": "三星红富士", + "offsetH": 0, + "offsetW": 0, + "offsetX": 0, + "offsetY": 0, + "phase": "7200,14400,25200,36000,46800", + "price": 560, + "sell": 0, + "soil": 0, + "time": 13, + "vipBuy": 0 + }, + { + "buy": 100, + "crop": 3, + "experience": 14, + "general": 1, + "harvest": 45, + "id": 1054, + "isBuy": 1, + "isVip": 0, + "level": 64, + "name": "三星白玉霓", + "offsetH": 0, + "offsetW": 0, + "offsetX": 0, + "offsetY": 0, + "phase": "10800,21600,32400,46800,68400", + "price": 560, + "sell": 0, + "soil": 0, + "time": 19, + "vipBuy": 0 + }, + { + "buy": 100, + "crop": 3, + "experience": 15, + "general": 1, + "harvest": 40, + "id": 1055, + "isBuy": 1, + "isVip": 0, + "level": 49, + "name": "三星百叶蔷薇", + "offsetH": 0, + "offsetW": 0, + "offsetX": 0, + "offsetY": 0, + "phase": "3600,10800,21600,32400,43200", + "price": 560, + "sell": 0, + "soil": 0, + "time": 12, + "vipBuy": 0 + }, + { + "buy": 100, + "crop": 3, + "experience": 14, + "general": 1, + "harvest": 40, + "id": 1056, + "isBuy": 1, + "isVip": 0, + "level": 58, + "name": "三星巨峰葡萄", + "offsetH": 0, + "offsetW": 0, + "offsetX": 0, + "offsetY": 0, + "phase": "3600,10800,21600,32400,43200", + "price": 560, + "sell": 0, + "soil": 0, + "time": 12, + "vipBuy": 0 + }, + { + "buy": 100, + "crop": 3, + "experience": 14, + "general": 1, + "harvest": 40, + "id": 1057, + "isBuy": 1, + "isVip": 0, + "level": 40, + "name": "三星荷花蔷薇", + "offsetH": 0, + "offsetW": 0, + "offsetX": 0, + "offsetY": 0, + "phase": "3600,10800,21600,32400,43200", + "price": 520, + "sell": 0, + "soil": 0, + "time": 12, + "vipBuy": 0 + }, + { + "buy": 100, + "crop": 3, + "experience": 12, + "general": 1, + "harvest": 40, + "id": 1058, + "isBuy": 1, + "isVip": 0, + "level": 28, + "name": "三星书生捧墨", + "offsetH": 0, + "offsetW": 0, + "offsetX": 0, + "offsetY": 0, + "phase": "3600,10800,21600,32400,43200", + "price": 450, + "sell": 0, + "soil": 0, + "time": 12, + "vipBuy": 0 + }, + { + "buy": 0, + "crop": 1, + "experience": 15, + "general": 1, + "harvest": 65, + "id": 1059, + "isBuy": 1, + "isVip": 1, + "level": 110, + "name": "四星五彩椒", + "offsetH": 0, + "offsetW": 0, + "offsetX": 0, + "offsetY": 0, + "phase": "14400,28800,46800,64800,86400", + "price": 5, + "sell": 0, + "soil": 0, + "time": 24, + "vipBuy": 49 + }, + { + "buy": 0, + "crop": 1, + "experience": 15, + "general": 1, + "harvest": 56, + "id": 1060, + "isBuy": 1, + "isVip": 1, + "level": 95, + "name": "四星围裙水仙", + "offsetH": 0, + "offsetW": 0, + "offsetX": 0, + "offsetY": 0, + "phase": "14400,28800,46800,64800,86400", + "price": 5, + "sell": 0, + "soil": 0, + "time": 24, + "vipBuy": 43 + }, + { + "buy": 0, + "crop": 1, + "experience": 15, + "general": 1, + "harvest": 61, + "id": 1061, + "isBuy": 1, + "isVip": 1, + "level": 105, + "name": "四星秦椒", + "offsetH": 0, + "offsetW": 0, + "offsetX": 0, + "offsetY": 0, + "phase": "14400,28800,46800,64800,86400", + "price": 5, + "sell": 0, + "soil": 0, + "time": 24, + "vipBuy": 47 + }, + { + "buy": 0, + "crop": 1, + "experience": 15, + "general": 1, + "harvest": 54, + "id": 1062, + "isBuy": 1, + "isVip": 1, + "level": 90, + "name": "四星丁香水仙", + "offsetH": 0, + "offsetW": 0, + "offsetX": 0, + "offsetY": 0, + "phase": "14400,28800,46800,64800,86400", + "price": 5, + "sell": 0, + "soil": 0, + "time": 24, + "vipBuy": 41 + }, + { + "buy": 0, + "crop": 1, + "experience": 15, + "general": 1, + "harvest": 59, + "id": 1063, + "isBuy": 1, + "isVip": 1, + "level": 100, + "name": "四星绿绒蒿", + "offsetH": 0, + "offsetW": 0, + "offsetX": 0, + "offsetY": 0, + "phase": "14400,28800,46800,64800,86400", + "price": 5, + "sell": 0, + "soil": 0, + "time": 24, + "vipBuy": 45 + }, + { + "buy": 0, + "crop": 1, + "experience": 16, + "general": 1, + "harvest": 70, + "id": 1064, + "isBuy": 1, + "isVip": 1, + "level": 120, + "name": "四星葛巾紫", + "offsetH": 0, + "offsetW": 0, + "offsetX": 0, + "offsetY": 0, + "phase": "14400,28800,46800,64800,86400", + "price": 5, + "sell": 0, + "soil": 0, + "time": 24, + "vipBuy": 52 + }, + { + "buy": 0, + "crop": 1, + "experience": 15, + "general": 1, + "harvest": 67, + "id": 1065, + "isBuy": 1, + "isVip": 1, + "level": 115, + "name": "四星香水百合", + "offsetH": 0, + "offsetW": 0, + "offsetX": 0, + "offsetY": 0, + "phase": "14400,28800,46800,64800,86400", + "price": 5, + "sell": 0, + "soil": 0, + "time": 24, + "vipBuy": 50 + }, + { + "buy": 100, + "crop": 3, + "experience": 15, + "general": 1, + "harvest": 50, + "id": 1066, + "isBuy": 1, + "isVip": 0, + "level": 81, + "name": "四星喇叭水仙", + "offsetH": 0, + "offsetW": 0, + "offsetX": 0, + "offsetY": 0, + "phase": "7200,14400,25200,36000,46800", + "price": 584, + "sell": 0, + "soil": 0, + "time": 13, + "vipBuy": 0 + }, + { + "buy": 100, + "crop": 3, + "experience": 14, + "general": 1, + "harvest": 45, + "id": 1067, + "isBuy": 1, + "isVip": 0, + "level": 76, + "name": "四星什样锦牡丹", + "offsetH": 0, + "offsetW": 0, + "offsetX": 0, + "offsetY": 0, + "phase": "7200,14400,25200,36000,46800", + "price": 565, + "sell": 0, + "soil": 0, + "time": 13, + "vipBuy": 0 + }, + { + "buy": 100, + "crop": 3, + "experience": 15, + "general": 1, + "harvest": 50, + "id": 1068, + "isBuy": 1, + "isVip": 0, + "level": 84, + "name": "四星卷丹百合", + "offsetH": 0, + "offsetW": 0, + "offsetX": 0, + "offsetY": 0, + "phase": "7200,14400,25200,36000,46800", + "price": 586, + "sell": 0, + "soil": 0, + "time": 13, + "vipBuy": 0 + }, + { + "buy": 100, + "crop": 3, + "experience": 15, + "general": 1, + "harvest": 50, + "id": 1069, + "isBuy": 1, + "isVip": 0, + "level": 70, + "name": "四星红富士", + "offsetH": 0, + "offsetW": 0, + "offsetX": 0, + "offsetY": 0, + "phase": "7200,14400,25200,36000,46800", + "price": 576, + "sell": 0, + "soil": 0, + "time": 13, + "vipBuy": 0 + }, + { + "buy": 100, + "crop": 3, + "experience": 15, + "general": 1, + "harvest": 50, + "id": 1070, + "isBuy": 1, + "isVip": 0, + "level": 64, + "name": "四星白玉霓", + "offsetH": 0, + "offsetW": 0, + "offsetX": 0, + "offsetY": 0, + "phase": "10800,21600,32400,46800,68400", + "price": 575, + "sell": 0, + "soil": 0, + "time": 19, + "vipBuy": 0 + }, + { + "buy": 100, + "crop": 3, + "experience": 15, + "general": 1, + "harvest": 55, + "id": 1071, + "isBuy": 1, + "isVip": 0, + "level": 49, + "name": "四星百叶蔷薇", + "offsetH": 0, + "offsetW": 0, + "offsetX": 0, + "offsetY": 0, + "phase": "3600,10800,21600,32400,43200", + "price": 575, + "sell": 0, + "soil": 0, + "time": 12, + "vipBuy": 0 + }, + { + "buy": 100, + "crop": 3, + "experience": 15, + "general": 1, + "harvest": 55, + "id": 1072, + "isBuy": 1, + "isVip": 0, + "level": 58, + "name": "四星巨峰葡萄", + "offsetH": 0, + "offsetW": 0, + "offsetX": 0, + "offsetY": 0, + "phase": "3600,10800,21600,32400,43200", + "price": 575, + "sell": 0, + "soil": 0, + "time": 12, + "vipBuy": 0 + }, + { + "buy": 100, + "crop": 3, + "experience": 14, + "general": 1, + "harvest": 55, + "id": 1073, + "isBuy": 1, + "isVip": 0, + "level": 40, + "name": "四星荷花蔷薇", + "offsetH": 0, + "offsetW": 0, + "offsetX": 0, + "offsetY": 0, + "phase": "3600,10800,21600,32400,43200", + "price": 560, + "sell": 0, + "soil": 0, + "time": 12, + "vipBuy": 0 + }, + { + "buy": 100, + "crop": 3, + "experience": 12, + "general": 1, + "harvest": 50, + "id": 1074, + "isBuy": 1, + "isVip": 0, + "level": 28, + "name": "四星书生捧墨", + "offsetH": 0, + "offsetW": 0, + "offsetX": 0, + "offsetY": 0, + "phase": "3600,10800,21600,32400,43200", + "price": 600, + "sell": 0, + "soil": 0, + "time": 12, + "vipBuy": 0 + }, + { + "buy": 0, + "crop": 1, + "experience": 15, + "general": 1, + "harvest": 72, + "id": 1075, + "isBuy": 1, + "isVip": 1, + "level": 110, + "name": "五星五彩椒", + "offsetH": 0, + "offsetW": 0, + "offsetX": 0, + "offsetY": 0, + "phase": "14400,28800,46800,64800,86400", + "price": 6, + "sell": 0, + "soil": 0, + "time": 24, + "vipBuy": 54 + }, + { + "buy": 0, + "crop": 1, + "experience": 15, + "general": 1, + "harvest": 62, + "id": 1076, + "isBuy": 1, + "isVip": 1, + "level": 95, + "name": "五星围裙水仙", + "offsetH": 0, + "offsetW": 0, + "offsetX": 0, + "offsetY": 0, + "phase": "14400,28800,46800,64800,86400", + "price": 6, + "sell": 0, + "soil": 0, + "time": 24, + "vipBuy": 48 + }, + { + "buy": 0, + "crop": 1, + "experience": 15, + "general": 1, + "harvest": 68, + "id": 1077, + "isBuy": 1, + "isVip": 1, + "level": 105, + "name": "五星秦椒", + "offsetH": 0, + "offsetW": 0, + "offsetX": 0, + "offsetY": 0, + "phase": "14400,28800,46800,64800,86400", + "price": 6, + "sell": 0, + "soil": 0, + "time": 24, + "vipBuy": 52 + }, + { + "buy": 0, + "crop": 1, + "experience": 15, + "general": 1, + "harvest": 60, + "id": 1078, + "isBuy": 1, + "isVip": 1, + "level": 90, + "name": "五星丁香水仙", + "offsetH": 0, + "offsetW": 0, + "offsetX": 0, + "offsetY": 0, + "phase": "14400,28800,46800,64800,86400", + "price": 6, + "sell": 0, + "soil": 0, + "time": 24, + "vipBuy": 46 + }, + { + "buy": 0, + "crop": 1, + "experience": 15, + "general": 1, + "harvest": 66, + "id": 1079, + "isBuy": 1, + "isVip": 1, + "level": 100, + "name": "五星绿绒蒿", + "offsetH": 0, + "offsetW": 0, + "offsetX": 0, + "offsetY": 0, + "phase": "14400,28800,46800,64800,86400", + "price": 6, + "sell": 0, + "soil": 0, + "time": 24, + "vipBuy": 50 + }, + { + "buy": 0, + "crop": 1, + "experience": 16, + "general": 1, + "harvest": 78, + "id": 1080, + "isBuy": 1, + "isVip": 1, + "level": 120, + "name": "五星葛巾紫", + "offsetH": 0, + "offsetW": 0, + "offsetX": 0, + "offsetY": 0, + "phase": "14400,28800,46800,64800,86400", + "price": 6, + "sell": 0, + "soil": 0, + "time": 24, + "vipBuy": 58 + }, + { + "buy": 0, + "crop": 1, + "experience": 15, + "general": 1, + "harvest": 74, + "id": 1081, + "isBuy": 1, + "isVip": 1, + "level": 115, + "name": "五星香水百合", + "offsetH": 0, + "offsetW": 0, + "offsetX": 0, + "offsetY": 0, + "phase": "14400,28800,46800,64800,86400", + "price": 6, + "sell": 0, + "soil": 0, + "time": 24, + "vipBuy": 56 + }, + { + "buy": 100, + "crop": 3, + "experience": 15, + "general": 1, + "harvest": 51, + "id": 1082, + "isBuy": 1, + "isVip": 0, + "level": 81, + "name": "五星喇叭水仙", + "offsetH": 0, + "offsetW": 0, + "offsetX": 0, + "offsetY": 0, + "phase": "7200,14400,25200,36000,46800", + "price": 642, + "sell": 0, + "soil": 0, + "time": 13, + "vipBuy": 0 + }, + { + "buy": 100, + "crop": 3, + "experience": 15, + "general": 1, + "harvest": 51, + "id": 1083, + "isBuy": 1, + "isVip": 0, + "level": 76, + "name": "五星什样锦牡丹", + "offsetH": 0, + "offsetW": 0, + "offsetX": 0, + "offsetY": 0, + "phase": "7200,14400,25200,36000,46800", + "price": 640, + "sell": 0, + "soil": 0, + "time": 13, + "vipBuy": 0 + }, + { + "buy": 100, + "crop": 3, + "experience": 15, + "general": 1, + "harvest": 51, + "id": 1084, + "isBuy": 1, + "isVip": 0, + "level": 84, + "name": "五星卷丹百合", + "offsetH": 0, + "offsetW": 0, + "offsetX": 0, + "offsetY": 0, + "phase": "7200,14400,25200,36000,46800", + "price": 644, + "sell": 0, + "soil": 0, + "time": 13, + "vipBuy": 0 + }, + { + "buy": 100, + "crop": 3, + "experience": 15, + "general": 1, + "harvest": 51, + "id": 1085, + "isBuy": 1, + "isVip": 0, + "level": 70, + "name": "五星红富士", + "offsetH": 0, + "offsetW": 0, + "offsetX": 0, + "offsetY": 0, + "phase": "7200,14400,25200,36000,46800", + "price": 635, + "sell": 0, + "soil": 0, + "time": 13, + "vipBuy": 0 + }, + { + "buy": 100, + "crop": 3, + "experience": 15, + "general": 1, + "harvest": 51, + "id": 1086, + "isBuy": 1, + "isVip": 0, + "level": 64, + "name": "五星白玉霓", + "offsetH": 0, + "offsetW": 0, + "offsetX": 0, + "offsetY": 0, + "phase": "10800,21600,32400,46800,68400", + "price": 630, + "sell": 0, + "soil": 0, + "time": 19, + "vipBuy": 0 + }, + { + "buy": 100, + "crop": 3, + "experience": 15, + "general": 1, + "harvest": 56, + "id": 1087, + "isBuy": 1, + "isVip": 0, + "level": 49, + "name": "五星百叶蔷薇", + "offsetH": 0, + "offsetW": 0, + "offsetX": 0, + "offsetY": 0, + "phase": "3600,10800,21600,32400,43200", + "price": 620, + "sell": 0, + "soil": 0, + "time": 12, + "vipBuy": 0 + }, + { + "buy": 100, + "crop": 3, + "experience": 15, + "general": 1, + "harvest": 56, + "id": 1088, + "isBuy": 1, + "isVip": 0, + "level": 58, + "name": "五星巨峰葡萄", + "offsetH": 0, + "offsetW": 0, + "offsetX": 0, + "offsetY": 0, + "phase": "3600,10800,21600,32400,43200", + "price": 630, + "sell": 0, + "soil": 0, + "time": 12, + "vipBuy": 0 + }, + { + "buy": 100, + "crop": 3, + "experience": 15, + "general": 1, + "harvest": 56, + "id": 1089, + "isBuy": 1, + "isVip": 0, + "level": 40, + "name": "五星荷花蔷薇", + "offsetH": 0, + "offsetW": 0, + "offsetX": 0, + "offsetY": 0, + "phase": "3600,10800,21600,32400,43200", + "price": 600, + "sell": 0, + "soil": 0, + "time": 12, + "vipBuy": 0 + }, + { + "buy": 100, + "crop": 3, + "experience": 13, + "general": 1, + "harvest": 50, + "id": 1090, + "isBuy": 1, + "isVip": 0, + "level": 28, + "name": "五星书生捧墨", + "offsetH": 0, + "offsetW": 0, + "offsetX": 0, + "offsetY": 0, + "phase": "3600,10800,21600,32400,43200", + "price": 650, + "sell": 0, + "soil": 0, + "time": 12, + "vipBuy": 0 + }, + { + "buy": 1360, + "crop": 4, + "experience": 19, + "general": 1, + "harvest": 48, + "id": 1091, + "isBuy": 1, + "isVip": 0, + "level": 20, + "name": "五星潜水镜", + "offsetH": 0, + "offsetW": 0, + "offsetX": 0, + "offsetY": 0, + "phase": "21600,43200,64800,90000,115200", + "price": 35, + "sell": 0, + "soil": 0, + "time": 32, + "vipBuy": 0 + } + ], + "schema": 1, + "source_sha256": "21f11e818909dda4d60864374062c46e0c86e9c2f01d2759225aba466d9de9eb" +} diff --git a/src/farm_core/domain.py b/src/farm_core/domain.py new file mode 100644 index 0000000..561ca75 --- /dev/null +++ b/src/farm_core/domain.py @@ -0,0 +1,213 @@ +"""不可变领域数据及源自旧版的纯计算规则。""" + +from dataclasses import asdict, dataclass +from enum import Enum +from math import isqrt +import json +from typing import Any + +MAX_VALUE = 2**63 - 1 +MAX_QUANTITY = 1_000_000 + + +class FarmError(ValueError): + """可由接入层区分的业务失败。""" + + def __init__(self, code: str): + self.code = code + super().__init__(code) + + +class StorageError(RuntimeError): + """数据库故障,不得作为无农场或业务成功处理。""" + + +class SchemaError(StorageError): + """数据库不属于本核心或 schema 不兼容。""" + + +class Asset(str, Enum): + """资产字段;经验与两种货币分别记账。""" + + POINT = "point" # 农场币。 + VIP_POINT = "vip_point" # 点券,不与农场币自动兑换。 + EXPERIENCE = "experience" # 累计经验。 + + +def integer(value: int, minimum: int = 0, maximum: int = MAX_VALUE) -> int: + """拒绝布尔值、非整数及超出明确范围的数值。""" + if type(value) is not int or not minimum <= value <= maximum: + raise FarmError("invalid_integer") + return value + + +def text(value: str, maximum: int = 128) -> str: + """身份和操作标识必须是有界的非空字符串。""" + if not isinstance(value, str) or not value.strip(): + raise FarmError("invalid_text") + if len(value) > maximum or any(ord(char) < 32 for char in value): + raise FarmError("invalid_text") + return value + + +def quantity(value: int) -> int: + return integer(value, 1, MAX_QUANTITY) + + +def level_for(experience: int) -> int: + """沿用 200*k*(k+1)//2 门槛,避免浮点误差。""" + return (isqrt(1 + integer(experience) // 25) - 1) // 2 + + +def boosted(base: int, percent: int = 0) -> int: + """单地块向下取整;调用方只对各地块结果求和。""" + integer(base) + integer(percent, -100, 10_000) + return integer(base * (100 + percent) // 100) + + +@dataclass(frozen=True) +class Identity: + """调用方明确提供的身份,核心不猜测账号等价性。""" + + deployment: str # 独立部署标识。 + realm: str # 调用方决定的游戏共享范围。 + platform: str # 消息平台命名空间,不是机器人框架名称。 + user_id: str # 平台用户标识,始终保存为字符串。 + + def __post_init__(self): + for value in asdict(self).values(): + text(value) + + @property + def key(self) -> str: + return json.dumps( + list(asdict(self).values()), ensure_ascii=False, + separators=(",", ":"), + ) + + +@dataclass(frozen=True) +class Plant: + """播种时保存的不可变结算定义。""" + + plant_id: int # 旧作物目录 ID。 + name: str # 唯一作物名称。 + level: int # 购买等级门槛。 + buy: int # 普通种子农场币价格。 + is_vip: bool # 是否使用点券购买。 + vip_buy: int # 点券种子价格。 + experience: int # 每季基础经验。 + harvest: int # 每季基础产量。 + price: int # 每个收获物的农场币售价。 + growth_seconds: int # 首次成熟所需秒数。 + crop: int # 最多可收获季数。 + regrowth_seconds: int # 兼容旧版的多季再生长秒数。 + + def __post_init__(self): + text(self.name) + if type(self.is_vip) is not bool: + raise FarmError("invalid_catalogue") + for name in ( + "plant_id", "growth_seconds", "crop", "regrowth_seconds", + "harvest", + ): + integer(getattr(self, name), 1) + for name in ("level", "buy", "vip_buy", "experience", "price"): + integer(getattr(self, name)) + + @property + def asset(self) -> Asset: + return Asset.VIP_POINT if self.is_vip else Asset.POINT + + @property + def cost(self) -> int: + return self.vip_buy if self.is_vip else self.buy + + @classmethod + def from_legacy(cls, row: dict[str, Any]) -> "Plant": + try: + for key in ( + "id", "level", "buy", "isVip", "vipBuy", "experience", + "harvest", "price", "time", "crop", + ): + integer(row[key]) + if row["isVip"] not in (0, 1): + raise FarmError("invalid_catalogue") + if not isinstance(row["phase"], str): + raise FarmError("invalid_catalogue") + parts = row["phase"].split(",") + if not parts or any(not p.isascii() or not p.isdigit() + for p in parts): + raise FarmError("invalid_catalogue") + phases = list(dict.fromkeys(int(part) for part in parts)) + for phase in phases: + integer(phase, 1) + if len(phases) < 2 or phases != sorted(phases): + raise FarmError("invalid_catalogue") + return cls( + row["id"], row["name"], row["level"], row["buy"], + bool(row["isVip"]), row["vipBuy"], row["experience"], + row["harvest"], row["price"], row["time"] * 3600, + row["crop"], sum(phases[1:]), + ) + except (KeyError, TypeError, ValueError, OverflowError) as exc: + raise FarmError("invalid_catalogue") from exc + + +@dataclass(frozen=True) +class Account: + """已提交的账户查询快照。""" + + name: str # 农场名称。 + point: int # 当前农场币。 + vip_point: int # 当前点券。 + experience: int # 当前累计经验。 + + @property + def level(self) -> int: + return level_for(self.experience) + + +@dataclass(frozen=True) +class InventoryItem: + """同一作物的种子、产物和锁定状态。""" + + name: str # 作物名称。 + seeds: int = 0 # 未播种种子数。 + crops: int = 0 # 已收获产物数。 + locked: bool = False # 是否禁止出售产物。 + + +@dataclass(frozen=True) +class Plot: + """地块状态;空地无 plant,最后一季后保留枯萎实例。""" + + slot: int # 从 1 开始的地块编号。 + plant: Plant | None = None # 播种时冻结的作物规则。 + mature_at: int | None = None # 下一季成熟 UTC 秒数。 + harvest_count: int = 0 # 当前实例已收获季数。 + yield_bonus: int = 0 # 此地块产量加成百分比。 + experience_bonus: int = 0 # 此地块经验加成百分比。 + + @property + def withered(self) -> bool: + return self.plant is not None and self.harvest_count >= self.plant.crop + + +@dataclass(frozen=True) +class Snapshot: + """同一数据库读事务内取得的一致快照。""" + + account: Account # 账户。 + inventory: tuple[InventoryItem, ...] # 按名称排序的仓库。 + plots: tuple[Plot, ...] # 按编号排序的地块。 + + +@dataclass(frozen=True) +class Receipt: + """可持久化、可重放的交易结果;data 只是脱离数据库的返回副本。""" + + operation_id: str # 调用方稳定提供的幂等操作标识。 + action: str # 已提交的业务动作。 + data: dict[str, Any] # 实际成交数值与提交后的资产。 diff --git a/src/farm_core/ports.py b/src/farm_core/ports.py new file mode 100644 index 0000000..a311870 --- /dev/null +++ b/src/farm_core/ports.py @@ -0,0 +1,32 @@ +"""应用层需要的最小事务仓储契约。""" + +from typing import Any, Callable, Protocol + +from .domain import ( + Account, Asset, Identity, InventoryItem, Plot, Receipt, Snapshot, +) + + +class Repository(Protocol): + def account(self) -> Account: ... + def register(self, name: str) -> bool: ... + def asset(self, asset: Asset, delta: int) -> None: ... + def item(self, name: str) -> InventoryItem: ... + def items(self) -> tuple[InventoryItem, ...]: ... + def stock(self, name: str, seeds: int = 0, crops: int = 0) -> None: ... + def lock(self, name: str, locked: bool) -> None: ... + def plots(self) -> tuple[Plot, ...]: ... + def save_plot(self, plot: Plot) -> None: ... + + +class Store(Protocol): + async def initialize(self) -> None: ... + async def close(self) -> None: ... + + async def transact( + self, identity: Identity, operation_id: str, action: str, + parameters: dict[str, Any], now: int, + operation: Callable[[Repository], dict[str, Any]], + ) -> Receipt: ... + + async def snapshot(self, identity: Identity) -> Snapshot: ... diff --git a/src/farm_core/presentation.py b/src/farm_core/presentation.py new file mode 100644 index 0000000..ccf4ff7 --- /dev/null +++ b/src/farm_core/presentation.py @@ -0,0 +1,44 @@ +"""仅渲染已提交结果,不发送框架消息或修改业务状态。""" + +from .domain import Receipt, Snapshot + + +def receipt_text(receipt: Receipt) -> str: + labels = { + "register": "注册", "buy": "购买", "sow": "播种", + "harvest": "收获", "eradicate": "铲除", + "lock": "锁定设置", "sell": "出售", + } + data = receipt.data + account = data["account"] + lines = [f"{labels[receipt.action]}已完成"] + if "created" in data: + lines.append("首次开通" if data["created"] else "农场已存在") + if "quantity" in data: + lines.append(f"实际数量:{data['quantity']}") + if "proceeds" in data: + lines.append(f"本次收入:{data['proceeds']}") + lines.append(f"农场币:{account['point']}") + lines.append(f"点券:{account['vip_point']}") + lines.append(f"累计经验:{account['experience']}") + return "\n".join(lines) + + +def snapshot_text(snapshot: Snapshot) -> str: + account = snapshot.account + lines = [ + account.name, f"等级:{account.level}", + f"农场币:{account.point}", f"点券:{account.vip_point}", + f"累计经验:{account.experience}", + ] + for item in snapshot.inventory: + lock = "锁定" if item.locked else "未锁定" + lines.append( + f"{item.name}:种子{item.seeds},作物{item.crops},{lock}" + ) + for plot in snapshot.plots: + state = "空地" if plot.plant is None else plot.plant.name + if plot.withered: + state += "(枯萎)" + lines.append(f"地块{plot.slot}:{state}") + return "\n".join(lines) diff --git a/src/farm_core/repository.py b/src/farm_core/repository.py new file mode 100644 index 0000000..cd97ec8 --- /dev/null +++ b/src/farm_core/repository.py @@ -0,0 +1,153 @@ +"""单个 SQLite 事务内的仓储;不自行提交或开启事务。""" + +from dataclasses import asdict +import json +import sqlite3 + +from .domain import ( + Account, Asset, FarmError, InventoryItem, Plot, Plant, StorageError, + integer, +) + + +class SQLiteRepository: + def __init__( + self, connection: sqlite3.Connection, identity: str, + operation_id: str, action: str, now: int, + ): + self._db = connection + self._identity = identity + self._operation_id = operation_id + self._action = action + self._now = now + + def account(self) -> Account: + row = self._db.execute( + "SELECT name, point, vip_point, experience " + "FROM accounts WHERE identity=?", (self._identity,), + ).fetchone() + if row is None: + raise FarmError("not_registered") + return Account(**dict(row)) + + def register(self, name: str) -> bool: + exists = self._db.execute( + "SELECT 1 FROM accounts WHERE identity=?", (self._identity,), + ).fetchone() + if exists: + return False + self._db.execute( + "INSERT INTO accounts(identity, name) VALUES (?, ?)", + (self._identity, name), + ) + self._db.executemany( + "INSERT INTO plots(identity, slot) VALUES (?, ?)", + [(self._identity, slot) for slot in range(1, 4)], + ) + self.asset(Asset.POINT, 500) + return True + + def asset(self, asset: Asset, delta: int) -> None: + if not isinstance(asset, Asset): + raise FarmError("invalid_asset") + integer(delta, -(2**63 - 1)) + previous = getattr(self.account(), asset.value) + updated = previous + delta + if updated < 0: + raise FarmError("insufficient_funds") + integer(updated) + if delta == 0: + return + column = asset.value + cursor = self._db.execute( + f"UPDATE accounts SET {column}=? " + f"WHERE identity=? AND {column}=?", + (updated, self._identity, previous), + ) + if cursor.rowcount != 1: + raise FarmError("state_conflict") + self._db.execute( + "INSERT INTO ledger(" + "identity, operation_id, asset, delta, reason, created_at" + ") VALUES (?, ?, ?, ?, ?, ?)", + ( + self._identity, self._operation_id, asset.value, delta, + self._action, self._now, + ), + ) + + def item(self, name: str) -> InventoryItem: + row = self._db.execute( + "SELECT name, seeds, crops, locked FROM inventory " + "WHERE identity=? AND name=?", (self._identity, name), + ).fetchone() + if row is None: + return InventoryItem(name) + return InventoryItem( + row["name"], row["seeds"], row["crops"], bool(row["locked"]), + ) + + def items(self) -> tuple[InventoryItem, ...]: + rows = self._db.execute( + "SELECT name, seeds, crops, locked FROM inventory " + "WHERE identity=? ORDER BY name", (self._identity,), + ).fetchall() + return tuple( + InventoryItem(r["name"], r["seeds"], r["crops"], bool(r["locked"])) + for r in rows + ) + + def stock(self, name: str, seeds: int = 0, crops: int = 0) -> None: + previous = self.item(name) + new_seeds, new_crops = previous.seeds + seeds, previous.crops + crops + if min(new_seeds, new_crops) < 0: + raise FarmError("insufficient_stock") + integer(new_seeds) + integer(new_crops) + self._db.execute( + "INSERT INTO inventory(identity, name, seeds, crops) " + "VALUES (?, ?, ?, ?) ON CONFLICT(identity, name) " + "DO UPDATE SET seeds=excluded.seeds, crops=excluded.crops", + (self._identity, name, new_seeds, new_crops), + ) + + def lock(self, name: str, locked: bool) -> None: + self.stock(name) + self._db.execute( + "UPDATE inventory SET locked=? WHERE identity=? AND name=?", + (int(locked), self._identity, name), + ) + + def plots(self) -> tuple[Plot, ...]: + rows = self._db.execute( + "SELECT * FROM plots WHERE identity=? ORDER BY slot", + (self._identity,), + ).fetchall() + try: + return tuple( + Plot( + r["slot"], + Plant(**json.loads(r["plant"])) if r["plant"] else None, + r["mature_at"], r["harvest_count"], + r["yield_bonus"], r["experience_bonus"], + ) + for r in rows + ) + except (TypeError, ValueError) as exc: + raise StorageError("invalid_plot_snapshot") from exc + + def save_plot(self, plot: Plot) -> None: + data = ( + json.dumps(asdict(plot.plant), ensure_ascii=False) + if plot.plant else None + ) + cursor = self._db.execute( + "UPDATE plots SET plant=?, mature_at=?, harvest_count=?, " + "yield_bonus=?, experience_bonus=? WHERE identity=? AND slot=?", + ( + data, plot.mature_at, plot.harvest_count, plot.yield_bonus, + plot.experience_bonus, self._identity, plot.slot, + ), + ) + if cursor.rowcount != 1: + raise FarmError("unknown_plot") diff --git a/src/farm_core/storage.py b/src/farm_core/storage.py new file mode 100644 index 0000000..34f056e --- /dev/null +++ b/src/farm_core/storage.py @@ -0,0 +1,253 @@ +"""显式初始化的 SQLite 存储;每个用例独占一条事务连接。""" + +import asyncio +from contextlib import closing, contextmanager +import hashlib +import json +import logging +from pathlib import Path +import sqlite3 + +from .domain import ( + FarmError, Identity, Receipt, SchemaError, Snapshot, StorageError, +) +from .repository import SQLiteRepository + +LOGGER = logging.getLogger(__name__) +APPLICATION_ID = 0x4641524D +SCHEMA_VERSION = 1 + +SCHEMA = ( + """CREATE TABLE accounts( + identity TEXT PRIMARY KEY, + name TEXT NOT NULL, + point INTEGER NOT NULL DEFAULT 0 + CHECK(typeof(point)='integer' AND point>=0), + vip_point INTEGER NOT NULL DEFAULT 0 + CHECK(typeof(vip_point)='integer' AND vip_point>=0), + experience INTEGER NOT NULL DEFAULT 0 + CHECK(typeof(experience)='integer' AND experience>=0) + )""", + """CREATE TABLE operations( + identity TEXT NOT NULL REFERENCES accounts(identity), + operation_id TEXT NOT NULL, + action TEXT NOT NULL, + fingerprint TEXT NOT NULL, + result TEXT NOT NULL, + created_at INTEGER NOT NULL, + PRIMARY KEY(identity, operation_id) + )""", + """CREATE TABLE inventory( + identity TEXT NOT NULL REFERENCES accounts(identity), + name TEXT NOT NULL, + seeds INTEGER NOT NULL DEFAULT 0 + CHECK(typeof(seeds)='integer' AND seeds>=0), + crops INTEGER NOT NULL DEFAULT 0 + CHECK(typeof(crops)='integer' AND crops>=0), + locked INTEGER NOT NULL DEFAULT 0 CHECK(locked IN (0,1)), + PRIMARY KEY(identity, name) + )""", + """CREATE TABLE plots( + identity TEXT NOT NULL REFERENCES accounts(identity), + slot INTEGER NOT NULL CHECK(slot>0), + plant TEXT, + mature_at INTEGER, + harvest_count INTEGER NOT NULL DEFAULT 0 CHECK(harvest_count>=0), + yield_bonus INTEGER NOT NULL DEFAULT 0, + experience_bonus INTEGER NOT NULL DEFAULT 0, + PRIMARY KEY(identity, slot) + )""", + """CREATE TABLE ledger( + id INTEGER PRIMARY KEY, + identity TEXT NOT NULL, + operation_id TEXT NOT NULL, + asset TEXT NOT NULL + CHECK(asset IN ('point','vip_point','experience')), + delta INTEGER NOT NULL CHECK(typeof(delta)='integer' AND delta!=0), + reason TEXT NOT NULL, + created_at INTEGER NOT NULL, + FOREIGN KEY(identity, operation_id) + REFERENCES operations(identity, operation_id) + DEFERRABLE INITIALLY DEFERRED + )""", +) + + +class SQLiteStore: + def __init__(self, path: str | Path, timeout: float = 10): + if str(path) == ":memory:": + raise ValueError("Use a dedicated file for transaction connections") + self.path = Path(path).resolve() + self._timeout = timeout + self._ready = False + self._pending = set() + self._lifecycle = asyncio.Lock() + + async def _offload(self, function): + task = asyncio.create_task(asyncio.to_thread(function)) + self._pending.add(task) + + def finished(completed): + self._pending.discard(completed) + if not completed.cancelled(): + completed.exception() + + task.add_done_callback(finished) + return await asyncio.shield(task) + + @contextmanager + def _connection(self): + try: + with closing(sqlite3.connect( + self.path, timeout=self._timeout, isolation_level=None, + )) as connection: + connection.row_factory = sqlite3.Row + connection.execute("PRAGMA foreign_keys=ON") + yield connection + except sqlite3.Error as exc: + raise StorageError("sqlite_failure") from exc + + @staticmethod + def _validate(connection): + app_id = connection.execute("PRAGMA application_id").fetchone()[0] + version = connection.execute("PRAGMA user_version").fetchone()[0] + tables = { + r[0] for r in connection.execute( + "SELECT name FROM sqlite_master WHERE type='table'" + ) + } + expected = {"accounts", "operations", "inventory", "plots", "ledger"} + if ( + app_id != APPLICATION_ID or version != SCHEMA_VERSION + or tables != expected + ): + raise SchemaError("unsupported_database_schema") + + async def initialize(self) -> None: + async with self._lifecycle: + await self._offload(self._initialize) + self._ready = True + + def _initialize(self): + self.path.parent.mkdir(parents=True, exist_ok=True) + with self._connection() as connection: + connection.execute("BEGIN IMMEDIATE") + try: + app = connection.execute("PRAGMA application_id").fetchone()[0] + version = connection.execute( + "PRAGMA user_version" + ).fetchone()[0] + tables = connection.execute( + "SELECT name FROM sqlite_master WHERE type='table'" + ).fetchall() + if app == 0 and version == 0 and not tables: + for statement in SCHEMA: + connection.execute(statement) + connection.execute( + f"PRAGMA application_id={APPLICATION_ID}" + ) + connection.execute(f"PRAGMA user_version={SCHEMA_VERSION}") + self._validate(connection) + connection.commit() + except BaseException: + connection.rollback() + raise + + async def close(self) -> None: + async with self._lifecycle: + self._ready = False + pending = tuple(self._pending) + if pending: + await asyncio.gather(*pending, return_exceptions=True) + + def _ensure_ready(self): + if not self._ready: + raise StorageError("store_not_initialized_or_closed") + + async def transact( + self, identity: Identity, operation_id: str, action: str, + parameters: dict, now: int, operation, + ) -> Receipt: + self._ensure_ready() + request = json.dumps( + {"action": action, "parameters": parameters}, + ensure_ascii=False, sort_keys=True, separators=(",", ":"), + ) + fingerprint = hashlib.sha256(request.encode("utf-8")).hexdigest() + + def execute(): + with self._connection() as connection: + self._validate(connection) + connection.execute("BEGIN IMMEDIATE") + try: + existing = connection.execute( + "SELECT fingerprint, result FROM operations " + "WHERE identity=? AND operation_id=?", + (identity.key, operation_id), + ).fetchone() + if existing: + if existing["fingerprint"] != fingerprint: + raise FarmError("operation_conflict") + result = json.loads(existing["result"]) + else: + repository = SQLiteRepository( + connection, identity.key, operation_id, action, now, + ) + result = operation(repository) + connection.execute( + "INSERT INTO operations VALUES (?, ?, ?, ?, ?, ?)", + ( + identity.key, operation_id, action, fingerprint, + json.dumps(result, ensure_ascii=False), now, + ), + ) + connection.commit() + except BaseException as exc: + connection.rollback() + LOGGER.log( + logging.INFO if isinstance(exc, FarmError) + else logging.WARNING, + "farm rollback action=%s operation=%s error=%s", + action, operation_id, type(exc).__name__, + ) + raise + LOGGER.info( + "farm committed action=%s operation=%s", action, operation_id, + ) + return Receipt(operation_id, action, result) + + return await self._offload(execute) + + async def snapshot(self, identity: Identity) -> Snapshot: + self._ensure_ready() + + def query(): + with self._connection() as connection: + self._validate(connection) + connection.execute("BEGIN") + repository = SQLiteRepository( + connection, identity.key, "", "query", 0, + ) + result = Snapshot( + repository.account(), repository.items(), + repository.plots(), + ) + connection.commit() + return result + + return await self._offload(query) + + async def ledger(self, identity: Identity) -> tuple[dict, ...]: + self._ensure_ready() + + def query(): + with self._connection() as connection: + self._validate(connection) + rows = connection.execute( + "SELECT operation_id, asset, delta, reason, created_at " + "FROM ledger WHERE identity=? ORDER BY id", + (identity.key,), + ).fetchall() + return tuple(dict(row) for row in rows) + + return await self._offload(query) diff --git a/src/farm_render/__init__.py b/src/farm_render/__init__.py new file mode 100644 index 0000000..39bfe28 --- /dev/null +++ b/src/farm_render/__init__.py @@ -0,0 +1,7 @@ +"""可选共享图片层;核心模块无需导入本包。""" + +from .models import RenderOptions, RenderResult +from .renderer import FarmRenderer +from .service import RenderService + +__all__ = ["FarmRenderer", "RenderOptions", "RenderResult", "RenderService"] diff --git a/src/farm_render/assets.py b/src/farm_render/assets.py new file mode 100644 index 0000000..0b2781f --- /dev/null +++ b/src/farm_render/assets.py @@ -0,0 +1,66 @@ +"""包内素材与有界 LRU;不下载、不解压到磁盘、不暴露共享图片。""" + +from collections import OrderedDict +from importlib import resources +from io import BytesIO +import json +from threading import RLock +from zipfile import ZipFile + +from PIL import Image + + +class ByteLRU: + def __init__(self, limit): + if type(limit) is not int or limit < 0: + raise ValueError("Invalid cache budget") + self.limit = limit + self.used_bytes = 0 + self._items = OrderedDict() + self._lock = RLock() + + def get(self, key): + with self._lock: + item = self._items.get(key) + if item is None: + return None + self._items.move_to_end(key) + return item[0] + + def put(self, key, value, size): + with self._lock: + old = self._items.pop(key, None) + if old is not None: + self.used_bytes -= old[1] + if size > self.limit or self.limit == 0: + return + while self._items and self.used_bytes + size > self.limit: + _, (_, removed) = self._items.popitem(last=False) + self.used_bytes -= removed + self._items[key] = (value, size) + self.used_bytes += size + + +class Assets: + def __init__(self, cache_bytes=32 * 1024 * 1024): + data = resources.files(__package__).joinpath("data") + self.metadata = json.loads(data.joinpath("metadata.json").read_bytes()) + self.cache = ByteLRU(cache_bytes) + self._bundle = data.joinpath("assets.zip") + + def image(self, name, size=None): + if name not in self.metadata["images"]: + raise KeyError("Unknown packaged image") + size = tuple(size or self.metadata["images"][name]["size"]) + key = (name, size) + cached = self.cache.get(key) + if cached is None: + with self._bundle.open("rb") as stream, ZipFile(stream) as bundle: + content = bundle.read(name) + with Image.open(BytesIO(content)) as original: + original.thumbnail(size, Image.Resampling.LANCZOS) + cached = original.convert("RGBA") + if cached.size != size: + cached = cached.resize(size, Image.Resampling.LANCZOS) + self.cache.put(key, cached, size[0] * size[1] * 4) + return cached.copy() diff --git a/src/farm_render/canvas.py b/src/farm_render/canvas.py new file mode 100644 index 0000000..3043a93 --- /dev/null +++ b/src/farm_render/canvas.py @@ -0,0 +1,77 @@ +"""同步 Pillow 图片原语;调用方负责整次绘制的异步调度。""" + +from functools import lru_cache +from io import BytesIO + +from PIL import Image, ImageDraw, ImageFont + + +@lru_cache(maxsize=32) +def font(path, size): + return ImageFont.truetype(str(path), size=size) + + +class Canvas: + def __init__(self, image): + self.image = image.convert("RGBA") + + @classmethod + def new(cls, size, color=(0, 0, 0, 0)): + return cls(Image.new("RGBA", size, color)) + + def copy(self): + return Canvas(self.image.copy()) + + def resize(self, size): + return Canvas(self.image.resize(size, Image.Resampling.LANCZOS)) + + def crop(self, box): + return Canvas(self.image.crop(box)) + + def paste(self, image, position): + source = image.image if isinstance(image, Canvas) else image + self.image.alpha_composite(source.convert("RGBA"), position) + + @staticmethod + def measure_text(text, font_path, size): + return font(font_path, size).getlength(text) + + @classmethod + def fit_text(cls, text, font_path, size, width): + text = " ".join(str(text).split())[:256] + if cls.measure_text(text, font_path, size) <= width: + return text + suffix = "…" + while text and cls.measure_text( + text + suffix, font_path, size, + ) > width: + text = text[:-1] + return text + suffix if cls.measure_text( + suffix, font_path, size, + ) <= width else "" + + def text(self, position, text, font_path, size, color): + ImageDraw.Draw(self.image).text( + position, text, font=font(font_path, size), fill=color, + ) + + def rectangle(self, box, color, radius=0): + ImageDraw.Draw(self.image).rounded_rectangle( + box, radius=radius, fill=color, + ) + + def encode(self, codec, quality=75, method=2): + output = BytesIO() + if codec == "JPEG": + flattened = Image.new("RGB", self.image.size, "white") + flattened.paste(self.image, mask=self.image.getchannel("A")) + flattened.save(output, "JPEG", quality=quality) + elif codec == "WEBP": + self.image.save( + output, "WEBP", quality=quality, method=method, + ) + elif codec == "PNG": + self.image.save(output, "PNG", compress_level=3) + else: + raise ValueError("Unsupported output codec") + return output.getvalue() diff --git a/src/farm_render/data/NOTICE.md b/src/farm_render/data/NOTICE.md new file mode 100644 index 0000000..9973a03 --- /dev/null +++ b/src/farm_render/data/NOTICE.md @@ -0,0 +1,8 @@ +# 绘图素材来源 + +assets.zip 原样复制自 Shu-Ying/zhenxun_plugin_farm 的本地保留基线, +对应源提交与完整校验见主仓库 docs/legacy-manifest.json。 +仅选取农场季节背景、草地/普通地、作物及两个状态图标;metadata.json +来自旧布局和作物配置。沿用仓库 GPLv3 许可与原始素材归属,迁移不 +增加第三方素材权利。后续对外发布前仍须核实历史素材来源及分发权限。 +不包含字体;部署方配置可合法使用的中文字体路径。 diff --git a/src/farm_render/data/assets.zip b/src/farm_render/data/assets.zip new file mode 100644 index 0000000..a144c02 Binary files /dev/null and b/src/farm_render/data/assets.zip differ diff --git a/src/farm_render/data/metadata.json b/src/farm_render/data/metadata.json new file mode 100644 index 0000000..8991299 --- /dev/null +++ b/src/farm_render/data/metadata.json @@ -0,0 +1,6464 @@ +{ + "canvas": [ + 3072, + 1872 + ], + "images": { + "background/decorateIcon/decorate_autumn_1.jpg": { + "sha256": "a3346953c2caeb4587da66046eb72b26ddc3120d8a03eb6d9fac3105d4876e02", + "size": [ + 3072, + 1872 + ] + }, + "background/decorateIcon/decorate_default_1.jpg": { + "sha256": "a8616f7e008125c868485186be104e58287c582cd86d7fdd2b314862932c9b40", + "size": [ + 3072, + 1872 + ] + }, + "background/decorateIcon/decorate_spring_1.jpg": { + "sha256": "2278e8c410717ef5339b5343884b4b96bf446e3192b02f4ec72a526692bd0939", + "size": [ + 3072, + 1872 + ] + }, + "background/decorateIcon/decorate_summer_1.jpg": { + "sha256": "1057135459c365a26dc3834ee96e04c8f8a728f24d52f1610274a112a7800e05", + "size": [ + 3072, + 1872 + ] + }, + "background/decorateIcon/decorate_winter_1.jpg": { + "sha256": "6f1cd2c0aa3be43f9ef64d9c7b8965adebd794458285ce87fbe5a401f995fc43", + "size": [ + 3072, + 1872 + ] + }, + "background/expansion.png": { + "sha256": "175e4a5e32b2e390801b554a0b6bda49a69ec660dec2c50d4dc395c231a157ab", + "size": [ + 92, + 92 + ] + }, + "background/ripe.png": { + "sha256": "1c94af3e0b923defc85d200bd25538bf1ee9a2e744406266cdaf7103c1ea2cbd", + "size": [ + 92, + 92 + ] + }, + "plant/ATTomato/1.png": { + "sha256": "48de2a54c2868852f70e981a0ef8228ad0bb09499e6c9e84057aaf82f16f7270", + "size": [ + 67, + 98 + ] + }, + "plant/ATTomato/2.png": { + "sha256": "d45b3e6896e6eaac51d40fe85ed460b686a8f1d91578a271369f0ea2250f9460", + "size": [ + 49, + 135 + ] + }, + "plant/ATTomato/3.png": { + "sha256": "86eb67cc3904bd2cf234cc30c27b01a911f1abdfeec6ccc58f9569a028ad0056", + "size": [ + 117, + 176 + ] + }, + "plant/ATTomato/4.png": { + "sha256": "14a494d9d0a42315cf09f7c48e7ad11e33a035bda5d0ae4de0b7e42832808c52", + "size": [ + 129, + 176 + ] + }, + "plant/ATTomato/5.png": { + "sha256": "52a455a943fa41d30db8c2d20861e2d6d465d6f28511933e2b2b3ec994b41a01", + "size": [ + 129, + 180 + ] + }, + "plant/ATTomato/icon.png": { + "sha256": "d068d503bc2abdee8037a02981d4511a9c0e3c8f9ebeec13a5ec52c14a4efb61", + "size": [ + 79, + 86 + ] + }, + "plant/basic/0.png": { + "sha256": "ae5a1a593c40f311862d4bc9fecb7e16f8a2d92307b7141295fe4fe5d2bab6c9", + "size": [ + 595, + 842 + ] + }, + "plant/basic/9.png": { + "sha256": "c63f5e7b4a948b5af1f8bf8ef52c6d93f52f0ed6fb0c68b35faae39fcdadd25d", + "size": [ + 150, + 145 + ] + }, + "plant/一点红/1.png": { + "sha256": "3c72c1f4768ab80d893b446bdd8507016eeb87a980b689b04c98b05b2ed95159", + "size": [ + 29, + 90 + ] + }, + "plant/一点红/2.png": { + "sha256": "15f013dbbfe4dcb1ddc4f9f425a262101a9a970ffaac151b995b252028d65c5a", + "size": [ + 78, + 95 + ] + }, + "plant/一点红/3.png": { + "sha256": "7f10a4e8a65323781db45befa1102b4c4d6a4ddc3195372c9da8ab28541fe8bb", + "size": [ + 107, + 114 + ] + }, + "plant/一点红/4.png": { + "sha256": "98b7ee9adc622b07dccb5985d002c8bbdb851a57c15b789e2d8b69b27b82b917", + "size": [ + 115, + 134 + ] + }, + "plant/一点红/5.png": { + "sha256": "2e798ef1d714b720359a2e367c0f752cb687f7cad7ede321e4402d056dd2a010", + "size": [ + 121, + 139 + ] + }, + "plant/一点红/icon.png": { + "sha256": "ad4068587a7cc6d5d956fc7d2f862812c5cf66d6e653409613f89be0d4e5c6e1", + "size": [ + 118, + 105 + ] + }, + "plant/三七/1.png": { + "sha256": "fe1d2a2a71b844052e685db09acd5bbc3df8dfce2b2838e9fcd7de1e1eeffe59", + "size": [ + 28, + 92 + ] + }, + "plant/三七/2.png": { + "sha256": "45ba5533d6f78b101358e2b3839293eabb12bf5bd481104da5e4a15f466cd741", + "size": [ + 61, + 104 + ] + }, + "plant/三七/3.png": { + "sha256": "1fa3239fba0a0df36096129080d28c2b9ea4e0440fecf5487b68ad6e1a535f95", + "size": [ + 79, + 127 + ] + }, + "plant/三七/4.png": { + "sha256": "3090215be724c0852d01648fe14b46b3cd4f55367bd6449df4bb4f1295bd5f54", + "size": [ + 85, + 139 + ] + }, + "plant/三七/5.png": { + "sha256": "efebadda237de8b7de613595c84f51c5ca043b00b7451b1e2f2e94a9a5d58457", + "size": [ + 90, + 152 + ] + }, + "plant/三七/icon.png": { + "sha256": "ef0d8011e9d7bd12d930f12b1781b027b335832d73ca0b42d0c1edf754140693", + "size": [ + 115, + 97 + ] + }, + "plant/丝瓜/1.png": { + "sha256": "b75580e6bd830d96da1840ebb45284998409e06e36b9001dc5011510d069b869", + "size": [ + 69, + 163 + ] + }, + "plant/丝瓜/2.png": { + "sha256": "95e82fb13a9c398df8d1e5ce1ace7aa95a4b75dc17a62cab88715c1c0bb7fbc5", + "size": [ + 81, + 138 + ] + }, + "plant/丝瓜/3.png": { + "sha256": "400af9e1eee70dc1848ac56273e34bcfd1be6b1c4205fd72e9021093c15fac18", + "size": [ + 95, + 164 + ] + }, + "plant/丝瓜/4.png": { + "sha256": "6077431d7a8db083c99c639ce51cdb85be07e0dd08e187183d0af62f86f04371", + "size": [ + 85, + 165 + ] + }, + "plant/丝瓜/5.png": { + "sha256": "cd311f081e56a940ea0227271818d44315ca03e0e8054e086dbede0e7d1b9572", + "size": [ + 98, + 159 + ] + }, + "plant/丝瓜/icon.png": { + "sha256": "00296af1512d7a17005f363db66c2ec7e0af7a1912d079cc1d846cd3d68c88a9", + "size": [ + 100, + 87 + ] + }, + "plant/乌饭子/1.png": { + "sha256": "2e0b0829f5ec28912f46c0c6d767655580a26c8e147a26771f936cb02115dadc", + "size": [ + 77, + 117 + ] + }, + "plant/乌饭子/2.png": { + "sha256": "f71428fe60ae693038a9e15cb5db70a5b666a03506f54f98c295b8f490251e1e", + "size": [ + 96, + 146 + ] + }, + "plant/乌饭子/3.png": { + "sha256": "2bdcb1ab9fba0085e73dbc7ef6ec2649d7b882f796139da028c95776fb129ac5", + "size": [ + 122, + 167 + ] + }, + "plant/乌饭子/4.png": { + "sha256": "efd432f290a460195d789fffecb91b24325899a880490ace6845473853ede42a", + "size": [ + 122, + 173 + ] + }, + "plant/乌饭子/5.png": { + "sha256": "3e15d27496b1c4f5c80410a29b8ba13d9b7c61ae1ea20d429e4a674b4016baad", + "size": [ + 125, + 169 + ] + }, + "plant/乌饭子/icon.png": { + "sha256": "a9706e469b128fd5b2b33d89b381851501049462c5ccac57d7639d06713999f6", + "size": [ + 110, + 94 + ] + }, + "plant/兔儿伞/1.png": { + "sha256": "ef15350890e305dc58c35c1610aec3557eab8870f596696bb50beeb8d3e54825", + "size": [ + 11, + 84 + ] + }, + "plant/兔儿伞/2.png": { + "sha256": "ff966e5852ad3edfbb39a89372ba2c775a7a336721c72ba7b35d65f0f18c1cd2", + "size": [ + 34, + 99 + ] + }, + "plant/兔儿伞/3.png": { + "sha256": "139d02f3e643937b7330f56f2a73339596a2cb36b5d08139962d85d152dc22c5", + "size": [ + 91, + 125 + ] + }, + "plant/兔儿伞/4.png": { + "sha256": "c965913561c318779c50a39792cb39bf3abc328abe819fd3d401fffe44a0a599", + "size": [ + 96, + 142 + ] + }, + "plant/兔儿伞/5.png": { + "sha256": "d3c9df81b724a3a52e692f6b973c07953802f38b3bc9e0ec380eb76d8e433b31", + "size": [ + 96, + 153 + ] + }, + "plant/兔儿伞/icon.png": { + "sha256": "e926dd93d2d40355f304921e7173b4f8de51a56a91e84271a49cbf64b01cb7a4", + "size": [ + 117, + 109 + ] + }, + "plant/关白附子/1.png": { + "sha256": "fbd4d75d0050e590ac8a28e5f14474f5c817b44e41ff96db0603d4bd40a0553a", + "size": [ + 51, + 77 + ] + }, + "plant/关白附子/2.png": { + "sha256": "26e4a917b247b4dfe1d37d8922c59e2e39a5a7ad10493b91c3b119464c0c86b4", + "size": [ + 65, + 115 + ] + }, + "plant/关白附子/3.png": { + "sha256": "c16a5e1624122a468e7ef8007aae5af05399a16568adf658ea43888a6a106b73", + "size": [ + 69, + 129 + ] + }, + "plant/关白附子/4.png": { + "sha256": "022f3953e86daf30a6590fed4948b742d79f790d90fa8dfdfe7471b480fd660f", + "size": [ + 84, + 156 + ] + }, + "plant/关白附子/5.png": { + "sha256": "9847cc567358d9198fd9fa28d978fdfcdbf257177b000f162c8b2f851c2984af", + "size": [ + 99, + 163 + ] + }, + "plant/关白附子/icon.png": { + "sha256": "d79e0be6debf78ac5a1ea853b7c45699d127fb3067c659cfc4d8d85e8c134992", + "size": [ + 118, + 109 + ] + }, + "plant/关苍术/1.png": { + "sha256": "bf10e095be420c3877ca6ebb553699e7c0604ab66db7a32b08d2b0a471c99aaa", + "size": [ + 23, + 87 + ] + }, + "plant/关苍术/2.png": { + "sha256": "f5ec24c0e5bd8eeb164558ced56ef702c0b7264ab32961ba462c516c77a302c8", + "size": [ + 48, + 103 + ] + }, + "plant/关苍术/3.png": { + "sha256": "5b2feb00c054e396b408d0e6461a58a2e27743a93286c354380341193cd34164", + "size": [ + 67, + 133 + ] + }, + "plant/关苍术/4.png": { + "sha256": "e8a7071f31d8d49f065fb195d8749d771257c56f354659c653980f872074a61a", + "size": [ + 70, + 140 + ] + }, + "plant/关苍术/5.png": { + "sha256": "4ef4d96c237638b20b6f63dbc05901d751ba6e334cc15c13ac93fb9b5254794f", + "size": [ + 77, + 151 + ] + }, + "plant/关苍术/icon.png": { + "sha256": "887c9d4f2f7514767c5075bd41ace5dffc4685c23ae7b031d5df2a62c266da6d", + "size": [ + 128, + 124 + ] + }, + "plant/冬凌草/1.png": { + "sha256": "a49b45a256120ecd3f243c4d15214a17907d980c937dc72e58db8ee2e0b7596b", + "size": [ + 33, + 81 + ] + }, + "plant/冬凌草/2.png": { + "sha256": "19d4fc927cc4f89d73df42aa7c906a7c38f8af875592f713c57e19b29aec3a50", + "size": [ + 49, + 110 + ] + }, + "plant/冬凌草/3.png": { + "sha256": "be6fbbe778b49ecf3fda5bfbe143ac7bb1f01c49d258ba35112989258b57042b", + "size": [ + 67, + 138 + ] + }, + "plant/冬凌草/4.png": { + "sha256": "e93c8eed1c2c92fa8fe12a2d4762a453119f6f98ff025668fc54666dd33f3909", + "size": [ + 80, + 150 + ] + }, + "plant/冬凌草/5.png": { + "sha256": "76716f373e1b7ce3a2988732cab3f30afbff47a7a2035a9a13cfdc3791b078d7", + "size": [ + 85, + 152 + ] + }, + "plant/冬凌草/icon.png": { + "sha256": "ed38c3b565204f904ed0142024132823f161e48dc6fa2c40ca1911f9e4104a40", + "size": [ + 98, + 125 + ] + }, + "plant/冬瓜/1.png": { + "sha256": "2c3431aa7b60595c43a127a9877582509cb4f3636939eeb09bf7183c45fc821f", + "size": [ + 46, + 117 + ] + }, + "plant/冬瓜/2.png": { + "sha256": "219fe9dc5c88659f659774dde963fc32cd3c849f5313f32f0cc67d4ae291afda", + "size": [ + 121, + 151 + ] + }, + "plant/冬瓜/3.png": { + "sha256": "353219c03bf3b4d534bfe0e45f8e36129b311cb860f2d6aa68c3a8e2895c00f5", + "size": [ + 130, + 151 + ] + }, + "plant/冬瓜/4.png": { + "sha256": "7a396acdb27af351102904df5477d1ac7c5e102fb8f02021111220257b2ee5b9", + "size": [ + 130, + 151 + ] + }, + "plant/冬瓜/5.png": { + "sha256": "18aa7676e59f6fd07f1625c9d594fce574aa500548d630f0d75946c2b82835cf", + "size": [ + 130, + 151 + ] + }, + "plant/冬瓜/icon.png": { + "sha256": "e5968bfce8e7abfc617ce7eec59001a0fcbc6109025c34d11cc83aea283c3bf9", + "size": [ + 100, + 79 + ] + }, + "plant/决明子/1.png": { + "sha256": "04fe55f4ae6204b073b37b149716f7add4e689f8ab24d2be6119789c3de0c348", + "size": [ + 42, + 80 + ] + }, + "plant/决明子/2.png": { + "sha256": "1f8f9aaed45514de83f2afa6d07480dde313be41242b8133154ae3c8aa473911", + "size": [ + 85, + 129 + ] + }, + "plant/决明子/3.png": { + "sha256": "89a3311d1b3c5c1808848236359b400c81b263747ada99baaa4b9864234349f1", + "size": [ + 95, + 145 + ] + }, + "plant/决明子/4.png": { + "sha256": "ee57cf04bd5ce0b54b836e836ae97e7ad743dadea754cd3c55b718f5940b6094", + "size": [ + 106, + 152 + ] + }, + "plant/决明子/5.png": { + "sha256": "0db21e2b5a269a76998c9fd320e5d6426f4be42bedd3d746c3dae39691335a3d", + "size": [ + 117, + 156 + ] + }, + "plant/决明子/icon.png": { + "sha256": "0c8c110c09f8ae87ad09003da6bbc65c19f291644356a523901573a237283256", + "size": [ + 121, + 121 + ] + }, + "plant/南瓜/1.png": { + "sha256": "a5bf0620aadb4fc9ca624dfb6a11b00c1d6f4028b5d6befd59cae59885085732", + "size": [ + 32, + 87 + ] + }, + "plant/南瓜/2.png": { + "sha256": "71153e744e97a506014f90569426b52a510af1fcc8c15db27dc2a1e58b359066", + "size": [ + 79, + 117 + ] + }, + "plant/南瓜/3.png": { + "sha256": "d2de11943128950bf708ec06ae440fa061760a05be9c08afc5d5d85f8878bae9", + "size": [ + 129, + 160 + ] + }, + "plant/南瓜/4.png": { + "sha256": "3fba9df28d1540a50ccfc3b0482ac7709d49bfc6c22210ab4b59b9a064628f06", + "size": [ + 129, + 160 + ] + }, + "plant/南瓜/5.png": { + "sha256": "1baa5daa917169081e61a1f4f2dc3044002b8afc66acec97b495b0777d31615d", + "size": [ + 111, + 158 + ] + }, + "plant/南瓜/icon.png": { + "sha256": "d359d58df510dd25ab5cb7469548fb2c3991516953e1346177128b79d6f40291", + "size": [ + 95, + 92 + ] + }, + "plant/双玉兰/1.png": { + "sha256": "030576ce164c3eb12ac3105dc4ce7c5b6d8ac676acc216f74ed13c4ebfa2f1b3", + "size": [ + 17, + 71 + ] + }, + "plant/双玉兰/2.png": { + "sha256": "d6dabbad486a58c0cda2850de4fe1491a23f85960b387b93dd8683c0625c59f5", + "size": [ + 46, + 99 + ] + }, + "plant/双玉兰/3.png": { + "sha256": "d6454d363f14dc44c35d867b426c6372fbc13e8ddc4babc1060e4ad31f8bc065", + "size": [ + 63, + 110 + ] + }, + "plant/双玉兰/4.png": { + "sha256": "4ed9f5b31c64728e996e54b682902d8b8e6abb31ab6c019f131c866857fbc18f", + "size": [ + 88, + 139 + ] + }, + "plant/双玉兰/5.png": { + "sha256": "a433966e33487cef0e7acd98fae419ff66d63c461a8fb63344fc2f516452fafe", + "size": [ + 114, + 167 + ] + }, + "plant/双玉兰/icon.png": { + "sha256": "c235b113f1a774bca8f13af972e2d2d7e40e5691b66ce20e6d48ddad732afacc", + "size": [ + 94, + 78 + ] + }, + "plant/可可豆/1.png": { + "sha256": "aba02a434f5404abfa50e7e500d775c4c0e9d2626e23171c84a4b0d22bcf3fd2", + "size": [ + 28, + 88 + ] + }, + "plant/可可豆/2.png": { + "sha256": "69468aaf7bb4fb752a43d1e0d215d2dbba3ecd8a655457907bc80071776f5a6a", + "size": [ + 101, + 121 + ] + }, + "plant/可可豆/3.png": { + "sha256": "45418c35d3add710805c403cd14c7c03f8c8edd4aeee230ffc874eaa2e009ff0", + "size": [ + 121, + 154 + ] + }, + "plant/可可豆/4.png": { + "sha256": "06de387193251997fac472f50a733464ef5e0497604bc1783c2de6ceafb853ba", + "size": [ + 125, + 155 + ] + }, + "plant/可可豆/5.png": { + "sha256": "22be6b287999ecb2ef1cd8ef6198281f5f566148c83b38dc9d0750bf8efb75c4", + "size": [ + 139, + 166 + ] + }, + "plant/可可豆/icon.png": { + "sha256": "9f76fca22a82122db0cea2ee6e60fe6e799f5b2be160ba80cf1ce0dea771633b", + "size": [ + 120, + 134 + ] + }, + "plant/哈密瓜/1.png": { + "sha256": "a5ae6c92787117b996a382424d9408e0dd642845eb94982e3374a93151bc5058", + "size": [ + 40, + 95 + ] + }, + "plant/哈密瓜/2.png": { + "sha256": "e2aa3e4ee2db805f1621b6345d03b58a353dd6e0abef04b6f6a9d492ef2e2091", + "size": [ + 100, + 152 + ] + }, + "plant/哈密瓜/3.png": { + "sha256": "417cb5f55e4c131dbcbab78295da853bb3a1bb3be8e769d159b5e4da995f8be3", + "size": [ + 100, + 159 + ] + }, + "plant/哈密瓜/4.png": { + "sha256": "c2686cda49c00606132d51d59b628a98c29db1a7efcdef262da6fda3c55574f5", + "size": [ + 100, + 157 + ] + }, + "plant/哈密瓜/5.png": { + "sha256": "73523941e8867e9047cab7bdd0e983280d23aba6051f28254abe39f687b96f56", + "size": [ + 104, + 161 + ] + }, + "plant/哈密瓜/icon.png": { + "sha256": "a5dd6edc3e4c2efe7546b2eec53a84598153070092b244b87d6804bbd89c01f9", + "size": [ + 104, + 73 + ] + }, + "plant/园艺世/1.png": { + "sha256": "aa9ab5450c0cf0db0d011e89184828b1c18a50878a614f4aad814b2d5048a126", + "size": [ + 26, + 78 + ] + }, + "plant/园艺世/2.png": { + "sha256": "b4eb08aca852b31b45f6c596e7bed0c3a041eb7daadeafef113580e26a7ceba7", + "size": [ + 42, + 85 + ] + }, + "plant/园艺世/3.png": { + "sha256": "63c059720d4f51b52d05861360f7c1a699cdcd189dd63d842e56ca30b99d2ed4", + "size": [ + 74, + 115 + ] + }, + "plant/园艺世/4.png": { + "sha256": "d5952ce94bab423bff3239b8197e0484f05cab594ae3fb3bcccfb1ff9f54f403", + "size": [ + 88, + 130 + ] + }, + "plant/园艺世/5.png": { + "sha256": "f0d7c8954db4a034c27b200462470e0cae457109bc567e0c7527d078e02e6053", + "size": [ + 124, + 157 + ] + }, + "plant/园艺世/icon.png": { + "sha256": "62cfbfafd48f213c19d5409a31362508e5a67d322751d36c93700629796ab18a", + "size": [ + 114, + 110 + ] + }, + "plant/土豆/1.png": { + "sha256": "90640b91dbb010e10cf084737a657776ea40fcaa0355c8764693ae3cac343d68", + "size": [ + 39, + 108 + ] + }, + "plant/土豆/2.png": { + "sha256": "1dea8e35473fc312abf0a9cfc1049c70f68f2e70171238533ab3c85d1850cdb6", + "size": [ + 72, + 123 + ] + }, + "plant/土豆/3.png": { + "sha256": "69a33de3754a8b9a491a3ed59d5704f57ccd54c7b821a69e133a1826b43325cf", + "size": [ + 109, + 138 + ] + }, + "plant/土豆/4.png": { + "sha256": "fbd3433c80a4a66f5ffcf88e7f05bbb187e593c4513d254f619a9dca3bfc01f0", + "size": [ + 109, + 138 + ] + }, + "plant/土豆/5.png": { + "sha256": "8b1639f4686cc8a27478c0035866848573fc300b639e7350e0cb89f983e26e88", + "size": [ + 114, + 183 + ] + }, + "plant/土豆/icon.png": { + "sha256": "81312cd03bccdf720c61aeb211aa1552d639900b9a3ae9d2fd944f89be8f2c97", + "size": [ + 87, + 105 + ] + }, + "plant/大白菜/1.png": { + "sha256": "f21fc639e3bea6023f1660b652bfdbd24d77384e6a17a9133fe28748bbb5744d", + "size": [ + 41, + 82 + ] + }, + "plant/大白菜/2.png": { + "sha256": "5e8875bceebcff36f74322e3b2aa64fa63336b4a40138fc66a7b8437c3a45d3a", + "size": [ + 66, + 113 + ] + }, + "plant/大白菜/3.png": { + "sha256": "ecaa0730d5e320a1035e622eba1a6c8f5ee30455405d48800a8ebbf6b50219c4", + "size": [ + 88, + 122 + ] + }, + "plant/大白菜/4.png": { + "sha256": "8c9919cb875a3399c98d2f56550c9f5008eae823472020d1351f7e7569c4c144", + "size": [ + 95, + 128 + ] + }, + "plant/大白菜/5.png": { + "sha256": "24f5a86b0c2be512532f2413d6d8e8b60519daec1687790e41b6be361f9e3f40", + "size": [ + 104, + 130 + ] + }, + "plant/大白菜/icon.png": { + "sha256": "2f38b02ecb284a20ad1eef4dd65c845b5bee02497da7fb9568b3db1e67b0e1b1", + "size": [ + 102, + 114 + ] + }, + "plant/大蒜/1.png": { + "sha256": "fe4fbce09d4d745f999b613d945ebc1e9233da84456e65a1bb997fc97f5a1f67", + "size": [ + 66, + 107 + ] + }, + "plant/大蒜/2.png": { + "sha256": "6f671f4a853501fd9a61c8f01dea70aeeda70482ccddd4c83d30a256e49b9c97", + "size": [ + 71, + 142 + ] + }, + "plant/大蒜/3.png": { + "sha256": "e904b29f1d9b26835d326609a63f11e0d9fa2655137a59a2a33b83a8c2edee76", + "size": [ + 82, + 177 + ] + }, + "plant/大蒜/4.png": { + "sha256": "cce558d74dadd1b0d61767fabab8f15c29ee063bdd5151914de05503104c0f93", + "size": [ + 106, + 170 + ] + }, + "plant/大蒜/5.png": { + "sha256": "c216e3aa78aca61c80a6b6828ca9e8aec596090fdf9c149c1586399ceec536f2", + "size": [ + 110, + 171 + ] + }, + "plant/大蒜/icon.png": { + "sha256": "716cc24764d083f9aa02a405c24f27e607161d86b7300baaaa20b777227c935d", + "size": [ + 93, + 100 + ] + }, + "plant/富贵竹/1.png": { + "sha256": "97b08fd5ff1d94b2750c0c45136c95b0dac23369efe12805748759a06423984a", + "size": [ + 25, + 77 + ] + }, + "plant/富贵竹/2.png": { + "sha256": "a68dcf801f26a503ffcb41f06d414d82384111a5d725991d36cba11b2bb62df6", + "size": [ + 47, + 104 + ] + }, + "plant/富贵竹/3.png": { + "sha256": "09a464c054b3bb0b6ed29a3debeeee74417301638d5d6072c8ae92be1d463a13", + "size": [ + 58, + 122 + ] + }, + "plant/富贵竹/4.png": { + "sha256": "811edb8c22798858e3572ec4260b957a9aac25c70b25ce9ffd357989fc907d79", + "size": [ + 79, + 136 + ] + }, + "plant/富贵竹/5.png": { + "sha256": "49887ea157c1d38ed44a22134c787047504095f480d341beacf8e938416a4190", + "size": [ + 114, + 169 + ] + }, + "plant/富贵竹/icon.png": { + "sha256": "9df2688d0f4ea9b352e3347b982854af1e956068193c097a7a60bba23a3cdedb", + "size": [ + 106, + 117 + ] + }, + "plant/小白菜/1.png": { + "sha256": "b49c2f942de76e46075ee18c0b0abf34dafd8dcae02cf0298692cb7aea5ba750", + "size": [ + 22, + 83 + ] + }, + "plant/小白菜/2.png": { + "sha256": "b8f5a211f90c881371d4f51476c579b625e82740a6f7e59902eec64c17f257bb", + "size": [ + 46, + 99 + ] + }, + "plant/小白菜/3.png": { + "sha256": "0e8105b45994155b95674fda949aaff0b6ec259e0d06fad1199a33fe463bfd5a", + "size": [ + 92, + 142 + ] + }, + "plant/小白菜/4.png": { + "sha256": "f879a0b07d43e6d4fca3122105d4de774b564095d49ef620a5fec5b9e849c375", + "size": [ + 92, + 142 + ] + }, + "plant/小白菜/icon.png": { + "sha256": "4328dd3d1ae61a811d4dfd003110156fb8e1520c90129c19c5307112b43cdaa8", + "size": [ + 78, + 115 + ] + }, + "plant/小麦/1.png": { + "sha256": "5883fd6220d7893eab3c0cdb80c55ebc8632d0dbaad24a7449efa3760c5c8f7f", + "size": [ + 36, + 106 + ] + }, + "plant/小麦/2.png": { + "sha256": "18a63e04f978fddd7964c35979244b2cc7518e860a44ef4e90faaa06cbad9024", + "size": [ + 65, + 141 + ] + }, + "plant/小麦/3.png": { + "sha256": "bf094c88ee84e392cc71673ef6b40b1262bb9b929ef2cc063cf10fc266142feb", + "size": [ + 103, + 154 + ] + }, + "plant/小麦/4.png": { + "sha256": "0a1a529994d6b38114926c43195f23d1a43cd8223f6804f0ffb8a853c06f8c20", + "size": [ + 111, + 165 + ] + }, + "plant/小麦/5.png": { + "sha256": "35853528cef793f746cfd957472e95e854f601ea86af0a3bfda79517ab6929a0", + "size": [ + 114, + 172 + ] + }, + "plant/小麦/icon.png": { + "sha256": "0b80564515c742e961010d99b4f6abd0958ea6238d1f8c4e669a6a198f2f42b9", + "size": [ + 91, + 100 + ] + }, + "plant/山竹/1.png": { + "sha256": "2bce5723615971a0c1ef0e5a2479ca32bd16d154121e0a8a246641707d222385", + "size": [ + 51, + 107 + ] + }, + "plant/山竹/2.png": { + "sha256": "252637660273ccd13f19faae6d4537f9d6adc1b05d2fbb504dc8429c7dc14328", + "size": [ + 79, + 125 + ] + }, + "plant/山竹/3.png": { + "sha256": "b51e21a124c33ef660dc0a8404dbe130268bf99858ac462c7402652180bd7895", + "size": [ + 118, + 166 + ] + }, + "plant/山竹/4.png": { + "sha256": "2e44afce34571872dd37516bd2fc13b57ad346188ac9645c69ed3e0bfd321dd1", + "size": [ + 123, + 171 + ] + }, + "plant/山竹/5.png": { + "sha256": "3af58c53778deabf2c643541ddffe8ce9ea5005906ea29c33e211d825ff33ffd", + "size": [ + 126, + 174 + ] + }, + "plant/山竹/icon.png": { + "sha256": "7b8309360ecd4bf3f26b7aafb7058c43fbd6614a0316faed3c765372c6a69f49", + "size": [ + 100, + 82 + ] + }, + "plant/无花果/1.png": { + "sha256": "08e10b4373c36b9c544b6fec4e4088568a4ecff3e58aabf6a8917ed09ce58466", + "size": [ + 49, + 81 + ] + }, + "plant/无花果/2.png": { + "sha256": "c844f93163145fdada632e15822806a22c3c56fbdeede5dadf3c92df9f9a905f", + "size": [ + 51, + 90 + ] + }, + "plant/无花果/3.png": { + "sha256": "efb4fe6f325dddfb689acecc9e95d9dc0438ee7ef5b8ddd176b956835b02ebfe", + "size": [ + 85, + 144 + ] + }, + "plant/无花果/4.png": { + "sha256": "b1624a555613d687b7868401f7151f3e4351b010cdeb1123af8fd3f993c86abe", + "size": [ + 101, + 174 + ] + }, + "plant/无花果/5.png": { + "sha256": "01648efa81b97857afdcb19680f54b2f5842c711b3273b05a6fb437b2d7dd2fd", + "size": [ + 104, + 174 + ] + }, + "plant/无花果/icon.png": { + "sha256": "f4c52eadecc25fdb020ae1bbc0cf8c5ae6bc112661b37ff0c4366e6b1d8ccd04", + "size": [ + 100, + 67 + ] + }, + "plant/月柿/1.png": { + "sha256": "c3c6be217b0253ec27062434ca7396c11e73ac05454b9eb515d7682b8680f97f", + "size": [ + 27, + 98 + ] + }, + "plant/月柿/2.png": { + "sha256": "ec07f1ec163b8b5a5877c0dad783f3ddd166a49ccb2e219c8a9a03b2e8d6a450", + "size": [ + 68, + 142 + ] + }, + "plant/月柿/3.png": { + "sha256": "4aec71a2a7b5e1429e27e36a2ffff694dd97711c81f1923f9d4cae5cc1a32531", + "size": [ + 109, + 153 + ] + }, + "plant/月柿/4.png": { + "sha256": "7cc2dd144b7a846a3a7f0f548c9400d90be838ab4b31cbb9c0d6345c1adb70a4", + "size": [ + 118, + 168 + ] + }, + "plant/月柿/5.png": { + "sha256": "8533c7965e707c999d3b4ac5b7d5c0c4d23c9c5adf3df50dbc00053ed2c1fd4b", + "size": [ + 111, + 169 + ] + }, + "plant/月柿/icon.png": { + "sha256": "e96a3afe563775b0362a5fdd2b546072096432f304c0cf16fdc2023196fcc9e2", + "size": [ + 99, + 57 + ] + }, + "plant/木瓜/1.png": { + "sha256": "0656a3f0be52e081ce87085689884a65ce1be441dea773ddb0e0c8e1ff9f0a20", + "size": [ + 47, + 83 + ] + }, + "plant/木瓜/2.png": { + "sha256": "b28f2056811282249db06ff6982b2b8a9aee596d0a636220ce2bb484bbe719f5", + "size": [ + 72, + 116 + ] + }, + "plant/木瓜/3.png": { + "sha256": "2e9826c1bb1955bb0dd63e4d40bd3fe11865c538c8a4b0f1dcb0a5a0abeaddcb", + "size": [ + 121, + 165 + ] + }, + "plant/木瓜/4.png": { + "sha256": "c5fddc12f721fce13567d8438574216f2c75c4779bec3fffa9c6192048e9f1ad", + "size": [ + 117, + 162 + ] + }, + "plant/木瓜/5.png": { + "sha256": "5e44f2e61ffce1afb60cdebdfd77788b6d4223310eb57a33469edc78c8ebe7e7", + "size": [ + 117, + 163 + ] + }, + "plant/木瓜/icon.png": { + "sha256": "6f9bb3eae80a7815068e949afe26c77f80b8452fa80a85896614abbd02456537", + "size": [ + 95, + 98 + ] + }, + "plant/李子/1.png": { + "sha256": "98cafe37c2c8bc0e9f108ccdc4564d58fcd31f1f0c24f00f3fc20a896be36728", + "size": [ + 26, + 77 + ] + }, + "plant/李子/2.png": { + "sha256": "fe4a5302d2c2655192e03e7a135398bf206a803e6ae26a07100ea360b8ecc8ac", + "size": [ + 53, + 100 + ] + }, + "plant/李子/3.png": { + "sha256": "a03b230085bfc4cc473c86a4a055f722649c504fbb168d1199652ba67f3c315c", + "size": [ + 102, + 158 + ] + }, + "plant/李子/4.png": { + "sha256": "b82dfdb582d7ffc05717ec499df35dff2a1fdb627398294b49a827aea792033e", + "size": [ + 116, + 166 + ] + }, + "plant/李子/5.png": { + "sha256": "421481fea1377808a9bf39c3b5fc2a6d12239e5149349fabbaf928800671bfbb", + "size": [ + 116, + 168 + ] + }, + "plant/李子/icon.png": { + "sha256": "60a02dab5df46e220ff0be75d2db713916c1ec59cbf2a1b67b3ce6aa226e4122", + "size": [ + 107, + 118 + ] + }, + "plant/杏子/1.png": { + "sha256": "24547cb06f1d83b9ee7a977a760da512ae94b0861733ff6cbfa5f108d84aafc3", + "size": [ + 49, + 115 + ] + }, + "plant/杏子/2.png": { + "sha256": "d151cb66f725ebacd671f782f805d57fb4921eb6e2e7597ddfae3c71ea95d279", + "size": [ + 90, + 167 + ] + }, + "plant/杏子/3.png": { + "sha256": "03dafcff7f620964411bf7cc761ca017ff67388391d15f3153731315c91bf251", + "size": [ + 100, + 179 + ] + }, + "plant/杏子/4.png": { + "sha256": "ee50bcf978cdb6f17ab4f448b80a30384223359b1ef3b758338f6485d7c080f6", + "size": [ + 100, + 179 + ] + }, + "plant/杏子/5.png": { + "sha256": "757e4a6e9cdf948efa7b31b565424ea317abf41d99b2730ff730b30700596ad2", + "size": [ + 100, + 179 + ] + }, + "plant/杏子/icon.png": { + "sha256": "c1a1347d7d32c0f958856080b61ebe7fa83c173b3da9f4e2747809f20db588fc", + "size": [ + 100, + 89 + ] + }, + "plant/杨桃/1.png": { + "sha256": "38c3a743d835af31e7ebe449391f30cb91598c95fa30efb925c2d2a4a682bc43", + "size": [ + 29, + 90 + ] + }, + "plant/杨桃/2.png": { + "sha256": "9588a6c33b87f24f2b39a64a50d8852cbd64ead49d83b17383025eff2bc10e10", + "size": [ + 75, + 143 + ] + }, + "plant/杨桃/3.png": { + "sha256": "9f2755680d7aca1907f3292a3916cea604871279dbe71cd8e7ac41bcd49a0f25", + "size": [ + 124, + 170 + ] + }, + "plant/杨桃/4.png": { + "sha256": "5a5f8694d0ea2b9d2cf81a1c0156291cb5227ca12b444d1878b72da432ae4047", + "size": [ + 124, + 168 + ] + }, + "plant/杨桃/5.png": { + "sha256": "2c589b9f82ab62898f7bf1474b8fdf1da14fc92b99fecb21ce5d21fd9a40b34a", + "size": [ + 128, + 168 + ] + }, + "plant/杨桃/icon.png": { + "sha256": "6cfe12632595a2ec0404d295d7f86804171fcc41a25c864d0cfc7699e7ca70fb", + "size": [ + 71, + 100 + ] + }, + "plant/杨梅/1.png": { + "sha256": "e1055ebf1c8cfff05e125201b0c762db7ea41f2a4c6468e49c61deb41bb3eb04", + "size": [ + 34, + 92 + ] + }, + "plant/杨梅/2.png": { + "sha256": "4aa64179780929c5f369476acd8ab6c19b42b68ac52684a1a74a0880987ee22c", + "size": [ + 74, + 134 + ] + }, + "plant/杨梅/3.png": { + "sha256": "dfdf16de981e04644e7ed6cc5eeac92ed58f3351262384d15df8a9e77e3d4af1", + "size": [ + 107, + 163 + ] + }, + "plant/杨梅/4.png": { + "sha256": "da2dff5da296195646fafc9e4461969dfd7b65a9d16093e101fbcad63f792d2e", + "size": [ + 110, + 164 + ] + }, + "plant/杨梅/5.png": { + "sha256": "a7b8ea1ea6798b451ef6a618727966c63d47541c17e0c0ad118e734dfc18a5a5", + "size": [ + 111, + 167 + ] + }, + "plant/杨梅/icon.png": { + "sha256": "9f274296080489dab5c7c38eb9a7466ed5a3004d638036850c971c341035dbd0", + "size": [ + 100, + 82 + ] + }, + "plant/枇杷/1.png": { + "sha256": "286052d49c675c1b82e5fad055bf087dfe3394ffc66a967194aa11570219561a", + "size": [ + 24, + 102 + ] + }, + "plant/枇杷/2.png": { + "sha256": "e86c199ae0a9a34ec1a9ab30d7ee11f362c14df728f680657ca8c9c944bbc2ce", + "size": [ + 55, + 106 + ] + }, + "plant/枇杷/3.png": { + "sha256": "79c24a14e067c802c9b185fcf99f82e2f48dfe1c494269d1610a90cecca10a9b", + "size": [ + 84, + 142 + ] + }, + "plant/枇杷/4.png": { + "sha256": "465b8c6df55413c4d834b3990ae4d41a2e4e275d0807a0cdbff8651244725b6d", + "size": [ + 88, + 151 + ] + }, + "plant/枇杷/5.png": { + "sha256": "3466dbe719f01df5afd442aca04383651fb3e97db804636b4f424a99c09c0e8b", + "size": [ + 98, + 163 + ] + }, + "plant/枇杷/icon.png": { + "sha256": "c3ae8df8df33cbc9abb7356397244ac1c5043b0bbbf153f9ca11c0ba1539e6f5", + "size": [ + 117, + 102 + ] + }, + "plant/柚子/1.png": { + "sha256": "44f6a6cfb3b47be10a4cd6955fbbf536d70317b0d7e6fad2306261ea24cd4c39", + "size": [ + 29, + 95 + ] + }, + "plant/柚子/2.png": { + "sha256": "37ae5033869e1930f918c1df5a12ddcedf9e07460a01efc1dedd3e64aad95cc9", + "size": [ + 78, + 118 + ] + }, + "plant/柚子/3.png": { + "sha256": "06ae06206d89bbe85982f5f34a57da1733a3c7eff4d4bd70b052bd62669d693e", + "size": [ + 118, + 174 + ] + }, + "plant/柚子/4.png": { + "sha256": "6a3981fddf6bdbe35ae49d3a459a958de9111ec5ea1c9979d3eb445600ad8b29", + "size": [ + 119, + 168 + ] + }, + "plant/柚子/5.png": { + "sha256": "bc3b7508bf5636fbdf8fd4f675df8561eadffbb19a841a198d4ccf7e1bcbe2f1", + "size": [ + 108, + 167 + ] + }, + "plant/柚子/icon.png": { + "sha256": "aab5ca9887ca2ee930a08bdc5fe4a95aaa2b4895acd82d14d10868c0aaf645c2", + "size": [ + 87, + 100 + ] + }, + "plant/柠檬/1.png": { + "sha256": "346dfe310bd976088251cedc1f69bc33f640c062aa9d60f69b4eb0932e22808d", + "size": [ + 30, + 87 + ] + }, + "plant/柠檬/2.png": { + "sha256": "aa47257d53f53a135d09325241bbdef7ac6349b146ade46e4478d377c936c0a2", + "size": [ + 47, + 116 + ] + }, + "plant/柠檬/3.png": { + "sha256": "be300a8be1bc6448e60d1d115fcbf809b92c63de173b1218e01d0820514263ea", + "size": [ + 107, + 141 + ] + }, + "plant/柠檬/4.png": { + "sha256": "630cdd5852744ba1e6174ec73ffdacdb6644954d0ab94c977938c1e1161755fa", + "size": [ + 127, + 162 + ] + }, + "plant/柠檬/5.png": { + "sha256": "c4ea033986a2efc1efd5bd6781b72f569cb07ac95d42f881fbe4b48974028815", + "size": [ + 137, + 170 + ] + }, + "plant/柠檬/icon.png": { + "sha256": "c4d3a8c19852859f2c75b8b1a4390bc864f389b49c64a97f08e51ada063fd105", + "size": [ + 102, + 105 + ] + }, + "plant/栗子/1.png": { + "sha256": "c74ed652d4d2d48da6bfeeb1aeb36ac5da598486fd4825c20fd64f7bf57b0694", + "size": [ + 31, + 91 + ] + }, + "plant/栗子/2.png": { + "sha256": "cba68c9034d987491fb49d58b8aa78dcf7ca725a22e45a4ac6350b6523ed4f0a", + "size": [ + 58, + 112 + ] + }, + "plant/栗子/3.png": { + "sha256": "f6587f6fe18c2db80e83b757ddbf0702b793a2006fb842357d9faddaad20b4cd", + "size": [ + 94, + 137 + ] + }, + "plant/栗子/4.png": { + "sha256": "15197d9e2e4470a7c75478812e594d8d672671c95e71b7ea2ea52512db99b769", + "size": [ + 123, + 169 + ] + }, + "plant/栗子/5.png": { + "sha256": "58039c65fc280d3f876c5ad6b4b99709e66cca7cb108bcbc6f5c49df97ca53b4", + "size": [ + 128, + 178 + ] + }, + "plant/栗子/icon.png": { + "sha256": "0ebbf495f0b8dbfd1752901fb529aad85fce9fc417223dc1f0045b7b839374aa", + "size": [ + 115, + 115 + ] + }, + "plant/核桃/1.png": { + "sha256": "34974dff3ea5a18f0e141cd44c0ad1efd5440119e8b60ae2f395d1521bfc5aac", + "size": [ + 32, + 92 + ] + }, + "plant/核桃/2.png": { + "sha256": "062e2c9dc607d894395bfa209bbab92f6e6751dc6548203824caa426447c8e8f", + "size": [ + 76, + 111 + ] + }, + "plant/核桃/3.png": { + "sha256": "afcffc0cbd458ef2112cd8446e7dca3541734e6f3c274e4d06e0e40c4c22f4d5", + "size": [ + 92, + 158 + ] + }, + "plant/核桃/4.png": { + "sha256": "9ffa676f2b2438419940f7cfcbfd47062b600e2062337c6d39f572f88a17d578", + "size": [ + 115, + 173 + ] + }, + "plant/核桃/5.png": { + "sha256": "c839e21dcd3bca16d148acf0a26d1514b2761e52e257e370f7a7790ece3bbd2e", + "size": [ + 115, + 173 + ] + }, + "plant/核桃/icon.png": { + "sha256": "cd44515f455488ca77e8a06d550215a332acd44a9ec7fed2b9d936f9139f5da3", + "size": [ + 103, + 104 + ] + }, + "plant/桂圆/1.png": { + "sha256": "9d924c2c55fc9e2898e262df37101d1b23ebd8a9a3e98cbe4b13e218b05dea33", + "size": [ + 28, + 103 + ] + }, + "plant/桂圆/2.png": { + "sha256": "67a038f9799f7bd988d9b48513085df99c8a309b88f33d2f0ed5b2be96d24261", + "size": [ + 47, + 107 + ] + }, + "plant/桂圆/3.png": { + "sha256": "200589c8736f4a5e38edaa0b21bd324fe481604f06c575cf652c93a816754ace", + "size": [ + 80, + 148 + ] + }, + "plant/桂圆/4.png": { + "sha256": "55c0d319be84ca549b1ca29fa7c47177d63533b8db7adc7537042076a320b986", + "size": [ + 103, + 157 + ] + }, + "plant/桂圆/5.png": { + "sha256": "93ccc38bbe880570f8619a9a91fc714042bd1150a57aea127302aa3e4452e840", + "size": [ + 107, + 164 + ] + }, + "plant/桂圆/icon.png": { + "sha256": "b1498fb415e942018484f76ee10e6119e30d8df1a7a8d9f64ca3a2bfd29b7c33", + "size": [ + 100, + 82 + ] + }, + "plant/桃子/1.png": { + "sha256": "c50a819fc51453ae5944a9cadbf8ad6631d209137bc2e117da2055f323f8000e", + "size": [ + 30, + 112 + ] + }, + "plant/桃子/2.png": { + "sha256": "87c5c2959df77af2ce2f1e5fdd2327aa5115173de3fb31dc35683c18c7bc2e11", + "size": [ + 98, + 145 + ] + }, + "plant/桃子/3.png": { + "sha256": "59acf7b4746949ac9cd1d61230a1f7b6d3edb94ec0d25c10b994c3e7a7475da3", + "size": [ + 109, + 156 + ] + }, + "plant/桃子/4.png": { + "sha256": "0ce1e58539c2d4973950c7af57fea61b7342b3a22fd2f39bc40f22c167850c50", + "size": [ + 110, + 152 + ] + }, + "plant/桃子/5.png": { + "sha256": "7759ec5cd17b1f5c2083e1aa20fae882f1902a98f13fbe79e3a512b03c025a24", + "size": [ + 109, + 156 + ] + }, + "plant/桃子/icon.png": { + "sha256": "1554571946f38ef51e69a32938d909be205dbd10d8312ef6aa16801e4c172029", + "size": [ + 112, + 86 + ] + }, + "plant/桑葚/1.png": { + "sha256": "e4af4860940f4861037d0758c1b306e2f47a3c234c7432bcb17924324876aa0c", + "size": [ + 37, + 97 + ] + }, + "plant/桑葚/2.png": { + "sha256": "818d3746182fbc9b57b69699814513e3c8802f8bcbea94c1d45a5514e093c53e", + "size": [ + 57, + 104 + ] + }, + "plant/桑葚/3.png": { + "sha256": "61fb6b1810b8ca8fb034f721f5adf0873a04c67e4f8cb9da98dddcd379cd2aed", + "size": [ + 107, + 149 + ] + }, + "plant/桑葚/4.png": { + "sha256": "b5010c6ed58a1a1be2f0f4b08b07e8df6ce47dbfa4b9f0ad4add3ae13ec87b20", + "size": [ + 119, + 161 + ] + }, + "plant/桑葚/5.png": { + "sha256": "7a540d5b1c73f5a9dfe2c3979bbe2a54696b2edac418cc1ebf11346be6c58f46", + "size": [ + 123, + 160 + ] + }, + "plant/桑葚/icon.png": { + "sha256": "b59e026d41fd79b2db6a96a4f1721ead48736dc1c53bffa3529faa7b5c422438", + "size": [ + 100, + 91 + ] + }, + "plant/梨/1.png": { + "sha256": "b22336d469ecec23ab9e0c9c51f822977526c5720239b83e3e8d481b4f64fa03", + "size": [ + 62, + 120 + ] + }, + "plant/梨/2.png": { + "sha256": "85e04af6e7119afa658ba9eae36fe34e041fc982ec5f35ae958de92d5769b8f8", + "size": [ + 117, + 170 + ] + }, + "plant/梨/3.png": { + "sha256": "b25b9f16d8415a13a6e6728d71ddec8dcd4380d157b6f940822d2c7eed37ee57", + "size": [ + 111, + 167 + ] + }, + "plant/梨/4.png": { + "sha256": "da7df2f49f62c7de5deef5ad89282694bebdc16a7f6b12f4de7099f6304fa08b", + "size": [ + 104, + 160 + ] + }, + "plant/梨/5.png": { + "sha256": "ca1e7348c8b932caf921a939503b75c407940277cf6abff1208c7d9d21ee36a3", + "size": [ + 119, + 172 + ] + }, + "plant/梨/icon.png": { + "sha256": "c036c59b9f73265616059e16045414fd1581053ecb1ec3d29868f88dd8e6ae02", + "size": [ + 59, + 102 + ] + }, + "plant/椰子/1.png": { + "sha256": "af1a0c2da0c515ec04e748f1c8a0bda3f7c7f8896c59752b48b74d76ed0c51cd", + "size": [ + 27, + 103 + ] + }, + "plant/椰子/2.png": { + "sha256": "f00c8d41de74b53c7d58565590739f49f5f8c9f316827cb4ca12f04ab6562f54", + "size": [ + 106, + 158 + ] + }, + "plant/椰子/3.png": { + "sha256": "f866bd18dde10b3b69b3788188a7a09bd1e9c9ba7d8287c01019e93ac5354324", + "size": [ + 132, + 154 + ] + }, + "plant/椰子/4.png": { + "sha256": "08053c89d953e8d0278376c8e2b64d9dfd587ee44bf21f9ea9e27d54047318f9", + "size": [ + 131, + 159 + ] + }, + "plant/椰子/5.png": { + "sha256": "8dcec7c86f93cbf284941422003df7314c7c49208251d9946264440e602c3ff8", + "size": [ + 131, + 160 + ] + }, + "plant/椰子/icon.png": { + "sha256": "680e371ec17489c49564dcd532c8c53b8e36a84c823282d48e0ae43977e7ef0a", + "size": [ + 97, + 100 + ] + }, + "plant/榅桲/1.png": { + "sha256": "63d469dd7ddb87840ae9dcbdb03280dfe148fae2a8fa9ccaad0bc6da4c8abc96", + "size": [ + 30, + 104 + ] + }, + "plant/榅桲/2.png": { + "sha256": "fcc8f41bf4ccfe76cd613d244b60a1e6c89bb917ed63d879f96d4c6bc8959794", + "size": [ + 61, + 131 + ] + }, + "plant/榅桲/3.png": { + "sha256": "0dca43a66b177f4c280e766f8bb78bf22c6dc25b9e8f12ea377732ce55f36cfa", + "size": [ + 114, + 160 + ] + }, + "plant/榅桲/4.png": { + "sha256": "de85f16c99bfe26dfb39b4a9e59454a28ab296a0670f874722decb40998ef58a", + "size": [ + 120, + 163 + ] + }, + "plant/榅桲/5.png": { + "sha256": "ba37f79b94f6b921e9876fd2f8dc38acd248c05b051448ec00d6739e1fc36820", + "size": [ + 130, + 169 + ] + }, + "plant/榅桲/icon.png": { + "sha256": "af1e97047e3d819736a032704e2ba1b9e89606236c983a553fb2164ae7e8738b", + "size": [ + 103, + 111 + ] + }, + "plant/榛子/1.png": { + "sha256": "57823524c2b59c12b3a7d181f4d20af6105daaa1718da051c70ca4068904e57e", + "size": [ + 21, + 123 + ] + }, + "plant/榛子/2.png": { + "sha256": "8d2cbe6d8ddfc309c24b2628e562f5d064861b4de1cb6268559274142068d10f", + "size": [ + 74, + 149 + ] + }, + "plant/榛子/3.png": { + "sha256": "684359711a897fca5faebf76e0ee3a07fedfca6f7ca41a197a467af8e28a6dac", + "size": [ + 94, + 160 + ] + }, + "plant/榛子/4.png": { + "sha256": "5d9655472465952bf09b35fe0e6a126237597ebdd1a3ff9d32d88e2ee794b890", + "size": [ + 105, + 172 + ] + }, + "plant/榛子/5.png": { + "sha256": "546cbcf436a582c1d8101831c0c7c60185a8ffd00bdda33d1ffe8aa6729fc328", + "size": [ + 110, + 178 + ] + }, + "plant/榛子/icon.png": { + "sha256": "252bb76bafadafbc806b3465b9cb7524ac533588560a8a3e6f42c4790e6396c3", + "size": [ + 100, + 88 + ] + }, + "plant/榴莲/1.png": { + "sha256": "069cfb168b19dda7e42bc9fc80a389013a0a187eb1a064442b0344172c447a6e", + "size": [ + 39, + 101 + ] + }, + "plant/榴莲/2.png": { + "sha256": "4d76eac2c861474420e631d98433a2a3bfab3fc80a19078262b7ac5faa136086", + "size": [ + 80, + 138 + ] + }, + "plant/榴莲/3.png": { + "sha256": "656bb4cf69bc0b4211afc3e479ba80f908212ff6077d60d68d8945f2d704ec67", + "size": [ + 114, + 171 + ] + }, + "plant/榴莲/4.png": { + "sha256": "421d013823ca76bc021ab35ecbb9edd1dfc33f227c0c51b285e924878505de80", + "size": [ + 114, + 171 + ] + }, + "plant/榴莲/5.png": { + "sha256": "4cffb0f238873bae35e96ce4e34bd0ee7ca141a15dc8c9de67681dd900427c10", + "size": [ + 121, + 180 + ] + }, + "plant/榴莲/icon.png": { + "sha256": "9e3deb57760570d9c3012abdbfb96244ff62b7e22bfe050a20de3bbe1ded9d88", + "size": [ + 100, + 117 + ] + }, + "plant/樱桃/1.png": { + "sha256": "f83084bc0c3f655f904074ab0a946ca0858127169ac3c6964b90b19fe2441704", + "size": [ + 32, + 90 + ] + }, + "plant/樱桃/2.png": { + "sha256": "9b62a5ce236a3793d4bc65d2509a48f5421fdb401699785312215f0a3b2e63b9", + "size": [ + 70, + 126 + ] + }, + "plant/樱桃/3.png": { + "sha256": "6cf8d0f8825fdcc5de1026a3f867b3fa29933828bc64c6b32f95864e51425834", + "size": [ + 121, + 142 + ] + }, + "plant/樱桃/4.png": { + "sha256": "50629768570c1c27dbe1c63fb3d8c37ad31500c12133303282c3901848c2917f", + "size": [ + 134, + 152 + ] + }, + "plant/樱桃/5.png": { + "sha256": "5b47c81b741383a8ec2fbeb6cb957ee2a8efcef07b27375f57b61d6eaa9d5e18", + "size": [ + 127, + 152 + ] + }, + "plant/樱桃/icon.png": { + "sha256": "e0847763add698cfbff5ba01c593e5c48e51c20cb49e4c520ee3d25ff049279b", + "size": [ + 95, + 100 + ] + }, + "plant/橄榄/1.png": { + "sha256": "d228b817f50c0b515e665bf36b169fd81a3cdb00afde9adb026e66132323d2cb", + "size": [ + 17, + 80 + ] + }, + "plant/橄榄/2.png": { + "sha256": "4c96df4a661164f805a566f5751f6a6cfa57459e3cfb62fc52fd818e4cd1603c", + "size": [ + 39, + 113 + ] + }, + "plant/橄榄/3.png": { + "sha256": "0e89a032b844ea12473470041f705eb7bfb1323cfbcd5684de3c8a89fadbaadf", + "size": [ + 96, + 139 + ] + }, + "plant/橄榄/4.png": { + "sha256": "5b71a2dd245498f03ea64e5607940a10badf6905eb4fa00bdde7526a9c23456c", + "size": [ + 125, + 180 + ] + }, + "plant/橄榄/5.png": { + "sha256": "2cbe55864dccf267849ac570d95b5b3dce3764c399a7d3242d1a780a78671536", + "size": [ + 124, + 178 + ] + }, + "plant/橄榄/icon.png": { + "sha256": "cc11e206890c9a73781e2caf094b6374789c1912b0cc531db754df22032d3d5a", + "size": [ + 100, + 56 + ] + }, + "plant/橙子/1.png": { + "sha256": "a457199742ec00115c8a79f5023b26d012ff9104439d9246b08bb9f1c5a6bada", + "size": [ + 29, + 85 + ] + }, + "plant/橙子/2.png": { + "sha256": "606959efc3385e4262339d3afc907f58a051fb2b23d19fc9104721ef89d9eb8e", + "size": [ + 49, + 128 + ] + }, + "plant/橙子/3.png": { + "sha256": "a17e0a18c50d9e2bf6d6af43d903c2ab660f45255faee7152070265be2a02771", + "size": [ + 130, + 157 + ] + }, + "plant/橙子/4.png": { + "sha256": "9158b672b6d6b14888ff65256bd1018ddb42815abc01d3070328d37ff5989341", + "size": [ + 132, + 155 + ] + }, + "plant/橙子/5.png": { + "sha256": "49e262b08f18f34184d476bb679d37788b456aa7573d842c8a2db877b93c7b22", + "size": [ + 131, + 153 + ] + }, + "plant/橙子/icon.png": { + "sha256": "d0f4b065f3c68793f7e8b50f942f2b15b3a8b80dc356fe3cf754b50927826c34", + "size": [ + 81, + 80 + ] + }, + "plant/水稻/1.png": { + "sha256": "9d59469abe1398826a6140e39a7f16147a8b2bf58c73580d9a5864977ccc0696", + "size": [ + 166, + 106 + ] + }, + "plant/水稻/2.png": { + "sha256": "a23e9b2c9c7576215f490957a0b2885387bacb90bfedbe183fe4d9472d3d77f4", + "size": [ + 167, + 160 + ] + }, + "plant/水稻/3.png": { + "sha256": "fa9243f259bdf6f67cf87948a52f23969990c2c79209efd06fef33f4e18b18db", + "size": [ + 166, + 185 + ] + }, + "plant/水稻/4.png": { + "sha256": "dce2247d47967c44edea8b298a0ad653347f126351d3c8cc7dccc3a1c5e31ba2", + "size": [ + 111, + 220 + ] + }, + "plant/水稻/5.png": { + "sha256": "c07e7dc220b78c3d68b12c6059cf0398dab34643b0f108a0dfe07c4b04ec9e51", + "size": [ + 120, + 245 + ] + }, + "plant/水稻/icon.png": { + "sha256": "716a384e906552ffd6a1840530f0c92a38a1c182445c3367847cf418dbf55413", + "size": [ + 104, + 97 + ] + }, + "plant/沙棘/1.png": { + "sha256": "2f874ca2c46c36f907f0a9add7bcd997add2dc103f35752c19f8e4a743defeff", + "size": [ + 37, + 107 + ] + }, + "plant/沙棘/2.png": { + "sha256": "db748357faad71fca5e11f8ec53785ba3255ce39658ac0d6b35230fe115f106f", + "size": [ + 66, + 126 + ] + }, + "plant/沙棘/3.png": { + "sha256": "946a9e346d433d1b9d79a34145efbfacac6e681ece32ed5d33bdc4d02e688e58", + "size": [ + 109, + 139 + ] + }, + "plant/沙棘/4.png": { + "sha256": "560d6830fc7ffb78cfa2f73697c83431124d3aca634895ae7561dbc3022d5193", + "size": [ + 134, + 158 + ] + }, + "plant/沙棘/5.png": { + "sha256": "edbf521399206ac3af3db272d9a9e5edf242c8c9ee260fd057991eb113ad96dc", + "size": [ + 135, + 157 + ] + }, + "plant/沙棘/icon.png": { + "sha256": "d6311cb8e16fb96232c695de7b782009af7bb87d8c0febc7b796143c89c1846a", + "size": [ + 110, + 101 + ] + }, + "plant/油菜/1.png": { + "sha256": "70f703c8a1782501341f1c231f45aeea58109b4eaf20185fd0eec4986dbb24c2", + "size": [ + 26, + 83 + ] + }, + "plant/油菜/2.png": { + "sha256": "07b03cb0b2a26ab10f3f850197ac387f87e3f86fdb33a49c2bfabcb17a23c20f", + "size": [ + 50, + 100 + ] + }, + "plant/油菜/3.png": { + "sha256": "5e2afe2d8ea659add5f61c01b581b0b9b42c7f6082dc3b0b898ca6fd486f6c60", + "size": [ + 88, + 125 + ] + }, + "plant/油菜/4.png": { + "sha256": "328998162674fa7db4dd1caf55214b59d7c4b5c86545c15456c0525884392e55", + "size": [ + 96, + 155 + ] + }, + "plant/油菜/icon.png": { + "sha256": "933a6980d88008c67d13d19deadd5390aa4c4c926556b420ce8ee23cf2882665", + "size": [ + 79, + 108 + ] + }, + "plant/火龙果/1.png": { + "sha256": "19b6b4b69d030d311449dbf1d4dc25e1ec8d2c0445a44fe6b0e789c103cce751", + "size": [ + 27, + 94 + ] + }, + "plant/火龙果/2.png": { + "sha256": "099efff46a25a31aefa1cf375591e0f144043b6cdfeab99c87993cf3da14976a", + "size": [ + 50, + 111 + ] + }, + "plant/火龙果/3.png": { + "sha256": "0ac5b5151c3b555ee9d3c638a271c59eecb0e5e6ae0610171d76e1daec7b4001", + "size": [ + 109, + 149 + ] + }, + "plant/火龙果/4.png": { + "sha256": "7a6280420c6a203b1ba95160d30e91d66ff4e03cc37dd57190174843d19117d8", + "size": [ + 112, + 151 + ] + }, + "plant/火龙果/5.png": { + "sha256": "ee95b3b0706f6b0729c677e52f86835022ab2fe93a460bb8acc99667190b7703", + "size": [ + 118, + 150 + ] + }, + "plant/火龙果/icon.png": { + "sha256": "d1085ac11285c860f3ef922007c84846534369c712631b721ba3d62d62e8244b", + "size": [ + 87, + 109 + ] + }, + "plant/灯笼果/1.png": { + "sha256": "29071a6239ff7c7d90a41cf1ad2a368fae8a5ce42e3bcf8e29d0d60b8da8f23c", + "size": [ + 39, + 112 + ] + }, + "plant/灯笼果/2.png": { + "sha256": "2d758cfdc362d6e339462feae7279d2c3148922ddc0e635c5c4b38d52a934976", + "size": [ + 63, + 126 + ] + }, + "plant/灯笼果/3.png": { + "sha256": "40638f68053c5e13a177a274cb1bd799ffeec229a0a050fd055fe752e8efcf80", + "size": [ + 95, + 162 + ] + }, + "plant/灯笼果/4.png": { + "sha256": "72eecf4ff531fc9fb43fd5e25d2188f00db08d117225a1a56bf767f98be945c8", + "size": [ + 106, + 175 + ] + }, + "plant/灯笼果/5.png": { + "sha256": "1663582e1cc5e4bc894b366b43cde2fc32ec7d572b220cc0b4aaed861f5ffe9f", + "size": [ + 117, + 187 + ] + }, + "plant/灯笼果/icon.png": { + "sha256": "47ed2b946f49cb4c55ca0898f2b1488c33b412fdf2eec81781c738cafd00d086", + "size": [ + 78, + 100 + ] + }, + "plant/点地梅/1.png": { + "sha256": "a92dcc6dd4bd1e76b38bc139b6153ec0b7751b57348a62ff2d254f55cfd77d39", + "size": [ + 45, + 76 + ] + }, + "plant/点地梅/2.png": { + "sha256": "6ffd408b3aa47270d823baefb604b4d85004c82b9083adb247a8e6c45785edc2", + "size": [ + 76, + 87 + ] + }, + "plant/点地梅/3.png": { + "sha256": "0ffb466ff4f52d0392348da29b7aebb364f245ef6c43f882ad13aa23ab0e0352", + "size": [ + 84, + 142 + ] + }, + "plant/点地梅/4.png": { + "sha256": "7d59cd9645dbd22d0b052b5424996930040f5b49e17e97fe3ac92be6c8b3ff8f", + "size": [ + 112, + 165 + ] + }, + "plant/点地梅/5.png": { + "sha256": "14789ff2158de1f21a8ee65c7cf91d205fa1c355bfde1c36fe298731f4509260", + "size": [ + 115, + 169 + ] + }, + "plant/点地梅/icon.png": { + "sha256": "116c898756079a2aefbaca110046ed6dc9676a9873369d6ed883b5d0aa873e36", + "size": [ + 109, + 109 + ] + }, + "plant/牧草/1.png": { + "sha256": "183d5a3119c4143cf6f9e6ccdd92deeb5f2ae2266962f9d86670d2952824cc50", + "size": [ + 39, + 117 + ] + }, + "plant/牧草/2.png": { + "sha256": "69ffc0934e2f81b0a17a83e25fa92b11674edf4979656a5f437c0c8613094335", + "size": [ + 63, + 119 + ] + }, + "plant/牧草/3.png": { + "sha256": "b7e126d771b7d2f388aa512a49aed59c1b40400389ea2b2776c1ff94ffb2fa1f", + "size": [ + 111, + 161 + ] + }, + "plant/牧草/4.png": { + "sha256": "d3599f32df48a5bb858b254c0197780683a02b9b383396d946b8d8cfc4d0e564", + "size": [ + 111, + 161 + ] + }, + "plant/牧草/icon.png": { + "sha256": "b98f6273c90c62deb6127769920ff95972011c8cd44564da4d2b9402686a843e", + "size": [ + 100, + 84 + ] + }, + "plant/猕猴桃/1.png": { + "sha256": "a3e09e2b953b0e88db555745cc3b8d169f0fa7e7187db497bbd35aeada448ce8", + "size": [ + 37, + 88 + ] + }, + "plant/猕猴桃/2.png": { + "sha256": "3448034012a68d2a7733f64bf38eb6900e80f8f8cfb87b73ad1bdb576101180a", + "size": [ + 59, + 103 + ] + }, + "plant/猕猴桃/3.png": { + "sha256": "b59790add2a110f871ed8795336bc0015d9a09ef6024a8146a1de5cd28528688", + "size": [ + 90, + 123 + ] + }, + "plant/猕猴桃/4.png": { + "sha256": "cfb163596ad42ffc3e1e8a564b54c83d26a843fd292bd9d13c14690831b61feb", + "size": [ + 117, + 149 + ] + }, + "plant/猕猴桃/5.png": { + "sha256": "3164533174d734b107c88b28e5b7137bab5c5f0156fa16bba488ab04894db179", + "size": [ + 128, + 158 + ] + }, + "plant/猕猴桃/icon.png": { + "sha256": "0cd8e030ea0c2580bbec2fdaa7fbdb1a130304b4d5450b62bc944db002421249", + "size": [ + 100, + 87 + ] + }, + "plant/玉米/1.png": { + "sha256": "ab6d85787babcb2f162c6a33a6ffe3531c617613a2cbe51fb847db2af12e5f72", + "size": [ + 43, + 103 + ] + }, + "plant/玉米/2.png": { + "sha256": "e678f97545bb8586918a2f53af95fe74d90fb70a2cc857621c24a8a0a2535b13", + "size": [ + 74, + 102 + ] + }, + "plant/玉米/3.png": { + "sha256": "3569183a08eb75819eada8330820eff378656cab8ee8f30a075e5fed5197a8ff", + "size": [ + 107, + 155 + ] + }, + "plant/玉米/4.png": { + "sha256": "ef83afb461d541d1aa5a0fabcd6e10aaab68d7b15f6cfe0daa02870e1a8d3438", + "size": [ + 119, + 164 + ] + }, + "plant/玉米/5.png": { + "sha256": "06ce289fbcb3caeb3d6f1436ed7bcfd4bdd6f13f7d0054952087d0ed0d27797f", + "size": [ + 116, + 157 + ] + }, + "plant/玉米/icon.png": { + "sha256": "ab85215c53ebb6494ae37a4b3f64f05091e76aef476282c0c77fab2ba5901ddd", + "size": [ + 98, + 92 + ] + }, + "plant/甘蔗/1.png": { + "sha256": "61b2bc787c83c3964f896d799c928fe206042d83de5ce446e297b3b0640f4464", + "size": [ + 41, + 108 + ] + }, + "plant/甘蔗/2.png": { + "sha256": "b1bdd2e4c2b3f2f67c7ff6949e34d6088548235472fed9fd918d4c29a2c8dd25", + "size": [ + 63, + 119 + ] + }, + "plant/甘蔗/3.png": { + "sha256": "15cb2142e0e4f03ed0150cbd385b1827732e93842eaab3164aab187f693a7f9a", + "size": [ + 77, + 134 + ] + }, + "plant/甘蔗/4.png": { + "sha256": "af3010905c65fcff73cbb352b237c8f3fecbe31f78599cb0adfeaa278255b147", + "size": [ + 104, + 158 + ] + }, + "plant/甘蔗/5.png": { + "sha256": "fef18ee638899247f25c8dfa5a1b2cdd9d13f2cfb87ee9a107780c9dc031e9fc", + "size": [ + 123, + 169 + ] + }, + "plant/甘蔗/icon.png": { + "sha256": "5166bee733ce7b1371e5cd688526259f8072de3f3f4a65f5fc9c259dd3075169", + "size": [ + 100, + 90 + ] + }, + "plant/生菜/1.png": { + "sha256": "63f58761cc51f774e6037cc1b2941667471ff5b0e2eb52da4f140ff4f078d390", + "size": [ + 25, + 80 + ] + }, + "plant/生菜/2.png": { + "sha256": "9e9cdad8809bb05f8cc9673ac89628f3728a1b2361e4f37871a82479a651e969", + "size": [ + 31, + 81 + ] + }, + "plant/生菜/3.png": { + "sha256": "6823949aa588389b388324e74edca069899e0069baf96dedd7871c17a0d401c4", + "size": [ + 54, + 97 + ] + }, + "plant/生菜/4.png": { + "sha256": "c806728b0409a36a2cfe71a2ff933c0c6448d72fc490a3b87b26eeee93f1923d", + "size": [ + 86, + 117 + ] + }, + "plant/生菜/5.png": { + "sha256": "6be682cd11eddac79891e62c587fd57655332f38943a4a24f20e3263397f842a", + "size": [ + 121, + 135 + ] + }, + "plant/生菜/icon.png": { + "sha256": "79b49f2a1ec0310ad5d2f4999ce210a06fbc55c4bdd825b0a944b29137719075", + "size": [ + 120, + 118 + ] + }, + "plant/番石榴/1.png": { + "sha256": "e0a46d739ac7f6295f7de980b0450f1f47f8c70b376e470411537db528b37964", + "size": [ + 49, + 110 + ] + }, + "plant/番石榴/2.png": { + "sha256": "ea27004deb2a6ea6ad905bf243cb485b8fb1caaf1b62efa169fbdf58f2b3a6fc", + "size": [ + 93, + 140 + ] + }, + "plant/番石榴/3.png": { + "sha256": "cc9a431e6595e18b734b23e675263214b1df065fc2ce070dfc0f0cb1fb9732cb", + "size": [ + 101, + 147 + ] + }, + "plant/番石榴/4.png": { + "sha256": "a02326c69787d63d4cacb57f462c6fec83a7ea7a2e0904604190e3f24922c910", + "size": [ + 109, + 154 + ] + }, + "plant/番石榴/5.png": { + "sha256": "4bb8636be46059f0adb39837a011370d6f1db791bfc3ce653240440ff1686012", + "size": [ + 117, + 162 + ] + }, + "plant/番石榴/icon.png": { + "sha256": "3092dbdcba62583f3b54e3f5568a51803815484f3b21770ec6c2b6d254d6a329", + "size": [ + 83, + 100 + ] + }, + "plant/番茄/1.png": { + "sha256": "48de2a54c2868852f70e981a0ef8228ad0bb09499e6c9e84057aaf82f16f7270", + "size": [ + 67, + 98 + ] + }, + "plant/番茄/2.png": { + "sha256": "d45b3e6896e6eaac51d40fe85ed460b686a8f1d91578a271369f0ea2250f9460", + "size": [ + 49, + 135 + ] + }, + "plant/番茄/3.png": { + "sha256": "86eb67cc3904bd2cf234cc30c27b01a911f1abdfeec6ccc58f9569a028ad0056", + "size": [ + 117, + 176 + ] + }, + "plant/番茄/4.png": { + "sha256": "14a494d9d0a42315cf09f7c48e7ad11e33a035bda5d0ae4de0b7e42832808c52", + "size": [ + 129, + 176 + ] + }, + "plant/番茄/5.png": { + "sha256": "52a455a943fa41d30db8c2d20861e2d6d465d6f28511933e2b2b3ec994b41a01", + "size": [ + 129, + 180 + ] + }, + "plant/番茄/icon.png": { + "sha256": "d068d503bc2abdee8037a02981d4511a9c0e3c8f9ebeec13a5ec52c14a4efb61", + "size": [ + 79, + 86 + ] + }, + "plant/番荔枝/1.png": { + "sha256": "0cbe9593e3491aec31c4ba8190e0d3209c7835b1744c118d054f9a22f7ab36ff", + "size": [ + 50, + 132 + ] + }, + "plant/番荔枝/2.png": { + "sha256": "486bb314164cf86f0112a95ce2ca1f0fb9ab902d306f5df038b742f9274f4a9e", + "size": [ + 76, + 147 + ] + }, + "plant/番荔枝/3.png": { + "sha256": "6fece8fd8f5ff7e802d532a9cfdcd5389a491d8c1e4c9236816ae72bc2f50457", + "size": [ + 118, + 171 + ] + }, + "plant/番荔枝/4.png": { + "sha256": "e639076ba0d039908193dc2a5d0eee4abe2ee45ebff74eb289588202c8b1a1c4", + "size": [ + 141, + 176 + ] + }, + "plant/番荔枝/5.png": { + "sha256": "6b4cc5b8b5ae659f34af621d3a7f096d127dadfc6503148973c1bf35d475d57a", + "size": [ + 142, + 174 + ] + }, + "plant/番荔枝/icon.png": { + "sha256": "946051b985a3195521f56e8e4741bb56440d9fb26e9e60323f8be81278353725", + "size": [ + 100, + 86 + ] + }, + "plant/白萝卜/1.png": { + "sha256": "dc2a69d8438e435b57870246d171a29b91d671b8b7846386e2535db68ab380c0", + "size": [ + 38, + 104 + ] + }, + "plant/白萝卜/2.png": { + "sha256": "79211fce07648b11a54e7c6b13e106c02accca504a4be3386371ec18d5c779c3", + "size": [ + 47, + 97 + ] + }, + "plant/白萝卜/3.png": { + "sha256": "4b77bb0f3db7fff21f873881de6c76df9bfc809f92f2fc521745c32550d720cc", + "size": [ + 63, + 118 + ] + }, + "plant/白萝卜/4.png": { + "sha256": "8e06fcb06a8081060dfbb8b6ac2db42a2a3cee14fab4ae57e083f907abc95632", + "size": [ + 133, + 177 + ] + }, + "plant/白萝卜/icon.png": { + "sha256": "ecb0e8d11a620b03f2fb990f1dab5b0ffa051eba136b7bf2c0119e6ec8d9f841", + "size": [ + 70, + 114 + ] + }, + "plant/百香果/1.png": { + "sha256": "0627e9f68daad4fb9dc83f930160e06e750f7c65e419b3cc8b7071108a8c0d70", + "size": [ + 38, + 95 + ] + }, + "plant/百香果/2.png": { + "sha256": "948bf55e5f667cd487237882a4eeddcfc57fbbe0d79f47b421debae0022bac7c", + "size": [ + 50, + 122 + ] + }, + "plant/百香果/3.png": { + "sha256": "c90f65069cd71ac64392dd0c990ddf7954e81d97d127d38e4911c7d4aa142267", + "size": [ + 112, + 171 + ] + }, + "plant/百香果/4.png": { + "sha256": "aeb0b24e74f80c9b842241613fe9c3303284565b56939415f93147a6b006139b", + "size": [ + 112, + 172 + ] + }, + "plant/百香果/5.png": { + "sha256": "a3f852c8ce2dc018411dfcdc651cc6a2adc632b103f82d49801a99b84b54592d", + "size": [ + 112, + 173 + ] + }, + "plant/百香果/icon.png": { + "sha256": "5eef021c067edf9610c4312362f25017b25c885553556e8fa0c86a2b89048b41", + "size": [ + 62, + 100 + ] + }, + "plant/益母草/1.png": { + "sha256": "9abf7208deb4fa48761c15ed825bc3a15c1afd751a1f6cf6a23d7b5b42a07809", + "size": [ + 19, + 77 + ] + }, + "plant/益母草/2.png": { + "sha256": "c542b2fd5605aa32e39e14535f974154fcfe2c8417f6376d0f481b6cac4615a0", + "size": [ + 47, + 99 + ] + }, + "plant/益母草/3.png": { + "sha256": "f09e31995ae863cda34334377066803145ad0c01abb725cafd5ca5ac18605821", + "size": [ + 71, + 118 + ] + }, + "plant/益母草/4.png": { + "sha256": "3a6f6bc009ad50466781f8a4386316c575f782ad50b8fbb7a9ec2285eeba80c6", + "size": [ + 98, + 145 + ] + }, + "plant/益母草/5.png": { + "sha256": "a08322b57a767d6b30f86824068da4c2ee10325c0fb85274dcac62dd34ee37f4", + "size": [ + 122, + 166 + ] + }, + "plant/益母草/icon.png": { + "sha256": "c7abe3edd448eb4af61edc53d25818f42c7ca35e5bb7f3bd5826216fb0f03a0b", + "size": [ + 119, + 101 + ] + }, + "plant/睡莲/1.png": { + "sha256": "345a197c7bd05671ee3183da48023b4d156ca798e8960d71c7db755f1c0575c5", + "size": [ + 133, + 97 + ] + }, + "plant/睡莲/2.png": { + "sha256": "1fabe9809448a903c3b744e9f39b8e21eb0512a544fb7e8f461622fb412db34b", + "size": [ + 133, + 113 + ] + }, + "plant/睡莲/3.png": { + "sha256": "e7d0692948e210901e91109a371c07c2ccad94e3469d7a416b96bbdb66f37a2a", + "size": [ + 133, + 113 + ] + }, + "plant/睡莲/4.png": { + "sha256": "914fe1078d65c61ea6cc9f877f9ca2056277ffa2d8e043a09681c2309718ff96", + "size": [ + 133, + 136 + ] + }, + "plant/睡莲/5.png": { + "sha256": "3f2cf80bac71a4302640a569d9ef8acf88f80ad5374548a965aebf96439ee190", + "size": [ + 133, + 137 + ] + }, + "plant/睡莲/icon.png": { + "sha256": "62bef47fa520c15eeaa8e48da03a669712a7f239f6257c2145d0280069a25884", + "size": [ + 121, + 101 + ] + }, + "plant/石榴/1.png": { + "sha256": "09544e01b3b9a13dcf538fa55beb8120c0c4a3393a08c1a4ce23abdd21c99dd7", + "size": [ + 32, + 119 + ] + }, + "plant/石榴/2.png": { + "sha256": "8888f07cde4e9b52eadbff4080440755c00724b82f2daec87d567e08c87e0f9d", + "size": [ + 79, + 122 + ] + }, + "plant/石榴/3.png": { + "sha256": "fef0b220cf10bb2ee23aabd0d5c68c49302addbbb9e8466336e2010dcef9c661", + "size": [ + 105, + 150 + ] + }, + "plant/石榴/4.png": { + "sha256": "ae5e7adf557381f3b0f7ac4e09f47589ad97ce3a20fe0059d6a364bfa26e945f", + "size": [ + 112, + 158 + ] + }, + "plant/石榴/5.png": { + "sha256": "6f5f68d91db058b781fc455b727ea935d982275d4bbfd0ce5fdb3006ace79d74", + "size": [ + 117, + 163 + ] + }, + "plant/石榴/icon.png": { + "sha256": "8b92d40654dadd72af99fa828d9cc426485e03e2c81bf72ce10bcf96e58f2a5b", + "size": [ + 97, + 100 + ] + }, + "plant/竹笋/1.png": { + "sha256": "1b683b134b5c5e851c6618e3869c3d116cab05a4cfa9d91d5be3ed4cae425f06", + "size": [ + 52, + 95 + ] + }, + "plant/竹笋/2.png": { + "sha256": "60543d08f856292fcc3248b6321b96561be4dce8e4c96a23d9f6d90663283c40", + "size": [ + 77, + 67 + ] + }, + "plant/竹笋/3.png": { + "sha256": "f8a17520b406b26d97caec5ab503311ebc95e9482dbceb8d154fa6c138e184b6", + "size": [ + 77, + 95 + ] + }, + "plant/竹笋/4.png": { + "sha256": "c7b62bd66d6ec765983d07b66d754a7163083b9c4494884b6a7fe327b536bb62", + "size": [ + 100, + 120 + ] + }, + "plant/竹笋/5.png": { + "sha256": "51665020450fcc59d3f00fd68f7d1483fac10d45f2d2854e7a17a3ccff269b36", + "size": [ + 108, + 130 + ] + }, + "plant/竹笋/icon.png": { + "sha256": "0869c695e8c4bdf0a4003069fa16c93329075a654fe0f56e2895c45ddb25a17a", + "size": [ + 117, + 124 + ] + }, + "plant/箬竹/1.png": { + "sha256": "b3413b8dcc810d21316b99c994c2cff60a74200c4fa65693418ba95ba0be027f", + "size": [ + 47, + 118 + ] + }, + "plant/箬竹/2.png": { + "sha256": "17eba0656d5c48f6819f34676b89d7b9f683a3a34ac502ac7eae468be464bf50", + "size": [ + 64, + 142 + ] + }, + "plant/箬竹/3.png": { + "sha256": "2f62b3036cc4cfc3ffe507932940ece597094b5d572e686d2507be47a563300f", + "size": [ + 80, + 152 + ] + }, + "plant/箬竹/4.png": { + "sha256": "3c81704c43db543e2bce11ca41b2ded23951ab7f81df1ec7e9243c17f682a6e1", + "size": [ + 103, + 169 + ] + }, + "plant/箬竹/5.png": { + "sha256": "4e208e53d927ba13b8a70eb79ad3d3485444e098a8cd62959dc4633fd01394fe", + "size": [ + 123, + 177 + ] + }, + "plant/箬竹/icon.png": { + "sha256": "3c2f04f7c6369210928e82821d86e071714d3eb46a21f41cbec80e06b619ae59", + "size": [ + 130, + 115 + ] + }, + "plant/红叶梅/1.png": { + "sha256": "980ad331b234d9d65a2d1bb7086251daf9847f1f161b0a66b422e09765725deb", + "size": [ + 24, + 76 + ] + }, + "plant/红叶梅/2.png": { + "sha256": "541b45ecb3c3f719e1a413ccabeb645ab72749ea692a3981cf901dbb93cafdf0", + "size": [ + 41, + 107 + ] + }, + "plant/红叶梅/3.png": { + "sha256": "1a443bec6a1aea5fbf58a9e3f0acfe6eba4343fd7143d3e165fe8ac97d5de1a7", + "size": [ + 74, + 122 + ] + }, + "plant/红叶梅/4.png": { + "sha256": "18336cebd29ecc8ef78b04b76d73841bd3f06c071aa088a61f907ceee7a34325", + "size": [ + 119, + 141 + ] + }, + "plant/红叶梅/5.png": { + "sha256": "366dcf64921244226cbd7045de0d8502202272fdb4636df9e7b23596eff21cb2", + "size": [ + 149, + 160 + ] + }, + "plant/红叶梅/icon.png": { + "sha256": "a068b39b571c99ad2c6b43d62388ac21a1139e1cfe6bffac5dec5414bb0f3148", + "size": [ + 102, + 95 + ] + }, + "plant/红枣/1.png": { + "sha256": "f94f0c732f1f31e35514fd74ff06d188d432ced16e080ae1e6b3a206bdedb600", + "size": [ + 51, + 101 + ] + }, + "plant/红枣/2.png": { + "sha256": "efe3074e67477714887ac8a5f6fc5b931dba585e11027ca91ebc1b48d020ab24", + "size": [ + 72, + 127 + ] + }, + "plant/红枣/3.png": { + "sha256": "fff959106d712a607c50e25def7891a54a1dd0b49ad3cc9e0c476bf8c398a3b1", + "size": [ + 99, + 149 + ] + }, + "plant/红枣/4.png": { + "sha256": "355b3b3ed414b21c540351b2179401da2983028bcc413eede019b8d8951a21bf", + "size": [ + 110, + 160 + ] + }, + "plant/红枣/5.png": { + "sha256": "8070f2acddee0670f8eb89442b44f9a7ac0cb46d2507a7bcc94dc02981ea4028", + "size": [ + 123, + 169 + ] + }, + "plant/红枣/icon.png": { + "sha256": "736a6d84f1fa93da922ef90fb578d42d27b7507b07b407434b0a0f75649fc3c4", + "size": [ + 85, + 100 + ] + }, + "plant/红毛丹/1.png": { + "sha256": "683062c1571ebdcd49dcdd3ca964d32dd06cab2ca4af1e5c62871d6ee2420ad7", + "size": [ + 41, + 111 + ] + }, + "plant/红毛丹/2.png": { + "sha256": "c74b738942b34582d81acfb4aaa4cef1981ea3439e8c893cdaa77aab138e319d", + "size": [ + 92, + 139 + ] + }, + "plant/红毛丹/3.png": { + "sha256": "a6896447e26ad0c29a880c9421fda654130e7cd78e73e21416a0107d863d1b04", + "size": [ + 107, + 160 + ] + }, + "plant/红毛丹/4.png": { + "sha256": "304c9c4357c71adcb12aebb9d2a391d741b7cf948ba64c19b8e60606589e0f04", + "size": [ + 119, + 174 + ] + }, + "plant/红毛丹/5.png": { + "sha256": "199fb4e700c3d5f006988842e667d15e01e93d608603b60b80a0a1157e555b80", + "size": [ + 129, + 176 + ] + }, + "plant/红毛丹/icon.png": { + "sha256": "3ad66e423f4951f8015a2cc9c34e8a8432e0d7aee65ce3f5c831180e86bef138", + "size": [ + 108, + 97 + ] + }, + "plant/红玫瑰/1.png": { + "sha256": "7277bfb069ef402cb85f021436a748289abdabc0076193b090bdefad1e7e8285", + "size": [ + 53, + 88 + ] + }, + "plant/红玫瑰/2.png": { + "sha256": "8d56e23c265555a755af863578d003895b2c5534c83384ba9b36cb9ff95b4ee2", + "size": [ + 73, + 127 + ] + }, + "plant/红玫瑰/3.png": { + "sha256": "479ce727328db8b8aa772829a7c7ec81d43aff8ed0ca00b740566cbab9283883", + "size": [ + 100, + 153 + ] + }, + "plant/红玫瑰/4.png": { + "sha256": "0b8e03a472d380b7429fb62b58903b6bc4df47cb8e20190b64835711e59c2fbc", + "size": [ + 118, + 159 + ] + }, + "plant/红玫瑰/5.png": { + "sha256": "0229e194e44ad2de8c2e57e8ab19fb333e2fbddd90852374b378178f3759195e", + "size": [ + 120, + 158 + ] + }, + "plant/红玫瑰/icon.png": { + "sha256": "5a1e4f71db8f8c432cc6e3078db0c3dbb083fd0590a64e72f6d8ca5dc5053f84", + "size": [ + 92, + 102 + ] + }, + "plant/胡萝卜/1.png": { + "sha256": "2bc84cb733bf8c1615d16c06c97b1ae75df7cab671a0163d289fc3abde0d6a85", + "size": [ + 38, + 100 + ] + }, + "plant/胡萝卜/2.png": { + "sha256": "d5413f0bcdae26d067fd4e1e227a03e823089c3fa4e81a9a6c24a818c52c812d", + "size": [ + 47, + 95 + ] + }, + "plant/胡萝卜/3.png": { + "sha256": "811422d67fb641c5df55ba30d0592329f58cfa8ac5ec2ce90eee4cd7c9cc460c", + "size": [ + 63, + 114 + ] + }, + "plant/胡萝卜/4.png": { + "sha256": "10761093cbaec3b308b688b4100a2ceb0ac91119b29cf08b37cb56a60c73b11a", + "size": [ + 84, + 168 + ] + }, + "plant/胡萝卜/icon.png": { + "sha256": "d1d1da2511f99fa843b671a187ca2d488ab2b6e925db3d13e0ceb6d5a88721b4", + "size": [ + 60, + 113 + ] + }, + "plant/芒果/1.png": { + "sha256": "f50cd34122a6fe5111d78e796889b4278edd5eac3bc9ae7a82df82628001e237", + "size": [ + 43, + 104 + ] + }, + "plant/芒果/2.png": { + "sha256": "365382d50026838bf45e2e47c641ce56739e91f5d34acb1a3ba0f680f2a77549", + "size": [ + 73, + 128 + ] + }, + "plant/芒果/3.png": { + "sha256": "971bc9a0191d746c76c1f43248c95ddef723433a8c8aa444b88ae9918d6a82a5", + "size": [ + 113, + 155 + ] + }, + "plant/芒果/4.png": { + "sha256": "896029f0f33861d0da6df61b489e44cb01b0c1fdbb7318a5caf2421cf1649fdc", + "size": [ + 124, + 173 + ] + }, + "plant/芒果/5.png": { + "sha256": "f2f218e287d15dfc2b9f89bf688ced2435db5421655b3828089ffbce1dbdd777", + "size": [ + 124, + 168 + ] + }, + "plant/芒果/icon.png": { + "sha256": "98146d3d643dec342ba0f19bb455d600eaba1e3cc7abd4d7bd150adec95a0bdd", + "size": [ + 110, + 104 + ] + }, + "plant/芦荟/0.png": { + "sha256": "555992a140d13779320cf1b529f0e579c0c9095ab8b6caddc442111bdb3ca8bb", + "size": [ + 23, + 25 + ] + }, + "plant/芦荟/1.png": { + "sha256": "3750257fc18884b444f0e7460ac564a816c5f665be999ce819a30f5a0d453467", + "size": [ + 30, + 80 + ] + }, + "plant/芦荟/2.png": { + "sha256": "b340b3dc41ca6bc63eacc269eac3c4dc6f4264be4c47c074460ba82945edf0a0", + "size": [ + 38, + 93 + ] + }, + "plant/芦荟/3.png": { + "sha256": "f019d7ceaa9ecb538acab0c56d921ef025941ddd26ce11ca1c7751d373d0e424", + "size": [ + 69, + 108 + ] + }, + "plant/芦荟/4.png": { + "sha256": "aab02531bc23d2942c498ebee86ae53a7e64465387e3a426b207ca43150c1b23", + "size": [ + 101, + 161 + ] + }, + "plant/芦荟/5.png": { + "sha256": "c58838bfb72c482045c9f884bca7439e9a700866484aa281404165281384ba7f", + "size": [ + 132, + 141 + ] + }, + "plant/芦荟/icon.png": { + "sha256": "430907d07e2bb080177537d79957f3cc1350e3f6ac09d578e5aeeac013386a4b", + "size": [ + 97, + 100 + ] + }, + "plant/芭蕉/1.png": { + "sha256": "06611ad6d2961b4e0c8d8993f6fe873159605ef90a7a698eeddefa352e31a4b2", + "size": [ + 39, + 96 + ] + }, + "plant/芭蕉/2.png": { + "sha256": "c6b9529d1f3561272b51ff54a07e31c501672e447f56e23f9566558ed12c248a", + "size": [ + 57, + 118 + ] + }, + "plant/芭蕉/3.png": { + "sha256": "e4a76d4524a8fab10425ae3c19809748aa385407ed9acc69ae7f2ce167213a8d", + "size": [ + 105, + 156 + ] + }, + "plant/芭蕉/4.png": { + "sha256": "27bfb6304efbf52a26cb148f10fb2d3484feac993b64c877de7d5ffd36fa22ea", + "size": [ + 111, + 162 + ] + }, + "plant/芭蕉/5.png": { + "sha256": "3bee0f94dc687fe3850ab4bdfdd1673777dc25248b500f20d847599913b9145f", + "size": [ + 119, + 170 + ] + }, + "plant/芭蕉/icon.png": { + "sha256": "b98968c8b9ed9530885b8b5b9f88b37ed3cd50c8d4d5ba80c5a79d1a470e6520", + "size": [ + 100, + 92 + ] + }, + "plant/花生/1.png": { + "sha256": "3396431e856ed6079f04f17678192767ced92c7bd3da74756c4a8e611ac7f8d7", + "size": [ + 55, + 86 + ] + }, + "plant/花生/2.png": { + "sha256": "924933797db6d9c2a8383284290d1c9cb380678ee12628b6cc85802a51b39ab6", + "size": [ + 74, + 112 + ] + }, + "plant/花生/3.png": { + "sha256": "e881d148a62c79e69e64bfcd1fb435fe4b6d5c2848c1facf19923f3ab2a15c8b", + "size": [ + 97, + 142 + ] + }, + "plant/花生/4.png": { + "sha256": "e443fd84beb6acb528e8112d40dfd907ed407ddc03b1d05ddddb7c440d964d09", + "size": [ + 100, + 141 + ] + }, + "plant/花生/5.png": { + "sha256": "7a8eccc6b558d2f8bfe9ca1e6e3585b262920f40ab8a4e2f07c9f6d622d96f64", + "size": [ + 112, + 154 + ] + }, + "plant/花生/icon.png": { + "sha256": "209782db4277b5fe74649b5903793ead7b7d15dd16a7ac1fe5c34e6916b3d2dc", + "size": [ + 96, + 100 + ] + }, + "plant/花菜/1.png": { + "sha256": "8af560b67bf18511011519606bfe7cf43fc25a6716edc9a7cdf6d8d4b44b4d29", + "size": [ + 22, + 67 + ] + }, + "plant/花菜/2.png": { + "sha256": "e910d20bbdd50bdd80383cb0a7d2fdaa6346d52373bce8eca5253d46696a891f", + "size": [ + 47, + 82 + ] + }, + "plant/花菜/3.png": { + "sha256": "2a6815eb90a55253d2729a8313d01d511f71969a6271d6ca55974cb2e3cf4088", + "size": [ + 78, + 108 + ] + }, + "plant/花菜/4.png": { + "sha256": "0789f207a19a6144b1b32e60bb2d33c4901792d27c1a7d81137a3349d61e1fc2", + "size": [ + 103, + 125 + ] + }, + "plant/花菜/5.png": { + "sha256": "1bcf80be3aeca323142277b7a3a3cc4600384e9c508d75dcd7609767ff927ea8", + "size": [ + 124, + 128 + ] + }, + "plant/花菜/icon.png": { + "sha256": "14f09974b3bd97262df037409ce645bdf4cf1d952393729a3c31474b4c817709", + "size": [ + 107, + 78 + ] + }, + "plant/芹菜/1.png": { + "sha256": "ce89731b1acb336204f5e6be67f1bd83ddb19fbe7b11c0b9f10a53c2f42ae1c4", + "size": [ + 21, + 69 + ] + }, + "plant/芹菜/2.png": { + "sha256": "9349654ca5a194ba634c2642561c58ef13713960062591ee0e1ff0f0d862a1c5", + "size": [ + 25, + 91 + ] + }, + "plant/芹菜/3.png": { + "sha256": "7613edaddce8c9ecd23fec87ff755cc87b7aa7f02d3f1920348089851009b521", + "size": [ + 46, + 113 + ] + }, + "plant/芹菜/4.png": { + "sha256": "a36164433a010e3df32bff1b80465a18be4671b94cfb4349ad33d09518e37caf", + "size": [ + 83, + 138 + ] + }, + "plant/芹菜/5.png": { + "sha256": "22eb46300650bab577002e22765e026d8e819dbcc5639d726f3c7459ffaa039e", + "size": [ + 121, + 162 + ] + }, + "plant/芹菜/icon.png": { + "sha256": "67d02f361300e5c35a857300246a6938f75327fc99796ff44da3aa0fa1693f5d", + "size": [ + 87, + 117 + ] + }, + "plant/苦瓜/1.png": { + "sha256": "1b867b253447c4017c088cb92f786a3bf69f04b30894c24e6ca036a8e595c0cf", + "size": [ + 40, + 120 + ] + }, + "plant/苦瓜/2.png": { + "sha256": "9d7a8dd73bb423b0ca16539562fc5acf91c8f4c63d413c962c5f69976c1eff62", + "size": [ + 132, + 120 + ] + }, + "plant/苦瓜/3.png": { + "sha256": "1c08fc26c01b608acd7066304bf12b29ecc1ede9f72aa297c1accd1b3f3c2173", + "size": [ + 135, + 125 + ] + }, + "plant/苦瓜/4.png": { + "sha256": "8b2ce7a3fa04bbd01c28c4049844f0b7b0e0798c3a3612564499608d3dbd3360", + "size": [ + 140, + 135 + ] + }, + "plant/苦瓜/5.png": { + "sha256": "0d565cc87eb81c53c91ee3a59ffc10104336e48950240048dcdc4ad815d432a8", + "size": [ + 140, + 136 + ] + }, + "plant/苦瓜/icon.png": { + "sha256": "7d27545e0e5fb484a07e11320d961a1adce233e4e79af0074a0075be05eb599b", + "size": [ + 121, + 113 + ] + }, + "plant/苹果/1.png": { + "sha256": "58489370ad633b70fb837d60182bb71fead19ac0d5d055b0b94848128d6ada09", + "size": [ + 26, + 104 + ] + }, + "plant/苹果/2.png": { + "sha256": "fa703b9efa2f95c7f22768d089c0ba0656aa0e5d040bc71e80de0b3e4fceebfb", + "size": [ + 58, + 99 + ] + }, + "plant/苹果/3.png": { + "sha256": "6181a433ba1100c1bc4efc4cce7c2ae4bd9e967fa7ce58000cf1e9348ec93035", + "size": [ + 128, + 154 + ] + }, + "plant/苹果/4.png": { + "sha256": "5210b120df524d38353ddc4b93fca03fcf7ad0b3e98ac8f609e91e50cf3978a3", + "size": [ + 128, + 154 + ] + }, + "plant/苹果/5.png": { + "sha256": "0a9b2c863dd1fd4f80f1568e5d809e6a97b2c8c0fdc9f7d8618f90556213d163", + "size": [ + 127, + 154 + ] + }, + "plant/苹果/icon.png": { + "sha256": "08205603d4cfbc1d48256c5a2454713f6b9ee19b7d2e02135b8fbedea3dd8bd2", + "size": [ + 81, + 84 + ] + }, + "plant/茄子/1.png": { + "sha256": "4cecea91a8855112274a9e5badc2ed4648395ee6ee25834027202ec884dd54e2", + "size": [ + 47, + 89 + ] + }, + "plant/茄子/2.png": { + "sha256": "b66daf3e5069b8688de1479fc9d33eda39141d18b6a478d48637620f236fe9da", + "size": [ + 87, + 109 + ] + }, + "plant/茄子/3.png": { + "sha256": "7442ba729f0d282109794491f2725fbb1cc9dd52134ea596a4910ddfcde01e4f", + "size": [ + 113, + 159 + ] + }, + "plant/茄子/4.png": { + "sha256": "54b2dd799b259cf486cd81b0590bb8dbfce2ca69ec367e69ad65dec776dce69a", + "size": [ + 113, + 160 + ] + }, + "plant/茄子/5.png": { + "sha256": "dfc35e0273b5855d488c7ffc550d04a1747d4b9053ef9e0d40508331a195a5f0", + "size": [ + 113, + 160 + ] + }, + "plant/茄子/icon.png": { + "sha256": "19024e5f6fa4531dd8d902e2516c9ee28838c1c05c609d1fce2a928b6b954995", + "size": [ + 71, + 114 + ] + }, + "plant/草莓/1.png": { + "sha256": "41e22d82821a2865861a1b033a1d6b3dd255fe1cfabb66dbee524ad1f856965f", + "size": [ + 32, + 81 + ] + }, + "plant/草莓/2.png": { + "sha256": "8b7b48aff4ebf32639d745f1cd6686a5b93d1e6a4186a3ec61d0b5d2fb2f5f9d", + "size": [ + 61, + 99 + ] + }, + "plant/草莓/3.png": { + "sha256": "0bad90f18f477e808d9c1a882c3df9f73342d0459dca3652b20ac6016c21b2ff", + "size": [ + 102, + 121 + ] + }, + "plant/草莓/4.png": { + "sha256": "b56e1cbe691afa90c0cba8d677faa55e0eb8f8f09caf882cce42e1d22701a57f", + "size": [ + 107, + 129 + ] + }, + "plant/草莓/5.png": { + "sha256": "7349d62a53c2bb81ecf737881b7e6e88849cd4d46869883be0329ab472087be8", + "size": [ + 112, + 139 + ] + }, + "plant/草莓/icon.png": { + "sha256": "944d31863cb42c60d4c966626ed22ef11aef830a59cc911cd3881415164c2c9d", + "size": [ + 68, + 85 + ] + }, + "plant/荔枝/1.png": { + "sha256": "ab9b782e02b4ca7986edd43806255d3d148ae37aab7464d8333448bdec03ae8f", + "size": [ + 49, + 108 + ] + }, + "plant/荔枝/2.png": { + "sha256": "269baba50df41791d84cfb31621f41b743fb3cdc0a0d993467d5c96c9c25a7e0", + "size": [ + 98, + 143 + ] + }, + "plant/荔枝/3.png": { + "sha256": "26589a2c99c4a98a0d26f90a0c7c2b318bb1a117648844508e6e25e80d5cb29c", + "size": [ + 119, + 163 + ] + }, + "plant/荔枝/4.png": { + "sha256": "c7db603fcf3957b5f1e4108f0082f33ad175e1a82e7e5b0ab9b198db49103d5a", + "size": [ + 123, + 163 + ] + }, + "plant/荔枝/5.png": { + "sha256": "cbf95c2fb840997fef4d2d3cdb6a8f29c548359707f0fc665a031d034ef692bc", + "size": [ + 123, + 168 + ] + }, + "plant/荔枝/icon.png": { + "sha256": "2aa93fd50fd3d04bf3711d831571502388943c8f0a3549bd623c78e1b435f8a4", + "size": [ + 96, + 94 + ] + }, + "plant/莲藕/1.png": { + "sha256": "a42bffcd5739905c108694800a2f9052ab598ef3142ceab32635c7d89fcca822", + "size": [ + 133, + 93 + ] + }, + "plant/莲藕/2.png": { + "sha256": "f1a8d16f644fc05f117ce260802f316c652500d9f184ca6f985de49aa576a533", + "size": [ + 133, + 94 + ] + }, + "plant/莲藕/3.png": { + "sha256": "d334a61e43e98dee4065cd2ae67aaf3f6ad916c0d79c576824c37e381fc035da", + "size": [ + 133, + 104 + ] + }, + "plant/莲藕/4.png": { + "sha256": "1c9dc807c9f1da6c5aaa852da5e2bbb756b55d3d1dd577f1e3dfa7d338bcc388", + "size": [ + 133, + 123 + ] + }, + "plant/莲藕/5.png": { + "sha256": "0994d5e20a46ea66ac8c797c0eb839200880bf9d5782e1e715683ed8aec647fb", + "size": [ + 133, + 124 + ] + }, + "plant/莲藕/icon.png": { + "sha256": "ea649264fa6a4f38130341d32ce5ee3012406bda3171be2650aa708d2bb7caa9", + "size": [ + 117, + 92 + ] + }, + "plant/莲雾/1.png": { + "sha256": "9431b91d919dd3d1437754974b794c916fcceb03b5951de2fa6bf34ffade136a", + "size": [ + 40, + 114 + ] + }, + "plant/莲雾/2.png": { + "sha256": "0b0f449a1250e128f788ef0b481a85ace5d344610405ddbaf03bbf8428e08f1a", + "size": [ + 98, + 130 + ] + }, + "plant/莲雾/3.png": { + "sha256": "3b956d1fad6b50c9240dbb4772806c035579995c7967fa9505932bdb400c29f2", + "size": [ + 117, + 161 + ] + }, + "plant/莲雾/4.png": { + "sha256": "22426916da13d9914b7f4f75ee929a4389a1f3bdf5d20e1074679220b1f3c11a", + "size": [ + 117, + 162 + ] + }, + "plant/莲雾/5.png": { + "sha256": "cd6f65f142652de62fb8527ed31349b1f0e4a7b350961f310dd9ccf5ef4b0e94", + "size": [ + 137, + 173 + ] + }, + "plant/莲雾/icon.png": { + "sha256": "00df393cf5a119b0f63af5450922fcc832f005096ad7462b0b9968e67d365060", + "size": [ + 110, + 115 + ] + }, + "plant/菠萝/1.png": { + "sha256": "095bc21487704ebff96be9dca7fec02109de462f7c1b3d16a1d1f0344431c8e5", + "size": [ + 34, + 78 + ] + }, + "plant/菠萝/2.png": { + "sha256": "251f18075ced808660aac387511f105fc3a6eaf36f55fb646e2146f7756a4e9c", + "size": [ + 89, + 103 + ] + }, + "plant/菠萝/3.png": { + "sha256": "8532c22701df600cafc33d46ce1aaae76098cd55f32b0ad7b269bac3750bf43d", + "size": [ + 134, + 134 + ] + }, + "plant/菠萝/4.png": { + "sha256": "d0c2ed6c65cd745cb6a3b6615eb60cf41001b3b2a1dc9d7707f44cf04f8ee11f", + "size": [ + 134, + 134 + ] + }, + "plant/菠萝/5.png": { + "sha256": "46f4d021e66b9ff16294971ee128f2f0583cf837fa8a04e5460ea9584c90b452", + "size": [ + 135, + 144 + ] + }, + "plant/菠萝/icon.png": { + "sha256": "bd917d4c1f654099c70c0d207d2f25b849dc8233bec2689e41058f59461fe354", + "size": [ + 52, + 98 + ] + }, + "plant/菠萝蜜/1.png": { + "sha256": "01e680897d54eb7760ac9d22fcc0885f39a3d6595816ee27e8468d87b70badc4", + "size": [ + 53, + 105 + ] + }, + "plant/菠萝蜜/2.png": { + "sha256": "bc7da7c643c85263cb276677aa495bbd447b77ee0b2928328cb23de27553356e", + "size": [ + 69, + 111 + ] + }, + "plant/菠萝蜜/3.png": { + "sha256": "4989ff8f7ce43ff6a59c9c43a369571a9ab01a6697dbf4cfd947ff2f123ed93d", + "size": [ + 104, + 137 + ] + }, + "plant/菠萝蜜/4.png": { + "sha256": "ee4520c2ed9cdb82517c5f25eb27c054c7dbc25adf7dc2ddced203645b373d8b", + "size": [ + 117, + 146 + ] + }, + "plant/菠萝蜜/5.png": { + "sha256": "201ba4fd4d32ba75791cd3afc1bdc5dcbb37dfa674fd7f76a9017967b1f9cb70", + "size": [ + 119, + 163 + ] + }, + "plant/菠萝蜜/icon.png": { + "sha256": "40bb7a2160585857fa2981b373f714ad28f5e26deaa47a5196d709a870bd4788", + "size": [ + 85, + 106 + ] + }, + "plant/葡萄/1.png": { + "sha256": "e05ea961bb08079022c11cdc0061567bd7d497952e259c8c9460331b57726715", + "size": [ + 47, + 108 + ] + }, + "plant/葡萄/2.png": { + "sha256": "0aa259ef712cb9a6c9c3aa876dd435d134702aa677c1d6a60d0d9692f8c8f3c3", + "size": [ + 63, + 135 + ] + }, + "plant/葡萄/3.png": { + "sha256": "ab6cad29787576494d550d2c22c4dd785d06c35dc8be4821dc7fc79bbbbe9f6d", + "size": [ + 112, + 167 + ] + }, + "plant/葡萄/4.png": { + "sha256": "3a68b89f6a2341337fd863c67cf77cdee17b5fd319d12bc867b86ad7ee3708b7", + "size": [ + 125, + 167 + ] + }, + "plant/葡萄/5.png": { + "sha256": "ad3b4a49da8a0c8ca9e9a22bdbbd29b29bda237d45bdc4ca85a0d342e7b128e7", + "size": [ + 121, + 164 + ] + }, + "plant/葡萄/icon.png": { + "sha256": "9f399f41d7edc4557f98173a18b52bf5bb08af54795c359a1ff93f0020d33c41", + "size": [ + 52, + 95 + ] + }, + "plant/葫芦/1.png": { + "sha256": "798cb9262f75503ca5afefdcd4c000a8722e4b9fb70246a4b457bef3c68d37fc", + "size": [ + 29, + 97 + ] + }, + "plant/葫芦/2.png": { + "sha256": "953d72d803c002de006564a2a56a3c05d5604644c7c946a0b94c814e000a04c7", + "size": [ + 67, + 115 + ] + }, + "plant/葫芦/3.png": { + "sha256": "5d78e1488b1ddb612700218f0c59eb6f5072c905215d5064f0d518b9df2d8721", + "size": [ + 102, + 141 + ] + }, + "plant/葫芦/4.png": { + "sha256": "e8170e24e1807ec40baf2f0dfd224a6bbce4505fffdd82d9538e45b7322987d6", + "size": [ + 113, + 158 + ] + }, + "plant/葫芦/5.png": { + "sha256": "809985462304c16907dc73c60d5e78be51bb214763f92bf899ed6d56a80e61b2", + "size": [ + 124, + 160 + ] + }, + "plant/葫芦/icon.png": { + "sha256": "0f14b06f23483b63133e2a238bdd7a5415db75fc3e9187b650cffb6d003a461c", + "size": [ + 72, + 102 + ] + }, + "plant/蓝莓/1.png": { + "sha256": "db60ee0cc1d84add18c184e2be8335f566f43b07bc33e6ee030af6676eb1cd61", + "size": [ + 37, + 93 + ] + }, + "plant/蓝莓/2.png": { + "sha256": "f955e71a0f0ef2bea245f9a9eff3dbf8b4d351018eb90b72f9235ae7ac007a03", + "size": [ + 59, + 100 + ] + }, + "plant/蓝莓/3.png": { + "sha256": "b61ce54acb086f0e879e218ccc1db52876506ee482bd2650a006054f54417eab", + "size": [ + 82, + 118 + ] + }, + "plant/蓝莓/4.png": { + "sha256": "984d082c1942607a3514841a00baa81f9e381bf359e2bbbf8d0fdabe39e3f50f", + "size": [ + 101, + 141 + ] + }, + "plant/蓝莓/5.png": { + "sha256": "6d2b91a09f7f7f084f25e4897d3487cb87b431b315391872cd6d20947a75b0b5", + "size": [ + 101, + 148 + ] + }, + "plant/蓝莓/icon.png": { + "sha256": "84e847a089565a529ca5d8a98f3624a70c8685dd81b5d13befa51659c419abcf", + "size": [ + 100, + 76 + ] + }, + "plant/薄荷/1.png": { + "sha256": "b3c640403fc3044c82a7b53deed4765c7deb566c33b4f194804f8b079096e99f", + "size": [ + 29, + 81 + ] + }, + "plant/薄荷/2.png": { + "sha256": "97f0efb30a0ca65b0536293e250848af0e2c1ddf16af51c103577f66d15578ac", + "size": [ + 60, + 130 + ] + }, + "plant/薄荷/3.png": { + "sha256": "675716bfaed63d2e6288a832ea8abe1ec239f9ef9f1abb55f0b520e43668af59", + "size": [ + 105, + 144 + ] + }, + "plant/薄荷/4.png": { + "sha256": "d04970084713fc4e795ca2beba6766aa167ab5cb175fb17304711c30c49af96a", + "size": [ + 114, + 155 + ] + }, + "plant/薄荷/5.png": { + "sha256": "ec1fd959ff6ad75c18e2d29da94a990691348ae67f1ffed3f0972b9cf4285a6e", + "size": [ + 129, + 166 + ] + }, + "plant/薄荷/icon.png": { + "sha256": "8f308a72fe8a928e0198a3d5e16c67359843f09e184be015607d5a3453f48b84", + "size": [ + 100, + 92 + ] + }, + "plant/蘑菇/1.png": { + "sha256": "f2bb06ef90351e4fb4f10efdcec33ef608ff0f2d173ddd7a707edc708302ed85", + "size": [ + 127, + 102 + ] + }, + "plant/蘑菇/2.png": { + "sha256": "388385d703e59ae9f1190a1ee6b1e74e1a628aba675b459a370fcb5c56d7eb02", + "size": [ + 127, + 97 + ] + }, + "plant/蘑菇/3.png": { + "sha256": "4bc6e6be409cbf6f1f0b6124a4263ad8689b6b32f6dbf133aaf43cdc48d0c44a", + "size": [ + 127, + 115 + ] + }, + "plant/蘑菇/4.png": { + "sha256": "a9006df5c6e663920657416da0a4928b3059f3d9e849e490266cbd1684902d80", + "size": [ + 127, + 121 + ] + }, + "plant/蘑菇/5.png": { + "sha256": "1aa4a1346b3d73e98165272f6ab9fd4a11c56ec85c50fb8188ad02b196fe74f9", + "size": [ + 127, + 121 + ] + }, + "plant/蘑菇/icon.png": { + "sha256": "34bf9bbcd9c989431852efc1eb7fddc50a0971afe481f463ff18a474a396f457", + "size": [ + 100, + 77 + ] + }, + "plant/蛋黄果/1.png": { + "sha256": "67b8e894d72adb0022f448223c0133e4437f83f9726f6f1cd52979c5ac01a573", + "size": [ + 47, + 94 + ] + }, + "plant/蛋黄果/2.png": { + "sha256": "3a1982519e8837f2d5d56941c3451907311655968ac01f15085e18f0fd216702", + "size": [ + 72, + 135 + ] + }, + "plant/蛋黄果/3.png": { + "sha256": "a4b75fe30365ff258a88604ad111c0810a188981d5f4abada5a9c076d80c492a", + "size": [ + 114, + 157 + ] + }, + "plant/蛋黄果/4.png": { + "sha256": "a95a9ca8b7cfae263af76c0315a6d47c3997273faf0e0bac2503363472ef6c63", + "size": [ + 120, + 161 + ] + }, + "plant/蛋黄果/5.png": { + "sha256": "da8ce79a68ad7452407a94203897da6cdcf845702325097b3a351a511381ad87", + "size": [ + 128, + 170 + ] + }, + "plant/蛋黄果/icon.png": { + "sha256": "711c9216c08ea14c123de56cf2c1229b66b2e61a4fc111bd9950af502ba27adb", + "size": [ + 130, + 118 + ] + }, + "plant/蜜思桃/1.png": { + "sha256": "95b415c1c8091082c25f7cd8260a11ba6849df5ce65902d55d7a17a4be2083f6", + "size": [ + 27, + 76 + ] + }, + "plant/蜜思桃/2.png": { + "sha256": "85eb9afcae07095dd3366ed6f1e3d0c64fbcc5fe09a7659e8d7dc9cc1b5a55d2", + "size": [ + 52, + 103 + ] + }, + "plant/蜜思桃/3.png": { + "sha256": "2cafd76d99c5f7e2930e538f186c3924ee35617d3539efd8a5b6783e8f76b478", + "size": [ + 78, + 132 + ] + }, + "plant/蜜思桃/4.png": { + "sha256": "2a34a2cc318e8494f07cf486b1448602478559d6216780f2e45191f51bcd7a40", + "size": [ + 112, + 162 + ] + }, + "plant/蜜思桃/5.png": { + "sha256": "6da6064944571b8842fba360ddec19f472938f9aa6504ce9dfb2628e5468f4b7", + "size": [ + 153, + 192 + ] + }, + "plant/蜜思桃/icon.png": { + "sha256": "023d21ce3726b4800c7489ac1344ca560060f5e137e87c1fe9aaad548be8d9e2", + "size": [ + 79, + 74 + ] + }, + "plant/西瓜/1.png": { + "sha256": "dc1504301ffd6d882aafe392838779efe4be07a8c2a935ac75368c5b70760637", + "size": [ + 62, + 101 + ] + }, + "plant/西瓜/2.png": { + "sha256": "6b518eca5d7360d82c863cd62bb277cf9e98b60d8c9cd71ecd8a1a31db650a95", + "size": [ + 119, + 158 + ] + }, + "plant/西瓜/3.png": { + "sha256": "b1db957942cf02dded0e8d436a1cb5ea8ddac30f431ae3982d5f08935186417d", + "size": [ + 129, + 139 + ] + }, + "plant/西瓜/4.png": { + "sha256": "d3dc0e03998fe57a4b07cde237c5beffe7056b506488c2ceef9c8221c1d62731", + "size": [ + 139, + 145 + ] + }, + "plant/西瓜/5.png": { + "sha256": "bd9002faa3cf03b6e6e43dbc1fca657a7365221d3bafe6abfcdcf9eeff3916f9", + "size": [ + 149, + 150 + ] + }, + "plant/西瓜/icon.png": { + "sha256": "565b1e9f2a759da7f298736360321a95a91931580f9a72b9af0c91186030f22c", + "size": [ + 94, + 101 + ] + }, + "plant/豌豆/1.png": { + "sha256": "463e53f03dabe321c89f38569b850adcbc79c6134ac83a079e339bec4ef9c917", + "size": [ + 35, + 109 + ] + }, + "plant/豌豆/2.png": { + "sha256": "5376878aa6176600b963150ed73fe72eb54eaa5ed8f5f25fb55fa3eb9fbea031", + "size": [ + 47, + 112 + ] + }, + "plant/豌豆/3.png": { + "sha256": "d615acb5bd4d4eebcc374dc10efc088c43d86b09582a58d68378c340059ab2b8", + "size": [ + 121, + 166 + ] + }, + "plant/豌豆/4.png": { + "sha256": "0fac42e995df75e4a36d600a36a6fba74b444522f2fcfe5a186331f4bc1a7f87", + "size": [ + 121, + 167 + ] + }, + "plant/豌豆/5.png": { + "sha256": "05ab191941e33e73b569978e2a44c2fb2273474e4321eaca16881e010341d44a", + "size": [ + 121, + 167 + ] + }, + "plant/豌豆/icon.png": { + "sha256": "7c26bda35e10a1ff1287c736b9eaf6bea56faf0cbf22e43b526adb6ab392fb14", + "size": [ + 58, + 95 + ] + }, + "plant/辣椒/1.png": { + "sha256": "cad6cf4f21be82b06e30f750b423d14e853fff2c34e05d224ecf7e684aba5c58", + "size": [ + 56, + 102 + ] + }, + "plant/辣椒/2.png": { + "sha256": "273b97ecead4aae60d04de144f0b82d24199284a4df494a40130c5edf54973e1", + "size": [ + 105, + 129 + ] + }, + "plant/辣椒/3.png": { + "sha256": "e1ff3074ac02ede871de0003972aac390abd6e9ea9ee226a618918eb8c48da67", + "size": [ + 115, + 161 + ] + }, + "plant/辣椒/4.png": { + "sha256": "99a54d6f5f7dbd3f39f99346a81a390654bb988cb074d8730697dc1409bb46c6", + "size": [ + 128, + 173 + ] + }, + "plant/辣椒/5.png": { + "sha256": "b13c5c25600e657f0eeeb255f365c540c97d52d3b80ae99f53312f0917435768", + "size": [ + 128, + 173 + ] + }, + "plant/辣椒/icon.png": { + "sha256": "a11c0b3b165b7ddaae6cffd7679bb7f0af747a02b9984f962193f4cb247ff6ed", + "size": [ + 60, + 92 + ] + }, + "plant/迎春花/1.png": { + "sha256": "e0149f31c9d867a370a6798bf0236fcd7f8628383b5ef0f2b144b20cc9d4eb6e", + "size": [ + 27, + 77 + ] + }, + "plant/迎春花/2.png": { + "sha256": "4c932cd6eeb59f3724d465f4e5edfc33e49de7aec364b4503f82a0030d78e31e", + "size": [ + 74, + 121 + ] + }, + "plant/迎春花/3.png": { + "sha256": "d85f5ea08431193d0e8d88c65ed36edfb7784f9d2f60b915e9c480365d16ac44", + "size": [ + 106, + 143 + ] + }, + "plant/迎春花/4.png": { + "sha256": "3f837da23e87f044faf4e1bfbc905f0ed9baefa1c79bda2c30a00ef63a90b8c4", + "size": [ + 124, + 157 + ] + }, + "plant/迎春花/5.png": { + "sha256": "f4a1df28db40b0d0edb415c34689a3ad9c276098ec0f941c579ac0a894242116", + "size": [ + 126, + 156 + ] + }, + "plant/迎春花/icon.png": { + "sha256": "d3716ccd92ba69849982cd4571be6987f663cd25364361d3134c2ecd098302d8", + "size": [ + 121, + 105 + ] + }, + "plant/金桔/1.png": { + "sha256": "0fce76a54e6b4c43171737eb2bcbabfc12c2185c195a644395ccc48705e7e8f6", + "size": [ + 52, + 100 + ] + }, + "plant/金桔/2.png": { + "sha256": "1758c9145210ad1e853a2879bb3face9190c994c726d61a0f1e7f2e14fa1bd14", + "size": [ + 74, + 123 + ] + }, + "plant/金桔/3.png": { + "sha256": "0c944112f99244ade5b6398e6ff3398257a3b2c25cf23dd3c4a0c4c129fd5dbf", + "size": [ + 79, + 158 + ] + }, + "plant/金桔/4.png": { + "sha256": "291db6a7e1227629c72a5953162cffac7cad834103d63e3532078470239edfb0", + "size": [ + 85, + 163 + ] + }, + "plant/金桔/5.png": { + "sha256": "bd3eab848d57122e153daf8975786c98f77c5b05339bad5117c15b6f0d82632b", + "size": [ + 89, + 167 + ] + }, + "plant/金桔/icon.png": { + "sha256": "82a1ce985cd3610d4491f3866fac3a287932bcf48915984e58e7f1895ac07989", + "size": [ + 101, + 75 + ] + }, + "plant/金针菇/1.png": { + "sha256": "1efed26ac1425139ffa3d98d11f611bac4477b8a79e92cc853b858775051062d", + "size": [ + 105, + 81 + ] + }, + "plant/金针菇/2.png": { + "sha256": "e566734bc86cca91b6a88f4ee2ce46327433a1a8475609b5ca6f11b08b64c44c", + "size": [ + 105, + 87 + ] + }, + "plant/金针菇/3.png": { + "sha256": "c5efbe64b713192e8c73b874797296e2450325e4e5fc4aa7c175175eaa212030", + "size": [ + 105, + 109 + ] + }, + "plant/金针菇/4.png": { + "sha256": "c6ad828cc46f5587c62027bed8ab544a0ef27b133153eda4668da732811fac4f", + "size": [ + 105, + 119 + ] + }, + "plant/金针菇/5.png": { + "sha256": "d38c274278c5cbadacddaf75d4b1da6c51f9757d611291352c4d56b33b8aecc6", + "size": [ + 106, + 146 + ] + }, + "plant/金针菇/icon.png": { + "sha256": "b041fa88ca183a60fd86041bf4f496c05ac939a878c1b6a19609aa781b7f7a9a", + "size": [ + 98, + 96 + ] + }, + "plant/韭菜/1.png": { + "sha256": "6484bed7cc56f4f063e43df193c227524ad39e2b4c231c5bd31f911e410f09d5", + "size": [ + 29, + 80 + ] + }, + "plant/韭菜/2.png": { + "sha256": "e561364959b91bc9a2ca12f74da149e303110b707862583d380c475e901c66dc", + "size": [ + 37, + 115 + ] + }, + "plant/韭菜/3.png": { + "sha256": "c78ffd20a16d626ef2adac8d593efe3f52cb6accae03a76a525ed0d35f501f94", + "size": [ + 74, + 143 + ] + }, + "plant/韭菜/4.png": { + "sha256": "98b83721285dbb372956bb1f80f579ec051cf80755173d300dc0c43eff667e45", + "size": [ + 104, + 167 + ] + }, + "plant/韭菜/5.png": { + "sha256": "1a7874a3d67e31cb3313aa7e0fcd2ff92e999ee52ce919a7f6b9b81717f6634e", + "size": [ + 136, + 171 + ] + }, + "plant/韭菜/icon.png": { + "sha256": "09ec856ee984c8f99b0ddb2c3e1a26f77cebb3213e8fe9ae2434970bce0fb8ca", + "size": [ + 119, + 127 + ] + }, + "plant/香瓜/1.png": { + "sha256": "e9a0f7426ea94c97464bb5fd90b5473373f6657a705dd426898dcb358dfb4f09", + "size": [ + 39, + 93 + ] + }, + "plant/香瓜/2.png": { + "sha256": "797b3798001f6b6bc7b597ebfed40642a2eec38c431cb821c2ea9c9cf19a4f8d", + "size": [ + 55, + 116 + ] + }, + "plant/香瓜/3.png": { + "sha256": "bc2d5b0eb47ce0525c52e28ef95ff81a285e9be8702cb89c91e9d31a76f33e70", + "size": [ + 135, + 129 + ] + }, + "plant/香瓜/4.png": { + "sha256": "18c616bf3235796e4d3dfa3fef58ecb7247b708bb99970de2c0f927ce4647b1d", + "size": [ + 135, + 129 + ] + }, + "plant/香瓜/5.png": { + "sha256": "160c9d7f91c2ddf4d734fcd5d8f16fefb83918e982f126b70ce0b859a20208bc", + "size": [ + 135, + 129 + ] + }, + "plant/香瓜/icon.png": { + "sha256": "7f2054256e9c745f1ba3c1feaffe0e660eb3b7851a72477cabc0401f89021416", + "size": [ + 120, + 94 + ] + }, + "plant/香粽龙舟/1.png": { + "sha256": "793cf8100c4ae38f23a9a49446f3eb5a06ae5fa3238653cd4f36ec54486850f5", + "size": [ + 140, + 89 + ] + }, + "plant/香粽龙舟/2.png": { + "sha256": "54242b5a4c6bf02a6a12656a9daae3ebcb8e7dae2dea38e2338df2eded747193", + "size": [ + 140, + 91 + ] + }, + "plant/香粽龙舟/3.png": { + "sha256": "0bf9282bf472fcef60211b725fe355fccd1ff93532961abf8b9399e8ebe34897", + "size": [ + 140, + 109 + ] + }, + "plant/香粽龙舟/4.png": { + "sha256": "31aecd0505949f08abfc76f409d1ae7e6fed97a85c3c5e43362bc9ec0a391ff8", + "size": [ + 140, + 137 + ] + }, + "plant/香粽龙舟/5.png": { + "sha256": "c7ac99320e206eb7e3d4af5e2ef4462bb3dc8cd251b8920825acfd9c71fac54c", + "size": [ + 152, + 156 + ] + }, + "plant/香粽龙舟/icon.png": { + "sha256": "3f6004e1e6c217d47caf8d7af37d1bc507088501613f3847270539e267db6a00", + "size": [ + 114, + 117 + ] + }, + "plant/香蕉/1.png": { + "sha256": "26113d5aea6bdd7a8683facc6d59c6e84fe003e910239968a776ccff807636f4", + "size": [ + 46, + 96 + ] + }, + "plant/香蕉/2.png": { + "sha256": "678283113bd1726485a08d3468d029669dc79784d054dba37998e5d63fc54761", + "size": [ + 85, + 61 + ] + }, + "plant/香蕉/3.png": { + "sha256": "29252b848974569d837d344f802749001359bcafbf4f67c5a837f24cbf1c3a4c", + "size": [ + 110, + 88 + ] + }, + "plant/香蕉/4.png": { + "sha256": "79ea86f7b89b758b0c7f3d57efec071f89a439a177af7b29a206b06a62ce0e38", + "size": [ + 112, + 109 + ] + }, + "plant/香蕉/5.png": { + "sha256": "f3d01f1eadacdf5d6686dc591377db8c28ef59872f8961bfce568ebd3f679b0f", + "size": [ + 125, + 122 + ] + }, + "plant/香蕉/icon.png": { + "sha256": "d1178061c5c8ff223bf1f39f240fae211567f623379f78b46329515c811f6bab", + "size": [ + 111, + 87 + ] + }, + "plant/魔法神花/1.png": { + "sha256": "bf44267bd05bc2998321c53891d14cc1e94d4434a6af0cb4fce114aa5fb7b5d9", + "size": [ + 31, + 89 + ] + }, + "plant/魔法神花/2.png": { + "sha256": "6b45f47e76bf971da657093b1a1f56dddfeb194d59b03503d285a1468c7aa878", + "size": [ + 59, + 98 + ] + }, + "plant/魔法神花/3.png": { + "sha256": "1f26b8348d48a8ffc4244000a74124205ca50bd581bd08231db5c101ed356fe4", + "size": [ + 93, + 155 + ] + }, + "plant/魔法神花/4.png": { + "sha256": "16036cb12a04bfd29e42b3a5479d8f8e14ac34a786dd12c25a400409349398b3", + "size": [ + 93, + 160 + ] + }, + "plant/魔法神花/5.png": { + "sha256": "7447674dd8efe1702ff39be9264f5e5ef6483a417c831124d1680500022e9736", + "size": [ + 115, + 186 + ] + }, + "plant/魔法神花/icon.png": { + "sha256": "617a0fe04f538f41e8eba451c2c10dfb870185e4dcc50b76610c4b003a43f8e7", + "size": [ + 121, + 122 + ] + }, + "plant/鳄梨/1.png": { + "sha256": "1a7b47812af0c21ea1f53efc98650c66b880c7a18311e0832680620ac2f533ae", + "size": [ + 67, + 112 + ] + }, + "plant/鳄梨/2.png": { + "sha256": "6768e030a436831e80ef12d9a8ae52fa9a90fc17954ddd136d0ccb5b6d089b86", + "size": [ + 102, + 137 + ] + }, + "plant/鳄梨/3.png": { + "sha256": "914ba6a762a18c68cf650ac9e86539be73ff83d48e9b553b2deb8d9a93db9342", + "size": [ + 137, + 158 + ] + }, + "plant/鳄梨/4.png": { + "sha256": "7ef94de28e332b8e520c1a34e97d5e0a638a12d1b31ec50239e551c7bcff5689", + "size": [ + 137, + 159 + ] + }, + "plant/鳄梨/5.png": { + "sha256": "0ad96b0ebdc828ecd946fd36c5e230bb631538dddb165ffdd7595cd74e8676f0", + "size": [ + 135, + 162 + ] + }, + "plant/鳄梨/icon.png": { + "sha256": "97e6ecca16bb17dc8f7f6563fa8dd1e3dcf18f7f69282b9bd998b0899d35c1bb", + "size": [ + 105, + 115 + ] + }, + "plant/鸾风玉/1.png": { + "sha256": "78e0d517e688d84f7a5802ba141098c6d9213d76b505cc590c51867c52fb6e90", + "size": [ + 17, + 80 + ] + }, + "plant/鸾风玉/2.png": { + "sha256": "6d0e5821868427e2a02b6727c2cbb6d6d4ff55026026c09cd3dc1341e7bd321f", + "size": [ + 22, + 87 + ] + }, + "plant/鸾风玉/3.png": { + "sha256": "f63e678de50abd562bad26af022e05b69040ba47d69a2bad146e9ef78de6ee2f", + "size": [ + 40, + 99 + ] + }, + "plant/鸾风玉/4.png": { + "sha256": "82e0661e13984347cc814696e9987779d6bbf30e4d2777d6517525b0edfa859a", + "size": [ + 47, + 133 + ] + }, + "plant/鸾风玉/5.png": { + "sha256": "609e48fb623d3fd15ad9534aa3aa26ac6ecf9320a85a966a0458a20149bfb8c9", + "size": [ + 59, + 163 + ] + }, + "plant/鸾风玉/icon.png": { + "sha256": "f31e3df709911938f57e1fcc945cffca9672bdecfe2c414abd4a0efda0f1b585", + "size": [ + 79, + 104 + ] + }, + "plant/黄瓜/1.png": { + "sha256": "1723dadabbf560369da0e36135481a03c8b841f3eaf5b778b0834d865080a3a7", + "size": [ + 47, + 116 + ] + }, + "plant/黄瓜/2.png": { + "sha256": "ee7895a1fc9821ef501d65130238e61ac4eba744d5b6e8e44515eb20f8484f0c", + "size": [ + 109, + 162 + ] + }, + "plant/黄瓜/3.png": { + "sha256": "4d94e5c0c41db9438e56df7a6cdfcd6744e67c121ec4b8bb0e378f11e3275a12", + "size": [ + 112, + 162 + ] + }, + "plant/黄瓜/4.png": { + "sha256": "e92042585b90cc6f914872106eff5ca50bd8ba0ac5a667a23b0697130ce8fd98", + "size": [ + 118, + 162 + ] + }, + "plant/黄瓜/5.png": { + "sha256": "de0bd5d49d6041cd020890c96ffda6a42f843d11779026643d0052c33519d993", + "size": [ + 118, + 162 + ] + }, + "plant/黄瓜/icon.png": { + "sha256": "c6fa3643a120a6fe8c27612b5c47148f7113220b3b38c638f94bacd372858376", + "size": [ + 96, + 100 + ] + }, + "plant/黄豆/1.png": { + "sha256": "ff6415449012472d8e6b3bec8bc6f715eef0e54b5a01fb1b5f0d85df831a4fdd", + "size": [ + 26, + 82 + ] + }, + "plant/黄豆/2.png": { + "sha256": "70cc8d13d023c0568f2fc808b459efc6415c7e5cd2bdf128c46c5feb7cdaebcd", + "size": [ + 42, + 105 + ] + }, + "plant/黄豆/3.png": { + "sha256": "2d9424f17e0c05a8453faee7b193d2ce53b5a2c81cfacc01987fd5487c02dcd5", + "size": [ + 79, + 124 + ] + }, + "plant/黄豆/4.png": { + "sha256": "7b2869bfbad9361e529cbb7f6355e2f56633c98b683f0c75fe5a2975905daffa", + "size": [ + 101, + 133 + ] + }, + "plant/黄豆/5.png": { + "sha256": "62fa2ff226d544c5161d3c0e89a7a2cf56dd17c6178df6816765a9fc44d97cb0", + "size": [ + 113, + 152 + ] + }, + "plant/黄豆/icon.png": { + "sha256": "7069a8142199e179732b3bbeeec5a0cb695e9ae60e2ef03fd525293303026516", + "size": [ + 103, + 117 + ] + }, + "plant/黄金果/1.png": { + "sha256": "8e0cf31997462d93f09657dab30d73f51cd8ee4b89135a074ff1574aa073d55b", + "size": [ + 20, + 78 + ] + }, + "plant/黄金果/2.png": { + "sha256": "9e718a303342baef2b4e1ab5d96d8448d8eb088d6834876d3dc3afe0e7c5515e", + "size": [ + 58, + 121 + ] + }, + "plant/黄金果/3.png": { + "sha256": "534bb23664b33a8cd1b16225ae883cb1936a850a404de251be5cbde0b833f070", + "size": [ + 118, + 151 + ] + }, + "plant/黄金果/4.png": { + "sha256": "01014d13fb7c3f73842aa71df861f8dc5207e64f33e80182f7928b60b3c5dec5", + "size": [ + 131, + 164 + ] + }, + "plant/黄金果/5.png": { + "sha256": "3b3167c728b87a47f9ee0fe203f83ed4cbe2757bf50579c26979181c5405daa5", + "size": [ + 139, + 167 + ] + }, + "plant/黄金果/icon.png": { + "sha256": "4945db2c8b11bad1048b3b01958d9ca7aaed72e35d1fb1da0fc2018553bf76f1", + "size": [ + 99, + 105 + ] + }, + "plant/黑布仑/1.png": { + "sha256": "ccf4770a84d99074922b0b968ecf4abb0a842a6716c8bc25983897c88a3ac58c", + "size": [ + 27, + 96 + ] + }, + "plant/黑布仑/2.png": { + "sha256": "e2b55bcf9a6719cff7079128da38f2d063b1fe0ed398486319b0ebde71d239f3", + "size": [ + 85, + 129 + ] + }, + "plant/黑布仑/3.png": { + "sha256": "257d1e1786b53c93f85dbd18e03fd3a4c4e19dc8583767fd0b15f1d511165814", + "size": [ + 126, + 144 + ] + }, + "plant/黑布仑/4.png": { + "sha256": "962bf983d96eaff024fdabe097fb1dca7f95065a5f949c354a174a4179c60c2a", + "size": [ + 154, + 158 + ] + }, + "plant/黑布仑/5.png": { + "sha256": "aad1daadc5775a7cef642fea6904c7dfee5e0dd859fa378571f87e3fe6494859", + "size": [ + 159, + 168 + ] + }, + "plant/黑布仑/icon.png": { + "sha256": "e37560c9702a3791acb2855fbe6fa9f1df8ebfb59606734218db31fdedb2e590", + "size": [ + 97, + 101 + ] + }, + "soil/普通土地.png": { + "sha256": "3df323044913fe5fd1a3aa5f4b35e244c78ef68b4357a88068a5390556ce582e", + "size": [ + 180, + 120 + ] + }, + "soil/草土地.png": { + "sha256": "7c88025b4018a1899acb519f51540100b8f8983a573c1698f5cc0556be6f6003", + "size": [ + 180, + 120 + ] + } + }, + "plants": { + "ATTomato": { + "general": 1, + "offsetH": 0, + "offsetW": 0, + "offsetX": 0, + "offsetY": 0, + "phase": "7200,18000,32400,46800,61200" + }, + "一星丁香水仙": { + "general": 1, + "offsetH": 0, + "offsetW": 0, + "offsetX": 0, + "offsetY": 0, + "phase": "14400,28800,46800,64800,86400" + }, + "一星书生捧墨": { + "general": 1, + "offsetH": 0, + "offsetW": 0, + "offsetX": 0, + "offsetY": 0, + "phase": "3600,10800,21600,32400,43200" + }, + "一星五彩椒": { + "general": 1, + "offsetH": 0, + "offsetW": 0, + "offsetX": 0, + "offsetY": 0, + "phase": "14400,28800,46800,64800,86400" + }, + "一星什样锦牡丹": { + "general": 1, + "offsetH": 0, + "offsetW": 0, + "offsetX": 0, + "offsetY": 0, + "phase": "7200,14400,25200,36000,46800" + }, + "一星卷丹百合": { + "general": 1, + "offsetH": 0, + "offsetW": 0, + "offsetX": 0, + "offsetY": 0, + "phase": "7200,14400,25200,36000,46800" + }, + "一星喇叭水仙": { + "general": 1, + "offsetH": 0, + "offsetW": 0, + "offsetX": 0, + "offsetY": 0, + "phase": "7200,14400,25200,36000,46800" + }, + "一星围裙水仙": { + "general": 1, + "offsetH": 0, + "offsetW": 0, + "offsetX": 0, + "offsetY": 0, + "phase": "14400,28800,46800,64800,86400" + }, + "一星巨峰葡萄": { + "general": 1, + "offsetH": 0, + "offsetW": 0, + "offsetX": 0, + "offsetY": 0, + "phase": "3600,10800,21600,32400,43200" + }, + "一星白玉霓": { + "general": 1, + "offsetH": 0, + "offsetW": 0, + "offsetX": 0, + "offsetY": 0, + "phase": "10800,21600,32400,46800,68400" + }, + "一星百叶蔷薇": { + "general": 1, + "offsetH": 0, + "offsetW": 0, + "offsetX": 0, + "offsetY": 0, + "phase": "3600,10800,21600,32400,43200" + }, + "一星秦椒": { + "general": 1, + "offsetH": 0, + "offsetW": 0, + "offsetX": 0, + "offsetY": 0, + "phase": "14400,28800,46800,64800,86400" + }, + "一星红富士": { + "general": 1, + "offsetH": 0, + "offsetW": 0, + "offsetX": 0, + "offsetY": 0, + "phase": "7200,14400,25200,36000,46800" + }, + "一星绿绒蒿": { + "general": 1, + "offsetH": 0, + "offsetW": 0, + "offsetX": 0, + "offsetY": 0, + "phase": "14400,28800,46800,64800,86400" + }, + "一星荷花蔷薇": { + "general": 1, + "offsetH": 0, + "offsetW": 0, + "offsetX": 0, + "offsetY": 0, + "phase": "3600,10800,21600,32400,43200" + }, + "一星葛巾紫": { + "general": 1, + "offsetH": 0, + "offsetW": 0, + "offsetX": 0, + "offsetY": 0, + "phase": "14400,28800,46800,64800,86400" + }, + "一星香水百合": { + "general": 1, + "offsetH": 0, + "offsetW": 0, + "offsetX": 0, + "offsetY": 0, + "phase": "14400,28800,46800,64800,86400" + }, + "一点红": { + "general": 1, + "offsetH": 0, + "offsetW": 0, + "offsetX": 0, + "offsetY": 0, + "phase": "32400,64800,97200,129600,162000" + }, + "丁香水仙": { + "general": 1, + "offsetH": 0, + "offsetW": 0, + "offsetX": 0, + "offsetY": 0, + "phase": "14400,28800,46800,64800,86400" + }, + "三七": { + "general": 1, + "offsetH": 0, + "offsetW": 0, + "offsetX": 0, + "offsetY": 0, + "phase": "32400,64800,97200,133200,169200" + }, + "三星丁香水仙": { + "general": 1, + "offsetH": 0, + "offsetW": 0, + "offsetX": 0, + "offsetY": 0, + "phase": "14400,28800,46800,64800,86400" + }, + "三星书生捧墨": { + "general": 1, + "offsetH": 0, + "offsetW": 0, + "offsetX": 0, + "offsetY": 0, + "phase": "3600,10800,21600,32400,43200" + }, + "三星五彩椒": { + "general": 1, + "offsetH": 0, + "offsetW": 0, + "offsetX": 0, + "offsetY": 0, + "phase": "14400,28800,46800,64800,86400" + }, + "三星什样锦牡丹": { + "general": 1, + "offsetH": 0, + "offsetW": 0, + "offsetX": 0, + "offsetY": 0, + "phase": "7200,14400,25200,36000,46800" + }, + "三星卷丹百合": { + "general": 1, + "offsetH": 0, + "offsetW": 0, + "offsetX": 0, + "offsetY": 0, + "phase": "7200,14400,25200,36000,46800" + }, + "三星喇叭水仙": { + "general": 1, + "offsetH": 0, + "offsetW": 0, + "offsetX": 0, + "offsetY": 0, + "phase": "7200,14400,25200,36000,46800" + }, + "三星围裙水仙": { + "general": 1, + "offsetH": 0, + "offsetW": 0, + "offsetX": 0, + "offsetY": 0, + "phase": "14400,28800,46800,64800,86400" + }, + "三星巨峰葡萄": { + "general": 1, + "offsetH": 0, + "offsetW": 0, + "offsetX": 0, + "offsetY": 0, + "phase": "3600,10800,21600,32400,43200" + }, + "三星果藤": { + "general": 1, + "offsetH": 0, + "offsetW": 0, + "offsetX": 0, + "offsetY": 0, + "phase": "10800,21600,32400,46800,68400" + }, + "三星白玉霓": { + "general": 1, + "offsetH": 0, + "offsetW": 0, + "offsetX": 0, + "offsetY": 0, + "phase": "10800,21600,32400,46800,68400" + }, + "三星百叶蔷薇": { + "general": 1, + "offsetH": 0, + "offsetW": 0, + "offsetX": 0, + "offsetY": 0, + "phase": "3600,10800,21600,32400,43200" + }, + "三星秦椒": { + "general": 1, + "offsetH": 0, + "offsetW": 0, + "offsetX": 0, + "offsetY": 0, + "phase": "14400,28800,46800,64800,86400" + }, + "三星红富士": { + "general": 1, + "offsetH": 0, + "offsetW": 0, + "offsetX": 0, + "offsetY": 0, + "phase": "7200,14400,25200,36000,46800" + }, + "三星绿绒蒿": { + "general": 1, + "offsetH": 0, + "offsetW": 0, + "offsetX": 0, + "offsetY": 0, + "phase": "14400,28800,46800,64800,86400" + }, + "三星荷花蔷薇": { + "general": 1, + "offsetH": 0, + "offsetW": 0, + "offsetX": 0, + "offsetY": 0, + "phase": "3600,10800,21600,32400,43200" + }, + "三星葛巾紫": { + "general": 1, + "offsetH": 0, + "offsetW": 0, + "offsetX": 0, + "offsetY": 0, + "phase": "14400,28800,46800,64800,86400" + }, + "三星香水百合": { + "general": 1, + "offsetH": 0, + "offsetW": 0, + "offsetX": 0, + "offsetY": 0, + "phase": "14400,28800,46800,64800,86400" + }, + "丝瓜": { + "general": 1, + "offsetH": 0, + "offsetW": 0, + "offsetX": 0, + "offsetY": 0, + "phase": "18000,39600,61200,86400,111600" + }, + "乌饭子": { + "general": 1, + "offsetH": 0, + "offsetW": 0, + "offsetX": 0, + "offsetY": 0, + "phase": "21600,39600,61200,82800,104400" + }, + "二星丁香水仙": { + "general": 1, + "offsetH": 0, + "offsetW": 0, + "offsetX": 0, + "offsetY": 0, + "phase": "14400,28800,46800,64800,86400" + }, + "二星书生捧墨": { + "general": 1, + "offsetH": 0, + "offsetW": 0, + "offsetX": 0, + "offsetY": 0, + "phase": "3600,10800,21600,32400,43200" + }, + "二星五彩椒": { + "general": 1, + "offsetH": 0, + "offsetW": 0, + "offsetX": 0, + "offsetY": 0, + "phase": "14400,28800,46800,64800,86400" + }, + "二星什样锦牡丹": { + "general": 1, + "offsetH": 0, + "offsetW": 0, + "offsetX": 0, + "offsetY": 0, + "phase": "7200,14400,25200,36000,46800" + }, + "二星卷丹百合": { + "general": 1, + "offsetH": 0, + "offsetW": 0, + "offsetX": 0, + "offsetY": 0, + "phase": "7200,14400,25200,36000,46800" + }, + "二星喇叭水仙": { + "general": 1, + "offsetH": 0, + "offsetW": 0, + "offsetX": 0, + "offsetY": 0, + "phase": "7200,14400,25200,36000,46800" + }, + "二星围裙水仙": { + "general": 1, + "offsetH": 0, + "offsetW": 0, + "offsetX": 0, + "offsetY": 0, + "phase": "14400,28800,46800,64800,86400" + }, + "二星巨峰葡萄": { + "general": 1, + "offsetH": 0, + "offsetW": 0, + "offsetX": 0, + "offsetY": 0, + "phase": "3600,10800,21600,32400,43200" + }, + "二星白玉霓": { + "general": 1, + "offsetH": 0, + "offsetW": 0, + "offsetX": 0, + "offsetY": 0, + "phase": "10800,21600,32400,46800,68400" + }, + "二星百叶蔷薇": { + "general": 1, + "offsetH": 0, + "offsetW": 0, + "offsetX": 0, + "offsetY": 0, + "phase": "3600,10800,21600,32400,43200" + }, + "二星秦椒": { + "general": 1, + "offsetH": 0, + "offsetW": 0, + "offsetX": 0, + "offsetY": 0, + "phase": "14400,28800,46800,64800,86400" + }, + "二星红富士": { + "general": 1, + "offsetH": 0, + "offsetW": 0, + "offsetX": 0, + "offsetY": 0, + "phase": "7200,14400,25200,36000,46800" + }, + "二星绿绒蒿": { + "general": 1, + "offsetH": 0, + "offsetW": 0, + "offsetX": 0, + "offsetY": 0, + "phase": "14400,28800,46800,64800,86400" + }, + "二星荷花蔷薇": { + "general": 1, + "offsetH": 0, + "offsetW": 0, + "offsetX": 0, + "offsetY": 0, + "phase": "3600,10800,21600,32400,43200" + }, + "二星葛巾紫": { + "general": 1, + "offsetH": 0, + "offsetW": 0, + "offsetX": 0, + "offsetY": 0, + "phase": "14400,28800,46800,64800,86400" + }, + "二星香水百合": { + "general": 1, + "offsetH": 0, + "offsetW": 0, + "offsetX": 0, + "offsetY": 0, + "phase": "14400,28800,46800,64800,86400" + }, + "五彩椒": { + "general": 1, + "offsetH": 0, + "offsetW": 0, + "offsetX": 0, + "offsetY": 0, + "phase": "14400,28800,46800,64800,86400" + }, + "五星丁香水仙": { + "general": 1, + "offsetH": 0, + "offsetW": 0, + "offsetX": 0, + "offsetY": 0, + "phase": "14400,28800,46800,64800,86400" + }, + "五星书生捧墨": { + "general": 1, + "offsetH": 0, + "offsetW": 0, + "offsetX": 0, + "offsetY": 0, + "phase": "3600,10800,21600,32400,43200" + }, + "五星五彩椒": { + "general": 1, + "offsetH": 0, + "offsetW": 0, + "offsetX": 0, + "offsetY": 0, + "phase": "14400,28800,46800,64800,86400" + }, + "五星什样锦牡丹": { + "general": 1, + "offsetH": 0, + "offsetW": 0, + "offsetX": 0, + "offsetY": 0, + "phase": "7200,14400,25200,36000,46800" + }, + "五星卷丹百合": { + "general": 1, + "offsetH": 0, + "offsetW": 0, + "offsetX": 0, + "offsetY": 0, + "phase": "7200,14400,25200,36000,46800" + }, + "五星喇叭水仙": { + "general": 1, + "offsetH": 0, + "offsetW": 0, + "offsetX": 0, + "offsetY": 0, + "phase": "7200,14400,25200,36000,46800" + }, + "五星围裙水仙": { + "general": 1, + "offsetH": 0, + "offsetW": 0, + "offsetX": 0, + "offsetY": 0, + "phase": "14400,28800,46800,64800,86400" + }, + "五星巨峰葡萄": { + "general": 1, + "offsetH": 0, + "offsetW": 0, + "offsetX": 0, + "offsetY": 0, + "phase": "3600,10800,21600,32400,43200" + }, + "五星潜水镜": { + "general": 1, + "offsetH": 0, + "offsetW": 0, + "offsetX": 0, + "offsetY": 0, + "phase": "21600,43200,64800,90000,115200" + }, + "五星白玉霓": { + "general": 1, + "offsetH": 0, + "offsetW": 0, + "offsetX": 0, + "offsetY": 0, + "phase": "10800,21600,32400,46800,68400" + }, + "五星百叶蔷薇": { + "general": 1, + "offsetH": 0, + "offsetW": 0, + "offsetX": 0, + "offsetY": 0, + "phase": "3600,10800,21600,32400,43200" + }, + "五星秦椒": { + "general": 1, + "offsetH": 0, + "offsetW": 0, + "offsetX": 0, + "offsetY": 0, + "phase": "14400,28800,46800,64800,86400" + }, + "五星红富士": { + "general": 1, + "offsetH": 0, + "offsetW": 0, + "offsetX": 0, + "offsetY": 0, + "phase": "7200,14400,25200,36000,46800" + }, + "五星绿绒蒿": { + "general": 1, + "offsetH": 0, + "offsetW": 0, + "offsetX": 0, + "offsetY": 0, + "phase": "14400,28800,46800,64800,86400" + }, + "五星荷花蔷薇": { + "general": 1, + "offsetH": 0, + "offsetW": 0, + "offsetX": 0, + "offsetY": 0, + "phase": "3600,10800,21600,32400,43200" + }, + "五星葛巾紫": { + "general": 1, + "offsetH": 0, + "offsetW": 0, + "offsetX": 0, + "offsetY": 0, + "phase": "14400,28800,46800,64800,86400" + }, + "五星香水百合": { + "general": 1, + "offsetH": 0, + "offsetW": 0, + "offsetX": 0, + "offsetY": 0, + "phase": "14400,28800,46800,64800,86400" + }, + "兔儿伞": { + "general": 1, + "offsetH": 0, + "offsetW": 0, + "offsetX": 0, + "offsetY": 0, + "phase": "32400,64800,100800,133200,165600" + }, + "关白附子": { + "general": 1, + "offsetH": 0, + "offsetW": 0, + "offsetX": 0, + "offsetY": 0, + "phase": "32400,64800,97200,129600,165600" + }, + "关苍术": { + "general": 1, + "offsetH": 0, + "offsetW": 0, + "offsetX": 0, + "offsetY": 0, + "phase": "32400,64800,100800,133200,165600" + }, + "冬凌草": { + "general": 1, + "offsetH": 0, + "offsetW": 0, + "offsetX": 0, + "offsetY": 0, + "phase": "36000,68400,100800,133200,165600" + }, + "冬瓜": { + "general": 1, + "offsetH": 0, + "offsetW": 0, + "offsetX": 0, + "offsetY": 0, + "phase": "18000,39600,61200,86400,111600" + }, + "决明子": { + "general": 1, + "offsetH": 0, + "offsetW": 0, + "offsetX": 0, + "offsetY": 0, + "phase": "32400,64800,97200,129600,162000" + }, + "南瓜": { + "general": 1, + "offsetH": 0, + "offsetW": 0, + "offsetX": 0, + "offsetY": 0, + "phase": "10800,25200,43200,61200,79200" + }, + "双玉兰": { + "general": 1, + "offsetH": 0, + "offsetW": 0, + "offsetX": 0, + "offsetY": 0, + "phase": "10800,21600,32400,46800,68400" + }, + "可可豆": { + "general": 1, + "offsetH": 0, + "offsetW": 0, + "offsetX": 0, + "offsetY": 0, + "phase": "28800,57600,86400,115200,144000" + }, + "哈密瓜": { + "general": 1, + "offsetH": 0, + "offsetW": 0, + "offsetX": 0, + "offsetY": 0, + "phase": "18000,39600,61200,86400,111600" + }, + "四星丁香水仙": { + "general": 1, + "offsetH": 0, + "offsetW": 0, + "offsetX": 0, + "offsetY": 0, + "phase": "14400,28800,46800,64800,86400" + }, + "四星书生捧墨": { + "general": 1, + "offsetH": 0, + "offsetW": 0, + "offsetX": 0, + "offsetY": 0, + "phase": "3600,10800,21600,32400,43200" + }, + "四星五彩椒": { + "general": 1, + "offsetH": 0, + "offsetW": 0, + "offsetX": 0, + "offsetY": 0, + "phase": "14400,28800,46800,64800,86400" + }, + "四星什样锦牡丹": { + "general": 1, + "offsetH": 0, + "offsetW": 0, + "offsetX": 0, + "offsetY": 0, + "phase": "7200,14400,25200,36000,46800" + }, + "四星卷丹百合": { + "general": 1, + "offsetH": 0, + "offsetW": 0, + "offsetX": 0, + "offsetY": 0, + "phase": "7200,14400,25200,36000,46800" + }, + "四星喇叭水仙": { + "general": 1, + "offsetH": 0, + "offsetW": 0, + "offsetX": 0, + "offsetY": 0, + "phase": "7200,14400,25200,36000,46800" + }, + "四星围裙水仙": { + "general": 1, + "offsetH": 0, + "offsetW": 0, + "offsetX": 0, + "offsetY": 0, + "phase": "14400,28800,46800,64800,86400" + }, + "四星巨峰葡萄": { + "general": 1, + "offsetH": 0, + "offsetW": 0, + "offsetX": 0, + "offsetY": 0, + "phase": "3600,10800,21600,32400,43200" + }, + "四星白玉霓": { + "general": 1, + "offsetH": 0, + "offsetW": 0, + "offsetX": 0, + "offsetY": 0, + "phase": "10800,21600,32400,46800,68400" + }, + "四星百叶蔷薇": { + "general": 1, + "offsetH": 0, + "offsetW": 0, + "offsetX": 0, + "offsetY": 0, + "phase": "3600,10800,21600,32400,43200" + }, + "四星秦椒": { + "general": 1, + "offsetH": 0, + "offsetW": 0, + "offsetX": 0, + "offsetY": 0, + "phase": "14400,28800,46800,64800,86400" + }, + "四星红富士": { + "general": 1, + "offsetH": 0, + "offsetW": 0, + "offsetX": 0, + "offsetY": 0, + "phase": "7200,14400,25200,36000,46800" + }, + "四星绿绒蒿": { + "general": 1, + "offsetH": 0, + "offsetW": 0, + "offsetX": 0, + "offsetY": 0, + "phase": "14400,28800,46800,64800,86400" + }, + "四星荷花蔷薇": { + "general": 1, + "offsetH": 0, + "offsetW": 0, + "offsetX": 0, + "offsetY": 0, + "phase": "3600,10800,21600,32400,43200" + }, + "四星葛巾紫": { + "general": 1, + "offsetH": 0, + "offsetW": 0, + "offsetX": 0, + "offsetY": 0, + "phase": "14400,28800,46800,64800,86400" + }, + "四星香水百合": { + "general": 1, + "offsetH": 0, + "offsetW": 0, + "offsetX": 0, + "offsetY": 0, + "phase": "14400,28800,46800,64800,86400" + }, + "园艺世": { + "general": 1, + "offsetH": 0, + "offsetW": 0, + "offsetX": 0, + "offsetY": 0, + "phase": "10800,21600,32400,46800,68400" + }, + "围裙水仙": { + "general": 1, + "offsetH": 0, + "offsetW": 0, + "offsetX": 0, + "offsetY": 0, + "phase": "14400,28800,46800,64800,86400" + }, + "土豆": { + "general": 1, + "offsetH": 0, + "offsetW": 0, + "offsetX": 0, + "offsetY": 0, + "phase": "7200,14400,25200,39600,54000" + }, + "大白菜": { + "general": 1, + "offsetH": 0, + "offsetW": 0, + "offsetX": 0, + "offsetY": 0, + "phase": "7200,14400,25200,36000,50400" + }, + "大蒜": { + "general": 1, + "offsetH": 0, + "offsetW": 0, + "offsetX": 0, + "offsetY": 0, + "phase": "7200,18000,25200,36000,50400" + }, + "富贵竹": { + "general": 1, + "offsetH": 0, + "offsetW": 0, + "offsetX": 0, + "offsetY": 0, + "phase": "10800,21600,32400,46800,68400" + }, + "小白菜": { + "general": 1, + "offsetH": 0, + "offsetW": 0, + "offsetX": 0, + "offsetY": 0, + "phase": "7200,14400,25200,39600,39600" + }, + "小麦": { + "general": 1, + "offsetH": 0, + "offsetW": 0, + "offsetX": 0, + "offsetY": 0, + "phase": "7200,14400,25200,36000,50400" + }, + "山竹": { + "general": 1, + "offsetH": 0, + "offsetW": 0, + "offsetX": 0, + "offsetY": 0, + "phase": "18000,39600,61200,86400,111600" + }, + "无花果": { + "general": 1, + "offsetH": 0, + "offsetW": 0, + "offsetX": 0, + "offsetY": 0, + "phase": "28800,57600,86400,118800,151200" + }, + "月柿": { + "general": 1, + "offsetH": 0, + "offsetW": 0, + "offsetX": 0, + "offsetY": 0, + "phase": "18000,39600,61200,86400,111600" + }, + "木瓜": { + "general": 1, + "offsetH": 0, + "offsetW": 0, + "offsetX": 0, + "offsetY": 0, + "phase": "28800,61200,93600,129600,165600" + }, + "李子": { + "general": 1, + "offsetH": 0, + "offsetW": 0, + "offsetX": 0, + "offsetY": 0, + "phase": "50400,100800,151200,205200,259200" + }, + "杏子": { + "general": 1, + "offsetH": 0, + "offsetW": 0, + "offsetX": 0, + "offsetY": 0, + "phase": "18000,39600,61200,86400,111600" + }, + "杨桃": { + "general": 1, + "offsetH": 0, + "offsetW": 0, + "offsetX": 0, + "offsetY": 0, + "phase": "28800,61200,93600,129600,165600" + }, + "杨梅": { + "general": 1, + "offsetH": 0, + "offsetW": 0, + "offsetX": 0, + "offsetY": 0, + "phase": "18000,39600,61200,86400,111600" + }, + "枇杷": { + "general": 1, + "offsetH": 0, + "offsetW": 0, + "offsetX": 0, + "offsetY": 0, + "phase": "28800,57600,86400,118800,151200" + }, + "柚子": { + "general": 1, + "offsetH": 0, + "offsetW": 0, + "offsetX": 0, + "offsetY": 0, + "phase": "39600,82800,126000,172800,219600" + }, + "柠檬": { + "general": 1, + "offsetH": 0, + "offsetW": 0, + "offsetX": 0, + "offsetY": 0, + "phase": "18000,39600,61200,86400,111600" + }, + "栗子": { + "general": 1, + "offsetH": 0, + "offsetW": 0, + "offsetX": 0, + "offsetY": 0, + "phase": "21600,43200,64800,86400,108000" + }, + "核桃": { + "general": 1, + "offsetH": 0, + "offsetW": 0, + "offsetX": 0, + "offsetY": 0, + "phase": "10800,25200,43200,43200,61200" + }, + "桂圆": { + "general": 1, + "offsetH": 0, + "offsetW": 0, + "offsetX": 0, + "offsetY": 0, + "phase": "18000,39600,61200,86400,111600" + }, + "桃子": { + "general": 1, + "offsetH": 0, + "offsetW": 0, + "offsetX": 0, + "offsetY": 0, + "phase": "28800,57600,86400,118800,151200" + }, + "桑葚": { + "general": 1, + "offsetH": 0, + "offsetW": 0, + "offsetX": 0, + "offsetY": 0, + "phase": "18000,39600,61200,86400,111600" + }, + "梨": { + "general": 1, + "offsetH": 0, + "offsetW": 0, + "offsetX": 0, + "offsetY": 0, + "phase": "21600,43200,64800,90000,115200" + }, + "椒蕉树": { + "general": 1, + "offsetH": 0, + "offsetW": 0, + "offsetX": 0, + "offsetY": 0, + "phase": "10800,21600,32400,46800,68400" + }, + "椰子": { + "general": 1, + "offsetH": 0, + "offsetW": 0, + "offsetX": 0, + "offsetY": 0, + "phase": "36000,75600,115200,154800,198000" + }, + "榅桲": { + "general": 1, + "offsetH": 0, + "offsetW": 0, + "offsetX": 0, + "offsetY": 0, + "phase": "36000,72000,108000,144000,180000" + }, + "榛子": { + "general": 1, + "offsetH": 0, + "offsetW": 0, + "offsetX": 0, + "offsetY": 0, + "phase": "28800,61200,93600,129600,165600" + }, + "榴莲": { + "general": 1, + "offsetH": 0, + "offsetW": 0, + "offsetX": 0, + "offsetY": 0, + "phase": "18000,39600,61200,86400,111600" + }, + "樱桃": { + "general": 1, + "offsetH": 0, + "offsetW": 0, + "offsetX": 0, + "offsetY": 0, + "phase": "50400,100800,151200,205200,259200" + }, + "橄榄": { + "general": 1, + "offsetH": 0, + "offsetW": 0, + "offsetX": 0, + "offsetY": 0, + "phase": "21600,43200,61200,82800,108000" + }, + "橙子": { + "general": 1, + "offsetH": 0, + "offsetW": 0, + "offsetX": 0, + "offsetY": 0, + "phase": "25200,50400,75600,104400,133200" + }, + "水稻": { + "general": 1, + "offsetH": 0, + "offsetW": 0, + "offsetX": 0, + "offsetY": 0, + "phase": "7200,14400,25200,36000,50400" + }, + "沙棘": { + "general": 1, + "offsetH": 0, + "offsetW": 0, + "offsetX": 0, + "offsetY": 0, + "phase": "32400,68400,104400,136800,169200" + }, + "油菜": { + "general": 1, + "offsetH": 0, + "offsetW": 0, + "offsetX": 0, + "offsetY": 0, + "phase": "14400,28800,46800,61200,61200" + }, + "澳洲青苹果": { + "general": 1, + "offsetH": 0, + "offsetW": 0, + "offsetX": 0, + "offsetY": 0, + "phase": "14400,28800,46800,64800,86400" + }, + "火龙果": { + "general": 1, + "offsetH": 0, + "offsetW": 0, + "offsetX": 0, + "offsetY": 0, + "phase": "46800,97200,147600,198000,252000" + }, + "灯笼果": { + "general": 1, + "offsetH": 0, + "offsetW": 0, + "offsetX": 0, + "offsetY": 0, + "phase": "21600,39600,57600,79200,104400" + }, + "点地梅": { + "general": 1, + "offsetH": 0, + "offsetW": 0, + "offsetX": 0, + "offsetY": 0, + "phase": "10800,21600,32400,46800,68400" + }, + "牧草": { + "general": 1, + "offsetH": 0, + "offsetW": 0, + "offsetX": 0, + "offsetY": 0, + "phase": "7200,14400,21600,28800,28800" + }, + "猕猴桃": { + "general": 1, + "offsetH": 0, + "offsetW": 0, + "offsetX": 0, + "offsetY": 0, + "phase": "28800,61200,93600,129600,165600" + }, + "玉米": { + "general": 1, + "offsetH": 0, + "offsetW": 0, + "offsetX": 0, + "offsetY": 0, + "phase": "7200,14400,25200,36000,50400" + }, + "甘蔗": { + "general": 1, + "offsetH": 0, + "offsetW": 0, + "offsetX": 0, + "offsetY": 0, + "phase": "18000,39600,61200,86400,111600" + }, + "生菜": { + "general": 1, + "offsetH": 0, + "offsetW": 0, + "offsetX": 0, + "offsetY": 0, + "phase": "10800,21600,36000,50400,68400" + }, + "番石榴": { + "general": 1, + "offsetH": 0, + "offsetW": 0, + "offsetX": 0, + "offsetY": 0, + "phase": "18000,39600,61200,86400,111600" + }, + "番茄": { + "general": 1, + "offsetH": 0, + "offsetW": 0, + "offsetX": 0, + "offsetY": 0, + "phase": "7200,18000,32400,46800,61200" + }, + "番荔枝": { + "general": 1, + "offsetH": 0, + "offsetW": 0, + "offsetX": 0, + "offsetY": 0, + "phase": "18000,43200,64800,93600,111600" + }, + "白萝卜": { + "general": 1, + "offsetH": 0, + "offsetW": 0, + "offsetX": 0, + "offsetY": 0, + "phase": "7200,14400,25200,36000,36000" + }, + "百香果": { + "general": 1, + "offsetH": 0, + "offsetW": 0, + "offsetX": 0, + "offsetY": 0, + "phase": "21600,43200,64800,86400,108000" + }, + "益母草": { + "general": 1, + "offsetH": 0, + "offsetW": 0, + "offsetX": 0, + "offsetY": 0, + "phase": "32400,64800,97200,133200,169200" + }, + "睡莲": { + "general": 1, + "offsetH": 0, + "offsetW": 0, + "offsetX": 0, + "offsetY": 0, + "phase": "39600,82800,126000,172800,219600" + }, + "石榴": { + "general": 1, + "offsetH": 0, + "offsetW": 0, + "offsetX": 0, + "offsetY": 0, + "phase": "36000,72000,108000,147600,187200" + }, + "秦椒": { + "general": 1, + "offsetH": 0, + "offsetW": 0, + "offsetX": 0, + "offsetY": 0, + "phase": "14400,28800,46800,64800,86400" + }, + "竹笋": { + "general": 1, + "offsetH": 0, + "offsetW": 0, + "offsetX": 0, + "offsetY": 0, + "phase": "18000,36000,54000,72000,90000" + }, + "箬竹": { + "general": 1, + "offsetH": 0, + "offsetW": 0, + "offsetX": 0, + "offsetY": 0, + "phase": "21600,43200,64800,86400,108000" + }, + "红叶梅": { + "general": 1, + "offsetH": 0, + "offsetW": 0, + "offsetX": 0, + "offsetY": 0, + "phase": "10800,21600,32400,46800,68400" + }, + "红枣": { + "general": 1, + "offsetH": 0, + "offsetW": 0, + "offsetX": 0, + "offsetY": 0, + "phase": "7200,18000,28800,43200,57600" + }, + "红毛丹": { + "general": 1, + "offsetH": 0, + "offsetW": 0, + "offsetX": 0, + "offsetY": 0, + "phase": "21600,46800,64800,90000,118800" + }, + "红玫瑰": { + "general": 1, + "offsetH": 0, + "offsetW": 0, + "offsetX": 0, + "offsetY": 0, + "phase": "10800,21600,32400,46800,64800" + }, + "绿绒蒿": { + "general": 1, + "offsetH": 0, + "offsetW": 0, + "offsetX": 0, + "offsetY": 0, + "phase": "14400,28800,46800,64800,86400" + }, + "胡萝卜": { + "general": 1, + "offsetH": 0, + "offsetW": 0, + "offsetX": 0, + "offsetY": 0, + "phase": "7200,18000,32400,46800,46800" + }, + "芒果": { + "general": 1, + "offsetH": 0, + "offsetW": 0, + "offsetX": 0, + "offsetY": 0, + "phase": "18000,39600,61200,86400,111600" + }, + "芦荟": { + "general": 0, + "offsetH": 0, + "offsetW": 0, + "offsetX": 0, + "offsetY": 0, + "phase": "21600,39600,57600,79200,104400" + }, + "芭蕉": { + "general": 1, + "offsetH": 0, + "offsetW": 0, + "offsetX": 0, + "offsetY": 0, + "phase": "21600,39600,57600,79200,104400" + }, + "花生": { + "general": 1, + "offsetH": 0, + "offsetW": 0, + "offsetX": 0, + "offsetY": 0, + "phase": "28800,57600,86400,118800,151200" + }, + "花菜": { + "general": 1, + "offsetH": 0, + "offsetW": 0, + "offsetX": 0, + "offsetY": 0, + "phase": "21600,43200,64800,82800,100800" + }, + "芹菜": { + "general": 1, + "offsetH": 0, + "offsetW": 0, + "offsetX": 0, + "offsetY": 0, + "phase": "10800,25200,39600,54000,72000" + }, + "苦瓜": { + "general": 1, + "offsetH": 0, + "offsetW": 0, + "offsetX": 0, + "offsetY": 0, + "phase": "18000,39600,61200,86400,111600" + }, + "苹果": { + "general": 1, + "offsetH": 0, + "offsetW": 0, + "offsetX": 0, + "offsetY": 0, + "phase": "14400,28800,43200,57600,75600" + }, + "茄子": { + "general": 1, + "offsetH": 0, + "offsetW": 0, + "offsetX": 0, + "offsetY": 0, + "phase": "7200,18000,28800,43200,57600" + }, + "草莓": { + "general": 1, + "offsetH": 0, + "offsetW": 0, + "offsetX": 0, + "offsetY": 0, + "phase": "14400,28800,46800,64800,86400" + }, + "荔枝": { + "general": 1, + "offsetH": 0, + "offsetW": 0, + "offsetX": 0, + "offsetY": 0, + "phase": "54000,108000,165600,219600,277200" + }, + "莲藕": { + "general": 1, + "offsetH": 0, + "offsetW": 0, + "offsetX": 0, + "offsetY": 0, + "phase": "21600,46800,75600,100800,122400" + }, + "莲雾": { + "general": 1, + "offsetH": 0, + "offsetW": 0, + "offsetX": 0, + "offsetY": 0, + "phase": "18000,39600,64800,93600,126000" + }, + "菠萝": { + "general": 1, + "offsetH": 0, + "offsetW": 0, + "offsetX": 0, + "offsetY": 0, + "phase": "43200,90000,136800,183600,230400" + }, + "菠萝蜜": { + "general": 1, + "offsetH": 0, + "offsetW": 0, + "offsetX": 0, + "offsetY": 0, + "phase": "28800,46800,64800,86400,100800" + }, + "葛巾紫": { + "general": 1, + "offsetH": 0, + "offsetW": 0, + "offsetX": 0, + "offsetY": 0, + "phase": "14400,28800,46800,64800,86400" + }, + "葡萄": { + "general": 1, + "offsetH": 0, + "offsetW": 0, + "offsetX": 0, + "offsetY": 0, + "phase": "28800,61200,93600,129600,165600" + }, + "葫芦": { + "general": 1, + "offsetH": 0, + "offsetW": 0, + "offsetX": 0, + "offsetY": 0, + "phase": "39600,82800,126000,172800,219600" + }, + "蓝莓": { + "general": 1, + "offsetH": 0, + "offsetW": 0, + "offsetX": 0, + "offsetY": 0, + "phase": "18000,39600,61200,86400,111600" + }, + "薄荷": { + "general": 1, + "offsetH": 0, + "offsetW": 0, + "offsetX": 0, + "offsetY": 0, + "phase": "21600,39600,57600,82800,108000" + }, + "蘑菇": { + "general": 1, + "offsetH": 0, + "offsetW": 0, + "offsetX": 0, + "offsetY": 0, + "phase": "25200,50400,79200,108000,108000" + }, + "蛋黄果": { + "general": 1, + "offsetH": 0, + "offsetW": 0, + "offsetX": 0, + "offsetY": 0, + "phase": "32400,64800,97200,129600,162000" + }, + "蜜思桃": { + "general": 1, + "offsetH": 0, + "offsetW": 0, + "offsetX": 0, + "offsetY": 0, + "phase": "14400,32400,54000,79200,108000" + }, + "西瓜": { + "general": 1, + "offsetH": 0, + "offsetW": 0, + "offsetX": 0, + "offsetY": 0, + "phase": "14400,32400,54000,75600,100800" + }, + "豌豆": { + "general": 1, + "offsetH": 0, + "offsetW": 0, + "offsetX": 0, + "offsetY": 0, + "phase": "10800,21600,32400,46800,64800" + }, + "辣椒": { + "general": 1, + "offsetH": 0, + "offsetW": 0, + "offsetX": 0, + "offsetY": 0, + "phase": "10800,25200,39600,54000,72000" + }, + "迎春花": { + "general": 1, + "offsetH": 0, + "offsetW": 0, + "offsetX": 0, + "offsetY": 0, + "phase": "10800,21600,39600,57600,86400" + }, + "金桔": { + "general": 1, + "offsetH": 0, + "offsetW": 0, + "offsetX": 0, + "offsetY": 0, + "phase": "18000,39600,61200,86400,111600" + }, + "金针菇": { + "general": 1, + "offsetH": 0, + "offsetW": 0, + "offsetX": 0, + "offsetY": 0, + "phase": "28800,57600,86400,118800,151200" + }, + "韭菜": { + "general": 1, + "offsetH": 0, + "offsetW": 0, + "offsetX": 0, + "offsetY": 0, + "phase": "7200,18000,32400,46800,61200" + }, + "香水百合": { + "general": 1, + "offsetH": 0, + "offsetW": 0, + "offsetX": 0, + "offsetY": 0, + "phase": "14400,28800,46800,64800,86400" + }, + "香瓜": { + "general": 1, + "offsetH": 0, + "offsetW": 0, + "offsetX": 0, + "offsetY": 0, + "phase": "21600,43200,68400,97200,126000" + }, + "香粽龙舟": { + "general": 1, + "offsetH": 0, + "offsetW": 0, + "offsetX": 0, + "offsetY": 0, + "phase": "28800,61200,93600,129600,165600" + }, + "香蕉": { + "general": 1, + "offsetH": 0, + "offsetW": 0, + "offsetX": 0, + "offsetY": 0, + "phase": "18000,39600,61200,86400,111600" + }, + "魔法神花": { + "general": 1, + "offsetH": 0, + "offsetW": 0, + "offsetX": 0, + "offsetY": 0, + "phase": "14400,32400,54000,75600,100800" + }, + "鳄梨": { + "general": 1, + "offsetH": 0, + "offsetW": 0, + "offsetX": 0, + "offsetY": 0, + "phase": "25200,54000,75600,97200,115200" + }, + "鸾风玉": { + "general": 1, + "offsetH": 0, + "offsetW": 0, + "offsetX": 0, + "offsetY": 0, + "phase": "10800,21600,32400,46800,68400" + }, + "黄瓜": { + "general": 1, + "offsetH": 0, + "offsetW": 0, + "offsetX": 0, + "offsetY": 0, + "phase": "14400,32400,54000,79200,100800" + }, + "黄豆": { + "general": 1, + "offsetH": 0, + "offsetW": 0, + "offsetX": 0, + "offsetY": 0, + "phase": "14400,32400,54000,75600,100800" + }, + "黄金果": { + "general": 1, + "offsetH": 0, + "offsetW": 0, + "offsetX": 0, + "offsetY": 0, + "phase": "32400,64800,97200,133200,169200" + }, + "黑布仑": { + "general": 1, + "offsetH": 0, + "offsetW": 0, + "offsetX": 0, + "offsetY": 0, + "phase": "18000,36000,54000,72000,90000" + } + }, + "positions": [ + { + "x": 1352, + "y": 552 + }, + { + "x": 1462, + "y": 611 + }, + { + "x": 1572, + "y": 670 + }, + { + "x": 1682, + "y": 729 + }, + { + "x": 1792, + "y": 788 + }, + { + "x": 1902, + "y": 847 + }, + { + "x": 1237, + "y": 606 + }, + { + "x": 1350, + "y": 665 + }, + { + "x": 1457, + "y": 724 + }, + { + "x": 1567, + "y": 783 + }, + { + "x": 1677, + "y": 842 + }, + { + "x": 1787, + "y": 901 + }, + { + "x": 1116, + "y": 663 + }, + { + "x": 1226, + "y": 722 + }, + { + "x": 1336, + "y": 782 + }, + { + "x": 1455, + "y": 840 + }, + { + "x": 1565, + "y": 899 + }, + { + "x": 1671, + "y": 958 + }, + { + "x": 1002, + "y": 720 + }, + { + "x": 1105, + "y": 779 + }, + { + "x": 1224, + "y": 838 + }, + { + "x": 1343, + "y": 897 + }, + { + "x": 1449, + "y": 956 + }, + { + "x": 1563, + "y": 1015 + }, + { + "x": 883, + "y": 777 + }, + { + "x": 995, + "y": 836 + }, + { + "x": 1112, + "y": 895 + }, + { + "x": 1222, + "y": 954 + }, + { + "x": 1336, + "y": 1013 + }, + { + "x": 1451, + "y": 1072 + } + ], + "schema": 1, + "soil_size": [ + 234, + 156 + ] +} diff --git a/src/farm_render/layout.py b/src/farm_render/layout.py new file mode 100644 index 0000000..42386cc --- /dev/null +++ b/src/farm_render/layout.py @@ -0,0 +1,58 @@ +"""从一次快照与统一时间构造可缓存场景;不访问存储。""" + +import hashlib + +from farm_core.domain import Identity, Snapshot, integer + +from .models import PlotView, Scene + + +def scene_from_snapshot(identity, snapshot, now, metadata): + if not isinstance(identity, Identity) or not isinstance(snapshot, Snapshot): + raise ValueError("Identity and snapshot are required") + integer(now) + slots = [p.slot for p in snapshot.plots] + if len(set(slots)) != len(slots) or any( + type(slot) is not int or not 1 <= slot <= 30 for slot in slots + ): + raise ValueError("Invalid plot slots") + views = [] + for plot in sorted(snapshot.plots, key=lambda p: p.slot): + if plot.plant is None: + views.append(PlotView(plot.slot, "empty")) + continue + if plot.withered: + views.append(PlotView( + plot.slot, "withered", "plant/basic/9.png", size=(150, 212), + )) + continue + integer(plot.mature_at) + name = plot.plant.name + info = metadata["plants"].get(name) + if info is None: + views.append(PlotView(plot.slot, "growing", "missing")) + continue + phases = tuple(dict.fromkeys(int(p) for p in info["phase"].split(","))) + duration = (plot.plant.growth_seconds if plot.harvest_count == 0 + else plot.plant.regrowth_seconds) + elapsed = max(0, now - (plot.mature_at - duration)) + ripe = now >= plot.mature_at + stage = len(phases) if ripe else sum( + elapsed * phases[-1] >= threshold * duration + for threshold in phases + ) + sprite = f"plant/{name}/{stage}.png" + size = None + if stage == 0: + if info["general"]: + sprite = "plant/basic/0.png" + size = (max(1, 35 + info["offsetW"]), + max(1, 58 + info["offsetH"])) + views.append(PlotView( + plot.slot, "ripe" if ripe else "growing", sprite, stage, + (info["offsetX"], info["offsetY"]), size, + )) + return Scene( + hashlib.sha256(identity.key.encode()).hexdigest(), + snapshot.account, tuple(views), + ) diff --git a/src/farm_render/models.py b/src/farm_render/models.py new file mode 100644 index 0000000..5569614 --- /dev/null +++ b/src/farm_render/models.py @@ -0,0 +1,74 @@ +"""框架无关、不可变的渲染输入与输出。""" + +from dataclasses import dataclass + +from PIL import features + +from farm_core.domain import Account + + +@dataclass(frozen=True) +class RenderOptions: + width: int = 1024 # 目标宽度;从此尺寸开始合成。 + format: str = "AUTO" # AUTO/WEBP/JPEG/PNG。 + quality: int = 75 # 有损编码质量,不代表分辨率。 + method: int = 2 # WebP 编码力度,0最快,6最慢。 + theme: str = "default" # 显式主题,不读取隐含季节或全局配置。 + + def __post_init__(self): + for value, low, high in ( + (self.width, 256, 3072), (self.quality, 1, 95), + (self.method, 0, 6), + ): + if type(value) is not int or not low <= value <= high: + raise ValueError("Invalid render option") + if self.format not in ("AUTO", "WEBP", "JPEG", "PNG"): + raise ValueError("Unsupported output format") + if self.theme not in ("default", "spring", "summer", "autumn", + "winter"): + raise ValueError("Unsupported theme") + + @classmethod + def high(cls, **kwargs): + return cls(width=1536, quality=85, **kwargs) + + def codec(self, supported=("JPEG",)): + if self.format == "AUTO": + if "WEBP" in supported and features.check("webp"): + return "WEBP" + if "JPEG" in supported: + return "JPEG" + if "PNG" in supported: + return "PNG" + raise ValueError("No supported image codec") + if self.format == "WEBP" and not features.check("webp"): + raise ValueError("Pillow WebP support is unavailable") + return self.format + + +@dataclass(frozen=True) +class PlotView: + slot: int # 旧布局的1到30地块编号。 + state: str # empty/growing/ripe/withered。 + sprite: str = "" # 包内受控素材名,不来自用户路径。 + stage: int = 0 # 本季展示阶段。 + offset: tuple[int, int] = (0, 0) # 旧素材中心偏移。 + size: tuple[int, int] | None = None # 萌芽/枯萎原始尺寸覆盖。 + + +@dataclass(frozen=True) +class Scene: + scope: str # 完整身份的摘要,只用于缓存隔离。 + account: Account # 一致快照中的可见账户。 + plots: tuple[PlotView, ...] # 已解锁地块及统一时刻的阶段。 + + +@dataclass(frozen=True) +class RenderResult: + content: bytes # 已编码图片,不携带用户对象。 + mime: str # 与实际编码一致的媒体类型。 + width: int # 实际宽度。 + height: int # 实际高度。 + compose_ms: float # 本次合成毫秒。 + encode_ms: float # 本次编码毫秒。 + cache_hit: bool = False # 成图缓存命中。 diff --git a/src/farm_render/renderer.py b/src/farm_render/renderer.py new file mode 100644 index 0000000..272e8f5 --- /dev/null +++ b/src/farm_render/renderer.py @@ -0,0 +1,204 @@ +"""纯快照农场合成:目标尺寸、独立素材、可见状态成图缓存。""" + +from dataclasses import replace +import hashlib +from io import BytesIO +import logging +from pathlib import Path +from time import perf_counter + +from PIL import Image, ImageDraw, UnidentifiedImageError + +from .assets import Assets, ByteLRU +from .canvas import Canvas, font +from .layout import scene_from_snapshot +from .models import RenderOptions, RenderResult + +logger = logging.getLogger(__name__) +MAX_AVATAR_BYTES = 2 * 1024 * 1024 +MAX_AVATAR_PIXELS = 4_000_000 + + +class FarmRenderer: + def __init__( + self, font_path, asset_cache_bytes=32 * 1024 * 1024, + result_cache_bytes=8 * 1024 * 1024, + ): + self.font_path = Path(font_path).resolve() + if not self.font_path.is_file(): + raise ValueError("Configure an existing Chinese font path") + font(self.font_path, 16) + self.assets = Assets(asset_cache_bytes) + self.results = ByteLRU(result_cache_bytes) + + def scene(self, identity, snapshot, now): + return scene_from_snapshot( + identity, snapshot, now, self.assets.metadata, + ) + + def request_key( + self, identity, snapshot, now, options=None, avatar=None, + supported=("JPEG",), + ): + options = options or RenderOptions() + if avatar is not None and not isinstance(avatar, bytes): + raise ValueError("Avatar must be bytes or None") + digest = hashlib.sha256(avatar).hexdigest() if ( + avatar and len(avatar) <= MAX_AVATAR_BYTES + ) else "default" + scene = self.scene(identity, snapshot, now) + return scene, options, options.codec(supported), digest + + def render( + self, identity, snapshot, now, options=None, avatar=None, + supported=("JPEG",), + ): + key = self.request_key( + identity, snapshot, now, options, avatar, supported, + ) + cached = self.results.get(key) + if cached is not None: + logger.info("farm render cache=hit bytes=%s", len(cached.content)) + return replace(cached, cache_hit=True, compose_ms=0, encode_ms=0) + scene, options, codec, _ = key + started = perf_counter() + canvas = self._base(scene, options) + self._plants(canvas, scene, options) + self._panel(canvas, scene, options, avatar) + composed = perf_counter() + content = canvas.encode(codec, options.quality, options.method) + encoded = perf_counter() + result = RenderResult( + content, {"JPEG":"image/jpeg", "PNG":"image/png", + "WEBP":"image/webp"}[codec], + *canvas.image.size, (composed - started) * 1000, + (encoded - composed) * 1000, + ) + self.results.put(key, result, len(content) + 4096) + logger.info( + "farm render cache=miss compose_ms=%.2f encode_ms=%.2f " + "width=%s height=%s bytes=%s codec=%s", + result.compose_ms, result.encode_ms, result.width, + result.height, len(content), codec, + ) + return result + + @staticmethod + def _scale(value, options): + return round(value * options.width / 3072) + + def _base(self, scene, options): + slots = frozenset(p.slot for p in scene.plots) + key = ("base", options.theme, options.width, slots) + cached = self.assets.cache.get(key) + if cached is not None: + return Canvas(cached.copy()) + s = lambda n: self._scale(n, options) + size = (options.width, s(1872)) + background = self.assets.image( + "background/decorateIcon/" + f"decorate_{options.theme}_1.jpg", size, + ) + canvas = Canvas(background) + soil_size = tuple(max(1, s(n)) for n in ( + self.assets.metadata["soil_size"] + )) + tiles = { + True: self.assets.image("soil/普通土地.png", soil_size), + False: self.assets.image("soil/草土地.png", soil_size), + } + expansion = False + for slot, pos in enumerate(self.assets.metadata["positions"], 1): + xy = (s(pos["x"]), s(pos["y"])) + canvas.paste(tiles[slot in slots], xy) + if slot not in slots and not expansion: + icon = self.assets.image( + "background/expansion.png", (max(1, s(90)),) * 2, + ) + canvas.paste(icon, (xy[0] + s(70), xy[1] + s(10))) + expansion = True + self.assets.cache.put(key, canvas.image, size[0] * size[1] * 4) + return canvas.copy() + + def _plants(self, canvas, scene, options): + s = lambda n: self._scale(n, options) + soil_w, soil_h = self.assets.metadata["soil_size"] + first_ripe = True + for plot in scene.plots: + if not plot.sprite: + continue + pos = self.assets.metadata["positions"][plot.slot - 1] + metadata = self.assets.metadata["images"].get(plot.sprite) + if metadata is None: + logger.warning("farm render missing packaged crop sprite") + plant = Image.new("RGBA", (max(1, s(60)),) * 2, "#597b45") + ImageDraw.Draw(plant).text( + (s(10), 0), "?", fill="white", + font=font(self.font_path, max(10, s(48))), + ) + else: + size = plot.size or metadata["size"] + plant = self.assets.image( + plot.sprite, tuple(max(1, s(n)) for n in size), + ) + offset_x, offset_y = plot.offset + xy = ( + s(pos["x"] + soil_w / 2 + offset_x) - plant.width // 2, + s(pos["y"] + soil_h / 2 + offset_y) - plant.height // 2, + ) + canvas.paste(plant, xy) + if plot.state == "ripe" and first_ripe: + icon = self.assets.image( + "background/ripe.png", (max(1, s(100)),) * 2, + ) + canvas.paste(icon, (s(pos["x"] + 70), s(pos["y"] - 80))) + first_ripe = False + + def _avatar(self, content, size): + if content and len(content) <= MAX_AVATAR_BYTES: + try: + with Image.open(BytesIO(content)) as image: + if image.width * image.height > MAX_AVATAR_PIXELS: + raise ValueError("Avatar dimensions exceed limit") + image.thumbnail(size, Image.Resampling.LANCZOS) + avatar = Image.new("RGBA", size, "#cce3c1") + avatar.alpha_composite(image.convert("RGBA"), ( + (size[0] - image.width) // 2, + (size[1] - image.height) // 2, + )) + return avatar + except (UnidentifiedImageError, OSError, ValueError, + Image.DecompressionBombError): + logger.warning("farm render avatar rejected; using default") + avatar = Image.new("RGBA", size, "#dce8cf") + draw = ImageDraw.Draw(avatar) + w, h = size + draw.ellipse((w*.3, h*.12, w*.7, h*.52), fill="#6f8a54") + draw.ellipse((w*.15, h*.55, w*.85, h*1.2), fill="#6f8a54") + return avatar + + def _panel(self, canvas, scene, options, avatar): + s = lambda n: self._scale(n, options) + canvas.rectangle( + tuple(s(n) for n in (90, 75, 1160, 425)), + "#fff9e8", s(24), + ) + canvas.paste(self._avatar(avatar, (s(170), s(170))), (s(125), s(125))) + size = max(12, s(55)) + name = canvas.fit_text( + scene.account.name, self.font_path, size, s(790), + ) + canvas.text((s(330), s(105)), name, self.font_path, size, "#493525") + account = scene.account + lines = ( + f"等级 {account.level} 经验 {account.experience}", + f"农场币 {account.point} 点券 {account.vip_point}", + f"已解锁 {len(scene.plots)}/30 块地", + ) + for index, line in enumerate(lines): + size = max(10, s(43)) + text = canvas.fit_text(line, self.font_path, size, s(790)) + canvas.text( + (s(330), s(195 + index * 65)), text, + self.font_path, size, "#674d34", + ) diff --git a/src/farm_render/service.py b/src/farm_render/service.py new file mode 100644 index 0000000..c3756e7 --- /dev/null +++ b/src/farm_render/service.py @@ -0,0 +1,89 @@ +"""异步入口:一次快照、有限任务、共享绘制与关闭排空。""" + +import asyncio +import logging +from time import perf_counter +import time + +logger = logging.getLogger(__name__) + + +class RenderService: + def __init__(self, renderer, concurrency=1, max_pending=8): + if (type(concurrency) is not int or type(max_pending) is not int + or concurrency < 1 or max_pending < concurrency): + raise ValueError("Invalid render capacity") + self.renderer = renderer + self._semaphore = asyncio.Semaphore(concurrency) + self._max_pending = max_pending + self._pending = {} + self._closed = False + + async def my_farm( + self, source, identity, *, now=None, options=None, avatar=None, + supported=("JPEG",), + ): + if self._closed: + raise RuntimeError("Render service closed") + started = perf_counter() + snapshot = await source.snapshot(identity) + logger.info("farm render query_ms=%.2f", (perf_counter()-started)*1000) + return await self.render( + identity, snapshot, int(time.time()) if now is None else now, + options, avatar, supported, + ) + + async def render( + self, identity, snapshot, now, options=None, avatar=None, + supported=("JPEG",), + ): + if self._closed: + raise RuntimeError("Render service closed") + key = self.renderer.request_key( + identity, snapshot, now, options, avatar, supported, + ) + task = self._pending.get(key) + if task is None: + if len(self._pending) >= self._max_pending: + raise RuntimeError("Render queue full") + task = asyncio.create_task(self._run( + identity, snapshot, now, options, avatar, supported, + )) + self._pending[key] = task + + def finished(done): + self._pending.pop(key, None) + if not done.cancelled(): + done.exception() + + task.add_done_callback(finished) + return await asyncio.shield(task) + + async def _run(self, *args): + started = perf_counter() + async with self._semaphore: + logger.info( + "farm render queue_ms=%.2f", (perf_counter()-started)*1000, + ) + worker = asyncio.create_task(asyncio.to_thread( + self.renderer.render, *args, + )) + try: + return await asyncio.shield(worker) + except asyncio.CancelledError: + await worker + raise + + async def close(self): + self._closed = True + pending = tuple(self._pending.values()) + if pending: + await asyncio.shield(asyncio.gather( + *pending, return_exceptions=True, + )) + + async def __aenter__(self): + return self + + async def __aexit__(self, *exc): + await self.close() diff --git a/tests/README.md b/tests/README.md index d1ee513..43cf438 100644 --- a/tests/README.md +++ b/tests/README.md @@ -1,12 +1,11 @@ -# 测试实施边界 +# 基础核心测试 -当前没有新核心或业务自动化测试,不将空目录算作测试通过。 -迁移完整性可运行 python scripts/check_baseline.py。 +- test_domain.py:整数规则、202条目录、身份和错误配置。 +- test_transactions.py:完整玩法、边界、回滚、幂等、并发、取消及schema。 +- test_packaging.py:框架隔离、导入副作用、离线wheel安装。 -后续按 docs/01-roadmap.md 和 docs/06-code-review.md 建立: -unit(纯规则与数值)、integration(SQLite 事务与并发)、 -contracts(框架输入输出)、migrations(迁移与恢复)、 -packaging(独立安装包及真实框架加载)。 - -首批必须覆盖旧缺陷 F01–F08/F11 和提交前取消、重复事件、跨日、 -同一玩家并发操作。测试使用脱敏构造数据,不提交真实玩家数据库。 +python scripts/verify_foundation.py 为统一离线验证入口。 +包装测试需本机已有 pip、setuptools、wheel,不访问网络安装依赖。 +测试只使用带明确前缀的临时目录和构造玩家,不读取真实玩家库。 +语法基线3.10;本轮执行环境为Windows/Python3.12.14。 +真实宿主合同、旧数据迁移和Linux运行矩阵尚待后续实现。 diff --git a/tests/test_domain.py b/tests/test_domain.py new file mode 100644 index 0000000..76bdf11 --- /dev/null +++ b/tests/test_domain.py @@ -0,0 +1,72 @@ +import json +from pathlib import Path +import unittest + +from farm_core import Catalogue, FarmError, Identity +from farm_core.domain import ( + MAX_VALUE, Plant, boosted, level_for, +) + + +class DomainTests(unittest.TestCase): + def test_integer_level_boundaries(self): + for level in (0, 1, 2, 50, 1000000): + threshold = 100 * level * (level + 1) + self.assertEqual(level_for(threshold), level) + if level: + self.assertEqual(level_for(threshold - 1), level - 1) + self.assertGreater(level_for(MAX_VALUE), 0) + + def test_per_plot_bonus_is_order_independent(self): + plots = [(20, 100), (30, 50), (17, 10)] + first = sum(boosted(*plot) for plot in plots) + second = sum(boosted(*plot) for plot in reversed(plots)) + self.assertEqual(first, 103) + self.assertEqual(first, second) + + def test_bool_and_out_of_range_values_are_not_integers(self): + for value in (True, False, -1, 1.5, MAX_VALUE + 1): + with self.subTest(value=value): + with self.assertRaises(FarmError): + level_for(value) + + def test_explicit_identity_encoding_does_not_collide(self): + first = Identity("a", "b:c", "d", "e") + second = Identity("a:b", "c", "d", "e") + self.assertNotEqual(first.key, second.key) + with self.assertRaises(FarmError): + Identity("a", "", "qq", "1") + + def test_bundled_catalogue_matches_all_source_rows(self): + root = Path(__file__).resolve().parents[1] + document = json.loads( + (root / "src/farm_core/data/plants.json").read_bytes() + ) + catalogue = Catalogue.bundled() + self.assertEqual(len(catalogue), 202) + for row in document["plants"]: + expected = Plant.from_legacy(row) + self.assertEqual(catalogue.get(row["name"]), expected) + self.assertEqual(catalogue.get("胡萝卜").growth_seconds, 13 * 3600) + + def test_corrupt_catalogue_is_rejected(self): + for content in ( + b"not json", b"{}", b'{"schema":true,"plants":[]}', + b'{"schema":2,"plants":[]}', + ): + with self.subTest(content=content): + with self.assertRaises(FarmError): + Catalogue.loads(content) + + def test_invalid_and_duplicate_plants_are_rejected(self): + plant = next(iter(Catalogue.bundled())) + with self.assertRaises(FarmError): + Catalogue([plant, plant]) + with self.assertRaises(FarmError): + Catalogue.bundled().get("不存在") + with self.assertRaises(FarmError): + Plant(1, "bad", 0, -1, False, 0, 1, 1, 1, 1, 1, 1) + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/test_packaging.py b/tests/test_packaging.py new file mode 100644 index 0000000..3e482cf --- /dev/null +++ b/tests/test_packaging.py @@ -0,0 +1,147 @@ +import ast +import os +from pathlib import Path +import subprocess +import sys +import tempfile +import unittest + + +ROOT = Path(__file__).resolve().parents[1] + + +class PackagingTests(unittest.TestCase): + def test_core_has_no_framework_imports_and_uses_python310_syntax(self): + for path in ( + * (ROOT / "src/farm_core").glob("*.py"), + * (ROOT / "src/farm_render").glob("*.py"), + ): + self.assertTrue(all( + len(line) <= 80 + for line in path.read_text(encoding="utf-8").splitlines() + ), path.name) + tree = ast.parse( + path.read_text(encoding="utf-8"), feature_version=(3, 10), + ) + for node in ast.walk(tree): + names = [] + if isinstance(node, ast.Import): + names = [alias.name for alias in node.names] + if isinstance(node, ast.ImportFrom) and node.level == 0: + names = [node.module or ""] + self.assertFalse( + any(name.split(".")[0] in ( + "nonebot", "astrbot", "zhenxun", + ) for name in names), + path.name, + ) + + def test_import_has_no_runtime_files_or_background_threads(self): + code = ( + "import os,sys,threading; " + "before=set(os.listdir('.')); " + "threads=len(threading.enumerate()); " + "sys.path.insert(0,sys.argv[1]); import farm_core; " + "assert set(os.listdir('.'))==before; " + "assert len(threading.enumerate())==threads" + ) + with tempfile.TemporaryDirectory( + prefix="farm-import-test-", + ) as temporary: + subprocess.run( + [sys.executable, "-I", "-B", "-c", code, str(ROOT / "src")], + cwd=temporary, check=True, timeout=30, + ) + + def test_wheel_runs_without_source_legacy_or_frameworks(self): + with tempfile.TemporaryDirectory( + prefix="farm-wheel-test-", + ) as temporary: + temporary = Path(temporary).resolve() + self.assertTrue(temporary.is_relative_to( + Path(tempfile.gettempdir()).resolve() + )) + wheels = temporary / "wheels" + installed = temporary / "installed" + self.run_checked([ + sys.executable, "-m", "pip", "--disable-pip-version-check", + "wheel", "--no-index", "--no-deps", "--no-build-isolation", + "--wheel-dir", str(wheels), str(ROOT), + ]) + wheel, = wheels.glob("farm_core-*.whl") + self.run_checked([ + sys.executable, "-m", "pip", "--disable-pip-version-check", + "install", "--no-index", "--no-deps", "--no-compile", + "--target", str(installed), str(wheel), + ]) + code = """ +import asyncio +from pathlib import Path +import sys + +target = Path(sys.argv[1]) +sys.path.insert(0, str(target)) +import farm_core +from farm_core import Catalogue, FarmService, Identity, SQLiteStore +from farm_render import FarmRenderer, RenderOptions +from PIL import Image +from io import BytesIO + +assert Path(farm_core.__file__).is_relative_to(target) +assert len(Catalogue.bundled()) == 202 + +async def check(): + store = SQLiteStore(sys.argv[2]) + await store.initialize() + try: + service = FarmService(store, Catalogue.bundled(), lambda: 0) + who = Identity("installed", "local", "local", "user") + registered = await service.register(who, "installed", "register") + assert registered.data["account"]["point"] == 500 + plant = next( + p for p in service.catalogue + if p.level == 0 and not p.is_vip and p.cost <= 500 + ) + await service.buy(who, plant.name, 1, "buy") + assert (await service.snapshot(who)).inventory[0].seeds == 1 + font = Path(sys.argv[3]) + renderer = FarmRenderer(font) + assert len(renderer.assets.metadata["images"]) > 600 + result = renderer.render( + who, await service.snapshot(who), 0, + RenderOptions(format="PNG"), + ) + assert Image.open(BytesIO(result.content)).size == (1024, 624) + finally: + await store.close() + +asyncio.run(check()) +""" + self.run_checked( + [ + sys.executable, "-I", "-B", "-c", code, str(installed), + str(temporary / "installed.sqlite3"), + str(Path(os.environ.get( + "FARM_TEST_FONT", ( + "C:/Windows/Fonts/msyh.ttc" if os.name == "nt" + else "/usr/share/fonts/truetype/" + "dejavu/DejaVuSans.ttf" + ), + ))), + ], + cwd=temporary, + ) + + def run_checked(self, command, cwd=ROOT): + result = subprocess.run( + command, cwd=cwd, capture_output=True, text=True, + encoding="utf-8", errors="replace", timeout=90, + env={**os.environ, "PIP_NO_INDEX": "1"}, + ) + self.assertEqual( + result.returncode, 0, result.stdout + result.stderr, + ) + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/test_render.py b/tests/test_render.py new file mode 100644 index 0000000..ac86642 --- /dev/null +++ b/tests/test_render.py @@ -0,0 +1,296 @@ +import asyncio +from dataclasses import replace +from io import BytesIO +from pathlib import Path +import os +import threading +import time +import unittest +from unittest.mock import patch + +from PIL import Image, features + +from farm_core import Catalogue, Identity +from farm_core.domain import Account, Plot, Snapshot + +try: + from farm_render import FarmRenderer, RenderOptions, RenderService + from farm_render.assets import ByteLRU + from farm_render.canvas import Canvas +except ImportError: + FarmRenderer = None + + +FONT = Path("C:/Windows/Fonts/msyh.ttc") +if not FONT.exists(): + FONT = Path("/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf") +FONT = Path(os.environ.get("FARM_TEST_FONT", FONT)) + + +def snapshot(plots=None, **account): + values = dict(name="演示农场", point=500, vip_point=0, experience=0) + values.update(account) + return Snapshot(Account(**values), (), tuple( + plots if plots is not None else [Plot(i) for i in range(1, 4)] + )) + + +class RenderTests(unittest.TestCase): + def setUp(self): + self.assertIsNotNone(FarmRenderer, "共享图片模块尚未实现") + self.renderer = FarmRenderer(FONT) + self.who = Identity("test", "local", "qq", "one") + self.plant = next(iter(Catalogue.bundled())) + + def test_canvas_alpha_crop_text_and_formats(self): + canvas = Canvas.new((100, 80), "white") + overlay = Image.new("RGBA", (20, 20), (255, 0, 0, 128)) + canvas.paste(overlay, (0, 0)) + self.assertEqual(canvas.image.getpixel((5, 5))[:3], (255, 127, 127)) + self.assertEqual(canvas.crop((0, 0, 10, 10)).image.size, (10, 10)) + self.assertEqual(canvas.resize((50, 40)).image.size, (50, 40)) + text = canvas.fit_text("long farm name" * 20, FONT, 16, 80) + self.assertLessEqual(canvas.measure_text(text, FONT, 16), 80) + canvas.text((1, 30), text, FONT, 16, "black") + for fmt in ("PNG", "JPEG", "WEBP"): + if fmt == "WEBP" and not features.check("webp"): + continue + content = canvas.encode(fmt, 75, 2) + with Image.open(BytesIO(content)) as image: + self.assertEqual(image.format, fmt) + self.assertEqual(image.size, (100, 80)) + + def test_profiles_formats_and_bad_avatar_fallback(self): + for width in (1024, 1536): + for fmt in ("JPEG", "PNG", "WEBP"): + if fmt == "WEBP" and not features.check("webp"): + continue + result = self.renderer.render( + self.who, snapshot(), 0, + RenderOptions(width=width, format=fmt), b"broken avatar", + ) + with Image.open(BytesIO(result.content)) as image: + self.assertEqual(image.width, width) + self.assertEqual(image.format, fmt) + self.assertEqual(result.width, width) + self.assertGreater(result.compose_ms, 0) + + def test_stage_boundaries_multiseason_and_withered(self): + plant = self.plant + plot = Plot(1, plant, plant.growth_seconds) + view = lambda p, now: self.renderer.scene( + self.who, snapshot([p]), now, + ).plots[0] + self.assertEqual(view(plot, 0).state, "growing") + self.assertEqual(view(plot, plant.growth_seconds).state, "ripe") + self.assertNotEqual(view(plot, 0).sprite, view(plot, 20000).sprite) + multi = replace(plant, crop=3) + again = Plot(1, multi, multi.regrowth_seconds + 100, 1) + self.assertEqual(view(again, 100).stage, 0) + self.assertGreater(view(again, again.mature_at - 1).stage, 0) + dead = replace(again, harvest_count=3) + self.assertEqual(view(dead, 999999).state, "withered") + self.assertEqual(view(dead, 999999).sprite, "plant/basic/9.png") + + def test_result_cache_visible_state_scope_and_time(self): + plant = self.plant + data = snapshot([Plot(1, plant, plant.growth_seconds)]) + first = self.renderer.render(self.who, data, 0) + warm = self.renderer.render(self.who, data, 1) + self.assertFalse(first.cache_hit) + self.assertTrue(warm.cache_hit) + self.assertEqual(first.content, warm.content) + ripe = self.renderer.render(self.who, data, plant.growth_seconds) + self.assertFalse(ripe.cache_hit) + self.assertNotEqual(first.content, ripe.content) + changed = replace(data, account=replace(data.account, point=501)) + self.assertFalse(self.renderer.render( + self.who, changed, 0, + ).cache_hit) + other = replace(self.who, realm="other") + self.assertFalse(self.renderer.render(other, data, 0).cache_hit) + self.assertFalse(self.renderer.render( + self.who, data, 0, avatar=b"changed", + ).cache_hit) + + def test_lru_budget_and_shared_image_safety(self): + cache = ByteLRU(10) + cache.put("a", b"123456", 6) + cache.put("b", b"abcdef", 6) + self.assertIsNone(cache.get("a")) + self.assertLessEqual(cache.used_bytes, 10) + cache.put("huge", bytes(20), 20) + self.assertIsNone(cache.get("huge")) + image = self.renderer.assets.image("soil/草土地.png", (80, 50)) + before = image.tobytes() + image.paste("red", (0, 0, 80, 50)) + fresh = self.renderer.assets.image("soil/草土地.png", (80, 50)) + self.assertEqual(before, fresh.tobytes()) + self.assertLessEqual( + self.renderer.assets.cache.used_bytes, + self.renderer.assets.cache.limit, + ) + + def test_auto_codec_fallback_and_invalid_options(self): + with patch("farm_render.models.features.check", return_value=False): + self.assertEqual(RenderOptions().codec(("WEBP", "JPEG")), "JPEG") + with self.assertRaises(ValueError): + RenderOptions(format="WEBP").codec(("WEBP",)) + for kwargs in ({"width":True}, {"width":99999}, {"quality":101}, + {"method":7}, {"theme":"../oops"}): + with self.assertRaises(ValueError): + RenderOptions(**kwargs) + + def test_first_stage_exact_boundary_and_target_canvas(self): + plot = Plot(1, self.plant, self.plant.growth_seconds) + metadata = self.renderer.assets.metadata["plants"][self.plant.name] + threshold = int(metadata["phase"].split(",")[0]) + data = snapshot([plot]) + self.assertEqual(self.renderer.scene( + self.who, data, threshold - 1, + ).plots[0].stage, 0) + self.assertEqual(self.renderer.scene( + self.who, data, threshold, + ).plots[0].stage, 1) + widths = [] + original = Canvas.__init__ + + def capture(canvas, image): + widths.append(image.width) + original(canvas, image) + + with patch.object(Canvas, "__init__", capture): + self.renderer.render(self.who, data, 0) + self.assertTrue(widths) + self.assertEqual(set(widths), {1024}) + + def test_missing_sprite_and_private_logs(self): + metadata = self.renderer.assets.metadata + plant = next(p for p in Catalogue.bundled() if ( + f'plant/{p.name}/1.png' not in metadata["images"] + )) + data = snapshot([Plot(1, plant, 0)], name="PRIVATE FARM") + with self.assertLogs("farm_render.renderer", "INFO") as recorded: + result = self.renderer.render(self.who, data, 0) + self.assertTrue(result.content) + logs = " ".join(recorded.output) + self.assertIn("missing packaged crop sprite", logs) + self.assertNotIn("PRIVATE FARM", logs) + self.assertNotIn(self.who.key, logs) + + def test_oversized_avatar_is_not_decoded(self): + content = BytesIO() + Image.new("RGB", (3000, 3000), "white").save(content, "PNG") + result = self.renderer.render( + self.who, snapshot(), 0, avatar=content.getvalue(), + ) + self.assertTrue(result.content) + with self.assertRaises(ValueError): + FarmRenderer(Path("missing-font.ttf")) + + +class AsyncRenderTests(unittest.IsolatedAsyncioTestCase): + async def asyncSetUp(self): + self.assertIsNotNone(FarmRenderer, "异步渲染模块尚未实现") + self.renderer = FarmRenderer(FONT) + self.who = Identity("test", "local", "qq", "async") + + async def test_snapshot_once_and_loop_not_blocked(self): + class Source: + calls = 0 + + async def snapshot(inner, who): + inner.calls += 1 + return snapshot() + + source = Source() + service = RenderService(self.renderer) + original = self.renderer.render + reached = threading.Event() + + def slow(*args, **kwargs): + reached.set() + time.sleep(0.1) + return original(*args, **kwargs) + + self.renderer.render = slow + task = asyncio.create_task(service.my_farm(source, self.who, now=0)) + await asyncio.to_thread(reached.wait, 2) + self.assertFalse(task.done()) + self.assertTrue((await task).content) + self.assertEqual(source.calls, 1) + await service.close() + + async def test_singleflight_cancel_capacity_and_close(self): + service = RenderService(self.renderer, concurrency=1, max_pending=1) + entered = threading.Event() + release = threading.Event() + original = self.renderer.render + calls = [] + + def slow(*args, **kwargs): + calls.append(1) + entered.set() + release.wait(3) + return original(*args, **kwargs) + + self.renderer.render = slow + data = snapshot() + first = asyncio.create_task(service.render(self.who, data, 0)) + await asyncio.to_thread(entered.wait, 2) + same = asyncio.create_task(service.render(self.who, data, 0)) + await asyncio.sleep(0) + first.cancel() + with self.assertRaises(asyncio.CancelledError): + await first + with self.assertRaises(RuntimeError): + await service.render(replace(self.who, user_id="two"), data, 0) + closing = asyncio.create_task(service.close()) + await asyncio.sleep(0.02) + self.assertFalse(closing.done()) + release.set() + self.assertTrue((await same).content) + await closing + self.assertEqual(len(calls), 1) + with self.assertRaises(RuntimeError): + await service.render(self.who, data, 0) + + async def test_failure_frees_capacity_for_retry(self): + service = RenderService(self.renderer) + original = self.renderer.render + with patch.object(self.renderer, "render", side_effect=ValueError): + with self.assertRaises(ValueError): + await service.render(self.who, snapshot(), 0) + self.renderer.render = original + self.assertTrue((await service.render( + self.who, snapshot(), 0, + )).content) + await service.close() + + async def test_limit_bounds_distinct_parallel_jobs(self): + service = RenderService(self.renderer, concurrency=2, max_pending=4) + lock = threading.Lock() + active = 0 + maximum = 0 + original = self.renderer.render + + def observed(*args, **kwargs): + nonlocal active, maximum + with lock: + active += 1 + maximum = max(maximum, active) + try: + time.sleep(0.03) + return original(*args, **kwargs) + finally: + with lock: + active -= 1 + + self.renderer.render = observed + await asyncio.gather(*( + service.render(replace(self.who, user_id=str(i)), snapshot(), 0) + for i in range(4) + )) + self.assertEqual(maximum, 2) + self.assertEqual(active, 0) + await service.close() diff --git a/tests/test_transactions.py b/tests/test_transactions.py new file mode 100644 index 0000000..eb433c5 --- /dev/null +++ b/tests/test_transactions.py @@ -0,0 +1,400 @@ +import asyncio +from contextlib import closing +from dataclasses import replace +from pathlib import Path +import sqlite3 +import tempfile +import threading +import unittest +from unittest.mock import patch + +from farm_core import Asset, Catalogue, FarmError, FarmService, Identity +from farm_core import SQLiteStore +from farm_core.domain import MAX_QUANTITY, Plant, SchemaError, StorageError +from farm_core.presentation import receipt_text, snapshot_text +from farm_core.repository import SQLiteRepository + + +def catalogue(): + return Catalogue([ + Plant(1, "萝卜", 0, 10, False, 0, 20, 2, 10, 2, 1, 1), + Plant(2, "番茄", 0, 5, False, 0, 30, 3, 7, 3, 2, 2), + Plant(3, "点券菜", 0, 999, True, 4, 8, 2, 3, 2, 1, 1), + Plant(4, "高级菜", 2, 10, False, 0, 20, 2, 10, 2, 1, 1), + ]) + + +class TransactionTests(unittest.IsolatedAsyncioTestCase): + async def asyncSetUp(self): + self.temporary = tempfile.TemporaryDirectory(prefix="farm-core-test-") + self.path = Path(self.temporary.name) / "farm.sqlite3" + self.store = SQLiteStore(self.path) + await self.store.initialize() + self.now = 1000 + self.service = FarmService( + self.store, catalogue(), lambda: self.now, + ) + self.identity = Identity("deployment", "realm", "qq", "user") + await self.service.register(self.identity, "测试农场", "register") + + async def asyncTearDown(self): + await self.store.close() + temporary_path = Path(self.temporary.name).resolve() + self.assertTrue(temporary_path.is_relative_to( + Path(tempfile.gettempdir()).resolve() + )) + self.temporary.cleanup() + + async def seed(self, action, callback): + return await self.store.transact( + self.identity, action, "test-fixture", {}, self.now, callback, + ) + + async def mature_radish(self, count=1): + await self.service.buy(self.identity, "萝卜", count, "buy") + await self.service.sow(self.identity, "萝卜", count, "sow") + self.now += 2 + return await self.service.harvest(self.identity, "harvest") + + async def test_registration_is_immediately_visible_and_not_repeated(self): + snapshot = await self.service.snapshot(self.identity) + self.assertEqual(snapshot.account.point, 500) + self.assertEqual(snapshot.account.vip_point, 0) + self.assertEqual(snapshot.account.experience, 0) + self.assertEqual(len(snapshot.plots), 3) + self.assertTrue(all(p.plant is None for p in snapshot.plots)) + repeated = await self.service.register( + self.identity, "另一个名称", "register-again", + ) + self.assertFalse(repeated.data["created"]) + self.assertEqual(repeated.data["account"]["point"], 500) + self.assertEqual(len(await self.store.ledger(self.identity)), 1) + + async def test_scopes_are_independent(self): + other = replace(self.identity, realm="different") + with self.assertRaisesRegex(FarmError, "not_registered"): + await self.service.snapshot(other) + await self.service.register(other, "另一农场", "register") + await self.service.buy(self.identity, "萝卜", 1, "buy") + self.assertEqual( + (await self.service.snapshot(other)).account.point, 500, + ) + + async def test_full_purchase_to_sale_round_trip(self): + harvest = await self.mature_radish() + self.assertEqual(harvest.data["experience"], 20) + sale = await self.service.sell(self.identity, None, None, "sell") + self.assertEqual(sale.data["proceeds"], 20) + self.assertEqual(sale.data["account"]["point"], 510) + self.assertEqual(sale.data["account"]["experience"], 20) + snapshot = await self.service.snapshot(self.identity) + self.assertEqual(snapshot.inventory[0].seeds, 0) + self.assertEqual(snapshot.inventory[0].crops, 0) + self.assertTrue(snapshot.plots[0].withered) + self.assertIn("本次收入:20", receipt_text(sale)) + self.assertIn("等级:", snapshot_text(snapshot)) + await self.service.eradicate(self.identity, "eradicate") + self.assertTrue(all( + p.plant is None + for p in (await self.service.snapshot(self.identity)).plots + )) + + async def test_existing_balance_and_experience_are_not_added_twice(self): + await self.seed("existing-exp", lambda repo: ( + repo.asset(Asset.EXPERIENCE, 100) or {} + )) + await self.mature_radish() + self.assertEqual( + (await self.service.snapshot(self.identity)).account.experience, + 120, + ) + await self.seed("balance-100", lambda repo: ( + repo.asset(Asset.POINT, -390) or {} + )) + sale = await self.service.sell(self.identity, "萝卜", None, "sell") + self.assertEqual(sale.data["account"]["point"], 120) + + async def test_invalid_inputs_do_not_change_assets(self): + before = await self.service.snapshot(self.identity) + for count in (0, -1, True, False, 1.5, MAX_QUANTITY + 1, 10**30): + with self.subTest(count=count): + with self.assertRaises(FarmError): + await self.service.buy( + self.identity, "萝卜", count, "invalid", + ) + with self.assertRaises(FarmError): + await self.service.sow( + self.identity, "萝卜", count, "invalid", + ) + with self.assertRaises(FarmError): + await self.service.sell( + self.identity, "萝卜", count, "invalid", + ) + self.assertEqual(await self.service.snapshot(self.identity), before) + + async def test_unaffordable_unknown_and_high_level_purchase_fail(self): + for name, count, code in ( + ("萝卜", 51, "insufficient_funds"), + ("高级菜", 1, "level_required"), + ("不存在", 1, "unknown_plant"), + ): + with self.subTest(name=name): + with self.assertRaisesRegex(FarmError, code): + await self.service.buy( + self.identity, name, count, "denied", + ) + snapshot = await self.service.snapshot(self.identity) + self.assertEqual(snapshot.account.point, 500) + self.assertEqual(snapshot.inventory, ()) + + async def test_vip_purchase_only_uses_vip_currency(self): + with self.assertRaisesRegex(FarmError, "insufficient_funds"): + await self.service.buy(self.identity, "点券菜", 1, "vip") + await self.seed("vip-fixture", lambda repo: ( + repo.asset(Asset.VIP_POINT, 10) or {} + )) + result = await self.service.buy(self.identity, "点券菜", 1, "vip") + self.assertEqual(result.data["account"]["point"], 500) + self.assertEqual(result.data["account"]["vip_point"], 6) + + async def test_sow_all_and_explicit_overflow(self): + await self.service.buy(self.identity, "萝卜", 5, "buy") + before = await self.service.snapshot(self.identity) + with self.assertRaisesRegex(FarmError, "insufficient_empty_plots"): + await self.service.sow(self.identity, "萝卜", 4, "too-many") + self.assertEqual(await self.service.snapshot(self.identity), before) + result = await self.service.sow(self.identity, "萝卜", None, "all") + self.assertEqual(result.data["quantity"], 3) + self.assertEqual(result.data["seeds"], 2) + + async def test_maturity_boundary_and_multi_crop_lifecycle(self): + await self.service.buy(self.identity, "番茄", 1, "buy") + await self.service.sow(self.identity, "番茄", 1, "sow") + self.now += 2 + with self.assertRaisesRegex(FarmError, "nothing_to_harvest"): + await self.service.harvest(self.identity, "early") + with self.assertRaisesRegex(FarmError, "nothing_to_eradicate"): + await self.service.eradicate(self.identity, "early-clear") + self.now += 1 + await self.service.harvest(self.identity, "first") + snapshot = await self.service.snapshot(self.identity) + self.assertEqual(snapshot.plots[0].mature_at, self.now + 2) + self.assertFalse(snapshot.plots[0].withered) + self.now += 2 + await self.service.harvest(self.identity, "second") + snapshot = await self.service.snapshot(self.identity) + self.assertTrue(snapshot.plots[0].withered) + self.assertEqual(snapshot.inventory[0].crops, 6) + self.assertEqual(snapshot.account.experience, 60) + with self.assertRaisesRegex(FarmError, "nothing_to_harvest"): + await self.service.harvest(self.identity, "third") + + async def test_planted_rules_are_frozen(self): + await self.service.buy(self.identity, "萝卜", 1, "buy") + await self.service.sow(self.identity, "萝卜", 1, "sow") + changed = replace( + self.service.catalogue.get("萝卜"), harvest=999, experience=999, + growth_seconds=100, crop=5, + ) + self.service.catalogue = Catalogue([changed]) + self.now += 2 + result = await self.service.harvest(self.identity, "harvest") + self.assertEqual(result.data["quantity"], 2) + self.assertEqual(result.data["experience"], 20) + self.assertTrue( + (await self.service.snapshot(self.identity)).plots[0].withered + ) + + async def test_per_plot_bonuses_are_independent_of_iteration_order(self): + await self.service.buy(self.identity, "萝卜", 2, "buy") + await self.service.sow(self.identity, "萝卜", 2, "sow") + + def bonuses(repo): + plots = repo.plots() + repo.save_plot(replace(plots[0], experience_bonus=100)) + repo.save_plot(replace(plots[1], experience_bonus=50)) + return {} + + await self.seed("bonuses", bonuses) + self.now += 2 + original = SQLiteRepository.plots + with patch.object( + SQLiteRepository, "plots", + lambda repo: tuple(reversed(original(repo))), + ): + result = await self.service.harvest(self.identity, "harvest") + self.assertEqual(result.data["experience"], 70) + + async def test_locks_protect_both_sale_paths_and_survive_restart(self): + await self.mature_radish() + await self.service.lock(self.identity, "萝卜", True, "lock") + for name, code in ( + ("萝卜", "plant_locked"), (None, "nothing_to_sell"), + ): + with self.assertRaisesRegex(FarmError, code): + await self.service.sell(self.identity, name, None, "locked") + await self.store.close() + await self.store.initialize() + self.assertTrue( + (await self.service.snapshot(self.identity)).inventory[0].locked + ) + await self.service.lock(self.identity, "萝卜", False, "unlock") + result = await self.service.sell(self.identity, None, None, "sale") + self.assertEqual(result.data["proceeds"], 20) + + async def test_failed_purchase_rolls_back_money_stock_and_ledger(self): + before = await self.service.snapshot(self.identity) + ledger = await self.store.ledger(self.identity) + with patch.object( + SQLiteRepository, "stock", + side_effect=sqlite3.OperationalError("injected"), + ): + with self.assertRaises(StorageError): + await self.service.buy(self.identity, "萝卜", 1, "buy") + self.assertEqual(await self.service.snapshot(self.identity), before) + self.assertEqual(await self.store.ledger(self.identity), ledger) + await self.service.buy(self.identity, "萝卜", 1, "buy") + self.assertEqual( + (await self.service.snapshot(self.identity)).account.point, 490, + ) + + async def test_failed_harvest_rolls_back_crop_and_plot(self): + await self.service.buy(self.identity, "萝卜", 1, "buy") + await self.service.sow(self.identity, "萝卜", 1, "sow") + self.now += 2 + before = await self.service.snapshot(self.identity) + original = SQLiteRepository.asset + + def fail_exp(repo, asset, delta): + if asset == Asset.EXPERIENCE: + raise sqlite3.OperationalError("injected") + original(repo, asset, delta) + + with patch.object(SQLiteRepository, "asset", fail_exp): + with self.assertRaises(StorageError): + await self.service.harvest(self.identity, "harvest") + self.assertEqual(await self.service.snapshot(self.identity), before) + result = await self.service.harvest(self.identity, "harvest") + self.assertEqual(result.data["quantity"], 2) + + async def test_failed_sale_rolls_back_inventory(self): + await self.mature_radish() + before = await self.service.snapshot(self.identity) + with patch.object( + SQLiteRepository, "asset", + side_effect=sqlite3.OperationalError("injected"), + ): + with self.assertRaises(StorageError): + await self.service.sell(self.identity, None, None, "sale") + self.assertEqual(await self.service.snapshot(self.identity), before) + + async def test_replay_and_parameter_conflict(self): + first = await self.service.buy(self.identity, "萝卜", 1, "buy") + self.now += 100 + second = await self.service.buy(self.identity, "萝卜", 1, "buy") + self.assertEqual(first, second) + with self.assertRaisesRegex(FarmError, "operation_conflict"): + await self.service.buy(self.identity, "萝卜", 2, "buy") + with self.assertRaisesRegex(FarmError, "operation_conflict"): + await self.service.sow(self.identity, "萝卜", 1, "buy") + snapshot = await self.service.snapshot(self.identity) + self.assertEqual(snapshot.account.point, 490) + self.assertEqual(snapshot.inventory[0].seeds, 1) + + async def test_same_operation_concurrently_commits_once(self): + results = await asyncio.gather(*( + self.service.buy(self.identity, "萝卜", 1, "same") + for _ in range(20) + )) + self.assertTrue(all(result == results[0] for result in results)) + snapshot = await self.service.snapshot(self.identity) + self.assertEqual(snapshot.account.point, 490) + self.assertEqual(snapshot.inventory[0].seeds, 1) + self.assertEqual(len(await self.store.ledger(self.identity)), 2) + + async def test_concurrent_buyers_cannot_overspend(self): + results = await asyncio.gather(*( + self.service.buy(self.identity, "萝卜", 1, f"buy-{index}") + for index in range(60) + ), return_exceptions=True) + self.assertEqual(sum(not isinstance(r, Exception) for r in results), 50) + failures = [r for r in results if isinstance(r, Exception)] + self.assertTrue(all( + isinstance(r, FarmError) and r.code == "insufficient_funds" + for r in failures + )) + snapshot = await self.service.snapshot(self.identity) + self.assertEqual(snapshot.account.point, 0) + self.assertEqual(snapshot.inventory[0].seeds, 50) + + async def test_concurrent_harvest_cannot_claim_same_season_twice(self): + await self.service.buy(self.identity, "萝卜", 1, "buy") + await self.service.sow(self.identity, "萝卜", 1, "sow") + self.now += 2 + results = await asyncio.gather(*( + self.service.harvest(self.identity, f"harvest-{index}") + for index in range(10) + ), return_exceptions=True) + self.assertEqual(sum(not isinstance(r, Exception) for r in results), 1) + snapshot = await self.service.snapshot(self.identity) + self.assertEqual(snapshot.inventory[0].crops, 2) + self.assertEqual(snapshot.account.experience, 20) + + async def test_restart_and_cancelled_request_can_replay_result(self): + entered = threading.Event() + release = threading.Event() + original = SQLiteRepository.stock + + def blocked(repo, *args, **kwargs): + entered.set() + if not release.wait(5): + raise RuntimeError("Test worker was not released") + original(repo, *args, **kwargs) + + with patch.object(SQLiteRepository, "stock", blocked): + task = asyncio.create_task( + self.service.buy(self.identity, "萝卜", 1, "cancelled-buy") + ) + try: + self.assertTrue(await asyncio.to_thread(entered.wait, 3)) + task.cancel() + with self.assertRaises(asyncio.CancelledError): + await task + finally: + release.set() + await self.store.close() + with self.assertRaises(StorageError): + await self.service.snapshot(self.identity) + await self.store.initialize() + result = await self.service.buy( + self.identity, "萝卜", 1, "cancelled-buy", + ) + self.assertEqual(result.data["account"]["point"], 490) + self.assertEqual( + (await self.service.snapshot(self.identity)).inventory[0].seeds, 1, + ) + + async def test_schema_version_is_not_silently_adopted(self): + await self.store.close() + with closing(sqlite3.connect(self.path)) as connection: + connection.execute("PRAGMA user_version=99") + with self.assertRaises(SchemaError): + await self.store.initialize() + + async def test_foreign_database_is_rejected_without_conversion(self): + path = Path(self.temporary.name) / "old.sqlite3" + with closing(sqlite3.connect(path)) as connection: + connection.execute("CREATE TABLE user(uid TEXT)") + other = SQLiteStore(path) + with self.assertRaises(SchemaError): + await other.initialize() + with closing(sqlite3.connect(path)) as connection: + self.assertEqual( + connection.execute( + "SELECT name FROM sqlite_master WHERE type='table'" + ).fetchall(), [("user",)], + ) + + +if __name__ == "__main__": + unittest.main()