UNPKG

@colyseus/schema

Version:

Binary state serializer with delta encoding for games

1 lines 158 kB
{"version":3,"file":"cli.cjs","sources":["../../src/codegen/argv.ts","../../src/codegen/types.ts","../../src/codegen/parser.ts","../../src/codegen/languages/csharp.ts","../../src/codegen/languages/cpp.ts","../../src/codegen/languages/haxe.ts","../../src/codegen/languages/ts.ts","../../src/codegen/languages/js.ts","../../src/codegen/languages/java.ts","../../src/codegen/languages/lua.ts","../../src/codegen/languages/c.ts","../../src/codegen/languages/gdscript.ts","../../src/codegen/api.ts","../../src/codegen/cli.ts"],"sourcesContent":["/**\n * @author Ethan Davis\n * https://github.com/ethanent/gar\n */\nexport default (sargs: string[]): any => {\n\tlet props: any = {}\n\tlet lones: any = []\n\n\tconst convertIfApplicable = (value: any) => (isNaN(value) ? (value.toString().toLowerCase() === 'true' ? true : (value.toString().toLowerCase() === 'false' ? false : value)) : Number(value))\n\tconst removeStartHyphens = (value: string) => value.replace(/^\\-+/g, '')\n\n\tfor (let i = 0; i < sargs.length; i++) {\n\t\tconst equalsIndex = sargs[i].indexOf('=')\n\t\tconst isNextRefProp = sargs[i].charAt(0) === '-' && sargs.length - 1 >= i + 1 && sargs[i + 1].indexOf('=') === -1 && sargs[i + 1].charAt(0) !== '-'\n\t\tconst argName = equalsIndex === -1 ? removeStartHyphens(sargs[i]) : removeStartHyphens(sargs[i].slice(0, equalsIndex))\n\n\t\tif (equalsIndex !== -1) {\n\t\t\tprops[argName] = convertIfApplicable(sargs[i].slice(equalsIndex + 1))\n\t\t}\n\t\telse if (isNextRefProp) {\n\t\t\tprops[argName] = convertIfApplicable(sargs[i + 1])\n\t\t\ti++\n\t\t} else if (sargs[i].charAt(0) === '-') {\n\t\t\tif (sargs[i].charAt(1) === '-') {\n\t\t\t\tprops[argName] = true\n\t\t\t}\n\t\t\telse {\n\t\t\t\tfor (let b = 0; b < argName.length; b++) {\n\t\t\t\t\tprops[argName.charAt(b)] = true\n\t\t\t\t}\n\t\t\t}\n\t\t} else {\n\t\t\tlones.push(convertIfApplicable(argName))\n\t\t}\n\t}\n\n\treturn Object.assign(props, {\n\t\t'_': lones\n\t})\n}\n","import * as fs from \"fs\";\nimport * as path from \"path\";\n\nif (typeof(__dirname) === \"undefined\") {\n global.__dirname = path.dirname(new URL(import.meta.url).pathname);\n}\n\nconst VERSION = JSON.parse(fs.readFileSync(__dirname + \"/../../package.json\").toString()).version;\nconst COMMENT_HEADER = `\nTHIS FILE HAS BEEN GENERATED AUTOMATICALLY\nDO NOT CHANGE IT MANUALLY UNLESS YOU KNOW WHAT YOU'RE DOING\n\nGENERATED USING @colyseus/schema ${VERSION}\n`;\n\nexport function getCommentHeader(singleLineComment: string = \"//\") {\n return `${COMMENT_HEADER.split(\"\\n\").map(line => `${singleLineComment} ${line}`).join(\"\\n\")}`;\n}\n\nexport class Context {\n classes: Class[] = [];\n interfaces: Interface[] = [];\n enums: Enum[] = [];\n\n getStructures() {\n return {\n classes: this.classes.filter(klass => {\n if (this.isSchemaClass(klass)) {\n return true;\n\n } else {\n let parentClass = klass;\n while (parentClass = this.getParentClass(parentClass)) {\n if (this.isSchemaClass(parentClass)) {\n return true;\n }\n }\n }\n return false;\n }),\n interfaces: this.interfaces,\n enums: this.enums,\n };\n }\n\n addStructure(structure: IStructure) {\n if (structure.context === this) { return; } // skip if already added.\n structure.context = this;\n\n if (structure instanceof Class) {\n this.classes.push(structure);\n } else if (structure instanceof Interface) {\n this.interfaces.push(structure);\n } else if (structure instanceof Enum) {\n this.enums.push(structure);\n }\n }\n\n private getParentClass(klass: Class) {\n return this.classes.find(c => c.name === klass.extends);\n }\n\n private isSchemaClass(klass: Class) {\n let isSchema: boolean = false;\n\n let currentClass = klass;\n while (!isSchema && currentClass) {\n //\n // TODO: ideally we should check for actual @colyseus/schema module\n // reference rather than arbitrary strings.\n //\n isSchema = (\n currentClass.extends === \"Schema\" ||\n currentClass.extends === \"schema.Schema\" ||\n currentClass.extends === \"Schema.Schema\"\n );\n\n //\n // When extending from `schema.Schema`, it is required to\n // normalize as \"Schema\" for code generation.\n //\n if (currentClass === klass && isSchema) {\n klass.extends = \"Schema\";\n }\n\n currentClass = this.getParentClass(currentClass);\n }\n\n return isSchema;\n }\n}\n\nexport interface IStructure {\n context: Context;\n name: string;\n properties: Property[];\n addProperty(property: Property): void;\n}\n\nexport class Interface implements IStructure {\n context: Context;\n name: string;\n properties: Property[] = [];\n\n addProperty(property: Property): void {\n if (property.type.indexOf(\"[]\") >= 0) {\n // is array!\n property.childType = property.type.match(/([^\\[]+)/i)[1];\n property.type = \"array\";\n this.properties.push(property);\n\n } else {\n this.properties.push(property);\n }\n }\n}\n\nexport class Class implements IStructure {\n context: Context;\n name: string;\n properties: Property[] = [];\n extends: string;\n\n addProperty(property: Property) {\n property.index = this.properties.length;\n this.properties.push(property);\n }\n\n postProcessing() {\n /**\n * Ensure the proprierties `index` are correct using inheritance\n */\n let parentKlass: Class = this;\n\n while (\n parentKlass &&\n (parentKlass = this.context.classes.find(k => k.name === parentKlass.extends))\n ) {\n this.properties.forEach(prop => {\n prop.index += parentKlass.properties.length;\n });\n }\n }\n}\n\nexport class Enum implements IStructure {\n context: Context;\n name: string;\n properties: Property[] = [];\n\n addProperty(property: Property) {\n this.properties.push(property);\n }\n}\n\nexport class Property {\n index: number;\n name: string;\n type: string;\n childType: string;\n deprecated?: boolean;\n}\n\nexport interface File {\n name: string\n content: string;\n}\n\n/**\n * Structured file representation for code generation.\n * Separates imports, local references, and body content to enable\n * clean bundling without string parsing.\n */\nexport interface GeneratedFile {\n name: string;\n /** External imports (e.g., \"@colyseus/schema\", \"Colyseus.Schema\") */\n imports: string[];\n /** References to other generated classes (used for imports in non-bundle mode) */\n localRefs: string[];\n /** The class/interface/enum definition body (without imports or namespace wrapper) */\n body: string;\n}\n\nexport function getInheritanceTree(klass: Class, allClasses: Class[], includeSelf: boolean = true) {\n let currentClass = klass;\n let inheritanceTree: Class[] = [];\n\n if (includeSelf) {\n inheritanceTree.push(currentClass);\n }\n\n while (currentClass.extends !== \"Schema\") {\n currentClass = allClasses.find(klass => klass.name == currentClass.extends);\n inheritanceTree.push(currentClass);\n }\n\n return inheritanceTree;\n}\n","import * as ts from \"typescript\";\nimport * as path from \"path\";\nimport { readFileSync } from \"fs\";\nimport { IStructure, Class, Interface, Property, Context, Enum } from \"./types.js\";\n\nlet currentStructure: IStructure;\nlet currentProperty: Property;\n\nlet globalContext: Context;\n\nfunction defineProperty(property: Property, initializer: any) {\n if (ts.isIdentifier(initializer)) {\n property.type = \"ref\";\n property.childType = initializer.text;\n\n } else if (initializer.kind == ts.SyntaxKind.ObjectLiteralExpression) {\n property.type = initializer.properties[0].name.text;\n property.childType = initializer.properties[0].initializer.text;\n\n } else if (initializer.kind == ts.SyntaxKind.ArrayLiteralExpression) {\n property.type = \"array\";\n property.childType = initializer.elements[0].text;\n\n } else {\n property.type = initializer.text;\n }\n}\n\nfunction inspectNode(node: ts.Node, context: Context, decoratorName: string) {\n switch (node.kind) {\n case ts.SyntaxKind.ImportClause:\n const specifier = (node.parent as any).moduleSpecifier;\n if (specifier && (specifier.text as string).startsWith('.')) {\n const currentDir = path.dirname(node.getSourceFile().fileName);\n const pathToImport = path.resolve(currentDir, specifier.text);\n parseFiles([pathToImport], decoratorName, globalContext);\n }\n break;\n\n case ts.SyntaxKind.ClassDeclaration:\n currentStructure = new Class();\n\n const heritageClauses = (node as ts.ClassLikeDeclarationBase).heritageClauses;\n if (heritageClauses && heritageClauses.length > 0) {\n (currentStructure as Class).extends = heritageClauses[0].types[0].expression.getText();\n }\n\n context.addStructure(currentStructure);\n break;\n\n case ts.SyntaxKind.InterfaceDeclaration:\n //\n // Only generate Interfaces if it has \"Message\" on its name.\n // Example: MyMessage\n //\n const interfaceName = (node as ts.TypeParameterDeclaration).name.escapedText.toString();\n if (interfaceName.indexOf(\"Message\") !== -1) {\n currentStructure = new Interface();\n currentStructure.name = interfaceName;\n\n context.addStructure(currentStructure);\n }\n break;\n\n case ts.SyntaxKind.EnumDeclaration:\n const enumName = (\n node as ts.EnumDeclaration\n ).name.escapedText.toString();\n currentStructure = new Enum();\n currentStructure.name = enumName;\n context.addStructure(currentStructure);\n break;\n\n case ts.SyntaxKind.ExtendsKeyword:\n // console.log(node.getText());\n break;\n\n case ts.SyntaxKind.PropertySignature:\n if (currentStructure instanceof Interface) {\n const parent = node.parent;\n\n // Only process direct children of InterfaceDeclaration, skip TypeLiterals\n if (!ts.isInterfaceDeclaration(parent)) {\n break;\n }\n\n // Skip if property if for a another interface than the one we're interested in.\n if (currentStructure.name !== parent.name.escapedText.toString()) {\n break;\n }\n\n // define a property of an interface\n const property = new Property();\n property.name = (node as any).name.escapedText.toString();\n property.type = (node as any).type.getText();\n currentStructure.addProperty(property);\n }\n break;\n\n case ts.SyntaxKind.Identifier:\n if (\n node.getText() === \"deprecated\" &&\n node.parent.kind !== ts.SyntaxKind.ImportSpecifier\n ) {\n currentProperty = new Property();\n currentProperty.deprecated = true;\n break;\n }\n\n if (node.getText() === decoratorName) {\n const prop: any = node.parent?.parent?.parent;\n const propDecorator = getDecorators(prop);\n const hasExpression = prop?.expression?.arguments;\n const hasDecorator = (propDecorator?.length > 0);\n\n /**\n * neither a `@type()` decorator or `type()` call. skip.\n */\n if (!hasDecorator && !hasExpression) {\n break;\n }\n\n // using as decorator\n if (propDecorator) {\n /**\n * Calling `@type()` as decorator\n */\n const typeDecorator: any = propDecorator.find((decorator => {\n return (decorator.expression as any).expression.escapedText === decoratorName;\n })).expression;\n\n const property = currentProperty || new Property();\n property.name = prop.name.escapedText;\n currentStructure.addProperty(property);\n\n const typeArgument = typeDecorator.arguments[0];\n defineProperty(property, typeArgument);\n\n } else if (\n prop.expression.arguments?.[1] &&\n prop.expression.expression.arguments?.[0]\n ) {\n /**\n * Calling `type()` as a regular method\n */\n const property = currentProperty || new Property();\n property.name = prop.expression.arguments[1].text;\n currentStructure.addProperty(property);\n\n const typeArgument = prop.expression.expression.arguments[0];\n defineProperty(property, typeArgument);\n }\n\n } else if (\n node.getText() === \"setFields\" &&\n (\n node.parent.kind === ts.SyntaxKind.CallExpression ||\n node.parent.kind === ts.SyntaxKind.PropertyAccessExpression\n )\n ) {\n /**\n * Metadata.setFields(klassName, { ... })\n */\n const callExpression = (node.parent.kind === ts.SyntaxKind.PropertyAccessExpression)\n ? node.parent.parent as ts.CallExpression\n : node.parent as ts.CallExpression;\n\n /**\n * Skip if @codegen-ignore comment is found before the call expression\n * TODO: currently, if @codegen-ignore is on the file, it will skip all the setFields calls.\n */\n const sourceFile = node.getSourceFile();\n const fullText = sourceFile.getFullText();\n const nodeStart = callExpression.getFullStart();\n const textBeforeNode = fullText.substring(0, nodeStart);\n if (textBeforeNode.includes('@codegen-ignore')) {\n break;\n }\n\n if (callExpression.kind !== ts.SyntaxKind.CallExpression) {\n break;\n }\n\n const classNameNode = callExpression.arguments[0];\n const className = ts.isClassExpression(classNameNode)\n ? classNameNode.name?.escapedText.toString()\n : classNameNode.getText();\n\n // skip if no className is provided\n if (!className) { break; }\n\n if (currentStructure?.name !== className) {\n currentStructure = new Class();\n }\n context.addStructure(currentStructure);\n (currentStructure as Class).extends = \"Schema\"; // force extends to Schema\n currentStructure.name = className;\n\n const types = callExpression.arguments[1] as any;\n for (let i = 0; i < types.properties.length; i++) {\n const prop = types.properties[i];\n\n const property = currentProperty || new Property();\n property.name = prop.name.escapedText;\n\n currentStructure.addProperty(property);\n defineProperty(property, prop.initializer);\n }\n\n } else if (\n node.getText() === \"defineTypes\" &&\n (\n node.parent.kind === ts.SyntaxKind.CallExpression ||\n node.parent.kind === ts.SyntaxKind.PropertyAccessExpression\n )\n ) {\n /**\n * JavaScript source file (`.js`)\n * Using `defineTypes()`\n */\n const callExpression = (node.parent.kind === ts.SyntaxKind.PropertyAccessExpression)\n ? node.parent.parent as ts.CallExpression\n : node.parent as ts.CallExpression;\n\n if (callExpression.kind !== ts.SyntaxKind.CallExpression) {\n break;\n }\n\n const className = callExpression.arguments[0].getText()\n currentStructure.name = className;\n\n const types = callExpression.arguments[1] as any;\n for (let i = 0; i < types.properties.length; i++) {\n const prop = types.properties[i];\n\n const property = currentProperty || new Property();\n property.name = prop.name.escapedText;\n currentStructure.addProperty(property);\n\n defineProperty(property, prop.initializer);\n }\n\n }\n\n if (node.parent.kind === ts.SyntaxKind.ClassDeclaration) {\n currentStructure.name = node.getText();\n }\n\n currentProperty = undefined;\n\n break;\n\n case ts.SyntaxKind.CallExpression:\n /**\n * Defining schema via `schema.schema({ ... })`\n * - schema.schema({})\n * - schema({})\n * - ClassName.extends({})\n */\n if (\n (\n (\n (node as ts.CallExpression).expression?.getText() === \"schema.schema\" ||\n (node as ts.CallExpression).expression?.getText() === \"schema\"\n ) ||\n (\n (node as ts.CallExpression).expression?.getText().indexOf(\".extends\") !== -1\n )\n ) &&\n (node as ts.CallExpression).arguments[0].kind === ts.SyntaxKind.ObjectLiteralExpression\n ) {\n const callExpression = node as ts.CallExpression;\n\n let className = callExpression.arguments[1]?.getText();\n\n if (!className && callExpression.parent.kind === ts.SyntaxKind.VariableDeclaration) {\n className = (callExpression.parent as ts.VariableDeclaration).name?.getText();\n }\n\n // skip if no className is provided\n if (!className) { break; }\n\n if (currentStructure?.name !== className) {\n currentStructure = new Class();\n context.addStructure(currentStructure);\n }\n\n if ((node as ts.CallExpression).expression?.getText().indexOf(\".extends\") !== -1) {\n // if it's using `.extends({})`\n const extendsClass = (node as any).expression?.expression?.escapedText;\n\n // skip if no extendsClass is provided\n if (!extendsClass) { break; }\n (currentStructure as Class).extends = extendsClass;\n\n } else {\n // if it's using `schema({})`\n (currentStructure as Class).extends = \"Schema\"; // force extends to Schema\n }\n\n currentStructure.name = className;\n\n const types = callExpression.arguments[0] as any;\n for (let i = 0; i < types.properties.length; i++) {\n const prop = types.properties[i];\n\n const property = currentProperty || new Property();\n property.name = prop.name.escapedText;\n\n currentStructure.addProperty(property);\n defineProperty(property, prop.initializer);\n }\n }\n\n break;\n\n case ts.SyntaxKind.EnumMember:\n if (currentStructure instanceof Enum) {\n const initializer = (node as any).initializer?.text;\n const name = node.getFirstToken().getText();\n const property = currentProperty || new Property();\n property.name = name;\n if (initializer !== undefined) {\n property.type = initializer;\n }\n currentStructure.addProperty(property);\n currentProperty = undefined;\n }\n break;\n }\n\n ts.forEachChild(node, (n: ts.Node) => inspectNode(n, context, decoratorName));\n}\n\nlet parsedFiles: { [filename: string]: boolean };\n\nexport function parseFiles(\n fileNames: string[],\n decoratorName: string = \"type\",\n context: Context = new Context()\n) {\n /**\n * Re-set globalContext for each test case\n */\n if (globalContext !== context) {\n parsedFiles = {};\n globalContext = context;\n }\n\n fileNames.forEach((fileName) => {\n let sourceFile: ts.Node;\n let sourceFileName: string;\n\n const fileNameAlternatives = [];\n\n if (\n !fileName.endsWith(\".ts\") &&\n !fileName.endsWith(\".js\") &&\n !fileName.endsWith(\".mjs\")\n ) {\n fileNameAlternatives.push(`${fileName}.ts`);\n fileNameAlternatives.push(`${fileName}/index.ts`);\n\n } else if (fileName.endsWith(\".js\")) {\n // Handle .js extensions by also trying .ts (ESM imports often use .js extension)\n fileNameAlternatives.push(fileName);\n fileNameAlternatives.push(fileName.replace(/\\.js$/, \".ts\"));\n\n } else {\n fileNameAlternatives.push(fileName);\n }\n\n for (let i = 0; i < fileNameAlternatives.length; i++) {\n try {\n sourceFileName = path.resolve(fileNameAlternatives[i]);\n\n if (parsedFiles[sourceFileName]) {\n break;\n }\n\n sourceFile = ts.createSourceFile(\n sourceFileName,\n readFileSync(sourceFileName).toString(),\n ts.ScriptTarget.Latest,\n true\n );\n\n parsedFiles[sourceFileName] = true;\n\n break;\n } catch (e) {\n // console.log(`${fileNameAlternatives[i]} => ${e.message}`);\n }\n }\n\n if (sourceFile) {\n inspectNode(sourceFile, context, decoratorName);\n }\n });\n\n return context.getStructures();\n}\n\n/**\n * TypeScript 4.8+ has introduced a change on how to access decorators.\n * - https://github.com/microsoft/TypeScript/pull/49089\n * - https://devblogs.microsoft.com/typescript/announcing-typescript-4-8/#decorators-are-placed-on-modifiers-on-typescripts-syntax-trees\n */\nexport function getDecorators(node: ts.Node | null | undefined,): undefined | ts.Decorator[] {\n if (node == undefined) { return undefined; }\n\n // TypeScript 4.7 and below\n // @ts-ignore\n if (node.decorators) { return node.decorators; }\n\n // TypeScript 4.8 and above\n // @ts-ignore\n if (ts.canHaveDecorators && ts.canHaveDecorators(node)) {\n // @ts-ignore\n const decorators = ts.getDecorators(node);\n return decorators ? Array.from(decorators) : undefined;\n }\n\n // @ts-ignore\n return node.modifiers?.filter(ts.isDecorator);\n}\n","import {\n Class,\n Property,\n File,\n getCommentHeader,\n Interface,\n Enum,\n} from \"../types.js\";\nimport { GenerateOptions } from \"../api.js\";\nimport { Context } from \"../types.js\";\n\nexport const name = \"Unity/C#\";\n\nconst typeMaps: { [key: string]: string } = {\n \"string\": \"string\",\n \"number\": \"float\",\n \"boolean\": \"bool\",\n \"int8\": \"sbyte\",\n \"uint8\": \"byte\",\n \"int16\": \"short\",\n \"uint16\": \"ushort\",\n \"int32\": \"int\",\n \"uint32\": \"uint\",\n \"int64\": \"long\",\n \"uint64\": \"ulong\",\n \"float32\": \"float\",\n \"float64\": \"double\",\n}\n\nconst COMMON_IMPORTS = `using Colyseus.Schema;\n#if UNITY_5_3_OR_NEWER\nusing UnityEngine.Scripting;\n#endif`;\n\n/**\n * C# Code Generator\n */\nconst capitalize = (s: string) => {\n if (typeof s !== 'string') return ''\n return s.charAt(0).toUpperCase() + s.slice(1);\n}\n\n/**\n * Generate individual files for each class/interface/enum\n */\nexport function generate(context: Context, options: GenerateOptions): File[] {\n // enrich typeMaps with enums\n context.enums.forEach((structure) => {\n typeMaps[structure.name] = structure.name;\n });\n return [\n ...context.classes.map(structure => ({\n name: `${structure.name}.cs`,\n content: generateClass(structure, options.namespace)\n })),\n ...context.interfaces.map(structure => ({\n name: `${structure.name}.cs`,\n content: generateInterface(structure, options.namespace),\n })),\n ...context.enums.filter(structure => structure.name !== 'OPERATION').map((structure) => ({\n name: `${structure.name}.cs`,\n content: generateEnum(structure, options.namespace),\n })),\n ];\n}\n\n/**\n * Generate a single bundled file containing all classes, interfaces, and enums\n */\nexport function renderBundle(context: Context, options: GenerateOptions): File {\n const fileName = options.namespace ? `${options.namespace}.cs` : \"Schema.cs\";\n const indent = options.namespace ? \"\\t\" : \"\";\n\n // enrich typeMaps with enums\n context.enums.forEach((structure) => {\n typeMaps[structure.name] = structure.name;\n });\n\n // Collect all bodies\n const classBodies = context.classes.map(klass => generateClassBody(klass, indent));\n const interfaceBodies = context.interfaces.map(iface => generateInterfaceBody(iface, indent));\n const enumBodies = context.enums\n .filter(structure => structure.name !== 'OPERATION')\n .map(e => generateEnumBody(e, indent));\n\n const allBodies = [...classBodies, ...interfaceBodies, ...enumBodies].join(\"\\n\\n\");\n\n const content = `${getCommentHeader()}\n\n${COMMON_IMPORTS}\n${options.namespace ? `\\nnamespace ${options.namespace} {\\n` : \"\"}\n${allBodies}\n${options.namespace ? \"}\" : \"\"}`;\n\n return { name: fileName, content };\n}\n\n/**\n * Generate just the class body (without imports/namespace) for bundling\n */\nfunction generateClassBody(klass: Class, indent: string = \"\"): string {\n return `${indent}public partial class ${klass.name} : ${klass.extends} {\n#if UNITY_5_3_OR_NEWER\n[Preserve]\n#endif\npublic ${klass.name}() { }\n${klass.properties.map((prop) => generateProperty(prop, indent)).join(\"\\n\\n\")}\n${indent}}`;\n}\n\n/**\n * Generate a complete class file with imports/namespace (for individual file mode)\n */\nfunction generateClass(klass: Class, namespace: string) {\n const indent = (namespace) ? \"\\t\" : \"\";\n return `${getCommentHeader()}\n\n${COMMON_IMPORTS}\n${namespace ? `\\nnamespace ${namespace} {` : \"\"}\n${generateClassBody(klass, indent)}\n${namespace ? \"}\" : \"\"}\n`;\n}\n\n/**\n * Check if all enum members resolve to non-negative integers,\n * allowing emission as a native C# `enum` (which only supports integral types).\n */\nfunction canUseNativeEnum(_enum: Enum): boolean {\n return _enum.properties.every((prop) => {\n if (!prop.type) return true;\n const n = Number(prop.type);\n return Number.isInteger(n) && n >= 0;\n });\n}\n\n/**\n * Generate just the enum body (without imports/namespace) for bundling\n */\nfunction generateEnumBody(_enum: Enum, indent: string = \"\"): string {\n if (canUseNativeEnum(_enum)) {\n const members = _enum.properties\n .map((prop, i) => {\n const value = prop.type ? Number(prop.type) : i;\n return `${indent}\\t${prop.name} = ${value},`;\n })\n .join(\"\\n\");\n return `${indent}public enum ${_enum.name} : int {\n${members}\n${indent}}`;\n }\n\n return `${indent}public struct ${_enum.name} {\n\n${_enum.properties\n .map((prop) => {\n let dataType: string = \"int\";\n let value: any;\n\n if(prop.type) {\n if(isNaN(Number(prop.type))) {\n value = `\"${prop.type}\"`;\n dataType = \"string\";\n } else {\n value = Number(prop.type);\n dataType = Number.isInteger(value)? 'int': 'float';\n }\n } else {\n value = _enum.properties.indexOf(prop);\n }\n return `${indent}\\tpublic const ${dataType} ${prop.name} = ${value};`;\n })\n .join(\"\\n\")}\n${indent}}`;\n}\n\n/**\n * Generate a complete enum file with imports/namespace (for individual file mode)\n */\nfunction generateEnum(_enum: Enum, namespace: string) {\n const indent = namespace ? \"\\t\" : \"\";\n return `${getCommentHeader()}\n${namespace ? `\\nnamespace ${namespace} {` : \"\"}\n${generateEnumBody(_enum, indent)}\n${namespace ? \"}\" : \"\"}`\n}\n\nfunction generateProperty(prop: Property, indent: string = \"\") {\n let typeArgs = `\"${prop.type}\"`;\n let property = \"public\";\n let langType: string;\n let initializer = \"\";\n\n if (prop.childType) {\n const isUpcaseFirst = prop.childType.match(/^[A-Z]/);\n\n langType = getType(prop);\n typeArgs += `, typeof(${langType})`;\n\n if (!isUpcaseFirst) {\n typeArgs += `, \"${prop.childType}\"`;\n }\n\n initializer = `null`;\n\n } else {\n langType = getType(prop);\n initializer = `default(${langType})`;\n }\n\n property += ` ${langType} ${prop.name}`;\n\n let ret = (prop.deprecated) ? `\\t\\t[System.Obsolete(\"field '${prop.name}' is deprecated.\", true)]\\n` : '';\n\n return ret + `\\t${indent}[Type(${prop.index}, ${typeArgs})]\n\\t${indent}${property} = ${initializer};`;\n}\n\n/**\n * Generate just the interface body (without imports/namespace) for bundling\n */\nfunction generateInterfaceBody(struct: Interface, indent: string = \"\"): string {\n return `${indent}public class ${struct.name} {\n${struct.properties.map(prop => `\\t${indent}public ${getType(prop)} ${prop.name};`).join(\"\\n\")}\n${indent}}`;\n}\n\n/**\n * Generate a complete interface file with imports/namespace (for individual file mode)\n */\nfunction generateInterface(struct: Interface, namespace: string) {\n const indent = (namespace) ? \"\\t\" : \"\";\n return `${getCommentHeader()}\n\nusing Colyseus.Schema;\n${namespace ? `\\nnamespace ${namespace} {` : \"\"}\n${generateInterfaceBody(struct, indent)}\n${namespace ? \"}\" : \"\"}\n`;\n}\n\nfunction getChildType(prop: Property) {\n return typeMaps[prop.childType];\n}\n\nfunction getType(prop: Property) {\n if (prop.childType) {\n const isUpcaseFirst = prop.childType.match(/^[A-Z]/);\n let type: string;\n\n if(prop.type === \"ref\") {\n type = (isUpcaseFirst)\n ? prop.childType\n : getChildType(prop);\n } else {\n const containerClass = capitalize(prop.type);\n type = (isUpcaseFirst)\n ? `${containerClass}Schema<${prop.childType}>`\n : `${containerClass}Schema<${getChildType(prop)}>`;\n }\n return type;\n\n } else {\n return (prop.type === \"array\")\n ? `${typeMaps[prop.childType] || prop.childType}[]`\n : typeMaps[prop.type];\n }\n}\n","import { Class, Property, File, getCommentHeader, getInheritanceTree, Context } from \"../types.js\";\nimport { GenerateOptions } from \"../api.js\";\n\nexport const name = \"C++\";\n\nconst typeMaps: { [key: string]: string } = {\n \"string\": \"string\",\n \"number\": \"varint_t\",\n \"boolean\": \"bool\",\n \"int8\": \"int8_t\",\n \"uint8\": \"uint8_t\",\n \"int16\": \"int16_t\",\n \"uint16\": \"uint16_t\",\n \"int32\": \"int32_t\",\n \"uint32\": \"uint32_t\",\n \"int64\": \"int64_t\",\n \"uint64\": \"uint64_t\",\n \"float32\": \"float32_t\",\n \"float64\": \"float64_t\",\n}\n\nconst typeInitializer: { [key: string]: string } = {\n \"string\": '\"\"',\n \"number\": \"0\",\n \"boolean\": \"false\",\n \"int8\": \"0\",\n \"uint8\": \"0\",\n \"int16\": \"0\",\n \"uint16\": \"0\",\n \"int32\": \"0\",\n \"uint32\": \"0\",\n \"int64\": \"0\",\n \"uint64\": \"0\",\n \"float32\": \"0\",\n \"float64\": \"0\",\n}\n\nconst COMMON_INCLUDES = `#include \"schema.h\"\n#include <typeinfo>\n#include <typeindex>\n\nusing namespace colyseus::schema;`;\n\n/**\n * C++ Code Generator\n */\n\nconst capitalize = (s: string) => {\n if (typeof s !== 'string') return ''\n return s.charAt(0).toUpperCase() + s.slice(1);\n}\nconst distinct = (value: string, index: number, self: string[]) =>\n self.indexOf(value) === index;\n\n/**\n * Generate individual files for each class\n */\nexport function generate (context: Context, options: GenerateOptions): File[] {\n return context.classes.map(klass => ({\n name: klass.name + \".hpp\",\n content: generateClass(klass, options.namespace, context.classes)\n }));\n}\n\n/**\n * Generate a single bundled header file containing all classes\n */\nexport function renderBundle(context: Context, options: GenerateOptions): File {\n const fileName = options.namespace ? `${options.namespace}.hpp` : \"schema.hpp\";\n const guardName = `__SCHEMA_CODEGEN_${(options.namespace || \"SCHEMA\").toUpperCase()}_H__`;\n\n const classBodies = context.classes.map(klass => generateClassBody(klass, context.classes, options.namespace));\n\n const content = `${getCommentHeader()}\n#ifndef ${guardName}\n#define ${guardName} 1\n\n${COMMON_INCLUDES}\n\n${options.namespace ? `namespace ${options.namespace} {\\n` : \"\"}\n${classBodies.join(\"\\n\\n\")}\n${options.namespace ? \"}\" : \"\"}\n\n#endif\n`;\n\n return { name: fileName, content };\n}\n\n/**\n * Generate just the class body (without includes/guards) for bundling\n */\nfunction generateClassBody(klass: Class, allClasses: Class[], namespace: string): string {\n const propertiesPerType: {[type: string]: Property[]} = {};\n const allRefs: Property[] = [];\n klass.properties.forEach(property => {\n let type = property.type;\n\n if (!propertiesPerType[type]) {\n propertiesPerType[type] = [];\n }\n\n propertiesPerType[type].push(property);\n\n // keep all refs list\n if ((type === \"ref\" || type === \"array\" || type === \"map\")) {\n allRefs.push(property);\n }\n });\n\n const allProperties = getAllProperties(klass, allClasses);\n const createInstanceMethod = (allRefs.length === 0) ? \"\" :\n `\\tinline Schema* createInstance(std::type_index type) {\n\\t\\t${generateFieldIfElseChain(allRefs,\n (property) => `type == typeid(${property.childType})`,\n (property) => `return new ${property.childType}();`,\n (property) => typeMaps[property.childType] === undefined)}\n\\t\\treturn ${klass.extends}::createInstance(type);\n\\t}`;\n\n return `class ${klass.name} : public ${klass.extends} {\npublic:\n${klass.properties.map(prop => generateProperty(prop)).join(\"\\n\")}\n\n\\t${klass.name}() {\n\\t\\tthis->_indexes = ${generateAllIndexes(allProperties)};\n\\t\\tthis->_types = ${generateAllTypes(allProperties)};\n\\t\\tthis->_childPrimitiveTypes = ${generateAllChildPrimitiveTypes(allProperties)};\n\\t\\tthis->_childSchemaTypes = ${generateAllChildSchemaTypes(allProperties)};\n\\t}\n\n\\tvirtual ~${klass.name}() {\n\\t\\t${generateDestructors(allProperties).join(\"\\n\\t\\t\")}\n\\t}\n\nprotected:\n${Object.keys(propertiesPerType).map(type =>\n generateGettersAndSetters(klass, type, propertiesPerType[type])).\n join(\"\\n\")}\n\n${createInstanceMethod}\n};`;\n}\n\n/**\n * Generate a complete class file with includes/guards (for individual file mode)\n */\nfunction generateClass(klass: Class, namespace: string, allClasses: Class[]) {\n const allRefs: Property[] = [];\n klass.properties.forEach(property => {\n let type = property.type;\n // keep all refs list\n if ((type === \"ref\" || type === \"array\" || type === \"map\")) {\n allRefs.push(property);\n }\n });\n\n const localIncludes = allRefs.\n filter(ref => ref.childType && typeMaps[ref.childType] === undefined).\n map(ref => ref.childType).\n concat(getInheritanceTree(klass, allClasses, false).map(klass => klass.name)).\n filter(distinct).\n map(childType => `#include \"${childType}.hpp\"`).\n join(\"\\n\");\n\n return `${getCommentHeader()}\n#ifndef __SCHEMA_CODEGEN_${klass.name.toUpperCase()}_H__\n#define __SCHEMA_CODEGEN_${klass.name.toUpperCase()}_H__ 1\n\n${COMMON_INCLUDES}\n${localIncludes}\n\n${namespace ? `namespace ${namespace} {` : \"\"}\n${generateClassBody(klass, allClasses, namespace)}\n${namespace ? \"}\" : \"\"}\n\n#endif\n`;\n}\n\nfunction generateProperty(prop: Property) {\n let property = \"\";\n let langType: string;\n let initializer = \"\";\n let isPropPointer = \"\";\n\n if (prop.childType) {\n const isUpcaseFirst = prop.childType.match(/^[A-Z]/);\n\n if(prop.type === \"ref\") {\n langType = `${prop.childType}`;\n initializer = `new ${prop.childType}()`;\n\n } else if(prop.type === \"array\") {\n langType = (isUpcaseFirst)\n ? `ArraySchema<${prop.childType}*>`\n : `ArraySchema<${typeMaps[prop.childType]}>`;\n initializer = `new ${langType}()`;\n\n } else if(prop.type === \"map\") {\n langType = (isUpcaseFirst)\n ? `MapSchema<${prop.childType}*>`\n : `MapSchema<${typeMaps[prop.childType]}>`;\n initializer = `new ${langType}()`;\n }\n isPropPointer = \"*\";\n\n } else {\n langType = typeMaps[prop.type];\n initializer = typeInitializer[prop.type];\n }\n\n property += ` ${langType} ${isPropPointer}${prop.name}`;\n\n return `\\t${property} = ${initializer};`\n}\n\nfunction generateGettersAndSetters(klass: Class, type: string, properties: Property[]) {\n let langType = typeMaps[type];\n let typeCast = \"\";\n\n const getMethodName = `get${capitalize(type)}`;\n const setMethodName = `set${capitalize(type)}`;\n\n if (type === \"ref\") {\n langType = \"Schema*\";\n\n } else if (type === \"array\") {\n langType = `ArraySchema<char*> *`;\n typeCast = `(ArraySchema<char*> *)`;\n\n } else if (type === \"map\") {\n langType = `MapSchema<char*> *`;\n typeCast = `(MapSchema<char*> *)`;\n }\n\n return `\\tinline ${langType} ${getMethodName}(const string &field)\n\\t{\n\\t\\t${generateFieldIfElseChain(properties,\n (property) => `field == \"${property.name}\"`,\n (property) => `return ${typeCast}this->${property.name};`)}\n\\t\\treturn ${klass.extends}::${getMethodName}(field);\n\\t}\n\n\\tinline void ${setMethodName}(const string &field, ${langType} value)\n\\t{\n\\t\\t${generateFieldIfElseChain(properties,\n (property) => `field == \"${property.name}\"`,\n (property) => {\n const isSchemaType = (typeMaps[property.childType] === undefined)\n\n if (type === \"ref\") {\n langType = `${property.childType}*`;\n typeCast = (isSchemaType)\n ? `(${property.childType}*)`\n : `/* bug? */`;\n\n } else if (type === \"array\") {\n typeCast = (isSchemaType)\n ? `(ArraySchema<${property.childType}*> *)`\n : `(ArraySchema<${typeMaps[property.childType]}> *)`;\n\n } else if (type === \"map\") {\n typeCast = (isSchemaType)\n ? `(MapSchema<${property.childType}*> *)`\n : `(MapSchema<${typeMaps[property.childType]}> *)`;\n }\n\n return `this->${property.name} = ${typeCast}value;\\n\\t\\t\\treturn;`\n })}\n\\t\\treturn ${klass.extends}::${setMethodName}(field, value);\n\\t}`;\n}\n\nfunction generateFieldIfElseChain(\n properties: Property[],\n ifCallback: (property: Property) => string,\n callback: (property: Property) => string,\n filter: (property: Property) => boolean = (_) => true,\n) {\n let chain = \"\";\n\n const uniqueChecks: string[] = [];\n properties.filter(filter).forEach((property, i) => {\n const check = ifCallback(property);\n if (uniqueChecks.indexOf(check) === -1) {\n uniqueChecks.push(check);\n\n } else {\n return;\n }\n\n if (i === 0) { chain += \"if \" } else { chain += \" else if \" }\n chain += `(${check})\n\\t\\t{\n\\t\\t\\t${callback(property)}\\n\n\\t\\t}`\n });\n\n return chain;\n}\n\nfunction generateAllIndexes(properties: Property[]) {\n return `{${properties.map((property, i) => `{${i}, \"${property.name}\"}`).join(\", \")}}`\n\n}\n\nfunction generateAllTypes(properties: Property[]) {\n return `{${properties.map((property, i) => `{${i}, \"${property.type}\"}`).join(\", \")}}`\n}\n\nfunction generateAllChildSchemaTypes(properties: Property[]) {\n return `{${properties.map((property, i) => {\n if (property.childType && typeMaps[property.childType] === undefined) {\n return `{${i}, typeid(${property.childType})}`\n } else {\n return null;\n }\n }).filter(r => r !== null).join(\", \")}}`\n}\n\nfunction generateAllChildPrimitiveTypes(properties: Property[]) {\n return `{${properties.map((property, i) => {\n if (typeMaps[property.childType] !== undefined) {\n return `{${i}, \"${property.childType}\"}`\n } else {\n return null;\n }\n }).filter(r => r !== null).join(\", \")}}`\n}\n\nfunction generateDestructors(properties: Property[]) {\n return properties.map((property, i) => {\n if (property.childType) {\n return `delete this->${property.name};`;\n } else {\n return null;\n }\n }).filter(r => r !== null);\n}\n\nfunction getAllProperties (klass: Class, allClasses: Class[]) {\n let properties: Property[] = [];\n\n getInheritanceTree(klass, allClasses).reverse().forEach((klass) => {\n properties = properties.concat(klass.properties);\n });\n\n return properties;\n}","import { Class, Property, File, getCommentHeader, Context } from \"../types.js\";\nimport { GenerateOptions } from \"../api.js\";\n\nexport const name = \"Haxe\";\n\nconst typeMaps: { [key: string]: string } = {\n \"string\": \"String\",\n \"number\": \"Dynamic\",\n \"boolean\": \"Bool\",\n \"int8\": \"Int\",\n \"uint8\": \"UInt\",\n \"int16\": \"Int\",\n \"uint16\": \"UInt\",\n \"int32\": \"Int\",\n \"uint32\": \"UInt\",\n \"int64\": \"Int\",\n \"uint64\": \"UInt\",\n \"float32\": \"Float\",\n \"float64\": \"Float\",\n}\n\nconst typeInitializer: { [key: string]: string } = {\n \"string\": '\"\"',\n \"number\": \"0\",\n \"boolean\": \"false\",\n \"int8\": \"0\",\n \"uint8\": \"0\",\n \"int16\": \"0\",\n \"uint16\": \"0\",\n \"int32\": \"0\",\n \"uint32\": \"0\",\n \"int64\": \"0\",\n \"uint64\": \"0\",\n \"float32\": \"0\",\n \"float64\": \"0\",\n}\n\nconst COMMON_IMPORTS = `import io.colyseus.serializer.schema.Schema;\nimport io.colyseus.serializer.schema.types.*;`;\n\n/**\n * Generate individual files for each class\n */\nexport function generate (context: Context, options: GenerateOptions): File[] {\n return context.classes.map(klass => ({\n name: klass.name + \".hx\",\n content: generateClass(klass, options.namespace, context.classes)\n }));\n}\n\n/**\n * Generate a single bundled file containing all classes\n */\nexport function renderBundle(context: Context, options: GenerateOptions): File {\n const fileName = options.namespace ? `${options.namespace}.hx` : \"Schema.hx\";\n\n const classBodies = context.classes.map(klass => generateClassBody(klass));\n\n const content = `${getCommentHeader()}\n\n${options.namespace ? `package ${options.namespace};` : \"\"}\n${COMMON_IMPORTS}\n\n${classBodies.join(\"\\n\\n\")}\n`;\n\n return { name: fileName, content };\n}\n\nfunction getInheritanceTree(klass: Class, allClasses: Class[], includeSelf: boolean = true) {\n let currentClass = klass;\n let inheritanceTree: Class[] = [];\n\n if (includeSelf) {\n inheritanceTree.push(currentClass);\n }\n\n while (currentClass.extends !== \"Schema\") {\n currentClass = allClasses.find(klass => klass.name == currentClass.extends);\n inheritanceTree.push(currentClass);\n }\n\n return inheritanceTree;\n}\n\n/**\n * Generate just the class body (without package/imports) for bundling\n */\nfunction generateClassBody(klass: Class): string {\n return `class ${klass.name} extends ${klass.extends} {\n${klass.properties.map(prop => generateProperty(prop)).join(\"\\n\")}\n}`;\n}\n\n/**\n * Generate a complete class file with package/imports (for individual file mode)\n */\nfunction generateClass(klass: Class, namespace: string, allClasses: Class[]) {\n return `${getCommentHeader()}\n\n${namespace ? `package ${namespace};` : \"\"}\n${COMMON_IMPORTS}\n\n${generateClassBody(klass)}\n`;\n}\n\nfunction generateProperty(prop: Property) {\n let langType: string;\n let initializer = \"\";\n let typeArgs = `\"${prop.type}\"`;\n\n if (prop.childType) {\n const isUpcaseFirst = prop.childType.match(/^[A-Z]/);\n\n if (isUpcaseFirst) {\n typeArgs += `, ${prop.childType}`;\n\n } else {\n typeArgs += `, \"${prop.childType}\"`;\n }\n\n if(prop.type === \"ref\") {\n langType = `${prop.childType}`;\n initializer = `new ${prop.childType}()`;\n\n } else if(prop.type === \"array\") {\n langType = (isUpcaseFirst)\n ? `ArraySchema<${prop.childType}>`\n : `ArraySchema<${typeMaps[prop.childType]}>`;\n initializer = `new ${langType}()`;\n\n } else if(prop.type === \"map\") {\n langType = (isUpcaseFirst)\n ? `MapSchema<${prop.childType}>`\n : `MapSchema<${typeMaps[prop.childType]}>`;\n initializer = `new ${langType}()`;\n }\n\n } else {\n langType = typeMaps[prop.type];\n initializer = typeInitializer[prop.type];\n }\n\n // TODO: remove initializer. The callbacks at the Haxe decoder side have a\n // \"FIXME\" comment about this on Decoder.hx\n\n return `\\t@:type(${typeArgs})\\n\\tpublic var ${prop.name}: ${langType} = ${initializer};\\n`\n // return `\\t@:type(${typeArgs})\\n\\tpublic var ${prop.name}: ${langType};\\n`\n}\n","import { Class, Property, File, getCommentHeader, getInheritanceTree, Context, Interface } from \"../types.js\";\nimport { GenerateOptions } from \"../api.js\";\n\nexport const name = \"TypeScript\";\n\nconst typeMaps: { [key: string]: string } = {\n \"string\": \"string\",\n \"number\": \"number\",\n \"boolean\": \"boolean\",\n \"int8\": \"number\",\n \"uint8\": \"number\",\n \"int16\": \"number\",\n \"uint16\": \"number\",\n \"int32\": \"number\",\n \"uint32\": \"number\",\n \"int64\": \"number\",\n \"uint64\": \"number\",\n \"float32\": \"number\",\n \"float64\": \"number\",\n}\n\nconst COMMON_IMPORTS = `import { Schema, type, ArraySchema, MapSchema, SetSchema, DataChange } from '@colyseus/schema';`;\n\nconst distinct = (value: string, index: number, self: string[]) =>\n self.indexOf(value) === index;\n\n/**\n * Generate individual files for each class/interface\n */\nexport function generate (context: Context, options: GenerateOptions): File[] {\n return [\n ...context.classes.map(structure => ({\n name: structure.name + \".ts\",\n content: generateClass(structure, options.namespace, context.classes)\n })),\n ...context.interfaces.map(structure => ({\n name: structure.name + \".ts\",\n content: generateInterface(structure, options.namespace, context.classes),\n }))\n ];\n}\n\n/**\n * Generate a single bundled file containing all classes and interfaces\n */\nexport function renderBundle(context: Context, options: GenerateOptions): File {\n const fileName = options.namespace ? `${options.namespace}.ts` : \"schema.ts\";\n\n // Collect all class bodies\n const classBodies = context.classes.map(klass => generateClassBody(klass));\n\n // Collect all interface bodies\n const interfaceBodies = context.interfaces.map(iface => generateInterfaceBody(iface));\n\n const content = `${getCommentHeader()}\n\n${COMMON_IMPORTS}\n\n${classBodies.join(\"\\n\\n\")}\n$