Files
achmad 72b73c4dd6 feat: replace CreateHTTPRequest with CreateHTTPRequestScriptVM
Allows the game client to make HTTP API calls from a listen server
(local lobby) instead of requiring a Steam dedicated server.
CreateHTTPRequestScriptVM has the exact same API signature but works
in both dedicated server and listen server contexts.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-05-29 17:36:08 +07:00

1162 lines
49 KiB
Lua

local ____lualib = require("lualib_bundle")
local __TS__Class = ____lualib.__TS__Class
local __TS__New = ____lualib.__TS__New
local __TS__ArrayIsArray = ____lualib.__TS__ArrayIsArray
local __TS__ArrayMap = ____lualib.__TS__ArrayMap
local __TS__StringTrim = ____lualib.__TS__StringTrim
local __TS__StringSplit = ____lualib.__TS__StringSplit
local __TS__ArrayFilter = ____lualib.__TS__ArrayFilter
local __TS__ObjectValues = ____lualib.__TS__ObjectValues
local __TS__StringSubstring = ____lualib.__TS__StringSubstring
local __TS__ObjectKeys = ____lualib.__TS__ObjectKeys
local __TS__TypeOf = ____lualib.__TS__TypeOf
local __TS__Number = ____lualib.__TS__Number
local Set = ____lualib.Set
local __TS__ArraySome = ____lualib.__TS__ArraySome
local __TS__Decorate = ____lualib.__TS__Decorate
local ____exports = {}
local ____tstl_2Dutils = require("lib.tstl-utils")
local reloadable = ____tstl_2Dutils.reloadable
local ____api_helper = require("api_helper")
local setApiHeaders = ____api_helper.setApiHeaders
local ____server_config = require("server_config")
local SERVER_CONFIG = ____server_config.SERVER_CONFIG
local ____ArsenalManager = require("arsenal.ArsenalManager")
local ArsenalManager = ____ArsenalManager.ArsenalManager
local ____store_manager = require("store_manager")
local StoreManager = ____store_manager.StoreManager
local LOG_PREFIX = "[MarketplaceManager]"
--- Вкл. шумные print (API, рефреш, слоты). По умолчанию выкл.
local MARKETPLACE_VERBOSE_LOG = false
--- Отдельно: разбор истории продаж / NetTable (вкл. при отладке пустой истории).
local MARKETPLACE_SALES_DEBUG_LOG = false
local function marketplaceLog(self, message)
if MARKETPLACE_VERBOSE_LOG then
print(message)
end
end
local function salesHistoryDebugLog(self, message)
if MARKETPLACE_SALES_DEBUG_LOG then
print((LOG_PREFIX .. "[sales] ") .. message)
end
end
local MARKET_MIN_PRICE_FREE = 5000
--- Слоты активных лотов на игрока (без донат-расширения).
local MARKET_BASE_SLOTS_LIMIT = 8
local MARKET_MAX_SLOTS_LIMIT = 8
____exports.MarketplaceManagerClass = __TS__Class()
local MarketplaceManagerClass = ____exports.MarketplaceManagerClass
MarketplaceManagerClass.name = "MarketplaceManagerClass"
MarketplaceManagerClass.____file_path = "scripts/vscripts/arsenal/MarketplaceManager.lua"
function MarketplaceManagerClass.prototype.____constructor(self)
self.marketBuyInFlightByPlayer = {}
self.marketCreateInFlightByPlayer = {}
end
function MarketplaceManagerClass.getInstance(self)
if not ____exports.MarketplaceManagerClass._instance then
____exports.MarketplaceManagerClass._instance = __TS__New(____exports.MarketplaceManagerClass)
end
return ____exports.MarketplaceManagerClass._instance
end
function MarketplaceManagerClass.prototype.initialize(self)
if not IsServer() then
return
end
self:registerListeners()
marketplaceLog(nil, LOG_PREFIX .. " initialized")
end
function MarketplaceManagerClass.prototype.registerListeners(self)
CustomGameEventManager:RegisterListener(
"arsenal_market_refresh",
function(_src, data)
local playerId = self:resolvePlayerId(data)
if playerId == nil then
return
end
local stats = self:parseStatKeys(data.stats)
self:refreshForPlayer(playerId, stats, false)
end
)
CustomGameEventManager:RegisterListener(
"arsenal_market_create",
function(_src, data)
local playerId = self:resolvePlayerId(data)
if playerId == nil then
return
end
local instanceId = tostring(data.instance_id or "")
local priceFree = math.max(
1,
math.floor(tonumber(data.price_free) or 0)
)
self:createListing(playerId, instanceId, priceFree)
end
)
CustomGameEventManager:RegisterListener(
"arsenal_market_buy",
function(_src, data)
local playerId = self:resolvePlayerId(data)
if playerId == nil then
return
end
local listingId = tostring(data.listing_id or "")
self:buyListing(playerId, listingId)
end
)
CustomGameEventManager:RegisterListener(
"arsenal_market_cancel",
function(_src, data)
local playerId = self:resolvePlayerId(data)
if playerId == nil then
return
end
local listingId = tostring(data.listing_id or "")
self:cancelListing(playerId, listingId)
end
)
end
function MarketplaceManagerClass.prototype.resolvePlayerId(self, data)
local ____opt_result_2
if data ~= nil then
____opt_result_2 = data.PlayerID
end
local ____opt_result_2_6 = ____opt_result_2
if ____opt_result_2_6 == nil then
local ____opt_result_5
if data ~= nil then
____opt_result_5 = data.playerId
end
____opt_result_2_6 = ____opt_result_5
end
local ____opt_result_2_6_10 = ____opt_result_2_6
if ____opt_result_2_6_10 == nil then
local ____opt_result_9
if data ~= nil then
____opt_result_9 = data.playerID
end
____opt_result_2_6_10 = ____opt_result_9
end
local raw = ____opt_result_2_6_10
if raw == nil or raw == nil then
return nil
end
local n = tonumber(tostring(raw))
if n == nil or n < 0 then
return nil
end
return n
end
function MarketplaceManagerClass.prototype.parseStatKeys(self, raw)
if not raw then
return {}
end
if __TS__ArrayIsArray(raw) then
return __TS__ArrayMap(
raw,
function(____, x) return tostring(x) end
)
end
if type(raw) == "string" then
return __TS__ArrayFilter(
__TS__ArrayMap(
__TS__StringSplit(raw, ","),
function(____, x) return __TS__StringTrim(tostring(x)) end
),
function(____, x) return #x > 0 end
)
end
if type(raw) == "table" then
return __TS__ArrayMap(
__TS__ObjectValues(raw),
function(____, x) return tostring(x) end
)
end
return {}
end
function MarketplaceManagerClass.prototype.getSteamId(self, playerId)
local steamId = PlayerResource:GetSteamAccountID(playerId)
if not steamId or steamId <= 0 then
return nil
end
return steamId
end
function MarketplaceManagerClass.prototype.sendResult(self, playerId, success, messageToken, op)
marketplaceLog(
nil,
(((((((LOG_PREFIX .. " result player=") .. tostring(playerId)) .. " ok=") .. tostring(success)) .. " msg=") .. messageToken) .. " op=") .. (op or "none")
)
local player = PlayerResource:GetPlayer(playerId)
if not player then
return
end
local payload = {success = success, message = messageToken}
if op ~= nil then
payload.op = op
end
CustomGameEventManager:Send_ServerToPlayer(player, "arsenal_market_result", payload)
end
function MarketplaceManagerClass.prototype.callApi(self, method, url, body, cb)
marketplaceLog(
nil,
(((((LOG_PREFIX .. " api -> ") .. method) .. " ") .. url) .. " body=") .. (body and json.encode(body) or "{}")
)
local req = CreateHTTPRequestScriptVM(method, url)
setApiHeaders(nil, req)
if method == "POST" or method == "PUT" then
req:SetHTTPRequestRawPostBody(
"application/json",
json.encode(body or ({}))
)
end
req:Send(function(result)
local ok = result.StatusCode >= 200 and result.StatusCode < 300
local payload = nil
if result.Body and #result.Body > 0 then
local decoded = {json.decode(result.Body)}
if decoded and type(decoded) == "table" then
payload = decoded
end
end
local bodySize = type(result.Body) == "string" and #result.Body or 0
marketplaceLog(
nil,
(((((((((LOG_PREFIX .. " api <- ") .. method) .. " ") .. url) .. " code=") .. tostring(result.StatusCode)) .. " ok=") .. tostring(ok)) .. " bodySize=") .. tostring(bodySize)
)
if result.Body and #result.Body > 0 then
local preview = #result.Body > 220 and __TS__StringSubstring(result.Body, 0, 220) .. "..." or result.Body
marketplaceLog(nil, (LOG_PREFIX .. " api body preview=") .. preview)
end
if payload and type(payload) == "table" then
local sampleItems = payload.items
if sampleItems and __TS__ArrayIsArray(sampleItems) then
marketplaceLog(
nil,
(LOG_PREFIX .. " api payload items=") .. tostring(#sampleItems)
)
else
local keys = table.concat(
__TS__ObjectKeys(payload),
","
)
marketplaceLog(nil, ((LOG_PREFIX .. " api payload keys=[") .. keys) .. "]")
end
end
if MARKETPLACE_SALES_DEBUG_LOG and (string.find(url, "arsenal_market/sales", nil, true) or 0) - 1 >= 0 then
local rawBody = type(result.Body) == "string" and result.Body or ""
local preview = #rawBody <= 0 and "(empty body)" or (#rawBody > 380 and __TS__StringSubstring(rawBody, 0, 380) .. "..." or rawBody)
salesHistoryDebugLog(
nil,
(((((("HTTP GET sales code=" .. tostring(result.StatusCode)) .. " ok=") .. tostring(ok)) .. " bodyLen=") .. tostring(bodySize)) .. " preview=") .. preview
)
end
if cb ~= nil then
cb(nil, ok, payload)
end
end)
end
function MarketplaceManagerClass.prototype.unwrapPayload(self, payload)
if not payload or type(payload) ~= "table" then
return payload
end
if __TS__ArrayIsArray(payload) then
for ____, part in ipairs(payload) do
do
if not part or type(part) ~= "table" then
goto __continue50
end
if __TS__ArrayIsArray(part) then
return part
end
if part.items ~= nil or part.listings ~= nil or part.data ~= nil then
return part
end
end
::__continue50::
end
end
return payload
end
function MarketplaceManagerClass.prototype.extractListingsFromPayload(self, payload)
local p = self:unwrapPayload(payload)
if not p or type(p) ~= "table" then
return {}
end
if __TS__ArrayIsArray(p) then
return p
end
local direct = p.items
if __TS__ArrayIsArray(direct) then
return direct
end
local listings = p.listings
if __TS__ArrayIsArray(listings) then
return listings
end
local data = p.data
if __TS__ArrayIsArray(data) then
return data
end
if data and type(data) == "table" then
local di = data.items
if __TS__ArrayIsArray(di) then
return di
end
local dl = data.listings
if __TS__ArrayIsArray(dl) then
return dl
end
end
return {}
end
function MarketplaceManagerClass.prototype.extractSalesHistoryFromPayload(self, payload)
local function pickRowsArray(____, node)
if not node or type(node) ~= "table" then
return {rows = {}, source = "empty_node"}
end
if __TS__ArrayIsArray(node) then
return {rows = node, source = "nested_array"}
end
local o = node
for ____, k in ipairs({
"items",
"sales",
"sales_history",
"history",
"rows"
}) do
local v = o[k]
if __TS__ArrayIsArray(v) then
return {rows = v, source = "object." .. k}
end
end
local data = o.data
if __TS__ArrayIsArray(data) then
return {rows = data, source = "object.data_array"}
end
if data and type(data) == "table" then
local ____data_items_13 = data.items
if ____data_items_13 == nil then
____data_items_13 = data.sales
end
local di = ____data_items_13
if __TS__ArrayIsArray(di) then
return {rows = di, source = "object.data.items_or_sales"}
end
end
return {rows = {}, source = "object_no_array"}
end
local rawType = (payload == nil or payload == nil) and "null" or __TS__TypeOf(payload)
local p = self:unwrapPayload(payload)
local unwrappedType = (p == nil or p == nil) and "null" or __TS__TypeOf(p)
local shape = "none"
local rows = {}
if not p then
salesHistoryDebugLog(nil, ("extract: rawType=" .. rawType) .. " unwrapped=null → 0 rows")
return {}
end
if __TS__ArrayIsArray(p) then
rows = p
shape = "unwrap_array"
elseif type(p) == "table" then
local picked = pickRowsArray(nil, p)
rows = picked.rows
shape = picked.source
else
salesHistoryDebugLog(nil, ((("extract: rawType=" .. rawType) .. " unwrapped=") .. unwrappedType) .. " → 0 rows (not object/array)")
return {}
end
local out = {}
for ____, row in ipairs(rows) do
do
if not row or type(row) ~= "table" then
goto __continue78
end
local ____math_max_17 = math.max
local ____math_floor_16 = math.floor
local ____row_price_free_14 = row.price_free
if ____row_price_free_14 == nil then
____row_price_free_14 = row.priceFree
end
local ____row_price_free_14_15 = ____row_price_free_14
if ____row_price_free_14_15 == nil then
____row_price_free_14_15 = 0
end
local price = ____math_max_17(
0,
____math_floor_16(__TS__Number(____row_price_free_14_15))
)
local ____math_max_21 = math.max
local ____math_floor_20 = math.floor
local ____row_commission_free_18 = row.commission_free
if ____row_commission_free_18 == nil then
____row_commission_free_18 = row.commissionFree
end
local ____row_commission_free_18_19 = ____row_commission_free_18
if ____row_commission_free_18_19 == nil then
____row_commission_free_18_19 = 0
end
local commission = ____math_max_21(
0,
____math_floor_20(__TS__Number(____row_commission_free_18_19))
)
local ____math_max_25 = math.max
local ____math_floor_24 = math.floor
local ____row_seller_received_free_22 = row.seller_received_free
if ____row_seller_received_free_22 == nil then
____row_seller_received_free_22 = row.sellerReceivedFree
end
local ____row_seller_received_free_22_23 = ____row_seller_received_free_22
if ____row_seller_received_free_22_23 == nil then
____row_seller_received_free_22_23 = 0
end
local received = ____math_max_25(
0,
____math_floor_24(__TS__Number(____row_seller_received_free_22_23))
)
local ____tostring_28 = tostring
local ____row_listing_id_26 = row.listing_id
if ____row_listing_id_26 == nil then
____row_listing_id_26 = row.listingId
end
local ____row_listing_id_26_27 = ____row_listing_id_26
if ____row_listing_id_26_27 == nil then
____row_listing_id_26_27 = ""
end
local ____tostring_28_result_42 = ____tostring_28(____row_listing_id_26_27)
local ____tostring_31 = tostring
local ____row_item_name_29 = row.item_name
if ____row_item_name_29 == nil then
____row_item_name_29 = row.itemName
end
local ____row_item_name_29_30 = ____row_item_name_29
if ____row_item_name_29_30 == nil then
____row_item_name_29_30 = ""
end
local ____tostring_31_result_43 = ____tostring_31(____row_item_name_29_30)
local ____tostring_33 = tostring
local ____row_quality_32 = row.quality
if ____row_quality_32 == nil then
____row_quality_32 = "common"
end
local ____tostring_33_result_44 = ____tostring_33(____row_quality_32)
local ____math_floor_36 = math.floor
local ____row_buyer_steam_id_34 = row.buyer_steam_id
if ____row_buyer_steam_id_34 == nil then
____row_buyer_steam_id_34 = row.buyerSteamId
end
local ____row_buyer_steam_id_34_35 = ____row_buyer_steam_id_34
if ____row_buyer_steam_id_34_35 == nil then
____row_buyer_steam_id_34_35 = 0
end
local ____math_floor_36_result_45 = ____math_floor_36(__TS__Number(____row_buyer_steam_id_34_35))
local ____tostring_39 = tostring
local ____row_buyer_name_37 = row.buyer_name
if ____row_buyer_name_37 == nil then
____row_buyer_name_37 = row.buyerName
end
local ____row_buyer_name_37_38 = ____row_buyer_name_37
if ____row_buyer_name_37_38 == nil then
____row_buyer_name_37_38 = ""
end
local ____tostring_39_result_46 = ____tostring_39(____row_buyer_name_37_38)
local ____temp_47 = received > 0 and received or math.max(0, price - commission)
local ____row_sold_at_40 = row.sold_at
if ____row_sold_at_40 == nil then
____row_sold_at_40 = row.soldAt
end
local ____row_sold_at_40_41 = ____row_sold_at_40
if ____row_sold_at_40_41 == nil then
____row_sold_at_40_41 = row.updated_at
end
out[#out + 1] = {
listing_id = ____tostring_28_result_42,
item_name = ____tostring_31_result_43,
quality = ____tostring_33_result_44,
price_free = price,
buyer_steam_id = ____math_floor_36_result_45,
buyer_name = ____tostring_39_result_46,
commission_free = commission,
seller_received_free = ____temp_47,
sold_at = ____row_sold_at_40_41
}
end
::__continue78::
end
local first = #out > 0 and out[1] or nil
salesHistoryDebugLog(
nil,
(((((((((("extract: rawType=" .. rawType) .. " unwrapped=") .. unwrappedType) .. " shape=") .. shape) .. " rawRows=") .. tostring(#rows)) .. " normalized=") .. tostring(#out)) .. " firstItem=") .. (first and tostring(first.item_name) or "-")
)
return out
end
function MarketplaceManagerClass.prototype.extractInventoryInstancesFromPayload(self, payload)
local visited = __TS__New(Set)
local walk
walk = function(____, node, depth)
if not node or type(node) ~= "table" then
return nil
end
if visited:has(node) then
return nil
end
visited:add(node)
if depth > 6 then
return nil
end
local directInstances = node.instances
if directInstances and type(directInstances) == "table" then
return directInstances
end
local ai = node.arsenal_inventory
if ai and type(ai) == "table" and ai.instances and type(ai.instances) == "table" then
return ai.instances
end
local d = node.data
if d and type(d) == "table" then
local fromData = walk(nil, d, depth + 1)
if fromData then
return fromData
end
end
if __TS__ArrayIsArray(node) then
for ____, part in ipairs(node) do
local found = walk(nil, part, depth + 1)
if found then
return found
end
end
return nil
end
for k in pairs(node) do
do
local v = node[k]
if not v or type(v) ~= "table" then
goto __continue94
end
local found = walk(nil, v, depth + 1)
if found then
return found
end
end
::__continue94::
end
return nil
end
return walk(nil, payload, 0) or ({})
end
function MarketplaceManagerClass.prototype.collectCreateCandidateIds(self, localInstanceId, owned, localInstances, apiInstances)
local out = {}
local seen = {}
local function push(____, v)
if v == nil or v == nil then
return
end
local s = tostring(v)
if not s or #s <= 0 then
return
end
if seen[s] then
return
end
seen[s] = true
out[#out + 1] = type(v) == "number" and v or s
end
push(
nil,
__TS__Number(owned.globalSerial or 0) > 0 and __TS__Number(owned.globalSerial or 0) or nil
)
push(
nil,
__TS__Number(owned.serial or 0) > 0 and __TS__Number(owned.serial or 0) or nil
)
push(nil, localInstanceId)
push(nil, owned.instanceId)
push(nil, owned.instance_id)
local function scanBag(____, bag)
for key in pairs(bag) do
local row = bag[key]
local ____tostring_56 = tostring
local ____opt_result_50
if row ~= nil then
____opt_result_50 = row.instanceId
end
local ____opt_result_50_54 = ____opt_result_50
if ____opt_result_50_54 == nil then
local ____opt_result_53
if row ~= nil then
____opt_result_53 = row.instance_id
end
____opt_result_50_54 = ____opt_result_53
end
local ____opt_result_50_54_55 = ____opt_result_50_54
if ____opt_result_50_54_55 == nil then
____opt_result_50_54_55 = ""
end
local rowInstance = ____tostring_56(____opt_result_50_54_55)
local ____opt_result_59
if row ~= nil then
____opt_result_59 = row.globalSerial
end
local ____opt_result_59_63 = ____opt_result_59
if ____opt_result_59_63 == nil then
local ____opt_result_62
if row ~= nil then
____opt_result_62 = row.global_serial
end
____opt_result_59_63 = ____opt_result_62
end
local ____opt_result_59_63_64 = ____opt_result_59_63
if ____opt_result_59_63_64 == nil then
____opt_result_59_63_64 = 0
end
local rowGlobal = __TS__Number(____opt_result_59_63_64)
local ____opt_result_67
if row ~= nil then
____opt_result_67 = row.serial
end
local ____opt_result_67_68 = ____opt_result_67
if ____opt_result_67_68 == nil then
____opt_result_67_68 = 0
end
local rowSerial = __TS__Number(____opt_result_67_68)
local sameInstance = #rowInstance > 0 and rowInstance == localInstanceId
local sameGlobal = __TS__Number(owned.globalSerial or 0) > 0 and rowGlobal > 0 and rowGlobal == __TS__Number(owned.globalSerial or 0)
local sameSerial = __TS__Number(owned.serial or 0) > 0 and rowSerial > 0 and rowSerial == __TS__Number(owned.serial or 0)
if sameInstance or sameGlobal or sameSerial then
push(nil, key)
push(nil, rowInstance)
if rowGlobal > 0 then
push(nil, rowGlobal)
end
if rowSerial > 0 then
push(nil, rowSerial)
end
end
end
end
scanBag(nil, localInstances)
scanBag(nil, apiInstances)
return out
end
function MarketplaceManagerClass.prototype.extractSlotsFromPayload(self, payload, myListingsCount)
local p = self:unwrapPayload(payload)
if not p or type(p) ~= "table" then
return {slots_limit = MARKET_BASE_SLOTS_LIMIT, slots_used = myListingsCount}
end
local ____temp_69
if p.data and type(p.data) == "table" then
____temp_69 = p.data
else
____temp_69 = p
end
local data = ____temp_69
local ____data_slots_limit_70 = data.slots_limit
if ____data_slots_limit_70 == nil then
____data_slots_limit_70 = data.market_slots_limit
end
local ____data_slots_limit_70_71 = ____data_slots_limit_70
if ____data_slots_limit_70_71 == nil then
____data_slots_limit_70_71 = MARKET_BASE_SLOTS_LIMIT
end
local rawLimit = __TS__Number(____data_slots_limit_70_71)
local ____data_slots_used_72 = data.slots_used
if ____data_slots_used_72 == nil then
____data_slots_used_72 = data.market_slots_used
end
local ____data_slots_used_72_73 = ____data_slots_used_72
if ____data_slots_used_72_73 == nil then
____data_slots_used_72_73 = myListingsCount
end
local rawUsed = __TS__Number(____data_slots_used_72_73)
return {
slots_limit = math.max(
MARKET_BASE_SLOTS_LIMIT,
math.min(
MARKET_MAX_SLOTS_LIMIT,
math.floor(rawLimit or MARKET_BASE_SLOTS_LIMIT)
)
),
slots_used = math.max(
0,
math.floor(rawUsed or myListingsCount)
)
}
end
function MarketplaceManagerClass.prototype.setMarketTables(self, playerId, publicListings, myListings, slots, selectedStats, salesHistory)
marketplaceLog(
nil,
((((((((((((((LOG_PREFIX .. " set tables player=") .. tostring(playerId)) .. " public=") .. tostring(#publicListings)) .. " my=") .. tostring(#myListings)) .. " sales=") .. tostring(#salesHistory)) .. " slots=") .. tostring(slots.slots_used or #myListings)) .. "/") .. tostring(slots.slots_limit or MARKET_BASE_SLOTS_LIMIT)) .. " stats=[") .. table.concat(selectedStats, ",")) .. "]"
)
local statKeysSet = {}
for ____, row in ipairs(publicListings) do
for ____, k in ipairs(row.stat_keys or ({})) do
if k and #k > 0 then
statKeysSet[k] = true
end
end
for ____, p in ipairs(row.stats_snapshot or ({})) do
local k = tostring(p.key or "")
if #k > 0 then
statKeysSet[k] = true
end
end
end
CustomNetTables:SetTableValue(
"arsenal_market",
"listings_public",
{
listings = publicListings,
stat_keys = __TS__ObjectKeys(statKeysSet),
selected_stats = selectedStats,
ts = GameRules:GetGameTime()
}
)
local salesJsonEncoded = json.encode(salesHistory)
salesHistoryDebugLog(
nil,
(((((("setMarketTables pid=" .. tostring(playerId)) .. " sales=") .. tostring(#salesHistory)) .. " sales_json_chars=") .. tostring(#salesJsonEncoded)) .. " my=") .. tostring(#myListings)
)
CustomNetTables:SetTableValue(
"arsenal_market",
tostring(playerId),
{
my_listings = myListings,
sales_history_json = salesJsonEncoded,
slots_limit = slots.slots_limit or MARKET_BASE_SLOTS_LIMIT,
slots_used = slots.slots_used or #myListings,
ts = GameRules:GetGameTime()
}
)
end
function MarketplaceManagerClass.prototype.refreshForAllConnectedPlayers(self)
do
local pid = 0
while pid < DOTA_MAX_PLAYERS do
do
if not PlayerResource:IsValidPlayerID(pid) then
goto __continue121
end
local playerId = pid
local player = PlayerResource:GetPlayer(playerId)
if not player then
goto __continue121
end
self:refreshForPlayer(playerId, {}, true)
end
::__continue121::
pid = pid + 1
end
end
end
function MarketplaceManagerClass.prototype.refreshForPlayer(self, playerId, selectedStats, silentOnError)
local steamId = self:getSteamId(playerId)
if not steamId then
return
end
marketplaceLog(
nil,
(((((((LOG_PREFIX .. " refresh player=") .. tostring(playerId)) .. " steam=") .. tostring(steamId)) .. " stats=[") .. table.concat(selectedStats, ",")) .. "] silent=") .. tostring(silentOnError)
)
local statsQuery = #selectedStats > 0 and "?stats=" .. table.concat(selectedStats, ",") or ""
self:callApi(
"GET",
(SERVER_CONFIG.API_URL .. "/arsenal_market/listings") .. statsQuery,
nil,
function(____, okPub, pubPayload)
self:callApi(
"GET",
((SERVER_CONFIG.API_URL .. "/player/") .. tostring(steamId)) .. "/arsenal_market/my_listings",
nil,
function(____, okMine, minePayload)
self:callApi(
"GET",
((SERVER_CONFIG.API_URL .. "/player/") .. tostring(steamId)) .. "/arsenal_market/slots",
nil,
function(____, okSlots, slotsPayload)
self:callApi(
"GET",
((SERVER_CONFIG.API_URL .. "/player/") .. tostring(steamId)) .. "/arsenal_market/sales",
nil,
function(____, okSales, salesPayload)
local publicListings = okPub and self:extractListingsFromPayload(pubPayload) or ({})
local myListings = okMine and self:extractListingsFromPayload(minePayload) or ({})
local slots = okSlots and self:extractSlotsFromPayload(slotsPayload, #myListings) or ({slots_limit = MARKET_BASE_SLOTS_LIMIT, slots_used = #myListings})
salesHistoryDebugLog(
nil,
(((((("GET sales player=" .. tostring(playerId)) .. " steam=") .. tostring(steamId)) .. " ok=") .. tostring(okSales)) .. " payloadType=") .. ((salesPayload == nil or salesPayload == nil) and "null" or __TS__TypeOf(salesPayload))
)
local salesHistory = okSales and self:extractSalesHistoryFromPayload(salesPayload) or ({})
salesHistoryDebugLog(
nil,
"GET sales → normalized=" .. tostring(#salesHistory)
)
self:setMarketTables(
playerId,
publicListings,
myListings,
slots,
selectedStats,
salesHistory
)
if (not okPub or not okMine or not okSlots) and not silentOnError then
self:sendResult(playerId, false, "#store_marketplace_error_fetch", "refresh")
end
end
)
end
)
end
)
end
)
end
function MarketplaceManagerClass.prototype.createListing(self, playerId, instanceId, priceFree)
marketplaceLog(
nil,
(((((LOG_PREFIX .. " create req player=") .. tostring(playerId)) .. " instance=") .. instanceId) .. " price=") .. tostring(priceFree)
)
if not instanceId or priceFree < MARKET_MIN_PRICE_FREE then
self:sendResult(playerId, false, "#store_marketplace_error_min_price", "create")
return
end
local steamId = self:getSteamId(playerId)
if not steamId then
return
end
local owned = ArsenalManager:getInventoryInstance(playerId, instanceId)
if not owned then
self:sendResult(playerId, false, "#arsenal_item_not_owned", "create")
return
end
marketplaceLog(
nil,
(((((((((((LOG_PREFIX .. " create owned local instanceId=") .. tostring(owned.instanceId or "")) .. " serial=") .. tostring(owned.serial or 0)) .. " globalSerial=") .. tostring(owned.globalSerial or 0)) .. " item=") .. tostring(owned.itemName or "")) .. " quality=") .. tostring(owned.quality or "")) .. " lvl=") .. tostring(owned.upgradeLevel or 0)
)
if ArsenalManager:isInstanceTradeLocked(playerId, instanceId) then
self:sendResult(playerId, false, "#store_marketplace_error_locked_item", "create")
return
end
local itemLevel = math.floor(__TS__Number(owned.upgradeLevel or 0))
if itemLevel < 1 then
self:sendResult(playerId, false, "#store_marketplace_error_min_level_1", "create")
return
end
local pidKey = playerId
if self.marketCreateInFlightByPlayer[pidKey] then
self:sendResult(playerId, false, "#store_marketplace_error_create_in_progress", "create")
return
end
self.marketCreateInFlightByPlayer[pidKey] = true
local function clearCreateFlight()
self.marketCreateInFlightByPlayer[pidKey] = false
end
local inventoryPayload = ArsenalManager:buildInventoryPayloadForMarket(playerId)
local payloadInstances = inventoryPayload.instances or ({})
marketplaceLog(
nil,
(LOG_PREFIX .. " create local payload instancesCount=") .. tostring(#__TS__ObjectKeys(payloadInstances))
)
local localRow = payloadInstances[instanceId]
if localRow then
marketplaceLog(
nil,
(((((((((LOG_PREFIX .. " create local payload hit key=") .. instanceId) .. " row.instanceId=") .. tostring(localRow.instanceId or "")) .. " row.instance_id=") .. tostring(localRow.instance_id or "")) .. " row.global_serial=") .. tostring(localRow.global_serial or 0)) .. " row.serial=") .. tostring(localRow.serial or 0)
)
else
marketplaceLog(nil, (LOG_PREFIX .. " create local payload miss key=") .. instanceId)
end
self:callApi(
"PUT",
((SERVER_CONFIG.API_URL .. "/player/") .. tostring(steamId)) .. "/arsenal_inventory",
{arsenal_inventory = inventoryPayload},
function(____, _okSync)
self:callApi(
"GET",
((SERVER_CONFIG.API_URL .. "/player/") .. tostring(steamId)) .. "/arsenal_inventory",
nil,
function(____, _okInv, invPayload)
local apiInstances = self:extractInventoryInstancesFromPayload(invPayload)
marketplaceLog(
nil,
(LOG_PREFIX .. " create api inventory instancesCount=") .. tostring(#__TS__ObjectKeys(apiInstances))
)
local byLocalKey = apiInstances[instanceId]
if byLocalKey then
marketplaceLog(
nil,
(((((((((LOG_PREFIX .. " create api hit localKey=") .. instanceId) .. " row.instanceId=") .. tostring(byLocalKey.instanceId or "")) .. " row.instance_id=") .. tostring(byLocalKey.instance_id or "")) .. " row.global_serial=") .. tostring(byLocalKey.global_serial or 0)) .. " row.serial=") .. tostring(byLocalKey.serial or 0)
)
else
marketplaceLog(nil, (LOG_PREFIX .. " create api miss localKey=") .. instanceId)
end
local candidateIds = self:collectCreateCandidateIds(instanceId, owned, payloadInstances, apiInstances)
marketplaceLog(
nil,
((LOG_PREFIX .. " create candidates=[") .. table.concat(
__TS__ArrayMap(
candidateIds,
function(____, x) return tostring(x) end
),
","
)) .. "]"
)
self:callApi(
"GET",
((SERVER_CONFIG.API_URL .. "/player/") .. tostring(steamId)) .. "/arsenal_market/slots",
nil,
function(____, okSlots, slotsPayload)
if not okSlots then
clearCreateFlight(nil)
self:sendResult(playerId, false, "#store_marketplace_error_fetch", "create")
return
end
local ____math_max_79 = math.max
local ____math_min_78 = math.min
local ____math_floor_77 = math.floor
local ____opt_result_76
if slotsPayload ~= nil then
____opt_result_76 = slotsPayload.slots_limit
end
local slotsLimit = ____math_max_79(
MARKET_BASE_SLOTS_LIMIT,
____math_min_78(
MARKET_MAX_SLOTS_LIMIT,
____math_floor_77(__TS__Number(____opt_result_76 or MARKET_BASE_SLOTS_LIMIT))
)
)
local ____math_max_84 = math.max
local ____math_floor_83 = math.floor
local ____opt_result_82
if slotsPayload ~= nil then
____opt_result_82 = slotsPayload.slots_used
end
local slotsUsed = ____math_max_84(
0,
____math_floor_83(__TS__Number(____opt_result_82 or 0))
)
if slotsUsed >= slotsLimit then
clearCreateFlight(nil)
self:sendResult(playerId, false, "#store_marketplace_error_slots_full", "create")
return
end
local createUrl = ((SERVER_CONFIG.API_URL .. "/player/") .. tostring(steamId)) .. "/arsenal_market/create"
local function buildCreateBody(____, primaryInstanceId)
return {
instance_id = primaryInstanceId,
instanceId = instanceId,
item_instance_id = primaryInstanceId,
itemInstanceId = instanceId,
serial = __TS__Number(owned.serial or 0),
global_serial = __TS__Number(owned.globalSerial or 0),
globalSerial = __TS__Number(owned.globalSerial or 0),
item_name = tostring(owned.itemName or ""),
itemName = tostring(owned.itemName or ""),
quality = tostring(owned.quality or ""),
upgrade_level = __TS__Number(owned.upgradeLevel or 0),
upgradeLevel = __TS__Number(owned.upgradeLevel or 0),
price_free = priceFree,
priceFree = priceFree,
request_id = tostring(DoUniqueString("market_create")),
requestId = tostring(DoUniqueString("market_create_alt"))
}
end
local function onCreateSuccess()
ArsenalManager:removeInventoryInstanceForMarket(playerId, instanceId)
clearCreateFlight(nil)
ArsenalManager:loadFromServer(playerId)
self:refreshForPlayer(playerId, {}, true)
self:sendResult(playerId, true, "#store_marketplace_success_create", "create")
end
local tryCreateByIndex
tryCreateByIndex = function(____, idx)
if idx >= #candidateIds then
clearCreateFlight(nil)
self:sendResult(playerId, false, "#store_marketplace_error_create", "create")
return
end
local currentId = candidateIds[idx + 1]
marketplaceLog(
nil,
(((LOG_PREFIX .. " create try idx=") .. tostring(idx)) .. " candidate=") .. tostring(currentId)
)
self:callApi(
"POST",
createUrl,
buildCreateBody(nil, currentId),
function(____, okTry, payloadTry)
if not okTry then
local ____tostring_93 = tostring
local ____opt_result_87
if payloadTry ~= nil then
____opt_result_87 = payloadTry.error
end
local ____opt_result_87_91 = ____opt_result_87
if ____opt_result_87_91 == nil then
local ____opt_result_90
if payloadTry ~= nil then
____opt_result_90 = payloadTry.message
end
____opt_result_87_91 = ____opt_result_90
end
local ____opt_result_87_91_92 = ____opt_result_87_91
if ____opt_result_87_91_92 == nil then
____opt_result_87_91_92 = ""
end
local err = ____tostring_93(____opt_result_87_91_92)
marketplaceLog(
nil,
((((((LOG_PREFIX .. " create try fail idx=") .. tostring(idx)) .. " candidate=") .. tostring(currentId)) .. " err=\"") .. err) .. "\""
)
if idx + 1 < #candidateIds then
tryCreateByIndex(nil, idx + 1)
return
end
clearCreateFlight(nil)
self:sendResult(playerId, false, "#store_marketplace_error_create", "create")
return
end
marketplaceLog(
nil,
(((LOG_PREFIX .. " create try success idx=") .. tostring(idx)) .. " candidate=") .. tostring(currentId)
)
onCreateSuccess(nil)
end
)
end
tryCreateByIndex(nil, 0)
end
)
end
)
end
)
end
function MarketplaceManagerClass.prototype.buyListing(self, playerId, listingId)
marketplaceLog(
nil,
(((LOG_PREFIX .. " buy req player=") .. tostring(playerId)) .. " listing=") .. listingId
)
if not listingId then
self:sendResult(playerId, false, "#store_marketplace_error_invalid_data", "buy")
return
end
local steamId = self:getSteamId(playerId)
if not steamId then
return
end
local pidKey = playerId
if self.marketBuyInFlightByPlayer[pidKey] then
self:sendResult(playerId, false, "#store_marketplace_error_buy_in_progress", "buy")
return
end
self.marketBuyInFlightByPlayer[pidKey] = true
local function clearBuyFlight()
self.marketBuyInFlightByPlayer[pidKey] = false
end
self:callApi(
"GET",
((SERVER_CONFIG.API_URL .. "/player/") .. tostring(steamId)) .. "/arsenal_market/my_listings",
nil,
function(____, okMine, minePayload)
if okMine then
local ownListings = self:extractListingsFromPayload(minePayload)
local isOwn = __TS__ArraySome(
ownListings,
function(____, x) return tostring(x.listing_id or "") == listingId end
)
if isOwn then
clearBuyFlight(nil)
self:sendResult(playerId, false, "#store_marketplace_error_buy", "buy")
return
end
end
self:callApi(
"POST",
((SERVER_CONFIG.API_URL .. "/player/") .. tostring(steamId)) .. "/arsenal_market/buy",
{
listing_id = listingId,
request_id = tostring(DoUniqueString("market_buy"))
},
function(____, ok, payload)
clearBuyFlight(nil)
if not ok then
self:sendResult(playerId, false, "#store_marketplace_error_buy", "buy")
return
end
ArsenalManager:loadFromServer(playerId)
StoreManager:getInstance():loadCurrencyFromServer(playerId)
self:refreshForPlayer(playerId, {}, true)
local ____opt_result_96
if payload ~= nil then
____opt_result_96 = payload.seller_steam_id
end
local sellerSteamId = __TS__Number(____opt_result_96 or 0)
if sellerSteamId > 0 then
local sellerPid = self:findConnectedPlayerBySteamId(sellerSteamId)
if sellerPid ~= nil then
ArsenalManager:loadFromServer(sellerPid)
StoreManager:getInstance():loadCurrencyFromServer(sellerPid)
self:refreshForPlayer(sellerPid, {}, true)
end
end
self:refreshForAllConnectedPlayers()
self:sendResult(playerId, true, "#store_marketplace_success_buy", "buy")
end
)
end
)
end
function MarketplaceManagerClass.prototype.cancelListing(self, playerId, listingId)
marketplaceLog(
nil,
(((LOG_PREFIX .. " cancel req player=") .. tostring(playerId)) .. " listing=") .. listingId
)
if not listingId then
self:sendResult(playerId, false, "#store_marketplace_error_invalid_data", "cancel")
return
end
local steamId = self:getSteamId(playerId)
if not steamId then
return
end
self:callApi(
"POST",
((SERVER_CONFIG.API_URL .. "/player/") .. tostring(steamId)) .. "/arsenal_market/cancel",
{
listing_id = listingId,
request_id = tostring(DoUniqueString("market_cancel"))
},
function(____, ok)
if not ok then
self:sendResult(playerId, false, "#store_marketplace_error_cancel", "cancel")
return
end
ArsenalManager:loadFromServer(playerId)
self:refreshForPlayer(playerId, {}, true)
self:sendResult(playerId, true, "#store_marketplace_success_cancel", "cancel")
end
)
end
function MarketplaceManagerClass.prototype.findConnectedPlayerBySteamId(self, steamId)
do
local pid = 0
while pid < DOTA_MAX_PLAYERS do
do
if not PlayerResource:IsValidPlayerID(pid) then
goto __continue175
end
if PlayerResource:GetSteamAccountID(pid) == steamId then
return pid
end
end
::__continue175::
pid = pid + 1
end
end
return nil
end
MarketplaceManagerClass = __TS__Decorate(MarketplaceManagerClass, MarketplaceManagerClass, {reloadable}, {kind = "class", name = "MarketplaceManagerClass"})
____exports.MarketplaceManagerClass = MarketplaceManagerClass
____exports.MarketplaceManager = ____exports.MarketplaceManagerClass:getInstance()
return ____exports