--[[========================================================================== PERP-X <-> GMR BALANCE TRANSFER BRIDGE (GARRY'S MOD LUA) Place this file on your GMR server: garrysmod/lua/autorun/server/gmr_eco_bridge.lua ============================================================================]] AddCSLuaFile() if CLIENT then return end -- --------------------------------------------------------------------------- -- 1. CONFIGURATION -- --------------------------------------------------------------------------- local GMR_BRIDGE = { -- Base URL of the PERP-X Transfer API ApiUrl = "https://admin.perpx.net/api/gmr.php", -- Security API Key configured in PERP-X web/config.php ($config['gmr_sync']['api_key']) ApiKey = "gmr_sec_9f7a8b3c1d4e2f6a8b0c5d7e3f1a9b2c", -- Storage Mode: "gamemode" (calls custom in-game methods) or "mysql" (direct database) StorageMode = "gamemode", -- Enable auto-claiming pending transfers when a player joins GMR AutoClaimOnSpawn = true } -- --------------------------------------------------------------------------- -- 2. CUSTOM GAMEMODE HOOKS (EDIT THESE FOR YOUR GAMEMODE) -- --------------------------------------------------------------------------- --- Get a player's balance (online from player memory or offline from database/files) -- @param steamid64 string Target player SteamID64 -- @param callback function(success, balanceNumber, errorMsg) function GMR_Transfer_GetBalance(steamid64, callback) steamid64 = tostring(steamid64) -- Case A: Player is currently online on GMR local ply = player.GetBySteamID64(steamid64) if IsValid(ply) then -- >>> REPLACE WITH YOUR SERVER'S IN-GAME GET BALANCE METHOD <<< -- Examples: -- ply:GetMoney() -- ply:GetGMRMoney() -- ply:getDarkRPVar("money") -- ply:GetPData("money", 0) local bal = 0 if ply.GetGMRMoney then bal = ply:GetGMRMoney() elseif ply.GetMoney then bal = ply:GetMoney() elseif ply.getDarkRPVar then bal = ply:getDarkRPVar("money") or 0 elseif ply.GetPData then bal = tonumber(ply:GetPData("gmr_money", 0)) or 0 end callback(true, tonumber(bal) or 0) return end -- Case B: Player is offline (query database or flatfile) if GMR_BRIDGE.StorageMode == "mysql" and _G.GMR_DB then -- >>> REPLACE WITH YOUR DATABASE QUERY <<< local query = string.format("SELECT money FROM player_economy WHERE steamid64 = '%s' LIMIT 1;", steamid64) _G.GMR_DB:Query(query, function(results) local bal = (results and results[1] and tonumber(results[1].money)) or 0 callback(true, bal) end, function(err) callback(false, 0, tostring(err)) end) return end -- Fallback offline query using PData / SQLite if MySQL is not present local offlineBal = sql.QueryValue(string.format("SELECT value FROM playerpdata WHERE infoid = '%s' AND value IS NOT NULL LIMIT 1;", steamid64)) callback(true, tonumber(offlineBal) or 0) end --- Adjust a player's balance (positive = deposit, negative = withdrawal) -- @param steamid64 string Target player SteamID64 -- @param amount number Amount to add (positive) or deduct (negative) -- @param reason string Audit reason -- @param callback function(success, newBalanceNumber, errorMsg) function GMR_Transfer_AdjustBalance(steamid64, amount, reason, callback) steamid64 = tostring(steamid64) amount = tonumber(amount) or 0 -- Case A: Player is currently online on GMR local ply = player.GetBySteamID64(steamid64) if IsValid(ply) then -- >>> REPLACE WITH YOUR SERVER'S IN-GAME ADD / DEDUCT METHOD <<< local currentBal = 0 if ply.GetGMRMoney then currentBal = ply:GetGMRMoney() elseif ply.GetMoney then currentBal = ply:GetMoney() elseif ply.getDarkRPVar then currentBal = ply:getDarkRPVar("money") or 0 elseif ply.GetPData then currentBal = tonumber(ply:GetPData("gmr_money", 0)) or 0 end if amount < 0 and math.abs(amount) > currentBal then callback(false, currentBal, "Insufficient funds on GMR.") return end if ply.AddGMRMoney then ply:AddGMRMoney(amount) elseif ply.addMoney then ply:addMoney(amount) elseif ply.SetPData then ply:SetPData("gmr_money", currentBal + amount) end local newBal = currentBal + amount -- In-game notification for the player if amount > 0 then ply:ChatPrint(string.format("[GMR Transfer] Received $%s transferred from PERP-X! (New Balance: $%s)", string.Comma(amount), string.Comma(newBal))) ply:SendLua(string.format("notification.AddLegacy('Transferred +$%s from PERP-X!', NOTIFY_GENERIC, 6)", string.Comma(amount))) else ply:ChatPrint(string.format("[GMR Transfer] Transferred $%s from GMR to PERP-X. (New Balance: $%s)", string.Comma(math.abs(amount)), string.Comma(newBal))) end callback(true, newBal) return end -- Case B: Player is offline (query database or flatfile) if GMR_BRIDGE.StorageMode == "mysql" and _G.GMR_DB then -- Fetch current offline balance with lock local selQuery = string.format("SELECT money FROM player_economy WHERE steamid64 = '%s' LIMIT 1;", steamid64) _G.GMR_DB:Query(selQuery, function(res) local oldBal = (res and res[1] and tonumber(res[1].money)) or 0 local newBal = oldBal + amount if newBal < 0 then callback(false, oldBal, "Insufficient offline funds on GMR.") return end local updQuery = string.format("INSERT INTO player_economy (steamid64, money) VALUES ('%s', %d) ON DUPLICATE KEY UPDATE money = %d;", steamid64, newBal, newBal) _G.GMR_DB:Query(updQuery, function() callback(true, newBal) end, function(err) callback(false, oldBal, tostring(err)) end) end, function(err) callback(false, 0, tostring(err)) end) return end -- Fallback SQLite offline update local oldBal = tonumber(sql.QueryValue(string.format("SELECT value FROM playerpdata WHERE infoid = '%s' LIMIT 1;", steamid64))) or 0 local newBal = oldBal + amount if newBal < 0 then callback(false, oldBal, "Insufficient offline funds.") return end sql.Query(string.format("INSERT OR REPLACE INTO playerpdata (infoid, value) VALUES ('%s', '%s');", steamid64, tostring(newBal))) callback(true, newBal) end -- --------------------------------------------------------------------------- -- 3. ESCROW SYSTEM: AUTO-CLAIM PENDING TRANSFERS ON PLAYER JOIN -- --------------------------------------------------------------------------- -- If a player transferred money from PERP-X before creating their GMR character, -- this hook automatically contacts PERP-X on player spawn and awards the money! if GMR_BRIDGE.AutoClaimOnSpawn then hook.Add("PlayerInitialSpawn", "GMR_CheckPendingTransfers", function(ply) if not IsValid(ply) or ply:IsBot() then return end local steamid64 = ply:SteamID64() if not steamid64 then return end timer.Simple(4, function() if not IsValid(ply) then return end local postData = { action = "claim_pending", steamid64 = steamid64, api_key = GMR_BRIDGE.ApiKey } http.Post(GMR_BRIDGE.ApiUrl, postData, function(body) local data = util.JSONToTable(body or "") if data and data.success and data.claimed_amount and data.claimed_amount > 0 then local amount = tonumber(data.claimed_amount) or 0 GMR_Transfer_AdjustBalance(steamid64, amount, "Claimed pending PERP-X transfer", function(ok, newBal) if ok and IsValid(ply) then ply:ChatPrint(string.format("[GMR Transfer] Welcome! You had $%s in pending transfers from PERP-X, which has now been credited to your GMR character!", string.Comma(amount))) surface_PlaySound = "garrysmod/save_load1.wav" end end) end end, function(err) print("[GMR Bridge] Error checking pending transfers: " .. tostring(err)) end) end) end) end -- --------------------------------------------------------------------------- -- 4. CONSOLE COMMANDS (FOR GMR SERVER ADMINS) -- --------------------------------------------------------------------------- concommand.Add("gmr_bridge_test", function(ply, cmd, args) if IsValid(ply) and not ply:IsSuperAdmin() then return end local targetSid = args[1] or (IsValid(ply) and ply:SteamID64()) or "76561198012507393" print("[GMR Bridge] Testing connection to PERP-X Transfer API...") http.Fetch(GMR_BRIDGE.ApiUrl .. "?action=status&api_key=" .. GMR_BRIDGE.ApiKey, function(body) print("[GMR Bridge] API Response: " .. tostring(body)) end, function(err) print("[GMR Bridge] Connection Failed: " .. tostring(err)) end) end) print("[GMR Bridge] PERP-X <-> GMR Balance Bridge loaded successfully.")