I need some help =(
I am trying to make a Spawnmaster version in Lua.
The goal is to query the zone populate a db with spawns.
then the user can view it in a table and select if they want to ignore or track said mob.
I would like to save the db so subsequent loading of the Lua will have the base already and only have to update information that changes like loc, mobid.
My table is laid out like this.
id INTEGER PRIMARY KEY,
mobname TEXT NOT NULL,
mobzonename TEXT NOT NULL,
mobloc TEXT,
mobid INTEGER DEFAULT 0,
I think my issue is i have no Unique ID that can be used from spawn data. Spawn.ID is only unique to the zone and spawn cycle. Thus the autoincrement id field for primary key.
I found another Lua that was doing sql stuff and heavily used it as a base.
thank you Jackalo for your SoloLootManager Lua. btw that thing sells so much faster than ninjaloot i swear its like 5 items a second.
I can get the mobs to populate the gui, I can not get the toggles to work the debug spits out nil values being passed to the SQL query
so basically I am only working on the in memory database. since it can't make updates and this never get to writing it out.
[CODE lang="Lua" title="SpawnMasterCheck"]-- Reworking SoloLootManager to be a Spawn Checker ~thank you Jackalo~
-- Basically Lua version of MQ2SpawnMaster using Sql Lite.
local SpawnMasterCheck = { _version = '1.0', author = 'Grimmier' }
--- @type Mq
local mq = require('mq')
--- @type ImGui
require('ImGui')
local ImguiHelper = require('mq/ImguiHelper')
-- https://gitlab.com/Knightly1/knightlinc
local Write = require('libraries/Write')
Write.prefix = 'SMC'
Write.loglevel = 'info'
local PackageMan = require('mq/PackageMan')
local lsql = PackageMan.Require('lsqlite3') do
Write.Debug("lsqlite version: %s", lsql.version())
end
local DBPath = string.format('%s\\%s', mq.configDir, 'SpawnChecker.sqlite3')
local Table_Cache = {
Rules = {},
Filtered = {},
Unhandled = {},
Mobs = {},
}
local Lookup = {
Rules = {},
}
local GUI_Main = {
Open = true,
Show = true,
Flags = bit32.bor(ImGuiWindowFlags.NoResize, ImGuiWindowFlags.AlwaysAutoResize),
Action = {
},
}
local GUI_Config = {
Open = false,
Show = false,
--Flags = bit32.bor(0),
Refresh = {
Sort = {
Rules = false,
Unhandled = false,
Mobs = false,
},
Table = {
Rules = false,
Filtered = false,
Unhandled = false,
Mobs = false,
},
},
Search = '',
Table = {
Column_ID = {
ID = 1,
MobName = 2,
MobZoneName = 3,
MobLoc = 4,
MobID = 5,
Action = 6,
Remove = 7,
},
Flags = bit32.bor(
ImGuiTableFlags.Resizable,
ImGuiTableFlags.Sortable,
ImGuiTableFlags.NoSavedSettings,
ImGuiTableFlags.RowBg,
ImGuiTableFlags.BordersV,
ImGuiTableFlags.BordersOuter,
ImGuiTableFlags.SizingStretchProp,
ImGuiTableFlags.ScrollY,
ImGuiTableFlags.Hideable
),
SortSpecs = {
Rules = nil,
Unhandled = nil,
Mobs = nil,
},
},
}
---@class Database
---@field private version integer
---@field private connection any lsqlite3.open() object
local Database = {version = 1} do
---@param dbpath string
---@return Database object
function Database:new(dbpath)
local obj = {}
setmetatable(obj, self)
self.__index = self
self.connection = lsql.open(dbpath)
self.connection:exec('PRAGMA primary_keys=on')
self:Initialize()
self:CheckDatabase()
return obj
end
---@package
function Database:CheckDatabase()
local dbVersion = self:GetConfig('version')
-- If there's a revision, upgrade here in steps to each successive version and recurse this function
if dbVersion == '1' then
Write.Debug("dbVersion is current: v%s", dbVersion)
elseif dbVersion ~= nil then
Write.Fatal("unknown database version: %s", dbVersion)
mq.exit()
end
end
---@package
---Initializes a database with the default tables and values.
function Database:Initialize()
self.connection:exec([[
CREATE TABLE IF NOT EXISTS config(
key TEXT PRIMARY KEY,
value TEXT NOT NULL
);
INSERT INTO config (key, value) VALUES ("version", "1")
ON CONFLICT DO NOTHING;
CREATE TABLE IF NOT EXISTS enum_action(
enum TEXT PRIMARY KEY
);
INSERT INTO enum_action (enum) VALUES ("unhandled")
ON CONFLICT DO NOTHING;
INSERT INTO enum_action (enum) VALUES ("ignore")
ON CONFLICT DO NOTHING;
INSERT INTO enum_action (enum) VALUES ("track")
ON CONFLICT DO NOTHING;
CREATE TABLE IF NOT EXISTS rule(
id INTEGER PRIMARY KEY NOT NULL,
mobname TEXT NOT NULL,
mobzonename TEXT,
mobloc TEXT,
mobid LONG DEFAULT 0,
enum_action TEXT DEFAULT "unhandled",
CONSTRAINT fk_enum_action
FOREIGN KEY (enum_action)
REFERENCES enum_action (enum)
);
]])
end
---@package
---Called before Initialize() to wipe the database first.
function Database:Reinitialize()
self.connection:exec([[
DROP TABLE IF EXISTS config;
DROP TABLE IF EXISTS rule;
DROP TABLE IF EXISTS enum_action;
]])
self:Initialize()
end
---@package
---@param key string
---@param value string
function Database:AddConfig(key, value)
local spc = self.connection:prepare([[INSERT INTO config (key, value) VALUES (?, ?) ON CONFLICT (key) DO UPDATE SET value = excluded.value]])
spc:bind_values(key, value)
spc:step()
spc:finalize()
end
---@package
---@param key string
function Database:RemoveConfig(key)
local spc = self.connection:prepare([[DELETE FROM config WHERE key = ?]])
spc:bind_values(key)
spc:step()
spc:finalize()
end
---@package
---@param key string
---@return string
function Database:GetConfig(key)
local spc = self.connection:prepare([[SELECT value FROM config WHERE key = ?]])
spc:bind_values(key)
for row in spc:nrows() do
spc:finalize()
return row.value
end
end
---@private
function RuleToEntry(rule)
local entry = {
MobName = rule.mobname,
MobZoneName = rule.mobzonename,
MobLoc = rule.mobloc,
MobID = rule.mobID,
Enum_Action = rule.enum_action,
}
return entry
end
---@private
local function EntryToRule(entry)
local rule = {
MobName = entry.mobname,
MobZoneName = entry.mobzonename,
MobLoc = entry.mobloc,
MobID = entry.mobid,
Enum_Action = entry.enum_action,
}
return rule
end
---@package
---@param id any
---@param entry table
function Database:AddRule(id, entry)
local keys, values, conflict
local binds = {}
table.insert(binds, id)
for k,v in pairs(EntryToRule(entry)) do
if #binds > 1 then
keys = keys .. ', ' .. k
values = values .. ', ?'
conflict = conflict .. string.format(', %s = excluded.%s', k, k)
else
keys = k
values = '?'
conflict = string.format('%s = excluded.%s', k, k)
end
table.insert(binds, v)
end
local query = string.format([[INSERT INTO rule (%s) VALUES (%s) ON CONFLICT (id) DO UPDATE SET %s]], keys, values, conflict)
local spc = self.connection:prepare(query)
--DEBUG
-- mq.cmd(string.format('/echo Query: %s ', query))
-- local spc = self.connection:prepare(query)
local spc, err = self.connection:prepare(query)
if not spc then
Write.Debug("Statement preparation failed: %s", err)
return -- handle the error appropriately
end
spc:bind_values(unpack(binds))
spc:step()
spc:finalize()
spc:exec("COMMIT;")
end
---@package
---@param id any
function Database:RemoveRule(id)
local spc = self.connection:prepare([[DELETE FROM rule WHERE id = ?]])
spc:bind_values(id)
spc:step()
spc:finalize()
end
---@package
---@param id string
---@return table
function Database:GetRule(id)
local spc = self.connection:prepare([[SELECT * FROM rule WHERE id = ?]])
spc:bind_values(id)
for row in spc:nrows() do
spc:finalize()
return RuleToEntry(row)
end
end
---@package
---@return table
function Database:GetAllRules()
local spc = self.connection:prepare([[SELECT * FROM rule]])
local newTable = {}
for row in spc:nrows() do
table.insert(newTable, RuleToEntry(row))
end
return newTable
end
end
local DB = Database:new(DBPath)
local function ReinitializeDB()
local title = "SMC > Reinitialize Warning"
local text = "Are you really sure you want to wipe the database?"
if ImguiHelper.Popup.Modal(title, text, { "Yes", "Cancel" }) == 1 then
DB:Reinitialize()
end
end
local function AddRule(entry)
Write.Debug('AddRule [%s] %s', entry.Enum_Action, entry.MobName)
DB:AddRule(entry.MobName, entry)
GUI_Config.Refresh.Table.Rules = true
GUI_Config.Refresh.Table.Filtered = true
GUI_Config.Refresh.Table.Unhandled = true
end
local function RemoveRule(entry)
Write.Debug('RemoveRule [%s] %s', entry.Enum_Action, entry.SpawnLink)
DB:RemoveRule(entry.ID)
GUI_Config.Refresh.Table.Rules = true
GUI_Config.Refresh.Table.Filtered = true
GUI_Config.Refresh.Table.Unhandled = true
end
local function CheckRule(entry)
if Lookup.Rules[entry.MobName] then
return true
else
return false
end
end
local function Compare(entryValue, wantedValue)
if entryValue == wantedValue then
return true
else
return false
end
end
local function SpawnToEntry(spawn)
local entry = {
MobName = spawn.CleanName(),
MobZoneName = mq.TLO.Zone.Name,
MobLoc = spawn.Loc(),
MobID = spawn.ID(), -- If you want to include MobID, keep it here
Enum_Action = 'unhandled',
}
return entry
end
local function InsertTableSpawn(dataTable, spawn, opts)
local entry = SpawnToEntry(spawn)
if opts then
for k,v in pairs(opts) do
entry[k] = v
end
end
table.insert(dataTable, entry)
end
local function TableSortSpecs(a, b)
for i = 1, GUI_Config.Table.SortSpecs.SpecsCount, 1 do
local spec = GUI_Config.Table.SortSpecs:Specs(i)
local delta = 0
if spec.ColumnUserID == GUI_Config.Table.Column_ID.MobName then
if a.mobname < b.mobname then
delta = -1
elseif a.mobname> b.mobname then
delta = 1
end
elseif spec.ColumnUserID == GUI_Config.Table.Column_ID.MobLoc then
if a.mobloc < b.mobloc then
delta = -1
elseif a.mobloc > b.mobloc then
delta = 1
end
elseif spec.ColumnUserID == GUI_Config.Table.Column_ID.MobID then
if a.mobid < b.mobid then
delta = -1
elseif a.mobid > b.mobid then
delta = 1
end
elseif spec.ColumnUserID == GUI_Config.Table.Column_ID.Action then
if a.Enum_Action < b.Enum_Action then
delta = -1
elseif a.Enum_Action > b.Enum_Action then
delta = 1
end
end
if delta ~= 0 then
if spec.SortDirection == ImGuiSortDirection.Ascending then
return delta < 0
else
return delta > 0
end
end
end
return a.Name < b.Name
end
local function RefreshRules()
Table_Cache.Rules = DB:GetAllRules()
local newTable = {}
for k,v in ipairs(Table_Cache.Rules) do
table.insert(newTable, v.ID, k)
end
Lookup.Rules = newTable
GUI_Config.Refresh.Table.Filtered = true
GUI_Config.Refresh.Table.Unhandled = true
GUI_Config.Refresh.Table.Rules = false
end
local function RefreshFiltered()
local splitSearch = {}
for part in string.gmatch(GUI_Config.Search, '[^%s]+') do
table.insert(splitSearch, part)
end
local newTable = {}
for k,v in ipairs(Table_Cache.Rules) do
local found = 0
for _,search in ipairs(splitSearch) do
if string.find(string.lower(v.Name), string.lower(search)) then
found = found + 1
end
end
if #splitSearch == found then
table.insert(newTable, v)
end
end
Table_Cache.Filtered = newTable
GUI_Config.Refresh.Sort.Rules = true
GUI_Config.Refresh.Table.Filtered = false
end
local function RefreshUnhandled()
local newTable = {}
for k,v in ipairs(Table_Cache.Rules) do
if v.Enum_Action == 'unhandled' then
table.insert(newTable, v)
end
end
Table_Cache.Unhandled = newTable
GUI_Config.Refresh.Sort.Unhandled = true
GUI_Config.Refresh.Table.Unhandled = false
end
-- Define a predicate function that checks if the spawn is an NPC
local function RefreshZone()
local newTable = {}
local zoneName = mq.TLO.Zone.Name
local npcs = mq.getFilteredSpawns(function(spawn) return spawn.Type() == 'NPC' end)
mq.cmd('/echo MobCount: Iinit', #npcs)
for i = 1, #npcs do
local spawn = npcs
local entry = SpawnToEntry(spawn)-- Convert spawn data to an entry
--entry.ID = i
entry.MobName = spawn.CleanName
entry.MobZoneName = mq.TLO.Zone.Name
entry.MobLoc = spawn.LocYXZ
entry.MobID = spawn.ID
if entry.MobName then table.insert(newTable, entry) end -- Add the entry to the newTable
--Debug:
-- mq.cmd('/echo ID: ', entry.ID)
end
--debug:
--mq.cmd('/echo MobCount: A', #npcs)
Table_Cache.Mobs = newTable
-- debug:
mq.cmd('/echo TableCount: Mobs', #Table_Cache.Mobs)
---mq.cmd('/echo key: Mobs', Table_Cache.Mobs)
for k, v in pairs(Table_Cache.Mobs) do
--Debug:
-- mq.cmd('/echo ipairs:', k, v)
if not CheckRule(v) then
AddRule(v)
end
end
GUI_Config.Refresh.Sort.Mobs = true
GUI_Config.Refresh.Table.Mobs = false
end
local function DrawRuleRow(entry)
ImGui.TableNextColumn()
ImGui.Text('%s', entry.MobName)
ImGui.TableNextColumn()
ImGui.Text('%s', (entry.MobZoneName))
ImGui.TableNextColumn()
ImGui.Text('%s', (entry.MobLoc))
ImGui.TableNextColumn()
ImGui.Text('%s', (entry.MobID))
ImGui.TableNextColumn()
if ImGui.RadioButton("Ignore", Compare(entry.Enum_Action, 'ignore')) then
entry.Enum_Action = 'ignore'
AddRule(entry)
end
ImGui.SameLine()
if ImGui.RadioButton("Track", Compare(entry.Enum_Action, 'track')) then
entry.Enum_Action = 'track'
AddRule(entry)
end
ImGui.TableNextColumn()
if ImGui.SmallButton("Remove") then RemoveRule(entry) end
end
local function DrawMainGUI()
if GUI_Main.Open then
GUI_Main.Open = ImGui.Begin("Spawn Master Checker", GUI_Main.Open, GUI_Main.Flags)
ImGui.PushStyleVar(ImGuiStyleVar.WindowPadding, 2, 2)
if #Table_Cache.Unhandled > 0 then
ImGui.PushStyleColor(ImGuiCol.Button, ImVec4(1, 0.3, 0.3, 1))
ImGui.PushStyleColor(ImGuiCol.ButtonHovered, ImVec4(1, 0.4, 0.4, 1))
ImGui.PushStyleColor(ImGuiCol.ButtonActive, ImVec4(1, 0.5, 0.5, 1))
end
if ImGui.SmallButton("Config") then GUI_Config.Open = not GUI_Config.Open end
if #Table_Cache.Unhandled > 0 then
ImGui.PopStyleColor(3)
end
ImGui.SameLine()
if ImGui.SmallButton("Refresh Zone") then RefreshZone() end
ImGui.SameLine()
-- if ImGui.SmallButton("Track Spawns") then end
ImGui.PopStyleVar()
ImGui.End()
end
end
local function DrawConfigGUI()
if GUI_Config.Open then
GUI_Config.Open, GUI_Config.Show = ImGui.Begin("SMC > Config", GUI_Config.Open)
if not GUI_Config.Show then
ImGui.End()
return GUI_Config.Open
end
ImGui.PushStyleVar(ImGuiStyleVar.WindowPadding, 2, 2)
if ImGui.BeginTabBar('##TabBar') then
if ImGui.BeginTabItem("All Rules") then
ImGui.PushItemWidth(-95)
local searchText, selected = ImGui.InputText("Search##RulesSearch", GUI_Config.Search)
ImGui.PopItemWidth()
if selected and GUI_Config.Search ~= searchText then
GUI_Config.Search = searchText
GUI_Config.Refresh.Sort.Rules = true
GUI_Config.Refresh.Table.Filtered = true
end
ImGui.SameLine()
if ImGui.Button("Clear##ClearRulesSearch") then
GUI_Config.Search = ''
GUI_Config.Refresh.Sort.Rules = true
GUI_Config.Refresh.Table.Filtered = true
end
ImGui.Separator()
if ImGui.BeginTable('##RulesTable', 6, GUI_Config.Table.Flags) then
ImGui.TableSetupScrollFreeze(0, 1)
ImGui.TableSetupColumn("Name", ImGuiTableColumnFlags.DefaultSort, 8, GUI_Config.Table.Column_ID.MobName)
ImGui.TableSetupColumn("Zone", ImGuiTableColumnFlags.DefaultSort, 8, GUI_Config.Table.Column_ID.MobZoneName)
ImGui.TableSetupColumn("Loc", ImGuiTableColumnFlags.DefaultSort, 8, GUI_Config.Table.Column_ID.MobLoc)
ImGui.TableSetupColumn("MobID", ImGuiTableColumnFlags.DefaultSort, 8, GUI_Config.Table.Column_ID.MobID)
ImGui.TableSetupColumn("Action", ImGuiTableColumnFlags.DefaultSort, 8, GUI_Config.Table.Column_ID.Action)
ImGui.TableSetupColumn("Remove", ImGuiTableColumnFlags.DefaultSort, 2, GUI_Config.Table.Column_ID.Remove)
ImGui.TableHeadersRow()
local sortSpecs = ImGui.TableGetSortSpecs()
if sortSpecs and (sortSpecs.SpecsDirty or GUI_Config.Refresh.Sort.Unhandled) then
if #Table_Cache.Unhandled > 1 then
GUI_Config.Table.SortSpecs = sortSpecs
table.sort(Table_Cache.Unhandled, TableSortSpecs)
GUI_Config.Table.SortSpecs = nil
end
sortSpecs.SpecsDirty = false
GUI_Config.Refresh.Sort.Unhandled = false
end
local clipper = ImGuiListClipper.new()
clipper:Begin(#Table_Cache.Mobs)
while clipper:Step() do
for i = clipper.DisplayStart, clipper.DisplayEnd - 1, 1 do
local entry = Table_Cache.Mobs[i + 1]
ImGui.PushID(entry.ID)
--ImGui.PushID(entry.MobName)
ImGui.TableNextRow()
DrawRuleRow(entry)
ImGui.PopID()
end
end
clipper:End()
ImGui.EndTable()
end
ImGui.EndTabItem()
end
if #Table_Cache.Unhandled > 0 then
ImGui.PushStyleColor(ImGuiCol.Tab, ImVec4(1, 0.3, 0.3, 1))
ImGui.PushStyleColor(ImGuiCol.TabHovered, ImVec4(1, 0.4, 0.4, 1))
ImGui.PushStyleColor(ImGuiCol.TabActive, ImVec4(1, 0.5, 0.5, 1))
end
if ImGui.BeginTabItem("Tracking") then
if ImGui.BeginTable('##UnhandledRulesTable', 4, GUI_Config.Table.Flags) then
ImGui.TableSetupScrollFreeze(0, 1)
ImGui.TableSetupColumn("Name", ImGuiTableColumnFlags.DefaultSort, 8, GUI_Config.Table.Column_ID.MobName)
ImGui.TableSetupColumn("Zone", ImGuiTableColumnFlags.DefaultSort, 8, GUI_Config.Table.Column_ID.MobZoneName)
ImGui.TableSetupColumn("Loc", ImGuiTableColumnFlags.DefaultSort, 8, GUI_Config.Table.Column_ID.MobLoc)
ImGui.TableSetupColumn("MobID", ImGuiTableColumnFlags.DefaultSort, 8, GUI_Config.Table.Column_ID.MobID)
ImGui.TableHeadersRow()
local sortSpecs = ImGui.TableGetSortSpecs()
if sortSpecs and (sortSpecs.SpecsDirty or GUI_Config.Refresh.Sort.Unhandled) then
if #Table_Cache.Unhandled > 1 then
GUI_Config.Table.SortSpecs = sortSpecs
table.sort(Table_Cache.Unhandled, TableSortSpecs)
GUI_Config.Table.SortSpecs = nil
end
sortSpecs.SpecsDirty = false
GUI_Config.Refresh.Sort.Unhandled = false
end
local clipper = ImGuiListClipper.new()
clipper:Begin(#Table_Cache.Unhandled)
while clipper:Step() do
for i = clipper.DisplayStart, clipper.DisplayEnd - 1, 1 do
local entry = Table_Cache.Unhandled[i + 1]
ImGui.PushID(entry.ID)
ImGui.TableNextRow()
DrawRuleRow(entry)
ImGui.PopID()
end
end
clipper:End()
ImGui.EndTable()
end
ImGui.EndTabItem()
end
if #Table_Cache.Unhandled > 0 then
ImGui.PopStyleColor(3)
end
ImGui.EndTabBar()
end
ImGui.PopStyleVar()
ImGui.End()
end
end
local function DrawGUI()
DrawMainGUI()
DrawConfigGUI()
end
-- Kickstart the data
GUI_Config.Refresh.Table.Rules = true
GUI_Config.Refresh.Table.Filtered = true
GUI_Config.Refresh.Table.Unhandled = true
mq.imgui.init('DrawMainGUI', DrawGUI)
mq.bind("/smcconfig", function() GUI_Config.Open = not GUI_Config.Open end)
mq.bind("/smcreinitialize", ReinitializeDB)
mq.bind("/smcquit", function() GUI_Main.Open = not GUI_Main.Open end)
while GUI_Main.Open do
mq.delay(50)
if GUI_Config.Refresh.Table.Rules then RefreshRules() end
if GUI_Config.Refresh.Table.Filtered then RefreshFiltered() end
if GUI_Config.Refresh.Table.Unhandled then RefreshUnhandled() end
end[/CODE]
I am trying to make a Spawnmaster version in Lua.
The goal is to query the zone populate a db with spawns.
then the user can view it in a table and select if they want to ignore or track said mob.
I would like to save the db so subsequent loading of the Lua will have the base already and only have to update information that changes like loc, mobid.
My table is laid out like this.
id INTEGER PRIMARY KEY,
mobname TEXT NOT NULL,
mobzonename TEXT NOT NULL,
mobloc TEXT,
mobid INTEGER DEFAULT 0,
I think my issue is i have no Unique ID that can be used from spawn data. Spawn.ID is only unique to the zone and spawn cycle. Thus the autoincrement id field for primary key.
I found another Lua that was doing sql stuff and heavily used it as a base.
thank you Jackalo for your SoloLootManager Lua. btw that thing sells so much faster than ninjaloot i swear its like 5 items a second.
I can get the mobs to populate the gui, I can not get the toggles to work the debug spits out nil values being passed to the SQL query
so basically I am only working on the in memory database. since it can't make updates and this never get to writing it out.
[CODE lang="Lua" title="SpawnMasterCheck"]-- Reworking SoloLootManager to be a Spawn Checker ~thank you Jackalo~
-- Basically Lua version of MQ2SpawnMaster using Sql Lite.
local SpawnMasterCheck = { _version = '1.0', author = 'Grimmier' }
--- @type Mq
local mq = require('mq')
--- @type ImGui
require('ImGui')
local ImguiHelper = require('mq/ImguiHelper')
-- https://gitlab.com/Knightly1/knightlinc
local Write = require('libraries/Write')
Write.prefix = 'SMC'
Write.loglevel = 'info'
local PackageMan = require('mq/PackageMan')
local lsql = PackageMan.Require('lsqlite3') do
Write.Debug("lsqlite version: %s", lsql.version())
end
local DBPath = string.format('%s\\%s', mq.configDir, 'SpawnChecker.sqlite3')
local Table_Cache = {
Rules = {},
Filtered = {},
Unhandled = {},
Mobs = {},
}
local Lookup = {
Rules = {},
}
local GUI_Main = {
Open = true,
Show = true,
Flags = bit32.bor(ImGuiWindowFlags.NoResize, ImGuiWindowFlags.AlwaysAutoResize),
Action = {
},
}
local GUI_Config = {
Open = false,
Show = false,
--Flags = bit32.bor(0),
Refresh = {
Sort = {
Rules = false,
Unhandled = false,
Mobs = false,
},
Table = {
Rules = false,
Filtered = false,
Unhandled = false,
Mobs = false,
},
},
Search = '',
Table = {
Column_ID = {
ID = 1,
MobName = 2,
MobZoneName = 3,
MobLoc = 4,
MobID = 5,
Action = 6,
Remove = 7,
},
Flags = bit32.bor(
ImGuiTableFlags.Resizable,
ImGuiTableFlags.Sortable,
ImGuiTableFlags.NoSavedSettings,
ImGuiTableFlags.RowBg,
ImGuiTableFlags.BordersV,
ImGuiTableFlags.BordersOuter,
ImGuiTableFlags.SizingStretchProp,
ImGuiTableFlags.ScrollY,
ImGuiTableFlags.Hideable
),
SortSpecs = {
Rules = nil,
Unhandled = nil,
Mobs = nil,
},
},
}
---@class Database
---@field private version integer
---@field private connection any lsqlite3.open() object
local Database = {version = 1} do
---@param dbpath string
---@return Database object
function Database:new(dbpath)
local obj = {}
setmetatable(obj, self)
self.__index = self
self.connection = lsql.open(dbpath)
self.connection:exec('PRAGMA primary_keys=on')
self:Initialize()
self:CheckDatabase()
return obj
end
---@package
function Database:CheckDatabase()
local dbVersion = self:GetConfig('version')
-- If there's a revision, upgrade here in steps to each successive version and recurse this function
if dbVersion == '1' then
Write.Debug("dbVersion is current: v%s", dbVersion)
elseif dbVersion ~= nil then
Write.Fatal("unknown database version: %s", dbVersion)
mq.exit()
end
end
---@package
---Initializes a database with the default tables and values.
function Database:Initialize()
self.connection:exec([[
CREATE TABLE IF NOT EXISTS config(
key TEXT PRIMARY KEY,
value TEXT NOT NULL
);
INSERT INTO config (key, value) VALUES ("version", "1")
ON CONFLICT DO NOTHING;
CREATE TABLE IF NOT EXISTS enum_action(
enum TEXT PRIMARY KEY
);
INSERT INTO enum_action (enum) VALUES ("unhandled")
ON CONFLICT DO NOTHING;
INSERT INTO enum_action (enum) VALUES ("ignore")
ON CONFLICT DO NOTHING;
INSERT INTO enum_action (enum) VALUES ("track")
ON CONFLICT DO NOTHING;
CREATE TABLE IF NOT EXISTS rule(
id INTEGER PRIMARY KEY NOT NULL,
mobname TEXT NOT NULL,
mobzonename TEXT,
mobloc TEXT,
mobid LONG DEFAULT 0,
enum_action TEXT DEFAULT "unhandled",
CONSTRAINT fk_enum_action
FOREIGN KEY (enum_action)
REFERENCES enum_action (enum)
);
]])
end
---@package
---Called before Initialize() to wipe the database first.
function Database:Reinitialize()
self.connection:exec([[
DROP TABLE IF EXISTS config;
DROP TABLE IF EXISTS rule;
DROP TABLE IF EXISTS enum_action;
]])
self:Initialize()
end
---@package
---@param key string
---@param value string
function Database:AddConfig(key, value)
local spc = self.connection:prepare([[INSERT INTO config (key, value) VALUES (?, ?) ON CONFLICT (key) DO UPDATE SET value = excluded.value]])
spc:bind_values(key, value)
spc:step()
spc:finalize()
end
---@package
---@param key string
function Database:RemoveConfig(key)
local spc = self.connection:prepare([[DELETE FROM config WHERE key = ?]])
spc:bind_values(key)
spc:step()
spc:finalize()
end
---@package
---@param key string
---@return string
function Database:GetConfig(key)
local spc = self.connection:prepare([[SELECT value FROM config WHERE key = ?]])
spc:bind_values(key)
for row in spc:nrows() do
spc:finalize()
return row.value
end
end
---@private
function RuleToEntry(rule)
local entry = {
MobName = rule.mobname,
MobZoneName = rule.mobzonename,
MobLoc = rule.mobloc,
MobID = rule.mobID,
Enum_Action = rule.enum_action,
}
return entry
end
---@private
local function EntryToRule(entry)
local rule = {
MobName = entry.mobname,
MobZoneName = entry.mobzonename,
MobLoc = entry.mobloc,
MobID = entry.mobid,
Enum_Action = entry.enum_action,
}
return rule
end
---@package
---@param id any
---@param entry table
function Database:AddRule(id, entry)
local keys, values, conflict
local binds = {}
table.insert(binds, id)
for k,v in pairs(EntryToRule(entry)) do
if #binds > 1 then
keys = keys .. ', ' .. k
values = values .. ', ?'
conflict = conflict .. string.format(', %s = excluded.%s', k, k)
else
keys = k
values = '?'
conflict = string.format('%s = excluded.%s', k, k)
end
table.insert(binds, v)
end
local query = string.format([[INSERT INTO rule (%s) VALUES (%s) ON CONFLICT (id) DO UPDATE SET %s]], keys, values, conflict)
local spc = self.connection:prepare(query)
--DEBUG
-- mq.cmd(string.format('/echo Query: %s ', query))
-- local spc = self.connection:prepare(query)
local spc, err = self.connection:prepare(query)
if not spc then
Write.Debug("Statement preparation failed: %s", err)
return -- handle the error appropriately
end
spc:bind_values(unpack(binds))
spc:step()
spc:finalize()
spc:exec("COMMIT;")
end
---@package
---@param id any
function Database:RemoveRule(id)
local spc = self.connection:prepare([[DELETE FROM rule WHERE id = ?]])
spc:bind_values(id)
spc:step()
spc:finalize()
end
---@package
---@param id string
---@return table
function Database:GetRule(id)
local spc = self.connection:prepare([[SELECT * FROM rule WHERE id = ?]])
spc:bind_values(id)
for row in spc:nrows() do
spc:finalize()
return RuleToEntry(row)
end
end
---@package
---@return table
function Database:GetAllRules()
local spc = self.connection:prepare([[SELECT * FROM rule]])
local newTable = {}
for row in spc:nrows() do
table.insert(newTable, RuleToEntry(row))
end
return newTable
end
end
local DB = Database:new(DBPath)
local function ReinitializeDB()
local title = "SMC > Reinitialize Warning"
local text = "Are you really sure you want to wipe the database?"
if ImguiHelper.Popup.Modal(title, text, { "Yes", "Cancel" }) == 1 then
DB:Reinitialize()
end
end
local function AddRule(entry)
Write.Debug('AddRule [%s] %s', entry.Enum_Action, entry.MobName)
DB:AddRule(entry.MobName, entry)
GUI_Config.Refresh.Table.Rules = true
GUI_Config.Refresh.Table.Filtered = true
GUI_Config.Refresh.Table.Unhandled = true
end
local function RemoveRule(entry)
Write.Debug('RemoveRule [%s] %s', entry.Enum_Action, entry.SpawnLink)
DB:RemoveRule(entry.ID)
GUI_Config.Refresh.Table.Rules = true
GUI_Config.Refresh.Table.Filtered = true
GUI_Config.Refresh.Table.Unhandled = true
end
local function CheckRule(entry)
if Lookup.Rules[entry.MobName] then
return true
else
return false
end
end
local function Compare(entryValue, wantedValue)
if entryValue == wantedValue then
return true
else
return false
end
end
local function SpawnToEntry(spawn)
local entry = {
MobName = spawn.CleanName(),
MobZoneName = mq.TLO.Zone.Name,
MobLoc = spawn.Loc(),
MobID = spawn.ID(), -- If you want to include MobID, keep it here
Enum_Action = 'unhandled',
}
return entry
end
local function InsertTableSpawn(dataTable, spawn, opts)
local entry = SpawnToEntry(spawn)
if opts then
for k,v in pairs(opts) do
entry[k] = v
end
end
table.insert(dataTable, entry)
end
local function TableSortSpecs(a, b)
for i = 1, GUI_Config.Table.SortSpecs.SpecsCount, 1 do
local spec = GUI_Config.Table.SortSpecs:Specs(i)
local delta = 0
if spec.ColumnUserID == GUI_Config.Table.Column_ID.MobName then
if a.mobname < b.mobname then
delta = -1
elseif a.mobname> b.mobname then
delta = 1
end
elseif spec.ColumnUserID == GUI_Config.Table.Column_ID.MobLoc then
if a.mobloc < b.mobloc then
delta = -1
elseif a.mobloc > b.mobloc then
delta = 1
end
elseif spec.ColumnUserID == GUI_Config.Table.Column_ID.MobID then
if a.mobid < b.mobid then
delta = -1
elseif a.mobid > b.mobid then
delta = 1
end
elseif spec.ColumnUserID == GUI_Config.Table.Column_ID.Action then
if a.Enum_Action < b.Enum_Action then
delta = -1
elseif a.Enum_Action > b.Enum_Action then
delta = 1
end
end
if delta ~= 0 then
if spec.SortDirection == ImGuiSortDirection.Ascending then
return delta < 0
else
return delta > 0
end
end
end
return a.Name < b.Name
end
local function RefreshRules()
Table_Cache.Rules = DB:GetAllRules()
local newTable = {}
for k,v in ipairs(Table_Cache.Rules) do
table.insert(newTable, v.ID, k)
end
Lookup.Rules = newTable
GUI_Config.Refresh.Table.Filtered = true
GUI_Config.Refresh.Table.Unhandled = true
GUI_Config.Refresh.Table.Rules = false
end
local function RefreshFiltered()
local splitSearch = {}
for part in string.gmatch(GUI_Config.Search, '[^%s]+') do
table.insert(splitSearch, part)
end
local newTable = {}
for k,v in ipairs(Table_Cache.Rules) do
local found = 0
for _,search in ipairs(splitSearch) do
if string.find(string.lower(v.Name), string.lower(search)) then
found = found + 1
end
end
if #splitSearch == found then
table.insert(newTable, v)
end
end
Table_Cache.Filtered = newTable
GUI_Config.Refresh.Sort.Rules = true
GUI_Config.Refresh.Table.Filtered = false
end
local function RefreshUnhandled()
local newTable = {}
for k,v in ipairs(Table_Cache.Rules) do
if v.Enum_Action == 'unhandled' then
table.insert(newTable, v)
end
end
Table_Cache.Unhandled = newTable
GUI_Config.Refresh.Sort.Unhandled = true
GUI_Config.Refresh.Table.Unhandled = false
end
-- Define a predicate function that checks if the spawn is an NPC
local function RefreshZone()
local newTable = {}
local zoneName = mq.TLO.Zone.Name
local npcs = mq.getFilteredSpawns(function(spawn) return spawn.Type() == 'NPC' end)
mq.cmd('/echo MobCount: Iinit', #npcs)
for i = 1, #npcs do
local spawn = npcs
local entry = SpawnToEntry(spawn)-- Convert spawn data to an entry
--entry.ID = i
entry.MobName = spawn.CleanName
entry.MobZoneName = mq.TLO.Zone.Name
entry.MobLoc = spawn.LocYXZ
entry.MobID = spawn.ID
if entry.MobName then table.insert(newTable, entry) end -- Add the entry to the newTable
--Debug:
-- mq.cmd('/echo ID: ', entry.ID)
end
--debug:
--mq.cmd('/echo MobCount: A', #npcs)
Table_Cache.Mobs = newTable
-- debug:
mq.cmd('/echo TableCount: Mobs', #Table_Cache.Mobs)
---mq.cmd('/echo key: Mobs', Table_Cache.Mobs)
for k, v in pairs(Table_Cache.Mobs) do
--Debug:
-- mq.cmd('/echo ipairs:', k, v)
if not CheckRule(v) then
AddRule(v)
end
end
GUI_Config.Refresh.Sort.Mobs = true
GUI_Config.Refresh.Table.Mobs = false
end
local function DrawRuleRow(entry)
ImGui.TableNextColumn()
ImGui.Text('%s', entry.MobName)
ImGui.TableNextColumn()
ImGui.Text('%s', (entry.MobZoneName))
ImGui.TableNextColumn()
ImGui.Text('%s', (entry.MobLoc))
ImGui.TableNextColumn()
ImGui.Text('%s', (entry.MobID))
ImGui.TableNextColumn()
if ImGui.RadioButton("Ignore", Compare(entry.Enum_Action, 'ignore')) then
entry.Enum_Action = 'ignore'
AddRule(entry)
end
ImGui.SameLine()
if ImGui.RadioButton("Track", Compare(entry.Enum_Action, 'track')) then
entry.Enum_Action = 'track'
AddRule(entry)
end
ImGui.TableNextColumn()
if ImGui.SmallButton("Remove") then RemoveRule(entry) end
end
local function DrawMainGUI()
if GUI_Main.Open then
GUI_Main.Open = ImGui.Begin("Spawn Master Checker", GUI_Main.Open, GUI_Main.Flags)
ImGui.PushStyleVar(ImGuiStyleVar.WindowPadding, 2, 2)
if #Table_Cache.Unhandled > 0 then
ImGui.PushStyleColor(ImGuiCol.Button, ImVec4(1, 0.3, 0.3, 1))
ImGui.PushStyleColor(ImGuiCol.ButtonHovered, ImVec4(1, 0.4, 0.4, 1))
ImGui.PushStyleColor(ImGuiCol.ButtonActive, ImVec4(1, 0.5, 0.5, 1))
end
if ImGui.SmallButton("Config") then GUI_Config.Open = not GUI_Config.Open end
if #Table_Cache.Unhandled > 0 then
ImGui.PopStyleColor(3)
end
ImGui.SameLine()
if ImGui.SmallButton("Refresh Zone") then RefreshZone() end
ImGui.SameLine()
-- if ImGui.SmallButton("Track Spawns") then end
ImGui.PopStyleVar()
ImGui.End()
end
end
local function DrawConfigGUI()
if GUI_Config.Open then
GUI_Config.Open, GUI_Config.Show = ImGui.Begin("SMC > Config", GUI_Config.Open)
if not GUI_Config.Show then
ImGui.End()
return GUI_Config.Open
end
ImGui.PushStyleVar(ImGuiStyleVar.WindowPadding, 2, 2)
if ImGui.BeginTabBar('##TabBar') then
if ImGui.BeginTabItem("All Rules") then
ImGui.PushItemWidth(-95)
local searchText, selected = ImGui.InputText("Search##RulesSearch", GUI_Config.Search)
ImGui.PopItemWidth()
if selected and GUI_Config.Search ~= searchText then
GUI_Config.Search = searchText
GUI_Config.Refresh.Sort.Rules = true
GUI_Config.Refresh.Table.Filtered = true
end
ImGui.SameLine()
if ImGui.Button("Clear##ClearRulesSearch") then
GUI_Config.Search = ''
GUI_Config.Refresh.Sort.Rules = true
GUI_Config.Refresh.Table.Filtered = true
end
ImGui.Separator()
if ImGui.BeginTable('##RulesTable', 6, GUI_Config.Table.Flags) then
ImGui.TableSetupScrollFreeze(0, 1)
ImGui.TableSetupColumn("Name", ImGuiTableColumnFlags.DefaultSort, 8, GUI_Config.Table.Column_ID.MobName)
ImGui.TableSetupColumn("Zone", ImGuiTableColumnFlags.DefaultSort, 8, GUI_Config.Table.Column_ID.MobZoneName)
ImGui.TableSetupColumn("Loc", ImGuiTableColumnFlags.DefaultSort, 8, GUI_Config.Table.Column_ID.MobLoc)
ImGui.TableSetupColumn("MobID", ImGuiTableColumnFlags.DefaultSort, 8, GUI_Config.Table.Column_ID.MobID)
ImGui.TableSetupColumn("Action", ImGuiTableColumnFlags.DefaultSort, 8, GUI_Config.Table.Column_ID.Action)
ImGui.TableSetupColumn("Remove", ImGuiTableColumnFlags.DefaultSort, 2, GUI_Config.Table.Column_ID.Remove)
ImGui.TableHeadersRow()
local sortSpecs = ImGui.TableGetSortSpecs()
if sortSpecs and (sortSpecs.SpecsDirty or GUI_Config.Refresh.Sort.Unhandled) then
if #Table_Cache.Unhandled > 1 then
GUI_Config.Table.SortSpecs = sortSpecs
table.sort(Table_Cache.Unhandled, TableSortSpecs)
GUI_Config.Table.SortSpecs = nil
end
sortSpecs.SpecsDirty = false
GUI_Config.Refresh.Sort.Unhandled = false
end
local clipper = ImGuiListClipper.new()
clipper:Begin(#Table_Cache.Mobs)
while clipper:Step() do
for i = clipper.DisplayStart, clipper.DisplayEnd - 1, 1 do
local entry = Table_Cache.Mobs[i + 1]
ImGui.PushID(entry.ID)
--ImGui.PushID(entry.MobName)
ImGui.TableNextRow()
DrawRuleRow(entry)
ImGui.PopID()
end
end
clipper:End()
ImGui.EndTable()
end
ImGui.EndTabItem()
end
if #Table_Cache.Unhandled > 0 then
ImGui.PushStyleColor(ImGuiCol.Tab, ImVec4(1, 0.3, 0.3, 1))
ImGui.PushStyleColor(ImGuiCol.TabHovered, ImVec4(1, 0.4, 0.4, 1))
ImGui.PushStyleColor(ImGuiCol.TabActive, ImVec4(1, 0.5, 0.5, 1))
end
if ImGui.BeginTabItem("Tracking") then
if ImGui.BeginTable('##UnhandledRulesTable', 4, GUI_Config.Table.Flags) then
ImGui.TableSetupScrollFreeze(0, 1)
ImGui.TableSetupColumn("Name", ImGuiTableColumnFlags.DefaultSort, 8, GUI_Config.Table.Column_ID.MobName)
ImGui.TableSetupColumn("Zone", ImGuiTableColumnFlags.DefaultSort, 8, GUI_Config.Table.Column_ID.MobZoneName)
ImGui.TableSetupColumn("Loc", ImGuiTableColumnFlags.DefaultSort, 8, GUI_Config.Table.Column_ID.MobLoc)
ImGui.TableSetupColumn("MobID", ImGuiTableColumnFlags.DefaultSort, 8, GUI_Config.Table.Column_ID.MobID)
ImGui.TableHeadersRow()
local sortSpecs = ImGui.TableGetSortSpecs()
if sortSpecs and (sortSpecs.SpecsDirty or GUI_Config.Refresh.Sort.Unhandled) then
if #Table_Cache.Unhandled > 1 then
GUI_Config.Table.SortSpecs = sortSpecs
table.sort(Table_Cache.Unhandled, TableSortSpecs)
GUI_Config.Table.SortSpecs = nil
end
sortSpecs.SpecsDirty = false
GUI_Config.Refresh.Sort.Unhandled = false
end
local clipper = ImGuiListClipper.new()
clipper:Begin(#Table_Cache.Unhandled)
while clipper:Step() do
for i = clipper.DisplayStart, clipper.DisplayEnd - 1, 1 do
local entry = Table_Cache.Unhandled[i + 1]
ImGui.PushID(entry.ID)
ImGui.TableNextRow()
DrawRuleRow(entry)
ImGui.PopID()
end
end
clipper:End()
ImGui.EndTable()
end
ImGui.EndTabItem()
end
if #Table_Cache.Unhandled > 0 then
ImGui.PopStyleColor(3)
end
ImGui.EndTabBar()
end
ImGui.PopStyleVar()
ImGui.End()
end
end
local function DrawGUI()
DrawMainGUI()
DrawConfigGUI()
end
-- Kickstart the data
GUI_Config.Refresh.Table.Rules = true
GUI_Config.Refresh.Table.Filtered = true
GUI_Config.Refresh.Table.Unhandled = true
mq.imgui.init('DrawMainGUI', DrawGUI)
mq.bind("/smcconfig", function() GUI_Config.Open = not GUI_Config.Open end)
mq.bind("/smcreinitialize", ReinitializeDB)
mq.bind("/smcquit", function() GUI_Main.Open = not GUI_Main.Open end)
while GUI_Main.Open do
mq.delay(50)
if GUI_Config.Refresh.Table.Rules then RefreshRules() end
if GUI_Config.Refresh.Table.Filtered then RefreshFiltered() end
if GUI_Config.Refresh.Table.Unhandled then RefreshUnhandled() end
end[/CODE]


