add # 新增使用开源字体

This commit is contained in:
2026-10-26 11:34:00 +08:00
parent dc17f7f65c
commit 22cfddcf06
68 changed files with 1772 additions and 461 deletions
+8 -3
View File
@@ -50,13 +50,18 @@ class Assets:
else data.joinpath('assets.zip'))
def image(self, name, size=None):
if name not in self.metadata["images"]:
upgrade = name in ('soil/红土地.png', 'soil/黑土地.png', 'soil/金土地.png')
if not upgrade and name not in self.metadata["images"]:
raise KeyError("Unknown packaged image")
size = tuple(size or self.metadata["images"][name]["size"])
size = tuple(size or (self.metadata['soil_size'] if upgrade
else self.metadata["images"][name]["size"]))
key = (name, size)
cached = self.cache.get(key)
if cached is None:
with self._bundle.open("rb") as stream, ZipFile(stream) as bundle:
source = (resources.files(__package__).joinpath(
'data', 'land-soils.zip',
) if upgrade else self._bundle)
with source.open("rb") as stream, ZipFile(stream) as bundle:
content = bundle.read(name)
with Image.open(BytesIO(content)) as original:
original.thumbnail(size, Image.Resampling.LANCZOS)
+12 -1
View File
@@ -5,4 +5,15 @@ assets.zip 原样复制自 Shu-Ying/zhenxun_plugin_farm 的本地保留基线,
仅选取农场季节背景、草地/普通地、作物及两个状态图标;metadata.json
来自旧布局和作物配置。沿用仓库 GPLv3 许可与原始素材归属,迁移不
增加第三方素材权利。后续对外发布前仍须核实历史素材来源及分发权限。
不包含字体;部署方配置可合法使用的中文字体路径。
## 小赖字体
默认字体由 farm_service 的 fonts/assets.zip 提供,插件不内置 TTF。
Xiaolai-Regular.ttf 原样取自 lxgw/kose-font 的 v3.126 发布,
项目地址:https://github.com/lxgw/kose-font 。
下载地址:https://github.com/lxgw/kose-font/releases/download/v3.126/Xiaolai-Regular.ttf
上游 SHA-256:e2f68daf0e72777a8cf58bc83de1b98634b251e537ddbfca24b0ae50d1802da2
字体采用 SIL Open Font License 1.1,完整版权声明与许可见同目录
OFL-Xiaolai.txt;字体不适用本项目代码的 GPLv3 许可。
随软件分发时须保留字体的版权声明和完整许可,不得单独销售字体。
插件缓存中同时保存原版字体和许可;下载摘要由 font-manifest.json 固定。
+94
View File
@@ -0,0 +1,94 @@
Copyright 2020-2024 LXGW (https://github.com/lxgw/kose-font)
Copyright 2014 Nozomi Seto (https://ja.osdn.net/projects/setofont/)
This Font Software is licensed under the SIL Open Font License, Version 1.1.
This license is copied below, and is also available with a FAQ at:
http://scripts.sil.org/OFL
-----------------------------------------------------------
SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
-----------------------------------------------------------
PREAMBLE
The goals of the Open Font License (OFL) are to stimulate worldwide
development of collaborative font projects, to support the font creation
efforts of academic and linguistic communities, and to provide a free and
open framework in which fonts may be shared and improved in partnership
with others.
The OFL allows the licensed fonts to be used, studied, modified and
redistributed freely as long as they are not sold by themselves. The
fonts, including any derivative works, can be bundled, embedded,
redistributed and/or sold with any software provided that any reserved
names are not used by derivative works. The fonts and derivatives,
however, cannot be released under any other type of license. The
requirement for fonts to remain under this license does not apply
to any document created using the fonts or their derivatives.
DEFINITIONS
"Font Software" refers to the set of files released by the Copyright
Holder(s) under this license and clearly marked as such. This may
include source files, build scripts and documentation.
"Reserved Font Name" refers to any names specified as such after the
copyright statement(s).
"Original Version" refers to the collection of Font Software components as
distributed by the Copyright Holder(s).
"Modified Version" refers to any derivative made by adding to, deleting,
or substituting -- in part or in whole -- any of the components of the
Original Version, by changing formats or by porting the Font Software to a
new environment.
"Author" refers to any designer, engineer, programmer, technical
writer or other person who contributed to the Font Software.
PERMISSION & CONDITIONS
Permission is hereby granted, free of charge, to any person obtaining
a copy of the Font Software, to use, study, copy, merge, embed, modify,
redistribute, and sell modified and unmodified copies of the Font
Software, subject to the following conditions:
1) Neither the Font Software nor any of its individual components,
in Original or Modified Versions, may be sold by itself.
2) Original or Modified Versions of the Font Software may be bundled,
redistributed and/or sold with any software, provided that each copy
contains the above copyright notice and this license. These can be
included either as stand-alone text files, human-readable headers or
in the appropriate machine-readable metadata fields within text or
binary files as long as those fields can be easily viewed by the user.
3) No Modified Version of the Font Software may use the Reserved Font
Name(s) unless explicit written permission is granted by the corresponding
Copyright Holder. This restriction only applies to the primary font name as
presented to the users.
4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
Software shall not be used to promote, endorse or advertise any
Modified Version, except to acknowledge the contribution(s) of the
Copyright Holder(s) and the Author(s) or with their explicit written
permission.
5) The Font Software, modified or unmodified, in part or in whole,
must be distributed entirely under this license, and must not be
distributed under any other license. The requirement for fonts to
remain under this license does not apply to any document created
using the Font Software.
TERMINATION
This license becomes null and void if any of the above conditions are
not met.
DISCLAIMER
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
OTHER DEALINGS IN THE FONT SOFTWARE.
+11
View File
@@ -0,0 +1,11 @@
{
"schema": 1,
"version": "xiaolai-v3.126",
"sha256": "c2d1c1a5247e05b766d12a8d8410594d78b1a8b86031624a05b76ee0515c1335",
"size_bytes": 14741741,
"sources": {
"github": "https://raw.githubusercontent.com/Shu-Ying/farm_service/main/fonts/assets.zip",
"gitea": "https://gitea.diuse.work/Diuse/farm_service/raw/branch/main/fonts/assets.zip",
"farm_service": "https://farm.example/v1/fonts/c2d1c1a5247e05b766d12a8d8410594d78b1a8b86031624a05b76ee0515c1335/assets.zip"
}
}
Binary file not shown.
+106
View File
@@ -0,0 +1,106 @@
"""从 service 仓库下载原版小赖字体,校验后原子写入本地缓存。"""
import hashlib
from importlib import resources
import json
from pathlib import Path
import tempfile
from zipfile import ZipFile
from .bundle import _LOCK, _copy_remote, validate_options
FONT_NAME = "Xiaolai-Regular.ttf"
FILES = {
FONT_NAME: (
22220806,
"e2f68daf0e72777a8cf58bc83de1b98634b251e537ddbfca24b0ae50d1802da2",
),
"OFL-Xiaolai.txt": (
4432,
"0df7e09be4c2c850a48bd8beb9cd64b343aad49cd5d3f6cfb2ad2e3d28a56ca4",
),
}
def expected_manifest():
return json.loads(resources.files(__package__).joinpath(
"data", "font-manifest.json",
).read_bytes())
def _valid(path, size, digest):
if not path.is_file() or path.stat().st_size != size:
return False
checksum = hashlib.sha256()
with path.open("rb") as stream:
for chunk in iter(lambda: stream.read(65536), b""):
checksum.update(chunk)
return checksum.hexdigest() == digest
def _atomic_write(path, content):
temporary = None
try:
with tempfile.NamedTemporaryFile(
dir=path.parent, suffix=".download", delete=False,
) as stream:
temporary = Path(stream.name)
stream.write(content)
temporary.replace(path)
finally:
if temporary is not None:
temporary.unlink(missing_ok=True)
def ensure_font(data_dir, source="Github", service_url=""):
"""校验全部缓存文件;无效时下载,始终同时保留完整 OFL 许可。"""
validate_options(source, service_url)
manifest = expected_manifest()
root = Path(data_dir) / "fonts" / manifest["sha256"]
with _LOCK:
if all(_valid(root / name, *checks)
for name, checks in FILES.items()):
return root / FONT_NAME
root.mkdir(parents=True, exist_ok=True)
archive_path = root / "assets.zip"
if not _valid(archive_path, manifest["size_bytes"],
manifest["sha256"]):
url = (
service_url.rstrip("/") + "/v1/fonts/"
+ manifest["sha256"] + "/assets.zip"
if source == "farm_service"
else manifest["sources"][source.lower()]
)
temporary = None
try:
with tempfile.NamedTemporaryFile(
dir=root, suffix=".download", delete=False,
) as stream:
temporary = Path(stream.name)
_copy_remote(url, stream, manifest["size_bytes"])
if not _valid(temporary, manifest["size_bytes"],
manifest["sha256"]):
raise ValueError("Font archive size or SHA256 mismatch")
temporary.replace(archive_path)
finally:
if temporary is not None:
temporary.unlink(missing_ok=True)
with ZipFile(archive_path) as archive:
entries = archive.infolist()
if (len(entries) != len(FILES)
or {item.filename for item in entries} != set(FILES)):
raise ValueError("Unexpected font archive contents")
content = {}
for item in entries:
size, digest = FILES[item.filename]
if item.file_size != size or item.flag_bits & 1:
raise ValueError("Invalid font archive member")
value = archive.read(item)
if hashlib.sha256(value).hexdigest() != digest:
raise ValueError("Font member SHA256 mismatch")
content[item.filename] = value
# 先写许可,再使字体可用;不使用 extractall 或远端提供的路径。
for name in ("OFL-Xiaolai.txt", FONT_NAME):
_atomic_write(root / name, content[name])
return root / FONT_NAME
+9 -4
View File
@@ -1,9 +1,10 @@
"""从一次快照与统一时间构造可缓存场景;不访问存储。"""
import hashlib
from dataclasses import replace
from farm_core.domain import Identity, Snapshot, integer
from farm_core.pests import advance_plot
from farm_core.pests import advance_plot, season_duration
from .models import PlotView, Scene
@@ -53,8 +54,7 @@ def scene_from_snapshot(identity, snapshot, now, metadata):
))
continue
phases = tuple(dict.fromkeys(int(p) for p in info["phase"].split(",")))
duration = (plot.plant.growth_seconds if plot.harvest_count == 0
else plot.plant.regrowth_seconds)
duration = season_duration(plot)
elapsed = max(0, now - (plot.mature_at - duration))
work = (elapsed * 100 if plot.growth_remaining is None else
duration * 100 - plot.growth_remaining)
@@ -72,5 +72,10 @@ def scene_from_snapshot(identity, snapshot, now, metadata):
pest_frames=frames,
))
return Scene(
scope, snapshot.account, tuple(views),
scope, snapshot.account, tuple(
replace(view, land_level=plot.land_level)
for view, plot in zip(views, sorted(
snapshot.plots, key=lambda p: p.slot,
))
),
)
+1
View File
@@ -57,6 +57,7 @@ class PlotView:
stage: int = 0 # 本季展示阶段。
pest_count: int = 0 # 当前可见虫数;参与成图缓存键。
pest_frames: tuple[str, ...] = () # 按虫序稳定选择的包内帧,参与缓存键。
land_level: int = 0 # 土地等级;影响底图和成图缓存。
@dataclass(frozen=True)
+8 -7
View File
@@ -133,8 +133,9 @@ class FarmRenderer:
)
def _base(self, scene, options):
slots = frozenset(p.slot for p in scene.plots)
key = ("base", options.theme, options.width, slots)
slots = {p.slot: p.land_level for p in scene.plots}
key = ("base", options.theme, options.width,
tuple(sorted(slots.items())))
cached = self.assets.cache.get(key)
if cached is not None:
return Canvas(cached.copy())
@@ -149,14 +150,14 @@ class FarmRenderer:
soil_size = tuple(max(1, s(n)) for n in (
self.assets.metadata["soil_size"]
))
tiles = {
True: self.assets.image("soil/普通土地.png", soil_size),
False: self.assets.image("soil/草土地.png", soil_size),
}
names = ('普通土地', '红土地', '黑土地', '金土地')
tiles = {level: self.assets.image(f'soil/{names[level]}.png', soil_size)
for level in set(slots.values())}
grass = self.assets.image('soil/草土地.png', soil_size)
expansion = False
for slot, pos in enumerate(self.assets.metadata["positions"], 1):
xy = (s(pos["x"]), s(pos["y"]))
canvas.paste(tiles[slot in slots], xy)
canvas.paste(tiles[slots[slot]] if slot in slots else grass, xy)
if slot not in slots and not expansion:
icon = self.assets.image(
"background/expansion.png", (max(1, s(90)),) * 2,