# PERP-X ⇄ GMR Balance Transfer System
## Integration & Setup Documentation for the GMR Team

Welcome! This system provides a standalone web portal where players and staff administrators can transfer economy balances between **PERP-X** and **GMR**.

> **Quick Setup:** You can download the ready-to-drop Garry's Mod Lua script directly from the portal by clicking **[⬇️ Download GMR Lua](https://admin.perpx.net/apps/gmr/index.php?download=lua)** in the header.

---

### Handling Players Without a GMR Character (Automatic Escrow)
- **The Problem**: Players may initiate a balance transfer from PERP-X to GMR before they have ever joined the GMR server or created a character.
- **How It's Solved**:
  - The web portal checks whether an active account exists for the player's SteamID on GMR.
  - If no character is found, the web portal warns the player. If they proceed with the transfer, the money is placed into **Secure Pending Escrow**.
  - When the player connects to the GMR server and spawns for the first time, the included Lua bridge (`gmr_eco_bridge.lua`) calls our API (`action=claim_pending`) and **automatically awards the transferred money directly into their newly created in-game character wallet**, playing a sound and notifying them in chat.
  - **Zero money vanishes into thin air.**

---

### Core Architecture & Account Rules

1. **GMR Account Model**:
   - **One account per SteamID64**. GMR balances are tied directly to the player's SteamID.
   - There is no character selection on GMR; each SteamID has exactly one GMR balance.

