---@type Tinkr local Tinkr = ... ---@class Bastion local Bastion = { DebugMode = false, } local TinkrScriptsBase = "scripts" local BastionBase = string.format("%s/%s", TinkrScriptsBase, "bastion") local BastionScriptsBase = string.format("%s/%s", BastionBase, "scripts") local ThirdPartyModulesBase = string.format("%s/%s", TinkrScriptsBase, "BastionScripts") Bastion.__index = Bastion ---@class Bastion.LoadedFiles.Table ---@field [number] { originalPath: string, loadedPath: string, reloadable: boolean, order: number, newPath: string } Bastion.LoadedFiles = {} ---@param filePath string function Bastion:CheckIfLoaded(filePath) for i, file in ipairs(Bastion.LoadedFiles) do if file.loadedPath == filePath or file.originalPath == filePath or file.newPath == filePath then return true end end return false end ---@param path string ---@param extension string ---@return string local function AppendExtension(path, extension) return string.format("%s.%s", path, extension) end ---@param path string ---@param extensions string|string[] local function CheckFileExtensions(path, extensions) local exts = {} if type(extensions) == "string" then exts = { extensions } else exts = extensions end for i, extension in ipairs(exts) do local newPath = path if newPath:sub(extension:len() * -1) ~= extension then newPath = AppendExtension(newPath, extension) end if FileExists(newPath) then return newPath:sub(1, (extension:len() + 2) * -1), true end end return path, false end --- 0 = Failed, 1 = Success, 2 = Already Loaded ---@param filePath string | { filePath: string, reloadable: boolean } ---@param ... any ---@return 0|1|2, ... function Bastion:Require(filePath, ...) local loadedFile = { originalPath = type(filePath) == "table" and filePath.filePath or tostring(filePath), newPath = type(filePath) == "table" and filePath.filePath or tostring(filePath), loadedPath = type(filePath) == "table" and filePath.filePath or tostring(filePath), reloadable = type(filePath) == "table" and filePath.reloadable or false, order = #Bastion.LoadedFiles + 1, } local filePathModifier = loadedFile.originalPath:sub(1, 1) if filePathModifier == "@" then -- If require starts with an @ then we require from the scripts/bastion/scripts folder loadedFile.newPath = string.format("%s%s", BastionScriptsBase, loadedFile.originalPath:sub(2)) loadedFile.loadedPath = loadedFile.newPath elseif filePathModifier == "~" then -- If file path starts with a ~ then we require from the scripts/bastion folder loadedFile.newPath = string.format("%s%s", BastionBase, loadedFile.originalPath:sub(2)) loadedFile.loadedPath = loadedFile.newPath end loadedFile.loadedPath = loadedFile.newPath local found = false -- Check if file path has a .lua or .luac extension. If not, try to add one and check if the file exists loadedFile.loadedPath, found = CheckFileExtensions(loadedFile.newPath, { "lua", "luac" }) if not found then Log(string.format("Bastion:Require - Not Found: %s (%s)", loadedFile.newPath, loadedFile.originalPath)) return 0, SafePack(nil) end if not loadedFile.reloadable then if Bastion:CheckIfLoaded(loadedFile.loadedPath) then --Log(string.format("Bastion:Require - Already loaded: %s (%s)", loadedFile.newPath, loadedFile.originalPath)) return 2, SafePack(nil) end end table.insert(Bastion.LoadedFiles, loadedFile) return 1, SafePack(require(loadedFile.loadedPath, Bastion, ...)) end local loadExamples = false local exampleNames = { "ExampleDependency.lua", "ExampleDependencyError.lua", "ExampleLibrary.lua", "ExampleModule.lua", } local function Load(dir) local dir = dir if dir:sub(1, 1) == "@" then dir = dir:sub(2) dir = string.format("%s/%s", BastionScriptsBase, dir) end if dir:sub(1, 1) == "~" then dir = dir:sub(2) dir = string.format("%s/%s", BastionBase, dir) end local files = ListFiles(dir) for i = 1, #files do local file = files[i] local loadFile = true if not loadExamples then for j = 1, #exampleNames do if file:find(exampleNames[j]) then loadFile = false break end end end if loadFile and (file:sub(-4) == ".lua" or file:sub(-5) == ".luac") then Bastion:Require(dir .. file:sub(1, -5)) end end end local function LoadThird() local thirdPartyModulesFolders = ListFolders(ThirdPartyModulesBase) for i = 1, #thirdPartyModulesFolders do local currentFolderDir = string.format("%s/%s", ThirdPartyModulesBase, thirdPartyModulesFolders[i]) local loaderFilePath = string.format("%s/%s", currentFolderDir, "loader") if FileExists(loaderFilePath .. ".lua") or FileExists(loaderFilePath .. ".luac") then Bastion:Require(loaderFilePath, currentFolderDir) end end end ---@generic V : string ---@param class `V` ---@return V ... function Bastion.require(class) ---@cast class string local newClass = class:gsub("Bastion%.", "") -- return require("scripts/bastion/src/" .. class .. "/" .. class, Bastion) local success, result = Bastion:Require("~/src/" .. newClass .. "/" .. newClass) if success == 0 then Log("Bastion.require - Failed to load " .. class .. ": " .. result) end return SafeUnpack(result) end Bastion.Globals = {} Bastion.ClassMagic = Bastion.require("Bastion.ClassMagic") Bastion.List = Bastion.require("Bastion.List") Bastion.Library = Bastion.require("Bastion.Library") Bastion.Notification = Bastion.require("Bastion.Notification") Bastion.NotificationList = Bastion.require("Bastion.NotificationList") Bastion.Vector3 = Bastion.require("Bastion.Vector3") Bastion.Sequencer = Bastion.require("Bastion.Sequencer") Bastion.Command = Bastion.require("Bastion.Command") Bastion.Cache = Bastion.require("Bastion.Cache") Bastion.Cacheable = Bastion.require("Bastion.Cacheable") Bastion.Refreshable = Bastion.require("Bastion.Refreshable") Bastion.Unit = Bastion.require("Bastion.Unit") Bastion.Aura = Bastion.require("Bastion.Aura") Bastion.APLTrait = Bastion.require("Bastion.APLTrait") Bastion.APLActor = Bastion.require("Bastion.APLActor") Bastion.APL = Bastion.require("Bastion.APL") Bastion.Module = Bastion.require("Bastion.Module") Bastion.UnitManager = Bastion.require("Bastion.UnitManager"):New() Bastion.ObjectManager = Bastion.require("Bastion.ObjectManager"):New() Bastion.EventManager = Bastion.require("Bastion.EventManager") Bastion.Globals.EventManager = Bastion.EventManager:New() Bastion.Spell = Bastion.require("Bastion.Spell") Bastion.SpellBook = Bastion.require("Bastion.SpellBook") Bastion.Globals.SpellBook = Bastion.SpellBook:New() Bastion.Item = Bastion.require("Bastion.Item") Bastion.ItemBook = Bastion.require("Bastion.ItemBook") Bastion.Globals.ItemBook = Bastion.ItemBook:New() Bastion.AuraTable = Bastion.require("Bastion.AuraTable") Bastion.Class = Bastion.require("Bastion.Class") Bastion.Timer = Bastion.require("Bastion.Timer") Bastion.CombatTimer = Bastion.Timer:New("combat") Bastion.MythicPlusUtils = Bastion.require("Bastion.MythicPlusUtils"):New() Bastion.Notifications = Bastion.NotificationList:New() Bastion.Config = Bastion.require("Bastion.Config") Bastion.TimeToDie = Bastion.require("Bastion.TimeToDie") ---@enum (key) CompareThisTable local compareThisTable = { [">"] = function(A, B) return A > B end, ["<"] = function(A, B) return A < B end, [">="] = function(A, B) return A >= B end, ["<="] = function(A, B) return A <= B end, ["=="] = function(A, B) return A == B end, ["min"] = function(A, B) return A < B end, ["max"] = function(A, B) return A > B end, } Bastion.Utils = { ---@generic A ---@param operator CompareThisTable ---@param a A ---@param b A CompareThis = function(operator, a, b) return compareThisTable[operator](a, b) end } local LIBRARIES = {} ---@type Bastion.Module[] local MODULES = {} Bastion.Enabled = false Bastion.Globals.EventManager:RegisterWoWEvent("UNIT_AURA", function(unit, auras) ---@type Bastion.Unit | nil local u = Bastion.UnitManager:Get(unit) if u then u:GetAuras():OnUpdate(auras) end end) Bastion.Globals.EventManager:RegisterWoWEvent("UNIT_SPELLCAST_SUCCEEDED", function(...) local unit, castGUID, spellID = ... local spell = Bastion.Globals.SpellBook:GetIfRegistered(spellID) if unit == "player" and spell then spell.lastCastAt = GetTime() if spell:GetPostCastFunction() then spell:GetPostCastFunction()(spell) end end end) local pguid = UnitGUID("player") local missed = {} ---@class Bastion.Globals.SpellName : { [spellId]: string } Bastion.Globals.SpellName = {} Bastion.Globals.EventManager:RegisterWoWEvent("COMBAT_LOG_EVENT_UNFILTERED", function() local args = { CombatLogGetCurrentEventInfo() } ---@type string local subEvent = args[2] ---@type string local sourceGUID = args[4] ---@type string local destGUID = args[8] ---@type number local spellID = args[12] ---@type string local spellName = args[13] if subEvent:find("SPELL") == 1 or subEvent:find("RANGE") == 1 then if (not Bastion.Globals.SpellName[spellID] or Bastion.Globals.SpellName[spellID] ~= spellName) then Bastion.Globals.SpellName[spellID] = spellName end end -- if sourceGUID == pguid then -- local args = { CombatLogGetCurrentEventInfo() } -- for i = 1, #args do -- Log(tostring(args[i])) -- end -- end --Bastion.UnitManager:SetCombatTime(sourceGUID) --Bastion.UnitManager:SetCombatTime(destGUID) local u = Bastion.UnitManager[sourceGUID] local u2 = Bastion.UnitManager[destGUID] local t = GetTime() if u then u:SetLastCombatTime(t) end if u2 then u2:SetLastCombatTime(t) if subEvent == "SPELL_MISSED" and sourceGUID == pguid and spellID == 408 then local missType = args[15] if missType == "IMMUNE" then local castingSpell = u:GetCastingOrChannelingSpell() if castingSpell then if not missed[castingSpell:GetID()] then missed[castingSpell:GetID()] = true end end end end end end) local Timer = { TTD = 0 } Bastion.Ticker = C_Timer.NewTicker(0.1, function() if not Bastion.CombatTimer:IsRunning() and UnitAffectingCombat("player") then Bastion.CombatTimer:Start() elseif Bastion.CombatTimer:IsRunning() and not UnitAffectingCombat("player") then Bastion.CombatTimer:Reset() end if Bastion.Enabled then Bastion.ObjectManager:Refresh() if GetTime() > Timer.TTD then Timer.TTD = GetTime() + Bastion.TimeToDie.Settings.Refresh Bastion.TimeToDie:Refresh() end for i = 1, #MODULES do MODULES[i]:Tick() end end end) function Bastion:Register(module) table.insert(MODULES, module) Bastion:Print("Registered", module) end -- Find a module by name function Bastion:FindModule(name) for i = 1, #MODULES do if MODULES[i].name == name then return MODULES[i] end end return nil end Bastion.PrintEnabled = false function Bastion:Print(...) if not Bastion.PrintEnabled then return end local args = { ... } local str = "|cFFDF362D[Bastion]|r |cFFFFFFFF" for i = 1, #args do str = str .. tostring(args[i]) .. " " end print(str) end function Bastion:Debug(...) if not Bastion.DebugMode then return end local args = { ... } local str = "|cFFDF6520[Bastion]|r |cFFFFFFFF" for i = 1, #args do str = str .. tostring(args[i]) .. " " end print(str) end local Command = Bastion.Command:New("bastion") Command:Register("toggle", "Toggle bastion on/off", function() Bastion.Enabled = not Bastion.Enabled if Bastion.Enabled then Bastion:Print("Enabled") else Bastion:Print("Disabled") end end) Command:Register("debug", "Toggle debug mode on/off", function() Bastion.DebugMode = not Bastion.DebugMode if Bastion.DebugMode then Bastion:Print("Debug mode enabled") else Bastion:Print("Debug mode disabled") end end) Command:Register("dumpspells", "Dump spells to a file", function() local i = 1 local rand = math.random(100000, 999999) while true do local spellName, spellSubName = GetSpellBookItemName(i, BOOKTYPE_SPELL) if not spellName then do break end end -- use spellName and spellSubName here local spellID = select(7, GetSpellInfo(spellName)) if spellID then spellName = spellName:gsub("[%W%s]", "") WriteFile("bastion-" .. UnitClass("player") .. "-" .. rand .. ".lua", "local " .. spellName .. " = Bastion.Globals.SpellBook:GetSpell(" .. spellID .. ")\n", true) end i = i + 1 end end) Command:Register("module", "Toggle a module on/off", function(args) local module = Bastion:FindModule(args[2]) if module then module:Toggle() if module.enabled then Bastion:Print("Enabled", module.name) else Bastion:Print("Disabled", module.name) end else Bastion:Print("Module not found") end end) Command:Register("mplus", "Toggle m+ module on/off", function(args) local cmd = args[2] if cmd == "debuffs" then Bastion.MythicPlusUtils:ToggleDebuffLogging() Bastion:Print("Debuff logging", Bastion.MythicPlusUtils.debuffLogging and "enabled" or "disabled") return end if cmd == "casts" then Bastion.MythicPlusUtils:ToggleCastLogging() Bastion:Print("Cast logging", Bastion.MythicPlusUtils.castLogging and "enabled" or "disabled") return end Bastion:Print("[MythicPlusUtils] Unknown command") Bastion:Print("Available commands:") Bastion:Print("debuffs") Bastion:Print("casts") end) Command:Register("missed", "Dump the list of immune kidney shot spells", function() for k, v in pairs(missed) do Bastion:Print(k) end end) ---@param library Bastion.Library function Bastion:RegisterLibrary(library) LIBRARIES[library.name] = library end function Bastion:CheckLibraryDependencies() for k, v in pairs(LIBRARIES) do if v.dependencies then for i = 1, #v.dependencies do local dep = v.dependencies[i] if LIBRARIES[dep] then if LIBRARIES[dep].dependencies then for j = 1, #LIBRARIES[dep].dependencies do if LIBRARIES[dep].dependencies[j] == v.name then Bastion:Print("Circular dependency detected between " .. v.name .. " and " .. dep) return false end end end else Bastion:Print("Library " .. v.name .. " depends on " .. dep .. " but it's not registered") return false end end end end return true end function Bastion:Import(library) local lib = self:GetLibrary(library) if not lib then error("Library " .. library .. " not found") end return lib:Resolve() end function Bastion:GetLibrary(name) if not LIBRARIES[name] then error("Library " .. name .. " not found") end local library = LIBRARIES[name] -- if library.dependencies then -- for i = 1, #library.dependencies do -- local dep = library.dependencies[i] -- if LIBRARIES[dep] then -- if LIBRARIES[dep].dependencies then -- for j = 1, #LIBRARIES[dep].dependencies do -- if LIBRARIES[dep].dependencies[j] == library.name then -- Bastion:Print("Circular dependency detected between " .. library.name .. " and " .. dep) -- return false -- end -- end -- end -- else -- Bastion:Print("Library " .. v.name .. " depends on " .. dep .. " but it's not registered") -- return false -- end -- end -- end return library end -- if not Bastion:CheckLibraryDependencies() then -- return -- end Load("@Libraries/") Load("@Modules/") Load("@") LoadThird()