automatic limit applies const OV_UNLIMITED = 1; // exempt (WW villages, trusted) -> no limit const OV_CUSTOM = 3; // custom_limit is an absolute cap (resources / window) /* ---- DB plumbing --------------------------------------------------- */ private static function link() { if (isset($GLOBALS['link']) && $GLOBALS['link']) { return $GLOBALS['link']; } if (isset($GLOBALS['database']) && isset($GLOBALS['database']->dblink)) { return $GLOBALS['database']->dblink; } return null; } /** Create both tables if missing (lazy — works without re-running installer). */ public static function ensureSchema() { $link = self::link(); if (!$link) { return; } @mysqli_query($link, "CREATE TABLE IF NOT EXISTS `" . TB_PREFIX . "resource_transfer_log` ( `id` int(11) NOT NULL AUTO_INCREMENT, `from_vref` int(11) NOT NULL DEFAULT 0, `to_vref` int(11) NOT NULL DEFAULT 0, `from_uid` int(11) NOT NULL DEFAULT 0, `to_uid` int(11) NOT NULL DEFAULT 0, `wood` int(11) NOT NULL DEFAULT 0, `clay` int(11) NOT NULL DEFAULT 0, `iron` int(11) NOT NULL DEFAULT 0, `crop` int(11) NOT NULL DEFAULT 0, `time` int(11) NOT NULL DEFAULT 0, PRIMARY KEY (`id`), KEY `to_uid_time` (`to_uid`,`time`), KEY `from_uid_time` (`from_uid`,`time`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8"); @mysqli_query($link, "CREATE TABLE IF NOT EXISTS `" . TB_PREFIX . "push_override` ( `uid` int(11) NOT NULL, `mode` tinyint(2) NOT NULL DEFAULT 0, `custom_limit` bigint(20) NOT NULL DEFAULT 0, `note` varchar(255) NOT NULL DEFAULT '', `set_by` int(11) NOT NULL DEFAULT 0, `time` int(11) NOT NULL DEFAULT 0, PRIMARY KEY (`uid`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8"); } /* ---- Data collection (called from Automation on delivery) ---------- */ /** * Log one completed cross-player resource delivery. Best-effort; never * throws. Skips same-owner transfers and system accounts. */ public static function logTransfer($fromVref, $toVref, $fromUid, $toUid, $wood, $clay, $iron, $crop, $time = 0) { try { $fromUid = (int) $fromUid; $toUid = (int) $toUid; if ($fromUid === $toUid || $fromUid <= 3 || $toUid <= 3) { return; // own transfer or system account } $wood = (int) $wood; $clay = (int) $clay; $iron = (int) $iron; $crop = (int) $crop; if (($wood + $clay + $iron + $crop) <= 0) { return; } $link = self::link(); if (!$link) { return; } self::ensureSchema(); $fromVref = (int) $fromVref; $toVref = (int) $toVref; $time = $time > 0 ? (int) $time : time(); $stmt = mysqli_prepare($link, "INSERT INTO `" . TB_PREFIX . "resource_transfer_log` (from_vref, to_vref, from_uid, to_uid, wood, clay, iron, crop, `time`) VALUES (?,?,?,?,?,?,?,?,?)"); if (!$stmt) { return; } mysqli_stmt_bind_param($stmt, 'iiiiiiiii', $fromVref, $toVref, $fromUid, $toUid, $wood, $clay, $iron, $crop, $time); mysqli_stmt_execute($stmt); mysqli_stmt_close($stmt); } catch (\Throwable $e) { // never break the automation cron } } /* ---- Overrides ----------------------------------------------------- */ /** All overrides as uid => ['mode','custom_limit','note','set_by','time']. */ public static function overrides() { $link = self::link(); $out = []; if (!$link) { return $out; } self::ensureSchema(); $r = @mysqli_query($link, "SELECT * FROM `" . TB_PREFIX . "push_override`"); if ($r) { while ($row = mysqli_fetch_assoc($r)) { $out[(int) $row['uid']] = $row; } mysqli_free_result($r); } return $out; } /** Insert/update or clear an override. mode OV_NONE removes it. */ public static function setOverride($uid, $mode, $customLimit, $note, $adminId) { $link = self::link(); if (!$link) { return false; } self::ensureSchema(); $uid = (int) $uid; $mode = (int) $mode; $custom = (int) $customLimit; $admin = (int) $adminId; $now = time(); if ($uid <= 3) { return false; } if ($mode === self::OV_NONE) { $stmt = mysqli_prepare($link, "DELETE FROM `" . TB_PREFIX . "push_override` WHERE uid = ?"); if (!$stmt) { return false; } mysqli_stmt_bind_param($stmt, 'i', $uid); $ok = mysqli_stmt_execute($stmt); mysqli_stmt_close($stmt); return $ok; } $note = substr((string) $note, 0, 255); $stmt = mysqli_prepare($link, "INSERT INTO `" . TB_PREFIX . "push_override` (uid, mode, custom_limit, note, set_by, `time`) VALUES (?,?,?,?,?,?) ON DUPLICATE KEY UPDATE mode=VALUES(mode), custom_limit=VALUES(custom_limit), note=VALUES(note), set_by=VALUES(set_by), `time`=VALUES(`time`)"); if (!$stmt) { return false; } mysqli_stmt_bind_param($stmt, 'iiisii', $uid, $mode, $custom, $note, $admin, $now); $ok = mysqli_stmt_execute($stmt); mysqli_stmt_close($stmt); return $ok; } /* ---- Exception sets (WW / artefact villages) ----------------------- */ /** Owners holding at least one WW village (fdata.f99t = 40 = WONDER). */ public static function wwOwners() { $link = self::link(); $out = []; if (!$link) { return $out; } $r = @mysqli_query($link, "SELECT DISTINCT v.owner FROM `" . TB_PREFIX . "vdata` v JOIN `" . TB_PREFIX . "fdata` f ON v.wref = f.vref WHERE f.f99t = 40 AND v.owner > 3"); if ($r) { while ($row = mysqli_fetch_row($r)) { $out[(int) $row[0]] = true; } mysqli_free_result($r); } return $out; } /** Owners holding at least one active artefact. */ public static function artefactOwners() { $link = self::link(); $out = []; if (!$link) { return $out; } $r = @mysqli_query($link, "SELECT DISTINCT owner FROM `" . TB_PREFIX . "artefacts` WHERE active = 1 AND (del = 0 OR del IS NULL) AND owner > 3"); if ($r) { while ($row = mysqli_fetch_row($r)) { $out[(int) $row[0]] = true; } mysqli_free_result($r); } return $out; } /* ---- Production model ---------------------------------------------- */ /** * Net hourly production for a whole account (sum of all villages). * Wood+Clay+Iron + max(0, Crop - population). Uses the same field/factory * math as Village.php (base fields + factory %, x SPEED), but omits oasis / * hero / plus bonuses (not resolvable in a batch context) — a documented, * slightly conservative approximation. Returns int resources/hour. */ public static function hourlyProduction($uid, $link = null) { $link = $link ?: self::link(); if (!$link) { return 0; } $uid = (int) $uid; $total = 0; $r = @mysqli_query($link, "SELECT f.*, v.pop FROM `" . TB_PREFIX . "fdata` f JOIN `" . TB_PREFIX . "vdata` v ON v.wref = f.vref WHERE v.owner = " . $uid); if (!$r) { return 0; } while ($f = mysqli_fetch_assoc($r)) { list($w, $c, $i, $cr) = self::villageProd($f); $pop = (int) ($f['pop'] ?? 0); $cropNet = max(0, $cr - $pop); $total += $w + $c + $i + $cropNet; } mysqli_free_result($r); return (int) $total; } /** Per-village production [wood,clay,iron,crop] per hour from an fdata row. */ private static function villageProd(array $f) { global $bid1, $bid2, $bid3, $bid4, $bid5, $bid6, $bid7, $bid8, $bid9; if (!isset($bid1)) { return [0, 0, 0, 0]; // buidata not loaded in this context } $wood = $clay = $iron = $crop = 0; $saw = $brick = $found = $mill = $bake = 0; for ($i = 1; $i <= 38; $i++) { $t = (int) ($f["f{$i}t"] ?? 0); $lv = (int) ($f["f{$i}"] ?? 0); if ($t === 1) { $wood += $bid1[$lv]['prod'] ?? 0; } elseif ($t === 2) { $clay += $bid2[$lv]['prod'] ?? 0; } elseif ($t === 3) { $iron += $bid3[$lv]['prod'] ?? 0; } elseif ($t === 4) { $crop += $bid4[$lv]['prod'] ?? 0; } elseif ($t === 5) { $saw = $lv; } elseif ($t === 6) { $brick = $lv; } elseif ($t === 7) { $found = $lv; } elseif ($t === 8) { $mill = $lv; } elseif ($t === 9) { $bake = $lv; } } if ($saw) { $wood += $wood / 100 * ($bid5[$saw]['attri'] ?? 0); } if ($brick) { $clay += $clay / 100 * ($bid6[$brick]['attri'] ?? 0); } if ($found) { $iron += $iron / 100 * ($bid7[$found]['attri'] ?? 0); } if ($mill || $bake) { $crop += $crop / 100 * (($bid8[$mill]['attri'] ?? 0) + ($bid9[$bake]['attri'] ?? 0)); } $spd = defined('SPEED') ? SPEED : 1; return [ (int) round($wood * $spd), (int) round($clay * $spd), (int) round($iron * $spd), (int) round($crop * $spd), ]; } /* ---- 7-day balance ------------------------------------------------- */ /** * Total resources each player RECEIVED FROM OTHERS in the window, keyed by * to_uid. Returns uid => ['total','wood','clay','iron','crop','senders']. */ public static function receivedFromOthers($sinceTs, $link = null) { $link = $link ?: self::link(); $out = []; if (!$link) { return $out; } self::ensureSchema(); $r = @mysqli_query($link, "SELECT to_uid, SUM(wood) w, SUM(clay) c, SUM(iron) i, SUM(crop) cr, SUM(wood+clay+iron+crop) tot, COUNT(DISTINCT from_uid) senders FROM `" . TB_PREFIX . "resource_transfer_log` WHERE `time` >= " . (int) $sinceTs . " AND from_uid <> to_uid GROUP BY to_uid"); if ($r) { while ($row = mysqli_fetch_assoc($r)) { $out[(int) $row['to_uid']] = [ 'total' => (int) $row['tot'], 'wood' => (int) $row['w'], 'clay' => (int) $row['c'], 'iron' => (int) $row['i'], 'crop' => (int) $row['cr'], 'senders' => (int) $row['senders'], ]; } mysqli_free_result($r); } return $out; } /* ---- Dashboard ----------------------------------------------------- */ /** * Build the push-protection dashboard rows. * * @param array $opts 'days','hours','only' ('all'|'over'|'flagged'). * @return array ['rows'=>..., 'window_days'=>..., 'hours_allowed'=>...] */ public static function dashboard(array $opts = []) { $link = self::link(); if (!$link) { return ['rows' => [], 'window_days' => self::WINDOW_DAYS, 'hours_allowed' => self::HOURS_ALLOWED]; } self::ensureSchema(); $days = isset($opts['days']) ? max(1, (int) $opts['days']) : self::WINDOW_DAYS; $hours = isset($opts['hours']) ? max(1, (int) $opts['hours']) : self::HOURS_ALLOWED; $only = $opts['only'] ?? 'all'; $since = time() - $days * 86400; $received = self::receivedFromOthers($since, $link); $overrides = self::overrides(); $ww = self::wwOwners(); $arte = self::artefactOwners(); // Candidate set: anyone who received from others, plus flagged/override. $uids = []; foreach (array_keys($received) as $u) { $uids[$u] = true; } foreach (array_keys($overrides) as $u) { $uids[$u] = true; } foreach (array_keys($ww) as $u) { $uids[$u] = true; } foreach (array_keys($arte) as $u) { $uids[$u] = true; } unset($uids[0]); if (empty($uids)) { return ['rows' => [], 'window_days' => $days, 'hours_allowed' => $hours]; } // Names / pop / village count in one pass. $in = implode(',', array_map('intval', array_keys($uids))); $meta = []; $r = @mysqli_query($link, "SELECT u.id, u.username, (SELECT COUNT(*) FROM `" . TB_PREFIX . "vdata` v WHERE v.owner = u.id) AS villages, (SELECT COALESCE(SUM(v2.pop),0) FROM `" . TB_PREFIX . "vdata` v2 WHERE v2.owner = u.id) AS pop FROM `" . TB_PREFIX . "users` u WHERE u.id IN ($in)"); if ($r) { while ($row = mysqli_fetch_assoc($r)) { $meta[(int) $row['id']] = $row; } mysqli_free_result($r); } $rows = []; foreach (array_keys($uids) as $uid) { $uid = (int) $uid; if ($uid <= 3 || !isset($meta[$uid])) { continue; } $rec = $received[$uid]['total'] ?? 0; $prod = self::hourlyProduction($uid, $link); $autoLimit = $hours * $prod; $isWW = isset($ww[$uid]); $isArte = isset($arte[$uid]); $ov = $overrides[$uid] ?? null; // Effective limit + status. $unlimited = false; $effLimit = $autoLimit; $ovLabel = ''; if ($ov) { $mode = (int) $ov['mode']; if ($mode === self::OV_UNLIMITED) { $unlimited = true; $ovLabel = 'Exempt (manual)'; } elseif ($mode === self::OV_CUSTOM) { $effLimit = (int) $ov['custom_limit']; $ovLabel = 'Custom cap'; } } if ($unlimited) { $pct = 0; $status = 'Exempt'; } elseif ($effLimit <= 0) { $pct = $rec > 0 ? 999 : 0; $status = $rec > 0 ? 'Over' : 'OK'; } else { $pct = (int) round($rec / $effLimit * 100); $status = $pct > 100 ? 'Over' : ($pct >= self::NEAR_LIMIT_PCT ? 'Near' : 'OK'); } $flags = []; if ($isWW) { $flags[] = 'WW village'; } if ($isArte) { $flags[] = 'Artefact village'; } $row = [ 'uid' => $uid, 'name' => $meta[$uid]['username'], 'villages' => (int) $meta[$uid]['villages'], 'pop' => (int) $meta[$uid]['pop'], 'prod_h' => $prod, 'auto_limit' => $autoLimit, 'eff_limit' => $unlimited ? -1 : $effLimit, // -1 = unlimited 'received' => $rec, 'senders' => $received[$uid]['senders'] ?? 0, 'pct' => $pct, 'status' => $status, 'is_ww' => $isWW, 'is_arte' => $isArte, 'ov_mode' => $ov ? (int) $ov['mode'] : self::OV_NONE, 'ov_label' => $ovLabel, 'ov_note' => $ov['note'] ?? '', 'flags' => $flags, ]; if ($only === 'over' && $status !== 'Over') { continue; } if ($only === 'flagged' && !$isWW && !$isArte && !$ov) { continue; } $rows[] = $row; } // Sort: Over first, then by pct desc, then received desc. usort($rows, function ($a, $b) { $rank = ['Over' => 0, 'Near' => 1, 'OK' => 2, 'Exempt' => 3]; $ra = $rank[$a['status']] ?? 4; $rb = $rank[$b['status']] ?? 4; if ($ra !== $rb) { return $ra <=> $rb; } if ($a['pct'] !== $b['pct']) { return $b['pct'] <=> $a['pct']; } return $b['received'] <=> $a['received']; }); return ['rows' => $rows, 'window_days' => $days, 'hours_allowed' => $hours]; } /* ---- Optional enforcement hook ------------------------------------ */ /** * OPTIONAL hard-enforcement helper for the market send flow. Returns how * many total resources $toUid may still receive from others in the window * (PHP_INT_MAX when exempt). Not wired by default — call from Market when * you want to actually block over-limit deliveries. */ public static function remainingAllowance($toUid, array $opts = []) { $link = self::link(); if (!$link) { return PHP_INT_MAX; } $toUid = (int) $toUid; $days = isset($opts['days']) ? max(1, (int) $opts['days']) : self::WINDOW_DAYS; $hours = isset($opts['hours']) ? max(1, (int) $opts['hours']) : self::HOURS_ALLOWED; $since = time() - $days * 86400; $ov = self::overrides()[$toUid] ?? null; if ($ov && (int) $ov['mode'] === self::OV_UNLIMITED) { return PHP_INT_MAX; } if (isset(self::wwOwners()[$toUid])) { return PHP_INT_MAX; // WW villages supplied without limit (TL rule) } $prod = self::hourlyProduction($toUid, $link); $limit = ($ov && (int) $ov['mode'] === self::OV_CUSTOM) ? (int) $ov['custom_limit'] : $hours * $prod; $rec = self::receivedFromOthers($since, $link)[$toUid]['total'] ?? 0; return max(0, $limit - $rec); } }