2. **PERP-X Account Model**:
   - **Multiple character accounts per SteamID**.
   - On PERP-X, a player can have multiple characters (e.g., Character #1 John Doe, Character #2 Jane Doe).
   - The web transfer portal requires the player to select **which specific PERP-X character account** is being debited or credited.

3. **Strict One-Way Money Transfer (NOT a Sync)**:
   - This is a **one-way transfer of balance**, not a mirror or continuous sync.
   - **Transfer from PERP-X to GMR**: The selected PERP-X character is deducted by $X, and the player's GMR account is credited by $X.
   - **Transfer from GMR to PERP-X**: The player's GMR account is deducted by $X, and the selected PERP-X character bank is credited by $X.
   - Example: If a player transfers $100,000 from PERP to GMR, their PERP character now has $0 and GMR has $100,000.

4. **Zero Changes Required on the PERP-X End**:
   - No in-game console commands.
   - No querying the web database in-game.
   - Everything is handled directly by the web portal with atomic MySQL transactions and concurrency locks to support both online and offline players.

---

### Giving Our System Access to GMR's Economy

The transfer system needs access to your server's economy to read balances and execute transfers. You can provide this in **one of two ways**:

---

### Option A: 2 Custom Functions (Recommended)

Since GMR runs its own custom gamemode and economy (not DarkRP), you only need to expose **2 custom functions** to connect our transfer portal to your economy:

1. **`GetGMRBalance(steamid64)`**: Returns the player's balance (online from memory or offline from your database).
2. **`AdjustGMRBalance(steamid64, amount, reason)`**: Modifies the player's balance (`amount` is positive for deposits from PERP-X, negative for withdrawals to PERP-X).

#### 1. In-Game Lua Implementation (GMR Server)

Hook your custom economy functions into a small bridge file on your GMR server (e.g. `garrysmod/lua/autorun/server/gmr_eco_bridge.lua`):

```lua
-- ======================================================================
-- GMR Custom Economy Bridge for Balance Transfer Portal
-- Implement your server's custom economy functions below
-- ======================================================================

--- Function 1: Get a player's balance (handles both online and offline)
-- @param steamid64 string Target player SteamID64
-- @param callback function(success, balanceNumber, errorMsg)
function GMR_Transfer_GetBalance(steamid64, callback)
    steamid64 = tostring(steamid64)

    -- 1. Check if the player is currently online on GMR
    local ply = player.GetBySteamID64(steamid64)
    if IsValid(ply) then
        -- >>> REPLACE THIS WITH YOUR CUSTOM IN-GAME GET BALANCE METHOD <<<
        local currentBalance = ply:GetGMRMoney() -- e.g. ply:GetMoney(), ply:GetBalance(), etc.
        callback(true, tonumber(currentBalance) or 0)
        return
    end

    -- 2. If the player is offline, fetch their balance from your database
    -- >>> REPLACE THIS WITH YOUR CUSTOM DATABASE QUERY <<<
    myCustomDB:Query(string.format("SELECT money FROM player_economy WHERE steamid64 = '%s' LIMIT 1", steamid64), 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)
end

--- Function 2: Adjust player 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

    -- 1. Check if player is currently online on GMR
    local ply = player.GetBySteamID64(steamid64)
    if IsValid(ply) then
        -- >>> REPLACE THIS WITH YOUR CUSTOM IN-GAME ADD/DEDUCT METHOD <<<
        local currentBal = ply:GetGMRMoney() or 0
        if amount < 0 and math.abs(amount) > currentBal then
            callback(false, currentBal, "Insufficient funds on GMR server.")
            return
        end

        ply:AddGMRMoney(amount) -- e.g. ply:AddMoney(amount), ply:TakeMoney(amt), etc.
        local newBal = ply:GetGMRMoney() or 0

        -- Optional: In-game chat notification for player
        if amount > 0 then
            ply:ChatPrint("[Transfer] Received $" .. string.Comma(amount) .. " transferred from PERP-X!")
        else
            ply:ChatPrint("[Transfer] Transferred $" .. string.Comma(math.abs(amount)) .. " from GMR to PERP-X.")
        end

        callback(true, newBal)
        return
    end

    -- 2. If the player is offline, update your database directly
    -- >>> REPLACE THIS WITH YOUR CUSTOM OFFLINE UPDATE QUERY <<<
    myCustomDB:Query(string.format("SELECT money FROM player_economy WHERE steamid64 = '%s' LIMIT 1", steamid64), function(results)
        local oldBal = (results and results[1] and tonumber(results[1].money)) or 0
        local newBal = oldBal + amount

        if newBal < 0 then
            callback(false, oldBal, "Insufficient offline funds on GMR server.")
            return
        end

        myCustomDB:Query(string.format("UPDATE player_economy SET money = %d WHERE steamid64 = '%s'", newBal, steamid64), function()
            callback(true, newBal)
        end, function(err)
            callback(false, oldBal, tostring(err))
        end)
    end, function(err)
        callback(false, 0, tostring(err))
    end)
end
```

#### 2. Web Endpoint Implementation (If hosting on GMR's Web Server)

If you prefer an HTTP webhook endpoint, create a simple script (e.g. `gmr_transfer_api.php`) on your web host:

```php
<?php
// GMR Economy Webhook Endpoint
header('Content-Type: application/json');

$apiKey = "gmr_sec_9f7a8b3c1d4e2f6a8b0c5d7e3f1a9b2c";
$providedKey = $_SERVER['HTTP_X_API_KEY'] ?? '';

if (!hash_equals($apiKey, $providedKey)) {
    http_response_code(401);
    echo json_encode(['success' => false, 'error' => 'Unauthorized']);
    exit;
}

$input = json_decode(file_get_contents('php://input'), true);
$action = $input['action'] ?? '';
$steamid64 = $input['steamid64'] ?? '';
$amount = (int)($input['amount'] ?? 0);

// Connect to your GMR economy database
$db = new PDO("mysql:host=localhost;dbname=gmr_server", "gmr_user", "password");

// 1. Get Balance
if ($action === 'get_balance') {
    $stmt = $db->prepare("SELECT money FROM player_economy WHERE steamid64 = ? LIMIT 1");
    $stmt->execute([$steamid64]);
    $bal = (int)($stmt->fetchColumn() ?: 0);
    echo json_encode(['success' => true, 'total' => $bal, 'wallet' => $bal]);
    exit;
}

// 2. Adjust Balance
if ($action === 'adjust_balance') {
    $db->beginTransaction();
    $stmt = $db->prepare("SELECT money FROM player_economy WHERE steamid64 = ? FOR UPDATE");
    $stmt->execute([$steamid64]);
    $oldBal = (int)($stmt->fetchColumn() ?: 0);
    $newBal = $oldBal + $amount;

    if ($newBal < 0) {
        $db->rollBack();
        echo json_encode(['success' => false, 'error' => 'Insufficient funds on GMR']);
        exit;
    }

    $upd = $db->prepare("UPDATE player_economy SET money = ? WHERE steamid64 = ?");
    $upd->execute([$newBal, $steamid64]);
    $db->commit();

    echo json_encode(['success' => true, 'old_balance' => $oldBal, 'new_balance' => $newBal]);
    exit;
}
```

---

### Option B: Direct MySQL Database Access

If your GMR server stores player money directly in MySQL / MariaDB and you wish to grant the transfer portal direct query access, provide your connection details to the PERP-X administrator:

```php
'gmr_sync' => [
    'enabled' => true,
    'gmr_db' => [
        'enabled'     => true,
        'host'        => 'gmr.yourdomain.com', // Your database IP/hostname
        'port'        => 3306,
        'dbname'      => 'your_economy_db',    // Your database name
        'username'    => 'transfer_user',       // User with SELECT and UPDATE permissions
        'password'    => 'your_password',
        'table'       => 'player_economy',      // Your player economy table
        'steamid_col' => 'steamid64',           // SteamID column
        'money_col'   => 'money',               // Money/balance column
        'bank_col'    => null                   // Optional bank column
    ]
]
```

The portal handles row-level locking (`FOR UPDATE`) and balance validation automatically.

---

### Whitelisted Staff Administrators

To grant GMR or PERP-X staff permission to view all player balances, execute manual staff transfers, view audit logs, and monitor suspected cheaters:

Add their SteamID64s to `web/config.php`:
```php
'gmr_sync' => [
    'admin_steamids' => [
        '76561198012507393', // BayShiro (Root)
        '76561198XXXXXXXXX', // GMR Head Admin SteamID64
    ]
]
```
