UNPKG

patchwork-mapconverter

Version:

Executable wrapper for https://github.com/ChiefOfGxBxL/WC3MapTranslator

755 lines (676 loc) 31.6 kB
import { TriggerDataRegistry } from '../enhancements/TriggerDataRegistry' import { type TriggerContainer } from './data/TriggerContainer' import { ContentType, type TriggerContent } from './data/content/TriggerContent' import { ContentTypeEnumConverter } from './util/ContentTypeEnumConverter' import { type ScriptContent } from './data/properties/ScriptContent' import { type CustomScript } from './data/content/CustomScript' import { type GlobalVariable } from './data/content/GlobalVariable' import { type GUITrigger } from './data/content/GUITrigger' import { type TriggerComment } from './data/content/TriggerComment' import { type Statement } from './data/statement/Statement' import { StatementTypeEnumConverter } from './util/StatementTypeEnumConverter' import { StatementType } from './data/statement/StatementType' import { type NestingStatement } from './data/statement/NestingStatement' import { ParameterTypeEnumConverter } from './util/ParameterTypeEnumConverter' import { type Parameter } from './data/parameter/Parameter' import { ParameterType } from './data/parameter/ParameterType' import { type WarResult, type JsonResult } from '../wc3maptranslator/CommonInterfaces' import { HexBuffer } from '../wc3maptranslator/HexBuffer' import { W3Buffer } from '../wc3maptranslator/W3Buffer' import { type Translator } from '../wc3maptranslator/translators' import { type ScriptedTrigger } from './data/content/ScriptedTrigger' interface TriggerTranslatorOutput { roots: TriggerContainer[] scriptReferences: Array<ScriptContent | null> } function countContentTypes (roots: TriggerContainer[]): Map<ContentType, number> { const triggerStack: TriggerContent[] = [...roots] const result = new Map<ContentType, number>() result.set(ContentType.HEADER, 0) result.set(ContentType.LIBRARY, 0) result.set(ContentType.CATEGORY, 0) result.set(ContentType.TRIGGER, 0) result.set(ContentType.COMMENT, 0) result.set(ContentType.CUSTOM_SCRIPT, 0) result.set(ContentType.VARIABLE, 0) while (triggerStack.length > 0) { const currentTrigger = triggerStack.pop() if (currentTrigger == null) continue const count = result.get(currentTrigger.contentType) if (count == null) { result.set(currentTrigger.contentType, 1) } else { result.set(currentTrigger.contentType, count + 1) } switch (currentTrigger.contentType) { case ContentType.HEADER: case ContentType.LIBRARY: case ContentType.CATEGORY: triggerStack.push(...(currentTrigger as TriggerContainer).children) } } return result } function getAllOfContentType (roots: TriggerContainer[], elementReference: Map<TriggerContent, number>, type: ContentType): Map<TriggerContent, number> { const triggerStack: TriggerContent[] = [...roots] const parentStack: TriggerContainer[] = [] const result = new Map<TriggerContent, number>() while (triggerStack.length > 0) { const currentTrigger = triggerStack.pop() if (currentTrigger == null) continue let parent = parentStack.pop() let parentId: number | undefined while (parent != null) { if (parent.children.findIndex((value) => value === currentTrigger) > -1) break // this is the parent parent = parentStack.pop() // go down the stack } if (parent == null) { parentId = -1 } else { parentStack.push(parent) parentId = elementReference.get(parent) if (parentId == null) { throw new Error('Parent ' + parent.name + ' has no ID??') // something wrong with tree traversal? :thinking: } } if ((currentTrigger as TriggerContainer).children != null) { parentStack.push(currentTrigger as TriggerContainer) } if (currentTrigger.contentType === type) { result.set(currentTrigger, parentId) } switch (currentTrigger.contentType) { case ContentType.HEADER: case ContentType.LIBRARY: case ContentType.CATEGORY: triggerStack.push(...(currentTrigger as TriggerContainer).children) } } return result } export class TriggersTranslator implements Translator<TriggerTranslatorOutput> { private static instance: TriggersTranslator | null = null private constructor () { } public static getInstance (): TriggersTranslator { if (this.instance == null) { this.instance = new this() } return this.instance } public static jsonToWar (triggers: TriggerTranslatorOutput): WarResult { return this.getInstance().jsonToWar(triggers) } public static warToJson (buffer: Buffer): JsonResult<TriggerTranslatorOutput> { return this.getInstance().warToJson(buffer) } public jsonToWar (json: TriggerTranslatorOutput): WarResult { const outBufferToWar = new HexBuffer() outBufferToWar.addChars('WTG!') // File header outBufferToWar.addByte(0x04) outBufferToWar.addByte(0x00) outBufferToWar.addByte(0x00) outBufferToWar.addByte(0x80) // Format version outBufferToWar.addInt(7) // TFT Game Version const contentTypeCounts = countContentTypes(json.roots) let totalElements = 0 for (const count of contentTypeCounts.values()) { totalElements = totalElements + count } const writeContentCount = function (contentType: ContentType): void { outBufferToWar.addInt(contentTypeCounts.get(contentType) as number) outBufferToWar.addInt(0) // deleted count } writeContentCount(ContentType.HEADER) writeContentCount(ContentType.LIBRARY) writeContentCount(ContentType.CATEGORY) writeContentCount(ContentType.TRIGGER) writeContentCount(ContentType.COMMENT) writeContentCount(ContentType.CUSTOM_SCRIPT) writeContentCount(ContentType.VARIABLE) outBufferToWar.addInt(0) // unknown outBufferToWar.addInt(0) // unknown outBufferToWar.addInt(2) // 1 for RoC?, 2 for TFT? const elementReference = new Map<TriggerContent, number>() let headerCount = 0 let libraryCount = 0 let categoryCount = 0 let triggerCount = 0 let commentCount = 0 let customScriptCount = 0 let variableCount = 0 const triggerStack: TriggerContent[] = [...json.roots] while (triggerStack.length > 0) { const currentTrigger = triggerStack.pop() if (currentTrigger == null) continue let elementId: number switch (currentTrigger.contentType) { case ContentType.HEADER: elementId = 0x00000000 + headerCount++ triggerStack.push(...(currentTrigger as TriggerContainer).children) break case ContentType.LIBRARY: elementId = 0x01000000 + libraryCount++ triggerStack.push(...(currentTrigger as TriggerContainer).children) break case ContentType.CATEGORY: elementId = 0x02000000 + categoryCount++ triggerStack.push(...(currentTrigger as TriggerContainer).children) break case ContentType.TRIGGER: case ContentType.TRIGGER_SCRIPTED: elementId = 0x03000000 + triggerCount++ break case ContentType.COMMENT: elementId = 0x04000000 + commentCount++ break case ContentType.CUSTOM_SCRIPT: elementId = 0x05000000 + customScriptCount++ break case ContentType.VARIABLE: elementId = 0x06000000 + variableCount++ break default: continue } elementReference.set(currentTrigger, elementId) } const variables = getAllOfContentType(json.roots, elementReference, ContentType.VARIABLE) as Map<GlobalVariable, number> outBufferToWar.addInt(variables.size) for (const [variable, parentId] of variables.entries()) { outBufferToWar.addString(variable.name) outBufferToWar.addString(variable.type) outBufferToWar.addInt(1) // unknown, always 1? outBufferToWar.addInt(variable.isArray ? 1 : 0) outBufferToWar.addInt(variable.arrayLength) outBufferToWar.addInt(variable.isInitialized ? 1 : 0) outBufferToWar.addString(variable.initialValue) const elementId = elementReference.get(variable) if (elementId == null) { throw new Error(`Variable ${variable.name} missing ID`) } outBufferToWar.addInt(elementId) outBufferToWar.addInt(parentId) // parent } triggerStack.push(...json.roots) const parentStack: TriggerContainer[] = [] outBufferToWar.addInt(totalElements) while (triggerStack.length > 0) { const currentTrigger = triggerStack.pop() if (currentTrigger == null) continue const elementId = elementReference.get(currentTrigger) if (elementId == null) { throw new Error(`TriggerContent ${currentTrigger.name} missing ID`) } outBufferToWar.addInt(ContentTypeEnumConverter.toIdentifier(currentTrigger.contentType)) let parent = parentStack.pop() let parentId: number | undefined while (parent != null) { if (parent.children.findIndex((value) => value === currentTrigger) > -1) break // this is the parent parent = parentStack.pop() // go down the stack } if (parent == null) { parentId = -1 } else { parentStack.push(parent) parentId = elementReference.get(parent) if (parentId == null) { throw new Error('Parent ' + parent.name + ' has no ID??') // something wrong with tree traversal? :thinking: } } if ((currentTrigger as TriggerContainer).children != null) { parentStack.push(currentTrigger as TriggerContainer) } let ecaCount: number switch (currentTrigger.contentType) { case ContentType.HEADER: case ContentType.LIBRARY: case ContentType.CATEGORY: outBufferToWar.addInt(elementId) outBufferToWar.addString(currentTrigger.name) outBufferToWar.addInt(0) // not a comment outBufferToWar.addInt((currentTrigger as TriggerContainer).isExpanded ? 1 : 0) // not expanded outBufferToWar.addInt(parentId) triggerStack.push(...(currentTrigger as TriggerContainer).children) break case ContentType.TRIGGER: case ContentType.TRIGGER_SCRIPTED: case ContentType.COMMENT: case ContentType.CUSTOM_SCRIPT: outBufferToWar.addString(currentTrigger.name) if (currentTrigger.contentType === ContentType.COMMENT) { outBufferToWar.addString((currentTrigger as TriggerComment).comment) } else { outBufferToWar.addString((currentTrigger as GUITrigger).description) } outBufferToWar.addInt(currentTrigger.contentType === ContentType.COMMENT ? 1 : 0) outBufferToWar.addInt(elementId) if (currentTrigger.contentType === ContentType.CUSTOM_SCRIPT) { outBufferToWar.addInt((currentTrigger as CustomScript).isEnabled ? 1 : 0) outBufferToWar.addInt(1) // is custom script outBufferToWar.addInt(0) // initially off outBufferToWar.addInt(0) // doesn't run on map init outBufferToWar.addInt(parentId) outBufferToWar.addInt(0) // ECA count 0 } else if (currentTrigger.contentType === ContentType.COMMENT) { outBufferToWar.addInt(0) // not enabled outBufferToWar.addInt(0) // is custom script outBufferToWar.addInt(0) // initially off outBufferToWar.addInt(0) // doesn't run on map init outBufferToWar.addInt(parentId) outBufferToWar.addInt(0) // ECA count 0 } else if (currentTrigger.contentType === ContentType.TRIGGER) { ecaCount = (currentTrigger as GUITrigger).events.length + (currentTrigger as GUITrigger).conditions.length + (currentTrigger as GUITrigger).actions.length outBufferToWar.addInt((currentTrigger as GUITrigger).isEnabled ? 1 : 0) outBufferToWar.addInt(0) // is custom script outBufferToWar.addInt((currentTrigger as GUITrigger).initiallyOff ? 1 : 0) outBufferToWar.addInt((currentTrigger as GUITrigger).runOnMapInit ? 1 : 0) outBufferToWar.addInt(parentId) outBufferToWar.addInt(ecaCount) const writeStatements = (parent: GUITrigger | Statement, ECAs: Statement[], group: number): void => { for (const eca of ECAs) { outBufferToWar.addInt(StatementTypeEnumConverter.toIdentifier(eca.type)) if (group !== -1) { outBufferToWar.addInt(group) } outBufferToWar.addString(eca.name) outBufferToWar.addInt(eca.isEnabled ? 1 : 0) const writeParams = (parent: Statement | Parameter, parameters: Parameter[]): void => { for (const param of parameters) { outBufferToWar.addInt(ParameterTypeEnumConverter.toIdentifier(param.type)) outBufferToWar.addString(param.value) if (param.statement != null) { outBufferToWar.addInt(1) // has sub params outBufferToWar.addInt(StatementTypeEnumConverter.toIdentifier(param.statement.type)) outBufferToWar.addString(param.statement.name) outBufferToWar.addInt(1) // begin function writeParams(param, param.statement.parameters) outBufferToWar.addInt(0) // unknown, end function maybe? } else { outBufferToWar.addInt(0) // no sub params } if (param.arrayIndex != null) { outBufferToWar.addInt(1) // is array writeParams(param, [param.arrayIndex]) } else { outBufferToWar.addInt(0) // is not array } } } writeParams(eca, eca.parameters) if ((eca as NestingStatement).statements != null) { const nestingStatement = (eca as NestingStatement) let ecaCount = 0 for (const nestedStatements of Object.values(nestingStatement.statements)) { if (nestedStatements != null) { ecaCount += nestedStatements.length } } outBufferToWar.addInt(ecaCount) for (const [group, nestedStatements] of Object.entries(nestingStatement.statements)) { if (nestedStatements != null) { writeStatements(eca, nestedStatements, Number(group)) } } } else { outBufferToWar.addInt(0) // ecaCount } } } writeStatements(currentTrigger as GUITrigger, (currentTrigger as GUITrigger).actions, -1) writeStatements(currentTrigger as GUITrigger, (currentTrigger as GUITrigger).events, -1) writeStatements(currentTrigger as GUITrigger, (currentTrigger as GUITrigger).conditions, -1) } else if (currentTrigger.contentType === ContentType.TRIGGER_SCRIPTED) { outBufferToWar.addInt((currentTrigger as ScriptedTrigger).isEnabled ? 1 : 0) outBufferToWar.addInt(1) // is custom script outBufferToWar.addInt(0) // initially off outBufferToWar.addInt((currentTrigger as ScriptedTrigger).runOnMapInit ? 1 : 0) outBufferToWar.addInt(parentId) outBufferToWar.addInt(0) } break case ContentType.VARIABLE: outBufferToWar.addInt(elementId) outBufferToWar.addString((currentTrigger as GlobalVariable).name) outBufferToWar.addInt(parentId) break } } return { buffer: outBufferToWar.getBuffer(), errors: [] } } public warToJson (buffer: Buffer): JsonResult<TriggerTranslatorOutput> { const outBufferToJSON = new W3Buffer(buffer) try { const fileId = outBufferToJSON.readChars(4) // WTG! const formatVersion = outBufferToJSON.readInt() // 04 00 00 80 const gameVersion = outBufferToJSON.readInt() // 4 = Roc, 7 = TFT const headerCount = outBufferToJSON.readInt() // always 1..? const deletedHeaderCount = outBufferToJSON.readInt() // Includes both existing and deleted headers. (Map script headers?) for (let i = 0; i < deletedHeaderCount; i++) { const headerId = outBufferToJSON.readInt() // always 0..? } const libraryCount = outBufferToJSON.readInt() const deletedLibraryCount = outBufferToJSON.readInt() for (let i = 0; i < deletedHeaderCount; i++) { const libraryId = outBufferToJSON.readInt() } const triggerCategoryCount = outBufferToJSON.readInt() const deletedCategoryCount = outBufferToJSON.readInt() for (let i = 0; i < deletedCategoryCount; i++) { // trigger categories const categoryId = outBufferToJSON.readInt() } const triggerCount = outBufferToJSON.readInt() const deletedTriggerCount = outBufferToJSON.readInt() for (let i = 0; i < deletedTriggerCount; i++) { const triggerId = outBufferToJSON.readInt() } const triggerCommentCount = outBufferToJSON.readInt() const deletedTriggerCommentCount = outBufferToJSON.readInt() for (let i = 0; i < deletedTriggerCommentCount; i++) { const triggerCommentId = outBufferToJSON.readInt() } const customScriptCount = outBufferToJSON.readInt() const deletedCustomScriptCount = outBufferToJSON.readInt() for (let i = 0; i < deletedCustomScriptCount; i++) { const customScriptId = outBufferToJSON.readInt() } const variableCount = outBufferToJSON.readInt() const deletedVariableCount = outBufferToJSON.readInt() for (let i = 0; i < deletedVariableCount; i++) { const variableId = outBufferToJSON.readInt() } outBufferToJSON.readInt() // unknown outBufferToJSON.readInt() // unknown const triggerDefinitionVersion = outBufferToJSON.readInt() // 1 for RoC?, 2 for TFT? const elementRelations = new Map<number, number>() const containers: Record<number, TriggerContainer> = {} const content: Record<number, TriggerContent> = {} const customScripts: Array<ScriptContent | null> = [] const allGlobalVariables: Record<number, GlobalVariable> = {} const existingVariablesCount = outBufferToJSON.readInt() for (let i = 0; i < existingVariablesCount; i++) { const globalVariable: GlobalVariable = { name: '', contentType: ContentType.VARIABLE, type: '', isArray: false, arrayLength: 0, isInitialized: false, initialValue: '' } globalVariable.name = outBufferToJSON.readString() globalVariable.type = outBufferToJSON.readString() outBufferToJSON.readInt() // unknown, always 1? globalVariable.isArray = outBufferToJSON.readInt() === 1 if (gameVersion === 7) { globalVariable.arrayLength = outBufferToJSON.readInt() } globalVariable.isInitialized = outBufferToJSON.readInt() === 1 globalVariable.initialValue = outBufferToJSON.readString() const variableId = outBufferToJSON.readInt() // last byte 06? allGlobalVariables[variableId] = globalVariable content[variableId] = globalVariable elementRelations.set(variableId, outBufferToJSON.readInt()) } const totalElements = outBufferToJSON.readInt() for (let i = 0; i < totalElements; i++) { const type = ContentTypeEnumConverter.toEnum(outBufferToJSON.readInt()) let elementId: number let name: string let description: string let isComment = false let isExpanded = false let isEnabled = false let isCustomScript = false let initiallyOff = false let runOnMapInit = false let ecaCount = 0 let parentElementId: number let trigger: GUITrigger let container: unknown switch (type) { case ContentType.HEADER: case ContentType.LIBRARY: case ContentType.CATEGORY: elementId = outBufferToJSON.readInt() name = outBufferToJSON.readString() if (gameVersion === 7) { isComment = outBufferToJSON.readInt() === 1 } isExpanded = outBufferToJSON.readInt() === 1 parentElementId = outBufferToJSON.readInt() elementRelations.set(elementId, parentElementId) container = { name, isExpanded, contentType: type, children: [] } containers[elementId] = container as TriggerContainer if (type === ContentType.HEADER) { customScripts.push(container as ScriptContent) } break case ContentType.TRIGGER: case ContentType.COMMENT: case ContentType.CUSTOM_SCRIPT: name = outBufferToJSON.readString() description = outBufferToJSON.readString() if (gameVersion === 7) { isComment = outBufferToJSON.readInt() === 1 } elementId = outBufferToJSON.readInt() isEnabled = outBufferToJSON.readInt() === 1 isCustomScript = outBufferToJSON.readInt() === 1 initiallyOff = outBufferToJSON.readInt() === 1 runOnMapInit = outBufferToJSON.readInt() === 1 parentElementId = outBufferToJSON.readInt() elementRelations.set(elementId, parentElementId) ecaCount = outBufferToJSON.readInt() if (type === ContentType.TRIGGER) { if (isCustomScript) { const script: ScriptedTrigger = { name, contentType: ContentType.TRIGGER_SCRIPTED, description, isEnabled, runOnMapInit, script: '' } content[elementId] = script customScripts.push(script) } else { trigger = { name, contentType: ContentType.TRIGGER, description, isEnabled, initiallyOff, runOnMapInit, events: [], conditions: [], actions: [] } const readStatements = (content: GUITrigger | Statement, functionCount: number, isChild: boolean): void => { for (let j = 0; j < functionCount; j++) { const functionType = StatementTypeEnumConverter.toEnum(outBufferToJSON.readInt()) let group = -1 if (isChild) { group = outBufferToJSON.readInt() } const name = outBufferToJSON.readString() const isEnabled = outBufferToJSON.readInt() === 1 const parameterCount = TriggerDataRegistry.getParameterCount(functionType, name) const readParams = (parent: Statement | Parameter, paramCount: number, arrayIndex: boolean): void => { for (let k = 0; k < paramCount; k++) { const paramType = ParameterTypeEnumConverter.toEnum(outBufferToJSON.readInt()) const value = outBufferToJSON.readString() const hasSubParameters = !(outBufferToJSON.readInt() === 0) const parameter: Parameter = { type: paramType, value } as Parameter if (hasSubParameters) { parameter.statement = { type: StatementTypeEnumConverter.toEnum(outBufferToJSON.readInt()), isEnabled: true, name: outBufferToJSON.readString(), parameters: [] as Parameter[] } satisfies Statement const beginParams = outBufferToJSON.readInt() !== 0 const subParamCount = TriggerDataRegistry.getParameterCount(parameter.statement.type, parameter.statement.name) if (subParamCount == null) { throw new Error('Missing parameter count for function ' + parameter.statement.name) } readParams(parameter.statement, subParamCount, false) } if (gameVersion === 4 && paramType === ParameterType.FUNCTION) { outBufferToJSON.readInt() } else if (gameVersion === 7 && hasSubParameters) { outBufferToJSON.readInt() } let isArray: boolean if (gameVersion !== 4 || paramType !== ParameterType.FUNCTION) { isArray = outBufferToJSON.readInt() === 1 } else { isArray = false } if (isArray) { readParams(parameter, 1, true) } if (arrayIndex) { (parent as Parameter).arrayIndex = parameter } else if ((parent as Parameter).statement != null) { ((parent as Parameter).statement as Statement).parameters.push(parameter) } else { (parent as Statement).parameters.push(parameter) } } } const statement: Statement = { name, type: functionType, isEnabled, parameters: [] } if (parameterCount == null) { throw new Error('Missing parameter count for function ' + name) } readParams(statement, parameterCount, false) if (gameVersion === 7) { const nestedEcaCount = outBufferToJSON.readInt() readStatements(statement, nestedEcaCount, true) } if ((content as GUITrigger).events != null) { // is GUITrigger switch (functionType) { case StatementType.EVENT: (content as GUITrigger).events.push(statement) break case StatementType.CONDITION: (content as GUITrigger).conditions.push(statement) break case StatementType.ACTION: (content as GUITrigger).actions.push(statement) break } } else { if ((content as NestingStatement).statements == null) { (content as NestingStatement).statements = [] } if ((content as NestingStatement).statements[group] != null) { (content as NestingStatement).statements[group].push(statement) } else { ((content as NestingStatement).statements[group]) = [statement] } } } } readStatements(trigger, ecaCount, false) content[elementId] = trigger customScripts.push(null) } } else if (type === ContentType.COMMENT) { content[elementId] = { name, contentType: ContentType.COMMENT, comment: description } satisfies TriggerComment as TriggerContent } else if (type === ContentType.CUSTOM_SCRIPT) { const script = { name, contentType: ContentType.CUSTOM_SCRIPT, description, isEnabled, script: '' } content[elementId] = script customScripts.push(script) } break case ContentType.VARIABLE: elementId = outBufferToJSON.readInt() name = outBufferToJSON.readString() // excess data? parentElementId = outBufferToJSON.readInt() elementRelations.set(elementId, parentElementId) break } } const roots: TriggerContainer[] = [] const missingElements: Array<{ data?: TriggerContainer | TriggerContent, elementId?: number, parentId?: number, foundParent: boolean, foundElement: boolean }> = [] // Generate data tree structure for (const [elementId, parentId] of elementRelations.entries()) { if (parentId === -1) { if (containers[elementId] != null) { roots.push(containers[elementId]) } } else { let parent: TriggerContainer | undefined if (containers[parentId] != null) { parent = containers[parentId] } let element: TriggerContainer | TriggerContent | undefined if (containers[elementId] != null) { element = containers[elementId] } else if (content[elementId] != null) { element = content[elementId] } if (parent == null || element == null) { missingElements.push({ foundElement: element != null, foundParent: parent != null, elementId, parentId, data: parent != null ? parent : element }) continue } parent.children.push(element) } } return { json: { roots, scriptReferences: customScripts }, errors: [] } } catch (e) { return { json: { roots: [], scriptReferences: [] }, errors: [ { message: ` Error at offset: ${(outBufferToJSON as unknown as { _offset: number })._offset}` }, { message: e as string } ] } } } } export type { TriggerTranslatorOutput }