mirror of
https://github.com/Shadowss/TravianZ.git
synced 2026-06-28 00:24:23 +00:00
3607a566f9
Add notice
385 lines
16 KiB
PHP
385 lines
16 KiB
PHP
<?php
|
||
|
||
|
||
/**
|
||
* crop_builder.php
|
||
* Admin UI to build/rebuild the precomputed croppers table (9c/15c + best oasis bonus).
|
||
* The UI is rendered inside the page’s main content area with normal “player” styling.
|
||
*/
|
||
|
||
#################################################################################
|
||
## -= YOU MAY NOT REMOVE OR CHANGE THIS NOTICE =- ##
|
||
## --------------------------------------------------------------------------- ##
|
||
## Project: TravianZ ##
|
||
## Filename build_croppers.php ##
|
||
## Developed by: TravianZ ##
|
||
## License: TravianZ Project ##
|
||
## Copyright: TravianZ (c) 2010-2026. All rights reserved. ##
|
||
## URLs: http://travian.shadowss.ro ##
|
||
## Source code: https://github.com/Shadowss/TravianZ ##
|
||
## ##
|
||
#################################################################################
|
||
|
||
use App\Utils\AccessLogger;
|
||
|
||
include_once("GameEngine/Generator.php");
|
||
$start_timer = $generator->pageLoadTimeStart();
|
||
|
||
include_once("GameEngine/Session.php");
|
||
include_once("GameEngine/config.php");
|
||
include_once("GameEngine/Database.php");
|
||
include_once("GameEngine/Village.php");
|
||
|
||
AccessLogger::logRequest();
|
||
|
||
// ---------- Admin gate ----------
|
||
if (!isset($session) || !isset($session->access) || (int)$session->access < 8) {
|
||
header('Location: dorf1.php');
|
||
exit;
|
||
}
|
||
|
||
// ---------- Setup ----------
|
||
@session_start();
|
||
|
||
$TBP = defined('TB_PREFIX') ? TB_PREFIX : 's1_';
|
||
$CROP_TABLE = $TBP . 'croppers';
|
||
$WDATA = $TBP . 'wdata';
|
||
|
||
// Build an absolute-safe asset prefix for CSS/JS
|
||
$assetBase = $session->gpack ?: GP_LOCATE;
|
||
$assetBase = '/'.ltrim($assetBase, '/');
|
||
|
||
// CSRF
|
||
if (empty($_SESSION['csrf_cb'])) {
|
||
$_SESSION['csrf_cb'] = bin2hex(random_bytes(16));
|
||
}
|
||
$csrf = $_SESSION['csrf_cb'];
|
||
|
||
function h($s){ return htmlspecialchars((string)$s, ENT_QUOTES, 'UTF-8'); }
|
||
|
||
function indexExists($db, $tableName, $indexName) {
|
||
$sql = "SELECT 1 FROM information_schema.statistics WHERE table_schema = DATABASE() AND table_name = '".mysqli_real_escape_string($db, $tableName)."' AND index_name = '".mysqli_real_escape_string($db, $indexName)."' LIMIT 1";
|
||
$result = mysqli_query($db, $sql);
|
||
if (!$result) {
|
||
return false;
|
||
}
|
||
return mysqli_num_rows($result) > 0;
|
||
}
|
||
|
||
function createIndexIfMissing($db, $tableName, $indexName, $columnsSql) {
|
||
if (!indexExists($db, $tableName, $indexName)) {
|
||
mysqli_query($db, "CREATE INDEX `{$indexName}` ON `{$tableName}` ({$columnsSql})");
|
||
}
|
||
}
|
||
|
||
function dropIndexIfExists($db, $tableName, $indexName) {
|
||
if (indexExists($db, $tableName, $indexName)) {
|
||
mysqli_query($db, "DROP INDEX `{$indexName}` ON `{$tableName}`");
|
||
}
|
||
}
|
||
|
||
// Ensure table exists (minimal schema, unsigned tinyints)
|
||
mysqli_query($database->dblink, "CREATE TABLE IF NOT EXISTS `$CROP_TABLE` (
|
||
`wref` INT UNSIGNED NOT NULL PRIMARY KEY,
|
||
`x` INT NOT NULL,
|
||
`y` INT NOT NULL,
|
||
`fieldtype` TINYINT UNSIGNED NOT NULL,
|
||
`best_oasis_bonus` TINYINT UNSIGNED NOT NULL,
|
||
`updated_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||
CHECK (`best_oasis_bonus` IN (0,25,50,75,100,125,150))
|
||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4");
|
||
|
||
// Helpful indexes (idempotent, avoids PHP8 mysqli duplicate-index exceptions)
|
||
createIndexIfMissing($database->dblink, $CROP_TABLE, 'idx_ft_bonus_xy', '`fieldtype`, `best_oasis_bonus`, `x`, `y`');
|
||
createIndexIfMissing($database->dblink, $CROP_TABLE, 'idx_xy', '`x`, `y`');
|
||
createIndexIfMissing($database->dblink, $CROP_TABLE, 'idx_bonus', '`best_oasis_bonus`');
|
||
|
||
// ---------- Helpers ----------
|
||
function worldSizeLabel(): string {
|
||
if (defined('WORLD_MIN') && defined('WORLD_MAX')) {
|
||
$min = (int)WORLD_MIN; $max = (int)WORLD_MAX;
|
||
return ($max - $min + 1) . "×" . ($max - $min + 1) . " (" . $min . " .. " . $max . ")";
|
||
}
|
||
if (defined('WORLD_MAX')) {
|
||
$max = (int)WORLD_MAX; $min = -$max;
|
||
return ($max - $min + 1) . "×" . ($max - $min + 1) . " (" . $min . " .. " . $max . ")";
|
||
}
|
||
return "unknown";
|
||
}
|
||
|
||
function getCounts($db, $WDATA, $CROP_TABLE) {
|
||
$c1 = mysqli_fetch_assoc(mysqli_query($db, "SELECT COUNT(*) c FROM `$WDATA` WHERE fieldtype IN (1,6)"));
|
||
$c2 = mysqli_fetch_assoc(mysqli_query($db, "SELECT COUNT(*) c FROM `$CROP_TABLE`"));
|
||
$lu = mysqli_fetch_assoc(mysqli_query($db, "SELECT MAX(updated_at) lu FROM `$CROP_TABLE`"));
|
||
return [
|
||
'croppers_world' => (int)($c1['c'] ?? 0),
|
||
'croppers_table' => (int)($c2['c'] ?? 0),
|
||
'last_updated' => $lu['lu'] ?? null,
|
||
];
|
||
}
|
||
|
||
// stream log
|
||
function startStreaming() {
|
||
@ini_set('output_buffering','off');
|
||
@ini_set('zlib.output_compression', 0);
|
||
while (ob_get_level()) { @ob_end_flush(); }
|
||
ob_implicit_flush(true);
|
||
echo "<pre id=\"log\" style=\"background:#0b0f17;color:#d7e1f8;padding:12px;border-radius:12px;max-height:60vh;overflow:auto;\">";
|
||
echo htmlspecialchars("[".date('H:i:s')."] Croppers builder started")."\n";
|
||
flush();
|
||
}
|
||
function logLine($msg) { echo htmlspecialchars("[".date('H:i:s')."] ".$msg)."\n"; flush(); }
|
||
function endStreaming() { echo "</pre>"; flush(); }
|
||
|
||
// ---------- Actions ----------
|
||
$action = $_POST['action'] ?? null;
|
||
$okCsrf = isset($_POST['csrf']) && hash_equals($_SESSION['csrf_cb'], $_POST['csrf']);
|
||
$notice = null;
|
||
|
||
if ($action && !$okCsrf) {
|
||
$notice = "Invalid CSRF token. Please reload the page.";
|
||
$action = null;
|
||
}
|
||
|
||
if ($action === 'truncate') {
|
||
mysqli_query($database->dblink, "TRUNCATE TABLE `$CROP_TABLE`");
|
||
$notice = "Croppers table truncated.";
|
||
}
|
||
if ($action === 'reindex') {
|
||
dropIndexIfExists($database->dblink, $CROP_TABLE, 'idx_ft_bonus_xy');
|
||
dropIndexIfExists($database->dblink, $CROP_TABLE, 'idx_xy');
|
||
dropIndexIfExists($database->dblink, $CROP_TABLE, 'idx_bonus');
|
||
createIndexIfMissing($database->dblink, $CROP_TABLE, 'idx_ft_bonus_xy', '`fieldtype`, `best_oasis_bonus`, `x`, `y`');
|
||
createIndexIfMissing($database->dblink, $CROP_TABLE, 'idx_xy', '`x`, `y`');
|
||
createIndexIfMissing($database->dblink, $CROP_TABLE, 'idx_bonus', '`best_oasis_bonus`');
|
||
$notice = "Indexes rebuilt.";
|
||
}
|
||
if ($action === 'estimate') {
|
||
$notice = "Estimated counts refreshed.";
|
||
}
|
||
|
||
$stats = getCounts($database->dblink, $WDATA, $CROP_TABLE);
|
||
$worldLabel = worldSizeLabel();
|
||
|
||
?>
|
||
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
|
||
<html>
|
||
<head>
|
||
<title><?php echo SERVER_NAME ?> - Mass Message</title>
|
||
<link rel="shortcut icon" href="favicon.ico"/>
|
||
<meta http-equiv="cache-control" content="max-age=0" />
|
||
<meta http-equiv="pragma" content="no-cache" />
|
||
<meta http-equiv="expires" content="0" />
|
||
<meta http-equiv="imagetoolbar" content="no" />
|
||
<meta http-equiv="content-type" content="text/html; charset=UTF-8" />
|
||
|
||
<script src="mt-full.js?0ac37" type="text/javascript"></script>
|
||
<script src="unx.js?f4b7h" type="text/javascript"></script>
|
||
<script src="new.js?0ac37" type="text/javascript"></script>
|
||
<link href="<?php echo GP_LOCATE; ?>lang/en/lang.css?f4b7d" rel="stylesheet" type="text/css" />
|
||
<link href="<?php echo GP_LOCATE; ?>lang/en/compact.css?f4b7i" rel="stylesheet" type="text/css" />
|
||
<?php
|
||
if($session->gpack == null || GP_ENABLE == false) {
|
||
echo "
|
||
<link href='".GP_LOCATE."travian.css?e21d2' rel='stylesheet' type='text/css' />
|
||
<link href='".GP_LOCATE."lang/en/lang.css?e21d2' rel='stylesheet' type='text/css' />";
|
||
} else {
|
||
echo "
|
||
<link href='".$session->gpack."travian.css?e21d2' rel='stylesheet' type='text/css' />
|
||
<link href='".$session->gpack."lang/en/lang.css?e21d2' rel='stylesheet' type='text/css' />";
|
||
}
|
||
?>
|
||
|
||
<script language="javascript" type="text/javascript">
|
||
function smilie(text) {
|
||
document.myform.message.value += text;
|
||
}
|
||
</script>
|
||
|
||
<script language="javascript">
|
||
function toggleDisplay(e){
|
||
element = document.getElementById(e).style;
|
||
element.display == 'none' ? element.display = 'block' :
|
||
element.display='none';
|
||
}
|
||
</script>
|
||
|
||
<script type="text/javascript">
|
||
|
||
window.addEvent('domready', start);
|
||
</script>
|
||
<?php
|
||
if($session->gpack == null || GP_ENABLE == false) {
|
||
echo "
|
||
<link href='".GP_LOCATE."travian.css?e21d2' rel='stylesheet' type='text/css' />
|
||
<link href='".GP_LOCATE."lang/en/lang.css?e21d2' rel='stylesheet' type='text/css' />";
|
||
} else {
|
||
echo "
|
||
<link href='".$session->gpack."travian.css?e21d2' rel='stylesheet' type='text/css' />
|
||
<link href='".$session->gpack."lang/en/lang.css?e21d2' rel='stylesheet' type='text/css' />";
|
||
}
|
||
?>
|
||
<script type="text/javascript">
|
||
window.addEvent('domready', start);
|
||
</script>
|
||
<style>
|
||
.cb-container{ max-width:980px;margin:0 auto;padding:0 12px; }
|
||
.cb-grid{ display:grid; grid-template-columns:1fr 1fr; gap:12px; }
|
||
.cb-card{ background:#fff;border:1px solid #dcdde1;border-radius:12px;padding:14px; box-shadow:0 1px 2px rgba(0,0,0,.04); }
|
||
.cb-title{ font-size:20px;margin:0 0 8px 0; }
|
||
.cb-muted{ color:#666;font-size:12px; }
|
||
.cb-kpis{ display:flex; gap:16px; margin-top:8px; flex-wrap:wrap; }
|
||
.cb-kpi{ background:#f7f9fb;border-radius:10px;padding:10px 12px; border:1px solid #e6ecf3; }
|
||
.cb-actions form{ display:flex; flex-wrap:wrap; gap:8px; align-items:center; }
|
||
.cb-btn{ background:#2f7d32;color:#fff;border:none;border-radius:10px;padding:10px 14px;cursor:pointer; }
|
||
.cb-btn.red{ background:#b23b3b; }
|
||
.cb-btn.gray{ background:#5c6b7a; }
|
||
.cb-input{ padding:8px 10px;border:1px solid #c9d3df;border-radius:10px;width:120px; }
|
||
.cb-note{ background:#fff4cc;border:1px solid #f5d36b;border-radius:10px;padding:10px 12px;margin-top:8px; }
|
||
.cb-notice{ background:#e8f6ff;border:1px solid #b3e0ff;color:#0b5380;border-radius:10px;padding:10px 12px;margin:12px 0; }
|
||
@media (max-width: 900px){ .cb-grid{ grid-template-columns:1fr; } }
|
||
pre#log { margin-top:14px; }
|
||
</style>
|
||
</head>
|
||
|
||
<body class="v35 ie ie8">
|
||
<div class="wrapper">
|
||
<img style="filter:chroma();" src="img/x.gif" id="msfilter" alt="" />
|
||
<div id="dynamic_header"></div>
|
||
<?php include("Templates/header.tpl"); ?>
|
||
<div id="mid">
|
||
<?php include("Templates/menu.tpl"); ?>
|
||
|
||
<!-- IMPORTANT: Use the normal game content style instead of "login" -->
|
||
<div id="content" class="player">
|
||
<div class="cb-container">
|
||
<h1 style="text-align:center;margin:12px 0 16px;">Build crop finder</h1>
|
||
|
||
<?php if ($notice): ?>
|
||
<div class="cb-notice"><?php echo h($notice); ?></div>
|
||
<?php endif; ?>
|
||
|
||
<div class="cb-grid">
|
||
<div class="cb-card">
|
||
<h3 class="cb-title">Status</h3>
|
||
<div class="cb-muted">World: <?php echo h($worldLabel); ?></div>
|
||
<div class="cb-kpis">
|
||
<div class="cb-kpi"><b>9c/15c in map:</b> <?php echo number_format($stats['croppers_world']); ?></div>
|
||
<div class="cb-kpi"><b>Rows in table:</b> <?php echo number_format($stats['croppers_table']); ?></div>
|
||
<div class="cb-kpi"><b>Last updated:</b> <?php echo $stats['last_updated'] ? h($stats['last_updated']) : '—'; ?></div>
|
||
</div>
|
||
<div class="cb-note">
|
||
The croppers table stores only <b>wref,x,y,fieldtype,best_oasis_bonus</b>.
|
||
Ownership and occupied status are pulled live from <code>vdata/users</code> by the finder.
|
||
</div>
|
||
</div>
|
||
|
||
<div class="cb-card cb-actions">
|
||
<h3 class="cb-title">Actions</h3>
|
||
<form method="post">
|
||
<input type="hidden" name="csrf" value="<?php echo h($csrf); ?>" />
|
||
<label>Batch size:</label>
|
||
<input class="cb-input" type="number" min="1000" max="20000" step="1000" name="batch" value="<?php echo isset($_POST['batch']) ? (int)$_POST['batch'] : 5000; ?>" />
|
||
<button class="cb-btn" name="action" value="build">Build / Rebuild</button>
|
||
<button class="cb-btn gray" name="action" value="estimate" type="submit">Estimate</button>
|
||
<button class="cb-btn gray" name="action" value="reindex" type="submit">Reindex</button>
|
||
<button class="cb-btn red" name="action" value="truncate" type="submit" onclick="return confirm('Really truncate the table?');">Truncate</button>
|
||
</form>
|
||
<div class="cb-muted" style="margin-top:8px;">
|
||
Building streams progress below. You can leave this page; the process stops when the request ends.
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<?php
|
||
// Stream build logs INSIDE the content area
|
||
if ($action === 'build' && $okCsrf) {
|
||
$batch = max(1000, min(20000, (int)($_POST['batch'] ?? 5000)));
|
||
startStreaming();
|
||
|
||
$cnt = mysqli_fetch_assoc(mysqli_query($database->dblink, "SELECT COUNT(*) AS c FROM `$WDATA` WHERE `fieldtype` IN (1,6)"));
|
||
$target = (int)($cnt['c'] ?? 0);
|
||
logLine("Detected $target croppers.");
|
||
|
||
$offset = 0; $total = 0;
|
||
while (true) {
|
||
$sql = "SELECT id AS wref, x, y, fieldtype
|
||
FROM `$WDATA`
|
||
WHERE `fieldtype` IN (1,6)
|
||
LIMIT $offset, $batch";
|
||
$res = mysqli_query($database->dblink, $sql);
|
||
if (!$res) { logLine('Query failed: '.mysqli_error($database->dblink)); break; }
|
||
|
||
$rows = [];
|
||
while ($r = mysqli_fetch_assoc($res)) { $rows[] = $r; }
|
||
if (!$rows) break;
|
||
|
||
$values = [];
|
||
foreach ($rows as $r) {
|
||
$x = (int)$r['x']; $y = (int)$r['y'];
|
||
$bonus = (int)$database->getBestOasisCropBonus($x, $y);
|
||
if (!in_array($bonus, [0,25,50,75,100,125,150], true)) {
|
||
$bonus = max(0, min(150, $bonus));
|
||
}
|
||
$values[] = sprintf("(%d,%d,%d,%d,%d)",
|
||
(int)$r['wref'], $x, $y, (int)$r['fieldtype'], $bonus);
|
||
}
|
||
if ($values) {
|
||
$sql = "REPLACE INTO `$CROP_TABLE`
|
||
(`wref`,`x`,`y`,`fieldtype`,`best_oasis_bonus`)
|
||
VALUES ".implode(',', $values);
|
||
if (!mysqli_query($database->dblink, $sql)) {
|
||
logLine('Upsert failed: '.mysqli_error($database->dblink));
|
||
break;
|
||
}
|
||
}
|
||
$countThis = count($rows);
|
||
$total += $countThis;
|
||
$offset += $batch;
|
||
logLine("Processed $total / $target");
|
||
}
|
||
|
||
@mysqli_query($database->dblink, "ANALYZE TABLE `$CROP_TABLE`");
|
||
logLine("Analyze complete.");
|
||
logLine("Done.");
|
||
endStreaming();
|
||
|
||
// Refresh stats after build
|
||
$stats = getCounts($database->dblink, $WDATA, $CROP_TABLE);
|
||
}
|
||
?>
|
||
</div>
|
||
</div>
|
||
|
||
<br /><br /><br /><br />
|
||
<div id="side_info">
|
||
<?php
|
||
include("Templates/multivillage.tpl");
|
||
include("Templates/quest.tpl");
|
||
include("Templates/news.tpl");
|
||
if(!NEW_FUNCTIONS_DISPLAY_LINKS) {
|
||
echo "<br><br><br><br>";
|
||
include("Templates/links.tpl");
|
||
}
|
||
?>
|
||
</div>
|
||
<div class="clear"></div>
|
||
</div>
|
||
<div class="footer-stopper"></div>
|
||
<div class="clear"></div>
|
||
|
||
<?php include("Templates/footer.tpl"); include("Templates/res.tpl"); ?>
|
||
|
||
<div id="stime">
|
||
<div id="ltime">
|
||
<div id="ltimeWrap">
|
||
<?php echo CALCULATED_IN;?> <b><?php echo round(($generator->pageLoadTimeEnd()-$start_timer)*1000); ?></b> ms
|
||
<br /><?php echo SERVER_TIME;?> <span id="tp1" class="b"><?php echo date('H:i:s'); ?></span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<div id="ce"></div>
|
||
</div>
|
||
</body>
|
||
</html>
|