UNPKG

patchwork-mapconverter

Version:

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

346 lines (318 loc) 15.9 kB
import path from 'path' import { LoggerFactory } from '../logging/LoggerFactory' import { GetTriggerContainerChildren, type TriggerContainer } from '../translator/data/TriggerContainer' import EnhancementManager from './EnhancementManager' import TreeIterator from '../util/TreeIterator' import { ContentType, type TriggerContent } from '../translator/data/content/TriggerContent' import { type MapHeader } from '../translator/data/MapHeader' import { type ScriptContent } from '../translator/data/properties/ScriptContent' import { type GlobalVariable } from '../translator/data/content/GlobalVariable' import { type TriggerComment } from '../translator/data/content/TriggerComment' import * as ini from 'ini' import { type GUITrigger } from '../translator/data/content/GUITrigger' import { WriteAndCreatePath } from '../util/WriteAndCreatePath' import type directoryTree from 'directory-tree' import { type DirectoryTree } from 'directory-tree' import { readFile } from 'fs/promises' import { type CustomScript } from '../translator/data/content/CustomScript' import { type ScriptedTrigger } from '../translator/data/content/ScriptedTrigger' import { FileBlacklist } from './FileBlacklist' import { FormatConverters } from '../converter/formats/FormatConverters' const log = LoggerFactory.createLogger('TriggerComposer') async function populateComment(element: TriggerComment, child: DirectoryTree): Promise<void> { element.comment = await readFile(child.path, 'utf8') } // handles both GUI trigger and Variable async function populateGUIContent(element: TriggerContent, child: DirectoryTree): Promise<void> { const trigger: unknown = FormatConverters[EnhancementManager.guiExtension].parse(await readFile(child.path, 'utf8')) for (const [key, value] of Object.entries(trigger as JSON)) { if (key === 'children') continue if (value == null || value === '') continue element[key] = value as unknown } } async function populateCustomScript(element: CustomScript, child: DirectoryTree): Promise<void> { element.script = await readFile(child.path, 'utf8') } async function populateParentDetails(parent: TriggerContainer, file: DirectoryTree): Promise<void> { const record = ini.parse(await readFile(file.path, 'utf8')) for (const [key, value] of Object.entries(record)) { if (key === 'children') continue // ignore children entry, that one is handled internally (shouldn't exist anyways) parent[key] = value as unknown } } function safeReplaceTriggerName(name: string): string { return name .replaceAll('/', '-') .replaceAll('\\', '-') .replaceAll(':', ';') .replaceAll('*', '+') .replaceAll('?', '!') .replaceAll('"', '\'') .replaceAll('<', '(') .replaceAll('>', ')') .replaceAll('|', '_') } function generateTriggerOrder(parent: TriggerContainer): string[] { const commentCounts: Record<string, number> = {} return parent.children.map(it => { it.name = safeReplaceTriggerName(it.name) if (it.contentType === ContentType.COMMENT) { if (commentCounts[it.name] != null) { const count = commentCounts[it.name] + 1 commentCounts[it.name] = count it.name = `${it.name}_${count}` } else { commentCounts[it.name] = 1 } } return it.name }) } type OrderedTriggerContainer = TriggerContainer & { order: string[] } function sortTriggerContent(root: OrderedTriggerContainer): void { let newChildrenOrder = new Array(root.order != null ? root.order.length : 0) as TriggerContent[] const unspecifiedChildren: TriggerContent[] = [] const containerChildrenRecord = Object.values(root.children).reduce((ret, value) => { ret[value.name] = value return ret }, {}) as Record<string, TriggerContent> if (root.order == null) root.order = [] const orderedContentRecord = Object.entries(root.order).reduce((ret, entry) => { const [key, value] = entry ret[value] = key return ret }, {}) as Record<string, number> for (const [name, content] of Object.entries(containerChildrenRecord)) { const desiredIndex = orderedContentRecord[name] if (desiredIndex == null) { unspecifiedChildren.push(content) } else { newChildrenOrder[desiredIndex] = content } } newChildrenOrder = newChildrenOrder.filter(it => it != null) newChildrenOrder.push(...unspecifiedChildren) root.children = newChildrenOrder for (const child of root.children) { if ((child as TriggerContainer).children != null) { sortTriggerContent(child as OrderedTriggerContainer) } } } const TriggerComposer = { composeTriggerJson: async function (input: DirectoryTree): Promise<TriggerContainer> { const tasks: Array<Promise<unknown>> = [] const result = { name: EnhancementManager.mapHeaderFilename, contentType: ContentType.HEADER, isExpanded: false, children: [], description: '', script: '' } satisfies MapHeader const parentMap = new Map<DirectoryTree, TriggerContainer>() parentMap.set(input, result) const triggerContentMap = new Map<TriggerContainer, Map<string, TriggerContent[]>>() triggerContentMap.set(result, new Map<string, TriggerContent[]>()) const commentCounters = new Map<TriggerContainer, Record<string, number>>() if (input.children == null) { return result } for (const [parents, file] of TreeIterator<DirectoryTree>(input, (parent: directoryTree.DirectoryTree<Record<string, string>>) => parent.children)) { if (FileBlacklist.isDirectoryTreeBlacklisted(file)) continue let parent = parents.pop() if (parent == null) { parent = input } const containerParent = parentMap.get(parent) if (containerParent == null) { throw new Error('Something went wrong 2') } const scriptDisabled = file.extension === EnhancementManager.disabledExtension if (scriptDisabled) { const pos1 = file.name.lastIndexOf(EnhancementManager.disabledExtension) file.name = file.name.substring(0, pos1) file.extension = file.name.substring(file.name.lastIndexOf('.'), pos1) } if (file.type === 'directory' && file !== input) { const container = { name: file.name, contentType: ContentType.CATEGORY, isExpanded: false, children: [] } satisfies TriggerContainer parentMap.set(file, container) triggerContentMap.set(container, new Map<string, TriggerContent[]>()) containerParent.children.push(container) } else if (file.type === 'file') { if (file.extension === EnhancementManager.containerInfoExtension) { tasks.push(populateParentDetails(containerParent, file)) } else if (file.extension === EnhancementManager.guiExtension) { const element = { name: file.name.substring(0, file.name.lastIndexOf('.')), contentType: ContentType.TRIGGER, actions: [], arrayLength: 0, conditions: [], description: '', events: [], initiallyOff: false, initialValue: '', isArray: false, isEnabled: false, isInitialized: false, runOnMapInit: false, type: '' } satisfies GUITrigger | GlobalVariable | ScriptedTrigger if ((triggerContentMap.get(containerParent)?.has(element.name)) ?? false) { triggerContentMap.get(containerParent)?.get(element.name)?.push(element as TriggerContent) } else { triggerContentMap.get(containerParent)?.set(element.name, [element]) containerParent.children.push(element) } tasks.push(populateGUIContent(element, file)) } else if (file.extension === EnhancementManager.scriptExtension) { if (file.name.endsWith(`${EnhancementManager.mapHeaderFilename}${EnhancementManager.scriptExtension}`)) { tasks.push(populateCustomScript(result as unknown as CustomScript, file)) } else { const element = { name: file.name.substring(0, file.name.lastIndexOf('.')), contentType: ContentType.CUSTOM_SCRIPT, script: '', description: '', isEnabled: !scriptDisabled } satisfies CustomScript if ((triggerContentMap.get(containerParent)?.has(element.name)) ?? false) { triggerContentMap.get(containerParent)?.get(element.name)?.push(element as TriggerContent) } else { triggerContentMap.get(containerParent)?.set(element.name, [element]) containerParent.children.push(element) } tasks.push(populateCustomScript(element, file)) } } else if (file.extension === EnhancementManager.commentExtension) { const element = { name: file.name.substring(0, file.name.lastIndexOf('.')), contentType: ContentType.COMMENT, comment: '' } satisfies TriggerComment if (!commentCounters.has(containerParent)) { commentCounters.set(containerParent, { [element.name]: 1 }) } else { const commentCounts = commentCounters.get(containerParent) as Record<string, number> if (commentCounts[element.name] != null) { const count = commentCounts[element.name] + 1 commentCounters[element.name] = count element.name = `${element.name}_${count}` } else { commentCounts[element.name] = 1 } } containerParent.children.push(element) tasks.push(populateComment(element, file)) } } } for (const file of input.children) { if (file.type === 'file') { if (file.extension === EnhancementManager.containerInfoExtension) { tasks.push(populateParentDetails(result, file)) } } } await Promise.all(tasks) for (const [container, contentMap] of triggerContentMap) { for (const [, contents] of contentMap) { if (contents.length > 1) { let injectedRef: CustomScript | GUITrigger | ScriptedTrigger | undefined let script: CustomScript | undefined let trigger: GUITrigger | ScriptedTrigger | undefined for (const content of contents) { if (injectedRef == null) { injectedRef = content as ScriptedTrigger } switch (content.contentType) { case ContentType.TRIGGER: case ContentType.TRIGGER_SCRIPTED: trigger = content as ScriptedTrigger | GUITrigger break case ContentType.CUSTOM_SCRIPT: script = content as CustomScript break } if (script != null && trigger != null) { (injectedRef as ScriptedTrigger).isEnabled = trigger.isEnabled; (injectedRef as ScriptedTrigger).script = script.script; (injectedRef as ScriptedTrigger).contentType = ContentType.TRIGGER_SCRIPTED } } } else { contents.forEach(it => container.children.push(it)) } } } sortTriggerContent(result as unknown as OrderedTriggerContainer) return result }, explodeTriggersJsonIntoSource: async function (output: string, triggersJson: TriggerContainer): Promise<void> { const sourceOutput = path.join(output, EnhancementManager.sourceFolder) log.info('Exploding triggers.json into a source code tree at', sourceOutput) triggersJson.name = '' // Delete header name const tasks: Array<Promise<unknown>> = [] for (const [parents, content] of TreeIterator<TriggerContent>(triggersJson, GetTriggerContainerChildren)) { const outPath = path.join(sourceOutput, ...parents.map(it => it.name)) const exportObj: Record<string, unknown> = { contentType: content.contentType } content.name = safeReplaceTriggerName(content.name) switch (content.contentType) { case ContentType.HEADER: exportObj.description = (content as MapHeader).description tasks.push(WriteAndCreatePath(path.join(outPath, `${EnhancementManager.mapHeaderFilename}${EnhancementManager.scriptExtension}`), (content as ScriptContent).script, 'utf8')) exportObj.isExpanded = (content as TriggerContainer).isExpanded exportObj.order = generateTriggerOrder(content as TriggerContainer) tasks.push(WriteAndCreatePath(path.join(outPath, `${EnhancementManager.mapHeaderFilename}${EnhancementManager.containerInfoExtension}`), ini.encode(exportObj), 'utf8')) break case ContentType.LIBRARY: case ContentType.CATEGORY: exportObj.isExpanded = (content as TriggerContainer).isExpanded exportObj.order = generateTriggerOrder(content as TriggerContainer) tasks.push(WriteAndCreatePath(path.join(outPath, content.name, `${content.name}${EnhancementManager.containerInfoExtension}`), ini.encode(exportObj), 'utf8')) break case ContentType.TRIGGER: exportObj.runOnMapInit = (content as GUITrigger).runOnMapInit exportObj.initiallyOff = (content as GUITrigger).initiallyOff exportObj.isEnabled = (content as GUITrigger).isEnabled exportObj.description = (content as GUITrigger).description exportObj.events = (content as GUITrigger).events exportObj.conditions = (content as GUITrigger).conditions exportObj.actions = (content as GUITrigger).actions tasks.push(WriteAndCreatePath(path.join(outPath, `${content.name}${EnhancementManager.guiExtension}`), FormatConverters[EnhancementManager.guiExtension].stringify(content), 'utf8')) break case ContentType.TRIGGER_SCRIPTED: tasks.push(WriteAndCreatePath(path.join(outPath, `${content.name}${EnhancementManager.scriptExtension}`), (content as ScriptContent).script, 'utf8')); (content as ScriptedTrigger).script = '' tasks.push(WriteAndCreatePath(path.join(outPath, `${content.name}${EnhancementManager.guiExtension}`), FormatConverters[EnhancementManager.guiExtension].stringify(content), 'utf8')) break case ContentType.COMMENT: tasks.push(WriteAndCreatePath(path.join(outPath, `${content.name}${EnhancementManager.commentExtension}`), (content as TriggerComment).comment, 'utf8')) break case ContentType.CUSTOM_SCRIPT: tasks.push(WriteAndCreatePath(path.join(outPath, `${content.name}${EnhancementManager.scriptExtension}${((content as CustomScript).isEnabled) ? ('') : (EnhancementManager.disabledExtension)}`), (content as ScriptContent).script, 'utf8')) break case ContentType.VARIABLE: exportObj.type = (content as GlobalVariable).type exportObj.isInitialized = (content as GlobalVariable).isInitialized exportObj.initialValue = (content as GlobalVariable).initialValue exportObj.isArray = (content as GlobalVariable).isArray exportObj.arrayLength = (content as GlobalVariable).arrayLength tasks.push(WriteAndCreatePath(path.join(outPath, `${content.name}${EnhancementManager.guiExtension}`), FormatConverters[EnhancementManager.guiExtension].stringify(content), 'utf8')) break } } await Promise.all(tasks) } } export { TriggerComposer }