216 lines
7.4 KiB
Python
216 lines
7.4 KiB
Python
import json, logging, ast, typing
|
|
import common, bme
|
|
from common import AssetKind
|
|
import pydantic
|
|
|
|
#region Assistant Checker
|
|
|
|
# TODO:
|
|
# If possible, following check should be done.
|
|
# They are not done now because they are so complex to implement.
|
|
# - The reference to variables and functions in programmable fields.
|
|
# - The return type of prorgammable fields.
|
|
# - Texture name referred in the programmable field in Face.
|
|
# - In instance, passed params to instance is fulfilled.
|
|
|
|
|
|
def _try_add(entries: set[str], entry: str) -> bool:
|
|
if entry in entries:
|
|
return False
|
|
else:
|
|
entries.add(entry)
|
|
return True
|
|
|
|
|
|
def _check_programmable_field(probe: str) -> None:
|
|
# TODO:
|
|
# If possible, allow checking the reference to variables and function,
|
|
# to make sure the statement must can be executed.
|
|
try:
|
|
ast.parse(probe)
|
|
except SyntaxError:
|
|
logging.error(f'String {probe} may not be a valid Python statement which is not suit for programmable field.')
|
|
|
|
|
|
def _check_showcase_icon(icon_name: str) -> None:
|
|
icon_path = common.get_raw_assets_folder(AssetKind.Icons) / 'bme' / f'{icon_name}.png'
|
|
if not icon_path.is_file():
|
|
logging.error(f'Showcase icon value {icon_name} may be invalid.')
|
|
|
|
|
|
#endregion
|
|
|
|
#region Core Validator
|
|
|
|
|
|
def _pre_validate_prototype(prototype: bme.Prototype, identifiers: set[str]) -> None:
|
|
identifier = prototype.identifier
|
|
|
|
# Show status
|
|
logging.info(f'Pre-checking prototype {identifier}')
|
|
|
|
# Check identifier and add it.
|
|
if not _try_add(identifiers, identifier):
|
|
logging.error(f'Identifier {identifier} is already registered.')
|
|
|
|
|
|
def _validate_showcase(showcase: bme.Showcase, variables: set[str]) -> None:
|
|
# I18N Module Req:
|
|
# The title of showcase should not be empty
|
|
if len(showcase.title) == 0:
|
|
logging.error('The title of showcase should not be empty.')
|
|
|
|
# Check icon name
|
|
_check_showcase_icon(showcase.icon)
|
|
|
|
# Check configuration list.
|
|
for cfg in showcase.cfgs:
|
|
# Check name
|
|
field_name = cfg.field
|
|
if not _try_add(variables, field_name):
|
|
logging.error(f'Field {field_name} is already registered.')
|
|
|
|
# I18N Module Req:
|
|
# The title and desc of cfg should not be empty.
|
|
# And they are should not be the same string.
|
|
if len(cfg.title) == 0:
|
|
logging.error('The title of showcase configuration entry should not be empty.')
|
|
if len(cfg.desc) == 0:
|
|
logging.error('The description of showcase configuration entry should not be empty.')
|
|
if cfg.title == cfg.desc:
|
|
logging.error('The title of showcase configuration entry and its description should not be same string.')
|
|
|
|
# Check programmable field
|
|
_check_programmable_field(cfg.default)
|
|
|
|
|
|
def _validate_params(params: list[bme.Param], variables: set[str]) -> None:
|
|
for param in params:
|
|
# Check name
|
|
field_name = param.field
|
|
if not _try_add(variables, field_name):
|
|
logging.error(f'Field {field_name} is already registered.')
|
|
|
|
# Check programmable fields
|
|
_check_programmable_field(param.data)
|
|
|
|
|
|
def _validate_vars(vars: list[bme.Var], variables: set[str]) -> None:
|
|
for var in vars:
|
|
# Check name
|
|
field_name = var.field
|
|
if not _try_add(variables, field_name):
|
|
logging.error(f'Field {field_name} is already registered.')
|
|
|
|
# Check programmable fields
|
|
_check_programmable_field(var.data)
|
|
|
|
|
|
def _validate_vertices(vertices: list[bme.Vertex]) -> None:
|
|
for vertex in vertices:
|
|
# Check programmable fields
|
|
_check_programmable_field(vertex.skip)
|
|
_check_programmable_field(vertex.data)
|
|
|
|
|
|
def _validate_faces(faces: list[bme.Face], vertices_count: int) -> None:
|
|
for face in faces:
|
|
# The index referred in indices should not be exceed the max value of vertices count.
|
|
for index in face.indices:
|
|
if index >= vertices_count:
|
|
logging.error(f'Index {index} is out of vertices range.')
|
|
|
|
# The size of uvs list and normals list (if existing)
|
|
# should be equal to the size of indices list.
|
|
edges = len(face.indices)
|
|
if len(face.uvs) != edges:
|
|
logging.error(f'The size of UVs list is not matched with indices.')
|
|
if face.normals is not None and len(face.normals) != edges:
|
|
logging.error(f'The size of Normals list is not matched with indices.')
|
|
|
|
# Check programmable fields
|
|
_check_programmable_field(face.skip)
|
|
_check_programmable_field(face.texture)
|
|
for uv in face.uvs:
|
|
_check_programmable_field(uv)
|
|
if face.normals is not None:
|
|
for normal in face.normals:
|
|
_check_programmable_field(normal)
|
|
|
|
|
|
def _validate_instances(instances: list[bme.Instance], identifiers: set[str]) -> None:
|
|
for instance in instances:
|
|
# The reference of identifier should be existing.
|
|
referred_identifier = instance.identifier
|
|
if referred_identifier not in identifiers:
|
|
logging.error(f'The identifier {referred_identifier} referred in instance is not existing.')
|
|
|
|
# Check programmable fields
|
|
_check_programmable_field(instance.skip)
|
|
for v in instance.params.values():
|
|
_check_programmable_field(v)
|
|
_check_programmable_field(instance.transform)
|
|
|
|
|
|
def _validate_prototype(prototype: bme.Prototype, identifiers: set[str]) -> None:
|
|
# Show status
|
|
logging.info(f'Checking prototype {prototype.identifier}')
|
|
|
|
# A set of all variable names registered in this prototypes
|
|
variables: set[str] = set()
|
|
|
|
# Check fields
|
|
if prototype.showcase is not None:
|
|
_validate_showcase(prototype.showcase, variables)
|
|
_validate_params(prototype.params, variables)
|
|
_check_programmable_field(prototype.skip)
|
|
_validate_vars(prototype.vars, variables)
|
|
_validate_vertices(prototype.vertices)
|
|
_validate_faces(prototype.faces, len(prototype.vertices))
|
|
_validate_instances(prototype.instances, identifiers)
|
|
|
|
|
|
#endregion
|
|
|
|
|
|
def validate_jsons() -> None:
|
|
raw_jsons_dir = common.get_raw_assets_folder(AssetKind.Jsons)
|
|
|
|
# Load all prototypes and check their basic format
|
|
prototypes: list[bme.Prototype] = []
|
|
for raw_json_file in raw_jsons_dir.glob('*.json'):
|
|
# Skip non-file
|
|
if not raw_json_file.is_file():
|
|
continue
|
|
|
|
# Show info
|
|
logging.info(f'Loading {raw_json_file}')
|
|
|
|
# Load prototypes
|
|
try:
|
|
with open(raw_json_file, 'r', encoding='utf-8') as f:
|
|
docuement = json.load(f)
|
|
file_prototypes = bme.Prototypes.model_validate(docuement)
|
|
except json.JSONDecodeError as e:
|
|
logging.error(f'File {raw_json_file} is not a valid JSON file. Reason: {e}')
|
|
except pydantic.ValidationError as e:
|
|
logging.error(f'JSON file {raw_json_file} lose essential fields. Detail: {e}')
|
|
|
|
# Append all prototypes into list
|
|
prototypes += file_prototypes.root
|
|
|
|
# Pre-validate first to collect identifier and check identifier first.
|
|
# We need collect it first because "instances" field need it to check the validation of identifier.
|
|
identifiers: set[str] = set()
|
|
for prototype in prototypes:
|
|
_pre_validate_prototype(prototype, identifiers)
|
|
|
|
# Start custom validation
|
|
for prototype in prototypes:
|
|
_validate_prototype(prototype, identifiers)
|
|
|
|
|
|
if __name__ == '__main__':
|
|
common.setup_logging()
|
|
validate_jsons()
|