This commit is contained in:
Mikan
2026-06-19 11:28:04 +03:00
commit 53c89829a8
80 changed files with 12482 additions and 0 deletions

View File

@@ -0,0 +1,108 @@
"""World-state JSON schema validator (player/NPC stats, inventory, etc.)."""
from __future__ import annotations
from typing import Any, Dict, List, Tuple
from jsonschema import ValidationError, validate
from app.logging_setup import get_logger
log = get_logger("state_validator")
def validate_state(state: Dict[str, Any], schema: Dict[str, Any]) -> Tuple[bool, List[str]]:
"""Validate state against world's JSON Schema. Returns (ok, errors)."""
if not schema:
return True, []
try:
validate(instance=state, schema=schema)
return True, []
except ValidationError as e:
return False, [f"{e.message} at path {list(e.absolute_path)}"]
except Exception as e:
return False, [f"schema_error: {e}"]
def apply_patch(state: Dict[str, Any], patch: Dict[str, Any]) -> Dict[str, Any]:
"""Apply a JSON-patch-like update to state.
Patch format:
{"set": {"path.to.field": value, ...},
"unset": ["path.to.field", ...],
"append": {"path.to.list": value, ...},
"increment": {"path.to.number": delta, ...}}
Paths use dot notation. Creates intermediate dicts as needed.
"""
if not patch:
return state
new_state = _deep_copy(state)
for op, items in patch.items():
if op == "set":
for path, value in items.items():
_set_path(new_state, path, value)
elif op == "unset":
for path in items:
_unset_path(new_state, path)
elif op == "append":
for path, value in items.items():
lst = _get_path(new_state, path) or []
if not isinstance(lst, list):
lst = []
lst.append(value)
_set_path(new_state, path, lst)
elif op == "increment":
for path, delta in items.items():
cur = _get_path(new_state, path) or 0
try:
cur = float(cur)
except (TypeError, ValueError):
cur = 0
_set_path(new_state, path, cur + delta)
elif op == "remove":
for path, value in items.items():
lst = _get_path(new_state, path) or []
if isinstance(lst, list):
lst = [x for x in lst if x != value]
_set_path(new_state, path, lst)
return new_state
def _deep_copy(obj: Any) -> Any:
if isinstance(obj, dict):
return {k: _deep_copy(v) for k, v in obj.items()}
if isinstance(obj, list):
return [_deep_copy(v) for v in obj]
return obj
def _get_path(obj: Any, path: str) -> Any:
cur = obj
for part in path.split("."):
if isinstance(cur, dict) and part in cur:
cur = cur[part]
else:
return None
return cur
def _set_path(obj: Dict[str, Any], path: str, value: Any) -> None:
cur = obj
parts = path.split(".")
for part in parts[:-1]:
if part not in cur or not isinstance(cur[part], dict):
cur[part] = {}
cur = cur[part]
cur[parts[-1]] = value
def _unset_path(obj: Dict[str, Any], path: str) -> None:
cur = obj
parts = path.split(".")
for part in parts[:-1]:
if not isinstance(cur, dict) or part not in cur:
return
cur = cur[part]
if isinstance(cur, dict):
cur.pop(parts[-1], None)