diff --git a/.gitignore b/.gitignore
index 5cc057a..2e41e79 100644
--- a/.gitignore
+++ b/.gitignore
@@ -16,6 +16,9 @@ ENV/
/build/
/dist/
/wheelhouse/
+*.whl
+pip-delete-this-directory.txt
+installed-files.txt
*.egg-info/
.eggs/
*.egg
@@ -72,6 +75,8 @@ Desktop.ini
/uploads/
/artifacts/
/.local/
+/.cache/
+/incoming/
**/farm_db/
**/farm_res/
*.log
diff --git a/docs/10-rendering.md b/docs/10-rendering.md
index 176841a..91e252e 100644
--- a/docs/10-rendering.md
+++ b/docs/10-rendering.md
@@ -74,6 +74,10 @@ now, options),统一 now 使用UTC秒整数。正常未注册/存储故障仍
## 阶段与缓存
+四季建筑与背景已接入:用户发送“我的农场”时按北京时间月份选图,
+蜂巢主题仅生成离线预览。建筑定位、缺图回退和生成方式见
+`17-season-backgrounds.md`。
+
布局使用旧30块地坐标。新播种及新一季使用虫害工作量计算阶段,
未启用虫害的迁移前当前季仍从成熟时间和基础时长推导。
枯萎优先于成熟。作物采用完整画布底边中点统一公式,
diff --git a/docs/11-zhenxun-preview.md b/docs/11-zhenxun-preview.md
index 266ff6a..37a3f67 100644
--- a/docs/11-zhenxun-preview.md
+++ b/docs/11-zhenxun-preview.md
@@ -1,124 +1,140 @@
-# 真寻农场预览版
+
-本文件随生成包分发;业务源码只在主仓库main_plugin_farm维护。
-生成目标为Shu-Ying/zhenxun_plugin_farm的farm-preview分支,保留main。
+# 真寻农场(zhenxun_plugin_farm)
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-preview.3恢复旧版信息底板和头像框、昵称、经验进度、等级及两种货币。
-按用户确认校正原素材上的字段错位,保留旧版经验计算口径;点券显示
-真实余额。仍使用目标尺寸绘制、一次快照查询及有界缓存。
+
-## 安装前
+[](https://qm.qq.com/q/7hsOD4rOw2)
-此版沿用“开通农场”“我的农场”等原指令,没有测试前缀。请先停止
-机器人,并停用旧农场插件,避免同名指令被旧版抢先处理。仅在菜单
-禁用旧版不一定阻止其导入;建议把旧插件目录备份到插件扫描目录之外。
-安装脚本不会自动禁用、删除或覆盖旧版。
+
-预览数据默认位于真寻DATA_PATH/farm_preview/farm-preview.sqlite3。
-不读取旧版数据库,不迁移真实玩家资产;同平台、同机器人内共享农场,
-不同机器人/平台分开。不要将预览数据路径指向旧版数据库。
+你是说可以种地对吧🤔?
-## 主仓库本地生成
+
-```text
-python scripts/build_zhenxun.py --output dist/zhenxun-preview
-python scripts/verify_zhenxun.py dist/zhenxun-preview/zhenxun_plugin_farm_preview-0.1.0-preview.1.zip
-python scripts/install_zhenxun.py dist/zhenxun-preview/zhenxun_plugin_farm_preview-0.1.0-preview.1.zip --plugins-dir /path/to/zhenxun_bot/zhenxun/plugins
-```
+---
+## 目录
+- [真寻农场(zhenxun\_plugin\_farm)](#真寻农场zhenxun_plugin_farm)
+ - [目录](#目录)
+ - [农场界面](#农场界面)
+ - [如何安装](#如何安装)
+ - [使用指令](#使用指令)
+ - [更新日志(详细):](#更新日志详细)
+ - [用户方面](#用户方面)
+ - [代码方面](#代码方面)
+ - [待办事宜 `Todo` 列表](#待办事宜-todo-列表)
+ - [关于](#关于)
+ - [致谢](#致谢)
+ - [许可证](#许可证)
+ - [上游仓库](#上游仓库)
-必须使用真寻的Python环境安装包内requirements.txt;该依赖约束为
-Pillow>=10.4,<11,不静默升级宿主到12。NoneBot、真寻和Uninfo由
-现有宿主提供,不把通用NoneBot适配器或AstrBot代码一起分发。
+---
-如果安装目录已存在,脚本会拒绝覆盖。更新前停机器人、备份插件目录,
-手动移走旧预览目录后再安装;数据目录位于插件外,不随插件删除。
+## 农场界面
-## 配置与指令
+
-默认优先使用真寻FONT_PATH目录下已有的中文字体,依次尝试
-HYWenHei-85W.ttf、msyh.ttf、syht.otf、syst.otf、
-AlibabaHealthFont2.0CN-85B.ttf;文件损坏会继续尝试下一个。
-宿主目录均不可用时,再尝试Windows微软雅黑/宋体或Linux常用
-Noto Sans CJK/文泉驿路径。不会下载或打包额外字体。
+---
-上述默认路径仅属于真寻适配层,不作为其他机器人宿主的默认配置。
-NoneBot/AstrBot接入时分别解析各自字体资源;共享farm_render只接收
-最终文件路径,不依赖真寻FONT_PATH或固定resources/font目录。
+## 如何安装
-通常不必额外配置;需要覆盖默认字体时,在真寻使用的.env指定路径:
+方法一(推荐):在小真寻后台的插件商店下载即可
+方法二:下载源码放在小真寻`plugin`目录下
-```dotenv
-FARM_PREVIEW_FONT=C:/Windows/Fonts/msyh.ttc
-FARM_PREVIEW_DEPLOYMENT=zhenxun-preview
-FARM_PREVIEW_IMAGE_FORMAT=AUTO
-```
+---
-Linux请提供合法可用的中文字体。显式配置优先,配置错误时不会静默
-替换成其他字体。所有默认候选都不可用时,文本交易可用,图片命令
-会提示配置;不捆绑Windows字体。可选FARM_PREVIEW_DATA_DIR显式设置
-独立预览数据目录。AUTO默认JPEG以保守兼容真实平台,确认WebP支持后
-可设WEBP,也支持JPEG/PNG。
+## 使用指令
-测试顺序(是否需要/前缀遵循宿主COMMAND_START配置):
+| 指令 | 描述 | Tip |
+| --- | --- | --- |
+| @小真寻 开通农场 | 首次开通农场 | |
+| 我的农场 | 你的农场 | |
+| 农场详述 | 农场详细信息 | |
+| 我的农场币 | 查询农场币 | |
+| 种子商店 [筛选关键字] [页数] or [页数] | 查看种子商店 | 当第一个参数为非整数时,会默认进入筛选状态。页数不填默认为1 |
+| 购买种子 [种子名称] [数量] | 购买种子 | 数量不填默认为1 |
+| 我的种子 | 查询仓库种子 | |
+| 播种 [种子名称] [数量] | 播种种子 | 数量不填默认将最大可能播种 |
+| 收获 | 收获成熟作物 | |
+| 铲除 | 铲除荒废作物 | |
+| 我的作物 | 你的作物 | |
+| 出售作物 [作物名称] [数量] | 从仓库里向系统售卖作物 | 不填写作物名将售卖仓库种全部作物 填作物名不填数量将指定作物全部出售 |
+| 偷菜 @美波理 | 偷别人的菜 | 每人每天只能偷5次 |
+| 购买农场币 | 将真寻金币兑换成农场币 | 兑换比例默认为1:2 手续费默认20% |
+| 更改农场名 [新的农场名] | 改名 | 农场名称无法存储特殊字符 |
+| 农场签到 | 签到 | 需要注意,该项会从服务器拉取签到数据 |
+| 土地升级 [地块ID] | 将土地升级,带来收益提升 | 如果土地升级时,土地有播种作物,那么将直接成熟 |
-```text
-农场帮助
-开通农场
-我的农场
-种子商店
-购买种子 白萝卜 1
-我的种子
-播种 白萝卜 1
-我的农场 高清
-农场详述
-收获
-```
+---
-作物未成熟时收获会明确失败,不提供管理员快进命令或加币后门。
-其他入口:铲除、我的作物、我的农场币、我的点券、作物加锁 作物、
-作物解锁 作物、出售作物 作物 数量;不带参数出售全部未锁定产物。
-也可发送“我的农场 buy-seed 白萝卜 1”等旧子命令(支持范围见帮助)。
-不实现签到、偷菜、充值、兑换、升级和旧数据迁移,不能替代旧版全部功能。
+## 更新日志[(详细)](./log/log.md):
+用户方面
+---
+- 新增土地升级指令,快来将土地升级至红土地、黑土地、金土地吧
+- 新增查漏补缺作物资源功能,自动更新最新作物和作物资源文件
+- 定时更新签到文件、作物资源从00:30调整至04:30
+- 修正了部分土地资源错误的情况
+- 修正了部分文本信息错误的情况
-重复事件通过稳定消息标识幂等;无法获取消息标识时写操作拒绝。
-头像最多等待2秒,失败使用默认图;图片缓存与后台任务均有限额。
-关键日志写入数据目录farm-preview.log,同时沿用宿主日志传播配置;
-不记录真实玩家数据。
+代码方面
+---
+- 修正部分事件连接机制
+- 修正网络请求端口
-## GitHub工作流
+## 待办事宜 `Todo` 列表
-先由你提交主仓库代码后,主仓库的Zhenxun preview工作流会检查并上传
-Actions artifact。下载并解压附件,取其中插件ZIP安装;不需要令牌。
-Windows/Linux、Pillow10.4/当前支持版属于CI矩阵,不代表本机已跑Linux。
+- [x] 完善我的农场图片,例如左上角显示用户数据
+- [ ] 完善升级数据、作物数据、作物图片
+- [x] 签到功能
+- [x] 在线更新作物信息
+- [ ] 添加渔场功能
+- [ ] 增加活动、交易行功能
+- [ ] 增加交易行总行功能
+- [ ] 添加其他游戏种子素材
+- [ ] 想不到了,想到再说
-若还需将生成内容写入发布仓库,主仓库手动Run workflow时勾选publish,
-并配置受保护environment zhenxun-preview。该环境Secret名为
-FARM_RELEASE_TOKEN,仅授权目标仓库Contents写入;生成内容含工作流,
-相应令牌也须允许Workflows写入(GitHub App使用对应权限)。配置
-环境审批及仅允许main分支部署。不把Token填进脚本、URL或输入参数。
-脚本默认dry-run,只有--publish才访问GitHub;固定写farm-preview,
-不更新main、不force、不创建Release。第一次创建预览分支,之后普通
-提交更新。专用预览分支只放生成内容,不在那里手改业务或存储治理资料。
-已有预览分支必须带本项目来源清单;若它已成为默认分支则拒绝发布,
-并发期间分支发生变化也拒绝更新,避免覆盖其他提交。
-用ZIP安装最直接,不能假设插件管理器支持任意分支地址。
+---
-产物内FARM-MANIFEST.json记录版本、源提交、dirty和每文件SHA256;
-源有未提交修改会标dirty,禁止发布这种产物。本地测试允许dirty。
-哈希用于内容一致性,不是签名或来源身份保证,请使用你信任的附件。
+## 关于
-## 验证边界与回退
+本人毫无任何Python经验,也从未正式的、系统的、完整的去学习Python。如有看到写的不对的地方,欢迎指出,也欢迎任何人一起来开发、完善农场。
-本轮不会在真实机器人中安装/启动,也不会执行真实GitHub同步。真实
-登录、权限、适配器消息格式和发图需要你部署后验证,不能把离线模拟
-报告为线上成功。旧素材455个阶段引用缺图时显示问号,不会整图失败。
+素材来均源于互联网,侵权请联系我删除
-源码中ZIP时间、系统类型和文件权限固定;文本统一LF,以便跨平台
-复现。构建后repository目录是发布仓库根,外层插件ZIP用于本地安装;
-二者不要再叠套一层主仓库目录。
+---
-回退:停机器人,移走预览插件,恢复旧插件目录,再由你启动;旧版库
-没有被本版更改。保留预览数据备份,不尝试让旧版打开新SQLite schema。
+## 致谢
-官方依据:[Actions附件](https://docs.github.com/en/actions/tutorials/store-and-share-data)、
-[手动触发](https://docs.github.com/en/actions/how-tos/write-workflows/choose-when-workflows-run/trigger-a-workflow)。
+最后感谢以下框架/作者的提供的技术支持(排名不分先后):
+
+- [QQ农场](https://qqnc.qq.com/) * 绝大多数的素材来源 *
+- [Nonebot2](https://github.com/nonebot/nonebot2) *✨ 跨平台 Python 异步机器人框架 ✨*
+- [zhenxun_bot](https://github.com/zhenxun-org/zhenxun_bot) *最爱真寻的一集*
+- [HibiKier](https://github.com/HibiKier) *阿咪为什么是神*
+- [ATTomato](https://github.com/ATTomatoo) *流泪偷码头.jpg*
+
+## 许可证
+
+`真寻农场(zhenxun_plugin_farm)`将采用的是`GPLv3`许可证进行开源
+
+## 上游仓库
+
+- [Client](https://github.com/Shu-Ying/main_plugin_farm)
\ No newline at end of file
diff --git a/docs/17-season-backgrounds.md b/docs/17-season-backgrounds.md
new file mode 100644
index 0000000..d21ac31
--- /dev/null
+++ b/docs/17-season-backgrounds.md
@@ -0,0 +1,61 @@
+# 四季背景与蜂巢预览
+
+## 用户指令
+
+`我的农场` 和 `我的农场 高清` 根据请求时刻的北京时间自动选择:
+
+| 月份 | 主题 |
+| --- | --- |
+| 3~5 月 | 春季 spring |
+| 6~8 月 | 夏季 summer |
+| 9~11 月 | 秋季 autumn |
+| 12~2 月 | 冬季 winter |
+
+使用固定 UTC+8,不依赖宿主操作系统时区或额外时区数据包。
+接入层只解析一次主题和当前时间,并将同一时间传给渲染服务。
+主题参与底图缓存与成图缓存,跨季节不会命中上一季的图片。
+服务无需重启即可在下一次请求时切换季节;没有后台定时广播。
+
+按用户要求,蜂巢 bee 只生成离线预览,用户指令不支持蜂巢参数。
+底层显式 RenderOptions(theme="bee") 供离线绘图使用;四季规则不会返回 bee。
+
+## 建筑与定位
+
+统一位置保存在 `src/farm_render/data/background_layout.json`。
+以已完成对照的默认主题完整背景为参考,保留九个独立建筑及装饰。
+配置保留每项来源依据:房屋原始变换链;矿洞和入口包含实例化行为推断;
+其余为用户同意采用的截图反算位置,不能称为所有主题的官方原始配置。
+
+各主题替换对应 PNG,使用相同底边中点与宽度,按本主题图片原始比例
+计算高度。秋、冬房屋为 890×821,其余房屋为 890×981,底边保持一致,
+不把较矮的房屋强行拉高。完整透明画布不裁剪,狗窝不额外镜像。
+码头属于前景,在作物和虫子之后叠加,其余装饰绘入缓存底图。
+
+## 缺失素材的明确回退
+
+- 春、夏没有对应邮箱:使用默认邮箱。
+- 秋季没有对应邮箱、加工坊标牌:使用默认图片。
+- 五个主题没有单独的稻草人标牌:使用默认标牌。
+
+导出的 metadata.decorations 为每项记录 source_theme、fallback 和 basis。
+不丢失建筑,不用其他任意主题图片替代,也不修改旧参考素材。
+主题图片均来自本地已有资源;没有为这些图片增加生成式美术内容。
+许愿树、直升机等额外动态模块不包含在这九张装饰中。
+
+## 打包与生成预览
+
+`scripts/export_render_assets.py` 原样打包六套主题底图及现存独立 PNG,
+并生成各主题九项定位数据。`--check` 验证可复现输出。
+渲染器使用包内资源,不依赖旧目录或本机预览路径。
+
+```text
+python scripts/preview_seasons.py --output <输出目录>
+```
+
+输出五张 `background-主题.jpg` 完整底图、五张 `farm-主题-1536.png`
+完整农场样图,以及 `all-themes.jpg` 总览。蜂巢仅存在于这类预览中。
+底图预览为 3072×1872,农场样图为 1536×936。
+
+本轮验证包含北京时间月份边界、缺图回退、房屋比例和落地点、
+前景码头图层、主题缓存隔离、普通/高清 QQ 指令选图。
+未部署正在运行的机器人,也未验证真实 QQ 上传。
diff --git a/images/farm-preview.png b/images/farm-preview.png
new file mode 100644
index 0000000..53c5b09
Binary files /dev/null and b/images/farm-preview.png differ
diff --git a/integrations/zhenxun/__init__.py b/integrations/zhenxun/__init__.py
index b4808ee..4693637 100644
--- a/integrations/zhenxun/__init__.py
+++ b/integrations/zhenxun/__init__.py
@@ -7,7 +7,7 @@ from ._version import __version__
from .runtime import COMMANDS, HELP
__plugin_meta__ = PluginMetadata(
- name="农场预览", description="共享农场核心的真寻预览版",
+ name="真寻农场", description="快乐的农场时光",
usage=HELP, type="application",
homepage="https://github.com/Shu-Ying/zhenxun_plugin_farm",
supported_adapters={"~onebot.v11", "~qq"},
diff --git a/integrations/zhenxun/runtime.py b/integrations/zhenxun/runtime.py
index fdf78db..60a47a1 100644
--- a/integrations/zhenxun/runtime.py
+++ b/integrations/zhenxun/runtime.py
@@ -11,6 +11,7 @@ from ._vendor.farm_core.domain import FarmError
from ._vendor.farm_core.storage import SCHEMA_VERSION
from ._vendor.farm_core.presentation import receipt_text, snapshot_text
from ._vendor.farm_render import FarmRenderer, RenderOptions, RenderService
+from ._vendor.farm_render.seasons import season_for
logger = logging.getLogger(__name__)
COMMANDS = {
@@ -30,6 +31,7 @@ HELP = (
"作物可填写名称或 #编号;同名作物请使用商店或仓库中的编号。\n"
"每只虫使生长速度降低5%;成熟时每只虫使本季减产5%。\n"
"成熟后除虫不能恢复本季减产;生长期间可发送:农场除虫。\n"
+ "我的农场按北京时间自动切换春夏秋冬背景;高清使用相同季节。\n"
"本版使用独立预览数据库,不会读取旧版资产。"
)
ERRORS = {
@@ -182,11 +184,18 @@ class Runtime:
raise ValueError()
if not self.rendering:
return "请配置 FARM_PREVIEW_FONT 为可用中文字体路径。"
- options = (RenderOptions.high(format=self.image_format)
- if args else RenderOptions(format=self.image_format))
+ now = self.service.clock()
+ theme = season_for(now)
+ logger.info("farm preview seasonal theme=%s", theme)
+ options = (
+ RenderOptions.high(format=self.image_format, theme=theme)
+ if args else RenderOptions(format=self.image_format,
+ theme=theme)
+ )
picture = await self.rendering.my_farm(
self.service, who, options=options, avatar=avatar,
supported=("JPEG",),
+ now=now,
)
return picture.content
if op is None:
diff --git a/scripts/build_zhenxun.py b/scripts/build_zhenxun.py
index faac53f..a057e77 100644
--- a/scripts/build_zhenxun.py
+++ b/scripts/build_zhenxun.py
@@ -63,6 +63,7 @@ def build(root, output, version):
for name, source in {
"LICENSE": root / "LICENSE",
"README.md": root / "docs/11-zhenxun-preview.md",
+ "images/farm-preview.png": root / "images/farm-preview.png",
"verify_package.py": root / "scripts/verify_zhenxun.py",
".github/workflows/verify.yml": root / "release/zhenxun/verify.yml",
}.items():
diff --git a/scripts/export_render_assets.py b/scripts/export_render_assets.py
index 89eccf9..a978686 100644
--- a/scripts/export_render_assets.py
+++ b/scripts/export_render_assets.py
@@ -21,6 +21,41 @@ def apply_positions(root, metadata):
metadata["placement"] = document
+def apply_decorations(root, metadata):
+ document = json.loads((
+ root / "src/farm_render/data/background_layout.json"
+ ).read_bytes())
+ if (document["schema"] != 1 or document["canvas"] != metadata["canvas"]
+ or document["anchor"] != "full_canvas_bottom_center"):
+ raise ValueError("Unsupported background layout")
+ prefix = "background/decorateIcon/"
+ layouts = {}
+ for theme in ("default", "spring", "summer", "autumn", "winter", "bee"):
+ entries = []
+ for definition in document["buildings"]:
+ default = prefix + definition["sprite"]
+ sprite = default.replace("decorate_default_", f"decorate_{theme}_")
+ source_theme = theme
+ if sprite not in metadata["images"]:
+ sprite, source_theme = default, "default"
+ image = metadata["images"][sprite]
+ x, y, w, h = definition["rect"]
+ if w <= 0 or h <= 0:
+ raise ValueError("Invalid background rectangle")
+ height = w * image["size"][1] / image["size"][0]
+ if definition["layer"] not in ("background", "foreground"):
+ raise ValueError("Invalid decoration layer")
+ entries.append({
+ "name": definition["name"], "sprite": sprite,
+ "position": [x, y + h - height], "size": [w, height],
+ "layer": definition["layer"], "source_theme": source_theme,
+ "fallback": source_theme != theme,
+ "basis": definition["basis"],
+ })
+ layouts[theme] = entries
+ metadata["decorations"] = layouts
+
+
def export(root):
legacy = root / "legacy/zhenxun_plugin_farm"
source = legacy / "resource"
@@ -34,7 +69,8 @@ def export(root):
paths += [source / "background/frame/frame.png"]
paths += [source / (
f"background/decorateIcon/decorate_{theme}_1.jpg"
- ) for theme in ("default", "spring", "summer", "autumn", "winter")]
+ ) for theme in ("default", "spring", "summer", "autumn", "winter", "bee")]
+ paths += list((source / "background/decorateIcon").glob("decorate_*.png"))
layout = json.loads((legacy / "config/soil.json").read_bytes())
catalogue = json.loads((
root / "src/farm_core/data/plants.json"
@@ -72,6 +108,7 @@ def export(root):
item.external_attr = 0o100644 << 16
bundle.writestr(item, content)
apply_positions(root, metadata)
+ apply_decorations(root, metadata)
return output.getvalue(), (
json.dumps(metadata, ensure_ascii=False, sort_keys=True, indent=2)
+ "\n"
diff --git a/scripts/preview_seasons.py b/scripts/preview_seasons.py
new file mode 100644
index 0000000..8799d19
--- /dev/null
+++ b/scripts/preview_seasons.py
@@ -0,0 +1,59 @@
+"""离线生成四季与蜂巢底图,以及同一农场状态的主题对照。"""
+
+import argparse
+from pathlib import Path
+import sys
+
+from PIL import Image, ImageDraw, ImageFont
+
+ROOT = Path(__file__).resolve().parents[1]
+sys.path.insert(0, str(ROOT / "src"))
+
+from ci_font import choose_font
+from farm_core import Identity
+from farm_render import FarmRenderer, RenderOptions
+from farm_render.canvas import Canvas
+from render_demo import demonstration
+
+
+def main():
+ parser = argparse.ArgumentParser(description=__doc__)
+ parser.add_argument("--output", type=Path, required=True)
+ parser.add_argument("--font", type=Path)
+ args = parser.parse_args()
+ args.output.mkdir(parents=True, exist_ok=True)
+ renderer = FarmRenderer(args.font or choose_font())
+ who = Identity("preview", "local", "qq", "seasons")
+ snapshot, now = demonstration()
+ themes = (("spring", "春季"), ("summer", "夏季"), ("autumn", "秋季"),
+ ("winter", "冬季"), ("bee", "蜂巢 · 仅预览"))
+ sheet = Image.new("RGB", (1536, 3 * 510), "#edf0eb")
+ draw = ImageDraw.Draw(sheet)
+ font = ImageFont.truetype(str(renderer.font_path), 24)
+ for index, (theme, label) in enumerate(themes):
+ options = RenderOptions(width=3072, theme=theme)
+ image = renderer.assets.image(
+ f"background/decorateIcon/decorate_{theme}_1.jpg", (3072, 1872),
+ )
+ canvas = Canvas(image)
+ renderer._decorations(canvas, options, "background")
+ renderer._decorations(canvas, options, "foreground")
+ canvas.image.convert("RGB").save(
+ args.output / f"background-{theme}.jpg", quality=92,
+ )
+ thumb = canvas.image.convert("RGB").resize((768, 468),
+ Image.Resampling.LANCZOS)
+ x, y = (index % 2) * 768, (index // 2) * 510
+ draw.text((x+15, y+8), label, font=font, fill="#203e2c")
+ sheet.paste(thumb, (x, y+40))
+ result = renderer.render(
+ who, snapshot, now,
+ RenderOptions.high(theme=theme, format="PNG"),
+ )
+ (args.output / f"farm-{theme}-1536.png").write_bytes(result.content)
+ sheet.save(args.output / "all-themes.jpg", quality=92)
+ print(f"Generated 5 backgrounds, 5 farm previews: {args.output}")
+
+
+if __name__ == "__main__":
+ main()
diff --git a/scripts/publish_zhenxun.py b/scripts/publish_zhenxun.py
index 3065ae0..6e6b9db 100644
--- a/scripts/publish_zhenxun.py
+++ b/scripts/publish_zhenxun.py
@@ -10,7 +10,7 @@ from urllib.request import Request, urlopen
from verify_zhenxun import read_package, verify_files
REPOSITORY = "Shu-Ying/zhenxun_plugin_farm"
-BRANCH = "farm-preview"
+BRANCH = "main"
def github(method, path, payload, token, allow_missing=False):
diff --git a/src/farm_render/data/assets.zip b/src/farm_render/data/assets.zip
index 22dec8f..9f2e977 100644
Binary files a/src/farm_render/data/assets.zip and b/src/farm_render/data/assets.zip differ
diff --git a/src/farm_render/data/background_layout.json b/src/farm_render/data/background_layout.json
new file mode 100644
index 0000000..8acc86c
--- /dev/null
+++ b/src/farm_render/data/background_layout.json
@@ -0,0 +1,16 @@
+{
+ "schema": 1,
+ "canvas": [3072, 1872],
+ "anchor": "full_canvas_bottom_center",
+ "buildings": [
+ {"name": "房屋", "sprite": "decorate_default_2.png", "rect": [1675.75, 158.36, 572.14, 630.64], "layer": "background", "basis": "prefab parent and image transforms"},
+ {"name": "矿洞", "sprite": "decorate_default_3.png", "rect": [1385.61, 256.06, 282.88, 282.88], "layer": "background", "basis": "prefab geometry and inferred runtime origin"},
+ {"name": "主题入口", "sprite": "decorate_default_4.png", "rect": [860.44, 423.68, 294.97, 249.07], "layer": "background", "basis": "prefab geometry and inferred runtime origin"},
+ {"name": "摊位", "sprite": "decorate_default_5.png", "rect": [514.20, 553.27, 298.97, 288.52], "layer": "background", "basis": "approved screenshot inverse projection"},
+ {"name": "狗窝", "sprite": "decorate_default_6.png", "rect": [277.95, 762.35, 305.25, 206.98], "layer": "background", "basis": "approved screenshot inverse projection"},
+ {"name": "码头", "sprite": "decorate_default_7.png", "rect": [1074.51, 1019.51, 355.42, 397.24], "layer": "foreground", "basis": "approved screenshot inverse projection"},
+ {"name": "邮箱", "sprite": "decorate_default_8.png", "rect": [790.18, 576.27, 104.54, 140.08], "layer": "background", "basis": "approved screenshot inverse projection"},
+ {"name": "加工坊标牌", "sprite": "decorate_default_9.png", "rect": [2347.77, 756.08, 269.70, 269.70], "layer": "background", "basis": "approved screenshot inverse projection"},
+ {"name": "稻草人标牌", "sprite": "decorate_default_10.png", "rect": [1120.51, 300.30, 238.34, 238.34], "layer": "background", "basis": "approved screenshot inverse projection"}
+ ]
+}
diff --git a/src/farm_render/data/metadata.json b/src/farm_render/data/metadata.json
index c29b947..3bb0762 100644
--- a/src/farm_render/data/metadata.json
+++ b/src/farm_render/data/metadata.json
@@ -3,6 +3,884 @@
3072,
1872
],
+ "decorations": {
+ "autumn": [
+ {
+ "basis": "prefab parent and image transforms",
+ "fallback": false,
+ "layer": "background",
+ "name": "房屋",
+ "position": [
+ 1675.75,
+ 261.2169213483146
+ ],
+ "size": [
+ 572.14,
+ 527.7830786516854
+ ],
+ "source_theme": "autumn",
+ "sprite": "background/decorateIcon/decorate_autumn_2.png"
+ },
+ {
+ "basis": "prefab geometry and inferred runtime origin",
+ "fallback": false,
+ "layer": "background",
+ "name": "矿洞",
+ "position": [
+ 1385.61,
+ 256.06000000000006
+ ],
+ "size": [
+ 282.88,
+ 282.88
+ ],
+ "source_theme": "autumn",
+ "sprite": "background/decorateIcon/decorate_autumn_3.png"
+ },
+ {
+ "basis": "prefab geometry and inferred runtime origin",
+ "fallback": false,
+ "layer": "background",
+ "name": "主题入口",
+ "position": [
+ 860.44,
+ 423.6792219679634
+ ],
+ "size": [
+ 294.97,
+ 249.07077803203663
+ ],
+ "source_theme": "autumn",
+ "sprite": "background/decorateIcon/decorate_autumn_4.png"
+ },
+ {
+ "basis": "approved screenshot inverse projection",
+ "fallback": false,
+ "layer": "background",
+ "name": "摊位",
+ "position": [
+ 514.2,
+ 554.498515625
+ ],
+ "size": [
+ 298.97,
+ 287.29148437500004
+ ],
+ "source_theme": "autumn",
+ "sprite": "background/decorateIcon/decorate_autumn_5.png"
+ },
+ {
+ "basis": "approved screenshot inverse projection",
+ "fallback": false,
+ "layer": "background",
+ "name": "狗窝",
+ "position": [
+ 277.95,
+ 763.445234375
+ ],
+ "size": [
+ 305.25,
+ 205.884765625
+ ],
+ "source_theme": "autumn",
+ "sprite": "background/decorateIcon/decorate_autumn_6.png"
+ },
+ {
+ "basis": "approved screenshot inverse projection",
+ "fallback": false,
+ "layer": "foreground",
+ "name": "码头",
+ "position": [
+ 1074.51,
+ 1019.67921875
+ ],
+ "size": [
+ 355.42,
+ 397.07078125
+ ],
+ "source_theme": "autumn",
+ "sprite": "background/decorateIcon/decorate_autumn_7.png"
+ },
+ {
+ "basis": "approved screenshot inverse projection",
+ "fallback": true,
+ "layer": "background",
+ "name": "邮箱",
+ "position": [
+ 790.18,
+ 576.9633333333334
+ ],
+ "size": [
+ 104.54,
+ 139.38666666666666
+ ],
+ "source_theme": "default",
+ "sprite": "background/decorateIcon/decorate_default_8.png"
+ },
+ {
+ "basis": "approved screenshot inverse projection",
+ "fallback": true,
+ "layer": "background",
+ "name": "加工坊标牌",
+ "position": [
+ 2347.77,
+ 756.0799999999999
+ ],
+ "size": [
+ 269.7,
+ 269.7
+ ],
+ "source_theme": "default",
+ "sprite": "background/decorateIcon/decorate_default_9.png"
+ },
+ {
+ "basis": "approved screenshot inverse projection",
+ "fallback": true,
+ "layer": "background",
+ "name": "稻草人标牌",
+ "position": [
+ 1120.51,
+ 300.29999999999995
+ ],
+ "size": [
+ 238.34,
+ 238.34
+ ],
+ "source_theme": "default",
+ "sprite": "background/decorateIcon/decorate_default_10.png"
+ }
+ ],
+ "bee": [
+ {
+ "basis": "prefab parent and image transforms",
+ "fallback": false,
+ "layer": "background",
+ "name": "房屋",
+ "position": [
+ 1675.75,
+ 158.36029213483152
+ ],
+ "size": [
+ 572.14,
+ 630.6397078651685
+ ],
+ "source_theme": "bee",
+ "sprite": "background/decorateIcon/decorate_bee_2.png"
+ },
+ {
+ "basis": "prefab geometry and inferred runtime origin",
+ "fallback": false,
+ "layer": "background",
+ "name": "矿洞",
+ "position": [
+ 1385.61,
+ 256.06000000000006
+ ],
+ "size": [
+ 282.88,
+ 282.88
+ ],
+ "source_theme": "bee",
+ "sprite": "background/decorateIcon/decorate_bee_3.png"
+ },
+ {
+ "basis": "prefab geometry and inferred runtime origin",
+ "fallback": false,
+ "layer": "background",
+ "name": "主题入口",
+ "position": [
+ 860.44,
+ 423.6792219679634
+ ],
+ "size": [
+ 294.97,
+ 249.07077803203663
+ ],
+ "source_theme": "bee",
+ "sprite": "background/decorateIcon/decorate_bee_4.png"
+ },
+ {
+ "basis": "approved screenshot inverse projection",
+ "fallback": false,
+ "layer": "background",
+ "name": "摊位",
+ "position": [
+ 514.2,
+ 554.498515625
+ ],
+ "size": [
+ 298.97,
+ 287.29148437500004
+ ],
+ "source_theme": "bee",
+ "sprite": "background/decorateIcon/decorate_bee_5.png"
+ },
+ {
+ "basis": "approved screenshot inverse projection",
+ "fallback": false,
+ "layer": "background",
+ "name": "狗窝",
+ "position": [
+ 277.95,
+ 763.445234375
+ ],
+ "size": [
+ 305.25,
+ 205.884765625
+ ],
+ "source_theme": "bee",
+ "sprite": "background/decorateIcon/decorate_bee_6.png"
+ },
+ {
+ "basis": "approved screenshot inverse projection",
+ "fallback": false,
+ "layer": "foreground",
+ "name": "码头",
+ "position": [
+ 1074.51,
+ 1019.67921875
+ ],
+ "size": [
+ 355.42,
+ 397.07078125
+ ],
+ "source_theme": "bee",
+ "sprite": "background/decorateIcon/decorate_bee_7.png"
+ },
+ {
+ "basis": "approved screenshot inverse projection",
+ "fallback": false,
+ "layer": "background",
+ "name": "邮箱",
+ "position": [
+ 790.18,
+ 576.9633333333334
+ ],
+ "size": [
+ 104.54,
+ 139.38666666666666
+ ],
+ "source_theme": "bee",
+ "sprite": "background/decorateIcon/decorate_bee_8.png"
+ },
+ {
+ "basis": "approved screenshot inverse projection",
+ "fallback": false,
+ "layer": "background",
+ "name": "加工坊标牌",
+ "position": [
+ 2347.77,
+ 756.0799999999999
+ ],
+ "size": [
+ 269.7,
+ 269.7
+ ],
+ "source_theme": "bee",
+ "sprite": "background/decorateIcon/decorate_bee_9.png"
+ },
+ {
+ "basis": "approved screenshot inverse projection",
+ "fallback": true,
+ "layer": "background",
+ "name": "稻草人标牌",
+ "position": [
+ 1120.51,
+ 300.29999999999995
+ ],
+ "size": [
+ 238.34,
+ 238.34
+ ],
+ "source_theme": "default",
+ "sprite": "background/decorateIcon/decorate_default_10.png"
+ }
+ ],
+ "default": [
+ {
+ "basis": "prefab parent and image transforms",
+ "fallback": false,
+ "layer": "background",
+ "name": "房屋",
+ "position": [
+ 1675.75,
+ 158.36029213483152
+ ],
+ "size": [
+ 572.14,
+ 630.6397078651685
+ ],
+ "source_theme": "default",
+ "sprite": "background/decorateIcon/decorate_default_2.png"
+ },
+ {
+ "basis": "prefab geometry and inferred runtime origin",
+ "fallback": false,
+ "layer": "background",
+ "name": "矿洞",
+ "position": [
+ 1385.61,
+ 256.06000000000006
+ ],
+ "size": [
+ 282.88,
+ 282.88
+ ],
+ "source_theme": "default",
+ "sprite": "background/decorateIcon/decorate_default_3.png"
+ },
+ {
+ "basis": "prefab geometry and inferred runtime origin",
+ "fallback": false,
+ "layer": "background",
+ "name": "主题入口",
+ "position": [
+ 860.44,
+ 423.6792219679634
+ ],
+ "size": [
+ 294.97,
+ 249.07077803203663
+ ],
+ "source_theme": "default",
+ "sprite": "background/decorateIcon/decorate_default_4.png"
+ },
+ {
+ "basis": "approved screenshot inverse projection",
+ "fallback": false,
+ "layer": "background",
+ "name": "摊位",
+ "position": [
+ 514.2,
+ 554.498515625
+ ],
+ "size": [
+ 298.97,
+ 287.29148437500004
+ ],
+ "source_theme": "default",
+ "sprite": "background/decorateIcon/decorate_default_5.png"
+ },
+ {
+ "basis": "approved screenshot inverse projection",
+ "fallback": false,
+ "layer": "background",
+ "name": "狗窝",
+ "position": [
+ 277.95,
+ 763.445234375
+ ],
+ "size": [
+ 305.25,
+ 205.884765625
+ ],
+ "source_theme": "default",
+ "sprite": "background/decorateIcon/decorate_default_6.png"
+ },
+ {
+ "basis": "approved screenshot inverse projection",
+ "fallback": false,
+ "layer": "foreground",
+ "name": "码头",
+ "position": [
+ 1074.51,
+ 1019.67921875
+ ],
+ "size": [
+ 355.42,
+ 397.07078125
+ ],
+ "source_theme": "default",
+ "sprite": "background/decorateIcon/decorate_default_7.png"
+ },
+ {
+ "basis": "approved screenshot inverse projection",
+ "fallback": false,
+ "layer": "background",
+ "name": "邮箱",
+ "position": [
+ 790.18,
+ 576.9633333333334
+ ],
+ "size": [
+ 104.54,
+ 139.38666666666666
+ ],
+ "source_theme": "default",
+ "sprite": "background/decorateIcon/decorate_default_8.png"
+ },
+ {
+ "basis": "approved screenshot inverse projection",
+ "fallback": false,
+ "layer": "background",
+ "name": "加工坊标牌",
+ "position": [
+ 2347.77,
+ 756.0799999999999
+ ],
+ "size": [
+ 269.7,
+ 269.7
+ ],
+ "source_theme": "default",
+ "sprite": "background/decorateIcon/decorate_default_9.png"
+ },
+ {
+ "basis": "approved screenshot inverse projection",
+ "fallback": false,
+ "layer": "background",
+ "name": "稻草人标牌",
+ "position": [
+ 1120.51,
+ 300.29999999999995
+ ],
+ "size": [
+ 238.34,
+ 238.34
+ ],
+ "source_theme": "default",
+ "sprite": "background/decorateIcon/decorate_default_10.png"
+ }
+ ],
+ "spring": [
+ {
+ "basis": "prefab parent and image transforms",
+ "fallback": false,
+ "layer": "background",
+ "name": "房屋",
+ "position": [
+ 1675.75,
+ 158.36029213483152
+ ],
+ "size": [
+ 572.14,
+ 630.6397078651685
+ ],
+ "source_theme": "spring",
+ "sprite": "background/decorateIcon/decorate_spring_2.png"
+ },
+ {
+ "basis": "prefab geometry and inferred runtime origin",
+ "fallback": false,
+ "layer": "background",
+ "name": "矿洞",
+ "position": [
+ 1385.61,
+ 256.06000000000006
+ ],
+ "size": [
+ 282.88,
+ 282.88
+ ],
+ "source_theme": "spring",
+ "sprite": "background/decorateIcon/decorate_spring_3.png"
+ },
+ {
+ "basis": "prefab geometry and inferred runtime origin",
+ "fallback": false,
+ "layer": "background",
+ "name": "主题入口",
+ "position": [
+ 860.44,
+ 423.6792219679634
+ ],
+ "size": [
+ 294.97,
+ 249.07077803203663
+ ],
+ "source_theme": "spring",
+ "sprite": "background/decorateIcon/decorate_spring_4.png"
+ },
+ {
+ "basis": "approved screenshot inverse projection",
+ "fallback": false,
+ "layer": "background",
+ "name": "摊位",
+ "position": [
+ 514.2,
+ 554.498515625
+ ],
+ "size": [
+ 298.97,
+ 287.29148437500004
+ ],
+ "source_theme": "spring",
+ "sprite": "background/decorateIcon/decorate_spring_5.png"
+ },
+ {
+ "basis": "approved screenshot inverse projection",
+ "fallback": false,
+ "layer": "background",
+ "name": "狗窝",
+ "position": [
+ 277.95,
+ 763.445234375
+ ],
+ "size": [
+ 305.25,
+ 205.884765625
+ ],
+ "source_theme": "spring",
+ "sprite": "background/decorateIcon/decorate_spring_6.png"
+ },
+ {
+ "basis": "approved screenshot inverse projection",
+ "fallback": false,
+ "layer": "foreground",
+ "name": "码头",
+ "position": [
+ 1074.51,
+ 1019.67921875
+ ],
+ "size": [
+ 355.42,
+ 397.07078125
+ ],
+ "source_theme": "spring",
+ "sprite": "background/decorateIcon/decorate_spring_7.png"
+ },
+ {
+ "basis": "approved screenshot inverse projection",
+ "fallback": true,
+ "layer": "background",
+ "name": "邮箱",
+ "position": [
+ 790.18,
+ 576.9633333333334
+ ],
+ "size": [
+ 104.54,
+ 139.38666666666666
+ ],
+ "source_theme": "default",
+ "sprite": "background/decorateIcon/decorate_default_8.png"
+ },
+ {
+ "basis": "approved screenshot inverse projection",
+ "fallback": false,
+ "layer": "background",
+ "name": "加工坊标牌",
+ "position": [
+ 2347.77,
+ 756.0799999999999
+ ],
+ "size": [
+ 269.7,
+ 269.7
+ ],
+ "source_theme": "spring",
+ "sprite": "background/decorateIcon/decorate_spring_9.png"
+ },
+ {
+ "basis": "approved screenshot inverse projection",
+ "fallback": true,
+ "layer": "background",
+ "name": "稻草人标牌",
+ "position": [
+ 1120.51,
+ 300.29999999999995
+ ],
+ "size": [
+ 238.34,
+ 238.34
+ ],
+ "source_theme": "default",
+ "sprite": "background/decorateIcon/decorate_default_10.png"
+ }
+ ],
+ "summer": [
+ {
+ "basis": "prefab parent and image transforms",
+ "fallback": false,
+ "layer": "background",
+ "name": "房屋",
+ "position": [
+ 1675.75,
+ 158.36029213483152
+ ],
+ "size": [
+ 572.14,
+ 630.6397078651685
+ ],
+ "source_theme": "summer",
+ "sprite": "background/decorateIcon/decorate_summer_2.png"
+ },
+ {
+ "basis": "prefab geometry and inferred runtime origin",
+ "fallback": false,
+ "layer": "background",
+ "name": "矿洞",
+ "position": [
+ 1385.61,
+ 256.06000000000006
+ ],
+ "size": [
+ 282.88,
+ 282.88
+ ],
+ "source_theme": "summer",
+ "sprite": "background/decorateIcon/decorate_summer_3.png"
+ },
+ {
+ "basis": "prefab geometry and inferred runtime origin",
+ "fallback": false,
+ "layer": "background",
+ "name": "主题入口",
+ "position": [
+ 860.44,
+ 423.6792219679634
+ ],
+ "size": [
+ 294.97,
+ 249.07077803203663
+ ],
+ "source_theme": "summer",
+ "sprite": "background/decorateIcon/decorate_summer_4.png"
+ },
+ {
+ "basis": "approved screenshot inverse projection",
+ "fallback": false,
+ "layer": "background",
+ "name": "摊位",
+ "position": [
+ 514.2,
+ 554.498515625
+ ],
+ "size": [
+ 298.97,
+ 287.29148437500004
+ ],
+ "source_theme": "summer",
+ "sprite": "background/decorateIcon/decorate_summer_5.png"
+ },
+ {
+ "basis": "approved screenshot inverse projection",
+ "fallback": false,
+ "layer": "background",
+ "name": "狗窝",
+ "position": [
+ 277.95,
+ 763.445234375
+ ],
+ "size": [
+ 305.25,
+ 205.884765625
+ ],
+ "source_theme": "summer",
+ "sprite": "background/decorateIcon/decorate_summer_6.png"
+ },
+ {
+ "basis": "approved screenshot inverse projection",
+ "fallback": false,
+ "layer": "foreground",
+ "name": "码头",
+ "position": [
+ 1074.51,
+ 1019.67921875
+ ],
+ "size": [
+ 355.42,
+ 397.07078125
+ ],
+ "source_theme": "summer",
+ "sprite": "background/decorateIcon/decorate_summer_7.png"
+ },
+ {
+ "basis": "approved screenshot inverse projection",
+ "fallback": true,
+ "layer": "background",
+ "name": "邮箱",
+ "position": [
+ 790.18,
+ 576.9633333333334
+ ],
+ "size": [
+ 104.54,
+ 139.38666666666666
+ ],
+ "source_theme": "default",
+ "sprite": "background/decorateIcon/decorate_default_8.png"
+ },
+ {
+ "basis": "approved screenshot inverse projection",
+ "fallback": false,
+ "layer": "background",
+ "name": "加工坊标牌",
+ "position": [
+ 2347.77,
+ 756.0799999999999
+ ],
+ "size": [
+ 269.7,
+ 269.7
+ ],
+ "source_theme": "summer",
+ "sprite": "background/decorateIcon/decorate_summer_9.png"
+ },
+ {
+ "basis": "approved screenshot inverse projection",
+ "fallback": true,
+ "layer": "background",
+ "name": "稻草人标牌",
+ "position": [
+ 1120.51,
+ 300.29999999999995
+ ],
+ "size": [
+ 238.34,
+ 238.34
+ ],
+ "source_theme": "default",
+ "sprite": "background/decorateIcon/decorate_default_10.png"
+ }
+ ],
+ "winter": [
+ {
+ "basis": "prefab parent and image transforms",
+ "fallback": false,
+ "layer": "background",
+ "name": "房屋",
+ "position": [
+ 1675.75,
+ 261.2169213483146
+ ],
+ "size": [
+ 572.14,
+ 527.7830786516854
+ ],
+ "source_theme": "winter",
+ "sprite": "background/decorateIcon/decorate_winter_2.png"
+ },
+ {
+ "basis": "prefab geometry and inferred runtime origin",
+ "fallback": false,
+ "layer": "background",
+ "name": "矿洞",
+ "position": [
+ 1385.61,
+ 256.06000000000006
+ ],
+ "size": [
+ 282.88,
+ 282.88
+ ],
+ "source_theme": "winter",
+ "sprite": "background/decorateIcon/decorate_winter_3.png"
+ },
+ {
+ "basis": "prefab geometry and inferred runtime origin",
+ "fallback": false,
+ "layer": "background",
+ "name": "主题入口",
+ "position": [
+ 860.44,
+ 423.6792219679634
+ ],
+ "size": [
+ 294.97,
+ 249.07077803203663
+ ],
+ "source_theme": "winter",
+ "sprite": "background/decorateIcon/decorate_winter_4.png"
+ },
+ {
+ "basis": "approved screenshot inverse projection",
+ "fallback": false,
+ "layer": "background",
+ "name": "摊位",
+ "position": [
+ 514.2,
+ 554.498515625
+ ],
+ "size": [
+ 298.97,
+ 287.29148437500004
+ ],
+ "source_theme": "winter",
+ "sprite": "background/decorateIcon/decorate_winter_5.png"
+ },
+ {
+ "basis": "approved screenshot inverse projection",
+ "fallback": false,
+ "layer": "background",
+ "name": "狗窝",
+ "position": [
+ 277.95,
+ 763.445234375
+ ],
+ "size": [
+ 305.25,
+ 205.884765625
+ ],
+ "source_theme": "winter",
+ "sprite": "background/decorateIcon/decorate_winter_6.png"
+ },
+ {
+ "basis": "approved screenshot inverse projection",
+ "fallback": false,
+ "layer": "foreground",
+ "name": "码头",
+ "position": [
+ 1074.51,
+ 1019.67921875
+ ],
+ "size": [
+ 355.42,
+ 397.07078125
+ ],
+ "source_theme": "winter",
+ "sprite": "background/decorateIcon/decorate_winter_7.png"
+ },
+ {
+ "basis": "approved screenshot inverse projection",
+ "fallback": false,
+ "layer": "background",
+ "name": "邮箱",
+ "position": [
+ 790.18,
+ 576.9633333333334
+ ],
+ "size": [
+ 104.54,
+ 139.38666666666666
+ ],
+ "source_theme": "winter",
+ "sprite": "background/decorateIcon/decorate_winter_8.png"
+ },
+ {
+ "basis": "approved screenshot inverse projection",
+ "fallback": false,
+ "layer": "background",
+ "name": "加工坊标牌",
+ "position": [
+ 2347.77,
+ 756.0799999999999
+ ],
+ "size": [
+ 269.7,
+ 269.7
+ ],
+ "source_theme": "winter",
+ "sprite": "background/decorateIcon/decorate_winter_9.png"
+ },
+ {
+ "basis": "approved screenshot inverse projection",
+ "fallback": true,
+ "layer": "background",
+ "name": "稻草人标牌",
+ "position": [
+ 1120.51,
+ 300.29999999999995
+ ],
+ "size": [
+ 238.34,
+ 238.34
+ ],
+ "source_theme": "default",
+ "sprite": "background/decorateIcon/decorate_default_10.png"
+ }
+ ]
+ },
"images": {
"background/decorateIcon/decorate_autumn_1.jpg": {
"sha256": "a3346953c2caeb4587da66046eb72b26ddc3120d8a03eb6d9fac3105d4876e02",
@@ -11,6 +889,111 @@
1872
]
},
+ "background/decorateIcon/decorate_autumn_2.png": {
+ "sha256": "188d506956cfff3e3d4e0847b227f1128deb5a9b58451a9b4726cfca4ecff148",
+ "size": [
+ 890,
+ 821
+ ]
+ },
+ "background/decorateIcon/decorate_autumn_3.png": {
+ "sha256": "5cf49b7475f7559c92ac8a6ab572da39c68b51aafe8106f683c05e422e5cdbc7",
+ "size": [
+ 300,
+ 300
+ ]
+ },
+ "background/decorateIcon/decorate_autumn_4.png": {
+ "sha256": "5bcf66b8c24b385f690a3edc7c5cdf09e585f24594a243588ac60b634e14d804",
+ "size": [
+ 437,
+ 369
+ ]
+ },
+ "background/decorateIcon/decorate_autumn_5.png": {
+ "sha256": "112675d95146b0ba0fe6a85ec8528e21ad4b3f786e653c512ca712f88c1c37cb",
+ "size": [
+ 384,
+ 369
+ ]
+ },
+ "background/decorateIcon/decorate_autumn_6.png": {
+ "sha256": "53da880a0d1733b6c358e88a153925043a1a5f613a1d19e231ef50596410f4e6",
+ "size": [
+ 384,
+ 259
+ ]
+ },
+ "background/decorateIcon/decorate_autumn_7.png": {
+ "sha256": "54d3e8333c5cbd828525d0a7eb6f1325783c08aa17a53b3e343801ca4637ba0f",
+ "size": [
+ 384,
+ 429
+ ]
+ },
+ "background/decorateIcon/decorate_bee_1.jpg": {
+ "sha256": "85b8b1135801bcbd3789c06f7d99655ee4c229506313772ffda13c59c1daeb40",
+ "size": [
+ 3072,
+ 1872
+ ]
+ },
+ "background/decorateIcon/decorate_bee_2.png": {
+ "sha256": "e6642aee792e67d1ac0156836e18e399cbf8eb1201843e0d4e6efbc9a054560d",
+ "size": [
+ 890,
+ 981
+ ]
+ },
+ "background/decorateIcon/decorate_bee_3.png": {
+ "sha256": "828237611364a567cb634f6a5384147197be8c53b3b6a1dc916a5651eea2d315",
+ "size": [
+ 300,
+ 300
+ ]
+ },
+ "background/decorateIcon/decorate_bee_4.png": {
+ "sha256": "eec209b03f25de33069f06a44431c60d7485321d2f36748d3d1112f34b65e890",
+ "size": [
+ 437,
+ 369
+ ]
+ },
+ "background/decorateIcon/decorate_bee_5.png": {
+ "sha256": "d8814b7fd81361480e53bcb5b166affeaaf9b84d7a54370921fa4e16dd527143",
+ "size": [
+ 384,
+ 369
+ ]
+ },
+ "background/decorateIcon/decorate_bee_6.png": {
+ "sha256": "fc4868215c771f0bb3552290c91e4e1fef26b19cb7b66d49b9411ce7cfe16992",
+ "size": [
+ 384,
+ 259
+ ]
+ },
+ "background/decorateIcon/decorate_bee_7.png": {
+ "sha256": "e8bcaa8daaec9e7f564c41f3150b308031691603b4910bc99e7be8b755bb6bdb",
+ "size": [
+ 384,
+ 429
+ ]
+ },
+ "background/decorateIcon/decorate_bee_8.png": {
+ "sha256": "da7a6a54d69cee5887fcce84e17167447aaf33e2c2213ae35c1f94e2c9f31a7e",
+ "size": [
+ 150,
+ 200
+ ]
+ },
+ "background/decorateIcon/decorate_bee_9.png": {
+ "sha256": "f52e3817c5af2a56ccc492635e111932d352bebc54c7bc446d199f2895a4c196",
+ "size": [
+ 200,
+ 200
+ ]
+ },
"background/decorateIcon/decorate_default_1.jpg": {
"sha256": "a8616f7e008125c868485186be104e58287c582cd86d7fdd2b314862932c9b40",
"size": [
@@ -18,6 +1001,69 @@
1872
]
},
+ "background/decorateIcon/decorate_default_10.png": {
+ "sha256": "f3cd7bb3ddcc7040da4c239a8df4b3125e26070369c99e5ea2fd6d19663c11b3",
+ "size": [
+ 200,
+ 200
+ ]
+ },
+ "background/decorateIcon/decorate_default_2.png": {
+ "sha256": "662a53b18cfe8b6fed270f5720eab8521d2718ed26bc254113bbabcaf4adb4d8",
+ "size": [
+ 890,
+ 981
+ ]
+ },
+ "background/decorateIcon/decorate_default_3.png": {
+ "sha256": "d3dfb3d1c697c150177146225237b75b675d216d4b6bbb9108ed91719a0ee5e2",
+ "size": [
+ 300,
+ 300
+ ]
+ },
+ "background/decorateIcon/decorate_default_4.png": {
+ "sha256": "8c9d0a225d4db217a5fd21348938abba4a27f28946de6d1adee63fade7bee322",
+ "size": [
+ 437,
+ 369
+ ]
+ },
+ "background/decorateIcon/decorate_default_5.png": {
+ "sha256": "b380dd97570b13b3a213424092a5a5809ee08ef3b02e324e5b16dce5053bb00f",
+ "size": [
+ 384,
+ 369
+ ]
+ },
+ "background/decorateIcon/decorate_default_6.png": {
+ "sha256": "209f5104156d5d1ed8a40f4dd28f270620ed38ae0cd2524bb0892bd16a48ca74",
+ "size": [
+ 384,
+ 259
+ ]
+ },
+ "background/decorateIcon/decorate_default_7.png": {
+ "sha256": "0d4ed91a537d4beb3059e7cb69525e996dc6b931763a5c1b9f4b052a745f8405",
+ "size": [
+ 384,
+ 429
+ ]
+ },
+ "background/decorateIcon/decorate_default_8.png": {
+ "sha256": "7409ae40b29be0014b1376e93ab130101e723df2d28d0f4394c0e883e7a70d53",
+ "size": [
+ 150,
+ 200
+ ]
+ },
+ "background/decorateIcon/decorate_default_9.png": {
+ "sha256": "86ac74284eab4d3b8c05229d303ad3e6ab426a567f4a6b22c48e94c267e7231d",
+ "size": [
+ 200,
+ 200
+ ]
+ },
"background/decorateIcon/decorate_spring_1.jpg": {
"sha256": "2278e8c410717ef5339b5343884b4b96bf446e3192b02f4ec72a526692bd0939",
"size": [
@@ -25,6 +1071,55 @@
1872
]
},
+ "background/decorateIcon/decorate_spring_2.png": {
+ "sha256": "10772cdf83c0ec68007c6d940a056183d3f015759b6d03663b2ff625e528fc78",
+ "size": [
+ 890,
+ 981
+ ]
+ },
+ "background/decorateIcon/decorate_spring_3.png": {
+ "sha256": "2f7605b7fc05badd7d7ab9338b1ed070fc0733bb05379d0ad99f277c7a7395fd",
+ "size": [
+ 300,
+ 300
+ ]
+ },
+ "background/decorateIcon/decorate_spring_4.png": {
+ "sha256": "ebff83c417196ead949e9804be8cfe4aa4bb8c1f4bfe90973289a07c940057f6",
+ "size": [
+ 437,
+ 369
+ ]
+ },
+ "background/decorateIcon/decorate_spring_5.png": {
+ "sha256": "5932c7f0b58255cd50ced8d11b7e4fe7172c10d3a892eb94ed3b598759a4b3c3",
+ "size": [
+ 384,
+ 369
+ ]
+ },
+ "background/decorateIcon/decorate_spring_6.png": {
+ "sha256": "4c7e4d4699894b7dcf23300a9d7125caaa56c3bf086cd4876fe95ca13adee0cb",
+ "size": [
+ 384,
+ 259
+ ]
+ },
+ "background/decorateIcon/decorate_spring_7.png": {
+ "sha256": "c2a4d78505fa1b167b634eea2a092368b64742af88a613a15e04d7f26b6f3c08",
+ "size": [
+ 384,
+ 429
+ ]
+ },
+ "background/decorateIcon/decorate_spring_9.png": {
+ "sha256": "8217102f54988afd3904fc64c4dabbb56b6ceddc3a2cd2ed7efe5661ae95a244",
+ "size": [
+ 200,
+ 200
+ ]
+ },
"background/decorateIcon/decorate_summer_1.jpg": {
"sha256": "1057135459c365a26dc3834ee96e04c8f8a728f24d52f1610274a112a7800e05",
"size": [
@@ -32,6 +1127,55 @@
1872
]
},
+ "background/decorateIcon/decorate_summer_2.png": {
+ "sha256": "40abb58c9e388383669e7b3981d14441de38a237dc4920b7e943653ec69d8555",
+ "size": [
+ 890,
+ 981
+ ]
+ },
+ "background/decorateIcon/decorate_summer_3.png": {
+ "sha256": "b63fe43714a82a53ed611b94cf3362aae836865be6a29464190404655f47a1ba",
+ "size": [
+ 300,
+ 300
+ ]
+ },
+ "background/decorateIcon/decorate_summer_4.png": {
+ "sha256": "6a5bace564df8d5142e624008be6ff81004ac42375a558fbf401cda8a47f0f24",
+ "size": [
+ 437,
+ 369
+ ]
+ },
+ "background/decorateIcon/decorate_summer_5.png": {
+ "sha256": "4e1b89a35d4f88182ea8683aa6a227c596ed57c7f27d9431a664cdee7ca85b07",
+ "size": [
+ 384,
+ 369
+ ]
+ },
+ "background/decorateIcon/decorate_summer_6.png": {
+ "sha256": "0b7ebc6fde8723da8fc3e63853b384a30a52aa2168df0db13e03cdb83d8c910f",
+ "size": [
+ 384,
+ 259
+ ]
+ },
+ "background/decorateIcon/decorate_summer_7.png": {
+ "sha256": "02b5c95d262974959e58428f51ca4f12d8c6b32446d4f4bde32ea50cff0fbc5e",
+ "size": [
+ 384,
+ 429
+ ]
+ },
+ "background/decorateIcon/decorate_summer_9.png": {
+ "sha256": "5a3e0fa79d6db2931f6350a43dbc20383524115d0f64cfb96038758fc03951f1",
+ "size": [
+ 200,
+ 200
+ ]
+ },
"background/decorateIcon/decorate_winter_1.jpg": {
"sha256": "6f1cd2c0aa3be43f9ef64d9c7b8965adebd794458285ce87fbe5a401f995fc43",
"size": [
@@ -39,6 +1183,62 @@
1872
]
},
+ "background/decorateIcon/decorate_winter_2.png": {
+ "sha256": "78fca463daf00da06db83ef3f43d7bea444a57043cdce39fa23e8551b90c1a5b",
+ "size": [
+ 890,
+ 821
+ ]
+ },
+ "background/decorateIcon/decorate_winter_3.png": {
+ "sha256": "dea09c184d23dd279d29c803678b5f17adf34fd1833f34d7df9fc8dabca3eb42",
+ "size": [
+ 300,
+ 300
+ ]
+ },
+ "background/decorateIcon/decorate_winter_4.png": {
+ "sha256": "e42dc3bd0d168fc386ee10bf6e07b92e3ddfd2267d01bfa63d27bd8fb04c372f",
+ "size": [
+ 437,
+ 369
+ ]
+ },
+ "background/decorateIcon/decorate_winter_5.png": {
+ "sha256": "c36c51e0b9f237233d10f9c921bf3a42dc960546fa08885ff5fa5ac2b261c773",
+ "size": [
+ 384,
+ 369
+ ]
+ },
+ "background/decorateIcon/decorate_winter_6.png": {
+ "sha256": "8de23b47a1baedbd0958fd1f957b80b9ea272623605878346d2f78117c80b849",
+ "size": [
+ 384,
+ 259
+ ]
+ },
+ "background/decorateIcon/decorate_winter_7.png": {
+ "sha256": "29be9ae301e0cd30f312d43cc0ad82d58dee99135183faee95331e5c981f5a60",
+ "size": [
+ 384,
+ 429
+ ]
+ },
+ "background/decorateIcon/decorate_winter_8.png": {
+ "sha256": "5a736ae5940ec0f3a1846b8874b6b501b7e88bc1cc59aeb98684391692c8d15c",
+ "size": [
+ 150,
+ 200
+ ]
+ },
+ "background/decorateIcon/decorate_winter_9.png": {
+ "sha256": "896ac8fdaa329934a56a08f8c84b3415259a3efbe34c119a2ee7993b2640e8c4",
+ "size": [
+ 200,
+ 200
+ ]
+ },
"background/expansion.png": {
"sha256": "175e4a5e32b2e390801b554a0b6bda49a69ec660dec2c50d4dc395c231a157ab",
"size": [
diff --git a/src/farm_render/models.py b/src/farm_render/models.py
index 616aa80..563c4e2 100644
--- a/src/farm_render/models.py
+++ b/src/farm_render/models.py
@@ -25,7 +25,7 @@ class RenderOptions:
if self.format not in ("AUTO", "WEBP", "JPEG", "PNG"):
raise ValueError("Unsupported output format")
if self.theme not in ("default", "spring", "summer", "autumn",
- "winter"):
+ "winter", "bee"):
raise ValueError("Unsupported theme")
@classmethod
diff --git a/src/farm_render/renderer.py b/src/farm_render/renderer.py
index e4468ac..eee3b14 100644
--- a/src/farm_render/renderer.py
+++ b/src/farm_render/renderer.py
@@ -65,6 +65,7 @@ class FarmRenderer:
canvas = self._base(scene, options)
self._plants(canvas, scene, options)
self._pests(canvas, scene, options)
+ self._decorations(canvas, options, "foreground")
self._panel(canvas, scene, options, avatar)
composed = perf_counter()
content = canvas.encode(codec, options.quality, options.method)
@@ -101,6 +102,7 @@ class FarmRenderer:
f"decorate_{options.theme}_1.jpg", size,
)
canvas = Canvas(background)
+ self._decorations(canvas, options, "background")
soil_size = tuple(max(1, s(n)) for n in (
self.assets.metadata["soil_size"]
))
@@ -121,6 +123,16 @@ class FarmRenderer:
self.assets.cache.put(key, canvas.image, size[0] * size[1] * 4)
return canvas.copy()
+ def _decorations(self, canvas, options, layer):
+ s = lambda n: self._scale(n, options)
+ for item in self.assets.metadata["decorations"][options.theme]:
+ if item["layer"] != layer:
+ continue
+ image = self.assets.image(
+ item["sprite"], tuple(max(1, s(n)) for n in item["size"]),
+ )
+ canvas.paste(image, tuple(s(n) for n in item["position"]))
+
def _plants(self, canvas, scene, options):
s = lambda n: self._scale(n, options)
soil_w, soil_h = self.assets.metadata["soil_size"]
diff --git a/src/farm_render/seasons.py b/src/farm_render/seasons.py
new file mode 100644
index 0000000..34f8dfc
--- /dev/null
+++ b/src/farm_render/seasons.py
@@ -0,0 +1,18 @@
+"""按北京时间选择四季主题;蜂巢主题只供显式离线预览。"""
+
+from datetime import datetime, timedelta, timezone
+
+from farm_core.domain import integer
+
+BEIJING = timezone(timedelta(hours=8))
+
+
+def season_for(now: int) -> str:
+ month = datetime.fromtimestamp(integer(now), BEIJING).month
+ if 3 <= month <= 5:
+ return "spring"
+ if 6 <= month <= 8:
+ return "summer"
+ if 9 <= month <= 11:
+ return "autumn"
+ return "winter"
diff --git a/tests/test_season_backgrounds.py b/tests/test_season_backgrounds.py
new file mode 100644
index 0000000..fc18d07
--- /dev/null
+++ b/tests/test_season_backgrounds.py
@@ -0,0 +1,74 @@
+from datetime import datetime, timezone
+from pathlib import Path
+import sys
+import unittest
+from unittest.mock import patch
+
+from farm_core import Identity
+from farm_core.domain import Account, Plot, Snapshot
+from farm_render import FarmRenderer, RenderOptions
+
+ROOT = Path(__file__).resolve().parents[1]
+sys.path.insert(0, str(ROOT / "scripts"))
+from ci_font import choose_font
+
+
+class SeasonBackgroundTests(unittest.TestCase):
+ def test_calendar_uses_beijing_time_and_month_boundaries(self):
+ from farm_render.seasons import season_for
+
+ cases = (
+ ("2026-02-28T15:59:59", "winter"),
+ ("2026-02-28T16:00:00", "spring"),
+ ("2026-05-31T15:59:59", "spring"),
+ ("2026-05-31T16:00:00", "summer"),
+ ("2026-08-31T16:00:00", "autumn"),
+ ("2026-11-30T16:00:00", "winter"),
+ ("2026-12-31T16:00:00", "winter"),
+ )
+ for value, expected in cases:
+ utc = datetime.fromisoformat(value).replace(tzinfo=timezone.utc)
+ self.assertEqual(season_for(int(utc.timestamp())), expected)
+
+ def test_every_theme_has_nine_decorations_and_known_fallbacks(self):
+ renderer = FarmRenderer(choose_font())
+ layouts = renderer.assets.metadata["decorations"]
+ for theme in ("default", "spring", "summer", "autumn", "winter", "bee"):
+ self.assertEqual(len(layouts[theme]), 9)
+ self.assertEqual(RenderOptions(theme=theme).theme, theme)
+ for item in layouts[theme]:
+ self.assertIn(item["sprite"], renderer.assets.metadata["images"])
+ spring_mail = layouts["spring"][6]
+ autumn_workshop = layouts["autumn"][7]
+ self.assertEqual(spring_mail["source_theme"], "default")
+ self.assertEqual(autumn_workshop["source_theme"], "default")
+
+ def test_shorter_house_keeps_ground_baseline_and_aspect_ratio(self):
+ renderer = FarmRenderer(choose_font())
+ layouts = renderer.assets.metadata["decorations"]
+ spring, winter = layouts["spring"][0], layouts["winter"][0]
+ self.assertAlmostEqual(
+ spring["position"][1] + spring["size"][1],
+ winter["position"][1] + winter["size"][1], places=5,
+ )
+ self.assertAlmostEqual(winter["size"][0] / winter["size"][1],
+ 890 / 821, places=5)
+
+ def test_dock_is_drawn_after_crops_and_theme_cache_is_separate(self):
+ renderer = FarmRenderer(choose_font())
+ who = Identity("test", "local", "qq", "seasons")
+ data = Snapshot(Account("四季测试", 500, 0, 0), (), (Plot(1),))
+ for theme in ("spring", "summer", "autumn", "winter", "bee"):
+ first = renderer.render(who, data, 0, RenderOptions(theme=theme))
+ warm = renderer.render(who, data, 1, RenderOptions(theme=theme))
+ self.assertFalse(first.cache_hit)
+ self.assertTrue(warm.cache_hit)
+ scene = renderer.scene(who, data, 0)
+ order = []
+ with patch.object(renderer, "_plants", side_effect=lambda *a:
+ order.append("plants")), patch.object(
+ renderer, "_decorations", side_effect=lambda c, o, layer:
+ order.append(layer)
+ ):
+ renderer.render(who, data, 0, RenderOptions(width=1536))
+ self.assertLess(order.index("plants"), order.index("foreground"))
diff --git a/tests/test_zhenxun_package.py b/tests/test_zhenxun_package.py
index d53280e..63a6b3e 100644
--- a/tests/test_zhenxun_package.py
+++ b/tests/test_zhenxun_package.py
@@ -93,6 +93,19 @@ class ZhenxunPackageTests(unittest.TestCase):
self.assertEqual(sentinel.read_text(), "preserved")
self.assertTrue((target / "__init__.py").is_file())
+ def test_readme_image_is_in_repository_archive_and_manifest(self):
+ expected = (ROOT / "images/farm-preview.png").read_bytes()
+ relative = "images/farm-preview.png"
+ self.assertEqual(
+ (self.output / "repository" / relative).read_bytes(), expected,
+ )
+ manifest = verify_package(self.bundle)
+ self.assertIn(relative, manifest["files"])
+ with ZipFile(self.bundle) as archive:
+ self.assertEqual(archive.read(
+ f"zhenxun_plugin_farm_preview/{relative}",
+ ), expected)
+
def test_tampering_extra_path_and_traversal_rejected(self):
for name in ("../outside.txt", "C:/escape.txt", "extra.txt",
"..\\outside.txt"):
diff --git a/tests/test_zhenxun_runtime.py b/tests/test_zhenxun_runtime.py
index 1792f72..a3add2a 100644
--- a/tests/test_zhenxun_runtime.py
+++ b/tests/test_zhenxun_runtime.py
@@ -78,6 +78,23 @@ class ZhenxunRuntimeTests(unittest.IsolatedAsyncioTestCase):
self.assertIn("不出售", await self.call("buy", "禁售菜 1", "blocked"))
self.assertIn("(#1)", await self.call("inventory"))
+ async def test_farm_uses_calendar_theme_and_bee_is_not_a_user_option(self):
+ rendering = self.runtime.rendering
+ from unittest.mock import AsyncMock
+ rendering.my_farm = AsyncMock(return_value=SimpleNamespace(content=b"ok"))
+ for now, expected in (
+ (1767225600, "winter"), (1775001600, "spring"),
+ (1782864000, "summer"), (1790812800, "autumn"),
+ ):
+ self.runtime.service.clock = lambda: now
+ self.assertEqual(await self.call("farm"), b"ok")
+ call = rendering.my_farm.call_args
+ self.assertEqual(call.kwargs["options"].theme, expected)
+ self.assertEqual(call.kwargs["now"], now)
+ self.assertEqual(await self.call("farm", "高清"), b"ok")
+ self.assertEqual(rendering.my_farm.call_args.kwargs["options"].width, 1536)
+ self.assertIn("参数有误", await self.call("farm", "蜂巢"))
+
async def test_pest_command_clears_pests_and_preserves_growth(self):
self.assertEqual(self.module.COMMANDS["农场除虫"], "clear_pests")
self.assertIn("每只虫", await self.call("help"))