Database and Events
SQL database operations and the event system (BridgeFunction) on the server.
Database (SQL)
Running Queries
SQLQuery(queryString, callback)
The callback receives a query object with results.
SELECT Example
local queryStr = "SELECT Name, Level FROM Character WHERE AccountID = 'admin'"
SQLQuery(queryStr, function(query)
if query.result > 0 then
while query:fetch() do
local name = query:getString("Name")
local level = query:getNumber("Level")
LogPrint(string.format("%s - Level %d", name, level))
end
else
LogPrint("No results")
end
end)
INSERT Example
local insertQuery = string.format(
"INSERT INTO MyTable (Name, Value) VALUES ('%s', %d)",
"PlayerName", 100
)
SQLQuery(insertQuery, function(query)
LogPrint("Inserted!")
end)
UPDATE Example
local updateQuery = string.format(
"UPDATE Character SET Level = %d WHERE Name = '%s'",
400, "PlayerName"
)
SQLQuery(updateQuery, function(query)
LogPrint("Updated!")
end)
Query Object
-- Properties
query.result -- 0 = no results, >0 = has results
query.rowCount -- Number of rows
query.colCount -- Number of columns
-- Methods
query:fetch() -- Move to next row (returns boolean)
query:getNumber("col") -- Read number
query:getSingle("col") -- Read float
query:getString("col") -- Read string
CRITICAL: Validate in Callbacks!
Always check if player is still online in SQL callbacks:
function MyPlugin:loadData(aIndex)
local playerName = GetObjectName(aIndex)
SQLQuery("SELECT ...", function(query)
-- CRITICAL: Validate!
if GetObjectConnected(aIndex) ~= OBJECT_ONLINE then
return
end
if GetObjectName(aIndex) ~= playerName then
return
end
-- Now it's safe to use aIndex
end)
end
Creating Tables
BridgeFunction:push("OnLoadScript", function()
local createTable = [[
IF NOT EXISTS (SELECT * FROM sys.objects
WHERE object_id = OBJECT_ID(N'MyTable'))
BEGIN
CREATE TABLE MyTable (
Name VARCHAR(10) PRIMARY KEY,
Value INT NOT NULL DEFAULT 0
)
END
]]
SQLQuery(createTable, function(query)
LogPrint("Table created!")
end)
end)
Event System (BridgeFunction)
Register callbacks for game events:
BridgeFunction:push(eventName, callback)
Core Events
-- On script load (first time)
BridgeFunction:push("OnReadScript", function()
CommandAddInfo("/mycommand", MyPlugin.Code)
end)
-- After OnReadScript
BridgeFunction:push("OnLoadScript", function()
-- Create tables, initialize data
end)
-- On server shutdown
BridgeFunction:push("OnShutScript", function()
-- Save data, cleanup
end)
-- Every second
BridgeFunction:push("OnTimerThread", function()
-- Periodic tasks
end)
Player Events
-- Player enters game
BridgeFunction:push("OnCharacterEntry", function(aIndex)
local name = GetObjectName(aIndex)
NoticeSend(aIndex, 1, "Welcome!")
end)
-- Player disconnects
BridgeFunction:push("OnCharacterClose", function(aIndex)
local name = GetObjectName(aIndex)
LogPrint(name .. " disconnected")
end)
-- Level up
BridgeFunction:push("OnUserLevelUp", function(aIndex)
local level = GetObjectLevel(aIndex)
NoticeSend(aIndex, 1, "Level UP! " .. level)
end)
-- Master level up
BridgeFunction:push("OnUserMasterLevelUp", function(aIndex)
local mlevel = GetObjectMasterLevel(aIndex)
NoticeSend(aIndex, 1, "Master Level UP! " .. mlevel)
end)
-- Reset/Master Reset
BridgeFunction:push("OnUserReset", function(aIndex)
local resets = GetObjectReset(aIndex)
NoticeSendToAll(1, GetObjectName(aIndex) .. " reset! Total: " .. resets)
end)
BridgeFunction:push("OnUserMasterReset", function(aIndex)
NoticeSendToAll(1, GetObjectName(aIndex) .. " master reset!")
end)
-- Player dies
BridgeFunction:push("OnUserDie", function(aIndex, bIndex)
local deadName = GetObjectName(aIndex)
local killerName = GetObjectName(bIndex)
LogPrint(deadName .. " killed by " .. killerName)
end)
-- Player respawns
BridgeFunction:push("OnUserRespawn", function(aIndex, killerType)
NoticeSend(aIndex, 1, "You respawned!")
end)
Monster Events
-- Monster dies
BridgeFunction:push("OnMonsterDie", function(aIndex, bIndex)
local monsterClass = GetObjectClass(aIndex)
local killerName = GetObjectName(bIndex)
-- aIndex = monster, bIndex = killer
end)
Item Events
-- Pick up item
BridgeFunction:push("OnItemPick", function(aIndex, itemIndex, item)
local itemName = ItemGetName(item.index)
-- Return 1 to block pickup
return 0
end)
-- Drop item
BridgeFunction:push("OnItemDrop", function(aIndex, slot, x, y, item)
-- Return 1 to block drop
return 0
end)
-- Use item
BridgeFunction:push("OnItemUse", function(aIndex, sourceSlot, targetSlot)
-- Return 1 to block use
return 0
end)
Command Events
BridgeFunction:push("OnCommandManager", function(aIndex, code, args)
if code == MyPlugin.Code then
MyPlugin:handleCommand(aIndex, args)
return 1 -- Processed
end
return 0 -- Not processed
end)
Network Packets
Register Packet IDs
PluginPacket = PluginPacket or {}
PluginPacket.MyPlugin = 15000 -- Must match client!
Receive from Client
MagicWorld:pushRecv(PluginPacket.MyPlugin, function(aIndex, packet)
local action = packet:getString()
local value = packet:getNumber()
-- Process request
-- Send response
end)
Send to Client
MagicWorld:sendData(aIndex, PluginPacket.MyPlugin, {
"response",
123,
"data"
})
Complete Example - Ranking System
Ranking = {}
Ranking.ScriptVersion = "1.0.0"
PluginPacket.Ranking = 15050
-- Create table
BridgeFunction:push("OnLoadScript", function()
SQLQuery([[
IF NOT EXISTS (SELECT * FROM sys.objects WHERE object_id = OBJECT_ID(N'Ranking'))
BEGIN
CREATE TABLE Ranking (
Name VARCHAR(10) PRIMARY KEY,
Points INT NOT NULL DEFAULT 0
)
END
]], function(query) end)
end)
-- Track kills
BridgeFunction:push("OnUserDie", function(aIndex, bIndex)
local deadName = GetObjectName(aIndex)
local killerName = GetObjectName(bIndex)
if deadName == killerName then return end
-- Update killer
local updateQuery = string.format([[
UPDATE Ranking SET Points = Points + 10 WHERE Name = '%s'
IF @@ROWCOUNT = 0
INSERT INTO Ranking (Name, Points) VALUES ('%s', 10)
]], killerName, killerName)
SQLQuery(updateQuery, function(q) end)
NoticeSend(bIndex, 1, "+10 ranking points!")
end)
-- Show ranking
function Ranking:show(aIndex)
SQLQuery("SELECT TOP 10 Name, Points FROM Ranking ORDER BY Points DESC",
function(query)
if GetObjectConnected(aIndex) ~= OBJECT_ONLINE then return end
if query.result > 0 then
while query:fetch() do
local name = query:getString("Name")
local points = query:getNumber("Points")
MagicWorld:sendData(aIndex, PluginPacket.Ranking, {
name, points
})
end
end
end)
end
LogColor(3, "[Ranking] Loaded - Version: " .. Ranking.ScriptVersion)
Best Practices
- ALWAYS validate player in SQL callbacks - Check OBJECT_ONLINE and name
- Use OnLoadScript for table creation
- Return 1 in events to block default behavior
- Keep OnTimerThread light - Runs every second
- Log important events for debugging
That's database and events! One more guide to go: utilities and commands.