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) +

+ + license + + + Python + + + ruff + + + Nonebot + + + Python + +

-preview.3恢复旧版信息底板和头像框、昵称、经验进度、等级及两种货币。 -按用户确认校正原素材上的字段错位,保留旧版经验计算口径;点券显示 -真实余额。仍使用目标尺寸绘制、一次快照查询及有界缓存。 +

-## 安装前 +[![tencent-qq](https://img.shields.io/badge/%E7%BE%A4-%E7%9C%9F%E5%AF%BB%E5%86%9C%E5%9C%BA%E6%B5%8B%E8%AF%95-%23FF99CC +)](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代码一起分发。 +--- -如果安装目录已存在,脚本会拒绝覆盖。更新前停机器人、备份插件目录, -手动移走旧预览目录后再安装;数据目录位于插件外,不随插件删除。 +## 农场界面 -## 配置与指令 +![农场界面](./images/farm-preview.png) -默认优先使用真寻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"))