UNPKG

mdx-m3-viewer

Version:

A browser WebGL model viewer. Mainly focused on models of the games Warcraft 3 and Starcraft 2.

379 lines 16.8 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.convertParameterInline = exports.convertParameter = exports.convertFunctionCall = exports.convertTrigger = void 0; const eca_1 = require("../../../parsers/w3x/wtg/eca"); const parameter_1 = require("../../../parsers/w3x/wtg/parameter"); const subparameters_1 = require("../../../parsers/w3x/wtg/subparameters"); const utils_1 = require("./utils"); /** * A list of vanilla operators that take 3 parameters. * The only one not here, which takes 2 parameters, is OperatorString. */ const OPERATOR_NAMES = new Set([ 'OperatorCompareBoolean', 'OperatorCompareAbilityId', 'OperatorCompareBuffId', 'OperatorCompareDestructible', 'OperatorCompareDestructableCode', 'OperatorCompareButton', 'OperatorCompareGameDifficulty', 'OperatorCompareGameSpeed', 'OperatorCompareHeroSkill', 'OperatorCompareInteger', 'OperatorCompareItem', 'OperatorCompareItemType', 'OperatorCompareItemCode', 'OperatorCompareMouseButton', 'OperatorCompareMeleeDifficulty', 'OperatorCompareOrderCode', 'OperatorComparePlayer', 'OperatorComparePlayerColor', 'OperatorComparePlayerControl', 'OperatorComparePlayerSlotStatus', 'OperatorCompareRace', 'OperatorCompareReal', 'OperatorCompareString', 'OperatorCompareTechCode', 'OperatorCompareTerrainType', 'OperatorCompareTrigger', 'OperatorCompareUnit', 'OperatorCompareUnitCode', 'OperatorInt', 'OperatorReal', ]); /** * A list of vanilla functions which have an implicit code parameter. * The control flow "functions" such as IfThenElseMultiple and ForLoopAMultiple are handled specifically. */ const HAS_IMPLICIT_CODE = new Set([ 'EnumDestructablesInRectAllMultiple', 'EnumDestructablesInCircleBJMultiple', 'EnumItemsInRectBJMultiple', 'ForForceMultiple', 'ForGroupMultiple', ]); /** * Converts a Trigger to a custom script string. * Callbacks that are generated due to the conversion are added to the input callbacks array. */ function convertTrigger(data, trigger, callbacks) { const name = (0, utils_1.ensureNameSafety)(trigger.name); const events = []; const conditions = []; const actions = []; // Separate the events/conditions/actions. for (const eca of trigger.ecas) { if (eca.type === 0) { events.push(eca); } else if (eca.type === 1) { conditions.push(eca); } else { actions.push(eca); } } const functions = []; if (events.length || conditions.length || actions.length) { const initBody = []; const conditionsBody = []; const actionsBody = []; // Reference the global trigger that WE generates. const triggerName = `gg_trg_${name}`; // Events don't explicitly define the trigger parameter. // Therefore it is created here, and prepended below to the parameters of each event. const triggerParameter = new parameter_1.default(); triggerParameter.type = 3; triggerParameter.value = triggerName; initBody.push(`set ${triggerName} = CreateTrigger()`); // Convert the events. for (const event of events) { event.parameters.unshift(triggerParameter); for (const eca of convertFunctionCall(data, event, callbacks)) { initBody.push(eca.parameters[0].value); } } // Convert the conditions. if (conditions.length) { initBody.push(`call TriggerAddCondition(${triggerName}, Condition(function Trig_${name}_Conditions)`); for (const condition of conditions) { for (const eca of convertFunctionCall(data, condition, callbacks)) { conditionsBody.push(`if ${eca.parameters[0].value} then\r\n return true\r\nendif`); } } } // Convert the actions. if (actions.length) { initBody.push(`call TriggerAddAction(${triggerName}, function Trig_${name}_Actions)`); for (const action of actions) { for (const eca of convertFunctionCall(data, action, callbacks)) { actionsBody.push(eca.parameters[0].value); } } } // Add the actions function. if (actionsBody.length) { functions.push(`function Trig_${name}_Actions takes nothing returns nothing\r\n${actionsBody.join('\r\n')}\r\nendfunction`); } // Add the conditions function. if (conditionsBody.length) { functions.push(`function Trig_${name}_Conditions takes nothing returns boolean\r\n${conditionsBody.join('\r\n')}\r\nreturn false\r\nendfunction`); } // Add the initalization function. functions.push(`function InitTrig_${name} takes nothing returns nothing\r\n${initBody.join('\r\n')}\r\nendfunction`); } // Finally, return the whole trigger as Jass. return functions.join('\r\n'); } exports.convertTrigger = convertTrigger; /** * Converts an ECA or SubParameters to an array of custom script ECAs. * Callbacks that are generated due to the conversion are added to the input callbacks array. */ function convertFunctionCall(data, object, callbacks) { const name = object.name; const ecas = []; const parameters = object.parameters; const signature = data.triggerData.getFunction(object.type, object.name); if (!signature) { throw new Error(`Could not find a function signature: ${name}. Stack: ${data.stackToString()}`); } let { args, scriptName } = signature; const argCount = args.length; let isCode = false; let isBoolexpr = false; let isScriptCode = false; const ecaObject = object; // Get correct typing for the cases where the object is known to be an ECA. scriptName = scriptName || object.name; if (argCount) { const lastArg = args[argCount - 1]; if (lastArg === 'code' || HAS_IMPLICIT_CODE.has(name)) { isCode = true; } else if (lastArg === 'boolexpr') { isBoolexpr = true; } else if (lastArg === 'scriptcode') { isScriptCode = true; } } // IfThenElse and other control flow "functions" must come before the generic code/boolexpr callback handling, since they don't follow the same rules. if (name === 'IfThenElse') { ecas.push(`if ${convertParameter(data, parameters[0], args[0], callbacks)} then`); ecas.push(`call ${convertParameter(data, parameters[1], args[1], callbacks)}`); ecas.push('else'); ecas.push(`call ${convertParameter(data, parameters[2], args[2], callbacks)}`); ecas.push('endif'); } else if (name === 'OrMultiple') { ecas.push(ecaObject.ecas.slice().map((eca) => convertFunctionCall(data, eca, callbacks).map((eca) => eca.parameters[0].value)).join(' or ')); } else if (name === 'AndMultiple') { ecas.push(ecaObject.ecas.slice().map((eca) => convertFunctionCall(data, eca, callbacks).map((eca) => eca.parameters[0].value)).join(' and ')); } else if (name === 'ForLoopAMultiple' || name === 'ForLoopBMultiple' || name === 'ForLoopVarMultiple') { let loopName = 'A'; if (name === 'ForLoopBMultiple') { loopName = 'B'; } else if (name === 'ForLoopVarMultiple') { loopName = 'Var'; } let index; if (loopName === 'A' || loopName === 'B') { index = `bj_forLoop${loopName}Index`; const indexEnd = `${index}End`; ecas.push(`set ${index} = ${convertParameter(data, parameters[0], args[0], callbacks)}`); ecas.push(`set ${indexEnd} = ${convertParameter(data, parameters[1], args[1], callbacks)}`); ecas.push('loop'); ecas.push(`exitwhen ${index} > ${indexEnd}`); } else { index = convertParameter(data, parameters[0], args[0], callbacks); ecas.push(`set ${index} = ${convertParameter(data, parameters[1], args[1], callbacks)}`); ecas.push('loop'); ecas.push(`exitwhen ${index} > ${convertParameter(data, parameters[2], args[2], callbacks)}`); } for (const action of ecaObject.ecas) { const replacements = convertFunctionCall(data, action, callbacks); for (const replacement of replacements) { ecas.push(`${replacement.parameters[0].value}`); } } ecas.push(`set ${index} = ${index} + 1`); ecas.push('endloop'); } else if (name === 'IfThenElseMultiple') { let condition; const thenActions = []; const elseActions = []; for (const eca of ecaObject.ecas) { if (eca.group === 0) { condition = eca; } else if (eca.group === 1) { thenActions.push(eca); } else if (eca.group === 2) { elseActions.push(eca); } } if (condition) { ecas.push(`if ${convertFunctionCall(data, condition, callbacks)[0].parameters[0].value} then`); } for (const action of thenActions) { const replacements = convertFunctionCall(data, action, callbacks); for (const replacement of replacements) { ecas.push(`${replacement.parameters[0].value}`); } } if (elseActions.length) { ecas.push('else'); for (const action of elseActions) { const replacements = convertFunctionCall(data, action, callbacks); for (const replacement of replacements) { ecas.push(`${replacement.parameters[0].value}`); } } } ecas.push('endif'); } else if (isCode || isBoolexpr) { const triggerName = data.getTriggerName(); const callbackName = `Trig_${(0, utils_1.ensureNameSafety)(triggerName)}_Func${callbacks.length}`; let call = `function ${callbackName}`; let returnType = 'nothing'; let callOrReturn = 'call'; let lastParam = parameters.length - 1; const isMultiple = object.name.endsWith('Multiple'); if (isBoolexpr) { call = `Filter(${call})`; returnType = 'boolean'; callOrReturn = 'return'; } if (isMultiple) { lastParam = parameters.length; } // The callback names are based on where they are in the callback array. // This breaks when one of the convert functions below need to create more callbacks before actually adding this one to the callbacks array. // To solve this, add a placeholder and save the index, and then use the index after converting everything. const callbackIndex = callbacks.length; callbacks[callbackIndex] = 'NOTHING'; const callParams = [...parameters.slice(0, lastParam).map((value, index) => convertParameter(data, value, args[index], callbacks)), call]; if (object instanceof eca_1.default) { ecas.push(`call ${scriptName}(${callParams.join(', ')})`); } else { ecas.push(`${scriptName}(${callParams.join(', ')})`); } let body; if (isMultiple) { body = ecaObject.ecas.map((eca) => convertFunctionCall(data, eca, callbacks).map((customScript) => customScript.parameters[0].value).join('\r\n')).join('\r\n'); } else { body = `${callOrReturn} ${convertParameter(data, parameters[lastParam], args[lastParam], callbacks)}`; } // Now use the callback index and replace the placeholder. callbacks[callbackIndex] = `function ${callbackName} takes nothing returns ${returnType}\r\n${body}\r\nendfunction`; } else if (isScriptCode) { ecas.push(convertParameter(data, ecaObject.parameters[0], 'scriptcode', callbacks)); } else if (name === 'SetVariable') { ecas.push(`set ${convertParameter(data, parameters[0], args[0], callbacks)} = ${convertParameter(data, parameters[1], args[1], callbacks)}`); } else if (name === 'OperatorString') { // String concat ecas.push(`${convertParameter(data, parameters[0], args[0], callbacks)} + ${convertParameter(data, parameters[1], args[1], callbacks)}`); } else if (OPERATOR_NAMES.has(name)) { // All other operators ecas.push(`${convertParameter(data, parameters[0], args[0], callbacks)} ${convertParameter(data, parameters[1], args[1], callbacks)} ${convertParameter(data, parameters[2], args[2], callbacks)}`); } else if (name === 'CommentString') { // Comment ecas.push(`// ${parameters[0].value}`); } else if (name === 'GetBooleanAnd') { ecas.push(`(${convertParameter(data, parameters[0], args[0], callbacks)} and ${convertParameter(data, parameters[1], args[1], callbacks)})`); } else if (name === 'GetBooleanOr') { ecas.push(`(${convertParameter(data, parameters[0], args[0], callbacks)} or ${convertParameter(data, parameters[1], args[1], callbacks)})`); } else if (object instanceof eca_1.default) { // If this is a trigger event, there is the implicit trigger parameter at the beginning. if (object.type === 0) { args = ['trigger', ...args]; } ecas.push(`call ${scriptName}(${parameters.map((value, index) => convertParameter(data, value, args[index], callbacks)).join(', ')})`); } else if (object instanceof subparameters_1.default) { ecas.push(`${scriptName}(${parameters.map((value, index) => convertParameter(data, value, args[index], callbacks)).join(', ')})`); } return ecas.map((eca) => (0, utils_1.createCustomScriptECA)(eca)); } exports.convertFunctionCall = convertFunctionCall; /** * Converts a parameter to custom script. * Callbacks that are generated due to the conversion are added to the input callbacks array. */ function convertParameter(data, parameter, dataType, callbacks) { const { type, value } = parameter; if (type === 0) { const preset = data.triggerData.getPreset(value); if (preset === undefined) { throw new Error(`Failed to find a preset: "${value}"`); } return preset; } else if (type === 1) { if (value.startsWith('gg_')) { // Used to track global generated variables and their status. data.updateGUIReference(value, false); return value; } else { let global = `udg_${value}`; if (parameter.isArray && parameter.arrayIndex) { global += `[${convertParameter(data, parameter.arrayIndex, 'integer', callbacks)}]`; } return global; } } else if (parameter.type === 2) { return convertFunctionCall(data, parameter.subParameters, callbacks)[0].parameters[0].value; } else if (parameter.type === 3) { const baseType = data.triggerData.getBaseType(dataType); // "value" // scriptcode needs to be converted as-is, and doesn't need quotes. if (baseType === 'string' && dataType !== 'scriptcode') { // Inline string table entries. if (value.startsWith('TRIGSTR') && data.stringTable) { const string = data.stringTable.getString(value); if (string !== undefined) { return `"${string.replace(/\r\n/g, '\\r\n')}"`; } else { data.change('missingstring', 'Entry not found in the string table', value); } } return `"${value.replace(/\\/g, '\\\\')}"`; } // 'value' if (baseType === 'integer' && isNaN(parseInt(value))) { return `'${value}'`; } // value return value; } else { return ''; } } exports.convertParameter = convertParameter; /** * Convert a parameter to a custom script string, discarding any generated callbacks. */ function convertParameterInline(data, parameter, dataType) { return convertParameter(data, parameter, dataType, []); } exports.convertParameterInline = convertParameterInline; //# sourceMappingURL=conversions.js.map