Bastion aims to serve as a highly performant, simplisitic, and expandable World of Warcraft data visualization framework.
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
Bastion/src/_bastion.lua

543 lines
17 KiB

---@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()