mirror of
https://github.com/Shu-Ying/farm_service.git
synced 2026-11-02 10:39:59 +08:00
139 lines
6.3 KiB
Python
139 lines
6.3 KiB
Python
"""Independent Service-side render contract; never imports a Farm checkout."""
|
|
|
|
import math
|
|
from pathlib import PurePosixPath
|
|
|
|
SOIL_MEMBERS = {"soil/红土地.png", "soil/黑土地.png", "soil/金土地.png"}
|
|
REQUIRED_IMAGES = {"webgl/seed.png", "webgl/wither.png", "background/ripe.png",
|
|
"soil/草土地.png", "soil/普通土地.png"}
|
|
|
|
|
|
def numbers(values, length):
|
|
if (type(values) is not list or len(values) != length
|
|
or any(type(v) not in (int, float) or abs(v) > 10000000
|
|
or not math.isfinite(v) for v in values)):
|
|
raise ValueError("invalid render coordinates")
|
|
return values
|
|
|
|
|
|
def image_size(values):
|
|
numbers(values, 2)
|
|
if (any(type(v) is not int or not 0 < v <= 8192 for v in values)
|
|
or values[0] * values[1] > 32 * 1024 * 1024):
|
|
raise ValueError("invalid image dimensions")
|
|
|
|
|
|
def rectangle(values):
|
|
numbers(values, 4)
|
|
if values[2] <= 0 or values[3] <= 0:
|
|
raise ValueError("invalid render rectangle")
|
|
|
|
|
|
def safe_member(name):
|
|
if (not isinstance(name, str) or not name or len(name) > 512
|
|
or "\\" in name or ":" in name):
|
|
raise ValueError("invalid image path")
|
|
path = PurePosixPath(name)
|
|
if path.is_absolute() or any(p in ("", ".", "..") for p in name.split("/")):
|
|
raise ValueError("invalid image path")
|
|
return name
|
|
|
|
|
|
def validate_metadata(value, rows):
|
|
"""Validate every renderer-required field; keep legacy phase placeholders."""
|
|
try:
|
|
if type(value["schema"]) is not int or value["schema"] != 1:
|
|
raise ValueError("unknown render metadata schema")
|
|
image_size(value["canvas"])
|
|
image_size(value["soil_size"])
|
|
if type(value["positions"]) is not list or len(value["positions"]) != 30:
|
|
raise ValueError("expected 30 plot positions")
|
|
for position in value["positions"]:
|
|
numbers([position["x"], position["y"]], 2)
|
|
images = value["images"]
|
|
if type(images) is not dict or not 0 < len(images) <= 10000:
|
|
raise ValueError("invalid image manifest")
|
|
for name, item in images.items():
|
|
safe_member(name)
|
|
image_size(item["size"])
|
|
checksum = item["sha256"]
|
|
if (not isinstance(checksum, str) or len(checksum) != 64
|
|
or any(c not in "0123456789abcdef" for c in checksum)):
|
|
raise ValueError("invalid image checksum")
|
|
plants = value["plants"]
|
|
if type(plants) is not dict or set(plants) != {row["name"] for row in rows}:
|
|
raise ValueError("catalogue and render plants differ")
|
|
for row in rows:
|
|
if plants[row["name"]]["phase"] != row["phase"]:
|
|
raise ValueError("catalogue and render phases differ")
|
|
placement = value["placement"]
|
|
if (type(placement["schema"]) is not int or placement["schema"] != 4
|
|
or placement["mode"] != "visible_contact_center"):
|
|
raise ValueError("unknown crop placement")
|
|
for key in ("common_scale", "growth_scale", "reference_soil_height",
|
|
"reference_soil_width"):
|
|
numbers([placement[key]], 1)
|
|
if placement[key] <= 0:
|
|
raise ValueError("invalid crop scale")
|
|
numbers([placement["contact_alpha_threshold"]], 1)
|
|
numbers([placement["contact_band_fraction"]], 1)
|
|
if (not 0 <= placement["contact_alpha_threshold"] <= 255
|
|
or not 0 < placement["contact_band_fraction"] <= 1):
|
|
raise ValueError("invalid ground contact rule")
|
|
matrix = placement["scene_transform"]
|
|
if type(matrix) is not list or len(matrix) != 2:
|
|
raise ValueError("invalid scene transform")
|
|
a, b = numbers(matrix[0], 2)
|
|
c, d = numbers(matrix[1], 2)
|
|
if abs(a * d - b * c) < 1e-8:
|
|
raise ValueError("degenerate scene transform")
|
|
pest = value["pest"]
|
|
image_size(pest["display_size"])
|
|
frames = pest["frames"]
|
|
if (type(frames) is not list or len(frames) < 3
|
|
or len(set(frames)) != len(frames)):
|
|
raise ValueError("invalid pest frames")
|
|
if any(name not in images for name in frames):
|
|
raise ValueError("missing pest image")
|
|
if type(pest["positions"]) is not list or len(pest["positions"]) < 3:
|
|
raise ValueError("missing pest positions")
|
|
for position in pest["positions"]:
|
|
numbers(position, 2)
|
|
panel = value["panel"]
|
|
image_size(panel["reference_canvas"])
|
|
rectangle(panel["avatar"])
|
|
fields = {"name", "experience", "level", "point", "vip_point"}
|
|
if (type(panel["fields"]) is not list
|
|
or {entry["field"] for entry in panel["fields"]} != fields):
|
|
raise ValueError("invalid panel fields")
|
|
for entry in panel["fields"]:
|
|
rectangle(entry["rect"])
|
|
numbers(entry["color"], 3)
|
|
if (any(not 0 <= v <= 255 for v in entry["color"])
|
|
or type(entry["alignment"]) is not int
|
|
or entry["alignment"] not in (3, 4)):
|
|
raise ValueError("invalid panel text")
|
|
if type(panel["sprites"]) is not list:
|
|
raise ValueError("invalid panel sprites")
|
|
for entry in panel["sprites"]:
|
|
rectangle(entry["rect"])
|
|
if entry["sprite"] not in images:
|
|
raise ValueError("missing panel sprite")
|
|
if set(value["decorations"]) != {"default", "spring", "summer",
|
|
"autumn", "winter", "bee"}:
|
|
raise ValueError("invalid decoration themes")
|
|
for entries in value["decorations"].values():
|
|
if type(entries) is not list:
|
|
raise ValueError("invalid decorations")
|
|
for entry in entries:
|
|
numbers(entry["position"], 2)
|
|
numbers(entry["size"], 2)
|
|
if (min(entry["size"]) <= 0
|
|
or entry["layer"] not in ("background", "foreground")
|
|
or entry["sprite"] not in images):
|
|
raise ValueError("invalid decoration")
|
|
if not REQUIRED_IMAGES <= set(images):
|
|
raise ValueError("missing required farm sprite")
|
|
except (KeyError, TypeError, IndexError, AttributeError) as error:
|
|
raise ValueError("invalid render metadata") from error
|