Add (Admin Panel) : Multi-account detection, Push protection, Heatmap, Gold Shop and Blacklist user names

Add (Admin Panel) : Multi-account detection, Push protection, Heatmap, Gold Shop (redem codes), Blacklist user names and Quest Editor + fix some php notice

Fix quest_core for mission 33,36, train 2 units, etc (NEW TRIBE)

There are some temporary bugs that will be fixed in next commit (pagination for new heatmap, quest reward)
This commit is contained in:
novgorodschi catalin
2026-07-16 11:09:48 +03:00
parent 37e845a4d6
commit c7ba50036f
27 changed files with 2450 additions and 115 deletions
+10
View File
@@ -98,6 +98,11 @@ class Account {
$form->addError("name", USRNM_TAKEN);
} elseif (User::exists($database, $_POST['name'])) {
$form->addError("name", USRNM_TAKEN);
} elseif (class_exists('RegBlock') && RegBlock::isBlockedName($database, $_POST['name'])) {
// Registration blocklist (admin: blockReg).
$form->addError("name", defined('REG_BLOCKED_NAME')
? REG_BLOCKED_NAME
: 'This username is not allowed. Please choose another.');
}
}
@@ -119,6 +124,11 @@ class Account {
$form->addError("email", EMAIL_INVALID);
} elseif (User::exists($database, $_POST['email'])) {
$form->addError("email", EMAIL_TAKEN);
} elseif (class_exists('RegBlock') && RegBlock::isBlockedEmail($database, $_POST['email'])) {
// Registration blocklist: exact address or whole domain (admin: blockReg).
$form->addError("email", defined('REG_BLOCKED_EMAIL')
? REG_BLOCKED_EMAIL
: 'Registration with this e-mail address or domain is not allowed.');
}
// Tribe
+80
View File
@@ -0,0 +1,80 @@
<?php
#################################################################################
## -= YOU MAY NOT REMOVE OR CHANGE THIS NOTICE =- ##
## --------------------------------------------------------------------------- ##
## Filename blockReg.php ##
## Type BACKEND (Registration blocklist add/remove) ##
## Developed by: Shadow ##
## License: TravianZ Project ##
## Copyright: TravianZ (c) 2010-2026. All rights reserved. ##
#################################################################################
require_once(__DIR__ . '/../csrf.php');
if (!isset($_SESSION)) session_start();
if ($_SESSION['access'] < 9) {
admin_deny('You must be signed in as an administrator to do this. '
. 'Your session may have expired — please return to the admin panel and sign in again.');
}
csrf_verify();
include_once("../../config.php");
$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/RegBlock.php");
$admid = (int)($_SESSION['id'] ?? 0);
// Defence in depth: confirm the acting account really is an admin.
$check = mysqli_query($GLOBALS['link'],
"SELECT access FROM " . TB_PREFIX . "users WHERE id = " . $admid);
$acc = $check ? mysqli_fetch_assoc($check) : null;
if (!$acc || (int)$acc['access'] < ADMIN) {
admin_deny('Your session may have expired — please sign in again.');
}
$do = $_POST['do'] ?? '';
if ($do === 'add') {
$type = $_POST['type'] ?? '';
$value = $_POST['value'] ?? '';
$note = $_POST['note'] ?? '';
list($ok, $msg) = RegBlock::add($type, $value, $note, $admid);
if ($ok) {
$logMsg = 'Registration block added: ' . $type . ' = ' . RegBlock_safe($value);
$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=blockReg&msg=" . urlencode($msg));
exit;
}
if ($do === 'remove') {
$id = (int)($_POST['id'] ?? 0);
if ($id > 0 && RegBlock::remove($id)) {
mysqli_query($GLOBALS['link'],
"INSERT INTO " . TB_PREFIX . "admin_log VALUES (0, " . $admid . ", 'Registration block removed (id " . $id . ")', " . time() . ")");
header("Location: ../../../Admin/admin.php?p=blockReg&msg=" . urlencode('Block removed.'));
exit;
}
header("Location: ../../../Admin/admin.php?p=blockReg&msg=" . urlencode('Could not remove block.'));
exit;
}
header("Location: ../../../Admin/admin.php?p=blockReg");
exit;
/** Small helper: keep the admin-log message readable/short. */
function RegBlock_safe($v)
{
$v = strtolower(trim((string)$v));
return strlen($v) > 80 ? substr($v, 0, 77) . '...' : $v;
}
?>
+81
View File
@@ -0,0 +1,81 @@
<?php
#################################################################################
## -= YOU MAY NOT REMOVE OR CHANGE THIS NOTICE =- ##
## --------------------------------------------------------------------------- ##
## Filename goldPromo.php ##
## Type BACKEND (Gold shop / promo codes) ##
## Developed by: Shadow ##
## License: TravianZ Project ##
## Copyright: TravianZ (c) 2010-2026. All rights reserved. ##
#################################################################################
require_once(__DIR__ . '/../csrf.php');
if (!isset($_SESSION)) session_start();
if ($_SESSION['access'] < ADMIN) {
admin_deny('You must be signed in as an administrator to do this. '
. 'Your session may have expired — please return to the admin panel and sign in again.');
}
csrf_verify();
include_once("../../config.php");
$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/GoldShop.php");
$admid = (int)($_SESSION['id'] ?? 0);
$check = mysqli_query($GLOBALS['link'],
"SELECT access FROM " . TB_PREFIX . "users WHERE id = " . $admid);
$acc = $check ? mysqli_fetch_assoc($check) : null;
if (!$acc || (int)$acc['access'] < ADMIN) {
admin_deny('Your session may have expired — please sign in again.');
}
$do = $_POST['do'] ?? '';
$msg = '';
if ($do === 'create') {
$code = $_POST['code'] ?? '';
$gold = (int)($_POST['gold'] ?? 0);
$maxUses = (int)($_POST['max_uses'] ?? 0);
$perUser = isset($_POST['per_user']) ? 1 : 0;
// Optional expiry: number of days from now (0/empty = never).
$expDays = (int)($_POST['expires_days'] ?? 0);
$expires = $expDays > 0 ? time() + $expDays * 86400 : 0;
$note = $_POST['note'] ?? '';
list($ok, $msg) = GoldShop::createCode($code, $gold, $maxUses, $perUser, $expires, $note, $admid);
if ($ok) {
$logMsg = mysqli_real_escape_string($GLOBALS['link'],
'Promo code created: ' . GoldShop::normCode($code) . ' (' . (int)$gold . ' gold)');
mysqli_query($GLOBALS['link'],
"INSERT INTO " . TB_PREFIX . "admin_log VALUES (0, " . $admid . ", '" . $logMsg . "', " . time() . ")");
}
} elseif ($do === 'toggle') {
$id = (int)($_POST['id'] ?? 0);
$active = (int)($_POST['active'] ?? 0);
if ($id > 0) {
GoldShop::setActive($id, $active);
$msg = $active ? 'Code enabled.' : 'Code disabled.';
}
} elseif ($do === 'delete') {
$id = (int)($_POST['id'] ?? 0);
if ($id > 0) {
GoldShop::deleteCode($id);
mysqli_query($GLOBALS['link'],
"INSERT INTO " . TB_PREFIX . "admin_log VALUES (0, " . $admid . ", 'Promo code deleted (id " . $id . ")', " . time() . ")");
$msg = 'Code deleted.';
}
}
header("Location: ../../../Admin/admin.php?p=goldShop&msg=" . urlencode($msg));
exit;
?>
+75
View File
@@ -0,0 +1,75 @@
<?php
#################################################################################
## -= YOU MAY NOT REMOVE OR CHANGE THIS NOTICE =- ##
## --------------------------------------------------------------------------- ##
## Filename questSave.php ##
## Type BACKEND (Quest editor save/reset) ##
## Developed by: Shadow ##
## License: TravianZ Project ##
## Copyright: TravianZ (c) 2010-2026. All rights reserved. ##
#################################################################################
require_once(__DIR__ . '/../csrf.php');
if (!isset($_SESSION)) session_start();
if ($_SESSION['access'] < ADMIN) {
admin_deny('You must be signed in as an administrator to do this. '
. 'Your session may have expired — please return to the admin panel and sign in again.');
}
csrf_verify();
include_once("../../config.php");
$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/QuestConfig.php");
$admid = (int)($_SESSION['id'] ?? 0);
$check = mysqli_query($GLOBALS['link'],
"SELECT access FROM " . TB_PREFIX . "users WHERE id = " . $admid);
$acc = $check ? mysqli_fetch_assoc($check) : null;
if (!$acc || (int)$acc['access'] < ADMIN) {
admin_deny('Your session may have expired — please sign in again.');
}
$do = $_POST['do'] ?? '';
$variant = QuestConfig::normVariant($_POST['variant'] ?? 'standard');
$msg = '';
if ($do === 'save' && isset($_POST['q']) && is_array($_POST['q'])) {
$n = 0;
foreach ($_POST['q'] as $qid => $fields) {
if (!is_array($fields)) {
continue;
}
QuestConfig::setQuest($variant, (int)$qid, [
'enabled' => !empty($fields['enabled']),
'wood' => $fields['wood'] ?? 0,
'clay' => $fields['clay'] ?? 0,
'iron' => $fields['iron'] ?? 0,
'crop' => $fields['crop'] ?? 0,
'gold' => $fields['gold'] ?? 0,
'plus_days' => $fields['plus_days'] ?? 0,
'req_level' => $fields['req_level'] ?? 0,
'note' => $fields['note'] ?? '',
]);
$n++;
}
mysqli_query($GLOBALS['link'],
"INSERT INTO " . TB_PREFIX . "admin_log VALUES (0, " . $admid . ", 'Quest editor: saved " . $n . " " . $variant . " quests', " . time() . ")");
$msg = $n . ' quests saved (' . $variant . ').';
} elseif ($do === 'reset') {
QuestConfig::resetVariant($variant);
mysqli_query($GLOBALS['link'],
"INSERT INTO " . TB_PREFIX . "admin_log VALUES (0, " . $admid . ", 'Quest editor: reset " . $variant . " to defaults', " . time() . ")");
$msg = $variant . ' quests reset to defaults.';
}
header("Location: ../../../Admin/admin.php?p=questEditor&variant=" . urlencode($variant) . "&msg=" . urlencode($msg));
exit;
?>
+2 -1
View File
@@ -3250,6 +3250,7 @@ class Automation {
$Attacker = $trapResult['Attacker'];
// we need to save the attacker heroid before the battle
$AttackerHeroID = 0;
if(isset($Attacker['uhero']) && $Attacker['uhero'] > 0){
$AttackerHeroID = $database->getHeroField($from['owner'], "heroid");
}
@@ -3690,7 +3691,7 @@ class Automation {
$reinf = $database->getEnforce($data['to'], $data['from']);
$database->modifyEnforce($reinf['id'], 31, 1, 1);
$data_fail = '0,0,4,1,0,0,0,0,0,0,0,0,0,0';
$database->addNotice($to['owner'], $to['wref'], (isset($targetally) ? $targetally : 0), 8, 'village of the elders reinforcement ' . addslashes($to['name']), $data_fail, $AttackArrivalTime);
$database->addNotice($to['owner'], $to['wref'], (isset($targetally) ? $targetally : 0), 8, 'village of the elders reinforcement ' . addslashes($to['name']), $data_fail, isset($AttackArrivalTime) ? $AttackArrivalTime : time());
}
// Flow 2 of sendreinfunitsComplete(): a standard reinforcement delivery. Handles
+1 -3
View File
@@ -6,9 +6,7 @@
## Filename : festival.php ##
## Type : Data Page for Festival ##
## --------------------------------------------------------------------------- ##
## Developed by : Dzoki ##
## Refactored by : Shadow ##
## Redesign by : Shadow ##
## Developed by : Shadow ##
## --------------------------------------------------------------------------- ##
## Contact : cata7007@gmail.com ##
## Project : TravianZ ##
+1 -1
View File
@@ -6,7 +6,7 @@
## Filename : hero_items.php ##
## Type : Static Data / Catalog for T4 Hero port ##
## --------------------------------------------------------------------------- ##
## Refactored by : Shadow ##
## Developed by : Shadow ##
## Project : TravianZ ##
## GitHub : https://github.com/Shadowss/TravianZ ##
## --------------------------------------------------------------------------- ##
+350
View File
@@ -0,0 +1,350 @@
<?php
#################################################################################
## -= YOU MAY NOT REMOVE OR CHANGE THIS NOTICE =- ##
## --------------------------------------------------------------------------- ##
## Filename : GoldShop.php ##
## Type : Gold shop / promo-code engine ##
## --------------------------------------------------------------------------- ##
## Developed by : Shadow ##
## Project : TravianZ ##
## GitHub : https://github.com/Shadowss/TravianZ ##
## --------------------------------------------------------------------------- ##
## License : TravianZ Project ##
## Copyright : TravianZ (c) 2010-2026. All rights reserved. ##
## --------------------------------------------------------------------------- ##
#################################################################################
/**
* GoldShop
* -------------------------------------------------------------------------
* Promo / voucher codes redeemable by players for gold. Admin creates codes
* (fixed gold amount, optional max-uses, optional per-user-once, optional
* expiry); players redeem them on the Plus page.
*
* "Free gold for bug hunters" is already covered by the existing admin pages
* (Give Free Gold To Specific User / To All) — a single-use code created here
* is also a convenient way to hand a bug hunter their reward.
*
* Self-contained (static, resolves DB link + optional Database from globals,
* self-creates its tables). Use-claiming is atomic (conditional UPDATE) so a
* limited code cannot be over-redeemed under concurrency.
*/
class GoldShop
{
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;
}
public static function ensureSchema()
{
$link = self::link();
if (!$link) {
return;
}
@mysqli_query($link, "CREATE TABLE IF NOT EXISTS `" . TB_PREFIX . "gold_promo` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`code` varchar(64) NOT NULL,
`gold` int(11) NOT NULL DEFAULT 0,
`max_uses` int(11) NOT NULL DEFAULT 0,
`uses` int(11) NOT NULL DEFAULT 0,
`per_user` tinyint(1) NOT NULL DEFAULT 1,
`expires` int(11) NOT NULL DEFAULT 0,
`active` tinyint(1) NOT NULL DEFAULT 1,
`note` varchar(255) NOT NULL DEFAULT '',
`created_by` int(11) NOT NULL DEFAULT 0,
`time` int(11) NOT NULL DEFAULT 0,
PRIMARY KEY (`id`),
UNIQUE KEY `code` (`code`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8");
@mysqli_query($link, "CREATE TABLE IF NOT EXISTS `" . TB_PREFIX . "gold_promo_redeem` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`promo_id` int(11) NOT NULL DEFAULT 0,
`uid` int(11) NOT NULL DEFAULT 0,
`gold` int(11) NOT NULL DEFAULT 0,
`time` int(11) NOT NULL DEFAULT 0,
PRIMARY KEY (`id`),
KEY `promo_uid` (`promo_id`,`uid`),
KEY `uid` (`uid`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8");
}
/** Normalise a code: trim, uppercase, collapse to allowed charset. */
public static function normCode($code)
{
$code = strtoupper(trim((string) $code));
return preg_replace('/[^A-Z0-9._-]/', '', $code);
}
/* ---- Admin management ---------------------------------------------- */
/**
* Create a promo code.
* @return array [ok(bool), message(string)]
*/
public static function createCode($code, $gold, $maxUses, $perUser, $expires, $note, $adminId)
{
$link = self::link();
if (!$link) {
return [false, 'No database connection.'];
}
self::ensureSchema();
$code = self::normCode($code);
if ($code === '' || strlen($code) < 3) {
return [false, 'Code must be at least 3 characters (A-Z 0-9 . _ -).'];
}
$gold = (int) $gold;
if ($gold <= 0) {
return [false, 'Gold amount must be positive.'];
}
$maxUses = max(0, (int) $maxUses);
$perUser = $perUser ? 1 : 0;
$expires = max(0, (int) $expires);
$note = substr((string) $note, 0, 255);
$admin = (int) $adminId;
$now = time();
$stmt = mysqli_prepare($link,
"INSERT INTO `" . TB_PREFIX . "gold_promo`
(code, gold, max_uses, uses, per_user, expires, active, note, created_by, time)
VALUES (?,?,?,0,?,?,1,?,?,?)");
if (!$stmt) {
return [false, 'Could not prepare statement.'];
}
mysqli_stmt_bind_param($stmt, 'siiiisii',
$code, $gold, $maxUses, $perUser, $expires, $note, $admin, $now);
$ok = mysqli_stmt_execute($stmt);
$err = mysqli_stmt_errno($stmt);
mysqli_stmt_close($stmt);
if (!$ok) {
if ($err === 1062) {
return [false, 'A code with that name already exists.'];
}
return [false, 'Could not create code.'];
}
return [true, 'Code "' . $code . '" created.'];
}
/** All codes, newest first, each with a resolved status string. */
public static function listCodes()
{
$link = self::link();
$out = [];
if (!$link) {
return $out;
}
self::ensureSchema();
$r = @mysqli_query($link,
"SELECT * FROM `" . TB_PREFIX . "gold_promo` ORDER BY `time` DESC, id DESC");
if ($r) {
$now = time();
while ($row = mysqli_fetch_assoc($r)) {
$row['status'] = self::statusOf($row, $now);
$out[] = $row;
}
mysqli_free_result($r);
}
return $out;
}
private static function statusOf($row, $now)
{
if ((int) $row['active'] !== 1) {
return 'Disabled';
}
if ((int) $row['expires'] > 0 && $now > (int) $row['expires']) {
return 'Expired';
}
if ((int) $row['max_uses'] > 0 && (int) $row['uses'] >= (int) $row['max_uses']) {
return 'Used up';
}
return 'Active';
}
public static function setActive($id, $active)
{
$link = self::link();
if (!$link) {
return false;
}
self::ensureSchema();
$stmt = mysqli_prepare($link,
"UPDATE `" . TB_PREFIX . "gold_promo` SET active = ? WHERE id = ?");
if (!$stmt) {
return false;
}
$a = $active ? 1 : 0;
$id = (int) $id;
mysqli_stmt_bind_param($stmt, 'ii', $a, $id);
$ok = mysqli_stmt_execute($stmt);
mysqli_stmt_close($stmt);
return $ok;
}
public static function deleteCode($id)
{
$link = self::link();
if (!$link) {
return false;
}
self::ensureSchema();
$id = (int) $id;
$stmt = mysqli_prepare($link, "DELETE FROM `" . TB_PREFIX . "gold_promo` WHERE id = ?");
if ($stmt) {
mysqli_stmt_bind_param($stmt, 'i', $id);
mysqli_stmt_execute($stmt);
mysqli_stmt_close($stmt);
}
return true;
}
/** Recent redemptions (joined with code + username) for the admin view. */
public static function recentRedemptions($limit = 30)
{
$link = self::link();
$out = [];
if (!$link) {
return $out;
}
self::ensureSchema();
$limit = max(1, min(200, (int) $limit));
$r = @mysqli_query($link,
"SELECT rr.time, rr.gold, rr.uid, u.username, p.code
FROM `" . TB_PREFIX . "gold_promo_redeem` rr
LEFT JOIN `" . TB_PREFIX . "users` u ON u.id = rr.uid
LEFT JOIN `" . TB_PREFIX . "gold_promo` p ON p.id = rr.promo_id
ORDER BY rr.id DESC LIMIT " . $limit);
if ($r) {
while ($row = mysqli_fetch_assoc($r)) {
$out[] = $row;
}
mysqli_free_result($r);
}
return $out;
}
/* ---- Player redemption --------------------------------------------- */
/**
* Redeem a code for a player.
* @return array [ok(bool), message(string), gold(int)]
*/
public static function redeem($uid, $code)
{
$uid = (int) $uid;
if ($uid <= 0) {
return [false, 'You must be logged in.', 0];
}
$link = self::link();
if (!$link) {
return [false, 'Service unavailable, try again later.', 0];
}
self::ensureSchema();
$code = self::normCode($code);
if ($code === '') {
return [false, 'Please enter a code.', 0];
}
// Fetch the code row.
$stmt = mysqli_prepare($link,
"SELECT * FROM `" . TB_PREFIX . "gold_promo` WHERE code = ? LIMIT 1");
if (!$stmt) {
return [false, 'Service unavailable, try again later.', 0];
}
mysqli_stmt_bind_param($stmt, 's', $code);
mysqli_stmt_execute($stmt);
$res = mysqli_stmt_get_result($stmt);
$promo = $res ? mysqli_fetch_assoc($res) : null;
mysqli_stmt_close($stmt);
if (!$promo) {
return [false, 'Invalid code.', 0];
}
$now = time();
if ((int) $promo['active'] !== 1) {
return [false, 'This code is no longer available.', 0];
}
if ((int) $promo['expires'] > 0 && $now > (int) $promo['expires']) {
return [false, 'This code has expired.', 0];
}
if ((int) $promo['max_uses'] > 0 && (int) $promo['uses'] >= (int) $promo['max_uses']) {
return [false, 'This code has already been fully redeemed.', 0];
}
$promoId = (int) $promo['id'];
$gold = (int) $promo['gold'];
// Per-user-once check.
if ((int) $promo['per_user'] === 1) {
$chk = mysqli_prepare($link,
"SELECT 1 FROM `" . TB_PREFIX . "gold_promo_redeem` WHERE promo_id = ? AND uid = ? LIMIT 1");
if ($chk) {
mysqli_stmt_bind_param($chk, 'ii', $promoId, $uid);
mysqli_stmt_execute($chk);
mysqli_stmt_store_result($chk);
$already = mysqli_stmt_num_rows($chk) > 0;
mysqli_stmt_close($chk);
if ($already) {
return [false, 'You have already redeemed this code.', 0];
}
}
}
// Atomically claim a use (guards against concurrent over-redemption).
$claim = mysqli_prepare($link,
"UPDATE `" . TB_PREFIX . "gold_promo`
SET uses = uses + 1
WHERE id = ? AND active = 1
AND (max_uses = 0 OR uses < max_uses)
AND (expires = 0 OR expires >= ?)");
if (!$claim) {
return [false, 'Service unavailable, try again later.', 0];
}
mysqli_stmt_bind_param($claim, 'ii', $promoId, $now);
mysqli_stmt_execute($claim);
$claimed = mysqli_stmt_affected_rows($claim) === 1;
mysqli_stmt_close($claim);
if (!$claimed) {
return [false, 'This code has already been fully redeemed.', 0];
}
// Grant the gold. Prefer the Database helper (also writes gold_fin_log).
$granted = false;
if (isset($GLOBALS['database']) && method_exists($GLOBALS['database'], 'modifyGold')) {
$GLOBALS['database']->modifyGold($uid, $gold, 1);
$granted = true;
}
if (!$granted) {
$g = mysqli_prepare($link,
"UPDATE `" . TB_PREFIX . "users` SET gold = gold + ? WHERE id = ?");
if ($g) {
mysqli_stmt_bind_param($g, 'ii', $gold, $uid);
mysqli_stmt_execute($g);
mysqli_stmt_close($g);
}
}
// Record the redemption.
$ins = mysqli_prepare($link,
"INSERT INTO `" . TB_PREFIX . "gold_promo_redeem` (promo_id, uid, gold, time)
VALUES (?,?,?,?)");
if ($ins) {
mysqli_stmt_bind_param($ins, 'iiii', $promoId, $uid, $gold, $now);
mysqli_stmt_execute($ins);
mysqli_stmt_close($ins);
}
return [true, 'Success! ' . $gold . ' gold has been added to your account.', $gold];
}
}
+212
View File
@@ -0,0 +1,212 @@
<?php
#################################################################################
## -= YOU MAY NOT REMOVE OR CHANGE THIS NOTICE =- ##
## --------------------------------------------------------------------------- ##
## Filename : Heatmap.php ##
## Type : World-map heatmap aggregation (Admin tool) ##
## --------------------------------------------------------------------------- ##
## Developed by : Shadow ##
## Project : TravianZ ##
## GitHub : https://github.com/Shadowss/TravianZ ##
## --------------------------------------------------------------------------- ##
## License : TravianZ Project ##
## Copyright : TravianZ (c) 2010-2026. All rights reserved. ##
## --------------------------------------------------------------------------- ##
#################################################################################
/**
* Heatmap
* -------------------------------------------------------------------------
* Aggregates the world map into a coarse grid for the admin heatmap: village
* density, per-tribe density, inactive-player density, and incoming-attack
* density. Read-only — no schema, purely derived from existing tables
* (vdata + wdata + users + movement). Helps with spawn / starting-balance
* decisions and spotting attack hotspots or dead zones.
*/
class Heatmap
{
const DEFAULT_RES = 40; // grid is RES x RES cells
const MIN_RES = 10;
const MAX_RES = 90;
const DEFAULT_INACTIVE_DAYS = 14;
/** Tribe id -> display name (for legend / tooltips). */
public static function tribeNames()
{
return [
1 => 'Romans', 2 => 'Teutons', 3 => 'Gauls',
6 => 'Huns', 7 => 'Egyptians', 8 => 'Spartans', 9 => 'Vikings',
];
}
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;
}
/** Map radius: coordinates run -MAX .. +MAX on both axes. */
private static function worldMax()
{
if (defined('WORLD_MAX') && (int) WORLD_MAX > 0) {
return (int) WORLD_MAX;
}
return 200; // sane fallback
}
/**
* Build the heatmap grid.
*
* @param array $opts 'res' (cells per side), 'inactive_days'.
* @return array {
* res, world_max, cell_span, inactive_days,
* cells => list of non-empty cells (see below),
* max => ['villages'=>, 'inactive'=>, 'attacks'=>, 'pop'=>],
* tribe_totals => [tribe => count],
* totals => ['villages'=>, 'inactive'=>, 'attacks'=>, 'players'=>],
* }
*
* Each cell:
* cx, cy grid indices (0..res-1, cy from top)
* x0,y0,x1,y1 world-coordinate bounds of the cell
* villages, inactive, attacks, pop
* tribes => [tribe => count]
* dom => dominant tribe id (0 if none)
*/
public static function grid(array $opts = [])
{
$res = isset($opts['res']) ? (int) $opts['res'] : self::DEFAULT_RES;
$res = max(self::MIN_RES, min(self::MAX_RES, $res));
$days = isset($opts['inactive_days']) ? max(1, (int) $opts['inactive_days']) : self::DEFAULT_INACTIVE_DAYS;
$max = self::worldMax();
$span = 2 * $max + 1; // total coordinate width
$cellSpan = $span / $res; // coords per cell
$empty = [
'res' => $res, 'world_max' => $max, 'cell_span' => $cellSpan,
'inactive_days' => $days, 'cells' => [],
'max' => ['villages' => 0, 'inactive' => 0, 'attacks' => 0, 'pop' => 0],
'tribe_totals' => [], 'totals' => ['villages' => 0, 'inactive' => 0, 'attacks' => 0, 'players' => 0],
];
$link = self::link();
if (!$link) {
return $empty;
}
// cellIndex: map a coordinate to 0..res-1.
$idx = function ($coord) use ($max, $span, $res) {
$i = (int) floor(($coord + $max) / $span * $res);
if ($i < 0) { $i = 0; }
if ($i >= $res) { $i = $res - 1; }
return $i;
};
$cells = []; // "cx:cy" => cell
$tribeTotals = [];
$totVil = $totInact = $totAtk = 0;
$players = [];
$touch = function ($cx, $cy) use (&$cells, $max, $cellSpan, $res) {
$key = $cx . ':' . $cy;
if (!isset($cells[$key])) {
$x0 = -$max + $cx * $cellSpan;
// cy is measured from the TOP (north = +y), so invert for y bounds.
$y1 = $max - $cy * $cellSpan;
$cells[$key] = [
'cx' => $cx, 'cy' => $cy,
'x0' => (int) round($x0), 'x1' => (int) round($x0 + $cellSpan),
'y0' => (int) round($y1 - $cellSpan), 'y1' => (int) round($y1),
'villages' => 0, 'inactive' => 0, 'attacks' => 0, 'pop' => 0,
'tribes' => [],
];
}
return $key;
};
// ---- Villages + tribe + inactivity ----
$now = time();
$cut = $now - $days * 86400;
$q = "SELECT w.x AS x, w.y AS y, u.tribe AS tribe, v.pop AS pop, u.timestamp AS ts, u.id AS uid
FROM `" . TB_PREFIX . "vdata` v
JOIN `" . TB_PREFIX . "wdata` w ON v.wref = w.id
JOIN `" . TB_PREFIX . "users` u ON v.owner = u.id
WHERE v.owner > 3 AND w.x IS NOT NULL AND w.y IS NOT NULL";
if ($r = @mysqli_query($link, $q)) {
while ($row = mysqli_fetch_assoc($r)) {
$cx = $idx((int) $row['x']);
// North is +y at the top of the map, so top row (cy=0) = +max.
$cy = $idx(-(int) $row['y']);
$key = $touch($cx, $cy);
$cells[$key]['villages']++;
$cells[$key]['pop'] += (int) $row['pop'];
$tribe = (int) $row['tribe'];
if ($tribe > 0) {
if (!isset($cells[$key]['tribes'][$tribe])) { $cells[$key]['tribes'][$tribe] = 0; }
$cells[$key]['tribes'][$tribe]++;
if (!isset($tribeTotals[$tribe])) { $tribeTotals[$tribe] = 0; }
$tribeTotals[$tribe]++;
}
if ((int) $row['ts'] > 0 && (int) $row['ts'] < $cut) {
$cells[$key]['inactive']++;
$totInact++;
}
$totVil++;
$players[(int) $row['uid']] = true;
}
mysqli_free_result($r);
}
// ---- Incoming attacks in flight (sort_type 3 = attack, 4 = raid) ----
$q2 = "SELECT w.x AS x, w.y AS y, COUNT(*) AS c
FROM `" . TB_PREFIX . "movement` m
JOIN `" . TB_PREFIX . "wdata` w ON m.`to` = w.id
WHERE m.sort_type IN (3,4) AND m.proc = 0
GROUP BY w.x, w.y";
if ($r = @mysqli_query($link, $q2)) {
while ($row = mysqli_fetch_assoc($r)) {
$cx = $idx((int) $row['x']);
$cy = $idx(-(int) $row['y']);
$key = $touch($cx, $cy);
$cells[$key]['attacks'] += (int) $row['c'];
$totAtk += (int) $row['c'];
}
mysqli_free_result($r);
}
// ---- Finalise: dominant tribe + maxima ----
$maxV = $maxI = $maxA = $maxP = 0;
$list = [];
foreach ($cells as $c) {
$dom = 0; $domCount = 0;
foreach ($c['tribes'] as $t => $n) {
if ($n > $domCount) { $domCount = $n; $dom = $t; }
}
$c['dom'] = $dom;
if ($c['villages'] > $maxV) { $maxV = $c['villages']; }
if ($c['inactive'] > $maxI) { $maxI = $c['inactive']; }
if ($c['attacks'] > $maxA) { $maxA = $c['attacks']; }
if ($c['pop'] > $maxP) { $maxP = $c['pop']; }
$list[] = $c;
}
return [
'res' => $res, 'world_max' => $max, 'cell_span' => $cellSpan,
'inactive_days' => $days,
'cells' => $list,
'max' => ['villages' => $maxV, 'inactive' => $maxI, 'attacks' => $maxA, 'pop' => $maxP],
'tribe_totals' => $tribeTotals,
'totals' => [
'villages' => $totVil, 'inactive' => $totInact,
'attacks' => $totAtk, 'players' => count($players),
],
];
}
}
+1 -1
View File
@@ -6,7 +6,7 @@
## Filename : HeroAdventure.php ##
## Type : T4 Hero port - adventures backend (Phase 3) ##
## --------------------------------------------------------------------------- ##
## Refactored by : Shadow ##
## Developed by : Shadow ##
## Project : TravianZ ##
## GitHub : https://github.com/Shadowss/TravianZ ##
## --------------------------------------------------------------------------- ##
+1 -1
View File
@@ -6,7 +6,7 @@
## Filename : HeroAuction.php ##
## Type : T4 Hero port - auction house backend (Phase 4) ##
## --------------------------------------------------------------------------- ##
## Refactored by : Shadow ##
## Developed by : Shadow ##
## Project : TravianZ ##
## GitHub : https://github.com/Shadowss/TravianZ ##
## --------------------------------------------------------------------------- ##
+1 -1
View File
@@ -6,7 +6,7 @@
## Filename : HeroBattleBonus.php ##
## Type : T4 Hero port - battle & speed integration (Phase 5) ##
## --------------------------------------------------------------------------- ##
## Refactored by : Shadow ##
## Developed by : Shadow ##
## Project : TravianZ ##
## GitHub : https://github.com/Shadowss/TravianZ ##
## --------------------------------------------------------------------------- ##
+9 -9
View File
@@ -6,7 +6,7 @@
## Filename : HeroItems.php ##
## Type : T4 Hero port - items backend (Phase 2) ##
## --------------------------------------------------------------------------- ##
## Refactored by : Shadow ##
## Developed by : Shadow ##
## Project : TravianZ ##
## GitHub : https://github.com/Shadowss/TravianZ ##
## --------------------------------------------------------------------------- ##
@@ -15,14 +15,14 @@
## --------------------------------------------------------------------------- ##
#################################################################################
## ##
## Backend for hero inventory / equipment / consumables / silver. ##
## - All statements are prepared (mysqli), all ids cast to int. ##
## - Per-request memoization caches (invalidated on every write), matching ##
## the Database.php cache convention. ##
## - Consumables whose game mechanic lands in a later phase (bandages in ##
## battle, cages in oasis attacks, law tablets in loyalty, artwork CP) ##
## return HeroItems::USE_DEFERRED so the caller knows the item exists ##
## but its effect is wired elsewhere. They are NOT silently consumed. ##
## Backend for hero inventory / equipment / consumables / silver. ##
## - All statements are prepared (mysqli), all ids cast to int. ##
## - Per-request memoization caches (invalidated on every write), matching ##
## the Database.php cache convention. ##
## - Consumables whose game mechanic lands in a later phase (bandages in ##
## battle, cages in oasis attacks, law tablets in loyalty, artwork CP) ##
## return HeroItems::USE_DEFERRED so the caller knows the item exists ##
## but its effect is wired elsewhere. They are NOT silently consumed. ##
## ##
#################################################################################
+1 -1
View File
@@ -5,7 +5,7 @@
## Filename : PushProtection.php ##
## Type : Push-Protection engine (Admin/Multihunter dashboard) ##
## --------------------------------------------------------------------------- ##
## Developed by : Shadow (Cătălin) ##
## Developed by : Shadow ##
## Project : TravianZ ##
## GitHub : https://github.com/Shadowss/TravianZ ##
## --------------------------------------------------------------------------- ##
+408
View File
@@ -0,0 +1,408 @@
<?php
#################################################################################
## -= YOU MAY NOT REMOVE OR CHANGE THIS NOTICE =- ##
## --------------------------------------------------------------------------- ##
## Filename : QuestConfig.php ##
## Type : Editable quest rewards (Admin quest editor) ##
## --------------------------------------------------------------------------- ##
## 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. ##
## --------------------------------------------------------------------------- ##
#################################################################################
/**
* QuestConfig
* -------------------------------------------------------------------------
* Makes the (previously hardcoded) quest rewards editable from the admin panel.
*
* The quest system ships in two variants — extended (Templates/Ajax/quest_core.tpl)
* and standard (quest_core25.tpl) — selected per player by $_SESSION['qtyp']==37.
* The two have different reward values and quest sets, so config is keyed by
* (variant, quest_id).
*
* This class is the SINGLE SOURCE OF TRUTH for what a completed quest grants:
* quest_core*.tpl calls QuestConfig::grantReward() instead of the old hardcoded
* modifyResource()/gold/plus lines (see "wiring" note in the admin editor). The
* table is seeded on first use with the exact current values, so behaviour is
* unchanged until an admin edits something.
*
* Self-contained (static, resolves DB link from globals, self-creates + seeds
* its table).
*/
class QuestConfig
{
const V_EXTENDED = 'extended';
const V_STANDARD = 'standard';
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;
}
/** Which quest variant is active for the current player/session. */
public static function activeVariant()
{
return (isset($_SESSION['qtyp']) && (int) $_SESSION['qtyp'] === 37)
? self::V_EXTENDED : self::V_STANDARD;
}
public static function ensureSchema()
{
$link = self::link();
if (!$link) {
return;
}
@mysqli_query($link, "CREATE TABLE IF NOT EXISTS `" . TB_PREFIX . "quest_config` (
`variant` varchar(16) NOT NULL DEFAULT 'standard',
`quest_id` int(11) NOT NULL,
`enabled` tinyint(1) NOT NULL DEFAULT 1,
`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,
`gold` int(11) NOT NULL DEFAULT 0,
`plus_days` float NOT NULL DEFAULT 0,
`req_level` int(11) NOT NULL DEFAULT 0,
`note` varchar(255) NOT NULL DEFAULT '',
PRIMARY KEY (`variant`,`quest_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8");
}
/** Seed the table with the current (extracted) hardcoded values, once. */
public static function seedDefaults()
{
$link = self::link();
if (!$link) {
return;
}
self::ensureSchema();
// Only seed when empty, so admin edits are never overwritten.
$r = @mysqli_query($link, "SELECT 1 FROM `" . TB_PREFIX . "quest_config` LIMIT 1");
if ($r && mysqli_num_rows($r) > 0) {
mysqli_free_result($r);
return;
}
if ($r) { mysqli_free_result($r); }
$stmt = mysqli_prepare($link,
"INSERT IGNORE INTO `" . TB_PREFIX . "quest_config`
(variant, quest_id, enabled, wood, clay, iron, crop, gold, plus_days, req_level, note)
VALUES (?,?,1,?,?,?,?,?,?,0,'')");
if (!$stmt) {
return;
}
foreach (self::defaults() as $variant => $quests) {
foreach ($quests as $qid => $v) {
$qid = (int) $qid;
$w = (int) $v['wood']; $c = (int) $v['clay']; $i = (int) $v['iron'];
$cr = (int) $v['crop']; $g = (int) $v['gold']; $pd = (float) $v['plus_days'];
mysqli_stmt_bind_param($stmt, 'siiiiiid', $variant, $qid, $w, $c, $i, $cr, $g, $pd);
mysqli_stmt_execute($stmt);
}
}
mysqli_stmt_close($stmt);
}
/* ---- Reads --------------------------------------------------------- */
/** All quest rows for a variant (seeds first). */
public static function all($variant)
{
$variant = self::normVariant($variant);
$link = self::link();
$out = [];
if (!$link) {
return $out;
}
self::seedDefaults();
$stmt = mysqli_prepare($link,
"SELECT * FROM `" . TB_PREFIX . "quest_config` WHERE variant = ? ORDER BY quest_id ASC");
if (!$stmt) {
return $out;
}
mysqli_stmt_bind_param($stmt, 's', $variant);
mysqli_stmt_execute($stmt);
$res = mysqli_stmt_get_result($stmt);
while ($res && $row = mysqli_fetch_assoc($res)) {
$out[(int) $row['quest_id']] = $row;
}
mysqli_stmt_close($stmt);
return $out;
}
/** One quest's config, falling back to the hardcoded default if absent. */
public static function get($variant, $qid)
{
$variant = self::normVariant($variant);
$qid = (int) $qid;
$link = self::link();
if ($link) {
self::ensureSchema();
$stmt = mysqli_prepare($link,
"SELECT * FROM `" . TB_PREFIX . "quest_config` WHERE variant = ? AND quest_id = ? LIMIT 1");
if ($stmt) {
mysqli_stmt_bind_param($stmt, 'si', $variant, $qid);
mysqli_stmt_execute($stmt);
$res = mysqli_stmt_get_result($stmt);
$row = $res ? mysqli_fetch_assoc($res) : null;
mysqli_stmt_close($stmt);
if ($row) {
return $row;
}
}
}
// Fallback to compiled-in defaults.
$d = self::defaults();
if (isset($d[$variant][$qid])) {
$v = $d[$variant][$qid];
return [
'variant' => $variant, 'quest_id' => $qid, 'enabled' => 1,
'wood' => $v['wood'], 'clay' => $v['clay'], 'iron' => $v['iron'],
'crop' => $v['crop'], 'gold' => $v['gold'], 'plus_days' => $v['plus_days'],
'req_level' => 0, 'note' => '',
];
}
return null;
}
/* ---- Write --------------------------------------------------------- */
public static function setQuest($variant, $qid, array $fields)
{
$variant = self::normVariant($variant);
$link = self::link();
if (!$link) {
return false;
}
self::ensureSchema();
$qid = (int) $qid;
$enabled = !empty($fields['enabled']) ? 1 : 0;
$wood = (int) ($fields['wood'] ?? 0);
$clay = (int) ($fields['clay'] ?? 0);
$iron = (int) ($fields['iron'] ?? 0);
$crop = (int) ($fields['crop'] ?? 0);
$gold = (int) ($fields['gold'] ?? 0);
$plus = (float) ($fields['plus_days'] ?? 0);
$req = (int) ($fields['req_level'] ?? 0);
$note = substr((string) ($fields['note'] ?? ''), 0, 255);
$stmt = mysqli_prepare($link,
"INSERT INTO `" . TB_PREFIX . "quest_config`
(variant, quest_id, enabled, wood, clay, iron, crop, gold, plus_days, req_level, note)
VALUES (?,?,?,?,?,?,?,?,?,?,?)
ON DUPLICATE KEY UPDATE enabled=VALUES(enabled), wood=VALUES(wood), clay=VALUES(clay),
iron=VALUES(iron), crop=VALUES(crop), gold=VALUES(gold), plus_days=VALUES(plus_days),
req_level=VALUES(req_level), note=VALUES(note)");
if (!$stmt) {
return false;
}
mysqli_stmt_bind_param($stmt, 'siiiiiiidis',
$variant, $qid, $enabled, $wood, $clay, $iron, $crop, $gold, $plus, $req, $note);
$ok = mysqli_stmt_execute($stmt);
mysqli_stmt_close($stmt);
return $ok;
}
/** Reset a whole variant back to compiled-in defaults. */
public static function resetVariant($variant)
{
$variant = self::normVariant($variant);
$link = self::link();
if (!$link) {
return false;
}
self::ensureSchema();
$stmt = mysqli_prepare($link,
"DELETE FROM `" . TB_PREFIX . "quest_config` WHERE variant = ?");
if ($stmt) {
mysqli_stmt_bind_param($stmt, 's', $variant);
mysqli_stmt_execute($stmt);
mysqli_stmt_close($stmt);
}
self::seedDefaults();
return true;
}
/* ---- Reward granting (called from quest_core*.tpl) ----------------- */
/**
* Grant a quest's reward from config. Replaces the old hardcoded
* modifyResource()/gold/plus lines in the quest switch. Special side
* effects (messages, FinishWoodcutter, etc.) stay in the template.
*
* @param object $database
* @param object $session
* @param int $qid
* @param string|null $variant Auto-detected from session if null.
*/
public static function grantReward($database, $session, $qid, $variant = null)
{
try {
$variant = $variant ? self::normVariant($variant) : self::activeVariant();
$cfg = self::get($variant, $qid);
if (!$cfg) {
return;
}
$vil = (isset($session->villages) && isset($session->villages[0])) ? $session->villages[0] : 0;
$uid = 0;
if (isset($session->uid)) {
$uid = (int) $session->uid;
} elseif (isset($session->userinfo) && isset($session->userinfo['id'])) {
$uid = (int) $session->userinfo['id'];
}
$w = (int) $cfg['wood']; $c = (int) $cfg['clay'];
$i = (int) $cfg['iron']; $cr = (int) $cfg['crop'];
if (($w || $c || $i || $cr) && $vil && method_exists($database, 'modifyResource')) {
$database->modifyResource($vil, $w, $c, $i, $cr, 1);
}
$gold = (int) $cfg['gold'];
if ($gold > 0 && $uid > 0 && method_exists($database, 'modifyGold')) {
$database->modifyGold($uid, $gold, 1);
}
$plusDays = (float) $cfg['plus_days'];
if ($plusDays > 0 && $uid > 0) {
$sec = (int) round($plusDays * 86400);
$now = time();
$link = self::link();
if ($link) {
@mysqli_query($link, "UPDATE `" . TB_PREFIX . "users`
SET plus = IF(plus > $now, plus + $sec, $now + $sec) WHERE id = $uid");
}
}
} catch (\Throwable $e) {
// never break the quest flow
}
}
/* ---- Helpers ------------------------------------------------------- */
public static function normVariant($v)
{
return ($v === self::V_EXTENDED) ? self::V_EXTENDED : self::V_STANDARD;
}
/**
* Quests that keep their original hardcoded logic in quest_core*.tpl and are
* therefore NOT affected by edits here (conditional rewards, atomic-claim
* milestones, special mechanics). Shown as read-only in the editor.
* 2 = FinishWoodcutter (completes a building, not a resource grant)
* 9 = conditional crop cost + spawned attack
* 26 = (standard only) two-branch reward depending on a session flag
* 91 = atomic-claim milestone (gold + Plus, issue #129)
* 97 = atomic-claim milestone (gold + Plus, issue #129)
*/
public static function nativeQuests($variant)
{
$variant = self::normVariant($variant);
return ($variant === self::V_STANDARD)
? [2, 9, 26, 91, 97]
: [2, 9, 91, 97];
}
/**
* Compiled-in default reward values, extracted from the shipped quest
* templates (extended = quest_core.tpl, standard = quest_core25.tpl).
* Used to seed the table and as a fallback.
*/
public static function defaults()
{
return [
'extended' => [
2 => ['wood'=>0,'clay'=>0,'iron'=>0,'crop'=>0,'gold'=>0,'plus_days'=>0],
3 => ['wood'=>0,'clay'=>0,'iron'=>0,'crop'=>0,'gold'=>0,'plus_days'=>1],
4 => ['wood'=>30,'clay'=>60,'iron'=>30,'crop'=>20,'gold'=>0,'plus_days'=>0],
5 => ['wood'=>40,'clay'=>30,'iron'=>20,'crop'=>30,'gold'=>0,'plus_days'=>0],
6 => ['wood'=>50,'clay'=>60,'iron'=>30,'crop'=>30,'gold'=>0,'plus_days'=>0],
7 => ['wood'=>0,'clay'=>0,'iron'=>0,'crop'=>0,'gold'=>20,'plus_days'=>0],
8 => ['wood'=>75,'clay'=>80,'iron'=>30,'crop'=>50,'gold'=>0,'plus_days'=>0],
9 => ['wood'=>0,'clay'=>0,'iron'=>0,'crop'=>-200,'gold'=>0,'plus_days'=>0],
10 => ['wood'=>75,'clay'=>90,'iron'=>30,'crop'=>50,'gold'=>0,'plus_days'=>0],
11 => ['wood'=>0,'clay'=>0,'iron'=>0,'crop'=>0,'gold'=>0,'plus_days'=>2],
12 => ['wood'=>60,'clay'=>30,'iron'=>40,'crop'=>90,'gold'=>0,'plus_days'=>0],
13 => ['wood'=>150,'clay'=>180,'iron'=>30,'crop'=>130,'gold'=>0,'plus_days'=>0],
14 => ['wood'=>60,'clay'=>50,'iron'=>40,'crop'=>30,'gold'=>0,'plus_days'=>0],
15 => ['wood'=>50,'clay'=>30,'iron'=>60,'crop'=>20,'gold'=>0,'plus_days'=>0],
16 => ['wood'=>75,'clay'=>75,'iron'=>40,'crop'=>40,'gold'=>0,'plus_days'=>0],
17 => ['wood'=>100,'clay'=>90,'iron'=>100,'crop'=>60,'gold'=>0,'plus_days'=>0],
18 => ['wood'=>0,'clay'=>0,'iron'=>0,'crop'=>0,'gold'=>0,'plus_days'=>0],
19 => ['wood'=>80,'clay'=>90,'iron'=>60,'crop'=>40,'gold'=>0,'plus_days'=>0],
20 => ['wood'=>70,'clay'=>120,'iron'=>90,'crop'=>50,'gold'=>0,'plus_days'=>0],
21 => ['wood'=>0,'clay'=>0,'iron'=>0,'crop'=>0,'gold'=>0,'plus_days'=>0],
22 => ['wood'=>200,'clay'=>200,'iron'=>700,'crop'=>450,'gold'=>0,'plus_days'=>0],
23 => ['wood'=>0,'clay'=>0,'iron'=>0,'crop'=>0,'gold'=>0,'plus_days'=>0],
24 => ['wood'=>300,'clay'=>320,'iron'=>360,'crop'=>570,'gold'=>0,'plus_days'=>0],
28 => ['wood'=>0,'clay'=>0,'iron'=>0,'crop'=>0,'gold'=>15,'plus_days'=>0],
29 => ['wood'=>240,'clay'=>280,'iron'=>180,'crop'=>100,'gold'=>0,'plus_days'=>0],
30 => ['wood'=>600,'clay'=>750,'iron'=>600,'crop'=>300,'gold'=>0,'plus_days'=>0],
31 => ['wood'=>900,'clay'=>850,'iron'=>600,'crop'=>300,'gold'=>0,'plus_days'=>0],
32 => ['wood'=>1800,'clay'=>2000,'iron'=>1650,'crop'=>800,'gold'=>0,'plus_days'=>0],
33 => ['wood'=>1600,'clay'=>1800,'iron'=>1950,'crop'=>1200,'gold'=>0,'plus_days'=>0],
34 => ['wood'=>3400,'clay'=>2800,'iron'=>3600,'crop'=>2200,'gold'=>0,'plus_days'=>0],
35 => ['wood'=>1050,'clay'=>800,'iron'=>900,'crop'=>750,'gold'=>0,'plus_days'=>0],
36 => ['wood'=>1600,'clay'=>2000,'iron'=>1800,'crop'=>1300,'gold'=>0,'plus_days'=>0],
37 => ['wood'=>0,'clay'=>0,'iron'=>0,'crop'=>0,'gold'=>0,'plus_days'=>0],
91 => ['wood'=>0,'clay'=>0,'iron'=>0,'crop'=>0,'gold'=>15,'plus_days'=>1],
92 => ['wood'=>217,'clay'=>247,'iron'=>177,'crop'=>207,'gold'=>0,'plus_days'=>0],
93 => ['wood'=>217,'clay'=>247,'iron'=>177,'crop'=>207,'gold'=>0,'plus_days'=>0],
94 => ['wood'=>217,'clay'=>247,'iron'=>177,'crop'=>207,'gold'=>0,'plus_days'=>0],
95 => ['wood'=>217,'clay'=>247,'iron'=>177,'crop'=>207,'gold'=>0,'plus_days'=>0],
96 => ['wood'=>217,'clay'=>247,'iron'=>177,'crop'=>207,'gold'=>0,'plus_days'=>0],
97 => ['wood'=>0,'clay'=>0,'iron'=>0,'crop'=>0,'gold'=>20,'plus_days'=>2],
],
'standard' => [
2 => ['wood'=>0,'clay'=>0,'iron'=>0,'crop'=>0,'gold'=>0,'plus_days'=>0],
3 => ['wood'=>0,'clay'=>0,'iron'=>0,'crop'=>0,'gold'=>0,'plus_days'=>1],
4 => ['wood'=>30,'clay'=>60,'iron'=>30,'crop'=>20,'gold'=>0,'plus_days'=>0],
5 => ['wood'=>40,'clay'=>30,'iron'=>20,'crop'=>30,'gold'=>0,'plus_days'=>0],
6 => ['wood'=>50,'clay'=>60,'iron'=>30,'crop'=>30,'gold'=>0,'plus_days'=>0],
7 => ['wood'=>0,'clay'=>0,'iron'=>0,'crop'=>0,'gold'=>20,'plus_days'=>0],
8 => ['wood'=>60,'clay'=>30,'iron'=>40,'crop'=>90,'gold'=>0,'plus_days'=>0],
9 => ['wood'=>0,'clay'=>0,'iron'=>0,'crop'=>-200,'gold'=>0,'plus_days'=>0],
10 => ['wood'=>100,'clay'=>80,'iron'=>40,'crop'=>40,'gold'=>0,'plus_days'=>0],
11 => ['wood'=>0,'clay'=>0,'iron'=>0,'crop'=>0,'gold'=>0,'plus_days'=>2],
12 => ['wood'=>75,'clay'=>140,'iron'=>40,'crop'=>40,'gold'=>0,'plus_days'=>0],
13 => ['wood'=>75,'clay'=>80,'iron'=>30,'crop'=>50,'gold'=>0,'plus_days'=>0],
14 => ['wood'=>120,'clay'=>200,'iron'=>140,'crop'=>100,'gold'=>0,'plus_days'=>0],
15 => ['wood'=>150,'clay'=>180,'iron'=>30,'crop'=>130,'gold'=>0,'plus_days'=>0],
16 => ['wood'=>60,'clay'=>50,'iron'=>40,'crop'=>30,'gold'=>0,'plus_days'=>0],
17 => ['wood'=>50,'clay'=>30,'iron'=>60,'crop'=>20,'gold'=>0,'plus_days'=>0],
18 => ['wood'=>75,'clay'=>75,'iron'=>40,'crop'=>40,'gold'=>0,'plus_days'=>0],
19 => ['wood'=>100,'clay'=>90,'iron'=>100,'crop'=>60,'gold'=>0,'plus_days'=>0],
20 => ['wood'=>0,'clay'=>0,'iron'=>0,'crop'=>0,'gold'=>0,'plus_days'=>0],
21 => ['wood'=>80,'clay'=>90,'iron'=>60,'crop'=>40,'gold'=>0,'plus_days'=>0],
22 => ['wood'=>70,'clay'=>120,'iron'=>90,'crop'=>50,'gold'=>0,'plus_days'=>0],
23 => ['wood'=>80,'clay'=>90,'iron'=>60,'crop'=>40,'gold'=>0,'plus_days'=>0],
24 => ['wood'=>80,'clay'=>90,'iron'=>60,'crop'=>40,'gold'=>0,'plus_days'=>0],
25 => ['wood'=>70,'clay'=>100,'iron'=>90,'crop'=>100,'gold'=>0,'plus_days'=>0],
26 => ['wood'=>200,'clay'=>200,'iron'=>700,'crop'=>250,'gold'=>0,'plus_days'=>0],
27 => ['wood'=>0,'clay'=>0,'iron'=>0,'crop'=>0,'gold'=>15,'plus_days'=>0],
28 => ['wood'=>80,'clay'=>70,'iron'=>60,'crop'=>40,'gold'=>0,'plus_days'=>0],
29 => ['wood'=>100,'clay'=>60,'iron'=>90,'crop'=>40,'gold'=>0,'plus_days'=>0],
30 => ['wood'=>100,'clay'=>140,'iron'=>90,'crop'=>50,'gold'=>0,'plus_days'=>0],
91 => ['wood'=>0,'clay'=>0,'iron'=>0,'crop'=>0,'gold'=>15,'plus_days'=>1],
92 => ['wood'=>217,'clay'=>247,'iron'=>177,'crop'=>207,'gold'=>0,'plus_days'=>0],
93 => ['wood'=>217,'clay'=>247,'iron'=>177,'crop'=>207,'gold'=>0,'plus_days'=>0],
94 => ['wood'=>217,'clay'=>247,'iron'=>177,'crop'=>207,'gold'=>0,'plus_days'=>0],
95 => ['wood'=>217,'clay'=>247,'iron'=>177,'crop'=>207,'gold'=>0,'plus_days'=>0],
96 => ['wood'=>217,'clay'=>247,'iron'=>177,'crop'=>207,'gold'=>0,'plus_days'=>0],
97 => ['wood'=>0,'clay'=>0,'iron'=>0,'crop'=>0,'gold'=>20,'plus_days'=>2],
],
];
}
}
+226
View File
@@ -0,0 +1,226 @@
<?php
#################################################################################
## -= YOU MAY NOT REMOVE OR CHANGE THIS NOTICE =- ##
## --------------------------------------------------------------------------- ##
## Filename : RegBlock.php ##
## Type : Registration blocklist engine ##
## --------------------------------------------------------------------------- ##
## Developed by : Shadow ##
## Project : TravianZ ##
## GitHub : https://github.com/Shadowss/TravianZ ##
## --------------------------------------------------------------------------- ##
## License : TravianZ Project ##
## Copyright : TravianZ (c) 2010-2026. All rights reserved. ##
## --------------------------------------------------------------------------- ##
#################################################################################
/**
* RegBlock
* -------------------------------------------------------------------------
* Blocklist for registration: reject specific usernames, specific e-mail
* addresses, or whole e-mail domains (e.g. an obscene username, or the whole
* "yahoo.com" domain). Managed from the admin panel (blockReg), enforced in
* Account::Signup().
*
* Match rules (all case-insensitive, exact — no accidental substring blocks):
* - username : exact username match
* - email : exact full-address match
* - domain : the part after "@" of the candidate e-mail
*
* Self-contained (static, resolves DB link from globals, self-creates its
* table) so both the registration flow and the admin panel can use it.
*/
class RegBlock
{
const T_USERNAME = 'username';
const T_EMAIL = 'email';
const T_DOMAIN = 'domain';
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 reg_block table if missing. */
public static function ensureSchema()
{
$link = self::link();
if (!$link) {
return;
}
@mysqli_query($link, "CREATE TABLE IF NOT EXISTS `" . TB_PREFIX . "reg_block` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`type` varchar(16) NOT NULL DEFAULT 'username',
`value` varchar(255) NOT NULL DEFAULT '',
`note` varchar(255) NOT NULL DEFAULT '',
`added_by` int(11) NOT NULL DEFAULT 0,
`time` int(11) NOT NULL DEFAULT 0,
PRIMARY KEY (`id`),
UNIQUE KEY `type_value` (`type`,`value`),
KEY `type` (`type`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8");
}
/** Normalise a value: trim + lowercase. */
private static function norm($v)
{
return strtolower(trim((string) $v));
}
/** Valid type check. */
private static function validType($t)
{
return in_array($t, [self::T_USERNAME, self::T_EMAIL, self::T_DOMAIN], true);
}
/* ---- Enforcement (called from registration) ------------------------ */
/** True if this username is blocked. */
public static function isBlockedName($database, $name)
{
return self::matches(self::T_USERNAME, self::norm($name));
}
/** True if this e-mail address OR its domain is blocked. */
public static function isBlockedEmail($database, $email)
{
$email = self::norm($email);
if ($email === '') {
return false;
}
if (self::matches(self::T_EMAIL, $email)) {
return true;
}
$at = strrpos($email, '@');
if ($at !== false) {
$domain = substr($email, $at + 1);
if ($domain !== '' && self::matches(self::T_DOMAIN, $domain)) {
return true;
}
}
return false;
}
/** Low-level exact lookup. */
private static function matches($type, $value)
{
$link = self::link();
if (!$link || $value === '') {
return false;
}
self::ensureSchema();
$stmt = mysqli_prepare($link,
"SELECT 1 FROM `" . TB_PREFIX . "reg_block` WHERE `type` = ? AND `value` = ? LIMIT 1");
if (!$stmt) {
return false;
}
mysqli_stmt_bind_param($stmt, 'ss', $type, $value);
mysqli_stmt_execute($stmt);
mysqli_stmt_store_result($stmt);
$hit = mysqli_stmt_num_rows($stmt) > 0;
mysqli_stmt_close($stmt);
return $hit;
}
/* ---- Admin management ---------------------------------------------- */
/** All blocks, newest first. */
public static function all()
{
$link = self::link();
$out = [];
if (!$link) {
return $out;
}
self::ensureSchema();
$r = @mysqli_query($link,
"SELECT * FROM `" . TB_PREFIX . "reg_block` ORDER BY `time` DESC, id DESC");
if ($r) {
while ($row = mysqli_fetch_assoc($r)) {
$out[] = $row;
}
mysqli_free_result($r);
}
return $out;
}
/**
* Add a block. Returns [ok(bool), message(string)].
* Domain values may be given with or without a leading "@".
*/
public static function add($type, $value, $note, $adminId)
{
$link = self::link();
if (!$link) {
return [false, 'No database connection.'];
}
self::ensureSchema();
$type = (string) $type;
if (!self::validType($type)) {
return [false, 'Invalid block type.'];
}
$value = self::norm($value);
if ($type === self::T_DOMAIN) {
$value = ltrim($value, '@');
}
if ($value === '') {
return [false, 'Value cannot be empty.'];
}
// Light sanity checks.
if ($type === self::T_EMAIL && strpos($value, '@') === false) {
return [false, 'That does not look like a full e-mail address.'];
}
if ($type === self::T_DOMAIN && strpos($value, '.') === false) {
return [false, 'That does not look like a domain (e.g. yahoo.com).'];
}
if (strlen($value) > 255) {
return [false, 'Value too long.'];
}
$note = substr((string) $note, 0, 255);
$admin = (int) $adminId;
$now = time();
$stmt = mysqli_prepare($link,
"INSERT INTO `" . TB_PREFIX . "reg_block` (`type`,`value`,`note`,`added_by`,`time`)
VALUES (?,?,?,?,?)
ON DUPLICATE KEY UPDATE `note` = VALUES(`note`), `added_by` = VALUES(`added_by`), `time` = VALUES(`time`)");
if (!$stmt) {
return [false, 'Could not prepare statement.'];
}
mysqli_stmt_bind_param($stmt, 'sssii', $type, $value, $note, $admin, $now);
$ok = mysqli_stmt_execute($stmt);
mysqli_stmt_close($stmt);
return $ok ? [true, 'Block saved.'] : [false, 'Could not save block.'];
}
/** Remove a block by id. */
public static function remove($id)
{
$link = self::link();
if (!$link) {
return false;
}
self::ensureSchema();
$stmt = mysqli_prepare($link,
"DELETE FROM `" . TB_PREFIX . "reg_block` WHERE id = ?");
if (!$stmt) {
return false;
}
$id = (int) $id;
mysqli_stmt_bind_param($stmt, 'i', $id);
$ok = mysqli_stmt_execute($stmt);
mysqli_stmt_close($stmt);
return $ok;
}
}