ALTER TABLE fragment to add it if missing. $autoRepairCols = [ 'description' => "ADD COLUMN `description` VARCHAR(1000) NOT NULL DEFAULT ''", 'img_url' => "ADD COLUMN `img_url` VARCHAR(255) NOT NULL DEFAULT ''", 'out_of_stock' => "ADD COLUMN `out_of_stock` VARCHAR(255) NOT NULL DEFAULT ''", 'slot_min_qty' => "ADD COLUMN `slot_min_qty` INT(11) NOT NULL DEFAULT 0", 'slot_max_qty' => "ADD COLUMN `slot_max_qty` INT(11) NOT NULL DEFAULT 0", 'price_daily' => "ADD COLUMN `price_daily` FLOAT(15,4) NOT NULL DEFAULT 0", 'price_monthly' => "ADD COLUMN `price_monthly` FLOAT(15,4) NOT NULL DEFAULT 0", 'price_year' => "ADD COLUMN `price_year` FLOAT(15,4) NOT NULL DEFAULT 0", 'remote_server_id' => "ADD COLUMN `remote_server_id` VARCHAR(255) NOT NULL DEFAULT ''", 'install_method' => "ADD COLUMN `install_method` VARCHAR(255) NOT NULL DEFAULT 'steamcmd'", ]; foreach ($autoRepairCols as $col => $alterFragment) { if (!col_exists($db, $tableName, $col)) { if ($db->query("ALTER TABLE `{$tableName}` {$alterFragment}")) { $messages[] = "✔ Auto-repaired: added column '{$col}' to {$tableName}."; } else { $messages[] = "✖ Could not add column '{$col}' to {$tableName}: " . $db->error; } } } // If critical columns are still missing after repair, skip the sync to avoid SQL errors. foreach (['service_name', 'mod_cfg_id', 'enabled'] as $critical) { if (!col_exists($db, $tableName, $critical)) { $messages[] = "⚠ Critical column '{$critical}' missing from {$tableName}; skipping sync."; return $messages; } } // Load all games/mods from panel config tables $gameMods = []; $res = $db->query( "SELECT cm.mod_cfg_id, cm.home_cfg_id, cm.mod_name, ch.game_name FROM `{$prefix}config_mods` cm JOIN `{$prefix}config_homes` ch ON ch.home_cfg_id = cm.home_cfg_id ORDER BY ch.game_name, cm.mod_name" ); if ($res) { while ($row = $res->fetch_assoc()) { $gameMods[(int)$row['mod_cfg_id']] = $row; } } if (empty($gameMods)) { // config_mods is empty or tables don't exist yet — nothing to sync return $messages; } // Load existing billing_services indexed by mod_cfg_id $existing = []; $svcRes = $db->query( "SELECT service_id, mod_cfg_id, enabled, out_of_stock FROM `{$prefix}billing_services`" ); if ($svcRes) { while ($row = $svcRes->fetch_assoc()) { $existing[(int)$row['mod_cfg_id']] = $row; } } // Insert new rows for game/mods not yet in billing_services foreach ($gameMods as $modCfgId => $gm) { if (isset($existing[$modCfgId])) { continue; } $svcName = $db->real_escape_string($gm['mod_name'] ?: $gm['game_name']); $homeCfgId = (int)$gm['home_cfg_id']; // remote_server_id is intentionally empty: no servers are assigned until // an admin reviews and enables the service on the adminserverlist page. $db->query( "INSERT INTO `{$prefix}billing_services` (home_cfg_id, mod_cfg_id, service_name, description, remote_server_id, enabled, out_of_stock, price_daily, price_monthly, price_year, slot_min_qty, slot_max_qty, install_method) VALUES ({$homeCfgId}, {$modCfgId}, '{$svcName}', '{$svcName}', '', 0, 0, 0, 0, 0, 1, 100, 'steamcmd')" ); $messages[] = "Added new service: " . ($gm['mod_name'] ?: $gm['game_name']); } // Soft-disable billing_services whose mod_cfg_id no longer appears in config_mods foreach ($existing as $modCfgId => $svcRow) { if ($modCfgId > 0 && !isset($gameMods[$modCfgId])) { $sid = (int)$svcRow['service_id']; $db->query( "UPDATE `{$prefix}billing_services` SET enabled = 0, out_of_stock = 1 WHERE service_id = {$sid} AND enabled = 1" ); if ($db->affected_rows > 0) { $messages[] = "Service ID {$sid} disabled — game mod no longer in config."; } } } return $messages; } $syncMessages = sync_billing_services($db, $table_prefix); $flash = []; $flashType = 'ok'; /* ----------------------------------------------------------------------- SAVE: service configuration form submitted ----------------------------------------------------------------------- */ if (isset($_POST['save_services'])) { // Load valid remote server IDs for validation $validServerIds = []; $rsRes = $db->query("SELECT remote_server_id FROM `{$table_prefix}remote_servers`"); while ($rsRes && ($rsRow = $rsRes->fetch_assoc())) { $validServerIds[] = (int)$rsRow['remote_server_id']; } $validSet = array_flip($validServerIds); $postedServices = $_POST['svc'] ?? []; $postedServers = $_POST['servers'] ?? []; foreach ((array)$postedServices as $sid => $svcData) { $sid = (int)$sid; $enabled = isset($svcData['enabled']) ? 1 : 0; $priceDaily = number_format((float)($svcData['price_daily'] ?? 0), 4, '.', ''); $priceMonthly = number_format((float)($svcData['price_monthly'] ?? 0), 4, '.', ''); $priceYear = number_format((float)($svcData['price_year'] ?? 0), 4, '.', ''); $slotMin = max(0, (int)($svcData['slot_min_qty'] ?? 0)); $slotMax = max(0, (int)($svcData['slot_max_qty'] ?? 0)); if ($slotMax < $slotMin) { $slotMax = $slotMin; } // Build comma-separated remote_server_id from checkboxes, validating each ID $checkedIds = []; foreach ((array)($postedServers[$sid] ?? []) as $rawId) { $rid = (int)$rawId; if (isset($validSet[$rid])) { $checkedIds[] = $rid; } } $remoteServerIdStr = $db->real_escape_string(implode(',', $checkedIds)); $db->query( "UPDATE `{$table_prefix}billing_services` SET enabled = {$enabled}, price_daily = '{$priceDaily}', price_monthly = '{$priceMonthly}', price_year = '{$priceYear}', slot_min_qty = {$slotMin}, slot_max_qty = {$slotMax}, remote_server_id = '{$remoteServerIdStr}' WHERE service_id = {$sid}" ); } $flash[] = "Services saved."; } /* ----------------------------------------------------------------------- Load data for display ----------------------------------------------------------------------- */ $remoteServers = []; $rsRes = $db->query( "SELECT remote_server_id, remote_server_name FROM `{$table_prefix}remote_servers` ORDER BY remote_server_name" ); while ($rsRes && ($row = $rsRes->fetch_assoc())) { $remoteServers[] = $row; } $services = []; $svcRes = $db->query( "SELECT service_id, service_name, enabled, price_daily, price_monthly, price_year, slot_min_qty, slot_max_qty, remote_server_id FROM `{$table_prefix}billing_services` ORDER BY service_name" ); while ($svcRes && ($row = $svcRes->fetch_assoc())) { $services[] = $row; } ?>

Service Configuration

Enable services, configure pricing and slot ranges, and select which remote servers each game can be installed on. The service list is automatically kept in sync with the panel game/mod configuration. Check one or more servers to make a game available for purchase; leaving all servers unchecked prevents the game from appearing in the store.

No billing services found. Ensure game configs are loaded in the panel (Home → Games configuration).

Game / Service Enabled Price / Day ($) Price / Month ($) Price / Year ($) Min Slots Max Slots Available Servers
ID:
> No remote servers configured

Notes: