добавлен AIO и магазин

This commit is contained in:
2025-08-21 02:41:27 +04:00
parent bf6283a63e
commit 6dcd1cd327
25 changed files with 8953 additions and 0 deletions
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,431 @@
local ServiceData = {}
local LinkData = {}
local NavData = {}
local CurrencyData = {}
local CreatureDisplays = {}
-- compatibility unpack function for Lua versions without unpack
if not _G["unpack"] then
function unpack(t)
return table.unpack(t)
end
end
local KEYS = {
currency = {
id = 0,
currencyType = 1,
name = 2,
icon = 3,
data = 4,
tooltip = 5
},
category = {
id = 1,
name = 2,
icon = 3,
requiredRank = 4,
flags = 5,
enabled = 6
},
service = {
id = 0,
serviceType = 1,
name = 2,
tooltipName = 3,
tooltipType = 4,
tooltipText = 5,
icon = 6,
price = 7,
currency = 8,
hyperlink = 9,
displayOrEntry = 10,
discount = 11,
flags = 12,
reward_1 = 13,
reward_2 = 14,
reward_3 = 15,
reward_4 = 16,
reward_5 = 17,
reward_6 = 18,
reward_7 = 19,
reward_8 = 20,
rewardCount_1 = 21,
rewardCount_2 = 22,
rewardCount_3 = 23,
rewardCount_4 = 24,
rewardCount_5 = 25,
rewardCount_6 = 26,
rewardCount_7 = 27,
rewardCount_8 = 28,
new = 29,
enabled = 30
},
}
function GetDataStructKeys()
return KEYS;
end
function NavData.Load()
NavData.Cache = {};
local Query = WorldDBQuery("SELECT * FROM store.store_categories")
if (Query) then
repeat
table.insert(NavData.Cache, {
Query:GetUInt32(KEYS.category.id - 1),
Query:GetString(KEYS.category.name - 1),
Query:GetString(KEYS.category.icon - 1),
Query:GetUInt32(KEYS.category.requiredRank - 1),
Query:GetUInt32(KEYS.category.flags - 1),
Query:GetUInt32(KEYS.category.enabled - 1)
})
until not Query:NextRow()
end
end
function CurrencyData.Load()
CurrencyData.Cache = {};
local Query = WorldDBQuery("SELECT * FROM store.store_currencies")
if (Query) then
repeat
CurrencyData.Cache[Query:GetUInt32(0)] = { -- id
Query:GetUInt32(1), -- type
Query:GetString(2), -- name
Query:GetString(3), -- icon
Query:GetUInt32(4), -- data
Query:GetString(5), -- tooltip
}
until not Query:NextRow()
end
end
function ServiceData.Load()
ServiceData.Cache = {};
local Query = WorldDBQuery("SELECT * FROM store.store_services;");
if (Query) then
repeat
if (Query:GetUInt32(KEYS.service.enabled) == 1) then
ServiceData.Cache[Query:GetUInt32(KEYS.service.id)] = {
Query:GetUInt32(KEYS.service.serviceType),
Query:GetString(KEYS.service.name),
Query:GetString(KEYS.service.tooltipName),
Query:GetString(KEYS.service.tooltipType),
Query:GetString(KEYS.service.tooltipText),
Query:GetString(KEYS.service.icon),
Query:GetUInt32(KEYS.service.price),
Query:GetUInt32(KEYS.service.currency),
Query:GetUInt32(KEYS.service.hyperlink),
Query:GetUInt32(KEYS.service.displayOrEntry),
Query:GetUInt32(KEYS.service.discount),
Query:GetUInt32(KEYS.service.flags),
Query:GetUInt32(KEYS.service.reward_1),
Query:GetUInt32(KEYS.service.reward_2),
Query:GetUInt32(KEYS.service.reward_3),
Query:GetUInt32(KEYS.service.reward_4),
Query:GetUInt32(KEYS.service.reward_5),
Query:GetUInt32(KEYS.service.reward_6),
Query:GetUInt32(KEYS.service.reward_7),
Query:GetUInt32(KEYS.service.reward_8),
Query:GetUInt32(KEYS.service.rewardCount_1),
Query:GetUInt32(KEYS.service.rewardCount_2),
Query:GetUInt32(KEYS.service.rewardCount_3),
Query:GetUInt32(KEYS.service.rewardCount_4),
Query:GetUInt32(KEYS.service.rewardCount_5),
Query:GetUInt32(KEYS.service.rewardCount_6),
Query:GetUInt32(KEYS.service.rewardCount_7),
Query:GetUInt32(KEYS.service.rewardCount_8),
Query:GetUInt32(KEYS.service.new),
}
end
until not Query:NextRow()
end
end
function CreatureDisplays.Load()
CreatureDisplays.Cache = {}
local tmp = ""
-- fetch all entries for creature cache that needs to be sent to the client for preview
-- store these as a comma separated string to be used in the sql query below
for k, v in pairs(ServiceData.Cache) do
if ((v[KEYS.service.serviceType] == 3 or v[KEYS.service.serviceType] == 4) and v[KEYS.service.displayOrEntry] > 0) then
-- first entry shouldn't append a comma
if(tmp ~= "") then
tmp = tmp .. ", "
end
-- append creature entry to the query string
tmp = tmp .. v[KEYS.service.displayOrEntry]
end
end
local Query = nil
if(GetCoreName() == "TrinityCore") then
Query = WorldDBQuery("SELECT entry, `name`, subname, IconName, type_flags, `type`, family, `rank`, KillCredit1, KillCredit2, HealthModifier, ManaModifier, RacialLeader, MovementType, modelId1, modelId2, modelId3, modelId4 FROM creature_template WHERE entry IN ("..tmp..");")
elseif(GetCoreName() == "AzerothCore") then
Query = WorldDBQuery("SELECT entry, `name`, subname, IconName, type_flags, `type`, family, `rank`, KillCredit1, KillCredit2, HealthModifier, ManaModifier, RacialLeader, MovementType FROM creature_template WHERE entry IN ("..tmp..");")
end
-- get all info and store it in the CreatureDisplays cache
if(Query) then
repeat
local model1, model2, model3, model4 = 0, 0, 0, 0
if(GetCoreName() == "TrinityCore") then
model1 = Query:GetUInt32(14)
model2 = Query:GetUInt32(15)
model3 = Query:GetUInt32(16)
model4 = Query:GetUInt32(17)
end
local entry = Query:GetUInt32(0)
CreatureDisplays.Cache[entry] = {
entry,Query:GetString(1),
Query:GetString(2),
Query:GetString(3),
Query:GetUInt32(4),
Query:GetUInt32(5),
Query:GetUInt32(6),
Query:GetUInt32(7),
Query:GetUInt32(8),
Query:GetUInt32(9),
model1,
model2,
model3,
model4,
Query:GetFloat(10),
Query:GetFloat(11),
Query:GetUInt32(12),
Query:GetUInt32(13)
}
until not Query:NextRow()
end
-- stupid model query for AC
if(GetCoreName() == "AzerothCore") then
local ModelQuery = WorldDBQuery("SELECT CreatureID, Idx, CreatureDisplayID FROM creature_template_model WHERE CreatureID IN ("..tmp..");")
if(ModelQuery) then
repeat
local entry = ModelQuery:GetUInt32(0)
if(CreatureDisplays.Cache[entry]) then
local key = ModelQuery:GetUInt32(1) + 11
CreatureDisplays.Cache[entry][key] = ModelQuery:GetUInt32(2)
end
until not ModelQuery:NextRow()
end
end
end
function LinkData.Load()
LinkData.Cache = {};
local Query = WorldDBQuery("SELECT * FROM store.store_category_service_link;");
if (Query) then
repeat
table.insert(LinkData.Cache, {Query:GetUInt32(0), Query:GetUInt32(1)})
until not Query:NextRow()
end
end
function GetServiceData()
return ServiceData.Cache;
end
function GetLinkData()
return LinkData.Cache;
end
function GetNavData()
return NavData.Cache;
end
function GetCurrencyData()
return CurrencyData.Cache;
end
ServiceData.Load()
LinkData.Load()
NavData.Load()
CurrencyData.Load()
CreatureDisplays.Load()
local SoundEffects = {
notEnoughMoney = {
[1] = { -- Human
[0] = 1908,
[1] = 2032,
},
[2] = { -- Orc
[0] = 2319,
[1] = 2356,
},
[3] = { -- Dwarf
[0] = 1598,
[1] = 1669,
},
[4] = { -- Night elf
[0] = 2151,
[1] = 2262,
},
[5] = { -- Undead
[0] = 2096,
[1] = 2207,
},
[6] = { -- Tauren
[0] = 2426,
[1] = 2462,
},
[7] = { -- Gnome
[0] = 1724,
[1] = 1779,
},
[8] = { -- Troll
[0] = 1835,
[1] = 1945,
},
[10] = { -- Blood elf
[0] = 9583,
[1] = 9584,
},
[11] = { -- Draenei
[0] = 9498,
[1] = 9499,
}
},
cantLearn = {
[1] = { -- Human
[0] = 2622,
[1] = 2585,
},
[2] = { -- Orc
[0] = 2949,
[1] = 2966,
},
[3] = { -- Dwarf
[0] = 2605,
[1] = 2893,
},
[4] = { -- Night elf
[0] = 2644,
[1] = 2661,
},
[5] = { -- Undead
[0] = 2633,
[1] = 2597,
},
[6] = { -- Tauren
[0] = 2616,
[1] = 2918,
},
[7] = { -- Gnome
[0] = 2882,
[1] = 2907,
},
[8] = { -- Troll
[0] = 2611,
[1] = 2977,
},
[10] = { -- Blood elf
[0] = 9571,
[1] = 9572,
},
[11] = { -- Draenei
[0] = 9487,
[1] = 9486,
}
},
cantUse = {
[1] = { -- Human
[0] = 1918,
[1] = 2042,
},
[2] = { -- Orc
[0] = 2329,
[1] = 2384,
},
[3] = { -- Dwarf
[0] = 1653,
[1] = 1696,
},
[4] = { -- Night elf
[0] = 2161,
[1] = 2272,
},
[5] = { -- Undead
[0] = 2106,
[1] = 2217,
},
[6] = { -- Tauren
[0] = 2483,
[1] = 2482,
},
[7] = { -- Gnome
[0] = 1753,
[1] = 1808,
},
[8] = { -- Troll
[0] = 1863,
[1] = 1987,
},
[10] = { -- Blood elf
[0] = 9611,
[1] = 9612,
},
[11] = { -- Draenei
[0] = 9535,
[1] = 9536,
}
}
}
function GetSoundEffect(key, race, gender)
local effect = 0
if (SoundEffects[key][race][gender]) then
effect = SoundEffects[key][race][gender]
end
return effect
end
local function SendCreatureQueryResponse(player, data)
local packet = CreatePacket(97, 100)
packet:WriteULong(data[1])
packet:WriteString(data[2] or "")
packet:WriteUByte(0)
packet:WriteUByte(0)
packet:WriteUByte(0)
packet:WriteString(data[3] or "")
packet:WriteString(data[4] or "")
packet:WriteULong(data[5])
packet:WriteULong(data[6])
packet:WriteULong(data[7])
packet:WriteULong(data[8])
packet:WriteULong(data[9])
packet:WriteULong(data[10])
packet:WriteULong(data[11])
packet:WriteULong(data[12])
packet:WriteULong(data[13])
packet:WriteULong(data[14])
packet:WriteFloat(data[15])
packet:WriteFloat(data[16])
packet:WriteUByte(data[17])
packet:WriteULong(0)
packet:WriteULong(0)
packet:WriteULong(0)
packet:WriteULong(0)
packet:WriteULong(0)
packet:WriteULong(0)
packet:WriteULong(data[18])
player:SendPacket(packet)
end
local function OnLogin(event, player)
for _, v in pairs(CreatureDisplays.Cache) do
SendCreatureQueryResponse(player, v)
end
end
RegisterPlayerEvent(3, OnLogin)
+375
View File
@@ -0,0 +1,375 @@
-- The below config options can be changed to suit your needs.
-- Anything not in the config options requires changes to the code below,
-- do so at your own discretion.
local CONFIG = {
maxLevel = 80, -- Character max level of server
mailSenderGUID = 1, -- GUID of the character shown as sender of purchase mails
strings = {
-- Currency name is appended to the end of this string
insufficientFunds = "You don't have enough",
-- Type is appended, ie. title, mount, pet etc.
alreadyKnown = "You already have this",
tooHighLevel = "Your level is too high",
mailBody = "Thank you for your purchase!",
-- The service name is prefixed to this message
successfulPurchase = "successfully purchased!"
}
}
--------------------
local AIO = AIO or require("AIO") and require("Store_DataStruct")
local CURRENCY_TYPES = {
[1] = "GOLD",
[2] = "ITEM_TOKEN",
[3] = "SERVER_HANDLED"
}
local SHOP_UI = {
serviceHandlers = {
[1] = "ItemHandler", -- Okay
[2] = "GoldHandler", -- Okay
[3] = "MountHandler", -- Okay
[4] = "PetHandler", -- Okay
[5] = "BuffHandler", -- Okay
[6] = "UnusedHandler", -- UNUSED
[7] = "ServiceHandler", -- Okay
[8] = "LevelHandler", -- Okay
[9] = "TitleHandler", -- Okay
}
}
local KEYS = GetDataStructKeys();
local StoreHandler = AIO.AddHandlers("STORE_SERVER", {})
function StoreHandler.FrameData(player)
AIO.Handle(player, "STORE_CLIENT", "FrameData", GetServiceData(), GetLinkData(), GetNavData(), GetCurrencyData(), player:GetGMRank())
end
function StoreHandler.UpdateCurrencies(player)
local tmp = {}
for currencyId, currency in pairs(GetCurrencyData()) do
local val = 0
local currencyTypeText = CURRENCY_TYPES[currency[KEYS.currency.currencyType]]
-- Handle the different currency types
if(currencyTypeText == "GOLD") then
val = math.floor(player:GetCoinage() / 10000)
end
if(currencyTypeText == "ITEM_TOKEN") then
val = player:GetItemCount(currency[KEYS.currency.data])
end
if(currencyTypeText == "SERVER_HANDLED") then
-- Add your custom handling here for retreiving your server handled currencies
end
-- If value is larger than 10k then truncate to make sure it fits within the shop frame
if(val > 9999) then
val = "9999+"
end
table.insert(tmp, val)
end
AIO.Handle(player, "STORE_CLIENT", "UpdateCurrencies", tmp)
end
function StoreHandler.Purchase(player, serviceId)
local services = GetServiceData()
-- See if the requested service exists
if(services[serviceId])then
-- add the id to the service subtable so we don't have to pass an additional variable around
services[serviceId].ID = serviceId
local typeId = services[serviceId][KEYS.service.serviceType]
local serviceHandler = SHOP_UI[SHOP_UI.serviceHandlers[typeId]]
if(serviceHandler) then
local success = serviceHandler(player, services[serviceId])
if(success) then
-- If purchase is successful, update the players currencies in UI and log purchase
StoreHandler.UpdateCurrencies(player)
SHOP_UI.LogPurchase(player, services[serviceId])
-- Play success sound
player:PlayDirectSound(120, player)
-- Send success toast
player:SendAreaTriggerMessage(services[serviceId][KEYS.service.name] .. " "..CONFIG.strings.successfulPurchase)
end
end
end
end
-- Helper functions
function SHOP_UI.DeductCurrency(player, currencyId, amount)
local currency = GetCurrencyData()
local currencyType = currency[currencyId][KEYS.currency.currencyType]
local currencyName = currency[currencyId][KEYS.currency.name]
local currencyData = currency[currencyId][KEYS.currency.data]
-- Gold handling
if(CURRENCY_TYPES[currencyType] == "GOLD") then
if(player:GetCoinage() < amount * 10000) then
player:SendAreaTriggerMessage("|cFFFF0000"..CONFIG.strings.insufficientFunds.." "..currencyName.."|r")
player:PlayDirectSound(GetSoundEffect("notEnoughMoney", player:GetRace(), player:GetGender()), player)
return false
end
player:SetCoinage(player:GetCoinage() - (amount * 10000))
end
-- Token handling
if(CURRENCY_TYPES[currencyType] == "ITEM_TOKEN") then
if not(player:HasItem(currencyData, amount)) then
player:SendAreaTriggerMessage("|cFFFF0000"..CONFIG.strings.insufficientFunds.." "..currencyName.."|r")
player:PlayDirectSound(GetSoundEffect("notEnoughMoney", player:GetRace(), player:GetGender()), player)
return false
end
player:RemoveItem(currencyData, amount)
end
-- Other special handlingm you have to add your own integration here.
if(CURRENCY_TYPES[currencyType] == "SERVER_HANDLED") then
return false
end
return true
end
function SHOP_UI.LogPurchase(player, data)
local currency, amount = data[KEYS.service.currency], data[KEYS.service.price] - data[KEYS.service.discount]
WorldDBExecute("INSERT INTO store.store_logs(account, guid, serviceId, currencyId, cost) VALUES("..player:GetAccountId()..", "..player:GetGUIDLow()..", "..data.ID..", "..currency..", "..amount..");")
end
-- ITEMS
function SHOP_UI.ItemHandler(player, data)
local currency, amount = data[KEYS.service.currency], data[KEYS.service.price] - data[KEYS.service.discount]
-- Deduct currency
local deducted = SHOP_UI.DeductCurrency(player, currency, amount)
-- If currency was not deducted from the player, abort and send message
if not(deducted) then
return false
end
-- Fetch all the rewards and store them temporarily
local items = {}
for i = 0, 7 do
if(data[KEYS.service.reward_1+i] > 0 and data[KEYS.service.rewardCount_1+i] > 0) then
table.insert(items, data[KEYS.service.reward_1+i])
table.insert(items, data[KEYS.service.rewardCount_1+i])
end
end
-- Send reward mail
SendMail("Purchase of: "..data[KEYS.service.name], CONFIG.strings.mailBody, player:GetGUIDLow(), CONFIG.mailSenderGUID, 62, 0, 0, 0, unpack(items))
return true
end
-- GOLD
function SHOP_UI.GoldHandler(player, data)
local currency, amount = data[KEYS.service.currency], data[KEYS.service.price] - data[KEYS.service.discount]
-- Deduct currency
local deducted = SHOP_UI.DeductCurrency(player, currency, amount)
-- If currency was not deducted from the player, abort and send message
if not(deducted) then
return false
end
-- Add gold to player
player:ModifyMoney(data[KEYS.service.reward_1]*10000)
return true
end
-- MOUNTS
function SHOP_UI.MountHandler(player, data)
local currency, amount = data[KEYS.service.currency], data[KEYS.service.price] - data[KEYS.service.discount]
local knownCount, rewardCount = 0, 0
for i = 0, 7 do
if(data[KEYS.service.reward_1+i] > 0) then
if(player:HasSpell(data[KEYS.service.reward_1+i])) then
knownCount = knownCount + 1
end
rewardCount = rewardCount + 1
end
end
-- check if player already has the spells learned
if(knownCount == rewardCount) then
player:SendAreaTriggerMessage("|cFFFF0000"..CONFIG.strings.alreadyKnown.." mount|r")
player:PlayDirectSound(GetSoundEffect("cantLearn", player:GetRace(), player:GetGender()), player)
return false
end
-- Deduct currency
local deducted = SHOP_UI.DeductCurrency(player, currency, amount)
-- If currency was not deducted from the player, abort and send message
if not(deducted) then
return false
end
-- Teach mounts
for i = 0, 7 do
if(data[KEYS.service.reward_1+i] > 0) then
player:LearnSpell(data[KEYS.service.reward_1+i])
end
end
return true
end
-- PETS
function SHOP_UI.PetHandler(player, data)
local currency, amount = data[KEYS.service.currency], data[KEYS.service.price] - data[KEYS.service.discount]
local knownCount, rewardCount = 0, 0
for i = 0, 7 do
if(data[KEYS.service.reward_1+i] > 0) then
if(player:HasSpell(data[KEYS.service.reward_1+i])) then
knownCount = knownCount + 1
end
rewardCount = rewardCount + 1
end
end
-- check if player already has the spells learned
if(knownCount == rewardCount) then
player:SendAreaTriggerMessage("|cFFFF0000"..CONFIG.strings.alreadyKnown.." pet|r")
player:PlayDirectSound(GetSoundEffect("cantLearn", player:GetRace(), player:GetGender()), player)
return false
end
-- Deduct currency
local deducted = SHOP_UI.DeductCurrency(player, currency, amount)
-- If currency was not deducted from the player, abort and send message
if not(deducted) then
return false
end
-- Teach pets
for i = 0, 7 do
if(data[KEYS.service.reward_1+i] > 0) then
player:LearnSpell(data[KEYS.service.reward_1+i])
end
end
return true
end
-- BUFFS
function SHOP_UI.BuffHandler(player, data)
local currency, amount = data[KEYS.service.currency], data[KEYS.service.price] - data[KEYS.service.discount]
-- Deduct currency
local deducted = SHOP_UI.DeductCurrency(player, currency, amount)
-- If currency was not deducted from the player, abort and send message
if not(deducted) then
return false
end
-- iterate over all reward slots and buff the player with all configured spells
for i = 0, 7 do
if(data[KEYS.service.reward_1+i] > 0) then
player:CastSpell(player, data[KEYS.service.reward_1+i], true)
end
end
return true
end
-- SERVICES
function SHOP_UI.ServiceHandler(player, data)
local currency, amount = data[KEYS.service.currency], data[KEYS.service.price] - data[KEYS.service.discount]
-- Deduct currency
local deducted = SHOP_UI.DeductCurrency(player, currency, amount)
-- If currency was not deducted from the player, abort and send message
if not(deducted) then
return false
end
-- Set the AtLogin flag to whatever is defined in reward_1
player:SetAtLoginFlag(data[KEYS.service.reward_1])
return true
end
-- LEVELS
function SHOP_UI.LevelHandler(player, data)
local currency, amount = data[KEYS.service.currency], data[KEYS.service.price] - data[KEYS.service.discount]
-- If flag is set to 1, then we set the player to the specified level instead of adding levels
-- We need to check this before deducting any money
if(data[KEYS.service.flags] == 1) then
if(player:GetLevel() >= data[KEYS.service.reward_1]) then
player:SendAreaTriggerMessage("|cFFFF0000"..CONFIG.strings.tooHighLevel.."|r")
player:PlayDirectSound(GetSoundEffect("cantUse", player:GetRace(), player:GetGender()), player)
return false
end
end
-- Deduct currency
local deducted = SHOP_UI.DeductCurrency(player, currency, amount)
-- If currency was not deducted from the player, abort and send message
if not(deducted) then
return false
end
local level = player:GetLevel() + data[KEYS.service.reward_1]
-- Ensure that players can't level higher than configured max
if(level > CONFIG.maxLevel) then
level = CONFIG.maxLevel
end
-- and again, if flag = 1 then we set the level instead of adding onto
if(data[KEYS.service.flags] == 1) then
level = data[KEYS.service.reward_1]
end
player:SetLevel(level)
return true
end
-- TITLES
function SHOP_UI.TitleHandler(player, data)
local currency, amount = data[KEYS.service.currency], data[KEYS.service.price] - data[KEYS.service.discount]
-- Check whether or not the player already has the specified title
if(player:HasTitle(data[KEYS.service.reward_1])) then
player:SendAreaTriggerMessage("|cFFFF0000"..CONFIG.strings.alreadyKnown.." title|r")
player:PlayDirectSound(GetSoundEffect("cantLearn", player:GetRace(), player:GetGender()), player)
return false
end
-- Deduct currency
local deducted = SHOP_UI.DeductCurrency(player, currency, amount)
-- If currency was not deducted from the player, abort and send message
if not(deducted) then
return false
end
-- Give the player the defined title
player:SetKnownTitle(data[KEYS.service.reward_1])
return true
end
-- UNUSED
function SHOP_UI.UnusedHandler(player, data)
local currency, amount = data[KEYS.service.currency], data[KEYS.service.price] - data[KEYS.service.discount]
-- Since this is unused, always return false until the function is in use.
return false
end