Files
RobustToolbox/Schemas/validate_rsis.py
leonidussaks 41b2ee19a1 add check if state name is empty in rsi validator (#6145)
* add check if state name exists

* Apply suggestions from code review

---------

Co-authored-by: Pieter-Jan Briers <pieterjan.briers@gmail.com>
2025-08-20 18:49:45 +02:00

173 lines
5.5 KiB
Python
Executable File

#!/usr/bin/env python3
import argparse
import json
import os
from PIL import Image
from glob import iglob
from jsonschema import Draft7Validator, ValidationError
from typing import Any, List, Optional
ALLOWED_RSI_DIR_GARBAGE = {
"meta.json",
".DS_Store",
"thumbs.db",
".directory"
}
errors: List["RsiError"] = []
def main() -> int:
parser = argparse.ArgumentParser("validate_rsis.py", description="Validates RSI file integrity for mistakes the engine does not catch while loading.")
parser.add_argument("directories", nargs="+", help="Directories to look for RSIs in")
args = parser.parse_args()
schema = load_schema()
for dir in args.directories:
check_dir(dir, schema)
for error in errors:
print(f"{error.path}: {error.message}")
return 1 if errors else 0
def check_dir(dir: str, schema: Draft7Validator):
for rsi_rel in iglob("**/*.rsi", root_dir=dir, recursive=True):
rsi_path = os.path.join(dir, rsi_rel)
try:
check_rsi(rsi_path, schema)
except Exception as e:
add_error(rsi_path, f"Failed to validate RSI (script bug): {e}")
def check_rsi(rsi: str, schema: Draft7Validator):
meta_path = os.path.join(rsi, "meta.json")
# Try to load meta.json
try:
meta_json = read_json(meta_path)
except Exception as e:
add_error(rsi, f"Failed to read meta.json: {e}")
return
# Check if meta.json passes schema.
schema_errors: List[ValidationError] = list(schema.iter_errors(meta_json))
if schema_errors:
for error in schema_errors:
add_error(rsi, f"meta.json: [{error.json_path}] {error.message}")
# meta.json may be corrupt, can't safely proceed.
return
state_names = {state["name"] for state in meta_json["states"]}
# Go over contents of RSI directory and ensure there is no extra garbage.
for name in os.listdir(rsi):
if name in ALLOWED_RSI_DIR_GARBAGE:
continue
if not name.endswith(".png"):
add_error(rsi, f"Illegal file inside RSI: {name}")
continue
# All PNGs must be defined in the meta.json
png_state_name = name[:-4]
if png_state_name not in state_names:
add_error(rsi, f"PNG not defined in metadata: {name}")
# Validate state delays.
for state in meta_json["states"]:
state_name: str = state["name"]
# Validate state delays.
delays: Optional[List[List[float]]] = state.get("delays")
if not delays:
continue
# Validate directions count in metadata and delays count matches.
directions: int = state.get("directions", 1)
if directions != len(delays):
add_error(rsi, f"{state_name}: direction count ({directions}) doesn't match delay set specified ({len(delays)})")
continue
# Validate that each direction array has the same length.
lengths: List[float] = []
for dir in delays:
# Robust rounds to millisecond precision.
lengths.append(round(sum(dir), 3))
if any(l != lengths[0] for l in lengths):
add_error(rsi, f"{state_name}: mismatching total durations between state directions: {', '.join(map(str, lengths))}")
frame_width = meta_json["size"]["x"]
frame_height = meta_json["size"]["y"]
# Validate state PNGs.
# We only check they're the correct size and that they actually exist and load.
for state in meta_json["states"]:
state_name: str = state["name"]
png_name = os.path.join(rsi, f"{state_name}.png")
try:
image = Image.open(png_name)
except Exception as e:
add_error(rsi, f"{state_name}: failed to open state {state_name}.png")
continue
# Check that size is a multiple of the metadata frame size.
size = image.size
if size[0] % frame_width != 0 or size[1] % frame_height != 0:
add_error(rsi, f"{state_name}: sprite sheet of {size[0]}x{size[1]} is not size multiple of RSI size ({frame_width}x{frame_height}).png")
continue
# Check that the sprite sheet is big enough to possibly fit all the frames listed in metadata.
frames_w = size[0] // frame_width
frames_h = size[1] // frame_height
directions: int = state.get("directions", 1)
delays: Optional[List[List[float]]] = state.get("delays", [[1]] * directions)
frame_count = sum(map(len, delays))
max_sheet_frames = frames_w * frames_h
if frame_count > max_sheet_frames:
add_error(rsi, f"{state_name}: sprite sheet of {size[0]}x{size[1]} is too small, metadata defines {frame_count} frames, but it can only fit {max_sheet_frames} at most")
continue
# Check if state name exists
for state in meta_json["states"]:
state_name: str = state["name"]
if state_name == "":
add_error(rsi, f"state name cannot be an empty string.")
return
# We're good!
return
def load_schema() -> Draft7Validator:
base_path = os.path.dirname(os.path.realpath(__file__))
schema_path = os.path.join(base_path, "rsi.json")
schema_json = read_json(schema_path)
return Draft7Validator(schema_json)
def read_json(path: str) -> Any:
with open(path, "r", encoding="utf-8-sig") as f:
return json.load(f)
def add_error(rsi: str, message: str):
errors.append(RsiError(rsi, message))
class RsiError:
def __init__(self, path: str, message: str):
self.path = path
self.message = message
exit(main())