@colyseus/schema
Version:
Binary state serializer with delta encoding for games
269 lines (231 loc) • 7.81 kB
text/typescript
import {
Class,
Property,
File,
getCommentHeader,
Interface,
Enum,
} from "../types.js";
import { GenerateOptions } from "../api.js";
import { Context } from "../types.js";
export const name = "Unity/C#";
const typeMaps: { [key: string]: string } = {
"string": "string",
"number": "float",
"boolean": "bool",
"int8": "sbyte",
"uint8": "byte",
"int16": "short",
"uint16": "ushort",
"int32": "int",
"uint32": "uint",
"int64": "long",
"uint64": "ulong",
"float32": "float",
"float64": "double",
}
const COMMON_IMPORTS = `using Colyseus.Schema;
#if UNITY_5_3_OR_NEWER
using UnityEngine.Scripting;
#endif`;
/**
* C# Code Generator
*/
const capitalize = (s: string) => {
if (typeof s !== 'string') return ''
return s.charAt(0).toUpperCase() + s.slice(1);
}
/**
* Generate individual files for each class/interface/enum
*/
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(structure => ({
name: `${structure.name}.cs`,
content: generateClass(structure, options.namespace)
})),
...context.interfaces.map(structure => ({
name: `${structure.name}.cs`,
content: generateInterface(structure, options.namespace),
})),
...context.enums.filter(structure => structure.name !== 'OPERATION').map((structure) => ({
name: `${structure.name}.cs`,
content: generateEnum(structure, options.namespace),
})),
];
}
/**
* Generate a single bundled file containing all classes, interfaces, and enums
*/
export function renderBundle(context: Context, options: GenerateOptions): File {
const fileName = options.namespace ? `${options.namespace}.cs` : "Schema.cs";
const indent = options.namespace ? "\t" : "";
// enrich typeMaps with enums
context.enums.forEach((structure) => {
typeMaps[structure.name] = structure.name;
});
// Collect all bodies
const classBodies = context.classes.map(klass => generateClassBody(klass, indent));
const interfaceBodies = context.interfaces.map(iface => generateInterfaceBody(iface, indent));
const enumBodies = context.enums
.filter(structure => structure.name !== 'OPERATION')
.map(e => generateEnumBody(e, indent));
const allBodies = [...classBodies, ...interfaceBodies, ...enumBodies].join("\n\n");
const content = `${getCommentHeader()}
${COMMON_IMPORTS}
${options.namespace ? `\nnamespace ${options.namespace} {\n` : ""}
${allBodies}
${options.namespace ? "}" : ""}`;
return { name: fileName, content };
}
/**
* Generate just the class body (without imports/namespace) for bundling
*/
function generateClassBody(klass: Class, indent: string = ""): string {
return `${indent}public partial class ${klass.name} : ${klass.extends} {
#if UNITY_5_3_OR_NEWER
[Preserve]
#endif
public ${klass.name}() { }
${klass.properties.map((prop) => generateProperty(prop, indent)).join("\n\n")}
${indent}}`;
}
/**
* Generate a complete class file with imports/namespace (for individual file mode)
*/
function generateClass(klass: Class, namespace: string) {
const indent = (namespace) ? "\t" : "";
return `${getCommentHeader()}
${COMMON_IMPORTS}
${namespace ? `\nnamespace ${namespace} {` : ""}
${generateClassBody(klass, indent)}
${namespace ? "}" : ""}
`;
}
/**
* Check if all enum members resolve to non-negative integers,
* allowing emission as a native C# `enum` (which only supports integral types).
*/
function canUseNativeEnum(_enum: Enum): boolean {
return _enum.properties.every((prop) => {
if (!prop.type) return true;
const n = Number(prop.type);
return Number.isInteger(n) && n >= 0;
});
}
/**
* Generate just the enum body (without imports/namespace) for bundling
*/
function generateEnumBody(_enum: Enum, indent: string = ""): string {
if (canUseNativeEnum(_enum)) {
const members = _enum.properties
.map((prop, i) => {
const value = prop.type ? Number(prop.type) : i;
return `${indent}\t${prop.name} = ${value},`;
})
.join("\n");
return `${indent}public enum ${_enum.name} : int {
${members}
${indent}}`;
}
return `${indent}public struct ${_enum.name} {
${_enum.properties
.map((prop) => {
let dataType: string = "int";
let value: any;
if(prop.type) {
if(isNaN(Number(prop.type))) {
value = `"${prop.type}"`;
dataType = "string";
} else {
value = Number(prop.type);
dataType = Number.isInteger(value)? 'int': 'float';
}
} else {
value = _enum.properties.indexOf(prop);
}
return `${indent}\tpublic const ${dataType} ${prop.name} = ${value};`;
})
.join("\n")}
${indent}}`;
}
/**
* Generate a complete enum file with imports/namespace (for individual file mode)
*/
function generateEnum(_enum: Enum, namespace: string) {
const indent = namespace ? "\t" : "";
return `${getCommentHeader()}
${namespace ? `\nnamespace ${namespace} {` : ""}
${generateEnumBody(_enum, indent)}
${namespace ? "}" : ""}`
}
function generateProperty(prop: Property, indent: string = "") {
let typeArgs = `"${prop.type}"`;
let property = "public";
let langType: string;
let initializer = "";
if (prop.childType) {
const isUpcaseFirst = prop.childType.match(/^[A-Z]/);
langType = getType(prop);
typeArgs += `, typeof(${langType})`;
if (!isUpcaseFirst) {
typeArgs += `, "${prop.childType}"`;
}
initializer = `null`;
} else {
langType = getType(prop);
initializer = `default(${langType})`;
}
property += ` ${langType} ${prop.name}`;
let ret = (prop.deprecated) ? `\t\t[System.Obsolete("field '${prop.name}' is deprecated.", true)]\n` : '';
return ret + `\t${indent}[Type(${prop.index}, ${typeArgs})]
\t${indent}${property} = ${initializer};`;
}
/**
* Generate just the interface body (without imports/namespace) for bundling
*/
function generateInterfaceBody(struct: Interface, indent: string = ""): string {
return `${indent}public class ${struct.name} {
${struct.properties.map(prop => `\t${indent}public ${getType(prop)} ${prop.name};`).join("\n")}
${indent}}`;
}
/**
* Generate a complete interface file with imports/namespace (for individual file mode)
*/
function generateInterface(struct: Interface, namespace: string) {
const indent = (namespace) ? "\t" : "";
return `${getCommentHeader()}
using Colyseus.Schema;
${namespace ? `\nnamespace ${namespace} {` : ""}
${generateInterfaceBody(struct, indent)}
${namespace ? "}" : ""}
`;
}
function getChildType(prop: Property) {
return typeMaps[prop.childType];
}
function getType(prop: Property) {
if (prop.childType) {
const isUpcaseFirst = prop.childType.match(/^[A-Z]/);
let type: string;
if(prop.type === "ref") {
type = (isUpcaseFirst)
? prop.childType
: getChildType(prop);
} else {
const containerClass = capitalize(prop.type);
type = (isUpcaseFirst)
? `${containerClass}Schema<${prop.childType}>`
: `${containerClass}Schema<${getChildType(prop)}>`;
}
return type;
} else {
return (prop.type === "array")
? `${typeMaps[prop.childType] || prop.childType}[]`
: typeMaps[prop.type];
}
}