forked from jeffi/Bastion
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.
543 lines
17 KiB
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()
|
|
|