FahimIA/Schema
0
1import gradio as gr2import json3from typing import Dict, List, Optional, Any, Tuple4from dataclasses import dataclass, field5from enum import Enum6import re7import tempfile8import os9import zipfile10 11 12# -----------------------------13# Data model14# -----------------------------15 16class FieldType(Enum):17 """Supported C# field types"""18 # Primitive types19 BYTE = "byte"20 SBYTE = "sbyte"21 SHORT = "short"22 USHORT = "ushort"23 INT = "int"24 UINT = "uint"25 LONG = "long"26 ULONG = "ulong"27 FLOAT = "float"28 DOUBLE = "double"29 BOOL = "bool"30 CHAR = "char"31 STRING = "string"32 33 # Unity types34 FLOAT2 = "float2"35 FLOAT3 = "float3"36 FLOAT4 = "float4"37 QUATERNION = "quaternion"38 FIXEDSTRING32 = "FixedString32Bytes"39 FIXEDSTRING64 = "FixedString64Bytes"40 FIXEDSTRING128 = "FixedString128Bytes"41 42 # Unity ECS types43 BLOB_ARRAY = "BlobArray"44 BLOB_ASSET_REFERENCE = "BlobAssetReference"45 ENTITY = "Entity"46 47 # Custom schema reference48 SCHEMA_REFERENCE = "SchemaReference"49 50 51@dataclass52class SchemaField:53 """Represents a field in a schema"""54 name: str55 field_type: str56 is_blob_array: bool = False57 default_value: Optional[str] = None58 is_reference: bool = False59 reference_type: Optional[str] = None60 description: Optional[str] = None61 62 63@dataclass64class SchemaDefinition:65 """Complete schema definition"""66 name: str67 namespace: str68 fields: List[SchemaField] = field(default_factory=list)69 implements_has_id: bool = True70 implements_equatable: bool = True71 custom_interfaces: List[str] = field(default_factory=list)72 generate_schema_class: bool = True73 generate_create_node: bool = True74 generate_schema_node: bool = True75 generate_blob_component: bool = True76 generate_authoring: bool = True77 folder_path: str = "Assets/Settings"78 menu_category: str = "Hold"79 80 81# -----------------------------82# Code generator83# -----------------------------84 85class CSharpCodeGenerator:86 """Generates C# code for schemas"""87 88 def __init__(self):89 self.using_statements = {90 "System",91 "Unity.Entities",92 "Unity.Collections",93 "Unity.Mathematics",94 "UnityEngine",95 "BovineLabs.Core.ObjectManagement"96 }97 98 def generate_data_struct(self, schema: SchemaDefinition) -> str:99 code_lines = []100 code_lines.extend([f"using {using};" for using in sorted(self.using_statements)])101 code_lines.append("")102 103 namespace = f"{schema.namespace}.Authoring.Data"104 code_lines.append(f"namespace {namespace}")105 code_lines.append("{")106 107 interfaces = []108 if schema.implements_has_id:109 interfaces.append("IHasID")110 if schema.implements_equatable:111 interfaces.append("IEquatable<ushort>")112 interfaces.extend(schema.custom_interfaces)113 interface_str = f" : {', '.join(interfaces)}" if interfaces else ""114 115 code_lines.append(f" [Serializable]")116 code_lines.append(f" public struct {schema.name}{interface_str}")117 code_lines.append(" {")118 119 # Fields120 for field in schema.fields:121 code_lines.append(f" {self._generate_field_declaration(field)}")122 123 code_lines.append("")124 125 # Implement IHasID126 if schema.implements_has_id:127 id_field = next((f for f in schema.fields if f.name == "id"), None)128 if id_field:129 code_lines.extend([130 " public int ID",131 " {",132 f" get => {id_field.name};",133 f" set => {id_field.name} = ({id_field.field_type})value;",134 " }",135 ""136 ])137 138 # Implement IEquatable139 if schema.implements_equatable:140 id_field = next((f for f in schema.fields if f.name == "id"), None)141 if id_field:142 code_lines.extend([143 f" public override int GetHashCode() => {id_field.name}.GetHashCode();",144 f" public bool Equals(ushort other) => {id_field.name} == other;",145 ""146 ])147 148 code_lines.append(" }")149 code_lines.append("}")150 return "\n".join(code_lines)151 152 def generate_schema_class(self, schema: SchemaDefinition) -> str:153 code_lines = []154 code_lines.extend([f"using {using};" for using in sorted(self.using_statements)])155 code_lines.append("")156 157 namespace = f"{schema.namespace}.Authoring.Schemas"158 code_lines.append(f"namespace {namespace}")159 code_lines.append("{")160 161 schema_class_name = f"{schema.name}Schema"162 type_string = schema.name.replace("Schema", "")163 164 code_lines.extend([165 f" [CreateAssetMenu(menuName = \"{schema.menu_category}/\" + TypeString + \"/Create \" + FieldName, fileName = FieldName)]",166 f" [AutoRef(",167 f" nameof({schema.name}Settings), nameof({schema.name}Settings.schemas),",168 f" FieldName, TypeString + \"/\" + FieldName",169 f" )]",170 f" public class {schema_class_name} : BakingSchema<{schema.name}>",171 " {",172 f" private const string FieldName = nameof({schema_class_name});",173 f" private const string TypeString = \"{type_string}\";",174 ""175 ])176 177 # Schema fields178 for field in schema.fields:179 if field.is_reference and not field.is_blob_array:180 code_lines.append(f" public {field.reference_type}Schema {field.name}Schema;")181 else:182 code_lines.append(f" {self._generate_schema_field_declaration(field)}")183 184 code_lines.append("")185 186 # ToData187 code_lines.extend([188 f" public override {schema.name} ToData()",189 " {",190 f" return new {schema.name}",191 " {"192 ])193 for field in schema.fields:194 if field.name == "id":195 code_lines.append(" id = (ushort)ID,")196 elif field.is_reference and not field.is_blob_array:197 code_lines.append(f" {field.name} = (ushort){field.name}Schema.ID,")198 elif field.is_blob_array:199 # Blob arrays handled in blob builder methods200 continue201 else:202 code_lines.append(f" {field.name} = {field.name},")203 code_lines.extend([204 " };",205 " }",206 ""207 ])208 209 # Blob conversion methods if any blob arrays210 if any(f.is_blob_array for f in schema.fields):211 code_lines.extend(self._generate_blob_conversion_methods(schema))212 213 code_lines.append(" }")214 code_lines.append("}")215 return "\n".join(code_lines)216 217 def generate_blob_component(self, schema: SchemaDefinition) -> str:218 code_lines = []219 using_statements = [220 f"{schema.namespace}.Authoring.Data",221 "Unity.Entities",222 "Unity.Collections"223 ]224 code_lines.extend([f"using {using};" for using in using_statements])225 code_lines.append("")226 227 namespace = f"{schema.namespace}.Authoring.BlobComponents"228 code_lines.append(f"namespace {namespace}")229 code_lines.append("{")230 struct_name = f"{schema.name}Blob"231 code_lines.extend([232 f" public struct {struct_name} : IComponentData",233 " {",234 f" public BlobAssetReference<BlobArray<{schema.name}>> BlobAssetRef;",235 " }",236 "}"237 ])238 return "\n".join(code_lines)239 240 def generate_authoring(self, schema: SchemaDefinition) -> str:241 code_lines = []242 using_statements = [243 "System",244 f"{schema.namespace}.Authoring.BlobComponents",245 f"{schema.namespace}.Authoring.Schemas",246 "Unity.Entities",247 "UnityEngine"248 ]249 code_lines.extend([f"using {using};" for using in using_statements])250 code_lines.append("")251 252 namespace = f"{schema.namespace}.Authoring"253 code_lines.append(f"namespace {namespace}")254 code_lines.append("{")255 256 class_name = f"{schema.name}Authoring"257 schema_class_name = f"{schema.name}Schema"258 blob_class_name = f"{schema.name}Blob"259 260 code_lines.extend([261 f" public class {class_name} : MonoBehaviour",262 " {",263 f" public {schema_class_name}[] {schema.name.lower()}Schemas = Array.Empty<{schema_class_name}>();",264 "",265 f" public class {schema.name}Baker : Baker<{class_name}>",266 " {",267 f" public override void Bake({class_name} authoring)",268 " {",269 " var entity = GetEntity(TransformUsageFlags.None);",270 f" AddComponent(entity, new {blob_class_name}",271 " {",272 f" BlobAssetRef = {schema_class_name}.ToAssetRef(authoring.{schema.name.lower()}Schemas)",273 " });",274 " }",275 " }",276 " }",277 "}"278 ])279 return "\n".join(code_lines)280 281 def generate_create_node(self, schema: SchemaDefinition) -> str:282 code_lines = []283 code_lines.extend([f"using {using};" for using in sorted(self.using_statements)])284 code_lines.append("")285 286 namespace = f"{schema.namespace}.Authoring.Schemas"287 code_lines.append(f"namespace {namespace}")288 code_lines.append("{")289 290 class_name = f"{schema.name}SchemaCreateNode"291 schema_class_name = f"{schema.name}Schema"292 293 code_lines.extend([294 " [Serializable]",295 f" internal class {class_name} : SchemaCreateNodeBase<{schema_class_name}>",296 " {",297 f" protected override string DefaultFolder => \"{schema.folder_path}/{schema.name}/{schema_class_name}\";",298 ""299 ])300 301 # Option constants for non-reference, non-id fields302 non_reference_fields = [f for f in schema.fields if not f.is_reference and f.name != "id"]303 for field in non_reference_fields:304 const_name = f"Opt{self._pascal(field.name)}"305 code_lines.append(f" const string {const_name} = \"{self._pascal(field.name)}\";")306 if non_reference_fields:307 code_lines.append("")308 309 if non_reference_fields:310 code_lines.extend([311 " protected override void DefineCustomOptions(INodeOptionDefinition ctx)",312 " {"313 ])314 for field in non_reference_fields:315 const_name = f"Opt{self._pascal(field.name)}"316 default_val = field.default_value or self._get_default_value_for_type(field.field_type)317 code_lines.append(318 f" ctx.AddNodeOption<{self._get_csharp_type(field.field_type)}>({const_name}, \"{self._pascal(field.name)}\", defaultValue: {default_val});")319 code_lines.extend([320 " }",321 ""322 ])323 324 reference_fields = [f for f in schema.fields if f.is_reference]325 if reference_fields:326 code_lines.extend([327 " protected override void DefineCustomPorts(IPortDefinitionContext ctx)",328 " {"329 ])330 for field in reference_fields:331 port_name = self._pascal(field.name)332 code_lines.append(333 f" ctx.AddInputPort<{field.reference_type}Schema>(\"{port_name}\").Build();")334 code_lines.extend([335 " }",336 ""337 ])338 339 code_lines.extend([340 f" protected override void ApplyCustomFields({schema_class_name} a)",341 " {"342 ])343 # Apply options for non-ref fields344 for field in non_reference_fields:345 const_name = f"Opt{self._pascal(field.name)}"346 var_name = field.name347 default_val = field.default_value or self._get_default_value_for_type(field.field_type)348 code_lines.append(f" {self._get_csharp_type(field.field_type)} {var_name} = {default_val};")349 code_lines.append(350 f" var p{self._pascal(var_name)} = GetNodeOptionByName({const_name}); p{self._pascal(var_name)}?.TryGetValue(out {var_name});")351 code_lines.append(f" a.{field.name} = {var_name};")352 code_lines.append("")353 354 # Apply refs355 for field in reference_fields:356 port_name = self._pascal(field.name)357 if field.is_blob_array:358 code_lines.extend([359 f" var {field.name}Port = GetInputPortByName(\"{port_name}\");",360 f" var connected{self._pascal(field.name)} = new System.Collections.Generic.List<IPort>();",361 f" {field.name}Port.GetConnectedPorts(connected{self._pascal(field.name)});",362 f" a.{field.name} = connected{self._pascal(field.name)}",363 f" .Select(MissionGraph.ResolvePortValue<{field.reference_type}Schema>)",364 f" .Where(g => g != null)",365 f" .Distinct()",366 f" .ToArray();",367 ""368 ])369 else:370 code_lines.extend([371 f" var {field.name}Port = GetInputPortByName(\"{port_name}\");",372 f" var {field.name} = MissionGraph.ResolvePortValue<{field.reference_type}Schema>({field.name}Port);",373 f" if ({field.name} != null) a.{field.name}Schema = {field.name};",374 ""375 ])376 377 code_lines.extend([378 " }",379 " }",380 "}"381 ])382 383 return "\n".join(code_lines)384 385 def generate_schema_node(self, schema: SchemaDefinition) -> str:386 schema_class_name = f"{schema.name}Schema"387 class_name = f"{schema_class_name}Node"388 return f"""using System;389 390namespace {schema.namespace}.Authoring.Schemas391{{392 [Serializable]393 internal class {class_name} : SchemaNode<{schema_class_name}> {{}}394}}"""395 396 # ---- helpers ----397 def _pascal(self, s: str) -> str:398 if not s:399 return s400 parts = re.split(r'[_\s]+', s)401 return "".join(p[:1].upper() + p[1:] for p in parts if p)402 403 def _generate_field_declaration(self, field: SchemaField) -> str:404 ftype = field.field_type405 if field.is_blob_array:406 ftype = f"BlobArray<{ftype}>"407 return f"public {ftype} {field.name};"408 409 def _generate_schema_field_declaration(self, field: SchemaField) -> str:410 # Schema side needs authoring-friendly containers411 if field.is_blob_array:412 if field.is_reference and field.reference_type:413 return f"public {field.reference_type}Schema[] {field.name} = Array.Empty<{field.reference_type}Schema>();"414 else:415 return f"public {field.field_type}[] {field.name} = Array.Empty<{field.field_type}>();"416 else:417 # normal value or single id backing field418 return f"public {field.field_type} {field.name};"419 420 def _generate_blob_conversion_methods(self, schema: SchemaDefinition) -> List[str]:421 lines = []422 schema_class_name = f"{schema.name}Schema"423 lines.extend([424 f" public static BlobAssetReference<BlobArray<{schema.name}>> ToAssetRef({schema_class_name}[] schemas)",425 " {",426 " var builder = new BlobBuilder(Allocator.Temp);",427 f" ref var blobArray = ref builder.ConstructRoot<BlobArray<{schema.name}>>();",428 " ToBlobArray(ref builder, ref blobArray, schemas);",429 f" var blobAssetRef = builder.CreateBlobAssetReference<BlobArray<{schema.name}>>(Allocator.Persistent);",430 " builder.Dispose();",431 " return blobAssetRef;",432 " }",433 "",434 f" public static void ToBlobArray(ref BlobBuilder builder, ref BlobArray<{schema.name}> blobArray, {schema_class_name}[] schemas)",435 " {",436 f" var items = builder.Allocate(ref blobArray, schemas.Length);",437 "",438 " for (int i = 0; i < schemas.Length; i++)",439 " {",440 " if (schemas[i] == null) continue;",441 f" items[i] = schemas[i].ToData();",442 ""443 ])444 # Blob array fields445 lines.extend(self._generate_blob_array_assignments(schema))446 lines.extend([447 " }",448 " }"449 ])450 return lines451 452 def _generate_blob_array_assignments(self, schema: SchemaDefinition) -> List[str]:453 lines = []454 for field in schema.fields:455 if not field.is_blob_array:456 continue457 lines.append(f" // {field.name} blob array")458 lines.append(f" if (schemas[i].{field.name} != null && schemas[i].{field.name}.Length > 0)")459 lines.append(" {")460 lines.append(461 f" var arr = builder.Allocate(ref items[i].{field.name}, schemas[i].{field.name}.Length);")462 lines.append(f" for (int j = 0; j < schemas[i].{field.name}.Length; j++)")463 lines.append(" {")464 if field.is_reference:465 lines.append(f" arr[j] = (ushort)schemas[i].{field.name}[j].ID;")466 else:467 lines.append(f" arr[j] = schemas[i].{field.name}[j];")468 lines.append(" }")469 lines.append(" }")470 lines.append(f" else builder.Allocate(ref items[i].{field.name}, 0);")471 lines.append("")472 return lines473 474 def _get_csharp_type(self, field_type: str) -> str:475 type_mapping = {476 "float2": "Unity.Mathematics.float2",477 "float3": "Unity.Mathematics.float3",478 "float4": "Unity.Mathematics.float4",479 "quaternion": "Unity.Mathematics.quaternion",480 "FixedString32Bytes": "Unity.Collections.FixedString32Bytes",481 "FixedString64Bytes": "Unity.Collections.FixedString64Bytes",482 "FixedString128Bytes": "Unity.Collections.FixedString128Bytes"483 }484 return type_mapping.get(field_type, field_type)485 486 def _get_default_value_for_type(self, field_type: str) -> str:487 defaults = {488 "byte": "0", "sbyte": "0", "short": "0", "ushort": "0",489 "int": "0", "uint": "0u", "long": "0L", "ulong": "0UL",490 "float": "0f", "double": "0.0", "bool": "false",491 "char": "'\\0'", "string": "\"\"",492 "float2": "Unity.Mathematics.float2.zero",493 "float3": "Unity.Mathematics.float3.zero",494 "float4": "Unity.Mathematics.float4.zero",495 "quaternion": "Unity.Mathematics.quaternion.identity",496 "FixedString32Bytes": "default",497 "FixedString64Bytes": "default",498 "FixedString128Bytes": "default"499 }500 return defaults.get(field_type, "default")501 502 503# -----------------------------504# Schema manager505# -----------------------------506 507class SchemaManager:508 def __init__(self):509 self.schemas: Dict[str, SchemaDefinition] = {}510 self.code_generator = CSharpCodeGenerator()511 512 def add_schema(self, schema: SchemaDefinition) -> None:513 self.schemas[schema.name] = schema514 515 def get_schema(self, name: str) -> Optional[SchemaDefinition]:516 return self.schemas.get(name)517 518 def list_schema_names(self) -> List[str]:519 return list(self.schemas.keys())520 521 def generate_all_code(self, schema_name: str) -> Dict[str, str]:522 schema = self.get_schema(schema_name)523 if not schema:524 return {}525 526 cg = self.code_generator527 results = {}528 results[f"{schema.name}.cs"] = cg.generate_data_struct(schema)529 if schema.generate_schema_class:530 results[f"{schema.name}Schema.cs"] = cg.generate_schema_class(schema)531 if schema.generate_blob_component:532 results[f"{schema.name}Blob.cs"] = cg.generate_blob_component(schema)533 if schema.generate_authoring:534 results[f"{schema.name}Authoring.cs"] = cg.generate_authoring(schema)535 if schema.generate_create_node:536 results[f"{schema.name}SchemaCreateNode.cs"] = cg.generate_create_node(schema)537 if schema.generate_schema_node:538 results[f"{schema.name}SchemaNode.cs"] = cg.generate_schema_node(schema)539 return results540 541 542schema_manager = SchemaManager()543 544 545# -----------------------------546# Gradio app547# -----------------------------548 549def create_gradio_interface():550 with gr.Blocks(title="C# Schema Code Generator", theme=gr.themes.Soft()) as interface:551 gr.Markdown("# C# Unity ECS Schema Code Generator")552 gr.Markdown(553 "Generate Unity ECS-friendly C# code with live preview. Edit fields directly in the table, import from C# struct, and dump as code.")554 555 # State556 current_schema_state = gr.State(value=None)557 558 # Helpers559 def _pascal(s: str) -> str:560 if not s:561 return s562 parts = re.split(r"[_\s]+", s)563 return "".join(p[:1].upper() + p[1:] for p in parts if p)564 565 def _guess_reference(field_name: str) -> Optional[str]:566 # id itself is not a reference567 if not field_name:568 return None569 if field_name.lower() == "id":570 return None571 m = re.match(r"^(.*?)(?:Id|ID|id)$", field_name)572 if not m:573 return None574 base = m.group(1)575 if not base:576 return None577 return _pascal(base)578 579 def normalize_table(table_value: Any) -> List[List[Any]]:580 try:581 if hasattr(table_value, "to_dict"):582 records = table_value.to_dict(orient="records")583 rows = [[r.get("Name"), r.get("Type"), bool(r.get("Blob Array") or False),584 bool(r.get("Reference") or False), r.get("Ref Type") or "", r.get("Default") or ""]585 for r in records]586 else:587 rows = table_value or []588 except Exception:589 rows = table_value or []590 # Ensure 6 columns and clean rows591 cleaned = []592 for r in rows:593 if not r:594 continue595 r = list(r) + [""] * (6 - len(r))596 name = (r[0] or "").strip()597 if not name:598 continue599 ftype = (r[1] or "").strip() or "ushort"600 is_blob = bool(r[2])601 is_ref = bool(r[3])602 ref_type = (r[4] or "").strip()603 default_val = (r[5] or "").strip()604 cleaned.append([name, ftype, is_blob, is_ref, ref_type, default_val])605 return cleaned606 607 def table_to_fields(rows: List[List[Any]], auto_infer: bool) -> List[SchemaField]:608 fields: List[SchemaField] = []609 seen = set()610 for r in rows:611 if len(r) < 2:612 continue613 name = str(r[0]).strip()614 if not re.match(r'^[a-zA-Z_][a-zA-Z0-9_]*$', name):615 gr.Warning(f"Skipping invalid field name: {name}")616 continue617 if name in seen:618 gr.Warning(f"Duplicate field '{name}' ignored (keep first).")619 continue620 seen.add(name)621 ftype = str(r[1]).strip() or "ushort"622 is_blob = bool(r[2])623 is_ref = bool(r[3])624 ref_type = (str(r[4]).strip() if r[4] else None)625 default_val = (str(r[5]).strip() if r[5] else None)626 627 # Auto-infer references for *Id fields628 if auto_infer and name.lower() != "id":629 guessed = _guess_reference(name)630 if guessed:631 is_ref = True if not is_ref else is_ref632 if not ref_type:633 ref_type = guessed634 635 if is_ref and (not ref_type):636 gr.Warning(f"Field '{name}' marked as reference but has no reference type.")637 638 fields.append(SchemaField(639 name=name,640 field_type=ftype,641 is_blob_array=is_blob,642 is_reference=is_ref,643 reference_type=ref_type,644 default_value=default_val645 ))646 return fields647 648 def fields_to_table(fields: List[SchemaField]) -> List[List[Any]]:649 out = []650 for f in fields:651 out.append([652 f.name,653 f.field_type,654 bool(f.is_blob_array),655 bool(f.is_reference),656 f.reference_type or "",657 f.default_value or ""658 ])659 return out660 661 def ensure_id_field_consistency(schema: SchemaDefinition):662 has_id_field = any(f.name == "id" for f in schema.fields)663 if schema.implements_has_id and not has_id_field:664 schema.fields.insert(0, SchemaField(name="id", field_type="ushort"))665 if not schema.implements_has_id and has_id_field:666 schema.fields = [f for f in schema.fields if f.name != "id"]667 668 def build_temp_schema(669 name: str, ns: str, folder: str, menu: str,670 gen_schema: bool, gen_blob: bool, gen_auth: bool, gen_create: bool, gen_node: bool,671 has_id: bool, equatable: bool, auto_infer: bool, table_value: Any672 ) -> Optional[SchemaDefinition]:673 name = (name or "").strip()674 if not name or not re.match(r'^[A-Z][a-zA-Z0-9]*$', name):675 return None676 schema = SchemaDefinition(677 name=name,678 namespace=(ns or "DefaultNamespace").strip(),679 folder_path=(folder or "Assets/Settings").strip(),680 menu_category=(menu or "Hold").strip(),681 generate_schema_class=bool(gen_schema),682 generate_blob_component=bool(gen_blob),683 generate_authoring=bool(gen_auth),684 generate_create_node=bool(gen_create),685 generate_schema_node=bool(gen_node),686 implements_has_id=bool(has_id),687 implements_equatable=bool(equatable),688 fields=table_to_fields(normalize_table(table_value), auto_infer)689 )690 ensure_id_field_consistency(schema)691 return schema692 693 def generate_code_preview(schema: Optional[SchemaDefinition]) -> List[str]:694 if not schema:695 return [""] * 6696 cg = schema_manager.code_generator697 files = {698 "data": cg.generate_data_struct(schema),699 "schema": cg.generate_schema_class(schema) if schema.generate_schema_class else "",700 "blob": cg.generate_blob_component(schema) if schema.generate_blob_component else "",701 "authoring": cg.generate_authoring(schema) if schema.generate_authoring else "",702 "create": cg.generate_create_node(schema) if schema.generate_create_node else "",703 "node": cg.generate_schema_node(schema) if schema.generate_schema_node else "",704 }705 return [files["data"], files["schema"], files["blob"], files["authoring"], files["create"], files["node"]]706 707 # -----------------------------708 # Left column (config)709 # -----------------------------710 with gr.Row():711 with gr.Column(scale=1):712 gr.Markdown("### Current Fields (Editable)")713 fields_display = gr.DataFrame(714 headers=["Name", "Type", "Blob Array", "Reference", "Ref Type", "Default"],715 datatype=["str", "str", "bool", "bool", "str", "str"],716 interactive=True,717 label="Current Fields"718 )719 720 with gr.Row():721 create_schema_btn = gr.Button("Create/Update Schema", variant="primary")722 update_from_table_btn = gr.Button("Apply Table Changes", variant="secondary")723 724 with gr.Row():725 save_config_btn = gr.Button("Save Configuration", variant="secondary")726 load_config_btn = gr.Button("Load Configuration", variant="secondary")727 728 # Config file input729 config_file = gr.File(label="Select Configuration (.json)", file_types=[".json"], visible=False)730 731 gr.Markdown("## Schema Configuration")732 with gr.Group():733 gr.Markdown("### Basic Information")734 schema_name = gr.Textbox(label="Schema Name", placeholder="e.g., Mission, Location, Name",735 value="Mission")736 namespace = gr.Textbox(label="Namespace", placeholder="e.g., Missions.Missions",737 value="Missions.Missions")738 folder_path = gr.Textbox(label="Folder Path", placeholder="Assets/Settings",739 value="Assets/Settings")740 menu_category = gr.Textbox(label="Menu Category", placeholder="Hold", value="Hold")741 742 with gr.Group():743 gr.Markdown("### Generation Options")744 with gr.Row():745 gen_schema_class = gr.Checkbox(label="Schema Class", value=True)746 gen_blob_component = gr.Checkbox(label="Blob Component", value=True)747 with gr.Row():748 gen_authoring = gr.Checkbox(label="Authoring", value=True)749 gen_create_node = gr.Checkbox(label="Create Node", value=True)750 with gr.Row():751 gen_schema_node = gr.Checkbox(label="Schema Node", value=True)752 implements_has_id = gr.Checkbox(label="IHasID", value=True)753 implements_equatable = gr.Checkbox(label="IEquatable", value=True)754 auto_infer_id_refs_ui = gr.Checkbox(label="Auto-detect '*Id' fields as references", value=True)755 756 with gr.Group():757 gr.Markdown("### Add Field")758 field_name = gr.Textbox(label="Field Name", placeholder="e.g., stationId, nameId")759 field_type = gr.Dropdown(760 label="Field Type",761 choices=[ft.value for ft in FieldType],762 value=FieldType.USHORT.value763 )764 with gr.Row():765 is_blob_array = gr.Checkbox(label="Blob Array", value=False)766 is_reference = gr.Checkbox(label="Schema Reference", value=False)767 reference_type = gr.Textbox(768 label="Reference Type",769 placeholder="e.g., Name, Location (for NameSchema, LocationSchema)",770 visible=False,771 lines=2772 )773 default_value = gr.Textbox(label="Default Value (optional)", placeholder='0, false, ""')774 with gr.Row():775 add_field_btn = gr.Button("Add Field", variant="primary")776 clear_fields_btn = gr.Button("Clear All Fields", variant="secondary")777 778 gr.Markdown("### Export")779 with gr.Row():780 export_all_btn = gr.Button("Export All Files", variant="primary")781 export_zip_btn = gr.Button("Download as ZIP", variant="secondary")782 export_files = gr.Files(label="Exported Files", file_count="multiple")783 zip_file_output = gr.File(label="Exported ZIP")784 785 # -----------------------------786 # Right column (code)787 # -----------------------------788 with gr.Column(scale=1):789 # --- Code Dump / Import-Export UI ---790 gr.Markdown("Paste a C# data struct to populate the UI/table, or dump the current schema as code.")791 csharp_dump_in = gr.Code(label="Paste C# Data Struct", language="c", interactive=True, lines=18)792 infer_id_refs = gr.Checkbox(label="Infer '*Id' fields as references", value=True)793 import_from_csharp_btn = gr.Button("Import From C#", variant="primary")794 with gr.Accordion("Code Dump / Import-Export", open=False):795 with gr.Row():796 dump_csharp_btn = gr.Button("Dump Current as C# Struct", variant="secondary")797 dump_json_btn = gr.Button("Dump Current as JSON", variant="secondary")798 dump_output = gr.Code(label="Dump Output", language="c", interactive=False, lines=18)799 800 gr.Markdown("## Generated Code")801 with gr.Tabs():802 with gr.TabItem("Data Struct"):803 data_struct_code = gr.Code(language="c", label="Data Struct Code", interactive=False)804 with gr.TabItem("Schema Class"):805 schema_class_code = gr.Code(language="c", label="Schema Class Code", interactive=False)806 with gr.TabItem("Blob Component"):807 blob_component_code = gr.Code(language="c", label="Blob Component Code", interactive=False)808 with gr.TabItem("Authoring"):809 authoring_code = gr.Code(language="c", label="Authoring Code", interactive=False)810 with gr.TabItem("Create Node"):811 create_node_code = gr.Code(language="c", label="Create Node Code", interactive=False)812 with gr.TabItem("Schema Node"):813 schema_node_code = gr.Code(language="c", label="Schema Node Code", interactive=False)814 815 # -----------------------------816 # Handlers817 # -----------------------------818 819 def on_is_reference_change(is_ref: bool):820 return gr.update(visible=is_ref)821 822 is_reference.change(823 fn=on_is_reference_change,824 inputs=[is_reference],825 outputs=[reference_type]826 )827 828 def apply_ui_and_preview(name, ns, folder, menu,829 gen_schema, gen_blob, gen_auth, gen_create, gen_node,830 has_id, equatable, auto_infer, table_value, current_schema):831 schema = build_temp_schema(name, ns, folder, menu,832 gen_schema, gen_blob, gen_auth, gen_create, gen_node,833 has_id, equatable, auto_infer, table_value)834 code = generate_code_preview(schema)835 if schema:836 return schema, code[0], code[1], code[2], code[3], code[4], code[5], fields_to_table(schema.fields)837 else:838 return current_schema, *([""] * 6), normalize_table(table_value)839 840 # Hook live updates to all meta controls, auto-infer toggle, and table841 live_inputs = [842 schema_name, namespace, folder_path, menu_category,843 gen_schema_class, gen_blob_component, gen_authoring, gen_create_node, gen_schema_node,844 implements_has_id, implements_equatable, auto_infer_id_refs_ui, fields_display, current_schema_state845 ]846 live_outputs = [847 current_schema_state,848 data_struct_code, schema_class_code, blob_component_code, authoring_code, create_node_code,849 schema_node_code,850 fields_display851 ]852 853 for c in [schema_name, namespace, folder_path, menu_category,854 gen_schema_class, gen_blob_component, gen_authoring, gen_create_node, gen_schema_node,855 implements_has_id, implements_equatable, auto_infer_id_refs_ui]:856 c.change(fn=apply_ui_and_preview, inputs=live_inputs, outputs=live_outputs)857 858 fields_display.change(fn=apply_ui_and_preview, inputs=live_inputs, outputs=live_outputs)859 860 def add_field(861 name, ftype, is_blob, is_ref, ref_type, default_val,862 s_name, ns, folder, menu, gen_schema, gen_blob, gen_auth, gen_create, gen_node, has_id, equatable,863 auto_infer,864 table_value, current_schema865 ):866 if not name or not name.strip():867 gr.Warning("Field name cannot be empty.")868 return apply_ui_and_preview(s_name, ns, folder, menu, gen_schema, gen_blob, gen_auth, gen_create,869 gen_node,870 has_id, equatable, auto_infer, table_value, current_schema)871 if not re.match(r'^[a-zA-Z_][a-zA-Z0-9_]*$', name.strip()):872 gr.Warning("Invalid field name. Use only letters, numbers, and underscores.")873 return apply_ui_and_preview(s_name, ns, folder, menu, gen_schema, gen_blob, gen_auth, gen_create,874 gen_node,875 has_id, equatable, auto_infer, table_value, current_schema)876 877 rows = normalize_table(table_value)878 879 # Auto-infer references for *Id fields when adding880 if auto_infer and name.strip().lower() != "id":881 guessed = _guess_reference(name.strip())882 if guessed and not is_ref:883 is_ref = True884 if not (ref_type and ref_type.strip()):885 ref_type = guessed886 887 if any((r[0] or "").strip() == name.strip() for r in rows):888 gr.Warning(f"Field '{name.strip()}' already exists.")889 else:890 rows.append([name.strip(), ftype, bool(is_blob), bool(is_ref), (ref_type or "").strip(),891 (default_val or "").strip()])892 893 return apply_ui_and_preview(s_name, ns, folder, menu, gen_schema, gen_blob, gen_auth, gen_create, gen_node,894 has_id, equatable, auto_infer, rows, current_schema)895 896 add_field_btn.click(897 fn=add_field,898 inputs=[899 field_name, field_type, is_blob_array, is_reference, reference_type, default_value,900 schema_name, namespace, folder_path, menu_category,901 gen_schema_class, gen_blob_component, gen_authoring, gen_create_node, gen_schema_node,902 implements_has_id, implements_equatable, auto_infer_id_refs_ui,903 fields_display, current_schema_state904 ],905 outputs=live_outputs906 )907 908 def clear_all_fields(909 s_name, ns, folder, menu, gen_schema, gen_blob, gen_auth, gen_create, gen_node, has_id, equatable,910 auto_infer,911 table_value, current_schema912 ):913 rows = []914 return apply_ui_and_preview(s_name, ns, folder, menu, gen_schema, gen_blob, gen_auth, gen_create, gen_node,915 has_id, equatable, auto_infer, rows, current_schema)916 917 clear_fields_btn.click(918 fn=clear_all_fields,919 inputs=[schema_name, namespace, folder_path, menu_category,920 gen_schema_class, gen_blob_component, gen_authoring, gen_create_node, gen_schema_node,921 implements_has_id, implements_equatable, auto_infer_id_refs_ui,922 fields_display, current_schema_state],923 outputs=live_outputs924 )925 926 update_from_table_btn.click(927 fn=apply_ui_and_preview,928 inputs=live_inputs,929 outputs=live_outputs930 )931 932 def create_or_update_schema(933 s_name, ns, folder, menu, gen_schema, gen_blob, gen_auth, gen_create, gen_node, has_id, equatable,934 auto_infer,935 table_value, current_schema936 ):937 if not s_name or not re.match(r'^[A-Z][a-zA-Z0-9]*$', s_name.strip()):938 gr.Warning("Schema name must start with uppercase letter and contain only letters and numbers.")939 return apply_ui_and_preview(s_name, ns, folder, menu, gen_schema, gen_blob, gen_auth, gen_create,940 gen_node,941 has_id, equatable, auto_infer, table_value, current_schema)942 943 schema = build_temp_schema(s_name, ns, folder, menu,944 gen_schema, gen_blob, gen_auth, gen_create, gen_node,945 has_id, equatable, auto_infer, table_value)946 if not schema:947 gr.Warning("Please fix schema errors before creating.")948 return apply_ui_and_preview(s_name, ns, folder, menu, gen_schema, gen_blob, gen_auth, gen_create,949 gen_node,950 has_id, equatable, auto_infer, table_value, current_schema)951 952 schema_manager.add_schema(schema)953 code = schema_manager.generate_all_code(schema.name)954 return (955 schema,956 code.get(f"{schema.name}.cs", ""),957 code.get(f"{schema.name}Schema.cs", ""),958 code.get(f"{schema.name}Blob.cs", ""),959 code.get(f"{schema.name}Authoring.cs", ""),960 code.get(f"{schema.name}SchemaCreateNode.cs", ""),961 code.get(f"{schema.name}SchemaNode.cs", ""),962 fields_to_table(schema.fields)963 )964 965 create_schema_btn.click(966 fn=create_or_update_schema,967 inputs=[schema_name, namespace, folder_path, menu_category,968 gen_schema_class, gen_blob_component, gen_authoring, gen_create_node, gen_schema_node,969 implements_has_id, implements_equatable, auto_infer_id_refs_ui, fields_display,970 current_schema_state],971 outputs=live_outputs972 )973 974 def save_configuration(current_schema: Optional[SchemaDefinition]):975 if not current_schema:976 gr.Warning("No schema to save.")977 return None978 config_data = {979 "name": current_schema.name,980 "namespace": current_schema.namespace,981 "folder_path": current_schema.folder_path,982 "menu_category": current_schema.menu_category,983 "generate_schema_class": current_schema.generate_schema_class,984 "generate_blob_component": current_schema.generate_blob_component,985 "generate_authoring": current_schema.generate_authoring,986 "generate_create_node": current_schema.generate_create_node,987 "generate_schema_node": current_schema.generate_schema_node,988 "implements_has_id": current_schema.implements_has_id,989 "implements_equatable": current_schema.implements_equatable,990 "fields": [991 {992 "name": f.name,993 "field_type": f.field_type,994 "is_blob_array": f.is_blob_array,995 "is_reference": f.is_reference,996 "reference_type": f.reference_type,997 "default_value": f.default_value,998 "description": f.description999 } for f in current_schema.fields1000 ]1001 }1002 tf = tempfile.NamedTemporaryFile(mode="w", suffix=".json", delete=False)1003 json.dump(config_data, tf, indent=2)1004 tf.close()1005 return tf.name1006 1007 save_config_btn.click(1008 fn=save_configuration,1009 inputs=[current_schema_state],1010 outputs=[config_file]1011 )1012 1013 def show_file_picker():1014 return gr.update(visible=True)1015 1016 load_config_btn.click(1017 fn=show_file_picker,1018 outputs=[config_file]1019 )1020 1021 def load_configuration(file_obj):1022 if not file_obj:1023 gr.Warning("No file selected.")1024 return (None, "", "", "", "", True, True, True, True, True, True, True, [], "", "", "", "", "", "")1025 1026 try:1027 path = file_obj.name if hasattr(file_obj, "name") else file_obj1028 with open(path, "r") as f:1029 data = json.load(f)1030 fields = []1031 for fld in data.get("fields", []):1032 fields.append(SchemaField(1033 name=fld["name"],1034 field_type=fld["field_type"],1035 is_blob_array=fld.get("is_blob_array", False),1036 is_reference=fld.get("is_reference", False),1037 reference_type=fld.get("reference_type"),1038 default_value=fld.get("default_value"),1039 description=fld.get("description")1040 ))1041 schema = SchemaDefinition(1042 name=data["name"],1043 namespace=data.get("namespace", "DefaultNamespace"),1044 folder_path=data.get("folder_path", "Assets/Settings"),1045 menu_category=data.get("menu_category", "Hold"),1046 generate_schema_class=data.get("generate_schema_class", True),1047 generate_blob_component=data.get("generate_blob_component", True),1048 generate_authoring=data.get("generate_authoring", True),1049 generate_create_node=data.get("generate_create_node", True),1050 generate_schema_node=data.get("generate_schema_node", True),1051 implements_has_id=data.get("implements_has_id", True),1052 implements_equatable=data.get("implements_equatable", True),1053 fields=fields1054 )1055 cg = schema_manager.code_generator1056 results = {1057 "data": cg.generate_data_struct(schema),1058 "schema": cg.generate_schema_class(schema) if schema.generate_schema_class else "",1059 "blob": cg.generate_blob_component(schema) if schema.generate_blob_component else "",1060 "authoring": cg.generate_authoring(schema) if schema.generate_authoring else "",1061 "create": cg.generate_create_node(schema) if schema.generate_create_node else "",1062 "node": cg.generate_schema_node(schema) if schema.generate_schema_node else "",1063 }1064 return (1065 schema,1066 schema.name, schema.namespace, schema.folder_path, schema.menu_category,1067 schema.generate_schema_class, schema.generate_blob_component, schema.generate_authoring,1068 schema.generate_create_node, schema.generate_schema_node,1069 schema.implements_has_id, schema.implements_equatable,1070 fields_to_table(schema.fields),1071 results["data"], results["schema"], results["blob"], results["authoring"], results["create"],1072 results["node"]1073 )1074 except Exception as e:1075 gr.Warning(f"Error loading configuration: {e}")1076 return (None, "", "", "", "", True, True, True, True, True, True, True, [], "", "", "", "", "", "")1077 1078 config_file.change(1079 fn=load_configuration,1080 inputs=[config_file],1081 outputs=[1082 current_schema_state,1083 schema_name, namespace, folder_path, menu_category,1084 gen_schema_class, gen_blob_component, gen_authoring, gen_create_node, gen_schema_node,1085 implements_has_id, implements_equatable,1086 fields_display,1087 data_struct_code, schema_class_code, blob_component_code, authoring_code, create_node_code,1088 schema_node_code1089 ]1090 )1091 1092 def export_all_files(current_schema: Optional[SchemaDefinition]):1093 if not current_schema:1094 gr.Warning("No schema to export.")1095 return []1096 generated = schema_manager.generate_all_code(current_schema.name) or {}1097 paths = []1098 for filename, code in generated.items():1099 tf = tempfile.NamedTemporaryFile(mode="w", suffix=".cs", delete=False)1100 tf.write(code)1101 tf.close()1102 target = os.path.join(os.path.dirname(tf.name), filename)1103 try:1104 os.replace(tf.name, target)1105 except Exception:1106 target = tf.name1107 paths.append(target)1108 return paths1109 1110 export_all_btn.click(1111 fn=export_all_files,1112 inputs=[current_schema_state],1113 outputs=[export_files]1114 )1115 1116 def export_zip(current_schema: Optional[SchemaDefinition]):1117 if not current_schema:1118 gr.Warning("No schema to export.")1119 return None1120 generated = schema_manager.generate_all_code(current_schema.name) or {}1121 if not generated:1122 gr.Warning("Nothing to export.")1123 return None1124 tf_zip = tempfile.NamedTemporaryFile(suffix=".zip", delete=False)1125 tf_zip.close()1126 with zipfile.ZipFile(tf_zip.name, "w", zipfile.ZIP_DEFLATED) as zipf:1127 for filename, code in generated.items():1128 zipf.writestr(filename, code)1129 return tf_zip.name1130 1131 export_zip_btn.click(1132 fn=export_zip,1133 inputs=[current_schema_state],1134 outputs=[zip_file_output]1135 )1136 1137 # ---------- C# import helpers ----------1138 def _strip_comments(code: str) -> str:1139 code = re.sub(r"//.*", "", code)1140 code = re.sub(r"/\*.*?\*/", "", code, flags=re.S)1141 return code1142 1143 def _extract_block(text: str, start_pos: int) -> str:1144 i = text.find("{", start_pos)1145 if i == -1:1146 return ""1147 depth, j = 1, i + 11148 while j < len(text):1149 c = text[j]1150 if c == "{":1151 depth += 11152 elif c == "}":1153 depth -= 11154 if depth == 0:1155 return text[i + 1: j]1156 j += 11157 return ""1158 1159 def _shorten_type(t: str) -> str:1160 t = t.strip()1161 t = t.replace("Unity.Mathematics.", "")1162 t = t.replace("Unity.Collections.", "")1163 t = t.replace("System.", "")1164 aliases = {1165 "UInt16": "ushort", "Int16": "short", "UInt32": "uint", "Int32": "int",1166 "UInt64": "ulong", "Int64": "long", "Boolean": "bool", "String": "string",1167 }1168 return aliases.get(t, t)1169 1170 def _canonicalize_type(t: str) -> Tuple[str, bool]:1171 t = t.strip()1172 m = re.match(r"BlobArray\s*<\s*([^>]+)\s*>", t)1173 if m:1174 inner = _shorten_type(m.group(1).strip())1175 return inner, True1176 if t.endswith("[]"):1177 inner = _shorten_type(t[:-2].strip())1178 return inner, True1179 t2 = _shorten_type(t)1180 return t2, False1181 1182 def parse_csharp_to_schema(code_text: str, infer_refs: bool, folder: str, menu: str) -> Optional[1183 SchemaDefinition]:1184 if not code_text or not code_text.strip():1185 return None1186 code = _strip_comments(code_text)1187 1188 ns_match = re.search(r"\bnamespace\s+([A-Za-z0-9_.]+)", code)1189 ns_full = ns_match.group(1) if ns_match else "DefaultNamespace"1190 base_ns = re.sub(r"\.Authoring\.Data$", "", ns_full)1191 1192 st_match = re.search(r"\bpublic\s+struct\s+([A-Za-z_][A-Za-z0-9_]*)\s*(?::\s*([^{]+))?\s*\{", code)1193 if not st_match:1194 return None1195 struct_name = st_match.group(1)1196 interfaces_raw = st_match.group(2) or ""1197 interfaces = [i.strip() for i in interfaces_raw.split(",")] if interfaces_raw else []1198 implements_has_id = any(i.startswith("IHasID") for i in interfaces)1199 implements_equatable = any(i.startswith("IEquatable") for i in interfaces)1200 