From 5731f39ca21dab59406d227f3d5c697a6e99b958 Mon Sep 17 00:00:00 2001 From: Ben Carter Date: Mon, 5 Feb 2024 23:43:45 -0500 Subject: [PATCH] updated plugin code for issues with second require in same code --- modules/classes/ui-utils.ts | 1 + modules/npcs/npcbot.ts | 75 ++++++++++-- package-lock.json | 88 ++++++++------ package.json | 10 +- plugins/aio-plugin.ts | 224 +++++++++++++++++++++++++++++++++++- tsconfig.json | 4 +- 6 files changed, 349 insertions(+), 53 deletions(-) diff --git a/modules/classes/ui-utils.ts b/modules/classes/ui-utils.ts index 503d73c..982741a 100644 --- a/modules/classes/ui-utils.ts +++ b/modules/classes/ui-utils.ts @@ -7,6 +7,7 @@ export function colors(name: string) { PURPLE: "|cff9F3FFF", BLUE: "|cff0070dd", ORANGE: "|cffFF8400", + YELLOW: "|cffFFFF00", }; const keyName = name.toUpperCase(); diff --git a/modules/npcs/npcbot.ts b/modules/npcs/npcbot.ts index acad58b..4241e98 100644 --- a/modules/npcs/npcbot.ts +++ b/modules/npcs/npcbot.ts @@ -1,3 +1,5 @@ +import { BotStat, BotEquipSlot } from '../constants/idmaps'; + const npcBotEmote: creature_event_on_died = (event: number, creature: Creature, player: Player) => { // player.KillPlayer(); @@ -29,26 +31,83 @@ const enterCombat: creature_event_on_enter_combat = (event: number, creature: Cr const playerEmote: player_event_on_text_emote = (event: number, player: Player, textEmote: number, emoteNum: number, guid: number) => { + print('Emote: ' + textEmote); + print('EmoteNum: ' + emoteNum); + const unit = player.GetSelection(); if(!unit) { return false; } -print(unit.GetTypeId()); if(unit.GetTypeId() == TypeID.TYPEID_UNIT) { const creature = unit.ToCreature(); - print(creature.GetName()); - print(creature.IsNPCBot()); + print(`BotName ${creature.GetName()}`); + + if(creature.IsNPCBot()) { + const owner = creature.GetOwner(); + print(owner); + if(owner !== undefined) { + print(`Owner: ${owner.GetName()}`); + print(`Bot Gear Item Level: ${creature.GetBotAverageItemLevel()}`); + print(`Bot Roles ${creature.GetBotRoles()}`); + print(`IsBotTank: ${creature.IsBotTank()}`); + print(`IsBotOffTank ${creature.IsBotOffTank()}`); + } + + print(`Generic Info ------------`); + const botclass = creature.GetClass(); + print(`Bot Class: ${botclass}`); + print(`Bot Str ${creature.GetBotStat(4)}`); + print(`Is Free Bot: ${creature.IsFreeBot()}`); + + } + } } const playerChat: player_event_on_chat = (event: number, player: Player, message: string, type: number, lang: number) => { - - const target = player.GetVictim(); - print(target); + + const unit = player.GetSelection(); + if(unit) { + const creature = unit.ToCreature(); + if(creature.IsNPCBot()) { - return 'hello'; + let [action, item] = message.split(" "); + if(action === 'botequip') { + if(item) { + const itemEntry = parseInt(item); + if(itemEntry < 1) { + player.SendBroadcastMessage('Invalid item entry'); + return false; + } + + if(!player.HasItem(itemEntry, 1)) { + player.SendBroadcastMessage('You do not have that item'); + return false; + } + + if(itemEntry) { + creature.BotEquipItem(itemEntry, BotEquipSlot.MAINHAND); + } + } else { + const mainhand = creature.GetBotEquipment(BotEquipSlot.MAINHAND); + + if(mainhand) { + print(`Mainhand: ${mainhand.GetName()}`); + creature.SendChatMessageToPlayer(ChatMsg.CHAT_MSG_SAY, Language.LANG_COMMON, `Mainhand: ${mainhand.GetItemLink()}`, player); + } + print(`Haste Rating: ${creature.GetBotStat(BotStat.HASTE_RATING)}`); + + } + } + + } + + + } + return ''; + } RegisterCreatureEvent( @@ -80,5 +139,3 @@ RegisterPlayerEvent(PlayerEvents.PLAYER_EVENT_ON_TEXT_EMOTE, (...args) => player RegisterPlayerEvent(PlayerEvents.PLAYER_EVENT_ON_CHAT, (...args) => playerChat(...args)); -PrintError('NPC Bot loaded!'); - diff --git a/package-lock.json b/package-lock.json index 5970008..1ea4cd4 100644 --- a/package-lock.json +++ b/package-lock.json @@ -5,21 +5,25 @@ "packages": { "": { "dependencies": { + "@types/fs-extra": "^11.0.4", + "@types/node": "^20.11.15", + "D": "^1.0.0", + "fs-extra": "^11.2.0", "ts-node": "^10.9.1", - "wow-eluna-ts-module": "^1.6.7" + "wow-eluna-ts-module": "^1.8.0" }, "devDependencies": { "cross-var": "^1.1.0", "dotenv-cli": "^7.3.0", - "ncp": "^2.0.0", "rimraf": "^5.0.5", - "typescript": "^5.2.2" + "typescript": "^5.2.2", + "typescript-to-lua": "^1.23.0" } }, "node_modules/@araxiaonline/wow-wotlk-declarations": { - "version": "3.3.5-3", - "resolved": "https://registry.npmjs.org/@araxiaonline/wow-wotlk-declarations/-/wow-wotlk-declarations-3.3.5-3.tgz", - "integrity": "sha512-iZ85TJWDgWwqMf7dWB/x42bwTorqGdHqxBhkySyUbp/zhe5oq/AKok9RG5+FTsRNFhuWiWR0z2Aoi9yK2QjARQ==" + "version": "3.3.5-4", + "resolved": "https://registry.npmjs.org/@araxiaonline/wow-wotlk-declarations/-/wow-wotlk-declarations-3.3.5-4.tgz", + "integrity": "sha512-GmlOaW5QGntrcWSDBNON8934epaZqDjFZ93damZB++cATPV50jKvVThD1Wp4GKo9++Vh20nYYB8Pdr9VjNUO1A==" }, "node_modules/@cspotcode/source-map-support": { "version": "0.8.1", @@ -101,13 +105,29 @@ "resolved": "https://registry.npmjs.org/@tsconfig/node16/-/node16-1.0.4.tgz", "integrity": "sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA==" }, - "node_modules/@types/node": { - "version": "20.8.6", - "resolved": "https://registry.npmjs.org/@types/node/-/node-20.8.6.tgz", - "integrity": "sha512-eWO4K2Ji70QzKUqRy6oyJWUeB7+g2cRagT3T/nxYibYcT4y2BDL8lqolRXjTHmkZCdJfIPaY73KbJAZmcryxTQ==", - "peer": true, + "node_modules/@types/fs-extra": { + "version": "11.0.4", + "resolved": "https://registry.npmjs.org/@types/fs-extra/-/fs-extra-11.0.4.tgz", + "integrity": "sha512-yTbItCNreRooED33qjunPthRcSjERP1r4MqCZc7wv0u2sUkzTFp45tgUfS5+r7FrZPdmCCNflLhVSP/o+SemsQ==", "dependencies": { - "undici-types": "~5.25.1" + "@types/jsonfile": "*", + "@types/node": "*" + } + }, + "node_modules/@types/jsonfile": { + "version": "6.1.4", + "resolved": "https://registry.npmjs.org/@types/jsonfile/-/jsonfile-6.1.4.tgz", + "integrity": "sha512-D5qGUYwjvnNNextdU59/+fI+spnwtTFmyQP0h+PfIOSkNfpU6AOICUOkm4i0OnSk+NyjdPJrxCDro0sJsWlRpQ==", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/node": { + "version": "20.11.15", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.11.15.tgz", + "integrity": "sha512-gscmuADZfvNULx1eyirVbr3kVOVZtpQtzKMCZpeSZcN6MfbkRXAR4s9/gsQ4CzxLHw6EStDtKLNtSDL3vbq05A==", + "dependencies": { + "undici-types": "~5.26.4" } }, "node_modules/@typescript-to-lua/language-extensions": { @@ -1259,6 +1279,12 @@ "which": "bin/which" } }, + "node_modules/D": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/D/-/D-1.0.0.tgz", + "integrity": "sha512-nQvrCBu7K2pSSEtIM0EEF03FVjcczCXInMt3moLNFbjlWx6bZrX72uT6/1uAXDbnzGUAx9gTyDiQ+vrFi663oA==", + "deprecated": "Package no longer supported. Contact support@npmjs.com for more info." + }, "node_modules/debug": { "version": "2.6.9", "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", @@ -1714,15 +1740,6 @@ "integrity": "sha512-W7tfG7vMOGtD30sHoZSSc/JVYiyDPEyQVso/Zz+/uQd0B0L46gtC+pHha5FFMRpil6fm/AoEcRWyOVi4+E/f8w==", "optional": true }, - "node_modules/ncp": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ncp/-/ncp-2.0.0.tgz", - "integrity": "sha512-zIdGUrPRFTUELUvr3Gmc7KZ2Sw/h1PiVM0Af/oHB6zgnV1ikqSfRk+TOufi79aHYCW3NiOXmr1BP5nWbzojLaA==", - "dev": true, - "bin": { - "ncp": "bin/ncp" - } - }, "node_modules/node-cleanup": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/node-cleanup/-/node-cleanup-2.1.2.tgz", @@ -2277,9 +2294,9 @@ "integrity": "sha512-KXXFFdAbFXY4geFIwoyNK+f5Z1b7swfXABfL7HXCmoIWMKU3dmS26672A4EeQtDzLKy7SXmfBu51JolvEKwtGA==" }, "node_modules/typescript": { - "version": "5.2.2", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.2.2.tgz", - "integrity": "sha512-mI4WrpHsbCIcwT9cF4FZvr80QUeKvsUsUvKDoR+X/7XHQH98xYD8YHZg7ANtz2GtZt/CBq2QJ0thkGJMHfqc1w==", + "version": "5.3.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.3.3.tgz", + "integrity": "sha512-pXWcraxM0uxAS+tN0AG/BF2TyqmHO014Z070UsJ+pFvYuRSq8KH8DmWpnbXe0pEPDHXZV3FcAbJkijJ5oNEnWw==", "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -2289,9 +2306,9 @@ } }, "node_modules/typescript-to-lua": { - "version": "1.22.0", - "resolved": "https://registry.npmjs.org/typescript-to-lua/-/typescript-to-lua-1.22.0.tgz", - "integrity": "sha512-6rMbWj/NsNQ/phNFVtMZj5LdHucUPdrmKP4VFsynM7SGtm2zR2RMLnZcJM3unrDb1VCAR6FnL7GJoKp77yM5qw==", + "version": "1.23.0", + "resolved": "https://registry.npmjs.org/typescript-to-lua/-/typescript-to-lua-1.23.0.tgz", + "integrity": "sha512-DZ6RWdtT058I2TtXl2odQxyPSdIg0+gGGwNG7zof0mxfFb55x2uRzzve7hfJ/yElP2osTSU6GAwt0zVkPmIt2w==", "dependencies": { "@typescript-to-lua/language-extensions": "1.19.0", "enhanced-resolve": "^5.8.2", @@ -2306,14 +2323,13 @@ "node": ">=16.10.0" }, "peerDependencies": { - "typescript": "5.2.2" + "typescript": "5.3.3" } }, "node_modules/undici-types": { - "version": "5.25.3", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.25.3.tgz", - "integrity": "sha512-Ga1jfYwRn7+cP9v8auvEXN1rX3sWqlayd4HP7OKk4mZWylEmu3KzXDUGrQUN6Ol7qo1gPvB2e5gX6udnyEPgdA==", - "peer": true + "version": "5.26.5", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz", + "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==" }, "node_modules/universalify": { "version": "2.0.1", @@ -2343,11 +2359,11 @@ } }, "node_modules/wow-eluna-ts-module": { - "version": "1.6.7", - "resolved": "https://registry.npmjs.org/wow-eluna-ts-module/-/wow-eluna-ts-module-1.6.7.tgz", - "integrity": "sha512-CK59WXHRKB54xb6FR5zS+TUTM4Tv5QMWG05CPRCWdY4QX9XtxXPek3uXlOlpx1WvbwNl3VUSOeb5PqTg7h4QxA==", + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/wow-eluna-ts-module/-/wow-eluna-ts-module-1.8.0.tgz", + "integrity": "sha512-7Tx9s7myt5F7amxcisQsoZ/T5rvrDb+LEE5PSENSoiKF+FpQo3A6n5CY/Ka6Gr0vL4oCBib87SraSC+b6qQEXw==", "dependencies": { - "@araxiaonline/wow-wotlk-declarations": "^3.3.5-3", + "@araxiaonline/wow-wotlk-declarations": "^3.3.5-4", "@typescript-to-lua/language-extensions": "^1.19.0", "commander": "^7.2.0", "dotenv": "^9.0.2", diff --git a/package.json b/package.json index c52e14e..f47b0d2 100644 --- a/package.json +++ b/package.json @@ -10,12 +10,16 @@ "devDependencies": { "cross-var": "^1.1.0", "dotenv-cli": "^7.3.0", - "ncp": "^2.0.0", "rimraf": "^5.0.5", - "typescript": "^5.2.2" + "typescript": "^5.2.2", + "typescript-to-lua": "^1.23.0" }, "dependencies": { + "@types/fs-extra": "^11.0.4", + "@types/node": "^20.11.15", + "D": "^1.0.0", + "fs-extra": "^11.2.0", "ts-node": "^10.9.1", - "wow-eluna-ts-module": "^1.6.7" + "wow-eluna-ts-module": "^1.8.0" } } diff --git a/plugins/aio-plugin.ts b/plugins/aio-plugin.ts index 949da47..0e6ee0c 100644 --- a/plugins/aio-plugin.ts +++ b/plugins/aio-plugin.ts @@ -1,12 +1,205 @@ import * as ts from "typescript"; import * as tstl from "typescript-to-lua"; +import * as path from "node:path"; +import { readFileSync, existsSync, ensureDirSync, mkdtempSync } from "fs-extra"; +import * as os from "node:os"; + +require('dotenv').config({ + path: 'ets.env' +}); + +type RequiredDefintion = { + module: string, + variable: string +} + +const requires: Map = new Map(); +const resolvedModules: string[] = []; + +/** + * Identifies the require symbol in code and parses into module and path. + */ +function requireSymbol(code: string): string[] | null { + let matcher = code.match(/____(.+) = require\(\"(.+)\"\)/); + if(matcher) { + return new Array(matcher[1], matcher[2]); + } else { + return null; + } +} + +/** + * This creates a consistent key for use in the source map that can identify values + * in both beforeEmit and afterPrint hooks + */ +function keyifyFile(file: string, program: ts.Program): string { + const newpath = file.replace(tstl.getProjectRoot(program), '').replace(path.normalize('/dist'), ''); + return newpath.replace(/\.ts|\.lua/g, ''); +} + +/** + * Builds a source map of modules to their code. + */ +function buildSourceMap(files: tstl.EmitFile[], program: ts.Program) { + const sourceMap: Map = new Map(); + for(const file of files) { + let sourceKey = file.outputPath.replace(program.getCompilerOptions().outDir + '/' ?? '', ''); + sourceKey = sourceKey.replace('.lua', ''); + sourceMap.set(sourceKey, file.code); + } + return sourceMap; +} + +/** + * This recursively (I hope) resolves module names to their respective code in the sourceMap + * that is built from files during the before Emit process. + * It only resolves modules once to avoid collistions in AddOn Namespace. + */ +function resolveRequire(modulepath: string, sourceMap: Map, code?: string): string { + + + // de-lua-ify the module name to a path. + let filepath = modulepath.replace(/\./g, "/"); + let output: string = code || ''; + + // skip resolved modules only want to include them once to avoid namespace issues. + if(resolvedModules.includes(modulepath)) { + console.log('skipping already resolved module: ', modulepath); + return ''; + } + + // have to check if the file we are resolving also has requires that need to be resolved. + let filecode = sourceMap.get(filepath) ?? ''; + + if(requireSymbol(filecode)) { + + for(const line of filecode.split("\n")) { + if(line.includes("lualib")) { + filecode = filecode.replace(line+"\n", ''); + continue; + } + const [variable, module] = requireSymbol(line) ?? ["", ""]; + + if(module) { + output += resolveRequire(module, sourceMap, output); + } + } + + } + output += filecode; + resolvedModules.push(modulepath); + + return output; +} +/** + * Removes tstl code that is added expecting the code to be exported only used for resolving module code. + */ +function resolveExports(code: string, name: string) { + return code + .replace(/local ____exports = \{\}/g,'') + .replace(/return ____exports/g,'') + .replace(/____exports/g, `____${name}`); +} + +/** + * The default mechanism for transpiling files is "require" the common Polyfill bundle code. This is actually great + * for server side code, however breaks client side code. So this hook transpiles the client code with "inline" + * setting so any used Polyfills from TS are added inline into the source code removing the require to lualib modules + * tstl puts in. + * + * This also handles building a map of the requires that will later be resolved in the beforeEmit handler. + */ +function afterPrint( + program: ts.Program, + options: tstl.CompilerOptions, + emitHost: tstl.EmitHost, + result: tstl.ProcessedFile[] +) { + for (const file of result) { + const mapKey = keyifyFile(file.fileName, program); + + if (file.fileName.includes(".client.ts")) { + + const sourceCode = readFileSync(file.fileName, "utf-8"); + const tmpPath = path.join(os.tmpdir(), 'ets-compile'); + + const result = tstl.transpileFiles([file.fileName],{ + outDir: tmpPath, + luaLibImport: tstl.LuaLibImportKind.Inline, + luaTarget: tstl.LuaTarget.Lua52, + strict: false, + target: ts.ScriptTarget.ESNext, + skipLibCheck: true, + noHeader: true, + lib: [ 'lib.esnext.d.ts', 'lib.dom.d.ts' ], + types: [ + 'lua-types/5.2', + '@typescript-to-lua/language-extensions', + 'wow-eluna-ts-module', + '@araxiaonline/wow-wotlk-declarations', + 'node' + ], + }); + + result.diagnostics.forEach((d) => { + console.error(`\x1b[31mTRANSPILE ERROR: ${d.messageText}\x1b[0m`); + }); + + const luaPath = file.fileName.replace(tstl.getSourceDir(program), '').replace('.ts', '.lua'); + const fallback = path.join(tmpPath, path.basename(file.fileName.replace('.ts', '.lua'))); + + let transpiled:string | null = null; + if(existsSync(path.join(tmpPath,luaPath))) { + transpiled = readFileSync(path.join(tmpPath,luaPath), 'utf-8'); + } else if(existsSync(fallback)) { + transpiled = readFileSync(fallback, 'utf-8'); + } else { + console.error(`\x1b[31mTRANSPILE ERROR: ${fallback} not found in ${tmpPath}\x1b[0m`); + } + + file.code = transpiled ?? file.code; + + for (const line of file.code.split("\n")) { + const [variable, module] = requireSymbol(line) ?? ["", ""]; + + if (module && module !== "AIO") { + file.code = file.code.replace(line, `local ____${variable} = {}\n-- INLINE(${module})`); + + const currentRequires = requires.get(mapKey) ?? []; + requires.set(mapKey, [...currentRequires, { module, variable }]); + } + } + } + } +} /** * This plugin will add AIO to the transpile process from TS assuming * there is a Global installation of AIO installed in the directory, which is * that follows Rochet2's instructions. + * + * Jan 31, 2024 Update + * The WoW Api Client does not support requires in Lua, instead it uses .toc files / xml which are not + * currently supported in AIO. + * + * When TypescriptToLua handles imports it replaces them with lua require statements. This creates a problem + * in runtime as that method is not present. This plugin now handles that by resolving the AIO client files + * by resolving the code and placing at the top of the transpiled file. This allows imports + * to effectively function as they would in TS land for client code. + * + * Caveats: + * - Your code must use the pattern if(!AIO.AddAddOn()) I realize there is otherways to use this but this is how the code identifies client files currently + * - filenames have to be *client.ts making it reserved now + * - the functions methods are all in the same namespace which could cause conflicts, I believe I handeled them in the background + * but have not done thorough user testing to verify + * - Don't go crazy my methodology here is pretty simple and it is going to work best for sharing common functions / constants / enums etcs that + * are often repeated when building UI components. + * + * @author @ben-of-codecraft + * @since 2024-01-31 */ const plugin: tstl.Plugin = { + afterPrint, beforeEmit( program: ts.Program, options: tstl.CompilerOptions, @@ -14,13 +207,38 @@ const plugin: tstl.Plugin = { result: tstl.EmitFile[], ) { + // build a source map first for resolving requires + const sourceMap = buildSourceMap(result, program); + for (const file of result) { - + const mapKey = keyifyFile(file.outputPath, program); if(file.code.includes("aio = {}")) { - console.log(`installing AIO for this file ${file.outputPath}`); + + // Handle necessary AIO replaces post transpile file.code = file.code.replace("-- @ts-expect-error", ""); file.code = file.code.replace("aio = {}", "local AIO = AIO or require(\"AIO\")"); - file.code = file.code.replace(/aio\./g, "AIO."); + file.code = file.code.replace(/aio[\.\:]/g, "AIO."); + + // Is targetted for AIO Client. + if(file.code.includes("if not AIO.AddAddon() then")) { + + requires.forEach( (requiredModules: RequiredDefintion[], caller) => { + + if(mapKey !== caller) { + return; + } + + requiredModules.forEach((requiredModule) => { + + const moduleCode = resolveExports(resolveRequire(requiredModule.module, sourceMap), requiredModule.variable); + file.code = file.code.replace(`-- INLINE(${requiredModule.module})\n`, `-- INLINE(${requiredModule.module})\n` + moduleCode); + }); + + }); + } + + /* Weirdness with import transpilation "local local"*/ + file.code = file.code.replace("local local AIO = AIO or require(\"AIO\")", "local AIO = AIO or require(\"AIO\")"); } } } diff --git a/tsconfig.json b/tsconfig.json index 8e7e348..bab9ed7 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -14,8 +14,8 @@ "lua-types/5.2", "@typescript-to-lua/language-extensions", "wow-eluna-ts-module", - "@araxiaonline/wow-wotlk-declarations" - + "@araxiaonline/wow-wotlk-declarations", + "node" ], "typeRoots": [ "./node_modules/wow-eluna-ts-module/types",