mirror of
https://github.com/space-wizards/RobustToolbox.git
synced 2026-02-15 03:30:53 +01:00
Better RSI validator script. (#3558)
This commit is contained in:
committed by
GitHub
parent
a3e9dea8d8
commit
f51b7bbd99
358
Schemas/rsi.json
358
Schemas/rsi.json
@@ -1,184 +1,188 @@
|
||||
{
|
||||
"$schema":"http://json-schema.org/draft-07/schema",
|
||||
"default":{},
|
||||
"description":"JSON Schema for SS14 RSI validation.",
|
||||
"examples":[
|
||||
{
|
||||
"version":1,
|
||||
"license":"CC-BY-SA-3.0",
|
||||
"copyright":"Taken from CODEBASE at COMMIT PERMALINK",
|
||||
"size":{
|
||||
"x":32,
|
||||
"y":32
|
||||
},
|
||||
"states":[
|
||||
{
|
||||
"name":"basic"
|
||||
},
|
||||
{
|
||||
"name":"basic-directions",
|
||||
"directions":4
|
||||
},
|
||||
{
|
||||
"name":"basic-delays",
|
||||
"delays":[
|
||||
[
|
||||
0.1,
|
||||
0.1
|
||||
]
|
||||
]
|
||||
},
|
||||
{
|
||||
"name":"basic-delays-directions",
|
||||
"directions":4,
|
||||
"delays":[
|
||||
[
|
||||
0.1,
|
||||
0.1
|
||||
],
|
||||
[
|
||||
0.1,
|
||||
0.1
|
||||
],
|
||||
[
|
||||
0.1,
|
||||
0.1
|
||||
],
|
||||
[
|
||||
0.1,
|
||||
0.1
|
||||
]
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"required":[
|
||||
"version",
|
||||
"license",
|
||||
"copyright",
|
||||
"size",
|
||||
"states"
|
||||
],
|
||||
"title":"RSI Schema",
|
||||
"type":"object",
|
||||
"properties":{
|
||||
"version":{
|
||||
"$id":"#/properties/version",
|
||||
"default":"",
|
||||
"description":"RSI version integer.",
|
||||
"title":"The version schema",
|
||||
"type":"integer"
|
||||
},
|
||||
"license":{
|
||||
"$id":"#/properties/license",
|
||||
"default":"",
|
||||
"description":"The license for the associated icon states. Restricted to SS14-compatible asset licenses.",
|
||||
"enum":[
|
||||
"CC-BY-SA-3.0",
|
||||
"CC-BY-SA-4.0",
|
||||
"CC-BY-NC-3.0",
|
||||
"CC-BY-NC-4.0",
|
||||
"CC-BY-NC-SA-3.0",
|
||||
"CC-BY-NC-SA-4.0",
|
||||
"CC0-1.0"
|
||||
],
|
||||
"examples":[
|
||||
"CC-BY-SA-3.0"
|
||||
],
|
||||
"title":"License",
|
||||
"type":"string"
|
||||
},
|
||||
"copyright":{
|
||||
"$id":"#/properties/copyright",
|
||||
"type":"string",
|
||||
"title":"Copyright Info",
|
||||
"description":"The copyright holder. This is typically a link to the commit of the codebase that the icon is pulled from.",
|
||||
"default":"",
|
||||
"examples":[
|
||||
"Taken from CODEBASE at COMMIT LINK"
|
||||
]
|
||||
},
|
||||
"size":{
|
||||
"$id":"#/properties/size",
|
||||
"default":{
|
||||
|
||||
},
|
||||
"description":"The dimensions of the sprites inside the RSI. This is not the size of the PNG files that store the sprite sheet.",
|
||||
"examples":[
|
||||
{
|
||||
"x":32,
|
||||
"y":32
|
||||
}
|
||||
],
|
||||
"title":"Sprite Dimensions",
|
||||
"required":[
|
||||
"x",
|
||||
"y"
|
||||
],
|
||||
"type":"object",
|
||||
"properties":{
|
||||
"x":{
|
||||
"$id":"#/properties/size/properties/x",
|
||||
"type":"integer",
|
||||
"default":32,
|
||||
"examples":[
|
||||
32
|
||||
]
|
||||
},
|
||||
"y":{
|
||||
"$id":"#/properties/size/properties/y",
|
||||
"type":"integer",
|
||||
"default":32,
|
||||
"examples":[
|
||||
32
|
||||
]
|
||||
}
|
||||
},
|
||||
"additionalProperties":true
|
||||
},
|
||||
"states":{
|
||||
"$id":"#/properties/states",
|
||||
"type":"array",
|
||||
"title":"Icon States",
|
||||
"description":"Metadata for icon states. Includes name, directions, delays, etc.",
|
||||
"default":[
|
||||
|
||||
],
|
||||
"examples":[
|
||||
[
|
||||
"$schema": "http://json-schema.org/draft-07/schema",
|
||||
"default": {},
|
||||
"description": "JSON Schema for SS14 RSI validation.",
|
||||
"examples": [
|
||||
{
|
||||
"version": 1,
|
||||
"license": "CC-BY-SA-3.0",
|
||||
"copyright": "Taken from CODEBASE at COMMIT PERMALINK",
|
||||
"size": {
|
||||
"x": 32,
|
||||
"y": 32
|
||||
},
|
||||
"states": [
|
||||
{
|
||||
"name":"basic"
|
||||
"name": "basic"
|
||||
},
|
||||
{
|
||||
"name":"basic-directions",
|
||||
"directions":4
|
||||
}
|
||||
]
|
||||
],
|
||||
"additionalItems":true,
|
||||
"items":{
|
||||
"$id":"#/properties/states/items",
|
||||
"type":"object",
|
||||
"required":[
|
||||
"name"
|
||||
],
|
||||
"properties":{
|
||||
"name":{
|
||||
"type":"string"
|
||||
"name": "basic-directions",
|
||||
"directions": 4
|
||||
},
|
||||
"directions":{
|
||||
"type":"integer",
|
||||
"enum":[
|
||||
1,
|
||||
4,
|
||||
8
|
||||
]
|
||||
{
|
||||
"name": "basic-delays",
|
||||
"delays": [
|
||||
[
|
||||
0.1,
|
||||
0.1
|
||||
]
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "basic-delays-directions",
|
||||
"directions": 4,
|
||||
"delays": [
|
||||
[
|
||||
0.1,
|
||||
0.1
|
||||
],
|
||||
[
|
||||
0.1,
|
||||
0.1
|
||||
],
|
||||
[
|
||||
0.1,
|
||||
0.1
|
||||
],
|
||||
[
|
||||
0.1,
|
||||
0.1
|
||||
]
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"required": [
|
||||
"version",
|
||||
"license",
|
||||
"copyright",
|
||||
"size",
|
||||
"states"
|
||||
],
|
||||
"title": "RSI Schema",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"version": {
|
||||
"$id": "#/properties/version",
|
||||
"default": "",
|
||||
"description": "RSI version integer.",
|
||||
"title": "The version schema",
|
||||
"type": "integer"
|
||||
},
|
||||
"license": {
|
||||
"$id": "#/properties/license",
|
||||
"default": "",
|
||||
"description": "The license for the associated icon states. Restricted to SS14-compatible asset licenses.",
|
||||
"enum": [
|
||||
"CC-BY-SA-3.0",
|
||||
"CC-BY-SA-4.0",
|
||||
"CC-BY-NC-3.0",
|
||||
"CC-BY-NC-4.0",
|
||||
"CC-BY-NC-SA-3.0",
|
||||
"CC-BY-NC-SA-4.0",
|
||||
"CC0-1.0"
|
||||
],
|
||||
"examples": [
|
||||
"CC-BY-SA-3.0"
|
||||
],
|
||||
"title": "License",
|
||||
"type": "string"
|
||||
},
|
||||
"copyright": {
|
||||
"$id": "#/properties/copyright",
|
||||
"type": "string",
|
||||
"title": "Copyright Info",
|
||||
"description": "The copyright holder. This is typically a link to the commit of the codebase that the icon is pulled from.",
|
||||
"default": "",
|
||||
"examples": [
|
||||
"Taken from CODEBASE at COMMIT LINK"
|
||||
]
|
||||
},
|
||||
"size": {
|
||||
"$id": "#/properties/size",
|
||||
"default": {},
|
||||
"description": "The dimensions of the sprites inside the RSI. This is not the size of the PNG files that store the sprite sheet.",
|
||||
"examples": [
|
||||
{
|
||||
"x": 32,
|
||||
"y": 32
|
||||
}
|
||||
],
|
||||
"title": "Sprite Dimensions",
|
||||
"required": [
|
||||
"x",
|
||||
"y"
|
||||
],
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"x": {
|
||||
"$id": "#/properties/size/properties/x",
|
||||
"type": "integer",
|
||||
"default": 32,
|
||||
"examples": [
|
||||
32
|
||||
]
|
||||
},
|
||||
"y": {
|
||||
"$id": "#/properties/size/properties/y",
|
||||
"type": "integer",
|
||||
"default": 32,
|
||||
"examples": [
|
||||
32
|
||||
]
|
||||
}
|
||||
},
|
||||
"additionalProperties": true
|
||||
},
|
||||
"states": {
|
||||
"$id": "#/properties/states",
|
||||
"type": "array",
|
||||
"title": "Icon States",
|
||||
"description": "Metadata for icon states. Includes name, directions, delays, etc.",
|
||||
"default": [],
|
||||
"examples": [
|
||||
[
|
||||
{
|
||||
"name": "basic"
|
||||
},
|
||||
{
|
||||
"name": "basic-directions",
|
||||
"directions": 4
|
||||
}
|
||||
]
|
||||
],
|
||||
"additionalItems": true,
|
||||
"items": {
|
||||
"$id": "#/properties/states/items",
|
||||
"type": "object",
|
||||
"required": [
|
||||
"name"
|
||||
],
|
||||
"properties": {
|
||||
"name": {
|
||||
"type": "string"
|
||||
},
|
||||
"directions": {
|
||||
"type": "integer",
|
||||
"enum": [
|
||||
1,
|
||||
4,
|
||||
8
|
||||
]
|
||||
},
|
||||
"delays": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "number"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"additionalProperties":true
|
||||
}
|
||||
|
||||
"additionalProperties": true
|
||||
}
|
||||
|
||||
165
Schemas/validate_rsis.py
Normal file
165
Schemas/validate_rsis.py
Normal file
@@ -0,0 +1,165 @@
|
||||
#!/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
|
||||
|
||||
# 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())
|
||||
Reference in New Issue
Block a user