Better RSI validator script. (#3558)

This commit is contained in:
Pieter-Jan Briers
2022-12-07 23:50:29 +01:00
committed by GitHub
parent a3e9dea8d8
commit f51b7bbd99
2 changed files with 346 additions and 177 deletions

View File

@@ -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
View 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())