Initial code push

This commit is contained in:
Foereaper
2021-07-16 01:08:10 +02:00
parent cce176570d
commit 59d8c37120
13 changed files with 1440 additions and 1 deletions

141
Client/CMH.lua Normal file
View File

@ -0,0 +1,141 @@
local CMH = {}
local links = {}
function CMH.OnReceive(self, event, prefix, _, Type, sender)
if event == "CHAT_MSG_ADDON" and sender == UnitName("player") and Type == "WHISPER" then
local source, functionId, link, linkCount, MSG = prefix:match("(%D)(%d%d)(%d%d%d)(%d%d%d)(.+)");
if not source or not functionId or not link or not linkCount or not MSG then
return
end
if(source == "S") then
functionId, link, linkCount = tonumber(functionId), tonumber(link), tonumber(linkCount);
links[functionId] = links[functionId] or {count = 0};
links[functionId][link] = MSG;
links[functionId].count = links[functionId].count + 1;
if (links[functionId].count ~= linkCount) then
return
end
local fullMessage = table.concat(links[functionId]);
links[functionId] = {count = 0};
local VarTable = ParseMessage(fullMessage)
if not VarTable then
return
end
if not(CMH[VarTable[1]]) then
return
end
local func = CMH[VarTable[1]][functionId]
if func then
_G[func](sender, VarTable)
end
return
end
end
end
local CMHFrame = CreateFrame("Frame")
CMHFrame:RegisterEvent("CHAT_MSG_ADDON")
CMHFrame:SetScript("OnEvent", CMH.OnReceive)
function RegisterServerResponses(config)
if(CMH[config.Prefix]) then
return;
end
CMH[config.Prefix] = {}
for functionId, functionName in pairs(config.Functions) do
CMH[config.Prefix][functionId] = functionName
end
end
function ParseMessage(str)
local output = {}
local valTemp = {}
local typeTemp = {}
local delim = {"", "", "", "", ""}
local valMatch = "[^"..table.concat(delim).."]+"
local typeMatch = "["..table.concat(delim).."]+"
-- Get values
for value in str:gmatch(valMatch) do
table.insert(valTemp, value)
end
-- Get type from delimiter
for varType in str:gmatch(typeMatch) do
for k, v in pairs(delim) do
if(v == varType) then
table.insert(typeTemp, k)
end
end
end
-- Convert value to correct type
for k, v in pairs(valTemp) do
local varType = typeTemp[k]
if(varType == 3) then -- Ints
v = tonumber(v)
elseif(varType == 4) then -- Tables
v = Smallfolk.loads(v, #v)
elseif(varType == 5) then -- Booleans
if(v == "true") then v = true else v = false end
end
table.insert(output, v)
end
valTemp = nil
typeTemp = nil
return output
end
function SendClientRequest(prefix, functionId, ...)
-- ♠ = Prefix prefix
-- ♥ = ArgumentPrefix for Strings
-- ♚ = ArgumentPrefix for Ints
-- ♛ = ArgumentPrefix for Tables
-- ♜ = ArgumentPrefix for Boolean
local arg = {...}
local splitLength = 230
local msg = "" .. prefix
for _, v in pairs(arg) do
if(type(v) == "string") then
msg = msg .. ""
elseif(type(v) == "number") then
msg = msg .. ""
elseif(type(v) == "table") then
-- use Smallfolk to convert table structure to string
v = Smallfolk.dumps(v)
msg = msg .. ""
elseif(type(v) == "boolean") then
v = tostring(v)
msg = msg .. ""
end
msg = msg .. v
end
local splits = math.ceil(msg:len() / splitLength)
local send
local counter = 1
for i=1, msg:len(), splitLength do
send = string.format("%01s%03d%02d%02d", "C", functionId, counter, splits)
if ((i + splitLength) > msg:len()) then
send = send .. msg:sub(i, msg:len())
else
send = send .. msg:sub(i, i + splitLength - 1)
end
counter = counter + 1
SendAddonMessage(send, "", "WHISPER", UnitName("player"))
end
end

8
Client/CMH.toc Normal file
View File

@ -0,0 +1,8 @@
## Interface: 30300
## Title: CMH
## Notes: Client & Server Message Handler framework for communication between server and client.
## Version: 1.0
## Author: Foereaper, Stoneharry, Terrorblade
smallfolk.lua
CMH.lua

152
Client/FrameXML.toc Normal file
View File

@ -0,0 +1,152 @@
# Do not delete the following line!
## Interface: 30300
##DebugHook.lua
GlobalStrings.lua
Constants.lua
Fonts.xml
FontStyles.xml
Localization.xml
## add new modules below here
## CMH START
smallfolk.lua
CMH.lua
## CMH END
BasicControls.xml
WorldFrame.xml
UIParent.xml
AnimTimerFrame.xml
MoneyFrame.lua
MoneyFrame.xml
MoneyInputFrame.lua
MoneyInputFrame.xml
GameTooltip.xml
UIMenu.xml
UIDropDownMenu.xml
UIPanelTemplates.lua
UIPanelTemplates.xml
SecureTemplates.xml
SecureHandlerTemplates.xml
ItemButtonTemplate.xml
SparkleFrame.xml
HybridScrollFrame.lua
HybridScrollFrame.xml
GameMenuFrame.xml
CharacterFrameTemplates.xml
TextStatusBar.lua
TextStatusBar.xml
UIErrorsFrame.xml
AutoComplete.xml
StaticPopup.xml
Sound.lua
OptionsFrameTemplates.xml
OptionsPanelTemplates.xml
VideoOptionsFrame.xml
VideoOptionsPanels.xml
AudioOptionsFrame.xml
AudioOptionsPanels.xml
InterfaceOptionsFrame.xml
InterfaceOptionsPanels.xml
AlertFrames.xml
MirrorTimer.xml
CoinPickupFrame.xml
StackSplitFrame.xml
FadingFrame.xml
ZoneText.xml
BattlefieldFrame.xml
MainMenuBar.xml
MainMenuBarMicroButtons.xml
TutorialFrame.xml
Minimap.xml
GameTime.xml
Cooldown.xml
ActionButtonTemplate.xml
ActionBarFrame.xml
MultiActionBars.xml
##ActionWindow.xml
BuffFrame.xml
CombatFeedback.xml
CastingBarFrame.xml
UnitPopup.xml
UnitFrame.xml
BNet.xml
HistoryKeeper.lua
BNConversations.xml
ChatFrame.xml
FloatingChatFrame.xml
VoiceChat.xml
ReadyCheck.xml
PlayerFrame.xml
PartyFrame.xml
TargetFrame.xml
TotemFrame.xml
PetFrame.xml
StatsFrame.xml
SpellBookFrame.xml
CharacterFrame.xml
EquipmentManager.lua
PaperDollFrame.xml
PetPaperDollFrame.xml
SkillFrame.xml
ReputationFrame.xml
HonorFrame.xml
QuestFrame.xml
QuestPOI.xml
WatchFrame.xml
QuestLogFrame.xml
QuestInfo.xml
MerchantFrame.xml
TradeFrame.xml
ContainerFrame.xml
LootFrame.xml
ItemTextFrame.xml
TaxiFrame.xml
BankFrame.xml
FriendsFrame.xml
RaidFrame.xml
ChannelFrame.xml
PetActionBarFrame.xml
MultiCastActionBarFrame.xml
BonusActionBarFrame.xml
MainMenuBarBagButtons.xml
WorldMapFrame.xml
CinematicFrame.xml
ItemRef.xml
ComboFrame.xml
TabardFrame.xml
GuildRegistrarFrame.xml
PetitionFrame.xml
HelpFrame.xml
KnowledgeBaseFrame.xml
ColorPickerFrame.xml
GossipFrame.xml
MailFrame.xml
PetStable.xml
DurabilityFrame.xml
WorldStateFrame.xml
DressUpFrame.xml
RaidWarning.xml
ClassTrainerFrameTemplates.xml
PVPFrame.xml
PVPBattlegroundFrame.xml
ArenaFrame.xml
ArenaRegistrarFrame.xml
LFGFrame.xml
LFDFrame.xml
LFRFrame.xml
MovieRecordingProgress.xml
MacOptionsFrame.xml
RatingMenuFrame.xml
TalentFrameBase.lua
TalentFrameTemplates.xml
RuneFrame.xml
EasyMenu.lua
ChatConfigFrame.xml
MovieFrame.xml
VehicleMenuBar.xml
AlternatePowerBar.xml
AnimationSystem.lua
## add new modules above here
LocalizationPost.xml

218
Client/smallfolk.lua Normal file
View File

@ -0,0 +1,218 @@
local M = {}
Smallfolk = M
local expect_object, dump_object
local error, tostring, pairs, type, floor, huge, concat = error, tostring, pairs, type, math.floor, math.huge, table.concat
local dump_type = {}
function dump_type:string(nmemo, memo, acc)
local nacc = #acc
acc[nacc + 1] = '"'
acc[nacc + 2] = self:gsub('"', '""')
acc[nacc + 3] = '"'
return nmemo
end
function dump_type:number(nmemo, memo, acc)
acc[#acc + 1] = ("%.17g"):format(self)
return nmemo
end
function dump_type:table(nmemo, memo, acc)
--[[
if memo[self] then
acc[#acc + 1] = '@'
acc[#acc + 1] = tostring(memo[self])
return nmemo
end
nmemo = nmemo + 1
]]
memo[self] = nmemo
acc[#acc + 1] = '{'
local nself = #self
for i = 1, nself do -- don't use ipairs here, we need the gaps
nmemo = dump_object(self[i], nmemo, memo, acc)
acc[#acc + 1] = ','
end
for k, v in pairs(self) do
if type(k) ~= 'number' or floor(k) ~= k or k < 1 or k > nself then
nmemo = dump_object(k, nmemo, memo, acc)
acc[#acc + 1] = ':'
nmemo = dump_object(v, nmemo, memo, acc)
acc[#acc + 1] = ','
end
end
acc[#acc] = acc[#acc] == '{' and '{}' or '}'
return nmemo
end
function dump_object(object, nmemo, memo, acc)
if object == true then
acc[#acc + 1] = 't'
elseif object == false then
acc[#acc + 1] = 'f'
elseif object == nil then
acc[#acc + 1] = 'n'
elseif object ~= object then
if (''..object):sub(1,1) == '-' then
acc[#acc + 1] = 'N'
else
acc[#acc + 1] = 'Q'
end
elseif object == huge then
acc[#acc + 1] = 'I'
elseif object == -huge then
acc[#acc + 1] = 'i'
else
local t = type(object)
if not dump_type[t] then
error('cannot dump type ' .. t)
end
return dump_type[t](object, nmemo, memo, acc)
end
return nmemo
end
function M.dumps(object)
local nmemo = 0
local memo = {}
local acc = {}
dump_object(object, nmemo, memo, acc)
return concat(acc)
end
local function invalid(i)
error('invalid input at position ' .. i)
end
local nonzero_digit = {['1'] = true, ['2'] = true, ['3'] = true, ['4'] = true, ['5'] = true, ['6'] = true, ['7'] = true, ['8'] = true, ['9'] = true}
local is_digit = {['0'] = true, ['1'] = true, ['2'] = true, ['3'] = true, ['4'] = true, ['5'] = true, ['6'] = true, ['7'] = true, ['8'] = true, ['9'] = true}
local function expect_number(string, start)
local i = start
local head = string:sub(i, i)
if head == '-' then
i = i + 1
head = string:sub(i, i)
end
if nonzero_digit[head] then
repeat
i = i + 1
head = string:sub(i, i)
until not is_digit[head]
elseif head == '0' then
i = i + 1
head = string:sub(i, i)
else
invalid(i)
end
if head == '.' then
local oldi = i
repeat
i = i + 1
head = string:sub(i, i)
until not is_digit[head]
if i == oldi + 1 then
invalid(i)
end
end
if head == 'e' or head == 'E' then
i = i + 1
head = string:sub(i, i)
if head == '+' or head == '-' then
i = i + 1
head = string:sub(i, i)
end
if not is_digit[head] then
invalid(i)
end
repeat
i = i + 1
head = string:sub(i, i)
until not is_digit[head]
end
return tonumber(string:sub(start, i - 1)), i
end
local expect_object_head = {
t = function(string, i) return true, i end,
f = function(string, i) return false, i end,
n = function(string, i) return nil, i end,
Q = function(string, i) return -(0/0), i end,
N = function(string, i) return 0/0, i end,
I = function(string, i) return 1/0, i end,
i = function(string, i) return -1/0, i end,
['"'] = function(string, i)
local nexti = i - 1
repeat
nexti = string:find('"', nexti + 1, true) + 1
until string:sub(nexti, nexti) ~= '"'
return string:sub(i, nexti - 2):gsub('""', '"'), nexti
end,
['0'] = function(string, i)
return expect_number(string, i - 1)
end,
['{'] = function(string, i, tables)
local nt, k, v = {}
local j = 1
tables[#tables + 1] = nt
if string:sub(i, i) == '}' then
return nt, i + 1
end
while true do
k, i = expect_object(string, i, tables)
if string:sub(i, i) == ':' then
v, i = expect_object(string, i + 1, tables)
nt[k] = v
else
nt[j] = k
j = j + 1
end
local head = string:sub(i, i)
if head == ',' then
i = i + 1
elseif head == '}' then
return nt, i + 1
else
invalid(i)
end
end
end,
--[[
['@'] = function(string, i, tables)
local match = string:match('^%d+', i)
local ref = tonumber(match)
if tables[ref] then
return tables[ref], i + #match
end
invalid(i)
end,
]]
}
expect_object_head['1'] = expect_object_head['0']
expect_object_head['2'] = expect_object_head['0']
expect_object_head['3'] = expect_object_head['0']
expect_object_head['4'] = expect_object_head['0']
expect_object_head['5'] = expect_object_head['0']
expect_object_head['6'] = expect_object_head['0']
expect_object_head['7'] = expect_object_head['0']
expect_object_head['8'] = expect_object_head['0']
expect_object_head['9'] = expect_object_head['0']
expect_object_head['-'] = expect_object_head['0']
expect_object_head['.'] = expect_object_head['0']
expect_object = function(string, i, tables)
local head = string:sub(i, i)
if expect_object_head[head] then
return expect_object_head[head](string, i + 1, tables)
end
invalid(i)
end
function M.loads(string, maxsize)
if #string > (maxsize or 10000) then
error 'input too large'
end
return (expect_object(string, 1, {}))
end
return M

View File

@ -0,0 +1,171 @@
-- Requiring of Client Message Handler not needed
-- Handled by addon dependency
local config = {
Prefix = "StatPointUI",
Functions = {
[1] = "OnCacheReceived"
}
}
local StatPointUI = {
cache = {}
}
function StatPointUI.OnLogin()
-- Load all UI assets before requesting cache from server
StatPointUI.OnLoad()
SendClientRequest(config.Prefix, 1)
end
function StatPointUI.OnLoad()
-- Create the main UI frame
StatPointUI.mainFrame = CreateFrame("Frame", config.Prefix, CharacterFrame)
StatPointUI.mainFrame:SetToplevel(true)
StatPointUI.mainFrame:SetSize(200, 260)
StatPointUI.mainFrame:SetBackdrop({
bgFile = [[Interface\TutorialFrame\TutorialFrameBackground]],
edgeFile = [[Interface\DialogFrame\UI-DialogBox-Border]],
edgeSize = 16,
tileSize = 32,
insets = {left = 5, right = 5, top = 5, bottom = 5}
})
StatPointUI.mainFrame:SetPoint("TOPRIGHT",170,-20)
StatPointUI.mainFrame:Hide()
-- Title bar
StatPointUI.titleBar = CreateFrame("Frame", config.Prefix.."TitleBar", StatPointUI.mainFrame)
StatPointUI.titleBar:SetSize(135, 25)
StatPointUI.titleBar:SetBackdrop(
{
bgFile = [[Interface/CHARACTERFRAME/UI-Party-Background]],
edgeFile = [[Interface/DialogFrame/UI-DialogBox-Border]],
tile = true,
edgeSize = 16,
tileSize = 16,
insets = {left = 5, right = 5, top = 5, bottom = 5}
})
StatPointUI.titleBar:SetPoint("TOP", 0, 9)
-- Titlebar text
StatPointUI.titleBarText = StatPointUI.titleBar:CreateFontString(config.Prefix.."TitleBarText")
StatPointUI.titleBarText:SetFont("Fonts\\FRIZQT__.TTF", 13)
StatPointUI.titleBarText:SetSize(190, 5)
StatPointUI.titleBarText:SetPoint("CENTER", 0, 0)
StatPointUI.titleBarText:SetText("|cffFFC125Attribute Points|r")
-- Generate row tables
local rowOffset = -30
local titleOffset = -100
local btnOffset = 40
local rowContent = {"Strength", "Agility", "Stamina", "Intellect", "Spirit"}
for k, v in pairs(rowContent) do
StatPointUI[v] = {}
-- Value (dummy, overwritten by server values
StatPointUI[v].Val = StatPointUI.mainFrame:CreateFontString(config.Prefix..v.."Val")
StatPointUI[v].Val:SetFont("Fonts\\FRIZQT__.TTF", 15)
if(k == 1) then
StatPointUI[v].Val:SetPoint("CENTER", StatPointUI.titleBar, "CENTER", 30, rowOffset)
else
StatPointUI[v].Val:SetPoint("CENTER", StatPointUI[rowContent[k-1]].Val, "CENTER", 0, rowOffset)
end
StatPointUI[v].Val:SetText("0")
-- Title
StatPointUI[v].Title = StatPointUI.mainFrame:CreateFontString(config.Prefix..v.."Title")
StatPointUI[v].Title:SetFont("Fonts\\FRIZQT__.TTF", 15)
StatPointUI[v].Title:SetPoint("LEFT", StatPointUI[v].Val, "LEFT", titleOffset, 0)
StatPointUI[v].Title:SetText(v..":")
-- Increase button
StatPointUI[v].Button = CreateFrame("Button", config.Prefix..v.."Button", StatPointUI.mainFrame)
StatPointUI[v].Button:SetSize(20, 20)
StatPointUI[v].Button:SetPoint("RIGHT", StatPointUI[v].Val, "RIGHT", btnOffset, 0)
StatPointUI[v].Button:EnableMouse(false)
StatPointUI[v].Button:Disable()
StatPointUI[v].Button:SetNormalTexture("Interface/BUTTONS/UI-SpellbookIcon-NextPage-Up")
StatPointUI[v].Button:SetHighlightTexture("Interface/BUTTONS/UI-Panel-MinimizeButton-Highlight")
StatPointUI[v].Button:SetPushedTexture("Interface/BUTTONS/UI-SpellbookIcon-NextPage-Down")
StatPointUI[v].Button:SetDisabledTexture("Interface/BUTTONS/UI-SpellbookIcon-NextPage-Disabled")
StatPointUI[v].Button:SetScript("OnMouseUp", function() SendClientRequest(config.Prefix, 2, k); PlaySound("UChatScrollButton"); end)
end
-- Attribute points left
StatPointUI.pointsLeftVal = StatPointUI.mainFrame:CreateFontString(config.Prefix.."PointsLeftVal")
StatPointUI.pointsLeftVal:SetFont("Fonts\\FRIZQT__.TTF", 15)
StatPointUI.pointsLeftVal:SetPoint("CENTER", StatPointUI[rowContent[#rowContent]].Val, "CENTER", 0, rowOffset)
StatPointUI.pointsLeftVal:SetText("0")
StatPointUI.pointsLeftTitle = StatPointUI.mainFrame:CreateFontString(config.Prefix.."PointsLeftVal")
StatPointUI.pointsLeftTitle:SetFont("Fonts\\FRIZQT__.TTF", 15)
StatPointUI.pointsLeftTitle:SetPoint("LEFT", StatPointUI.pointsLeftVal, "LEFT", titleOffset, 0)
StatPointUI.pointsLeftTitle:SetText("Points left:")
-- Reset button
StatPointUI.resetButton = CreateFrame("Button", config.Prefix.."ResetButton", StatPointUI.mainFrame)
StatPointUI.resetButton:SetSize(100, 25)
StatPointUI.resetButton:SetPoint("CENTER", StatPointUI.titleBar, "CENTER", 0, -220)
StatPointUI.resetButton:EnableMouse(true)
StatPointUI.resetButton:SetText("RESET")
StatPointUI.resetButton:SetNormalFontObject("GameFontNormalSmall")
local ntex = StatPointUI.resetButton:CreateTexture()
ntex:SetTexture("Interface/Buttons/UI-Panel-Button-Up")
ntex:SetTexCoord(0, 0.625, 0, 0.6875)
ntex:SetAllPoints()
StatPointUI.resetButton:SetNormalTexture(ntex)
local htex = StatPointUI.resetButton:CreateTexture()
htex:SetTexture("Interface/Buttons/UI-Panel-Button-Highlight")
htex:SetTexCoord(0, 0.625, 0, 0.6875)
htex:SetAllPoints()
StatPointUI.resetButton:SetHighlightTexture(htex)
local ptex = StatPointUI.resetButton:CreateTexture()
ptex:SetTexture("Interface/Buttons/UI-Panel-Button-Down")
ptex:SetTexCoord(0, 0.625, 0, 0.6875)
ptex:SetAllPoints()
StatPointUI.resetButton:SetPushedTexture(ptex)
StatPointUI.resetButton:SetScript("OnMouseUp", function() SendClientRequest(config.Prefix, 3); PlaySound("UChatScrollButton"); end)
-- Hook the character frame and hide/show with the char frame
PaperDollFrame:HookScript("OnShow", function() StatPointUI.mainFrame:Show() end)
PaperDollFrame:HookScript("OnHide", function() StatPointUI.mainFrame:Hide() end)
end
function OnCacheReceived(sender, argTable)
StatPointUI.cache = argTable[2]
local rowContent = {"Strength", "Agility", "Stamina", "Intellect", "Spirit"}
for i = 1, 5 do
StatPointUI[rowContent[i]].Val:SetText(StatPointUI.cache[i])
-- If a point has been spent in the stat row, set color to green, otherwise white
if(StatPointUI.cache[i] > 0) then
StatPointUI[rowContent[i]].Val:SetTextColor(0,1,0,1)
else
StatPointUI[rowContent[i]].Val:SetTextColor(1,1,1,1)
end
-- Disable buttons if the player has no more stats to spend
if(StatPointUI.cache[6] > 0) then
StatPointUI[rowContent[i]].Button:EnableMouse(true)
StatPointUI[rowContent[i]].Button:Enable()
else
StatPointUI[rowContent[i]].Button:EnableMouse(false)
StatPointUI[rowContent[i]].Button:Disable()
end
end
StatPointUI.pointsLeftVal:SetText(StatPointUI.cache[6])
end
RegisterServerResponses(config)
-- Event frame to trigger cache request on both login and reload
local EventFrame = CreateFrame("Frame")
EventFrame:RegisterEvent("PLAYER_LOGIN")
EventFrame:SetScript("OnEvent", function() StatPointUI.OnLogin() end)

View File

@ -0,0 +1,8 @@
## Interface: 30300
## Title: StatPointUI
## Notes: CMH Stat Point UI example script.
## Version: 1.0
## Author: Foereaper, Kaev
##RequiredDeps: CMH
StatPointUI.lua

View File

@ -0,0 +1,126 @@
-- Require the Server Message Handler
require("SMH")
local config = {
Prefix = "StatPointUI",
Functions = {
[1] = "OnFullCacheRequest",
[2] = "OnSpendPointRequest",
[3] = "OnStatResetRequest"
}
}
local StatPointUI = {
cache = {}
}
function StatPointUI.LoadData(guid)
local query = CharDBQuery("SELECT `str`, `agi`, `stam`, `int`, `spirit`, `points` FROM character_stats_extra WHERE `guid`="..guid..";");
if(query) then
StatPointUI.cache[guid] = {
query:GetUInt32(0), -- Strength
query:GetUInt32(1), -- Agility
query:GetUInt32(2), -- Stamina
query:GetUInt32(3), -- Intellect
query:GetUInt32(4), -- Spirit
query:GetUInt32(5) -- statpoints
}
else
StatPointUI.cache[guid] = {0, 0, 0, 0, 0, 0};
CharDBQuery("INSERT INTO character_stats_extra(`guid`, `str`, `agi`, `stam`, `int`, `spirit`, `points`) VALUES ("..guid..", 0, 0, 0, 0, 0, 0);");
end
end
function StatPointUI.OnLogin(event, player)
if not(StatPointUI.cache[player:GetGUIDLow()]) then
StatPointUI.LoadData(player:GetGUIDLow())
end
StatPointUI.SetStats(player:GetGUIDLow())
end
function StatPointUI.SetStats(guid, stat)
stat = stat or nil
local player = GetPlayerByGUID(guid)
local auras = {7464, 7471, 7477, 7468, 7474}
if(player) then
if stat == nil then
for i = 1, 5 do
local aura = player:GetAura(auras[i])
if (aura) then
aura:SetStackAmount(StatPointUI.cache[guid][i])
else
if(StatPointUI.cache[guid][i] > 0) then
player:AddAura(auras[i], player):SetStackAmount(StatPointUI.cache[guid][i])
end
end
end
else
local aura = player:GetAura(auras[stat])
if (aura) then
aura:SetStackAmount(StatPointUI.cache[guid][stat])
else
if(StatPointUI.cache[player:GetGUIDLow()][stat] > 0) then
player:AddAura(auras[stat], player):SetStackAmount(StatPointUI.cache[guid][stat])
end
end
end
end
end
function StatPointUI.ResetStats(guid)
local player = GetPlayerByGUID(guid)
local auras = {7464, 7471, 7477, 7468, 7474}
for _, aura in pairs(auras) do
player:RemoveAura(aura)
end
end
function StatPointUI.OnElunaStartup(event)
-- Re-cache online players' data in case of a hot reload
for _, player in pairs(GetPlayersInWorld()) do
StatPointUI.LoadData(player:GetGUIDLow())
end
end
function StatPointUI.OnPointSpent(guid, stat)
local inttostr = {"str", "agi", "stam", "int", "spirit"}
CharDBQuery("UPDATE character_stats_extra SET `"..inttostr[stat].."` = `"..inttostr[stat].."` + 1, `points`=`points`-1 WHERE `guid`="..guid..";")
StatPointUI.cache[guid][stat] = StatPointUI.cache[guid][stat]+1
StatPointUI.cache[guid][6] = StatPointUI.cache[guid][6]-1
StatPointUI.SetStats(guid, stat)
end
function StatPointUI.OnPointsReset(guid)
local total = 0
for _, points in pairs(StatPointUI.cache[guid]) do
total = total+points
end
CharDBQuery("UPDATE character_stats_extra SET `str`=0, `agi`=0, `stam`=0, `int`=0, `spirit`=0, `points`="..total.." WHERE `guid`="..guid..";");
StatPointUI.cache[guid] = {0, 0, 0, 0, 0, total};
StatPointUI.ResetStats(guid)
end
function OnFullCacheRequest(player, argTable)
player:SendServerResponse(config.Prefix, 1, StatPointUI.cache[player:GetGUIDLow()])
end
function OnSpendPointRequest(player, argTable)
if(StatPointUI.cache[player:GetGUIDLow()][6] > 0) then
-- Double check that the stat requested is actually a valid number
if(tonumber(argTable[2]) <= 5 and tonumber(argTable[2]) >= 0) then
StatPointUI.OnPointSpent(player:GetGUIDLow(), argTable[2])
end
else
player:SendBroadcastMessage("You have no points left!")
end
player:SendServerResponse(config.Prefix, 1, StatPointUI.cache[player:GetGUIDLow()])
end
function OnStatResetRequest(player, argTable)
StatPointUI.OnPointsReset(player:GetGUIDLow())
player:SendServerResponse(config.Prefix, 1, StatPointUI.cache[player:GetGUIDLow()])
end
RegisterPlayerEvent(3, StatPointUI.OnLogin)
RegisterServerEvent(33, StatPointUI.OnElunaStartup)
RegisterClientRequests(config)

View File

@ -0,0 +1,10 @@
-- Dumping structure for table character.character_stats_extra
CREATE TABLE IF NOT EXISTS `character_stats_extra` (
`guid` int(11) DEFAULT NULL,
`str` int(11) DEFAULT NULL,
`agi` int(11) DEFAULT NULL,
`stam` int(11) DEFAULT NULL,
`int` int(11) DEFAULT NULL,
`spirit` int(11) DEFAULT NULL,
`points` int(11) DEFAULT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;

View File

@ -1,2 +1,50 @@
# CSMH # CSMH
Client and Server Message Handler framework for communication between Eluna and WoW
### What is CSMH?
CSMH is a Client and Server Message Handler framework for communication between Eluna and the WoW interface. It has been tested on Mangos and TrinityCore 3.3.5a, but it will probably work for other versions as well.
CSMH consists of two parts, the Client Message Handler and the Server Message Handler respectively.
### How does this compare to AIO?
While AIO is the most used solution of this kind, it has its drawbacks as well. While it allows you to write all your code server-side, it also limits you to the Lua API only. AIO also sends the full addon code to the client on startup and reload, which is fairly network intensive. Upside of AIO is its ease of distribution compared to dedicated client-side addons.
CSMH is only meant to transport data between the client and the server, and is therefore not as network intensive as AIO. You write your server-side code on the server, and you distribute your client-side code either as an addon, or in a patch. This allows you to use XML and templates, as well as the full Lua API.
Both AIO and CSMH uses smallfolk for serialization, and is compatible with each other. You can use both AIO and CSMH in the same project.
### What does CSMH do and *not* do?
CSMH intentionally does not do certain things, primarily for ease of integration and personal preferences around implementations like; Flood protection, data validation, packet filtering etc.
CSMH **does** verify the sender and recipient of a message, to prevent messages being sent and accepted on someone else's behalf.
CSMH **does not** natively check and verify what is being sent to and from the client and server. It is up to you to sanity check data being sent back and forth. A good rule of thumb is to never inherently trust data being sent to the server from the client, you need to verify data before accepting it.
CSMH also **does not** have any form of built in flood protection. This is again up to you to decide on an implementation of your choice.
### How do I use CSMH?
I would recommend going through the examples provided in this repo to get a feel for how registering, sending and receiving data on both the client and the server works. A full API and how-to will be posted soon™.
## Installation:
### Server:
- Copy everything from the Server directory to your Eluna scripts directory. That's it!
### Client:
The CMH can be distributed either as a stand-alone addon, or through a patch. Files are provided for both solutions in the Client directory, but be aware of the differences:
#### Addon:
- Copy **CMH.Lua**, **CMH.toc** and **smallfolk.lua** to **Interface\AddOns\CMH**
#### MPQ Patch:
- Copy **CMH.Lua**, **FrameXML.toc** and **smallfolk.lua** to **Interface\FrameXML**
## API:
Soon™
## Credits:
- [Stoneharry](https://github.com/stoneharry)
- [Terrorblade](https://github.com/Terrorblade)
- Kaev
- [Rochet / AIO](https://github.com/Rochet2)
- [Eluna](https://github.com/ElunaLuaEngine/Eluna)
- [smallfolk](https://github.com/gvx/Smallfolk)

50
README.md.bak Normal file
View File

@ -0,0 +1,50 @@
# CSMH
### What is CSMH?
CSMH is a Client and Server Message Handler framework for communication between Eluna and the WoW interface. It has been tested on Mangos and TrinityCore 3.3.5a, but it will probably work for other versions as well.
CSMH consists of two parts, the Client Message Handler and the Server Message Handler respectively.
### How does this compare to AIO?
While AIO is the most used solution of this kind, it has its drawbacks as well. While it allows you to write all your code server-side, it also limits you to the Lua API only. AIO also sends the full addon code to the client on startup and reload, which is fairly network intensive. Upside of AIO is its ease of distribution compared to dedicated client-side addons.
CSMH is only meant to transport data between the client and the server, and is therefore not as network intensive as AIO. You write your server-side code on the server, and you distribute your client-side code either as an addon, or in a patch. This allows you to use XML and templates, as well as the full Lua API.
Both AIO and CSMH uses smallfolk for serialization, and is compatible with each other. You can use both AIO and CSMH in the same project.
### What does CSMH do and *not* do?
CSMH intentionally does not do certain things, primarily for ease of integration and personal preferences around implementations like; Flood protection, data validation, packet filtering etc.
CSMH **does** verify the sender and recipient of a message, to prevent messages being sent and accepted on someone else's behalf.
CSMH **does not** natively check and verify what is being sent to and from the client and server. It is up to you to sanity check data being sent back and forth. A good rule of thumb is to never inherently trust data being sent to the server from the client, you need to verify data before accepting it.
CSMH also **does not** have any form of built in flood protection. This is again up to you to decide on an implementation of your choice.
### How do I use CSMH?
I would recommend going through the examples provided in this repo to get a feel for how registering, sending and receiving data on both the client and the server works. A full API and how-to will be posted soon™.
## Installation:
### Server:
- Copy everything from the Server directory to your Eluna scripts directory. That's it!
### Client:
CSMH can be distributed either as a stand-alone addon, or through a patch. Files are provided for both solutions in the Client directory, but be aware of the differences:
#### Addon:
- Copy **CMH.Lua**, **CMH.toc** and **smallfolk.lua** to **Interface\AddOns\CMH**
#### MPQ Patch:
- Copy **CMH.Lua**, **FrameXML.toc** and **smallfolk.lua** to **Interface\FrameXML**
## API:
Soon™
## Credits:
- [Stoneharry](https://github.com/stoneharry)
- [Terrorblade](https://github.com/Terrorblade)
- Kaev
- [Rochet / AIO](https://github.com/Rochet2)
- [Eluna](https://github.com/ElunaLuaEngine/Eluna)
- [smallfolk](https://github.com/gvx/Smallfolk)

218
Server/.lib/smallfolk.lua Normal file
View File

@ -0,0 +1,218 @@
local M = {}
Smallfolk = M
local expect_object, dump_object
local error, tostring, pairs, type, floor, huge, concat = error, tostring, pairs, type, math.floor, math.huge, table.concat
local dump_type = {}
function dump_type:string(nmemo, memo, acc)
local nacc = #acc
acc[nacc + 1] = '"'
acc[nacc + 2] = self:gsub('"', '""')
acc[nacc + 3] = '"'
return nmemo
end
function dump_type:number(nmemo, memo, acc)
acc[#acc + 1] = ("%.17g"):format(self)
return nmemo
end
function dump_type:table(nmemo, memo, acc)
--[[
if memo[self] then
acc[#acc + 1] = '@'
acc[#acc + 1] = tostring(memo[self])
return nmemo
end
nmemo = nmemo + 1
]]
memo[self] = nmemo
acc[#acc + 1] = '{'
local nself = #self
for i = 1, nself do -- don't use ipairs here, we need the gaps
nmemo = dump_object(self[i], nmemo, memo, acc)
acc[#acc + 1] = ','
end
for k, v in pairs(self) do
if type(k) ~= 'number' or floor(k) ~= k or k < 1 or k > nself then
nmemo = dump_object(k, nmemo, memo, acc)
acc[#acc + 1] = ':'
nmemo = dump_object(v, nmemo, memo, acc)
acc[#acc + 1] = ','
end
end
acc[#acc] = acc[#acc] == '{' and '{}' or '}'
return nmemo
end
function dump_object(object, nmemo, memo, acc)
if object == true then
acc[#acc + 1] = 't'
elseif object == false then
acc[#acc + 1] = 'f'
elseif object == nil then
acc[#acc + 1] = 'n'
elseif object ~= object then
if (''..object):sub(1,1) == '-' then
acc[#acc + 1] = 'N'
else
acc[#acc + 1] = 'Q'
end
elseif object == huge then
acc[#acc + 1] = 'I'
elseif object == -huge then
acc[#acc + 1] = 'i'
else
local t = type(object)
if not dump_type[t] then
error('cannot dump type ' .. t)
end
return dump_type[t](object, nmemo, memo, acc)
end
return nmemo
end
function M.dumps(object)
local nmemo = 0
local memo = {}
local acc = {}
dump_object(object, nmemo, memo, acc)
return concat(acc)
end
local function invalid(i)
error('invalid input at position ' .. i)
end
local nonzero_digit = {['1'] = true, ['2'] = true, ['3'] = true, ['4'] = true, ['5'] = true, ['6'] = true, ['7'] = true, ['8'] = true, ['9'] = true}
local is_digit = {['0'] = true, ['1'] = true, ['2'] = true, ['3'] = true, ['4'] = true, ['5'] = true, ['6'] = true, ['7'] = true, ['8'] = true, ['9'] = true}
local function expect_number(string, start)
local i = start
local head = string:sub(i, i)
if head == '-' then
i = i + 1
head = string:sub(i, i)
end
if nonzero_digit[head] then
repeat
i = i + 1
head = string:sub(i, i)
until not is_digit[head]
elseif head == '0' then
i = i + 1
head = string:sub(i, i)
else
invalid(i)
end
if head == '.' then
local oldi = i
repeat
i = i + 1
head = string:sub(i, i)
until not is_digit[head]
if i == oldi + 1 then
invalid(i)
end
end
if head == 'e' or head == 'E' then
i = i + 1
head = string:sub(i, i)
if head == '+' or head == '-' then
i = i + 1
head = string:sub(i, i)
end
if not is_digit[head] then
invalid(i)
end
repeat
i = i + 1
head = string:sub(i, i)
until not is_digit[head]
end
return tonumber(string:sub(start, i - 1)), i
end
local expect_object_head = {
t = function(string, i) return true, i end,
f = function(string, i) return false, i end,
n = function(string, i) return nil, i end,
Q = function(string, i) return -(0/0), i end,
N = function(string, i) return 0/0, i end,
I = function(string, i) return 1/0, i end,
i = function(string, i) return -1/0, i end,
['"'] = function(string, i)
local nexti = i - 1
repeat
nexti = string:find('"', nexti + 1, true) + 1
until string:sub(nexti, nexti) ~= '"'
return string:sub(i, nexti - 2):gsub('""', '"'), nexti
end,
['0'] = function(string, i)
return expect_number(string, i - 1)
end,
['{'] = function(string, i, tables)
local nt, k, v = {}
local j = 1
tables[#tables + 1] = nt
if string:sub(i, i) == '}' then
return nt, i + 1
end
while true do
k, i = expect_object(string, i, tables)
if string:sub(i, i) == ':' then
v, i = expect_object(string, i + 1, tables)
nt[k] = v
else
nt[j] = k
j = j + 1
end
local head = string:sub(i, i)
if head == ',' then
i = i + 1
elseif head == '}' then
return nt, i + 1
else
invalid(i)
end
end
end,
--[[
['@'] = function(string, i, tables)
local match = string:match('^%d+', i)
local ref = tonumber(match)
if tables[ref] then
return tables[ref], i + #match
end
invalid(i)
end,
]]
}
expect_object_head['1'] = expect_object_head['0']
expect_object_head['2'] = expect_object_head['0']
expect_object_head['3'] = expect_object_head['0']
expect_object_head['4'] = expect_object_head['0']
expect_object_head['5'] = expect_object_head['0']
expect_object_head['6'] = expect_object_head['0']
expect_object_head['7'] = expect_object_head['0']
expect_object_head['8'] = expect_object_head['0']
expect_object_head['9'] = expect_object_head['0']
expect_object_head['-'] = expect_object_head['0']
expect_object_head['.'] = expect_object_head['0']
expect_object = function(string, i, tables)
local head = string:sub(i, i)
if expect_object_head[head] then
return expect_object_head[head](string, i + 1, tables)
end
invalid(i)
end
function M.loads(string, maxsize)
if #string > (maxsize or 10000) then
error 'input too large'
end
return (expect_object(string, 1, {}))
end
return M

145
Server/SMH.lua Normal file
View File

@ -0,0 +1,145 @@
local smallfolk = smallfolk or require("smallfolk")
local SMH = {}
local links = {}
function SMH.OnReceive(event, sender, _type, prefix, _, target)
if not sender or not target or not sender.GetName or not target.GetName or type(sender) ~= "userdata" or type(target) ~= "userdata" then
return
end
if sender:GetName() == target:GetName() and _type == 7 then
local source, functionId, link, linkCount, MSG = prefix:match("(%D)(%d%d%d)(%d%d)(%d%d)(.+)");
if not source or not functionId or not link or not linkCount or not MSG then
return
end
if(source == "C") then
functionId, link, linkCount = tonumber(functionId), tonumber(link), tonumber(linkCount);
links[sender:GetGUIDLow()] = links[sender:GetGUIDLow()] or {}
links[sender:GetGUIDLow()][functionId] = links[sender:GetGUIDLow()][functionId] or {count = 0};
links[sender:GetGUIDLow()][functionId][link] = MSG;
links[sender:GetGUIDLow()][functionId].count = links[sender:GetGUIDLow()][functionId].count + 1;
if (links[sender:GetGUIDLow()][functionId].count ~= linkCount) then
return
end
local fullMessage = table.concat(links[sender:GetGUIDLow()][functionId]);
links[sender:GetGUIDLow()][functionId] = {count = 0};
local VarTable = ParseMessage(fullMessage)
if not VarTable then
return
end
if not(SMH[VarTable[1]]) then
return
end
local func = SMH[VarTable[1]][functionId]
if func then
_G[func](sender, VarTable)
end
return
end
end
end
RegisterServerEvent(30, SMH.OnReceive)
function RegisterClientRequests(config)
-- If a config table with the Prefix already exists, abort loading it into the register.
if(SMH[config.Prefix]) then
return;
end
-- Create subtable for PrefixName
SMH[config.Prefix] = {}
-- Insert function ID and function name into the register table.
for functionId, functionName in pairs(config.Functions) do
SMH[config.Prefix][functionId] = functionName
end
end
function ParseMessage(str)
local output = {}
local valTemp = {}
local typeTemp = {}
local delim = {"", "", "", "", ""}
local valMatch = "[^"..table.concat(delim).."]+"
local typeMatch = "["..table.concat(delim).."]+"
-- Get values
for value in str:gmatch(valMatch) do
table.insert(valTemp, value)
end
-- Get type from delimiter
for varType in str:gmatch(typeMatch) do
for k, v in pairs(delim) do
if(v == varType) then
table.insert(typeTemp, k)
end
end
end
-- Convert value to correct type
for k, v in pairs(valTemp) do
local varType = typeTemp[k]
if(varType == 3) then -- Ints
v = tonumber(v)
elseif(varType == 4) then -- Tables
v = smallfolk.loads(v)
elseif(varType == 5) then -- Booleans
if(v == "true") then v = true else v = false end
end
table.insert(output, v)
end
valTemp = nil
typeTemp = nil
return output
end
function Player:SendServerResponse(prefix, functionId, ...)
-- ♠ = Prefix prefix
-- ♥ = ArgumentPrefix for Strings
-- ♚ = ArgumentPrefix for Ints
-- ♛ = ArgumentPrefix for Tables
-- ♜ = ArgumentPrefix for Boolean
local arg = {...}
local splitLength = 230
local msg = "" .. prefix
for _, v in pairs(arg) do
if(type(v) == "string") then
msg = msg .. ""
elseif(type(v) == "number") then
msg = msg .. ""
elseif(type(v) == "table") then
-- use Smallfolk to convert table structure to string
v = smallfolk.dumps(v)
msg = msg .. ""
elseif(type(v) == "boolean") then
v = tostring(v)
msg = msg .. ""
end
msg = msg .. v
end
local splits = math.ceil(msg:len() / splitLength)
local send
local counter = 1
for i=1, msg:len(), splitLength do
send = string.format("%01s%02d%03d%03d", "S", functionId, counter, splits)
if ((i + splitLength) > msg:len()) then
send = send .. msg:sub(i, msg:len())
else
send = send .. msg:sub(i, i + splitLength - 1)
end
counter = counter + 1
self:SendAddonMessage(send, "", 7, self)
end
end

144
Server/SMH.lua.bak Normal file
View File

@ -0,0 +1,144 @@
local smallfolk = smallfolk or require("smallfolk")
local SMH = {}
local links = {}
function SMH.OnReceive(event, sender, _type, prefix, _, target)
if not sender or not target or not sender.GetName or not target.GetName or type(sender) ~= "userdata" or type(target) ~= "userdata" then
return
end
if sender:GetName() == target:GetName() and _type == 7 then
local source, functionId, link, linkCount, MSG = prefix:match("(%D)(%d%d%d)(%d%d)(%d%d)(.+)");
if not source or not functionId or not link or not linkCount or not MSG then
return
end
if(source == "C") then
functionId, link, linkCount = tonumber(functionId), tonumber(link), tonumber(linkCount);
links[sender:GetGUIDLow()] = links[sender:GetGUIDLow()] or {}
links[sender:GetGUIDLow()][functionId] = links[sender:GetGUIDLow()][functionId] or {count = 0};
links[sender:GetGUIDLow()][functionId][link] = MSG;
links[sender:GetGUIDLow()][functionId].count = links[sender:GetGUIDLow()][functionId].count + 1;
if (links[sender:GetGUIDLow()][functionId].count ~= linkCount) then
return
end
local fullMessage = table.concat(links[sender:GetGUIDLow()][functionId]);
links[sender:GetGUIDLow()][functionId] = {count = 0};
local VarTable = ParseMessage(fullMessage)
if not VarTable then
return
end
if not(SMH[VarTable[1]]) then
return
end
local func = SMH[VarTable[1]][functionId]
if func then
_G[func](sender, VarTable)
end
return
end
end
end
RegisterServerEvent(30, SMH.OnReceive)
function RegisterClientRequests(config)
-- If a config table with the Prefix already exists, abort loading it into the register.
if(SMH[config.Prefix]) then
return;
end
-- Create subtable for PrefixName
SMH[config.Prefix] = {}
-- Insert function ID and function name into the register table.
for functionId, functionName in pairs(config.Functions) do
SMH[config.Prefix][functionId] = functionName
end
end
function ParseMessage(str)
local output = {}
local valTemp = {}
local typeTemp = {}
local delim = {"", "", "", "", ""}
local valMatch = "[^"..table.concat(delim).."]+"
local typeMatch = "["..table.concat(delim).."]+"
-- Get values
for value in str:gmatch(valMatch) do
table.insert(valTemp, value)
end
-- Get type from delimiter
for varType in str:gmatch(typeMatch) do
for k, v in pairs(delim) do
if(v == varType) then
table.insert(typeTemp, k)
end
end
end
-- Convert value to correct type
for k, v in pairs(valTemp) do
local varType = typeTemp[k]
if(varType == 3) then -- Ints
v = tonumber(v)
elseif(varType == 4) then -- Tables
v = smallfolk.loads(v)
elseif(varType == 5) then -- Booleans
if(v == "true") then v = true else v = false end
end
table.insert(output, v)
end
valTemp = nil
typeTemp = nil
return output
end
function Player:SendServerResponse(prefix, functionId, ...)
-- ♠ = Prefix prefix
-- ♥ = ArgumentPrefix for Strings
-- ♚ = ArgumentPrefix for Ints
-- ♛ = ArgumentPrefix for Tables
-- ♜ = ArgumentPrefix for Boolean
local arg = {...}
local splitLength = 230
local msg = "" .. prefix
for _, v in pairs(arg) do
if(type(v) == "string") then
msg = msg .. ""
elseif(type(v) == "number") then
msg = msg .. ""
elseif(type(v) == "table") then
-- use Smallfolk to convert table structure to string
v = smallfolk.dumps(v)
msg = msg .. ""
elseif(type(v) == "boolean") then
v = tostring(v)
msg = msg .. ""
end
msg = msg .. v
end
local splits = math.ceil(msg:len() / splitLength)
local send
local counter = 1
for i=1, msg:len(), splitLength do
send = string.format("%01s%02d%03d%03d", "S", functionId, counter, splits)
if ((i + splitLength) > msg:len()) then
send = send .. msg:sub(i, msg:len())
else
send = send .. msg:sub(i, i + splitLength - 1)
end
counter = counter + 1
self:SendAddonMessage(send, "", 7, self)
end
end