[IGS] Базовый код для аукциона предметов

По заказу одного клиента писал базовые функции, с которыми он мог бы сделать интерфейс донат аукциона.

Задумка (ТЗ):

  • Первый игрок выставляет купленный предмет на “аукцион”
  • этот предмет у него исчезает (поэтому есть смысл разрешать выставлять только те предметы, которые не ограничены по сроку действия)
  • запись о том, что предмет продается попадает в БД
  • Второй игрок выбирает номер лота, который хочет купить
  • если ему хватает денег, то они переводятся Первому игроку
  • предмет удаляется с аукциона и попадает в донат инвентарь Второму

Еще важные нюансы

  • “Торговаться” за лот нельзя, да и нет смысла, я считаю. Впрочем, как и сама идея продажи донат услуг за меньшую сумму считаю плохой

  • Интерфейса в “комплекте” нет. Это только 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

P.S. Если кто сделает простейший демо интерфейс для этого кода – дайте знать, пожалуйста. Выдам медальку, да и с радостью воспользуюсь кодом

P.P.S. Эти ссылки могут быть интересны тем, кто будет делать интеграцию (если будет):