mirror of
https://github.com/Shadowss/TravianZ.git
synced 2026-07-16 01:26:07 +00:00
Add MultiAccount Detection, Add Push Protection Detection, Add register Default Gold
Add MultiAccount Detection, Add Push Protection Detection, Add register Default Gold
This commit is contained in:
@@ -105,10 +105,6 @@ $editIcon = '<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke=
|
||||
<tr><td>New Alliance & Embassy Mechanics <em class="tooltip">?<span class="classic">https://github.com/Shadowss/TravianZ/wiki/New-Alliance-&-Embassy-Mechanics</span></em></td><td><?php echo NEW_FUNCTIONS_EMBASSY_MECHANICS ? "<span class='badge green'>Enabled</span>" : "<span class='badge red'>Disabled</span>"; ?></td></tr>
|
||||
<tr><td>New forum post message <em class="tooltip">?<span class="classic">Enable (Disable) forum subscription messages</span></em></td><td><?php echo NEW_FUNCTIONS_FORUM_POST_MESSAGE ? "<span class='badge green'>Enabled</span>" : "<span class='badge red'>Disabled</span>"; ?></td></tr>
|
||||
<tr><td>Tribes images in profile <em class="tooltip">?<span class="classic">Enable (Disable) displaying images of tribes</span></em></td><td><?php echo NEW_FUNCTIONS_TRIBE_IMAGES ? "<span class='badge green'>Enabled</span>" : "<span class='badge red'>Disabled</span>"; ?></td></tr>
|
||||
<tr><td>New Tribe: Huns <em class="tooltip">?<span class="classic">Enable (Disable) the Huns tribe</span></em></td><td><?php echo (defined('NEW_FUNCTION_TRIBE_HUNS') && NEW_FUNCTION_TRIBE_HUNS) ? "<span class='badge green'>Enabled</span>" : "<span class='badge red'>Disabled</span>"; ?></td></tr>
|
||||
<tr><td>New Tribe: Egyptians <em class="tooltip">?<span class="classic">Enable (Disable) the Egyptians tribe</span></em></td><td><?php echo (defined('NEW_FUNCTION_TRIBE_EGIPTEANS') && NEW_FUNCTION_TRIBE_EGIPTEANS) ? "<span class='badge green'>Enabled</span>" : "<span class='badge red'>Disabled</span>"; ?></td></tr>
|
||||
<tr><td>New Tribe: Spartans <em class="tooltip">?<span class="classic">Enable (Disable) the Spartans tribe</span></em></td><td><?php echo (defined('NEW_FUNCTION_TRIBE_SPARTANS') && NEW_FUNCTION_TRIBE_SPARTANS) ? "<span class='badge green'>Enabled</span>" : "<span class='badge red'>Disabled</span>"; ?></td></tr>
|
||||
<tr><td>New Tribe: Vikings <em class="tooltip">?<span class="classic">Enable (Disable) the Vikings tribe</span></em></td><td><?php echo (defined('NEW_FUNCTION_TRIBE_VIKINGS') && NEW_FUNCTION_TRIBE_VIKINGS) ? "<span class='badge green'>Enabled</span>" : "<span class='badge red'>Disabled</span>"; ?></td></tr>
|
||||
<tr><td>MHs images in profile <em class="tooltip">?<span class="classic">Enable (Disable) displaying images of Multihunters</span></em></td><td><?php echo NEW_FUNCTIONS_MHS_IMAGES ? "<span class='badge green'>Enabled</span>" : "<span class='badge red'>Disabled</span>"; ?></td></tr>
|
||||
<tr><td>Display artifact in profile <em class="tooltip">?<span class="classic">Enable (Disable) the display of the artifact</span></em></td><td><?php echo NEW_FUNCTIONS_DISPLAY_ARTIFACT ? "<span class='badge green'>Enabled</span>" : "<span class='badge red'>Disabled</span>"; ?></td></tr>
|
||||
<tr><td>Display WoW in profile <em class="tooltip">?<span class="classic">Enable (Disable) the display of the wonder</span></em></td><td><?php echo NEW_FUNCTIONS_DISPLAY_WONDER ? "<span class='badge green'>Enabled</span>" : "<span class='badge red'>Disabled</span>"; ?></td></tr>
|
||||
@@ -123,6 +119,10 @@ $editIcon = '<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke=
|
||||
<tr><td>Server Milestones<em class="tooltip">?<span class="classic">Server Milestones</span></em></td><td><?php echo NEW_FUNCTIONS_MILESTONES ? "<span class='badge green'>Enabled</span>" : "<span class='badge red'>Disabled</span>"; ?></td></tr>
|
||||
<tr><td>Server Medal Reset Timer<em class="tooltip">?<span class="classic">Server Medal Reset Timer</span></em></td><td><?php echo NEW_FUNCTIONS_MEDAL_RESET ? "<span class='badge green'>Enabled</span>" : "<span class='badge red'>Disabled</span>"; ?></td></tr>
|
||||
<tr><td>T4 Hero (items, adventures, auction)<em class="tooltip">?<span class="classic">T4 Hero (items, adventures, auction)</span></em></td><td><?php echo NEW_FUNCTIONS_HERO_T4 ? "<span class='badge green'>Enabled</span>" : "<span class='badge red'>Disabled</span>"; ?></td></tr>
|
||||
<tr><td>New Tribe: Huns <em class="tooltip">?<span class="classic">Enable (Disable) the Huns tribe</span></em></td><td><?php echo (defined('NEW_FUNCTION_TRIBE_HUNS') && NEW_FUNCTION_TRIBE_HUNS) ? "<span class='badge green'>Enabled</span>" : "<span class='badge red'>Disabled</span>"; ?></td></tr>
|
||||
<tr><td>New Tribe: Egyptians <em class="tooltip">?<span class="classic">Enable (Disable) the Egyptians tribe</span></em></td><td><?php echo (defined('NEW_FUNCTION_TRIBE_EGIPTEANS') && NEW_FUNCTION_TRIBE_EGIPTEANS) ? "<span class='badge green'>Enabled</span>" : "<span class='badge red'>Disabled</span>"; ?></td></tr>
|
||||
<tr><td>New Tribe: Spartans <em class="tooltip">?<span class="classic">Enable (Disable) the Spartans tribe</span></em></td><td><?php echo (defined('NEW_FUNCTION_TRIBE_SPARTANS') && NEW_FUNCTION_TRIBE_SPARTANS) ? "<span class='badge green'>Enabled</span>" : "<span class='badge red'>Disabled</span>"; ?></td></tr>
|
||||
<tr><td>New Tribe: Vikings <em class="tooltip">?<span class="classic">Enable (Disable) the Vikings tribe</span></em></td><td><?php echo (defined('NEW_FUNCTION_TRIBE_VIKINGS') && NEW_FUNCTION_TRIBE_VIKINGS) ? "<span class='badge green'>Enabled</span>" : "<span class='badge red'>Disabled</span>"; ?></td></tr>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -287,6 +287,23 @@ if($_SESSION['access'] < 9) die(ACCESS_DENIED_ADMIN);
|
||||
<option value="False" <?php if(!defined('NEW_FUNCTION_TRIBE_VIKINGS') || NEW_FUNCTION_TRIBE_VIKINGS == false) echo "selected";?>>False</option>
|
||||
</select>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="b">Registration Bonus Gold <em class="tooltip">?<span class="classic">Enable (Disable) a one-time gold bonus granted to every newly registered player at account creation (system accounts id≤3 are skipped). The amount is set in the field below.</span></em>
|
||||
<?php if (!defined('NEW_FUNCTION_REGISTRATION_GOLD')): ?><br><span style="color:#c0392b;font-size:11px;font-weight:normal;text-transform:none;">Not present in config.php yet — saving this form once will add it (defaults to False until then).</span><?php endif; ?>
|
||||
</td>
|
||||
<td>
|
||||
<select name="new_function_registration_gold">
|
||||
<option value="True" <?php if(defined('NEW_FUNCTION_REGISTRATION_GOLD') && NEW_FUNCTION_REGISTRATION_GOLD == true) echo "selected";?>>True</option>
|
||||
<option value="False" <?php if(!defined('NEW_FUNCTION_REGISTRATION_GOLD') || NEW_FUNCTION_REGISTRATION_GOLD == false) echo "selected";?>>False</option>
|
||||
</select>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="b">Registration Bonus Gold – amount <em class="tooltip">?<span class="classic">How much gold each new player receives when the toggle above is True. Default: 200.</span></em></td>
|
||||
<td>
|
||||
<input type="number" min="0" step="1" name="new_function_registration_gold_value" value="<?php echo (defined('NEW_FUNCTION_REGISTRATION_GOLD_VALUE') ? (int) NEW_FUNCTION_REGISTRATION_GOLD_VALUE : 200); ?>">
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
@@ -0,0 +1,168 @@
|
||||
<?php
|
||||
#################################################################################
|
||||
## Filename : Admin/Templates/multiacc.tpl ##
|
||||
## Type : Admin page - Multi-Account Detection ##
|
||||
## Project : TravianZ ##
|
||||
## License : TravianZ Project ##
|
||||
#################################################################################
|
||||
|
||||
// Access: admins (9) and multihunters (8) only.
|
||||
if (!isset($_SESSION['access']) || $_SESSION['access'] < MULTIHUNTER) {
|
||||
echo '<p style="color:#f87171;padding:16px;">Access denied.</p>';
|
||||
return;
|
||||
}
|
||||
|
||||
// Filters (GET). All optional.
|
||||
$days = isset($_GET['days']) && ctype_digit((string)$_GET['days']) ? max(1, min(365, (int)$_GET['days'])) : MultiAccount::WINDOW_DAYS;
|
||||
$minScore = isset($_GET['min']) && ctype_digit((string)$_GET['min']) ? max(0, min(100, (int)$_GET['min'])) : MultiAccount::MIN_REPORT_SCORE;
|
||||
$focusUid = isset($_GET['focus']) && ctype_digit((string)$_GET['focus']) ? (int)$_GET['focus'] : 0;
|
||||
|
||||
$data = MultiAccount::riskPairs(['days' => $days, 'min_score' => $minScore, 'focus_uid' => $focusUid]);
|
||||
$pairs = $data['pairs'];
|
||||
?>
|
||||
<style>
|
||||
.mad-wrap{color:#e2e8f0;font-family:Verdana,Arial,sans-serif;font-size:12px;padding:6px 4px 26px;}
|
||||
.mad-wrap h2{font-size:18px;margin:0 0 4px;color:#fff;}
|
||||
.mad-wrap h2 span{color:#f59e0b;}
|
||||
.mad-intro{color:#94a3b8;font-size:11px;margin:0 0 14px;max-width:820px;line-height:1.5;}
|
||||
.mad-filter{background:#111827;border:1px solid #1f2937;border-radius:8px;padding:12px 14px;display:flex;flex-wrap:wrap;gap:14px;align-items:flex-end;margin-bottom:16px;}
|
||||
.mad-filter label{display:block;font-size:10px;text-transform:uppercase;letter-spacing:.6px;color:#94a3b8;margin-bottom:4px;}
|
||||
.mad-filter input{background:#0b1220;border:1px solid #334155;border-radius:6px;color:#e2e8f0;padding:6px 8px;width:110px;}
|
||||
.mad-filter button{background:#f59e0b;color:#111827;font-weight:bold;border:0;border-radius:6px;padding:8px 16px;cursor:pointer;}
|
||||
.mad-filter a.reset{color:#94a3b8;font-size:11px;text-decoration:none;padding:8px 4px;}
|
||||
.mad-meta{color:#64748b;font-size:11px;margin-bottom:10px;}
|
||||
.mad-warn{background:#422006;border:1px solid #92400e;color:#fbbf24;border-radius:6px;padding:8px 12px;font-size:11px;margin-bottom:12px;}
|
||||
.mad-table{width:100%;border-collapse:collapse;background:#0b1220;border:1px solid #1f2937;border-radius:8px;overflow:hidden;}
|
||||
.mad-table th{background:#111827;text-align:left;padding:9px 10px;font-size:10px;text-transform:uppercase;letter-spacing:.5px;color:#94a3b8;border-bottom:1px solid #1f2937;}
|
||||
.mad-table td{padding:9px 10px;border-bottom:1px solid #14203a;vertical-align:top;}
|
||||
.mad-table tr:hover td{background:#0f1a30;}
|
||||
.mad-acc a{color:#e2e8f0;text-decoration:none;font-weight:bold;}
|
||||
.mad-acc a:hover{color:#f59e0b;}
|
||||
.mad-acc .uid{color:#475569;font-weight:normal;font-size:10px;}
|
||||
.mad-acc .sub{display:block;margin-top:2px;}
|
||||
.mad-acc .sub a{font-size:10px;color:#64748b;font-weight:normal;margin-right:8px;}
|
||||
.mad-score{font-weight:bold;font-size:14px;}
|
||||
.badge{display:inline-block;padding:2px 8px;border-radius:10px;font-size:10px;font-weight:bold;text-transform:uppercase;letter-spacing:.5px;}
|
||||
.b-high{background:#7f1d1d;color:#fecaca;}
|
||||
.b-med{background:#78350f;color:#fde68a;}
|
||||
.b-low{background:#1e3a5f;color:#bfdbfe;}
|
||||
.chips span{display:inline-block;background:#1e293b;color:#cbd5e1;border-radius:4px;padding:2px 7px;margin:2px 3px 0 0;font-size:10px;}
|
||||
.mad-empty{padding:26px;text-align:center;color:#64748b;}
|
||||
.mad-bar{height:6px;background:#1e293b;border-radius:3px;margin-top:4px;overflow:hidden;}
|
||||
.mad-bar i{display:block;height:100%;}
|
||||
</style>
|
||||
|
||||
<div class="mad-wrap">
|
||||
<h2>Multi-Account <span>Detection</span></h2>
|
||||
<p class="mad-intro">
|
||||
Heuristic correlation of account pairs by shared IP, subnet, device/browser
|
||||
fingerprint (User-Agent), overlapping login times, and resource-transfer flow.
|
||||
This is a <b>risk score, not proof</b> — use it to prioritise which
|
||||
pairs a human should investigate. Nothing is banned automatically.
|
||||
</p>
|
||||
|
||||
<form method="get" action="admin.php" class="mad-filter">
|
||||
<input type="hidden" name="p" value="multiacc">
|
||||
<div>
|
||||
<label>Window (days)</label>
|
||||
<input type="number" name="days" min="1" max="365" value="<?php echo (int)$days; ?>">
|
||||
</div>
|
||||
<div>
|
||||
<label>Min score</label>
|
||||
<input type="number" name="min" min="0" max="100" value="<?php echo (int)$minScore; ?>">
|
||||
</div>
|
||||
<div>
|
||||
<label>Focus on UID (optional)</label>
|
||||
<input type="number" name="focus" min="0" value="<?php echo $focusUid ?: ''; ?>">
|
||||
</div>
|
||||
<button type="submit">Analyse</button>
|
||||
<?php if ($focusUid || $days != MultiAccount::WINDOW_DAYS || $minScore != MultiAccount::MIN_REPORT_SCORE): ?>
|
||||
<a class="reset" href="admin.php?p=multiacc">reset</a>
|
||||
<?php endif; ?>
|
||||
</form>
|
||||
|
||||
<?php if ($data['truncated']): ?>
|
||||
<div class="mad-warn">
|
||||
Row cap reached while scanning login history (window is large / very busy server).
|
||||
Results still valid but may be incomplete — narrow the window for full coverage.
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<div class="mad-meta">
|
||||
Window: last <?php echo (int)$data['window_days']; ?> days |
|
||||
scanned <?php echo (int)$data['scanned']['login_log']; ?> login-log +
|
||||
<?php echo (int)$data['scanned']['mad_session']; ?> fingerprint rows |
|
||||
<?php echo count($pairs); ?> suspicious pair<?php echo count($pairs) == 1 ? '' : 's'; ?>
|
||||
<?php if ($focusUid): ?> | focused on UID <?php echo (int)$focusUid; ?><?php endif; ?>
|
||||
</div>
|
||||
|
||||
<?php if (empty($pairs)): ?>
|
||||
<div class="mad-table"><div class="mad-empty">
|
||||
No account pairs at or above the current score threshold.<br>
|
||||
<?php if ((int)$data['scanned']['mad_session'] === 0): ?>
|
||||
Tip: the User-Agent signal starts filling in only after players log in
|
||||
once with this feature deployed. IP & login-time signals work on
|
||||
existing history immediately.
|
||||
<?php else: ?>
|
||||
Try lowering the minimum score or widening the window.
|
||||
<?php endif; ?>
|
||||
</div></div>
|
||||
<?php else: ?>
|
||||
<table class="mad-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th style="width:80px;">Risk</th>
|
||||
<th>Account A</th>
|
||||
<th>Account B</th>
|
||||
<th style="width:44%;">Why</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<?php foreach ($pairs as $p):
|
||||
$cls = $p['label'] === 'High' ? 'b-high' : ($p['label'] === 'Medium' ? 'b-med' : 'b-low');
|
||||
$barColor = $p['label'] === 'High' ? '#ef4444' : ($p['label'] === 'Medium' ? '#f59e0b' : '#3b82f6');
|
||||
?>
|
||||
<tr>
|
||||
<td>
|
||||
<div class="mad-score" style="color:<?php echo $barColor; ?>;"><?php echo (int)$p['score']; ?></div>
|
||||
<span class="badge <?php echo $cls; ?>"><?php echo e($p['label']); ?></span>
|
||||
<div class="mad-bar"><i style="width:<?php echo (int)$p['score']; ?>%;background:<?php echo $barColor; ?>;"></i></div>
|
||||
</td>
|
||||
<td class="mad-acc">
|
||||
<a href="admin.php?p=player&uid=<?php echo (int)$p['uid_a']; ?>"><?php echo e($p['name_a']); ?></a>
|
||||
<span class="uid">#<?php echo (int)$p['uid_a']; ?></span>
|
||||
<span class="sub">
|
||||
<a href="admin.php?p=userlogin&uid=<?php echo (int)$p['uid_a']; ?>">login log</a>
|
||||
<a href="admin.php?p=multiacc&focus=<?php echo (int)$p['uid_a']; ?>">focus</a>
|
||||
</span>
|
||||
</td>
|
||||
<td class="mad-acc">
|
||||
<a href="admin.php?p=player&uid=<?php echo (int)$p['uid_b']; ?>"><?php echo e($p['name_b']); ?></a>
|
||||
<span class="uid">#<?php echo (int)$p['uid_b']; ?></span>
|
||||
<span class="sub">
|
||||
<a href="admin.php?p=userlogin&uid=<?php echo (int)$p['uid_b']; ?>">login log</a>
|
||||
<a href="admin.php?p=multiacc&focus=<?php echo (int)$p['uid_b']; ?>">focus</a>
|
||||
</span>
|
||||
</td>
|
||||
<td class="chips">
|
||||
<?php foreach ($p['reasons'] as $r): ?>
|
||||
<span><?php echo e($r); ?></span>
|
||||
<?php endforeach; ?>
|
||||
<?php if (!empty($p['shared_ip_list'])): ?>
|
||||
<div style="margin-top:5px;color:#475569;font-size:10px;">
|
||||
IPs: <?php echo e(implode(', ', $p['shared_ip_list'])); ?>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
<?php if ($p['trade_gross'] > 0): ?>
|
||||
<div style="margin-top:3px;color:#475569;font-size:10px;">
|
||||
transfer volume: <?php echo number_format((int)$p['trade_gross']); ?> res
|
||||
<?php echo $p['trade_dir'] ? '(one-directional)' : '(two-way)'; ?>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
</td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
@@ -0,0 +1,170 @@
|
||||
<?php
|
||||
#################################################################################
|
||||
## Filename : Admin/Templates/pushprot.tpl ##
|
||||
## Type : Admin page - Push Protection dashboard ##
|
||||
## Project : TravianZ ##
|
||||
## License : TravianZ Project ##
|
||||
#################################################################################
|
||||
|
||||
if (!isset($_SESSION['access']) || $_SESSION['access'] < MULTIHUNTER) {
|
||||
echo '<p style="color:#f87171;padding:16px;">Access denied.</p>';
|
||||
return;
|
||||
}
|
||||
|
||||
$days = isset($_GET['days']) && ctype_digit((string)$_GET['days']) ? max(1, min(30, (int)$_GET['days'])) : PushProtection::WINDOW_DAYS;
|
||||
$hours = isset($_GET['hours']) && ctype_digit((string)$_GET['hours']) ? max(1, min(168, (int)$_GET['hours'])): PushProtection::HOURS_ALLOWED;
|
||||
$only = isset($_GET['only']) && in_array($_GET['only'], ['all','over','flagged'], true) ? $_GET['only'] : 'all';
|
||||
|
||||
$data = PushProtection::dashboard(['days' => $days, 'hours' => $hours, 'only' => $only]);
|
||||
$rows = $data['rows'];
|
||||
|
||||
function pp_num($n) { return number_format((int)$n); }
|
||||
?>
|
||||
<style>
|
||||
.pp-wrap{color:#e2e8f0;font-family:Verdana,Arial,sans-serif;font-size:12px;padding:6px 4px 26px;}
|
||||
.pp-wrap h2{font-size:18px;margin:0 0 4px;color:#fff;}
|
||||
.pp-wrap h2 span{color:#f59e0b;}
|
||||
.pp-intro{color:#94a3b8;font-size:11px;margin:0 0 14px;max-width:860px;line-height:1.5;}
|
||||
.pp-filter{background:#111827;border:1px solid #1f2937;border-radius:8px;padding:12px 14px;display:flex;flex-wrap:wrap;gap:14px;align-items:flex-end;margin-bottom:14px;}
|
||||
.pp-filter label{display:block;font-size:10px;text-transform:uppercase;letter-spacing:.6px;color:#94a3b8;margin-bottom:4px;}
|
||||
.pp-filter input,.pp-filter select{background:#0b1220;border:1px solid #334155;border-radius:6px;color:#e2e8f0;padding:6px 8px;}
|
||||
.pp-filter input{width:90px;}
|
||||
.pp-filter button{background:#f59e0b;color:#111827;font-weight:bold;border:0;border-radius:6px;padding:8px 16px;cursor:pointer;}
|
||||
.pp-meta{color:#64748b;font-size:11px;margin-bottom:10px;}
|
||||
.pp-table{width:100%;border-collapse:collapse;background:#0b1220;border:1px solid #1f2937;border-radius:8px;overflow:hidden;}
|
||||
.pp-table th{background:#111827;text-align:left;padding:9px 10px;font-size:10px;text-transform:uppercase;letter-spacing:.5px;color:#94a3b8;border-bottom:1px solid #1f2937;}
|
||||
.pp-table td{padding:9px 10px;border-bottom:1px solid #14203a;vertical-align:top;}
|
||||
.pp-table tr:hover td{background:#0f1a30;}
|
||||
.pp-acc a{color:#e2e8f0;text-decoration:none;font-weight:bold;}
|
||||
.pp-acc a:hover{color:#f59e0b;}
|
||||
.pp-acc .uid{color:#475569;font-weight:normal;font-size:10px;display:block;}
|
||||
.badge{display:inline-block;padding:2px 8px;border-radius:10px;font-size:10px;font-weight:bold;text-transform:uppercase;letter-spacing:.4px;}
|
||||
.s-over{background:#7f1d1d;color:#fecaca;}
|
||||
.s-near{background:#78350f;color:#fde68a;}
|
||||
.s-ok{background:#14532d;color:#bbf7d0;}
|
||||
.s-exempt{background:#1e3a5f;color:#bfdbfe;}
|
||||
.flag{display:inline-block;background:#334155;color:#e2e8f0;border-radius:4px;padding:1px 6px;font-size:9px;margin:2px 3px 0 0;text-transform:uppercase;letter-spacing:.4px;}
|
||||
.flag.ww{background:#5b21b6;color:#ede9fe;}
|
||||
.flag.arte{background:#0e7490;color:#cffafe;}
|
||||
.pp-bar{height:6px;background:#1e293b;border-radius:3px;margin-top:4px;overflow:hidden;width:120px;}
|
||||
.pp-bar i{display:block;height:100%;}
|
||||
.pp-ov{display:flex;gap:5px;align-items:center;flex-wrap:wrap;}
|
||||
.pp-ov select,.pp-ov input{background:#0b1220;border:1px solid #334155;border-radius:5px;color:#e2e8f0;padding:4px 6px;font-size:11px;}
|
||||
.pp-ov input.lim{width:90px;}
|
||||
.pp-ov input.note{width:120px;}
|
||||
.pp-ov button{background:#334155;color:#fff;border:0;border-radius:5px;padding:5px 10px;font-size:11px;cursor:pointer;}
|
||||
.pp-ov button:hover{background:#f59e0b;color:#111827;}
|
||||
.pp-cur{font-size:10px;color:#94a3b8;display:block;margin-bottom:3px;}
|
||||
.pp-empty{padding:26px;text-align:center;color:#64748b;}
|
||||
.num{font-variant-numeric:tabular-nums;}
|
||||
</style>
|
||||
|
||||
<div class="pp-wrap">
|
||||
<h2>Push Protection <span>Dashboard</span></h2>
|
||||
<p class="pp-intro">
|
||||
Per-player <b>7-day resource balance</b> (received from other players) versus an
|
||||
automatic limit derived from the player's hourly production
|
||||
(<span class="num"><?php echo (int)$data['hours_allowed']; ?>h</span> of production per window).
|
||||
WW villages and artefact villages are flagged as supply exceptions. Set a manual
|
||||
override to exempt a player (WW / trusted) or give a custom cap. Nothing is blocked
|
||||
automatically — this is visibility & control for a human moderator.
|
||||
</p>
|
||||
|
||||
<form method="get" action="admin.php" class="pp-filter">
|
||||
<input type="hidden" name="p" value="pushprot">
|
||||
<div>
|
||||
<label>Window (days)</label>
|
||||
<input type="number" name="days" min="1" max="30" value="<?php echo (int)$days; ?>">
|
||||
</div>
|
||||
<div>
|
||||
<label>Hours of prod. allowed</label>
|
||||
<input type="number" name="hours" min="1" max="168" value="<?php echo (int)$hours; ?>">
|
||||
</div>
|
||||
<div>
|
||||
<label>Show</label>
|
||||
<select name="only">
|
||||
<option value="all" <?php echo $only==='all'?'selected':''; ?>>All with activity</option>
|
||||
<option value="over" <?php echo $only==='over'?'selected':''; ?>>Over limit only</option>
|
||||
<option value="flagged" <?php echo $only==='flagged'?'selected':''; ?>>WW / artefact / overridden</option>
|
||||
</select>
|
||||
</div>
|
||||
<button type="submit">Refresh</button>
|
||||
</form>
|
||||
|
||||
<div class="pp-meta">
|
||||
Window: last <?php echo (int)$data['window_days']; ?> days |
|
||||
limit = <?php echo (int)$data['hours_allowed']; ?>h × hourly production |
|
||||
<?php echo count($rows); ?> player<?php echo count($rows)==1?'':'s'; ?> shown
|
||||
</div>
|
||||
|
||||
<?php if (empty($rows)): ?>
|
||||
<div class="pp-table"><div class="pp-empty">
|
||||
No inter-player transfers recorded in this window yet.<br>
|
||||
The 7-day balance fills in as merchant deliveries between different players
|
||||
are processed after this feature is deployed.
|
||||
</div></div>
|
||||
<?php else: ?>
|
||||
<table class="pp-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Player</th>
|
||||
<th>Villages / Pop</th>
|
||||
<th>Prod/h</th>
|
||||
<th>Received (7d)</th>
|
||||
<th>Limit</th>
|
||||
<th>Usage</th>
|
||||
<th style="width:26%;">Override</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<?php foreach ($rows as $r):
|
||||
$sCls = $r['status']==='Over' ? 's-over' : ($r['status']==='Near' ? 's-near' : ($r['status']==='Exempt' ? 's-exempt' : 's-ok'));
|
||||
$barCol = $r['status']==='Over' ? '#ef4444' : ($r['status']==='Near' ? '#f59e0b' : ($r['status']==='Exempt' ? '#3b82f6' : '#22c55e'));
|
||||
$barW = $r['eff_limit']==-1 ? 100 : min(100, (int)$r['pct']);
|
||||
?>
|
||||
<tr>
|
||||
<td class="pp-acc">
|
||||
<a href="admin.php?p=player&uid=<?php echo (int)$r['uid']; ?>"><?php echo e($r['name']); ?></a>
|
||||
<span class="uid">#<?php echo (int)$r['uid']; ?></span>
|
||||
<?php foreach ($r['flags'] as $fl): ?>
|
||||
<span class="flag <?php echo stripos($fl,'WW')!==false?'ww':'arte'; ?>"><?php echo e($fl); ?></span>
|
||||
<?php endforeach; ?>
|
||||
</td>
|
||||
<td class="num"><?php echo (int)$r['villages']; ?> / <?php echo pp_num($r['pop']); ?></td>
|
||||
<td class="num"><?php echo pp_num($r['prod_h']); ?></td>
|
||||
<td class="num">
|
||||
<?php echo pp_num($r['received']); ?>
|
||||
<span style="color:#64748b;font-size:10px;">from <?php echo (int)$r['senders']; ?></span>
|
||||
</td>
|
||||
<td class="num">
|
||||
<?php echo $r['eff_limit']==-1 ? '<span style="color:#93c5fd;">unlimited</span>' : pp_num($r['eff_limit']); ?>
|
||||
<?php if ($r['ov_label']): ?><span style="color:#64748b;font-size:10px;display:block;"><?php echo e($r['ov_label']); ?></span><?php endif; ?>
|
||||
</td>
|
||||
<td>
|
||||
<span class="badge <?php echo $sCls; ?>"><?php echo e($r['status']); ?></span>
|
||||
<?php if ($r['eff_limit']!=-1): ?>
|
||||
<div style="font-size:10px;color:#94a3b8;margin-top:3px;"><?php echo (int)$r['pct']; ?>%</div>
|
||||
<?php endif; ?>
|
||||
<div class="pp-bar"><i style="width:<?php echo (int)$barW; ?>%;background:<?php echo $barCol; ?>;"></i></div>
|
||||
</td>
|
||||
<td>
|
||||
<?php if ($r['ov_note']): ?><span class="pp-cur">note: <?php echo e($r['ov_note']); ?></span><?php endif; ?>
|
||||
<form method="post" action="../GameEngine/Admin/Mods/pushOverride.php" class="pp-ov">
|
||||
<?php echo csrf_field(); ?>
|
||||
<input type="hidden" name="uid" value="<?php echo (int)$r['uid']; ?>">
|
||||
<select name="mode">
|
||||
<option value="0" <?php echo $r['ov_mode']==0?'selected':''; ?>>Auto</option>
|
||||
<option value="1" <?php echo $r['ov_mode']==1?'selected':''; ?>>Exempt<?php echo $r['is_ww']?' (WW)':''; ?></option>
|
||||
<option value="3" <?php echo $r['ov_mode']==3?'selected':''; ?>>Custom cap</option>
|
||||
</select>
|
||||
<input class="lim num" type="number" min="0" name="custom_limit" placeholder="cap" value="<?php echo $r['ov_mode']==3 && $r['eff_limit']>0 ? (int)$r['eff_limit'] : ''; ?>">
|
||||
<input class="note" type="text" name="note" maxlength="255" placeholder="note" value="<?php echo e($r['ov_note']); ?>">
|
||||
<button type="submit">Save</button>
|
||||
</form>
|
||||
</td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
@@ -42,6 +42,8 @@ include_once("../GameEngine/Lang/" . LANG . ".php");
|
||||
include_once("../GameEngine/Admin/database.php");
|
||||
include_once("../GameEngine/Data/buidata.php");
|
||||
include_once("../GameEngine/Artifacts.php");
|
||||
include_once("../GameEngine/MultiAccount.php");
|
||||
include_once("../GameEngine/PushProtection.php");
|
||||
|
||||
// ─── SECURITY HELPERS ────────────────────────────────────────────────────────
|
||||
|
||||
@@ -95,6 +97,8 @@ function admin_validated_page(string $raw): string
|
||||
'village', 'editResources', 'addTroops', 'addABTroops', 'editVillage',
|
||||
'villagelog', 'techlog', 'msg',
|
||||
'alliance', 'editAli', 'delAli','editNewFunctions',
|
||||
'multiacc',
|
||||
'pushprot',
|
||||
];
|
||||
|
||||
return in_array($raw, $whitelist, true) ? $raw : '';
|
||||
@@ -169,6 +173,14 @@ if ($page !== '') {
|
||||
$subpage = 'Search IGMs/Reports';
|
||||
break;
|
||||
|
||||
case 'multiacc':
|
||||
$subpage = 'Multi-Account Detection';
|
||||
break;
|
||||
|
||||
case 'pushprot':
|
||||
$subpage = 'Push Protection';
|
||||
break;
|
||||
|
||||
case 'massmessage':
|
||||
$subpage = 'Mass Message';
|
||||
break;
|
||||
@@ -847,6 +859,8 @@ body.app #menu li.sub ul li a:hover{color:#d97706!important}
|
||||
<li class="sub"><a href="#">Admin</a>
|
||||
<ul>
|
||||
<li><a href="?p=admin_log"><font color="Red"><b>Admin Log</b></font></a></li>
|
||||
<li><a href="?p=multiacc"><font color="Red"><b>Multi-Account Detection</b></font></a></li>
|
||||
<li><a href="?p=pushprot"><font color="Red"><b>Push Protection</b></font></a></li>
|
||||
<li><a href="?p=debug_log">Debug Error Log</a></li>
|
||||
<li><a href="?p=config">Server Settings</a></li>
|
||||
<li><a href="?p=maintenance">Server Maintenance</a></li>
|
||||
@@ -866,6 +880,8 @@ body.app #menu li.sub ul li a:hover{color:#d97706!important}
|
||||
<li><a href="?p=search">Search</a></li>
|
||||
<li><a href="?p=message">Msg/Rep</a></li>
|
||||
<li><a href="?p=ban">Ban</a></li>
|
||||
<li><a href="?p=multiacc">Multi-Account Detection</a></li>
|
||||
<li><a href="?p=pushprot">Push Protection</a></li>
|
||||
<li><a href="?action=logout">Logout</a></li>
|
||||
</ul>
|
||||
|
||||
|
||||
@@ -73,12 +73,13 @@ $fh = fopen($myFile, 'w') or die("<br/><br/><br/>Can't open file: GameEngine\con
|
||||
$NEW_FUNCTIONS_MEDAL_10YEAR = (NEW_FUNCTIONS_MEDAL_10YEAR == false ? 'false' : 'true');
|
||||
$NEW_FUNCTIONS_SPECIAL_MEDALS_SYSTEM = (NEW_FUNCTIONS_SPECIAL_MEDALS_SYSTEM == false ? 'false' : 'true');
|
||||
$NEW_FUNCTIONS_MILESTONES = (NEW_FUNCTIONS_MILESTONES == false ? 'false' : 'true');
|
||||
$NEW_FUNCTIONS_HERO_T4 = (defined('NEW_FUNCTIONS_HERO_T4') && NEW_FUNCTIONS_HERO_T4 ? 'true' : 'false'); // fix: lipsea (bug preexistent)
|
||||
$NEW_FUNCTIONS_HERO_T4 = (defined('NEW_FUNCTIONS_HERO_T4') && NEW_FUNCTIONS_HERO_T4 ? 'true' : 'false');
|
||||
$NEW_FUNCTION_TRIBE_HUNS = (defined('NEW_FUNCTION_TRIBE_HUNS') && NEW_FUNCTION_TRIBE_HUNS ? 'true' : 'false');
|
||||
$NEW_FUNCTION_TRIBE_EGIPTEANS = (defined('NEW_FUNCTION_TRIBE_EGIPTEANS') && NEW_FUNCTION_TRIBE_EGIPTEANS ? 'true' : 'false');
|
||||
$NEW_FUNCTION_TRIBE_SPARTANS = (defined('NEW_FUNCTION_TRIBE_SPARTANS') && NEW_FUNCTION_TRIBE_SPARTANS ? 'true' : 'false');
|
||||
$NEW_FUNCTION_TRIBE_VIKINGS = (defined('NEW_FUNCTION_TRIBE_VIKINGS') && NEW_FUNCTION_TRIBE_VIKINGS ? 'true' : 'false');
|
||||
$NEW_FUNCTIONS_MEDAL_RESET = (NEW_FUNCTIONS_MEDAL_RESET == false ? 'false' : 'true');
|
||||
|
||||
|
||||
$text = admin_config_template_contents();
|
||||
$text = preg_replace("'%ERRORREPORT%'", $ERRORREPORT, $text);
|
||||
@@ -171,12 +172,15 @@ $fh = fopen($myFile, 'w') or die("<br/><br/><br/>Can't open file: GameEngine\con
|
||||
$text = preg_replace("'%NEW_FUNCTIONS_MEDAL_10YEAR%'", $NEW_FUNCTIONS_MEDAL_10YEAR, $text);
|
||||
$text = preg_replace("'%NEW_FUNCTIONS_SPECIAL_MEDALS_SYSTEM%'", $NEW_FUNCTIONS_SPECIAL_MEDALS_SYSTEM, $text);
|
||||
$text = preg_replace("'%NEW_FUNCTIONS_MILESTONES%'", $NEW_FUNCTIONS_MILESTONES, $text);
|
||||
$text = preg_replace("'%NEW_FUNCTIONS_HERO_T4%'", $NEW_FUNCTIONS_HERO_T4, $text); // fix: lipsea
|
||||
$text = preg_replace("'%NEW_FUNCTIONS_HERO_T4%'", $NEW_FUNCTIONS_HERO_T4, $text);
|
||||
$text = preg_replace("'%NEW_FUNCTION_TRIBE_HUNS%'", $NEW_FUNCTION_TRIBE_HUNS, $text);
|
||||
$text = preg_replace("'%NEW_FUNCTION_TRIBE_EGIPTEANS%'", $NEW_FUNCTION_TRIBE_EGIPTEANS, $text);
|
||||
$text = preg_replace("'%NEW_FUNCTION_TRIBE_SPARTANS%'", $NEW_FUNCTION_TRIBE_SPARTANS, $text);
|
||||
$text = preg_replace("'%NEW_FUNCTION_TRIBE_VIKINGS%'", $NEW_FUNCTION_TRIBE_VIKINGS, $text);
|
||||
$text = preg_replace("'%NEW_FUNCTIONS_MEDAL_RESET%'", $NEW_FUNCTIONS_MEDAL_RESET, $text);
|
||||
// Preserve registration-bonus-gold settings (owned by editNewFunctions.php).
|
||||
$text = preg_replace("'%NEW_FUNCTION_REGISTRATION_GOLD%'", (defined('NEW_FUNCTION_REGISTRATION_GOLD') && NEW_FUNCTION_REGISTRATION_GOLD ? 'true' : 'false'), $text);
|
||||
$text = preg_replace("'%NEW_FUNCTION_REGISTRATION_GOLD_VALUE%'", (string) (defined('NEW_FUNCTION_REGISTRATION_GOLD_VALUE') ? (int) NEW_FUNCTION_REGISTRATION_GOLD_VALUE : 200), $text);
|
||||
|
||||
// PLUS settings need to be kept intact
|
||||
$text = preg_replace("'%PLUS_TIME%'", PLUS_TIME, $text);
|
||||
|
||||
@@ -174,6 +174,9 @@ $fh = fopen($myFile, 'w') or die("<br/><br/><br/>Can't open file: GameEngine\con
|
||||
$text = preg_replace("'%NEW_FUNCTION_TRIBE_EGIPTEANS%'", $NEW_FUNCTION_TRIBE_EGIPTEANS, $text);
|
||||
$text = preg_replace("'%NEW_FUNCTION_TRIBE_SPARTANS%'", $NEW_FUNCTION_TRIBE_SPARTANS, $text);
|
||||
$text = preg_replace("'%NEW_FUNCTION_TRIBE_VIKINGS%'", $NEW_FUNCTION_TRIBE_VIKINGS, $text);
|
||||
// Preserve registration-bonus-gold settings (owned by editNewFunctions.php).
|
||||
$text = preg_replace("'%NEW_FUNCTION_REGISTRATION_GOLD%'", (defined('NEW_FUNCTION_REGISTRATION_GOLD') && NEW_FUNCTION_REGISTRATION_GOLD ? 'true' : 'false'), $text);
|
||||
$text = preg_replace("'%NEW_FUNCTION_REGISTRATION_GOLD_VALUE%'", (string) (defined('NEW_FUNCTION_REGISTRATION_GOLD_VALUE') ? (int) NEW_FUNCTION_REGISTRATION_GOLD_VALUE : 200), $text);
|
||||
|
||||
// PLUS settings need to be kept intact
|
||||
$text = preg_replace("'%PLUS_TIME%'", PLUS_TIME, $text);
|
||||
|
||||
@@ -174,6 +174,9 @@ $fh = fopen($myFile, 'w') or die("<br/><br/><br/>Can't open file: GameEngine\con
|
||||
$text = preg_replace("'%NEW_FUNCTION_TRIBE_EGIPTEANS%'", $NEW_FUNCTION_TRIBE_EGIPTEANS, $text);
|
||||
$text = preg_replace("'%NEW_FUNCTION_TRIBE_SPARTANS%'", $NEW_FUNCTION_TRIBE_SPARTANS, $text);
|
||||
$text = preg_replace("'%NEW_FUNCTION_TRIBE_VIKINGS%'", $NEW_FUNCTION_TRIBE_VIKINGS, $text);
|
||||
// Preserve registration-bonus-gold settings (owned by editNewFunctions.php).
|
||||
$text = preg_replace("'%NEW_FUNCTION_REGISTRATION_GOLD%'", (defined('NEW_FUNCTION_REGISTRATION_GOLD') && NEW_FUNCTION_REGISTRATION_GOLD ? 'true' : 'false'), $text);
|
||||
$text = preg_replace("'%NEW_FUNCTION_REGISTRATION_GOLD_VALUE%'", (string) (defined('NEW_FUNCTION_REGISTRATION_GOLD_VALUE') ? (int) NEW_FUNCTION_REGISTRATION_GOLD_VALUE : 200), $text);
|
||||
|
||||
// PLUS settings need to be kept intact
|
||||
$text = preg_replace("'%PLUS_TIME%'", PLUS_TIME, $text);
|
||||
|
||||
@@ -136,6 +136,12 @@ $fh = fopen($myFile, 'w') or die("<br/><br/><br/>Can't open file: GameEngine\con
|
||||
$text = preg_replace("'%NEW_FUNCTION_TRIBE_EGIPTEANS%'", $_POST['new_function_tribe_egipteans'], $text);
|
||||
$text = preg_replace("'%NEW_FUNCTION_TRIBE_SPARTANS%'", $_POST['new_function_tribe_spartans'], $text);
|
||||
$text = preg_replace("'%NEW_FUNCTION_TRIBE_VIKINGS%'", $_POST['new_function_tribe_vikings'], $text);
|
||||
// Registration bonus gold: owned by THIS Mod (edited from the New Functions form).
|
||||
$__reg_gold_on = (isset($_POST['new_function_registration_gold']) && strtolower((string)$_POST['new_function_registration_gold']) === 'true') ? 'true' : 'false';
|
||||
$__reg_gold_val = (int) ($_POST['new_function_registration_gold_value'] ?? 200);
|
||||
if ($__reg_gold_val < 0) { $__reg_gold_val = 0; }
|
||||
$text = preg_replace("'%NEW_FUNCTION_REGISTRATION_GOLD%'", $__reg_gold_on, $text);
|
||||
$text = preg_replace("'%NEW_FUNCTION_REGISTRATION_GOLD_VALUE%'", (string) $__reg_gold_val, $text);
|
||||
|
||||
// PLUS settings need to be kept intact
|
||||
$text = preg_replace("'%PLUS_TIME%'", PLUS_TIME, $text);
|
||||
|
||||
@@ -180,6 +180,9 @@ $fh = fopen($myFile, 'w') or die("<br/><br/><br/>Can't open file: GameEngine\con
|
||||
$text = preg_replace("'%NEW_FUNCTION_TRIBE_EGIPTEANS%'", $NEW_FUNCTION_TRIBE_EGIPTEANS, $text);
|
||||
$text = preg_replace("'%NEW_FUNCTION_TRIBE_SPARTANS%'", $NEW_FUNCTION_TRIBE_SPARTANS, $text);
|
||||
$text = preg_replace("'%NEW_FUNCTION_TRIBE_VIKINGS%'", $NEW_FUNCTION_TRIBE_VIKINGS, $text);
|
||||
// Preserve registration-bonus-gold settings (owned by editNewFunctions.php).
|
||||
$text = preg_replace("'%NEW_FUNCTION_REGISTRATION_GOLD%'", (defined('NEW_FUNCTION_REGISTRATION_GOLD') && NEW_FUNCTION_REGISTRATION_GOLD ? 'true' : 'false'), $text);
|
||||
$text = preg_replace("'%NEW_FUNCTION_REGISTRATION_GOLD_VALUE%'", (string) (defined('NEW_FUNCTION_REGISTRATION_GOLD_VALUE') ? (int) NEW_FUNCTION_REGISTRATION_GOLD_VALUE : 200), $text);
|
||||
|
||||
// PLUS settings need to be kept intact
|
||||
$text = preg_replace("'%PLUS_TIME%'", PLUS_TIME, $text);
|
||||
|
||||
@@ -158,6 +158,9 @@ $fh = fopen($myFile, 'w') or die("<br/><br/><br/>Can't open file: GameEngine\con
|
||||
$text = preg_replace("'%NEW_FUNCTION_TRIBE_EGIPTEANS%'", $NEW_FUNCTION_TRIBE_EGIPTEANS, $text);
|
||||
$text = preg_replace("'%NEW_FUNCTION_TRIBE_SPARTANS%'", $NEW_FUNCTION_TRIBE_SPARTANS, $text);
|
||||
$text = preg_replace("'%NEW_FUNCTION_TRIBE_VIKINGS%'", $NEW_FUNCTION_TRIBE_VIKINGS, $text);
|
||||
// Preserve registration-bonus-gold settings (owned by editNewFunctions.php).
|
||||
$text = preg_replace("'%NEW_FUNCTION_REGISTRATION_GOLD%'", (defined('NEW_FUNCTION_REGISTRATION_GOLD') && NEW_FUNCTION_REGISTRATION_GOLD ? 'true' : 'false'), $text);
|
||||
$text = preg_replace("'%NEW_FUNCTION_REGISTRATION_GOLD_VALUE%'", (string) (defined('NEW_FUNCTION_REGISTRATION_GOLD_VALUE') ? (int) NEW_FUNCTION_REGISTRATION_GOLD_VALUE : 200), $text);
|
||||
|
||||
// PLUS SETTINGS
|
||||
$text = preg_replace("'%PLUS_TIME%'", $_POST['plus_time'], $text);
|
||||
|
||||
@@ -169,6 +169,9 @@ $fh = fopen($myFile, 'w') or die("<br/><br/><br/>Can't open file: GameEngine\con
|
||||
$text = preg_replace("'%NEW_FUNCTION_TRIBE_EGIPTEANS%'", $NEW_FUNCTION_TRIBE_EGIPTEANS, $text);
|
||||
$text = preg_replace("'%NEW_FUNCTION_TRIBE_SPARTANS%'", $NEW_FUNCTION_TRIBE_SPARTANS, $text);
|
||||
$text = preg_replace("'%NEW_FUNCTION_TRIBE_VIKINGS%'", $NEW_FUNCTION_TRIBE_VIKINGS, $text);
|
||||
// Preserve registration-bonus-gold settings (owned by editNewFunctions.php).
|
||||
$text = preg_replace("'%NEW_FUNCTION_REGISTRATION_GOLD%'", (defined('NEW_FUNCTION_REGISTRATION_GOLD') && NEW_FUNCTION_REGISTRATION_GOLD ? 'true' : 'false'), $text);
|
||||
$text = preg_replace("'%NEW_FUNCTION_REGISTRATION_GOLD_VALUE%'", (string) (defined('NEW_FUNCTION_REGISTRATION_GOLD_VALUE') ? (int) NEW_FUNCTION_REGISTRATION_GOLD_VALUE : 200), $text);
|
||||
|
||||
// PLUS settings need to be kept intact
|
||||
$text = preg_replace("'%PLUS_TIME%'", PLUS_TIME, $text);
|
||||
|
||||
@@ -0,0 +1,73 @@
|
||||
<?php
|
||||
#################################################################################
|
||||
## -= YOU MAY NOT REMOVE OR CHANGE THIS NOTICE =- ##
|
||||
## --------------------------------------------------------------------------- ##
|
||||
## Filename pushOverride.php ##
|
||||
## Type BACKEND (Push Protection override) ##
|
||||
## Developed by: Shadow ##
|
||||
## License: TravianZ Project ##
|
||||
## Copyright: TravianZ (c) 2010-2026. All rights reserved. ##
|
||||
#################################################################################
|
||||
|
||||
// #299 style: load CSRF helpers + admin_deny() before the access check.
|
||||
require_once(__DIR__ . '/../csrf.php');
|
||||
if (!isset($_SESSION)) session_start();
|
||||
if ($_SESSION['access'] < MULTIHUNTER) {
|
||||
admin_deny('You must be signed in as an administrator or multihunter to do this. '
|
||||
. 'Your session may have expired — please return to the admin panel and sign in again.');
|
||||
}
|
||||
|
||||
// This Mod is POSTed to directly, so verify the CSRF token itself.
|
||||
csrf_verify();
|
||||
|
||||
include_once("../../config.php");
|
||||
|
||||
// autoloader (find it walking up, like the other Mods)
|
||||
$autoprefix = '';
|
||||
for ($i = 0; $i < 5; $i++) {
|
||||
$autoprefix = str_repeat('../', $i);
|
||||
if (file_exists($autoprefix . 'autoloader.php')) break;
|
||||
}
|
||||
include_once($autoprefix . "GameEngine/Database.php");
|
||||
include_once($autoprefix . "GameEngine/PushProtection.php");
|
||||
|
||||
$admid = (int)($_SESSION['id'] ?? 0);
|
||||
$uid = (int)($_POST['uid'] ?? 0);
|
||||
$mode = (int)($_POST['mode'] ?? 0);
|
||||
$limit = (int)($_POST['custom_limit'] ?? 0);
|
||||
$note = trim((string)($_POST['note'] ?? ''));
|
||||
|
||||
// Verify the acting user really is admin/MH in the DB (defence in depth).
|
||||
$check = mysqli_query($GLOBALS['link'],
|
||||
"SELECT access, username FROM " . TB_PREFIX . "users WHERE id = " . $admid);
|
||||
$acc = $check ? mysqli_fetch_assoc($check) : null;
|
||||
if (!$acc || (int)$acc['access'] < MULTIHUNTER) {
|
||||
admin_deny('Your session may have expired — please sign in again.');
|
||||
}
|
||||
|
||||
if ($uid > 3) {
|
||||
// Normalise mode to the allowed set.
|
||||
if (!in_array($mode, [
|
||||
PushProtection::OV_NONE,
|
||||
PushProtection::OV_UNLIMITED,
|
||||
PushProtection::OV_CUSTOM,
|
||||
], true)) {
|
||||
$mode = PushProtection::OV_NONE;
|
||||
}
|
||||
if ($limit < 0) {
|
||||
$limit = 0;
|
||||
}
|
||||
|
||||
PushProtection::setOverride($uid, $mode, $limit, $note, $admid);
|
||||
|
||||
$modeLabel = $mode === PushProtection::OV_UNLIMITED ? 'exempt'
|
||||
: ($mode === PushProtection::OV_CUSTOM ? ('custom cap ' . $limit) : 'cleared');
|
||||
$logMsg = 'Push protection override for uid ' . $uid . ' set to <b>' . $modeLabel . '</b>';
|
||||
$logMsg = mysqli_real_escape_string($GLOBALS['link'], $logMsg);
|
||||
mysqli_query($GLOBALS['link'],
|
||||
"INSERT INTO " . TB_PREFIX . "admin_log VALUES (0, " . $admid . ", '" . $logMsg . "', " . time() . ")");
|
||||
}
|
||||
|
||||
header("Location: ../../../Admin/admin.php?p=pushprot");
|
||||
exit;
|
||||
?>
|
||||
@@ -709,6 +709,18 @@ class Automation {
|
||||
$database->addNotice($from['owner'],$to['wref'],$ownally,$sort_type,''.addslashes($from['name']).' send resources to '.addslashes($to['name']).'',''.$from['owner'].','.$from['wref'].','.$data['wood'].','.$data['clay'].','.$data['iron'].','.$data['crop'].'',$data['endtime']);
|
||||
}
|
||||
$database->modifyResource($data['to'],$data['wood'],$data['clay'],$data['iron'],$data['crop'],1);
|
||||
|
||||
// Push protection: record completed cross-player deliveries so the
|
||||
// admin dashboard can compute 7-day resource balance. Best-effort;
|
||||
// skips own-village transfers internally.
|
||||
if (!$ownTransfer) {
|
||||
PushProtection::logTransfer(
|
||||
(int)$from['wref'], (int)$to['wref'],
|
||||
(int)$from['owner'], (int)$to['owner'],
|
||||
(int)$data['wood'], (int)$data['clay'], (int)$data['iron'], (int)$data['crop'],
|
||||
(int)$data['endtime']
|
||||
);
|
||||
}
|
||||
$targettribe = $userData_to["tribe"];
|
||||
$endtime = $units->getWalkingTroopsTime($data['from'], $data['to'], 0, 0, [$targettribe], 0) + $data['endtime'];
|
||||
$database->addMovement(2, $data['to'], $data['from'], $data['merchant'], time(), $endtime, $data['send'], $data['wood'], $data['clay'], $data['iron'], $data['crop']);
|
||||
|
||||
@@ -834,6 +834,7 @@ class MYSQLi_DB implements IDbConnection {
|
||||
if($stmt->execute()){
|
||||
$id = $stmt->insert_id ?: $uid;
|
||||
$stmt->close();
|
||||
$this->grantRegistrationGold($id);
|
||||
return $id;
|
||||
}
|
||||
$stmt->close();
|
||||
@@ -849,11 +850,52 @@ class MYSQLi_DB implements IDbConnection {
|
||||
if($stmt2->execute()){
|
||||
$id = $stmt2->insert_id ?: $uid;
|
||||
$stmt2->close();
|
||||
$this->grantRegistrationGold($id);
|
||||
return $id;
|
||||
}
|
||||
$stmt2->close();
|
||||
return false;
|
||||
}
|
||||
/**
|
||||
* Registration bonus gold (NEW_FUNCTION_REGISTRATION_GOLD).
|
||||
* Grants a one-time gold bonus to a freshly created player account. Called
|
||||
* from register() right after the users row is inserted, so it covers every
|
||||
* registration path (email activation, instant registration, admin-created
|
||||
* users). No-ops when disabled, amount <= 0, or system account (id <= 3).
|
||||
*/
|
||||
private function grantRegistrationGold($id) {
|
||||
$id = (int) $id;
|
||||
if ($id <= 3) {
|
||||
return; // system accounts: admin / nature / natars
|
||||
}
|
||||
if (!defined('NEW_FUNCTION_REGISTRATION_GOLD') || !NEW_FUNCTION_REGISTRATION_GOLD) {
|
||||
return;
|
||||
}
|
||||
$amount = defined('NEW_FUNCTION_REGISTRATION_GOLD_VALUE') ? (int) NEW_FUNCTION_REGISTRATION_GOLD_VALUE : 0;
|
||||
if ($amount <= 0) {
|
||||
return;
|
||||
}
|
||||
$this->modifyGold($id, $amount, 1); // mode 1 = add
|
||||
|
||||
// Best-effort audit trail. The village does not exist yet, so wid = 0.
|
||||
if (defined('LOG_GOLD_FIN') && LOG_GOLD_FIN) {
|
||||
try {
|
||||
$now = time();
|
||||
$details = 'Registration bonus';
|
||||
$stmt = $this->dblink->prepare(
|
||||
"INSERT INTO `".TB_PREFIX."gold_fin_log` (wid, uid, action, gold, time, details)
|
||||
VALUES (0, ?, 'Registration bonus Gold', ?, ?, ?)"
|
||||
);
|
||||
if ($stmt) {
|
||||
$stmt->bind_param("iiis", $id, $amount, $now, $details);
|
||||
$stmt->execute();
|
||||
$stmt->close();
|
||||
}
|
||||
} catch (\Throwable $e) {
|
||||
// swallow: logging must never block account creation
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function activate($username, $password, $email, $tribe, $locate, $act, $act2) {
|
||||
$username = trim($username);
|
||||
|
||||
@@ -186,6 +186,45 @@ class Market
|
||||
/**
|
||||
* Send resources.
|
||||
*/
|
||||
/**
|
||||
* Push-protection enforcement (Travian-Legends style).
|
||||
* Returns true when this send must be BLOCKED because the recipient has
|
||||
* reached the amount of resources it may receive from other players in the
|
||||
* rolling window (see PushProtection::remainingAllowance).
|
||||
*
|
||||
* No-ops (returns false) for: own-village transfers, system accounts, when
|
||||
* the PushProtection engine is absent, or when hard enforcement is globally
|
||||
* disabled via define("PUSH_PROTECTION_ENFORCE", false); in config.php
|
||||
* (the dashboard and logging keep working either way — default is ON).
|
||||
*
|
||||
* WW villages and manual "Exempt" overrides return an unlimited allowance
|
||||
* inside remainingAllowance(), so they are never blocked here.
|
||||
*/
|
||||
private function exceedsPushLimit($database, $fromWid, $toWid, array $resource)
|
||||
{
|
||||
if (defined('PUSH_PROTECTION_ENFORCE') && !PUSH_PROTECTION_ENFORCE) {
|
||||
return false;
|
||||
}
|
||||
if (!class_exists('PushProtection')) {
|
||||
return false; // engine not deployed -> no enforcement
|
||||
}
|
||||
|
||||
$fromOwner = (int) $database->getVillageField($fromWid, 'owner');
|
||||
$toOwner = (int) $database->getVillageField($toWid, 'owner');
|
||||
|
||||
// Own transfer or system account -> never limited.
|
||||
if ($fromOwner === $toOwner || $toOwner <= 3) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$sendTotal = (int) $resource[0] + (int) $resource[1] + (int) $resource[2] + (int) $resource[3];
|
||||
if ($sendTotal <= 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return $sendTotal > PushProtection::remainingAllowance($toOwner);
|
||||
}
|
||||
|
||||
private function sendResource($post)
|
||||
{
|
||||
global $database, $village, $session, $generator, $logging, $form;
|
||||
@@ -249,6 +288,16 @@ class Market
|
||||
($post['send3'] > 1 && !$session->goldclub)
|
||||
) {
|
||||
$form->addError('error', INVALID_MERCHANTS_REPETITION);
|
||||
} elseif (
|
||||
$this->exceedsPushLimit($database, $village->wid, $id, $resource)
|
||||
) {
|
||||
// Push protection: recipient has reached its transfer limit for the
|
||||
// current window. Uses a language constant when available, else a
|
||||
// clear English fallback (add PUSH_PROTECTION_LIMIT to your Lang
|
||||
// files to localise).
|
||||
$form->addError('error', defined('PUSH_PROTECTION_LIMIT')
|
||||
? PUSH_PROTECTION_LIMIT
|
||||
: 'This account has reached its resource-transfer limit for now and cannot receive that many resources. Send less, or wait for the limit to recover.');
|
||||
} elseif (
|
||||
$availableWood >= $post['r1'] &&
|
||||
$availableClay >= $post['r2'] &&
|
||||
|
||||
@@ -0,0 +1,609 @@
|
||||
<?php
|
||||
#################################################################################
|
||||
## -= YOU MAY NOT REMOVE OR CHANGE THIS NOTICE =- ##
|
||||
## --------------------------------------------------------------------------- ##
|
||||
## Filename : MultiAccount.php ##
|
||||
## Type : Multi-Account Detection engine (Admin/Multihunter tool) ##
|
||||
## --------------------------------------------------------------------------- ##
|
||||
## Developed by : Shadow ##
|
||||
## Project : TravianZ ##
|
||||
## URLs: : https://travianz.org ##
|
||||
## GitHub : https://github.com/Shadowss/TravianZ ##
|
||||
## --------------------------------------------------------------------------- ##
|
||||
## License : TravianZ Project ##
|
||||
## Copyright : TravianZ (c) 2010-2026. All rights reserved. ##
|
||||
## --------------------------------------------------------------------------- ##
|
||||
#################################################################################
|
||||
|
||||
/**
|
||||
* MultiAccount
|
||||
* -------------------------------------------------------------------------
|
||||
* Heuristic multi-account (bot / pushing account) detection. It does NOT ban
|
||||
* anyone; it produces a ranked list of suspicious account PAIRS with a risk
|
||||
* score (0-100) and a breakdown of WHY, so a human (admin / multihunter) can
|
||||
* investigate.
|
||||
*
|
||||
* Data sources (all optional — the engine degrades gracefully):
|
||||
* - login_log : existing table (uid, ip, date). Gives shared-IP and
|
||||
* login-timing signals for history that predates this
|
||||
* feature.
|
||||
* - mad_session : NEW table written by recordSession() at every login.
|
||||
* Adds the User-Agent signal (login_log has no UA).
|
||||
* - movement / send : in-flight merchant transfers -> "currently pushing"
|
||||
* trade signal.
|
||||
* - resource_transfer_log : if present (created by the Push-Protection phase),
|
||||
* gives a richer historical trade-flow signal. Used
|
||||
* automatically when the table exists.
|
||||
*
|
||||
* The engine is intentionally self-contained (static methods, resolves the DB
|
||||
* link from globals) so it can be called from both the in-game login flow and
|
||||
* the admin panel without wiring.
|
||||
*/
|
||||
class MultiAccount
|
||||
{
|
||||
/* ---- Tunables (safe to adjust) ------------------------------------- */
|
||||
|
||||
/** How far back to look, in days. */
|
||||
const WINDOW_DAYS = 30;
|
||||
|
||||
/** Hard cap on login rows scanned per source (memory / runtime guard). */
|
||||
const MAX_ROWS = 60000;
|
||||
|
||||
/** A shared key (IP / subnet / UA) used by MORE than this many accounts is
|
||||
* treated as a public/NAT/proxy artefact: it still counts, but with a
|
||||
* reduced weight and it never explodes pair generation. */
|
||||
const POPULAR_KEY_CAP = 12;
|
||||
|
||||
/** Two logins closer than this (seconds) from the SAME IP look like one
|
||||
* person switching accounts. */
|
||||
const SWITCH_WINDOW = 900; // 15 minutes
|
||||
|
||||
/** Only pairs at or above this score are returned. */
|
||||
const MIN_REPORT_SCORE = 20;
|
||||
|
||||
/** Max pairs returned to the UI. */
|
||||
const MAX_PAIRS = 150;
|
||||
|
||||
/* ---- DB plumbing --------------------------------------------------- */
|
||||
|
||||
/** Resolve the raw mysqli link from whatever context we run in. */
|
||||
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 the mad_session table if it does not exist. Called lazily so the
|
||||
* feature works even on servers that never re-ran the installer.
|
||||
*/
|
||||
public static function ensureSchema()
|
||||
{
|
||||
$link = self::link();
|
||||
if (!$link) {
|
||||
return;
|
||||
}
|
||||
$sql = "CREATE TABLE IF NOT EXISTS `" . TB_PREFIX . "mad_session` (
|
||||
`id` int(11) NOT NULL AUTO_INCREMENT,
|
||||
`uid` int(11) NOT NULL,
|
||||
`ip` varbinary(16) DEFAULT NULL,
|
||||
`ip_text` varchar(45) NOT NULL DEFAULT '',
|
||||
`ua_hash` char(32) NOT NULL DEFAULT '',
|
||||
`ua_text` varchar(255) NOT NULL DEFAULT '',
|
||||
`login_time` int(11) NOT NULL DEFAULT 0,
|
||||
PRIMARY KEY (`id`),
|
||||
KEY `uid` (`uid`),
|
||||
KEY `ip` (`ip`),
|
||||
KEY `ua_hash` (`ua_hash`),
|
||||
KEY `login_time` (`login_time`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8";
|
||||
@mysqli_query($link, $sql);
|
||||
}
|
||||
|
||||
/* ---- Data collection (called at login) ----------------------------- */
|
||||
|
||||
/**
|
||||
* Record one login "session fingerprint". Best-effort: any failure is
|
||||
* swallowed so it can never block a login.
|
||||
*
|
||||
* @param int $uid User id.
|
||||
* @param string $ipText Client IP (already resolved by the caller).
|
||||
* @param string $userAgent Raw User-Agent header ($_SERVER['HTTP_USER_AGENT']).
|
||||
*/
|
||||
public static function recordSession($uid, $ipText, $userAgent)
|
||||
{
|
||||
try {
|
||||
$uid = (int) $uid;
|
||||
if ($uid <= 3) {
|
||||
return; // system accounts
|
||||
}
|
||||
|
||||
$link = self::link();
|
||||
if (!$link) {
|
||||
return;
|
||||
}
|
||||
|
||||
self::ensureSchema();
|
||||
|
||||
$ipText = (string) $ipText;
|
||||
$packed = @inet_pton($ipText);
|
||||
if ($packed === false) {
|
||||
$packed = null;
|
||||
}
|
||||
$uaText = substr((string) $userAgent, 0, 255);
|
||||
$uaHash = md5($uaText);
|
||||
$now = time();
|
||||
|
||||
$stmt = mysqli_prepare(
|
||||
$link,
|
||||
"INSERT INTO `" . TB_PREFIX . "mad_session`
|
||||
(uid, ip, ip_text, ua_hash, ua_text, login_time)
|
||||
VALUES (?,?,?,?,?,?)"
|
||||
);
|
||||
if (!$stmt) {
|
||||
return;
|
||||
}
|
||||
// ip is binary -> bind as blob ('b') via send_long_data-free path:
|
||||
// mysqli lets us bind a string to a varbinary column with type 's'.
|
||||
$ipBind = $packed === null ? '' : $packed;
|
||||
mysqli_stmt_bind_param($stmt, 'issssi', $uid, $ipBind, $ipText, $uaHash, $uaText, $now);
|
||||
mysqli_stmt_execute($stmt);
|
||||
mysqli_stmt_close($stmt);
|
||||
} catch (\Throwable $e) {
|
||||
// never break login
|
||||
}
|
||||
}
|
||||
|
||||
/* ---- Correlation / scoring ---------------------------------------- */
|
||||
|
||||
/**
|
||||
* Build the ranked list of suspicious account pairs.
|
||||
*
|
||||
* @param array $opts Optional overrides: 'days', 'min_score', 'focus_uid'.
|
||||
* @return array {
|
||||
* 'pairs' => list of pair rows (see below), sorted by score desc,
|
||||
* 'scanned' => ['login_log'=>int, 'mad_session'=>int],
|
||||
* 'window_days' => int,
|
||||
* 'truncated' => bool, // true if a source hit MAX_ROWS
|
||||
* }
|
||||
*
|
||||
* Each pair row:
|
||||
* uid_a, name_a, uid_b, name_b, score (0-100), label,
|
||||
* shared_ips, shared_subnets, shared_uas, switch_events,
|
||||
* trade_gross, trade_dir, reasons (string[])
|
||||
*/
|
||||
public static function riskPairs(array $opts = [])
|
||||
{
|
||||
$link = self::link();
|
||||
if (!$link) {
|
||||
return ['pairs' => [], 'scanned' => ['login_log' => 0, 'mad_session' => 0],
|
||||
'window_days' => self::WINDOW_DAYS, 'truncated' => false];
|
||||
}
|
||||
self::ensureSchema();
|
||||
|
||||
$days = isset($opts['days']) ? max(1, (int) $opts['days']) : self::WINDOW_DAYS;
|
||||
$minScore = isset($opts['min_score']) ? (int) $opts['min_score'] : self::MIN_REPORT_SCORE;
|
||||
$focusUid = isset($opts['focus_uid']) ? (int) $opts['focus_uid'] : 0;
|
||||
$since = time() - $days * 86400;
|
||||
|
||||
// Per-uid aggregates and inverted indices.
|
||||
$ips = []; // uid => [ip => true]
|
||||
$subnets = []; // uid => [subnet => true]
|
||||
$uas = []; // uid => [uaHash => true]
|
||||
$logins = []; // uid => [ [ts, ip], ... ]
|
||||
|
||||
$ipIndex = []; // ip => [uid => true]
|
||||
$subnetIndex = []; // subnet => [uid => true]
|
||||
$uaIndex = []; // uaHash => [uid => true]
|
||||
|
||||
$scannedLogin = 0;
|
||||
$scannedMad = 0;
|
||||
$truncated = false;
|
||||
|
||||
// ---- Source 1: login_log (uid, ip text, date) ----
|
||||
$cap = self::MAX_ROWS;
|
||||
$q = "SELECT uid, ip, UNIX_TIMESTAMP(`date`) AS ts
|
||||
FROM `" . TB_PREFIX . "login_log`
|
||||
WHERE uid > 3 AND UNIX_TIMESTAMP(`date`) >= " . (int) $since . "
|
||||
ORDER BY `date` DESC
|
||||
LIMIT " . (int) $cap;
|
||||
if ($res = @mysqli_query($link, $q)) {
|
||||
while ($row = mysqli_fetch_assoc($res)) {
|
||||
$scannedLogin++;
|
||||
self::ingest((int) $row['uid'], (string) $row['ip'], (int) $row['ts'], '',
|
||||
$ips, $subnets, $uas, $logins, $ipIndex, $subnetIndex, $uaIndex);
|
||||
}
|
||||
mysqli_free_result($res);
|
||||
if ($scannedLogin >= $cap) {
|
||||
$truncated = true;
|
||||
}
|
||||
}
|
||||
|
||||
// ---- Source 2: mad_session (adds UA) ----
|
||||
$q = "SELECT uid, ip_text, ua_hash, login_time
|
||||
FROM `" . TB_PREFIX . "mad_session`
|
||||
WHERE uid > 3 AND login_time >= " . (int) $since . "
|
||||
ORDER BY login_time DESC
|
||||
LIMIT " . (int) $cap;
|
||||
if ($res = @mysqli_query($link, $q)) {
|
||||
while ($row = mysqli_fetch_assoc($res)) {
|
||||
$scannedMad++;
|
||||
self::ingest((int) $row['uid'], (string) $row['ip_text'], (int) $row['login_time'],
|
||||
(string) $row['ua_hash'],
|
||||
$ips, $subnets, $uas, $logins, $ipIndex, $subnetIndex, $uaIndex);
|
||||
}
|
||||
mysqli_free_result($res);
|
||||
if ($scannedMad >= $cap) {
|
||||
$truncated = true;
|
||||
}
|
||||
}
|
||||
|
||||
// ---- Candidate pair generation from shared keys ----
|
||||
$candidates = []; // "a:b" => true
|
||||
self::pairsFromIndex($ipIndex, $candidates, $focusUid);
|
||||
self::pairsFromIndex($subnetIndex, $candidates, $focusUid);
|
||||
self::pairsFromIndex($uaIndex, $candidates, $focusUid);
|
||||
|
||||
// ---- Score each candidate pair ----
|
||||
$pairs = [];
|
||||
foreach ($candidates as $key => $_) {
|
||||
list($a, $b) = explode(':', $key);
|
||||
$a = (int) $a;
|
||||
$b = (int) $b;
|
||||
|
||||
$scored = self::scorePair($a, $b, $ips, $subnets, $uas, $logins, $link, $days);
|
||||
if ($scored['score'] >= $minScore) {
|
||||
$pairs[] = $scored;
|
||||
}
|
||||
}
|
||||
|
||||
// Resolve names for the pairs we keep (bounded set).
|
||||
self::attachNames($pairs, $link);
|
||||
|
||||
// Sort by score desc, then trade gross desc.
|
||||
usort($pairs, function ($x, $y) {
|
||||
if ($x['score'] === $y['score']) {
|
||||
return $y['trade_gross'] <=> $x['trade_gross'];
|
||||
}
|
||||
return $y['score'] <=> $x['score'];
|
||||
});
|
||||
|
||||
if (count($pairs) > self::MAX_PAIRS) {
|
||||
$pairs = array_slice($pairs, 0, self::MAX_PAIRS);
|
||||
}
|
||||
|
||||
return [
|
||||
'pairs' => $pairs,
|
||||
'scanned' => ['login_log' => $scannedLogin, 'mad_session' => $scannedMad],
|
||||
'window_days' => $days,
|
||||
'truncated' => $truncated,
|
||||
];
|
||||
}
|
||||
|
||||
/** Fold one login event into all aggregates + indices. */
|
||||
private static function ingest($uid, $ip, $ts, $uaHash,
|
||||
&$ips, &$subnets, &$uas, &$logins, &$ipIndex, &$subnetIndex, &$uaIndex)
|
||||
{
|
||||
if ($uid <= 3) {
|
||||
return;
|
||||
}
|
||||
$ip = trim($ip);
|
||||
if ($ip !== '' && $ip !== '0.0.0.0') {
|
||||
$ips[$uid][$ip] = true;
|
||||
$ipIndex[$ip][$uid] = true;
|
||||
$sub = self::subnet($ip);
|
||||
if ($sub !== '') {
|
||||
$subnets[$uid][$sub] = true;
|
||||
$subnetIndex[$sub][$uid] = true;
|
||||
}
|
||||
}
|
||||
if ($uaHash !== '' && $uaHash !== md5('')) {
|
||||
$uas[$uid][$uaHash] = true;
|
||||
$uaIndex[$uaHash][$uid] = true;
|
||||
}
|
||||
if ($ts > 0) {
|
||||
$logins[$uid][] = [$ts, $ip];
|
||||
}
|
||||
}
|
||||
|
||||
/** /24 for IPv4, /48-ish (first 3 hextets) for IPv6. */
|
||||
private static function subnet($ip)
|
||||
{
|
||||
if (strpos($ip, '.') !== false) {
|
||||
$p = explode('.', $ip);
|
||||
if (count($p) === 4) {
|
||||
return $p[0] . '.' . $p[1] . '.' . $p[2] . '.';
|
||||
}
|
||||
} elseif (strpos($ip, ':') !== false) {
|
||||
$p = explode(':', $ip);
|
||||
return $p[0] . ':' . ($p[1] ?? '') . ':' . ($p[2] ?? '') . '::';
|
||||
}
|
||||
return '';
|
||||
}
|
||||
|
||||
/**
|
||||
* Turn an inverted index (key => [uid => true]) into candidate pairs.
|
||||
* Skips popular keys (public NAT/proxy) to avoid noise and O(n^2) blowup.
|
||||
*/
|
||||
private static function pairsFromIndex(array $index, array &$candidates, $focusUid)
|
||||
{
|
||||
foreach ($index as $key => $uidSet) {
|
||||
$uids = array_keys($uidSet);
|
||||
$n = count($uids);
|
||||
if ($n < 2 || $n > self::POPULAR_KEY_CAP) {
|
||||
continue;
|
||||
}
|
||||
sort($uids);
|
||||
for ($i = 0; $i < $n; $i++) {
|
||||
for ($j = $i + 1; $j < $n; $j++) {
|
||||
$a = $uids[$i];
|
||||
$b = $uids[$j];
|
||||
if ($focusUid && $a !== $focusUid && $b !== $focusUid) {
|
||||
continue;
|
||||
}
|
||||
$candidates[$a . ':' . $b] = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** Score a single (a,b) pair and produce a reason breakdown. */
|
||||
private static function scorePair($a, $b, $ips, $subnets, $uas, $logins, $link, $days)
|
||||
{
|
||||
$sharedIps = array_keys(array_intersect_key(
|
||||
$ips[$a] ?? [], $ips[$b] ?? []
|
||||
));
|
||||
$sharedSubs = array_keys(array_intersect_key(
|
||||
$subnets[$a] ?? [], $subnets[$b] ?? []
|
||||
));
|
||||
$sharedUas = array_keys(array_intersect_key(
|
||||
$uas[$a] ?? [], $uas[$b] ?? []
|
||||
));
|
||||
|
||||
// Subnets that are shared but NOT already covered by a shared full IP.
|
||||
// Compare exact subnets (self::subnet) — a string-prefix test would treat
|
||||
// e.g. "85.204.1." as a prefix of IP "85.204.13.9" (false positive).
|
||||
$subOnly = [];
|
||||
foreach ($sharedSubs as $s) {
|
||||
$hasFull = false;
|
||||
foreach ($sharedIps as $ip) {
|
||||
if (self::subnet($ip) === $s) {
|
||||
$hasFull = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!$hasFull) {
|
||||
$subOnly[] = $s;
|
||||
}
|
||||
}
|
||||
|
||||
// Login "switch" events: logins of a and b from the same shared IP within
|
||||
// SWITCH_WINDOW seconds of each other.
|
||||
$switch = self::switchEvents($logins[$a] ?? [], $logins[$b] ?? [], $sharedIps);
|
||||
|
||||
// Trade flow between the pair (villages resolved inside).
|
||||
$trade = self::tradeFlow($a, $b, $link, $days);
|
||||
|
||||
// ---- Weighted score ----
|
||||
$score = 0;
|
||||
$reasons = [];
|
||||
|
||||
if (count($sharedIps) > 0) {
|
||||
$add = min(50, 35 + 5 * (count($sharedIps) - 1));
|
||||
$score += $add;
|
||||
$reasons[] = count($sharedIps) . ' shared IP' . (count($sharedIps) > 1 ? 's' : '');
|
||||
}
|
||||
if (count($sharedUas) > 0) {
|
||||
$add = min(30, 20 + 5 * (count($sharedUas) - 1));
|
||||
$score += $add;
|
||||
$reasons[] = count($sharedUas) . ' identical device/browser fingerprint'
|
||||
. (count($sharedUas) > 1 ? 's' : '');
|
||||
}
|
||||
if (count($subOnly) > 0) {
|
||||
$score += 10;
|
||||
$reasons[] = 'same /24 subnet';
|
||||
}
|
||||
if ($switch > 0) {
|
||||
$add = min(30, 15 + 5 * $switch);
|
||||
$score += $add;
|
||||
$reasons[] = $switch . ' rapid account switch' . ($switch > 1 ? 'es' : '')
|
||||
. ' from one IP';
|
||||
}
|
||||
if ($trade['gross'] > 0 && $trade['directional']) {
|
||||
$score += 20;
|
||||
$reasons[] = 'one-directional resource transfers';
|
||||
} elseif ($trade['gross'] > 0) {
|
||||
$score += 8;
|
||||
$reasons[] = 'resource transfers between them';
|
||||
}
|
||||
|
||||
$score = (int) min(100, $score);
|
||||
$label = $score >= 70 ? 'High' : ($score >= 40 ? 'Medium' : 'Low');
|
||||
|
||||
return [
|
||||
'uid_a' => $a,
|
||||
'uid_b' => $b,
|
||||
'name_a' => '',
|
||||
'name_b' => '',
|
||||
'score' => $score,
|
||||
'label' => $label,
|
||||
'shared_ips' => count($sharedIps),
|
||||
'shared_ip_list' => array_slice($sharedIps, 0, 6),
|
||||
'shared_subnets' => count($subOnly),
|
||||
'shared_uas' => count($sharedUas),
|
||||
'switch_events' => $switch,
|
||||
'trade_gross' => (int) $trade['gross'],
|
||||
'trade_dir' => $trade['directional'] ? 1 : 0,
|
||||
'reasons' => $reasons,
|
||||
];
|
||||
}
|
||||
|
||||
/** Count login "switches" from a shared IP within SWITCH_WINDOW seconds. */
|
||||
private static function switchEvents(array $la, array $lb, array $sharedIps)
|
||||
{
|
||||
if (empty($sharedIps) || empty($la) || empty($lb)) {
|
||||
return 0;
|
||||
}
|
||||
$sharedSet = array_flip($sharedIps);
|
||||
|
||||
// Keep only events from shared IPs.
|
||||
$ea = [];
|
||||
foreach ($la as $e) {
|
||||
if (isset($sharedSet[$e[1]])) {
|
||||
$ea[] = $e[0];
|
||||
}
|
||||
}
|
||||
$eb = [];
|
||||
foreach ($lb as $e) {
|
||||
if (isset($sharedSet[$e[1]])) {
|
||||
$eb[] = $e[0];
|
||||
}
|
||||
}
|
||||
if (empty($ea) || empty($eb)) {
|
||||
return 0;
|
||||
}
|
||||
sort($ea);
|
||||
sort($eb);
|
||||
|
||||
// Two-pointer count of a<->b logins within the window.
|
||||
$count = 0;
|
||||
$j = 0;
|
||||
$m = count($eb);
|
||||
foreach ($ea as $ta) {
|
||||
while ($j < $m && $eb[$j] < $ta - self::SWITCH_WINDOW) {
|
||||
$j++;
|
||||
}
|
||||
$k = $j;
|
||||
while ($k < $m && $eb[$k] <= $ta + self::SWITCH_WINDOW) {
|
||||
$count++;
|
||||
$k++;
|
||||
if ($count >= 20) {
|
||||
return 20; // cap
|
||||
}
|
||||
}
|
||||
}
|
||||
return $count;
|
||||
}
|
||||
|
||||
/**
|
||||
* Resource-transfer signal between two players.
|
||||
* Prefers resource_transfer_log (Push-Protection phase) when present, else
|
||||
* falls back to in-flight merchant movements. Returns gross total moved and
|
||||
* whether flow is strongly one-directional.
|
||||
*/
|
||||
private static function tradeFlow($a, $b, $link, $days)
|
||||
{
|
||||
$out = ['gross' => 0, 'directional' => false];
|
||||
|
||||
// Village ids owned by each player.
|
||||
$va = self::villagesOf($a, $link);
|
||||
$vb = self::villagesOf($b, $link);
|
||||
if (empty($va) || empty($vb)) {
|
||||
return $out;
|
||||
}
|
||||
$vaIn = implode(',', array_map('intval', $va));
|
||||
$vbIn = implode(',', array_map('intval', $vb));
|
||||
$since = time() - $days * 86400;
|
||||
|
||||
$aToB = 0;
|
||||
$bToA = 0;
|
||||
|
||||
// Preferred: persistent transfer log (created by push protection).
|
||||
$hasLog = @mysqli_query($link,
|
||||
"SELECT 1 FROM `" . TB_PREFIX . "resource_transfer_log` LIMIT 1");
|
||||
if ($hasLog !== false) {
|
||||
@mysqli_free_result($hasLog);
|
||||
$sumCols = "(wood+clay+iron+crop)";
|
||||
$r = @mysqli_query($link,
|
||||
"SELECT COALESCE(SUM($sumCols),0) FROM `" . TB_PREFIX . "resource_transfer_log`
|
||||
WHERE from_vref IN ($vaIn) AND to_vref IN ($vbIn) AND `time` >= " . (int) $since);
|
||||
if ($r && ($row = mysqli_fetch_row($r))) {
|
||||
$aToB = (int) $row[0];
|
||||
}
|
||||
$r = @mysqli_query($link,
|
||||
"SELECT COALESCE(SUM($sumCols),0) FROM `" . TB_PREFIX . "resource_transfer_log`
|
||||
WHERE from_vref IN ($vbIn) AND to_vref IN ($vaIn) AND `time` >= " . (int) $since);
|
||||
if ($r && ($row = mysqli_fetch_row($r))) {
|
||||
$bToA = (int) $row[0];
|
||||
}
|
||||
} else {
|
||||
// Fallback: in-flight merchant transfers (movement sort_type = 0),
|
||||
// resources live in the linked `send` row (m.ref = s.id).
|
||||
$sql = "SELECT COALESCE(SUM(s.wood+s.clay+s.iron+s.crop),0)
|
||||
FROM `" . TB_PREFIX . "movement` m
|
||||
JOIN `" . TB_PREFIX . "send` s ON m.ref = s.id
|
||||
WHERE m.sort_type = 0 AND m.proc = 0
|
||||
AND m.`from` IN (%FROM%) AND m.`to` IN (%TO%)";
|
||||
$r = @mysqli_query($link, str_replace(['%FROM%', '%TO%'], [$vaIn, $vbIn], $sql));
|
||||
if ($r && ($row = mysqli_fetch_row($r))) {
|
||||
$aToB = (int) $row[0];
|
||||
}
|
||||
$r = @mysqli_query($link, str_replace(['%FROM%', '%TO%'], [$vbIn, $vaIn], $sql));
|
||||
if ($r && ($row = mysqli_fetch_row($r))) {
|
||||
$bToA = (int) $row[0];
|
||||
}
|
||||
}
|
||||
|
||||
$gross = $aToB + $bToA;
|
||||
$out['gross'] = $gross;
|
||||
if ($gross > 0) {
|
||||
$maxDir = max($aToB, $bToA);
|
||||
// Strongly one-directional if >= 85% flows one way and it is material.
|
||||
$out['directional'] = ($maxDir >= 0.85 * $gross) && ($gross >= 5000);
|
||||
}
|
||||
return $out;
|
||||
}
|
||||
|
||||
/** Village wrefs owned by a user (cached per-request). */
|
||||
private static function villagesOf($uid, $link)
|
||||
{
|
||||
static $cache = [];
|
||||
$uid = (int) $uid;
|
||||
if (isset($cache[$uid])) {
|
||||
return $cache[$uid];
|
||||
}
|
||||
$out = [];
|
||||
$r = @mysqli_query($link,
|
||||
"SELECT wref FROM `" . TB_PREFIX . "vdata` WHERE owner = " . $uid);
|
||||
if ($r) {
|
||||
while ($row = mysqli_fetch_row($r)) {
|
||||
$out[] = (int) $row[0];
|
||||
}
|
||||
mysqli_free_result($r);
|
||||
}
|
||||
return $cache[$uid] = $out;
|
||||
}
|
||||
|
||||
/** Fill name_a / name_b for the reported pairs in one query. */
|
||||
private static function attachNames(array &$pairs, $link)
|
||||
{
|
||||
if (empty($pairs)) {
|
||||
return;
|
||||
}
|
||||
$ids = [];
|
||||
foreach ($pairs as $p) {
|
||||
$ids[$p['uid_a']] = true;
|
||||
$ids[$p['uid_b']] = true;
|
||||
}
|
||||
$in = implode(',', array_map('intval', array_keys($ids)));
|
||||
$names = [];
|
||||
$r = @mysqli_query($link,
|
||||
"SELECT id, username FROM `" . TB_PREFIX . "users` WHERE id IN ($in)");
|
||||
if ($r) {
|
||||
while ($row = mysqli_fetch_assoc($r)) {
|
||||
$names[(int) $row['id']] = $row['username'];
|
||||
}
|
||||
mysqli_free_result($r);
|
||||
}
|
||||
foreach ($pairs as &$p) {
|
||||
$p['name_a'] = $names[$p['uid_a']] ?? ('#' . $p['uid_a']);
|
||||
$p['name_b'] = $names[$p['uid_b']] ?? ('#' . $p['uid_b']);
|
||||
}
|
||||
unset($p);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,529 @@
|
||||
<?php
|
||||
#################################################################################
|
||||
## -= YOU MAY NOT REMOVE OR CHANGE THIS NOTICE =- ##
|
||||
## --------------------------------------------------------------------------- ##
|
||||
## Filename : PushProtection.php ##
|
||||
## Type : Push-Protection engine (Admin/Multihunter dashboard) ##
|
||||
## --------------------------------------------------------------------------- ##
|
||||
## Developed by : Shadow (Cătălin) ##
|
||||
## Project : TravianZ ##
|
||||
## GitHub : https://github.com/Shadowss/TravianZ ##
|
||||
## --------------------------------------------------------------------------- ##
|
||||
## License : TravianZ Project ##
|
||||
## Copyright : TravianZ (c) 2010-2026. All rights reserved. ##
|
||||
## --------------------------------------------------------------------------- ##
|
||||
#################################################################################
|
||||
|
||||
/**
|
||||
* PushProtection
|
||||
* -------------------------------------------------------------------------
|
||||
* Visibility + control layer over inter-player resource transfers, modelled
|
||||
* on Travian Legends' pushing-protection rules:
|
||||
*
|
||||
* - Every COMPLETED marketplace delivery between DIFFERENT players is logged
|
||||
* to resource_transfer_log (hook in Automation::marketCompleteDeliveries).
|
||||
* - For each receiving player we compute, over a rolling 7-day window, the
|
||||
* total resources RECEIVED FROM OTHERS ("7-day balance").
|
||||
* - We compute an automatic limit from the player's hourly production
|
||||
* ("hours of production" model: limit = HOURS_ALLOWED * net hourly prod).
|
||||
* - Exceptions (like TL): WW villages may be supplied without limit; artefact
|
||||
* villages are crop-supply exceptions. These are surfaced as suggested
|
||||
* overrides, applied by an admin with one click (push_override table).
|
||||
*
|
||||
* This is a DASHBOARD + override store. It does not block transfers by itself;
|
||||
* an optional enforcement hook (allowedToReceive) is provided for the market
|
||||
* send flow if you later want hard enforcement.
|
||||
*
|
||||
* Self-contained (static, resolves DB link from globals) so both Automation
|
||||
* (logging) and the admin panel (dashboard) can use it without wiring.
|
||||
*/
|
||||
class PushProtection
|
||||
{
|
||||
/* ---- Tunables ------------------------------------------------------ */
|
||||
|
||||
/** Rolling balance window, in days (TL uses 7). */
|
||||
const WINDOW_DAYS = 7;
|
||||
|
||||
/** Aggregate allowance: how many HOURS of the receiver's own production may
|
||||
* be received from other players within the window. TL varies this by
|
||||
* relationship (1 / 14 / 21h); this single aggregate cap is a moderation
|
||||
* simplification — tune to taste. */
|
||||
const HOURS_ALLOWED = 24;
|
||||
|
||||
/** Percent-of-limit at which a player is flagged "near limit". */
|
||||
const NEAR_LIMIT_PCT = 80;
|
||||
|
||||
/** Override modes stored in push_override.mode. */
|
||||
const OV_NONE = 0; // no override -> 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);
|
||||
}
|
||||
}
|
||||
@@ -201,6 +201,15 @@ function __construct() {
|
||||
|
||||
$logging->addLoginLog($dbarray['id'], \App\Utils\IpResolver::getClientIp() ?? ($_SERVER['REMOTE_ADDR'] ?? '0.0.0.0'));
|
||||
|
||||
// Multi-account detection: record this login's fingerprint (IP + User-Agent).
|
||||
// Best-effort — MultiAccount::recordSession() swallows all errors so it can
|
||||
// never block a login, and it self-creates its table on first use.
|
||||
MultiAccount::recordSession(
|
||||
$dbarray['id'],
|
||||
\App\Utils\IpResolver::getClientIp() ?? ($_SERVER['REMOTE_ADDR'] ?? '0.0.0.0'),
|
||||
$_SERVER['HTTP_USER_AGENT'] ?? ''
|
||||
);
|
||||
|
||||
if ($dbarray['id'] == 1) {
|
||||
header("Location: nachrichten.php");
|
||||
exit;
|
||||
|
||||
@@ -340,6 +340,8 @@ define("NEW_FUNCTION_TRIBE_HUNS", %NEW_FUNCTION_TRIBE_HUNS%);
|
||||
define("NEW_FUNCTION_TRIBE_EGIPTEANS", %NEW_FUNCTION_TRIBE_EGIPTEANS%);
|
||||
define("NEW_FUNCTION_TRIBE_SPARTANS", %NEW_FUNCTION_TRIBE_SPARTANS%);
|
||||
define("NEW_FUNCTION_TRIBE_VIKINGS", %NEW_FUNCTION_TRIBE_VIKINGS%);
|
||||
define("NEW_FUNCTION_REGISTRATION_GOLD", %NEW_FUNCTION_REGISTRATION_GOLD%);
|
||||
define("NEW_FUNCTION_REGISTRATION_GOLD_VALUE", %NEW_FUNCTION_REGISTRATION_GOLD_VALUE%);
|
||||
|
||||
//////////////////////////////////////////
|
||||
// **** DO NOT EDIT SETTINGS **** //
|
||||
|
||||
@@ -176,6 +176,8 @@ class Process {
|
||||
$findReplace["%NEW_FUNCTION_TRIBE_EGIPTEANS%"] = $_POST['new_function_tribe_egipteans'];
|
||||
$findReplace["%NEW_FUNCTION_TRIBE_SPARTANS%"] = $_POST['new_function_tribe_spartans'];
|
||||
$findReplace["%NEW_FUNCTION_TRIBE_VIKINGS%"] = $_POST['new_function_tribe_vikings'];
|
||||
$findReplace["%NEW_FUNCTION_REGISTRATION_GOLD%"] = $_POST['new_function_registration_gold'];
|
||||
$findReplace["%NEW_FUNCTION_REGISTRATION_GOLD_VALUE%"] = $_POST['new_function_registration_gold_value'];
|
||||
|
||||
fwrite($gameConfig, str_replace(array_keys($findReplace), array_values($findReplace), $text));
|
||||
|
||||
|
||||
@@ -212,7 +212,9 @@ $mechs = [
|
||||
'new_function_tribe_huns' => 'New Tribe: Huns',
|
||||
'new_function_tribe_egipteans' => 'New Tribe: Egyptians',
|
||||
'new_function_tribe_spartans' => 'New Tribe: Spartans',
|
||||
'new_function_tribe_vikings' => 'New Tribe: Vikings'
|
||||
'new_function_tribe_vikings' => 'New Tribe: Vikings',
|
||||
'new_function_tribe_vikings' => 'New Tribe: Vikings',
|
||||
'new_function_registration_gold' => 'Registration Bonus Gold'
|
||||
];
|
||||
|
||||
foreach($mechs as $k => $l){
|
||||
@@ -226,7 +228,10 @@ foreach($mechs as $k => $l){
|
||||
</div>";
|
||||
}
|
||||
?>
|
||||
|
||||
<div style='display:flex;flex-direction:column;gap:4px;margin-top:4px;'>
|
||||
<label>Registration Bonus Gold – amount (used only when the toggle above is true)</label>
|
||||
<input class='input' type='number' min='0' step='1' name='new_function_registration_gold_value' id='new_function_registration_gold_value' value='200'>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -1121,6 +1121,68 @@ CREATE TABLE IF NOT EXISTS `%PREFIX%login_log` (
|
||||
PRIMARY KEY (`id`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
|
||||
|
||||
-- --------------------------------------------------------
|
||||
|
||||
--
|
||||
-- Table structure for table `%prefix%mad_session`
|
||||
-- (Multi-Account Detection: per-login fingerprint - IP + User-Agent)
|
||||
--
|
||||
|
||||
CREATE TABLE IF NOT EXISTS `%PREFIX%mad_session` (
|
||||
`id` int(11) NOT NULL AUTO_INCREMENT,
|
||||
`uid` int(11) NOT NULL,
|
||||
`ip` varbinary(16) DEFAULT NULL,
|
||||
`ip_text` varchar(45) NOT NULL DEFAULT '',
|
||||
`ua_hash` char(32) NOT NULL DEFAULT '',
|
||||
`ua_text` varchar(255) NOT NULL DEFAULT '',
|
||||
`login_time` int(11) NOT NULL DEFAULT 0,
|
||||
PRIMARY KEY (`id`),
|
||||
KEY `uid` (`uid`),
|
||||
KEY `ip` (`ip`),
|
||||
KEY `ua_hash` (`ua_hash`),
|
||||
KEY `login_time` (`login_time`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
|
||||
|
||||
-- --------------------------------------------------------
|
||||
|
||||
--
|
||||
-- Table structure for table `%prefix%resource_transfer_log`
|
||||
-- (Push Protection: completed cross-player marketplace deliveries)
|
||||
--
|
||||
|
||||
CREATE TABLE IF NOT EXISTS `%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;
|
||||
|
||||
-- --------------------------------------------------------
|
||||
|
||||
--
|
||||
-- Table structure for table `%prefix%push_override`
|
||||
-- (Push Protection: manual per-player exemptions / custom caps)
|
||||
--
|
||||
|
||||
CREATE TABLE IF NOT EXISTS `%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;
|
||||
|
||||
--
|
||||
-- Dumping data for table `%prefix%login_log`
|
||||
--
|
||||
|
||||
Reference in New Issue
Block a user