UNPKG

plaxtony

Version:

Static code analysis of SC2 Galaxy Script

647 lines (565 loc) 19.1 kB
import * as fs from 'fs'; import * as xml from 'xml2js'; export const enum ElementFlag { Native = 1 << 1, FuncAction = 1 << 2, FuncCall = 1 << 3, Event = 1 << 4, Template = 1 << 5, PresetGenConstVar = 1 << 6, PresetCustom = 1 << 7, CustomScript = 1 << 8, Operator = 1 << 9, CustomAI = 1 << 10, SubFunctions = 1 << 11, AllowBreak = 1 << 12, Hidden = 1 << 13, NoScriptPrefix = 1 << 14, Deprecated = 1 << 15, Internal = 1 << 16, Restricted = 1 << 17, } export class ElementReference<T extends Element> { private store: TriggerStore; id: string; type: typeof Element; library?: string; public constructor(container: TriggerStore) { this.store = container; } public link() { return this.type.name + '/' + this.id; } public globalLink() { let link = this.link(); if (this.library) { link = this.library + '/' + link; } return link; } public resolve(): T | undefined { if (this.library) { const lib = this.store.getLibraries().get(this.library); return lib.findElementById(this.link(), null) as T; } return this.store.findElementById(this.link(), null) as T; // return this.container.findElementById(this.id, this.type) as T; } } export class ParameterType { type: string; gameType?: string; // when type == gamelink; e.g. Model; (may be omitted - "any") typeElement?: ElementReference<Preset>; // when type == preset; get galaxyType(): string { switch (this.type) { case 'anygamelink': case 'gamelink': case 'filepath': case 'actormsg': case 'aidef': case 'modelanim': case 'attributegame': case 'attributeplayer': case 'attributevalue': case 'catalogentry': case 'catalogfieldname': case 'catalogfieldpath': case 'reference': case 'catalogscope': case 'charge': case 'convcharacter': case 'convline': case 'convstateindex': case 'conversationtag': case 'cooldown': case 'fontstyle': case 'gameoption': case 'gameoptionvalue': case 'modelcamera': case 'timeofday': case 'layoutframe': case 'layoutframerel': case 'userfield': case 'userinstance': case 'water': { return 'string'; } case 'aidefwave': case 'cinematic': case 'conversation': case 'reply': case 'datatable': case 'dialog': case 'control': case 'difficulty': case 'objective': case 'path': case 'ping': case 'planet': case 'playercolor': case 'portrait': case 'transmission': { return 'int'; } case 'animlengthquery': { return 'generichandle'; } case 'targetfilter': { return 'unitfilter'; } case 'preset': return this.typeElement.resolve().baseType; default: return this.type; } } } export abstract class Tag { static prefix?: string; libId?: string; id: string; name?: string; public link() { return (this.libId ? this.libId + '/' : '') + (this.constructor.name + '/') + this.id; } public toString() { const parts: string[] = []; if (this.libId) { parts.push('lib' + this.libId); } const prefix = (<any>this.constructor).prefix; if (prefix) { parts.push(prefix); } parts.push(this.name ? this.name : this.id); return parts.join('_'); } public textKey(kind: string) { const parts: string[] = []; parts.push(this.constructor.name); parts.push(kind); if (this.libId) { parts.push(['lib', this.libId, this.id].join('_')); } else { parts.push(this.id); } return parts.join('/'); } } export abstract class Element extends Tag { flags: ElementFlag; label?: ElementReference<Label>; items: ElementReference<Element>[] = []; public toString() { if (this.flags & ElementFlag.Native) { return this.name ? this.name : this.id; } return super.toString(); } } export class ParamDef extends Element { type: ParameterType; isReference?: boolean; // ParamFlagReference default?: ElementReference<Param>; get galaxyType(): string { const rtype = this.type.galaxyType; if (this.isReference) { switch (rtype) { case 'unit': { return 'unitref'; break; } } } return rtype; } } export class FunctionDef extends Element { static prefix = 'gf'; parameters: ElementReference<ParamDef>[] = []; returnType?: ParameterType; scriptCode?: string; eventResponses: ElementReference<FunctionDef>[] = []; public getParameters() { return this.parameters.map((paramRef): ParamDef => { return paramRef.resolve() as ParamDef; }); } } export class Preset extends Element { baseType: string; values: ElementReference<PresetValue>[] = []; } export class PresetValue extends Element { value?: string; } export class Param extends Element { functionCall?: ElementReference<FunctionCall>; preset?: ElementReference<PresetValue>; value?: string; valueType?: string; valueElement?: ElementReference<Preset>; } export class FunctionCall extends Element { functionDef: ElementReference<FunctionDef>; } export class Category extends Element { }; export class Label extends Element { icon?: string; color?: string; }; const ElementClasses = { ParamDef, FunctionDef, Preset, PresetValue, Param, FunctionCall, Category, Label, }; // export class TriggerExplorer { // protected containers: ElementContainer; // } export abstract class ElementContainer { public items: ElementReference<Element>[] = []; protected elements: Map<string, Element> = new Map(); protected nameMap: Map<string, Element> = new Map(); public addElement(identifier: string, el: Element) { el.id = identifier; if (el instanceof FunctionDef) { this.nameMap.set(el.name, el); } this.elements.set(el.constructor.name + '/' + el.id, el); } public findElementByName(name: string): Element | undefined { return this.nameMap.get(name); } public findPresetValueByStr(value: string): PresetValue | undefined { for (const el of this.elements.values()) { if (!(el instanceof PresetValue)) continue; if (el.value && el.value == value) { return el; } } return null; } public findPresetByValue(value: PresetValue): Preset | undefined { for (const el of this.elements.values()) { if (!(el instanceof Preset)) continue; const belongsTo = el.values.find((localVal) => { return value.id === localVal.id; }); if (belongsTo) { return el; } } return null; } public findElementById<T extends Element>(id: string, type?: { new(): T ;}): T | undefined { if (type && type.name !== 'Element') { id = type.name + '/' + id; } return this.elements.get(id) as T; } public getElements() { return <ReadonlyMap<string, Element>>this.elements; } } export class Library extends ElementContainer { protected id: string; constructor(id: string) { super(); this.id = id; } public addElement(identifier: string, el: Element) { el.libId = this.id; super.addElement(identifier, el); } public getId() { return this.id; } } export type LibraryContainer = Map<string, Library>; export class TriggerStore extends ElementContainer { protected libraries: LibraryContainer = new Map<string, Library>(); protected unresolvedReferences = new Map<string, ElementReference<Element>[]>(); public addElementReference(ref: ElementReference<Element>) { const link = ref.globalLink(); if (this.unresolvedReferences.has(link)) { const refList = this.unresolvedReferences.get(link); refList.push(ref); } else { this.unresolvedReferences.set(link, [ref]); } } public addLibrary(library: Library) { this.libraries.set(library.getId(), library); } // public findElementById<T extends Element>(elementId: string, libraryId?: string): T | undefined { // if (libraryId) { // return this.libraries.get(libraryId).findElementById(elementId) as T; // } // return super.findElementById(elementId) as T; // } public getLibraries() { return <ReadonlyMap<string, Library>>this.libraries; } } export class XMLReader { protected store: TriggerStore; private parseReference<T extends Element>(data: any, type: { new(): T }): ElementReference<T> { const ref = new ElementReference<T>(this.store); ref.id = data.$.Id; if (data.$.Library) { ref.library = data.$.Library; } ref.type = (ElementClasses as any)[type.name]; this.store.addElementReference(ref); return ref; } private parseParam(item: any): Param { const element = new Param(); if (item.FunctionCall) { element.functionCall = this.parseReference(item.FunctionCall[0], FunctionCall); } if (item.Preset) { element.preset = this.parseReference(item.Preset[0], PresetValue); } if (item.Value) { element.value = item.Value[0]; } if (item.ValueType) { element.valueType = item.ValueType[0].$.Type; if (item.ValueGameType) { // item.ValueGameType[0].$.Type; } } if (item.ValueElement) { element.valueElement = this.parseReference(item.ValueElement[0], Preset); } return element; } private parseParamDef(item: any): ParamDef { const paramDef = new ParamDef(); paramDef.type = this.parseParameterType(item.ParameterType[0]); if (item.Default) { paramDef.default = this.parseReference(item.Default[0], Param); } if (item.ParamFlagReference) { paramDef.isReference = true; } return paramDef; } private parseFunctionCall(item: any): FunctionCall { const element = new FunctionCall(); if (item.FunctionDef) { // TODO: check one of the void story libraries - it doesn't have FunctionDef element.functionDef = this.parseReference(item.FunctionDef[0], FunctionDef); } return element; } private parseParameterType(item: any): ParameterType { const element = new ParameterType(); element.type = item.Type[0].$.Value; if (element.type === 'gamelink' && item.GameType) { element.gameType = item.GameType[0].$.Value; } if (element.type === 'preset') { element.typeElement = this.parseReference(item.TypeElement[0], Preset); } return element; } private parseCategory(item: any) { const element = new Category(); return element; } private parseLabel(item: any) { const element = new Label(); if (item.Icon) { element.icon = item.Icon[0]; } if (item.Color) { element.color = item.Color[0]; } return element; } private parseElement(item: any): Element { let el: Element; switch (item.$.Type) { case 'FunctionDef': { const func = el = new FunctionDef(); func.flags |= item.FlagNative ? ElementFlag.Native : 0; func.flags |= item.FlagAction ? ElementFlag.FuncAction : 0; func.flags |= item.FlagCall ? ElementFlag.FuncCall : 0; func.flags |= item.FlagEvent ? ElementFlag.Event : 0; func.flags |= item.Template ? ElementFlag.Template : 0; func.flags |= item.FlagCustomScript ? ElementFlag.CustomScript : 0; func.flags |= item.FlagOperator ? ElementFlag.Operator : 0; func.flags |= item.FlagCustomAI ? ElementFlag.CustomAI : 0; func.flags |= item.FlagSubFunctions ? ElementFlag.SubFunctions : 0; func.flags |= item.FlagAllowBreak ? ElementFlag.AllowBreak : 0; func.flags |= item.FlagHidden ? ElementFlag.Hidden : 0; func.flags |= item.FlagNoScriptPrefix ? ElementFlag.NoScriptPrefix : 0; func.flags |= item.Deprecated ? ElementFlag.Deprecated : 0; func.flags |= item.Internal ? ElementFlag.Internal : 0; func.flags |= item.FlagRestricted ? ElementFlag.Restricted : 0; if (item.Parameter) { for (const param of item.Parameter) { if (param.$.Type === 'Comment') continue; func.parameters.push(this.parseReference(param, ParamDef)); } } if (item.ReturnType) { func.returnType = this.parseParameterType(item.ReturnType[0]); } if (item.ScriptCode) { func.scriptCode = item.ScriptCode[0]; const whitespace = func.scriptCode.match(/^\r?\n(\s+)/); if (whitespace) { func.scriptCode = func.scriptCode.trim().replace(/\r?\n/g, '\n').replace(new RegExp('^' + whitespace[1], 'gm'), ''); } } if (item.EventResponse) { for (const er of item.EventResponse) { func.eventResponses.push(this.parseReference(er, FunctionDef)); } } break; } case 'ParamDef': { el = this.parseParamDef(item); break; } case 'Param': el = this.parseParam(item); break; case 'FunctionCall': el = this.parseFunctionCall(item); break; case 'Preset': { const preset = el = new Preset(); preset.flags |= item.PresetGenConstVar ? ElementFlag.PresetGenConstVar : 0; preset.flags |= item.PresetCustom ? ElementFlag.PresetCustom : 0; preset.baseType = item.BaseType[0].$.Value; if (item.Item) { for (const val of item.Item) { if (val.$.Type !== 'PresetValue') continue; preset.values.push(this.parseReference(val, PresetValue)); } } // item.$.PresetCustom // item.$.PresetGenConstVar // item.$.PresetAsBits break; } case 'PresetValue': { const presetValue = el = new PresetValue(); if (item.Value) { presetValue.value = item.Value[0]; } break; } case 'Category': el = this.parseCategory(item); break; case 'Label': el = this.parseLabel(item); break; default: { return null; } } if (item.Item) { el.items = this.parseItems(item); } if (item.Label) { el.label = this.parseReference(item.Label[0], Label); } if (item.Identifier) { el.name = item.Identifier[0]; } return el; } private parseItems(data: any) { const elItems: ElementReference<Element>[] = []; for (const itEntry of data.Item) { const cls = (ElementClasses as any)[itEntry.$.Type]; if (!cls) continue; elItems.push(this.parseReference(itEntry, cls)); } return elItems; } private parseTree(data: any, container: ElementContainer) { if (data.Root && data.Root[0].Item) { container.items = this.parseItems(data.Root[0]); } if (!data.Element) return; for (const item of data.Element) { const el = this.parseElement(item); if (el) { container.addElement(item.$.Id, el); } } } private parseLibrary(id: string, data: any): Library { const lib = new Library(id); this.parseTree(data, lib); return lib; } constructor(container: TriggerStore) { this.store = container; } protected loadXML(content: string) { return new Promise<any>((resolve, reject) => { xml.parseString(content, (err, result) => { if (err) { reject(err); } else { try { resolve(result.TriggerData); } catch (err) { reject(err); } } }); }); } public async loadLibrary(content: string): Promise<Library> { const data = await this.loadXML(content); const lib = this.parseLibrary(data.Standard[0].$.Id, data); this.store.addLibrary(lib); return lib; } public async load(content: string, onlyLibraries: boolean = false): Promise<TriggerStore> { const data = await this.loadXML(content); if (data.Library) { for (const item of data.Library) { const lib = this.parseLibrary(item.$.Id, item); this.store.addLibrary(lib); } } if (!onlyLibraries) { this.parseTree(data, this.store); } return this.store; } } // export function forEachElement(element: Element) { // function visitElement() // }