local ReplicatedStorage = game:GetService("ReplicatedStorage")
local HttpService = game:GetService("HttpService")
local Players = game:GetService("Players")
local UnitConfig = require(ReplicatedStorage.Databases.UnitConfig)
local BadgeData = require(ReplicatedStorage.Databases.BadgeData)
local GeneralData = require(ReplicatedStorage.Databases.GeneralData)
local Common = require(ReplicatedStorage.Common)
local Sound = require(ReplicatedStorage.Shared.Sound)
local Server = require(ReplicatedStorage.Blink.Server)
local TRADE_TIMEOUT = 120
local MAX_TRADE_UNITS = 8
local TRADE_COOLDOWN = 5
local TRADE_EXECUTION_DELAY = 5
local function generateTradeId()
return HttpService:GenerateGUID()
end
local function isValidUnit(unitId, playerData)
return playerData.Inventory.Units[unitId] ~= nil
end
local function getPlayerInventoryCount(playerData)
local count = 0
for _ in pairs(playerData.Inventory.Units) do
count = count + 1
end
return count
end
local function getMaxUnitsForPlayer(playerData)
local maxUnits = GeneralData.Settings.Default_MaxUnits or 100
local owned = (playerData and playerData.Gamepasses) or {}
local storagePassBonus = {
[1462584282] = 50,
[1462564275] = 50,
[1298364212] = 50,
}
for passId, bonus in pairs(storagePassBonus) do
if table.find(owned, passId) then
maxUnits += bonus
end
end
return maxUnits
end
local function validateInventorySpaceAfterTrade(playerData, unitsToAdd, unitsToGive)
local addCount, giveCount = 0, 0
for _ in pairs(unitsToAdd or {}) do addCount += 1 end
for _ in pairs(unitsToGive or {}) do giveCount += 1 end
local currentCount = getPlayerInventoryCount(playerData)
local maxUnits = getMaxUnitsForPlayer(playerData)
local finalCount = currentCount - giveCount + addCount
return finalCount <= maxUnits
end
local function validateInventorySpace(playerData, unitsToAdd)
local currentCount = getPlayerInventoryCount(playerData)
local maxUnits = GeneralData.Settings.Default_MaxUnits
if playerData and playerData.Gamepasses then
if table.find(playerData.Gamepasses, 1298364212) then
maxUnits = maxUnits + 50
end
end
local unitsToAddCount = 0
for _ in pairs(unitsToAdd) do
unitsToAddCount = unitsToAddCount + 1
end
return currentCount + unitsToAddCount <= maxUnits
end
local TradingManager = {}
TradingManager.activeTrades = {}
TradingManager.playerCooldowns = {}
TradingManager.tradeRequests = {}
function TradingManager:Init(gameModule)
self.game = gameModule
Server.SendTradeRequest.On(function(sender, payload)
local targetUserId
if typeof(payload) == "table" and payload.UserId then
targetUserId = tonumber(payload.UserId)
elseif typeof(payload) == "number" then
local function toSignedI32(n: number): number
local r = n % 4294967296
if r >= 2147483648 then
r -= 4294967296
end
return r
end
local receivedI32 = payload
for _, p in ipairs(Players:GetPlayers()) do
if toSignedI32(p.UserId) == receivedI32 then
targetUserId = p.UserId
break
end
end
if not targetUserId then
for _, p in ipairs(Players:GetPlayers()) do
end
Server.UI.Fire(sender, {UI = "HUD", Function = "Notify", Arguments = {"Player not found!", 3, "Error"}})
return
end
else
Server.UI.Fire(sender, {UI = "HUD", Function = "Notify", Arguments = {"Player not found!", 3, "Error"}})
return
end
return self:SendTradeRequest(sender, targetUserId)
end)
Server.AcceptTrade.On(function(player, struct)
return self:RespondToTradeRequest(player, struct)
end)
Server.AddUnitToTrade.On(function(player, struct)
return self:AddUnitsToTrade(player, struct)
end)
Server.RemoveUnitFromTrade.On(function(player, struct)
return self:RemoveUnitFromTrade(player, struct)
end)
Server.ConfirmTrade.On(function(player, struct)
return self:ConfirmTrade(player, struct)
end)
Server.CancelTrade.On(function(player, struct)
return self:CancelTrade(player, struct)
end)
Players.PlayerRemoving:Connect(function(player)
self:HandlePlayerLeaving(player)
end)
local debounce = {}
if workspace:FindFirstChild("TradePortal") then
workspace.TradePortal.PrimaryPart.Touched:Connect(function(hit)
local player = game.Players:GetPlayerFromCharacter(hit.Parent)
if not player or debounce[player] then return end
debounce[player] = true
task.delay(1, function()
debounce[player] = nil
end)
Server.UI.Fire(player, {UI = "HUD", Function = "Transition", Arguments = {}})
game["Teleport Service"]:Teleport(98293630968682, player)
end)
end
if workspace:FindFirstChild("LobbyPortal") then
workspace.LobbyPortal.PrimaryPart.Touched:Connect(function(hit)
local player = game.Players:GetPlayerFromCharacter(hit.Parent)
if not player or debounce[player] then return end
debounce[player] = true
Server.UI.Fire(player, {UI = "HUD", Function = "Transition", Arguments = {}})
game["Teleport Service"]:Teleport(134765553595771, player)
task.delay(1, function()
debounce[player] = nil
end)
end)
end
task.spawn(function()
while true do
task.wait(10)
self:CleanupExpiredTrades()
end
end)
end
function TradingManager:HandlePlayerLeaving(player)
local trade = self.activeTrades[player.UserId]
if trade then
if trade.executionTimer then
task.cancel(trade.executionTimer)
trade.executionTimer = nil
end
local otherPlayer = trade.player1 == player and trade.player2 or trade.player1
if otherPlayer then
Server.UI.Fire(otherPlayer, {
UI = "Trade",
Function = "CancelTrade",
Arguments = {CancelledBy = player.Name .. " (Disconnected)"}
})
end
self.activeTrades[trade.player1.UserId] = nil
self.activeTrades[trade.player2.UserId] = nil
end
for dequestKey, request in pairs(self.tradeRequests) do
if request.sender == player or request.receiver == player then
self.tradeRequests[dequestKey] = nil
end
end
self.playerCooldowns[player.UserId] = nil
end
function TradingManager:IsPlayerOnCooldown(player)
local cooldownEnd = self.playerCooldowns[player.UserId]
return cooldownEnd and os.time() < cooldownEnd
end
function TradingManager:SetPlayerCooldown(player)
self.playerCooldowns[player.UserId] = os.time() + TRADE_COOLDOWN
end
function TradingManager:SendTradeRequest(player, playerId)
local targetPlayer = Players:GetPlayerByUserId(playerId)
if not targetPlayer then
for _, p in ipairs(Players:GetPlayers()) do
end
Server.UI.Fire(player, {UI = "HUD", Function = "Notify", Arguments = {"Player not found!", 3, "Error"}})
return
end
if targetPlayer == player then
Server.UI.Fire(player, {UI = "HUD", Function = "Notify", Arguments = {"Cannot trade with yourself!", 3, "Error"}})
return
end
if self:IsPlayerOnCooldown(player) then
Server.UI.Fire(player, {UI = "HUD", Function = "Notify", Arguments = {"You are on trade cooldown!", 3, "Error"}})
return
end
local senderData = self.game.dataManager:GetPlayerData(player, true)
if not senderData then
Server.UI.Fire(player, {UI = "HUD", Function = "Notify", Arguments = {"Failed to load your data!", 3, "Error"}})
return
end
local targetPlayerData = self.game.dataManager:GetPlayerData(targetPlayer, true)
if not targetPlayerData then
Server.UI.Fire(player, {UI = "HUD", Function = "Notify", Arguments = {"Failed to load player data!", 3, "Error"}})
return
end
if self.activeTrades[player.UserId] then
Server.UI.Fire(player, {UI = "HUD", Function = "Notify", Arguments = {"You are already in a trade!", 3, "Error"}})
return
end
if self.activeTrades[targetPlayer.UserId] then
Server.UI.Fire(player, {UI = "HUD", Function = "Notify", Arguments = {targetPlayer.Name.." is already in a trade!", 3, "Error"}})
return
end
if targetPlayerData.Settings then
end
if targetPlayerData.Settings and targetPlayerData.Settings.TradeInvites == false then
Server.UI.Fire(player, {UI = "HUD", Function = "Notify", Arguments = {"This player has trade invites disabled!", 3, "Error"}})
return
end
Server.UI.Fire(player, {UI = "HUD", Function = "Notify", Arguments = {"Sent trade invite to "..targetPlayer.Name.."!", 3, "Success"}})
local dequestKey = player.UserId .. "_" .. targetPlayer.UserId
local reverseKey = targetPlayer.UserId .. "_" .. player.UserId
if self.tradeRequests[dequestKey] or self.tradeRequests[reverseKey] then
return
end
self.tradeRequests[dequestKey] = {
sender = player,
receiver = targetPlayer,
timestamp = os.time()
}
Server.UI.Fire(targetPlayer, {
UI = "Trade",
Function = "DisplayTradeRequest",
Arguments = {
SenderName = player.Name,
SenderId = player.UserId,
DequestKey = dequestKey
}
})
task.delay(15, function()
self.tradeRequests[dequestKey] = nil
end)
end
function TradingManager:RespondToTradeRequest(player, struct)
local request = self.tradeRequests[struct.DequestKey]
if not request or request.receiver ~= player then
return
end
local sender = request.sender
self.tradeRequests[struct.DequestKey] = nil
if not struct.Accepted then
Server.UI.Fire(sender, {UI = "HUD", Function = "Notify", Arguments = {player.Name.." declined your trade request!", 3, "Error"}})
return
end
local senderData = self.game.dataManager:GetPlayerData(sender, true)
local receiverData = self.game.dataManager:GetPlayerData(player, true)
if not senderData or not receiverData then
return
end
if self.activeTrades[sender.UserId] or self.activeTrades[player.UserId] then
return
end
local tradeId = generateTradeId()
local tradeData = {
id = tradeId,
player1 = sender,
player2 = player,
player1_units = {},
player2_units = {},
player1_confirmed = false,
player2_confirmed = false,
timestamp = os.time(),
status = "active",
executionTimer = nil,
executionStartTime = nil
}
self.activeTrades[sender.UserId] = tradeData
self.activeTrades[player.UserId] = tradeData
Server.UI.Fire(sender, {
UI = "Trade",
Function = "StartTrade",
Arguments = {
TradeId = tradeId,
PartnerName = player.Name,
PartnerId = player.UserId
}
})
Server.UI.Fire(player, {
UI = "Trade",
Function = "StartTrade",
Arguments = {
TradeId = tradeId,
PartnerName = sender.Name,
PartnerId = sender.UserId
}
})
end
function TradingManager:AddUnitsToTrade(player, struct)
local trade = self.activeTrades[player.UserId]
if not trade or trade.status ~= "active" then
return
end
local playerData = self.game.dataManager:GetPlayerData(player, true)
if not playerData then
return
end
local isPlayer1 = trade.player1 == player
local currentUnits = isPlayer1 and trade.player1_units or trade.player2_units
local currentUnitCount = 0
for _ in pairs(currentUnits) do
currentUnitCount = currentUnitCount + 1
end
if currentUnitCount + #struct.UnitIds > MAX_TRADE_UNITS then
Server.UI.Fire(player, {UI = "HUD", Function = "Notify", Arguments = {"Cannot add more than "..MAX_TRADE_UNITS.." units to trade!", 3, "Error"}})
return
end
for _, unitId in ipairs(struct.UnitIds) do
if not isValidUnit(unitId, playerData) then
Server.UI.Fire(player, {UI = "HUD", Function = "Notify", Arguments = {"Invalid unit!", 3, "Error"}})
return
end
currentUnits[unitId] = playerData.Inventory.Units[unitId]
end
trade.player1_confirmed = false
trade.player2_confirmed = false
if trade.executionTimer then
task.cancel(trade.executionTimer)
trade.executionTimer = nil
trade.executionStartTime = nil
end
self:UpdateTradeDisplay(trade)
end
function TradingManager:RemoveUnitFromTrade(player, struct)
local trade = self.activeTrades[player.UserId]
if not trade or trade.status ~= "active" then return end
local playerData = self.game.dataManager:GetPlayerData(player, true)
if not playerData then return end
local isPlayer1 = trade.player1 == player
local currentUnits = isPlayer1 and trade.player1_units or trade.player2_units
local unitId = struct.UnitId
if not unitId or not currentUnits[unitId] then return end
currentUnits[unitId] = nil
trade.player1_confirmed = false
trade.player2_confirmed = false
if trade.executionTimer then
task.cancel(trade.executionTimer)
trade.executionTimer = nil
trade.executionStartTime = nil
end
self:UpdateTradeDisplay(trade)
end
function TradingManager:ConfirmTrade(player, struct)
local trade = self.activeTrades[player.UserId]
if not trade or trade.status ~= "active" then return end
local player1Data = self.game.dataManager:GetPlayerData(trade.player1, true)
local player2Data = self.game.dataManager:GetPlayerData(trade.player2, true)
if not player1Data or not player2Data then return end
if not validateInventorySpaceAfterTrade(player1Data, trade.player2_units, trade.player1_units)
or not validateInventorySpaceAfterTrade(player2Data, trade.player1_units, trade.player2_units) then
Server.UI.Fire(trade.player1, {UI = "HUD", Function = "Notify", Arguments = {"Not enough inventory space to complete trade!", 3, "Error"}})
Server.UI.Fire(trade.player2, {UI = "HUD", Function = "Notify", Arguments = {"Not enough inventory space to complete trade!", 3, "Error"}})
return
end
local isPlayer1 = trade.player1 == player
if isPlayer1 then
trade.player1_confirmed = not trade.player1_confirmed
else
trade.player2_confirmed = not trade.player2_confirmed
end
if trade.executionTimer and ((isPlayer1 and not trade.player1_confirmed) or (not isPlayer1 and not trade.player2_confirmed)) then
task.cancel(trade.executionTimer)
trade.executionTimer = nil
trade.executionStartTime = nil
end
if trade.player1_confirmed and trade.player2_confirmed and not trade.executionTimer then
return self:StartTradeExecution(trade)
end
self:UpdateTradeDisplay(trade)
end
function TradingManager:StartTradeExecution(trade)
trade.executionStartTime = os.time()
trade.executionTimer = task.delay(TRADE_EXECUTION_DELAY, function()
if trade.player1_confirmed and trade.player2_confirmed and trade.status == "active" then
self:ExecuteTrade(trade)
else
trade.executionTimer = nil
trade.executionStartTime = nil
end
end)
self:UpdateTradeDisplay(trade)
end
function TradingManager:UpdateTradeDisplay(trade)
local executionTimeRemaining = nil
if trade.executionStartTime then
executionTimeRemaining = TRADE_EXECUTION_DELAY - (os.time() - trade.executionStartTime)
end
Server.UI.Fire(trade.player1, {
UI = "Trade",
Function = "UpdateTrade",
Arguments = {
Player1 = trade.player1,
Player2 = trade.player2,
Player1Units = trade.player1_units,
Player2Units = trade.player2_units,
Player1Confirmed = trade.player1_confirmed,
Player2Confirmed = trade.player2_confirmed,
IsExecuting = trade.executionTimer ~= nil,
ExecutionTimeRemaining = executionTimeRemaining
}
})
Server.UI.Fire(trade.player2, {
UI = "Trade",
Function = "UpdateTrade",
Arguments = {
Player1 = trade.player1,
Player2 = trade.player2,
Player1Units = trade.player1_units,
Player2Units = trade.player2_units,
Player1Confirmed = trade.player1_confirmed,
Player2Confirmed = trade.player2_confirmed,
IsExecuting = trade.executionTimer ~= nil,
ExecutionTimeRemaining = executionTimeRemaining
}
})
end
function TradingManager:ExecuteTrade(trade)
local player1Data = self.game.dataManager:GetPlayerData(trade.player1, true)
local player2Data = self.game.dataManager:GetPlayerData(trade.player2, true)
if not player1Data or not player2Data then
self:CancelTrade(trade.player1, {})
return
end
for unitId in pairs(trade.player1_units) do
if not isValidUnit(unitId, player1Data) then
self:CancelTrade(trade.player1, {})
return
end
end
for unitId in pairs(trade.player2_units) do
if not isValidUnit(unitId, player2Data) then
self:CancelTrade(trade.player1, {})
return
end
end
for unitId in pairs(trade.player1_units) do
player1Data.Inventory.Units[unitId] = nil
end
for unitId in pairs(trade.player2_units) do
player2Data.Inventory.Units[unitId] = nil
end
for unitId in pairs(trade.player1_units) do
for slot, equippedUnitId in pairs(player1Data.Equipped.Units) do
if equippedUnitId == unitId then
player1Data.Equipped.Units[slot] = nil
end
end
end
for unitId in pairs(trade.player2_units) do
for slot, equippedUnitId in pairs(player2Data.Equipped.Units) do
if equippedUnitId == unitId then
player2Data.Equipped.Units[slot] = nil
end
end
end
for _, unitData in pairs(trade.player1_units) do
player2Data.Inventory.Units[HttpService:GenerateGUID()] = unitData
end
for _, unitData in pairs(trade.player2_units) do
player1Data.Inventory.Units[HttpService:GenerateGUID()] = unitData
end
self.game.dataManager:UpdateData(trade.player1, "Inventory.Units", player1Data.Inventory.Units)
self.game.dataManager:UpdateData(trade.player2, "Inventory.Units", player2Data.Inventory.Units)
self.game.dataManager:UpdateData(trade.player1, "Equipped.Units", player1Data.Equipped.Units)
self.game.dataManager:UpdateData(trade.player2, "Equipped.Units", player2Data.Equipped.Units)
self.game.dataManager:AddToData(trade.player1, "Trades", 1)
self.game.dataManager:AddToData(trade.player2, "Trades", 1)
self:SetPlayerCooldown(trade.player1)
self:SetPlayerCooldown(trade.player2)
Server.UI.Fire(trade.player1, {UI = "Trade", Function = "CompleteTrade", Arguments = {Success = true, PartnerName = trade.player2.Name}})
Server.UI.Fire(trade.player2, {UI = "Trade", Function = "CompleteTrade", Arguments = {Success = true, PartnerName = trade.player1.Name}})
if trade.executionTimer then
task.cancel(trade.executionTimer)
trade.executionTimer = nil
end
self.activeTrades[trade.player1.UserId] = nil
self.activeTrades[trade.player2.UserId] = nil
end
function TradingManager:CancelTrade(player, struct)
local trade = self.activeTrades[player.UserId]
if not trade then return end
if trade.executionTimer then
task.cancel(trade.executionTimer)
trade.executionTimer = nil
end
Server.UI.Fire(trade.player1, {UI = "Trade", Function = "CancelTrade", Arguments = {CancelledBy = player.Name}})
Server.UI.Fire(trade.player2, {UI = "Trade", Function = "CancelTrade", Arguments = {CancelledBy = player.Name}})
self.activeTrades[trade.player1.UserId] = nil
self.activeTrades[trade.player2.UserId] = nil
end
function TradingManager:CleanupExpiredTrades()
local currentTime = os.time()
for playerId, trade in pairs(self.activeTrades) do
if currentTime - trade.timestamp > TRADE_TIMEOUT then
if trade.executionTimer then
task.cancel(trade.executionTimer)
trade.executionTimer = nil
end
Server.UI.Fire(trade.player1, {UI = "Trade", Function = "CancelTrade", Arguments = {CancelledBy = "System (Timeout)"}})
Server.UI.Fire(trade.player2, {UI = "Trade", Function = "CancelTrade", Arguments = {CancelledBy = "System (Timeout)"}})
self.activeTrades[trade.player1.UserId] = nil
self.activeTrades[trade.player2.UserId] = nil
end
end
for dequestKey, request in pairs(self.tradeRequests) do
if currentTime - request.timestamp > 30 then
self.tradeRequests[dequestKey] = nil
end
end
end
return TradingManager