@colyseus/schema
Version:
Binary state serializer with delta encoding for games
220 lines (180 loc) • 6.3 kB
text/typescript
import {
Class,
Property,
File,
getCommentHeader,
getInheritanceTree,
Context,
Enum,
} from "../types.js";
import { GenerateOptions } from "../api.js";
export const name = "GDScript";
/**
* Type mappings from schema types to GDScript Colyseus.Schema type constants
*/
const typeMaps: { [key: string]: string } = {
"string": "Colyseus.Schema.STRING",
"number": "Colyseus.Schema.NUMBER",
"boolean": "Colyseus.Schema.BOOLEAN",
"int8": "Colyseus.Schema.INT8",
"uint8": "Colyseus.Schema.UINT8",
"int16": "Colyseus.Schema.INT16",
"uint16": "Colyseus.Schema.UINT16",
"int32": "Colyseus.Schema.INT32",
"uint32": "Colyseus.Schema.UINT32",
"int64": "Colyseus.Schema.INT64",
"uint64": "Colyseus.Schema.UINT64",
"float32": "Colyseus.Schema.FLOAT32",
"float64": "Colyseus.Schema.FLOAT64",
};
const containerMaps: { [key: string]: string } = {
"array": "Colyseus.Schema.ARRAY",
"map": "Colyseus.Schema.MAP",
"ref": "Colyseus.Schema.REF",
};
const distinct = (value: string, index: number, self: string[]) =>
self.indexOf(value) === index;
/**
* GDScript Code Generator
*/
/**
* Generate individual files for each class
*/
export function generate(context: Context, options: GenerateOptions): File[] {
// Enrich typeMaps with enums
context.enums.forEach((structure) => {
typeMaps[structure.name] = structure.name;
});
return [
...context.classes.map(klass => ({
name: `${klass.name}.gd`,
content: generateClass(klass, options.namespace, context.classes)
})),
...context.enums.filter(structure => structure.name !== 'OPERATION').map((structure) => ({
name: `${structure.name}.gd`,
content: generateEnum(structure, options.namespace),
})),
];
}
/**
* Generate a single bundled file containing all classes and enums
*/
export function renderBundle(context: Context, options: GenerateOptions): File {
const fileName = options.namespace ? `${options.namespace}.gd` : "schema.gd";
// Enrich typeMaps with enums
context.enums.forEach((structure) => {
typeMaps[structure.name] = structure.name;
});
const enumBodies = context.enums
.filter(structure => structure.name !== 'OPERATION')
.map(e => generateEnumBody(e));
const classBodies = context.classes.map(klass => generateClassBody(klass));
const content = `${getCommentHeader("#")}
${enumBodies.length > 0 ? enumBodies.join("\n\n") + "\n\n" : ""}${classBodies.join("\n\n")}
`;
return { name: fileName, content };
}
/**
* Generate just the class body (without preload) for bundling
*/
function generateClassBody(klass: Class): string {
// Determine parent class
const parentClass = (klass.extends !== "Schema")
? klass.extends
: "Colyseus.Schema";
const properties = klass.properties;
const fieldsContent = properties.length > 0
? properties.map(prop => generateFieldDefinition(prop)).join(",\n") + ","
: "";
// Generate _to_string() method
const toStringMethod = generateToStringMethod(klass.name, properties);
return `class ${klass.name} extends ${parentClass}:
static func definition():
return [
${fieldsContent}
]
${toStringMethod}`;
}
/**
* Generate _to_string() method for the class
*/
function generateToStringMethod(className: string, properties: Property[]): string {
const fieldNames = properties.map(prop => prop.name);
const allFields = ["__ref_id", ...fieldNames];
const formatParts = allFields.map(name => `${name}: %s`).join(", ");
const formatString = `${className}(${formatParts})`;
const selfReferences = allFields.map(name => `self.${name}`).join(", ");
return `\tfunc _to_string() -> String:
return "${formatString}" % [${selfReferences}]`;
}
/**
* Generate a complete class file with preload (for individual file mode)
*/
function generateClass(klass: Class, namespace: string, allClasses: Class[]) {
const allRefs: Property[] = [];
klass.properties.forEach(property => {
let type = property.type;
// Keep all refs list
if ((type === "ref" || type === "array" || type === "map")) {
allRefs.push(property);
}
});
// Get required preloads for referenced types
const preloads = allRefs
.filter(ref => ref.childType && typeMaps[ref.childType] === undefined)
.map(ref => ref.childType)
.concat(getInheritanceTree(klass, allClasses, false).map(klass => klass.name))
.filter(distinct)
.map(childType => `const ${childType} = preload("${childType}.gd")`)
.join("\n");
return `${getCommentHeader("#")}
${preloads ? preloads + "\n\n" : ""}${generateClassBody(klass)}
`;
}
/**
* Generate a field definition for the definition() array
*/
function generateFieldDefinition(prop: Property): string {
let args: string[];
if (prop.childType) {
const isUpcaseFirst = prop.childType.match(/^[A-Z]/);
// Array or Map container
const containerType = containerMaps[prop.type];
const childTypeRef = isUpcaseFirst ? prop.childType : typeMaps[prop.childType] || `"${prop.childType}"`;
args = [`"${prop.name}"`, containerType, childTypeRef];
} else {
// Primitive type
const typeRef = typeMaps[prop.type] || `"${prop.type}"`;
args = [`"${prop.name}"`, typeRef];
}
return `\t\t\tColyseus.Schema.Field.new(${args.join(", ")})`;
}
/**
* Generate just the enum body for bundling
*/
function generateEnumBody(_enum: Enum): string {
const enumValues = _enum.properties.map((prop, index) => {
let value: any;
if (prop.type) {
if (isNaN(Number(prop.type))) {
value = `"${prop.type}"`;
} else {
value = Number(prop.type);
}
} else {
value = index;
}
return `\t"${prop.name}": ${value},`;
}).join("\n");
return `const ${_enum.name} = {
${enumValues}
}`;
}
/**
* Generate a complete enum file (for individual file mode)
*/
function generateEnum(_enum: Enum, _namespace: string) {
return `${getCommentHeader("#")}
${generateEnumBody(_enum)}
`;
}