По заказу одного клиента писал базовые функции, с которыми он мог бы сделать интерфейс донат аукциона.
Задумка (ТЗ):
- Первый игрок выставляет купленный предмет на “аукцион”
- этот предмет у него исчезает (поэтому есть смысл разрешать выставлять только те предметы, которые не ограничены по сроку действия)
- запись о том, что предмет продается попадает в БД
- Второй игрок выбирает номер лота, который хочет купить
- если ему хватает денег, то они переводятся Первому игроку
- предмет удаляется с аукциона и попадает в донат инвентарь Второму
Еще важные нюансы
-
“Торговаться” за лот нельзя, да и нет смысла, я считаю. Впрочем, как и сама идея продажи донат услуг за меньшую сумму считаю плохой
-
Интерфейса в “комплекте” нет. Это только SERVER часть кода для разработчиков, чтобы сэкономить кучу времени, если вдруг решите сделать продажу донат предметов. Но для тестов есть консольные команды.
-
Игрок может выставить предмет на продажу и выйти с сервера. Другой игрок все равно сможет его купить. В коде предусмотрена возможность для работы даже с лотами игроков, которые находятся в оффлайне.
-
Игрок сможет выставить на продажу только те итемы, у которых стоит
:SetMeta("for_sale", true)
, так что пропишите это итемам заранее, либо уберите проверку с кода. -
Вам также скорее всего потребуется метод
:SetNetworked()
для итемов, которым вы выставляетеfor_sale true
, чтобы покупка появилась на CLIENT. Без SetNetworked в списке покупок игрока предмета не будет. -
Таблица
ITEMS_FOR_SALE
создана лишь как имитация базы данных. Функции, которые обращаются к этой таблице (в коде 3 шт) в идеале должны работать с sv.db/mysql
TODO
- Пока что нет функции, чтобы убрать предмет из продажи (вернуть обратно), кроме как выкупить его самому. Допишу код за пару минут, если кто-то напишет, чтобы я это сделал.
Использование “как есть” (тестирование)
- Кидаете код в
addons/igs-modification/lua/autorun/server/anyname.lua
- Перезапускаете сервер
- Выставляете предметам, с которыми хотите провести тест
:SetMeta("for_sale", true):SetNetworked()
- Покупаете один из тестовых предметов
- Вводите в консоль
i_want_to_SELL_donate_item item_uid price
, гдеitem_uid
это UID купленного предмета, а price – цена в рублях, за которую вы хотите его продать - Появится ID “лота”. Запомните его для следующей команды.
- Введите в консоль
i_want_to_BUY_donate_item 1
. Это спишет у вас донат валюту, отправит владельцу лота (даже если он не в сети) и поместит предмет в ваш донат инвентарь
Код
local putItemForSale, getItemFromSale, deleteItemFromSale do -- эти функцию стоит переписать для работы с SQL
local ITEMS_FOR_SALE = util.JSONToTable( file.Read("igs_demo_auctiond.dat") or "[]" )
-- sale_lot_dat obj: {owner_s64, item_uid, price}
function putItemForSale(sale_lot_dat, fCallback)
-- if math.random(3) == 1 then fCallback(false, "Ошибка добавления в базу данных, хотя покупка удалена в IGS") return end -- чисто для примера
local sale_uid = table.insert(ITEMS_FOR_SALE, sale_lot_dat)
file.Write("igs_demo_auction.dat", util.TableToJSON(ITEMS_FOR_SALE))
fCallback(true, sale_uid) -- sale_uid может быть любым значением. Как числом, так и uuid
end
-- должен возвращать (true, nil) если лот не найден.
-- (true, {owner_s64, item_uid, price}), если лот найден по ид
-- (false, "message"), если ошибка
function getItemFromSale(sale_uid, fCallback)
local sale_item = ITEMS_FOR_SALE[sale_uid]
fCallback(true, sale_item) --> nil or {owner_s64, item_uid, price}
end
function deleteItemFromSale(sale_uid, fCallback)
ITEMS_FOR_SALE[sale_uid] = nil
file.Write("igs_demo_auction.dat", util.TableToJSON(ITEMS_FOR_SALE))
if fCallback then fCallback(true) end
end
end
-- SV
local playerRequestedItemSale do
-- Удаляет самую старую покупку с указанным item_uid
local function _disablePurchaseByUID(s64, item_uid, fCallback)
IGS.GetPlayerPurchases(s64, function(tPurchases) -- list of {`ID`,`Server`,`Item`,`Purchase`,`Expire`(таймштамп),`SteamID`, `Nick`}
local found_purchase = nil
for _,tPurchase in ipairs(tPurchases) do
if tPurchase.Item == item_uid then
found_purchase = tPurchase
break
end
end
if not found_purchase then
fCallback(false, "У вас уже нет предмета, который вы хотели выставить на продажу") -- race condition, rare case
return
end
local purchase_id = found_purchase.ID
IGS.DisablePurchase(purchase_id, function(bDisabled)
if not bDisabled then
fCallback(false, "Донат предмет не удалось отключить для последующей продажи") -- anti race condition
return
end
fCallback(true)
end)
end)
end
playerRequestedItemSale = function(pl, item_uid, price, fCallback)
local purchases_amount = pl:HasPurchase(item_uid)
if not purchases_amount then
fCallback(false, "У вас нет предмета, который вы хотите выставить на продажу")
return
end
local ITEM = IGS.GetItemByUID(item_uid)
if not ITEM:GetMeta("for_sale") then
fCallback(false, "Этот предмет нельзя выставить на продажу")
return
end
if price <= 0 then
fCallback(false, "Цена не может быть отрицательной")
return
end
if pl.block_race_condition then
fCallback(false, "Запрос в процессе выполнения #1")
return
end
pl.block_race_condition = true
local s64 = pl:SteamID64()
_disablePurchaseByUID(s64, item_uid, function(bOk, sError)
if not bOk then
pl.block_race_condition = nil
fCallback(false, sError)
return
end
if IsValid(pl) then -- не вышел с сервера в процессе операции
IGS.LoadPlayerPurchases(pl, function() -- перезагрузка донат предметов, чтобы услуга тут же снялась
IGS.Notify(pl, "Покупки перезагружены") -- уведомление в чат игроку
end)
end
local sale_lot_dat = {s64, item_uid, price}
putItemForSale(sale_lot_dat, function(bOk2, sErr_Or_Id)
pl.block_race_condition = nil
fCallback(bOk2, sErr_Or_Id)
end)
end)
end
end
-- SV
local playerRequestedItemPurchase do
playerRequestedItemPurchase = function(pl, sale_uid, fCallback)
getItemFromSale(sale_uid, function(ok, sale_lot_dat) -- nil or {owner_s64, item_uid, price}
if not ok then fCallback(false, sale_lot_dat) return end
if ok and not sale_lot_dat then -- ок, но предмета нет
fCallback(false, "Предмет уже продан или указан несуществующий id лота")
return
end
local owner_s64, item_uid, price = unpack(sale_lot_dat)
if not IGS.CanAfford(pl, price) then
fCallback(false, "У вас недостаточно средств")
return
end
if pl.igs_unfinished_purchase then
fCallback(false, "Запрос в процессе выполнения #2")
return
end
pl.igs_unfinished_purchase = true -- название взято с igs-core. Не принципиально так то и можно было юзать block_race_condition
IGS.PayP2P(pl:SteamID64(), owner_s64, price, "Аукцион " .. item_uid .. ". ID ордера " .. sale_uid, function()
deleteItemFromSale(sale_uid, function(rem_ok, sMessage)
if not rem_ok then pl.igs_unfinished_purchase = nil fCallback(false, sMessage) return end
IGS.PlayerPurchasedItemByUID(pl, item_uid, function(purch_ok, iPurchOrInvId)
pl.igs_unfinished_purchase = nil
fCallback(purch_ok, iPurchOrInvId)
end)
end)
end)
end)
end
end
concommand.Add("i_want_to_SELL_donate_item", function(pl, _, args)
local item_uid, price = args[1], tonumber(args[2])
if not item_uid then IGS.Notify(pl, "Укажите uid предмета после команды") return end
if not price then IGS.Notify(pl, "Укажите цену предмета в руб после uid предмета, по которой вы хотите его продать") return end
playerRequestedItemSale(pl, item_uid, price, function(bOk, sMessage)
IGS.Notify(pl, bOk and ("Предмет выставлен на продажу. ID: " .. sMessage) or ("Ошибка. " .. sMessage))
end)
end)
concommand.Add("i_want_to_BUY_donate_item", function(pl, _, args)
local sale_uid = tonumber(args[1]) or args[1]
if not sale_uid then IGS.Notify(pl, "Укажите ID лота после команды") return end
playerRequestedItemPurchase(pl, sale_uid, function(bOk, sMessage)
IGS.Notify(pl, bOk and ("Предмет успешно куплен! GMD inv or purch ID: " .. sMessage) or ("Ошибка. " .. sMessage))
end)
end)
Полезный бонус для devs
Функция получения итемов, которые игрок может выставить на продажу
-- Возвращает kv таблицу uid => amount с итемами, у которых указано :SetMeta("for_sale", true)
-- Функция SHARED, но для использования на CLIENT нужен ITEM:SetNetworked() на каждом продаваемом предмете
local function getItemsAvailableToSell(pl)
local tItems = {}
local uid_amount = IGS.PlayerPurchases(pl)
for item_uid, amount in pairs(uid_amount) do
local ITEM = IGS.GetItemByUID(item_uid)
if ITEM:GetMeta("for_sale") then
tItems[item_uid] = amount
end
end
return tItems
end