Merge pull request #137 from newtcv/master

PHP 8 hardening, installer/croppers performance improvements, and admin/docs updates
This commit is contained in:
Catalin Novgorodschi
2026-03-16 10:43:15 +02:00
committed by GitHub
29 changed files with 871 additions and 418 deletions
+13 -6
View File
@@ -1,11 +1,18 @@
# MySQL Database Configuration
MYSQL_ROOT_PASSWORD=rootpassword
MYSQL_DATABASE=travian
MYSQL_USER=travianz
MYSQL_PASSWORD=travianzpass
# MariaDB Database Configuration (preferred)
MARIADB_ROOT_PASSWORD=rootpassword
MARIADB_DATABASE=travian
MARIADB_USER=travianz
MARIADB_PASSWORD=travianzpass
# Legacy compatibility keys (optional)
# These keys inherit directly from MARIADB_* to avoid duplicate values.
MYSQL_ROOT_PASSWORD=${MARIADB_ROOT_PASSWORD}
MYSQL_DATABASE=${MARIADB_DATABASE}
MYSQL_USER=${MARIADB_USER}
MYSQL_PASSWORD=${MARIADB_PASSWORD}
# Application Configuration
# These values will be used during the installation wizard
# These values will be used during the installation wizard.
# Hostname for database connection (use 'db' when running in Docker)
DB_HOST=db
DB_PORT=3306
+5 -4
View File
@@ -13,6 +13,7 @@
#################################################################################
function get_map_tile_info($coord_x, $coord_y){ // todo mv to queries file
$tbp = TB_PREFIX;
$q = 'SELECT map_data.`id` AS village_id, map_data.`fieldtype`, map_data.`oasistype`, map_data.`occupied`, map_data.`image`, '.
'oasis_data.`type`, '.
'CASE '.
@@ -21,10 +22,10 @@ function get_map_tile_info($coord_x, $coord_y){ // todo mv to queries file
'ELSE village_data.`owner` '.
'END AS owner_id, '.
'u.`username` '.
'FROM (SELECT * FROM `s1_wdata` WHERE `x` = '.$coord_x.' AND `y` = '.$coord_y.') AS map_data '.
'LEFT JOIN `s1_odata` AS oasis_data ON map_data.`id` = oasis_data.`wref` '.
'LEFT JOIN `s1_vdata` AS village_data ON village_data.`wref` = map_data.`id` '.
'LEFT JOIN s1_users AS u ON u.`id` = COALESCE(oasis_data.`owner`, village_data.`owner`, 0);'; // todo check this db tables fields -- del doublings like oasistype = '2' and image = 'o2'
'FROM (SELECT * FROM `'.$tbp.'wdata` WHERE `x` = '.$coord_x.' AND `y` = '.$coord_y.') AS map_data '.
'LEFT JOIN `'.$tbp.'odata` AS oasis_data ON map_data.`id` = oasis_data.`wref` '.
'LEFT JOIN `'.$tbp.'vdata` AS village_data ON village_data.`wref` = map_data.`id` '.
'LEFT JOIN `'.$tbp.'users` AS u ON u.`id` = COALESCE(oasis_data.`owner`, village_data.`owner`, 0);'; // todo check this db tables fields -- del doublings like oasistype = '2' and image = 'o2'
$result = mysqli_query($GLOBALS['link'], $q);
return mysqli_fetch_assoc($result);
}
+6 -2
View File
@@ -12,7 +12,11 @@ include_once("../GameEngine/Generator.php");
include_once("../GameEngine/Technology.php");
include_once("../GameEngine/Message.php");
if(isset($_GET['nid']) && is_numeric($_GET['nid'])) $msg = $database->getMessage($_GET['nid'], 3);
$msg = [];
$allMessages = [];
$nid = (isset($_GET['nid']) && is_numeric($_GET['nid'])) ? (int) $_GET['nid'] : 0;
if($nid > 0) $msg = $database->getMessage($nid, 3);
else
{
$sql = "SELECT * FROM " . TB_PREFIX . "mdata ORDER BY time DESC ";
@@ -75,5 +79,5 @@ echo stripslashes(nl2br($bbcoded));
<div id="read_foot" class="msg_foot"></div>
</div><?php
}
else echo "Message ID ".$_GET['nid']." doesn't exist!";
else echo "Message ID ".$nid." doesn't exist!";
?>
+10 -4
View File
@@ -12,7 +12,13 @@ include_once("../GameEngine/Generator.php");
include_once("../GameEngine/Technology.php");
include_once("../GameEngine/Message.php");
if ($_GET['bid']) $rep = $database->getNotice2($_GET['bid']);
$rep = null;
$rep1 = [];
$bid = isset($_GET['bid']) ? (int) $_GET['bid'] : 0;
if ($bid > 0) {
$rep = $database->getNotice2($bid);
}
else
{
$sql = "SELECT * FROM " . TB_PREFIX . "ndata ORDER BY time DESC ";
@@ -20,7 +26,7 @@ else
$rep1 = $database->mysqli_fetch_all($result);
}
if($rep1)
if(!empty($rep1))
{
?>
<link href="../<?php echo GP_LOCATE; ?>lang/en/lang.css?f4b7d" rel="stylesheet" type="text/css">
@@ -34,7 +40,7 @@ if($rep1)
</div>
<?php
}
elseif($rep)
elseif(!empty($rep))
{
?>
<link href="../<?php echo GP_LOCATE; ?>lang/en/lang.css?f4b7d" rel="stylesheet" type="text/css">
@@ -50,5 +56,5 @@ elseif($rep)
$message->readingNotice = $rep;
include ("../Templates/Notice/".$message->getReportType($rep['ntype']).".tpl");
}
else echo "Report ID ".$_GET['bid']." doesn't exist!";
else echo "Report ID ".$bid." doesn't exist!";
?>
+6 -4
View File
@@ -1,4 +1,4 @@
<?php
<?php
#################################################################################
## -= YOU MAY NOT REMOVE OR CHANGE THIS NOTICE =- ##
## --------------------------------------------------------------------------- ##
@@ -12,10 +12,12 @@
include_once("../../GameEngine/config.php");
include_once("../../GameEngine/Database.php");
if (!isset($_SESSION)) {
session_start();
if (session_status() !== PHP_SESSION_ACTIVE) {
session_start();
}
if (!isset($_SESSION['access']) || (int)$_SESSION['access'] !== (int)ADMIN) {
die("<h1><font color=\"red\">Access Denied: You are not Admin!</font></h1>");
}
if($_SESSION['access'] != ADMIN) die("<h1><font color=\"red\">Access Denied: You are not Admin!</font></h1>");
set_time_limit(0);
// TODO: truncate ALL tables (in a single query, not like this),
// then perform install steps (createDbStructure() && populateWorldData())
+98
View File
@@ -0,0 +1,98 @@
<?php
#################################################################################
## -= YOU MAY NOT REMOVE OR CHANGE THIS NOTICE =- ##
## --------------------------------------------------------------------------- ##
## Filename users.tpl ##
## Purpose: Admin users list with pagination ##
## ##
#################################################################################
$page = isset($_GET['upage']) ? (int)$_GET['upage'] : 1;
if ($page < 1) {
$page = 1;
}
$perPage = 100;
$offset = ($page - 1) * $perPage;
$totalRes = mysqli_query($GLOBALS['link'], "SELECT COUNT(*) AS cnt FROM " . TB_PREFIX . "users");
$totalRow = $totalRes ? mysqli_fetch_assoc($totalRes) : ['cnt' => 0];
$totalUsers = (int)($totalRow['cnt'] ?? 0);
$totalPages = max(1, (int)ceil($totalUsers / $perPage));
if ($page > $totalPages) {
$page = $totalPages;
$offset = ($page - 1) * $perPage;
}
$sql = "SELECT id, username, email, access, tribe, gold, timestamp " .
"FROM " . TB_PREFIX . "users " .
"ORDER BY id DESC LIMIT " . (int)$perPage . " OFFSET " . (int)$offset;
$result = mysqli_query($GLOBALS['link'], $sql);
function tribeLabel($tribe) {
$tribe = (int)$tribe;
if ($tribe === 1) return 'Roman';
if ($tribe === 2) return 'Teuton';
if ($tribe === 3) return 'Gaul';
return 'N/A';
}
?>
<table id="member" cellpadding="1" cellspacing="1">
<thead>
<tr>
<th colspan="7">Users list (<?php echo $totalUsers; ?>)</th>
</tr>
<tr>
<td>ID</td>
<td>Username</td>
<td>Email</td>
<td>Access</td>
<td>Tribe</td>
<td>Gold</td>
<td>Last activity</td>
</tr>
</thead>
<tbody>
<?php if ($result && mysqli_num_rows($result) > 0) { ?>
<?php while ($row = mysqli_fetch_assoc($result)) { ?>
<tr>
<td><?php echo (int)$row['id']; ?></td>
<td>
<a href="?p=player&amp;uid=<?php echo (int)$row['id']; ?>">
<?php echo htmlspecialchars((string)$row['username'], ENT_QUOTES, 'UTF-8'); ?>
</a>
</td>
<td>
<?php if (!empty($row['email'])) { ?>
<a href="mailto:<?php echo htmlspecialchars((string)$row['email'], ENT_QUOTES, 'UTF-8'); ?>">
<?php echo htmlspecialchars((string)$row['email'], ENT_QUOTES, 'UTF-8'); ?>
</a>
<?php } else { ?>
-
<?php } ?>
</td>
<td><?php echo (int)$row['access']; ?></td>
<td><?php echo tribeLabel($row['tribe']); ?></td>
<td><?php echo (int)$row['gold']; ?></td>
<td><?php echo !empty($row['timestamp']) ? date('d.m.Y H:i:s', (int)$row['timestamp']) : '-'; ?></td>
</tr>
<?php } ?>
<?php } else { ?>
<tr>
<td colspan="7" class="hab">No users found.</td>
</tr>
<?php } ?>
</tbody>
</table>
<div style="margin-top:10px;">
<?php if ($page > 1) { ?>
<a href="?p=users&amp;upage=<?php echo $page - 1; ?>">&laquo; Previous</a>
<?php } ?>
<span style="margin:0 10px;">Page <?php echo $page; ?> / <?php echo $totalPages; ?></span>
<?php if ($page < $totalPages) { ?>
<a href="?p=users&amp;upage=<?php echo $page + 1; ?>">Next &raquo;</a>
<?php } ?>
</div>
+5
View File
@@ -131,6 +131,10 @@ if (!empty($_GET['p'])) {
$subpage = 'Create Users';
break;
case 'users':
$subpage = 'Users List';
break;
case 'admin_log':
$subpage = 'Admin Log';
break;
@@ -559,6 +563,7 @@ if (!empty($_GET['p'])) {
</li>
<li class="sub"><a href="#">Users</a>
<ul>
<li><a href="?p=users">List Users</a></li>
<li><a href="?p=addUsers">Create Users</a></li>
</ul>
</li>
+73
View File
@@ -0,0 +1,73 @@
# Changelog
All notable changes to this project are documented in this file.
Format inspired by Keep a Changelog and Semantic Versioning principles.
## [Unreleased]
## [2026-03-14] - Stability, PHP 8, Docker and Installer Hardening
### Added
- New root changelog file.
- Admin panel option to list users in the Users menu (`?p=users`).
- New admin users list page with pagination and links to player details: `Admin/Templates/users.tpl`.
- Installer SQL defaults in config form now read from `.env` (with fallback to legacy `MYSQL_*` keys).
- Random database prefix generation per installation session.
- Installer timezone option for Brazil (`America/Sao_Paulo`) and default timezone set to Sao Paulo.
- SSE explicit error payload in croppers builder to avoid infinite reconnect loops.
### Changed
- Docker runtime upgraded to PHP 8.3 (Apache).
- Docker Compose database moved to MariaDB latest.
- Compose configuration modernized (v2 style), env naming standardized to `MARIADB_*` with compatibility support.
- README and Docker docs updated to reflect PHP 8/MariaDB and modern compose commands.
- Installer timezone id mapping fixed in `install/index.php` to match form options.
- Croppers build progress behavior adjusted to report incremental progress in batches.
### Fixed
- PHP 8 compatibility:
- Replaced deprecated `each()` usage.
- Added safe guards for `magic_quotes_*` checks.
- Multiple warnings/fatals from undefined variables and null array offsets across game/admin/installer pages.
- Duplicate constant definition warnings in language file (preventing header issues).
- Duplicate key failures for croppers index creation in PHP 8 (`mysqli_sql_exception` behavior) by making index ops idempotent.
- Installer and runtime DB connection robustness:
- host:port normalization,
- retry logic,
- throwable-safe handling.
- Duplicate primary key errors in units insertion by making `addUnits` idempotent (`ON DUPLICATE KEY UPDATE`).
- Admin template warnings in report/message pages (undefined keys/indexes).
- Admin map tile query now uses dynamic table prefix (`TB_PREFIX`) instead of hardcoded `s1_` tables.
- Session/header issues in admin reset script:
- removed output before `<?php`,
- proper session start checks,
- safe admin access check.
- Mass message and system message flows no longer trigger undefined control variable warnings.
- Plus menu warning fixed by correcting `isset($_GET['id'])` condition precedence.
- Plus page now displays active option remaining time with seconds and end time.
### Performance
- World data generation optimized for large maps:
- temporary session tuning for bulk writes,
- temporary drop/recreate of secondary indexes in `wdata` during heavy insert.
- Croppers generation optimized:
- per-tile bonus query replaced with chunk-level batch bonus calculation,
- reduced lock contention and improved perceived progress updates.
- Build/installer flow updated to avoid stalled progress UI states.
### Security and Safety
- Stricter checks around request/session variables to prevent warnings escalating into header failures.
- More defensive null-safe access patterns in DB result handling.
- Better error propagation in SSE endpoints.
### Developer Experience
- `.env.example` restored and aligned with MariaDB naming.
- Legacy `MYSQL_*` compatibility keys now inherit values from `MARIADB_*`.
- Multiple PHP syntax validation passes performed after patches.
---
## Notes
- This changelog reflects the current hardening/migration cycle and operational fixes applied to make the project run cleanly on PHP 8 + MariaDB.
- If you want strict version tags (e.g. `v8-migration.1`, `v8-migration.2`), convert the dated section above into tagged releases as part of your next release process.
+30 -31
View File
@@ -29,21 +29,21 @@ cp .env.example .env
Edit `.env` file to set your database credentials:
```env
MYSQL_ROOT_PASSWORD=yourStrongRootPassword
MYSQL_DATABASE=travian
MYSQL_USER=travianz
MYSQL_PASSWORD=yourStrongPassword
MARIADB_ROOT_PASSWORD=yourStrongRootPassword
MARIADB_DATABASE=travian
MARIADB_USER=travianz
MARIADB_PASSWORD=yourStrongPassword
```
### 3. Start the Containers
```bash
docker-compose up -d
docker compose up -d
```
This command will:
- Build the TravianZ web application container
- Start a MySQL 5.7 database container
- Start a MariaDB (latest) database container
- Start a phpMyAdmin container for database management
- Set up a network for all containers to communicate
@@ -75,35 +75,35 @@ After starting the containers, the following services will be available:
- **TravianZ Web Application:** http://localhost:8080
- **phpMyAdmin:** http://localhost:8081
- **MySQL Database:** localhost:3306 (for external connections)
- **MariaDB Database:** localhost:3306 (for external connections)
## Container Management
### View Running Containers
```bash
docker-compose ps
docker compose ps
```
### View Logs
```bash
# All containers
docker-compose logs
docker compose logs
# Specific container
docker-compose logs web
docker-compose logs db
docker-compose logs phpmyadmin
docker compose logs web
docker compose logs db
docker compose logs phpmyadmin
# Follow logs in real-time
docker-compose logs -f web
docker compose logs -f web
```
### Stop Containers
```bash
docker-compose down
docker compose down
```
### Stop and Remove All Data
@@ -111,13 +111,13 @@ docker-compose down
**WARNING:** This will delete all database data!
```bash
docker-compose down -v
docker compose down -v
```
### Restart Containers
```bash
docker-compose restart
docker compose restart
```
### Rebuild Containers
@@ -125,8 +125,8 @@ docker-compose restart
If you make changes to the Dockerfile or application code:
```bash
docker-compose down
docker-compose up -d --build
docker compose down
docker compose up -d --build
```
## Accessing the Containers
@@ -160,12 +160,12 @@ docker exec -it travianz-web chmod -R 777 /var/www/html/var
1. Make sure the database container is running:
```bash
docker-compose ps
docker compose ps
```
2. Check database logs:
```bash
docker-compose logs db
docker compose logs db
```
3. Verify the hostname is set to `db` (not `localhost` or `127.0.0.1`)
@@ -176,7 +176,7 @@ If you need to start the installation over:
1. Stop containers:
```bash
docker-compose down -v
docker compose down -v
```
2. Remove the installed flag:
@@ -187,14 +187,14 @@ If you need to start the installation over:
3. Start containers again:
```bash
docker-compose up -d
docker compose up -d
```
4. Access the installation wizard again at http://localhost:8080/install
### Port Already in Use
If port 8080 or 8081 is already in use, edit `docker-compose.yml` and change the ports:
If port 8080 or 8081 is already in use, edit `docker compose` configuration (`docker-compose.yml`) and change the ports:
```yaml
services:
@@ -211,13 +211,13 @@ services:
### Backup Database
```bash
docker exec travianz-db mysqldump -u root -p travian > backup_$(date +%Y%m%d).sql
docker exec travianz-db mariadb-dump -u root -p travian > backup_$(date +%Y%m%d).sql
```
### Restore Database
```bash
docker exec -i travianz-db mysql -u root -p travian < backup_20231125.sql
docker exec -i travianz-db mariadb -u root -p travian < backup_20231125.sql
```
### Backup Application Files
@@ -237,7 +237,7 @@ For production environments, consider the following:
2. **Use SSL/TLS:** Set up a reverse proxy (nginx/traefik) with Let's Encrypt
3. **Limit Database Access:** Remove the database port exposure in `docker-compose.yml`
3. **Limit Database Access:** Remove the database port exposure in the `docker compose` configuration (`docker-compose.yml`)
4. **Regular Backups:** Set up automated backup scripts
@@ -257,15 +257,14 @@ services:
## Performance Optimization
### MySQL Tuning
### MariaDB Tuning
Edit `docker-compose.yml` to add MySQL configuration:
Edit `docker-compose.yml` to add MariaDB configuration:
```yaml
services:
db:
command: >
--default-authentication-plugin=mysql_native_password
--sql_mode=""
--max_connections=200
--innodb_buffer_pool_size=512M
@@ -299,8 +298,8 @@ To update TravianZ to the latest version:
```bash
git pull origin main
docker-compose down
docker-compose up -d --build
docker compose down
docker compose up -d --build
```
## Support
+6 -10
View File
@@ -1,9 +1,9 @@
FROM php:7.4-apache
FROM php:8.3-apache
# Install system dependencies
RUN apt-get update && apt-get install -y \
libpng-dev \
libjpeg-dev \
libjpeg62-turbo-dev \
libfreetype6-dev \
libzip-dev \
zip \
@@ -26,18 +26,14 @@ RUN a2enmod rewrite headers
# Set working directory
WORKDIR /var/www/html
# Copy application files
COPY . /var/www/html/
# Set permissions
RUN chown -R www-data:www-data /var/www/html \
&& chmod -R 755 /var/www/html \
&& chmod -R 777 /var/www/html/var
# Runtime source code is bind-mounted by docker-compose.
# Keep only minimal folder bootstrap inside the image.
RUN mkdir -p /var/www/html/var
# Configure Apache to use /var/www/html as DocumentRoot
RUN sed -i 's!/var/www/html!/var/www/html!g' /etc/apache2/sites-available/000-default.conf
# Expose port 80
# Expose Apache port
EXPOSE 80
# Start Apache
+4 -1
View File
@@ -1192,7 +1192,7 @@ class Automation {
if($traps >= $totalTroops){
for($i = 1; $i <= 11; $i++) ${'traped'.$i} = $data['t'.$i];
}
else
else if($totalTroops > 0)
{
$multiplier = $traps / $totalTroops;
@@ -1213,6 +1213,9 @@ class Automation {
}
}
}
else {
for($i = 1; $i <= 11; $i++) ${'traped'.$i} = 0;
}
}
if(empty($scout) || $NatarCapital){
+12 -10
View File
@@ -477,6 +477,8 @@ class Battle {
$result['Attack_points'] = $rap;
$result['Defend_points'] = $rdp;
$winner = ($rap > $rdp);
$safeRap = max(1, (float) $rap);
$safeRdp = max(1, (float) $rdp);
// Formula for calculating the Morale bonus
// WW villages aren't affected by this bonus
@@ -495,7 +497,7 @@ class Battle {
// $type = 1 Scout, 2 Enforcement
// $type = 3 Normal, 4 Raid
if($type == 1){
$holder = pow((($rdp * $moralbonus) / $rap), $Mfactor);
$holder = pow((($rdp * $moralbonus) / $safeRap), $Mfactor);
if($holder > 1) $holder = 1;
if ($rdp > $rap) $holder = 1;
@@ -507,7 +509,7 @@ class Battle {
//Defender result
$result[2] = 0;
}else if($type == 4) {
$holder = ($winner) ? pow((($rdp * $moralbonus) / $rap), $Mfactor) : pow(($rap / ($rdp * $moralbonus)), $Mfactor);
$holder = ($winner) ? pow((($rdp * $moralbonus) / $safeRap), $Mfactor) : pow(($safeRap / max($safeRdp * $moralbonus, 1)), $Mfactor);
$holder = $holder / (1 + $holder);
//Attacker result
$result[1] = $winner ? $holder : 1 - $holder;
@@ -518,7 +520,7 @@ class Battle {
}else if($type == 3){
// Attacker
$result[1] = ($winner) ? pow((($rdp * $moralbonus) / $rap), $Mfactor) : 1;
$result[1] = ($winner) ? pow((($rdp * $moralbonus) / $safeRap), $Mfactor) : 1;
if ($result[1] > 1){
$result[1] = 1;
@@ -527,9 +529,9 @@ class Battle {
}
// Defender
$result[2] = (!$winner) ? pow(($rap / ($rdp * $moralbonus)), $Mfactor) : 1;
$result[2] = (!$winner) ? pow(($safeRap / max($safeRdp * $moralbonus, 1)), $Mfactor) : 1;
if ($result[1] == 1) $result[2] = pow(($rap / ($rdp * $moralbonus)), $Mfactor);
if ($result[1] == 1) $result[2] = pow(($safeRap / max($safeRdp * $moralbonus, 1)), $Mfactor);
if ($result[2] > 1) {
$result[2] = 1;
@@ -569,14 +571,14 @@ class Battle {
$catpMoraleBonus = min(max(($attpop / ($defpop > 0 ? $defpop : 1)) ** 0.3, 1), 3);
//New level of the building (only for warsim.php)
$catapultsDamage = $this->calculateCatapultsDamage($catp, $upgrades, $durability, $rap / $rdp, $strongerbuildings, $catpMoraleBonus);
$catapultsDamage = $this->calculateCatapultsDamage($catp, $upgrades, $durability, $safeRap / $safeRdp, $strongerbuildings, $catpMoraleBonus);
$result[3] = $this->calculateNewBuildingLevel($tblevel, $catapultsDamage);
$result[4] = $tblevel;
//Results for Automation.php
$result['catapults']['upgrades'] = $upgrades;
$result['catapults']['durability'] = $durability;
$result['catapults']['attackDefenseRatio'] = $rap / $rdp;
$result['catapults']['attackDefenseRatio'] = $safeRap / $safeRdp;
$result['catapults']['strongerBuildings'] = $strongerbuildings;
$result['catapults']['moraleBonus'] = $catpMoraleBonus;
}
@@ -590,19 +592,19 @@ class Battle {
$durability = ($stonemason > 0 ? $bid34[$stonemason]['attri'] / 100 : 1);
// New level of the building (only for warsim.php)
$ramsDamage = $this->calculateCatapultsDamage($ram, $upgrades, $durability, $rap / $rdp, $strongerbuildings, 1);
$ramsDamage = $this->calculateCatapultsDamage($ram, $upgrades, $durability, $safeRap / $safeRdp, $strongerbuildings, 1);
$result[7] = $this->calculateNewBuildingLevel($walllevel, $ramsDamage);
$result[8] = $walllevel;
// Results for Automation.php
$result['rams']['upgrades'] = $upgrades;
$result['rams']['durability'] = $durability;
$result['rams']['attackDefenseRatio'] = $rap / $rdp;
$result['rams']['attackDefenseRatio'] = $safeRap / $safeRdp;
$result['rams']['strongerBuildings'] = $strongerbuildings;
$result['rams']['moraleBonus'] = 1;
}
$result[6] = pow($rap / ($rdp * $moralbonus > 0 ? $rdp * $moralbonus : 1), $Mfactor);
$result[6] = pow($safeRap / max($safeRdp * $moralbonus, 1), $Mfactor);
$result['moralBonus'] = $moralbonus;
$total_att_units = count($units['Att_unit']);
+8 -4
View File
@@ -865,10 +865,14 @@ class Building {
$rclay = $uprequire['clay'] - $village->aclay;
$rcrop = $uprequire['crop'] - $village->acrop;
$riron = $uprequire['iron'] - $village->airon;
$rwtime = $rwood / $village->getProd("wood") * 3600;
$rcltime = $rclay / $village->getProd("clay") * 3600;
$rctime = $rcrop / $village->getProd("crop") * 3600;
$ritime = $riron / $village->getProd("iron") * 3600;
$woodProd = $village->getProd("wood");
$clayProd = $village->getProd("clay");
$cropProd = $village->getProd("crop");
$ironProd = $village->getProd("iron");
$rwtime = ($rwood <= 0) ? 0 : ($woodProd > 0 ? $rwood / $woodProd * 3600 : 0);
$rcltime = ($rclay <= 0) ? 0 : ($clayProd > 0 ? $rclay / $clayProd * 3600 : 0);
$rctime = ($rcrop <= 0) ? 0 : ($cropProd > 0 ? $rcrop / $cropProd * 3600 : 0);
$ritime = ($riron <= 0) ? 0 : ($ironProd > 0 ? $riron / $ironProd * 3600 : 0);
$reqtime = max($rwtime, $rctime, $rcltime, $ritime);
$reqtime += time();
return $generator->procMtime($reqtime);
+261 -128
View File
@@ -434,28 +434,40 @@ class MYSQLi_DB implements IDbConnection {
* @see \App\Database\IDbConnection::connect()
*/
public function connect() {
// try to connect
try {
$this->dblink = mysqli_connect( $this->hostname, $this->username, $this->password, $this->dbname, $this->port );
} catch (\Exception $exception) {
$this->dblink = mysqli_connect( $this->hostname . ':' . $this->port, $this->username, $this->password );
$host = (string) $this->hostname;
$port = (int) $this->port;
if ($port <= 0) $port = 3306;
// return on error
if (mysqli_error($this->dblink)) {
return false;
// If host is in form host:port (common in env files), split it once.
if (strpos($host, ':') !== false && substr_count($host, ':') === 1) {
$hostPort = explode(':', $host, 2);
if (isset($hostPort[0], $hostPort[1]) && is_numeric($hostPort[1])) {
$host = $hostPort[0];
$port = (int) $hostPort[1];
}
// select the DB to use
mysqli_select_db($this->dblink, $this->dbname);
}
// return on error
if (mysqli_error($this->dblink)) {
return false;
} else {
// connected and DB exists, we're good to go
return true;
}
$attempts = 15;
$waitMicros = 1000000; // 1 second
for ($i = 0; $i < $attempts; $i++) {
try {
$this->dblink = mysqli_connect($host, $this->username, $this->password, $this->dbname, $port);
} catch (\Throwable $exception) {
$this->dblink = false;
}
if ($this->dblink instanceof \mysqli) {
return true;
}
// During container startup DB may still be booting.
if ($i < $attempts - 1) {
usleep($waitMicros);
}
}
return false;
}
/**
@@ -1162,7 +1174,7 @@ public function getBestOasisCropBonus($x, $y) {
$q = "SELECT timestamp from " . TB_PREFIX . "deleting where uid = $uid LIMIT 1";
$result = mysqli_query($this->dblink,$q);
$dbarray = mysqli_fetch_array($result);
return $dbarray['timestamp'];
return isset($dbarray['timestamp']) ? (int)$dbarray['timestamp'] : 0;
}
function modifyGold($userid, $amt, $mode) {
@@ -1728,10 +1740,10 @@ public function getBestOasisCropBonus($x, $y) {
function getVillageState($wref, $use_cache = true) {
// retrieve this value from the full village data cache
if ($use_cache && ($cachedValue = self::returnCachedContent(self::$villageFieldsCacheByWorldID, $wref)) && !is_null($cachedValue)) {
return ($cachedValue['occupied'] != 0 || $cachedValue['oasistype'] != 0);
return (((int)($cachedValue['occupied'] ?? 0)) != 0 || ((int)($cachedValue['oasistype'] ?? 0)) != 0);
} else {
$vil = $this->getVillageByWorldID($wref, $use_cache);
return ($vil['occupied'] != 0 || $vil['oasistype'] != 0);
return (((int)($vil['occupied'] ?? 0)) != 0 || ((int)($vil['oasistype'] ?? 0)) != 0);
}
}
@@ -2075,7 +2087,7 @@ public function getBestOasisCropBonus($x, $y) {
// return a single value
if (!$array_passed) {
self::$villageFieldsCacheByWorldID[$vid[0]] = $result[0];
self::$villageFieldsCacheByWorldID[$vid[0]] = (isset($result[0]) && is_array($result[0])) ? $result[0] : [];
} else {
if ($result && count($result)) {
foreach ( $result as $record ) {
@@ -3715,7 +3727,7 @@ public function getBestOasisCropBonus($x, $y) {
'LIMIT 1');
$row = mysqli_fetch_array($result, MYSQLI_ASSOC);
self::$fieldLevelsInVillageSearchCache[$vid.$fieldType] = $row['level'];
self::$fieldLevelsInVillageSearchCache[$vid.$fieldType] = isset($row['level']) ? (int)$row['level'] : 0;
return self::$fieldLevelsInVillageSearchCache[$vid.$fieldType];
}
@@ -3730,9 +3742,9 @@ public function getBestOasisCropBonus($x, $y) {
$q = "SELECT f" . $field . " from " . TB_PREFIX . "fdata where vref = $vid LIMIT 1";
$result = mysqli_query($this->dblink,$q);
$row = mysqli_fetch_array($result);
$row = $result ? mysqli_fetch_array($result, MYSQLI_ASSOC) : null;
self::$resourceLevelsCache[$vid.$field] = $row["f" . $field];
self::$resourceLevelsCache[$vid.$field] = (is_array($row) && array_key_exists("f".$field, $row)) ? (int)$row["f" . $field] : 0;
return self::$resourceLevelsCache[$vid.$field];
}
@@ -5874,22 +5886,36 @@ References: User ID/Message ID, Mode
*/
function addUnits($vid, $troopsArray = null) {
list($vid, $type, $values) = $this->escape_input($vid, $type, $values);
list($vid) = $this->escape_input($vid);
if(empty($vid)) return;
if (!is_array($vid)) $vid = [$vid];
$types = $values = "";
$types = "";
$typeKeys = [];
$values = [];
if($troopsArray != null){
$types = $troopsArray[0];
$typeKeys = $troopsArray[0];
$values = $troopsArray[1];
$types = ",u".implode(",u", $types);
$types = ",u".implode(",u", $typeKeys);
}
foreach ($vid as $index => $vidValue) $vid[$index] = (int) $vidValue.($troopsArray != null ? ",".implode(",", $values[$index]) : "");
foreach ($vid as $index => $vidValue) $vid[$index] = (int) $vidValue.($troopsArray != null ? ",".implode(",", $values[$index]) : "");
$q = "INSERT into " . TB_PREFIX . "units (vref$types) values (".implode('),(', $vid).")";
$duplicateUpdate = "";
if(!empty($typeKeys)){
$duplicateColumns = [];
foreach($typeKeys as $typeKey){
$typeKey = (int) $typeKey;
$duplicateColumns[] = "u".$typeKey."=VALUES(u".$typeKey.")";
}
$duplicateUpdate = " ON DUPLICATE KEY UPDATE ".implode(', ', $duplicateColumns);
}else{
$duplicateUpdate = " ON DUPLICATE KEY UPDATE vref=vref";
}
$q = "INSERT into " . TB_PREFIX . "units (vref$types) values (".implode('),(', $vid).")".$duplicateUpdate;
return mysqli_query($this->dblink,$q);
}
@@ -6965,13 +6991,47 @@ References: User ID/Message ID, Mode
public function populateWorldData() {
global $autoprefix;
$wdataTable = TB_PREFIX . "wdata";
$droppedIndexes = [];
try {
// check if we don't already have world data
$data_exist = $this->query_return("SELECT * FROM " . TB_PREFIX . "wdata LIMIT 1");
$data_exist = $this->query_return("SELECT * FROM " . $wdataTable . " LIMIT 1");
if ($data_exist && count($data_exist)) {
return false;
}
// Best-effort session tuning for faster bulk inserts.
$sessionTuningStatements = [
"SET SESSION innodb_flush_log_at_trx_commit=2",
"SET SESSION sync_binlog=0",
"SET SESSION unique_checks=0",
"SET SESSION foreign_key_checks=0",
];
foreach ($sessionTuningStatements as $stmt) {
try {
mysqli_query($this->dblink, $stmt);
} catch (Throwable $e) {
// Ignore tuning failures, they are not required for correctness.
}
}
// Temporarily drop secondary indexes before heavy INSERT .. SELECT.
// Recreated at the end to speed up map generation on larger worlds.
$indexesToDrop = ['occupied', 'fieldtype', 'x-y'];
foreach ($indexesToDrop as $indexName) {
try {
$escapedIndexName = mysqli_real_escape_string($this->dblink, $indexName);
$res = mysqli_query($this->dblink, "SHOW INDEX FROM `{$wdataTable}` WHERE Key_name = '{$escapedIndexName}'");
if ($res && mysqli_num_rows($res) > 0) {
mysqli_query($this->dblink, "ALTER TABLE `{$wdataTable}` DROP INDEX `{$indexName}`");
$droppedIndexes[$indexName] = true;
}
} catch (Throwable $e) {
// If we can't drop an index, continue with generation anyway.
}
}
// load the data generation SQL file
$str = file_get_contents($autoprefix."var/db/datagen-world-data.sql");
$str = preg_replace(["'%PREFIX%'", "'%WORLDSIZE%'"], [TB_PREFIX, WORLD_MAX], $str);
@@ -6988,8 +7048,31 @@ References: User ID/Message ID, Mode
if (!$result) {
return -1;
}
} catch (\Exception $e) {
} catch (\Throwable $e) {
return -1;
} finally {
// Recreate dropped indexes to keep runtime query performance unchanged.
if (!empty($droppedIndexes)) {
try {
if (isset($droppedIndexes['occupied'])) {
mysqli_query($this->dblink, "CREATE INDEX `occupied` ON `{$wdataTable}` (`occupied`)");
}
if (isset($droppedIndexes['fieldtype'])) {
mysqli_query($this->dblink, "CREATE INDEX `fieldtype` ON `{$wdataTable}` (`fieldtype`)");
}
if (isset($droppedIndexes['x-y'])) {
mysqli_query($this->dblink, "CREATE INDEX `x-y` ON `{$wdataTable}` (`x`, `y`)");
}
} catch (Throwable $e) {
// Best effort only; installation can proceed and indexes may be rebuilt manually if needed.
}
}
// Restore conservative defaults for this session (best effort).
try { mysqli_query($this->dblink, "SET SESSION unique_checks=1"); } catch (Throwable $e) {}
try { mysqli_query($this->dblink, "SET SESSION foreign_key_checks=1"); } catch (Throwable $e) {}
try { mysqli_query($this->dblink, "SET SESSION innodb_flush_log_at_trx_commit=1"); } catch (Throwable $e) {}
try { mysqli_query($this->dblink, "SET SESSION sync_binlog=1"); } catch (Throwable $e) {}
}
return true;
@@ -7012,121 +7095,171 @@ References: User ID/Message ID, Mode
}
public function populateCroppers(int $countTotal = 0, bool $truncateFirst = false, int $batch = 20000, ?callable $reporter = null ): array {
@set_time_limit(0);
@ini_set('memory_limit', '1G');
@set_time_limit(0);
@ini_set('memory_limit', '1G');
$TBP = defined('TB_PREFIX') ? TB_PREFIX : 's1_';
$CROP_TABLE = $TBP . 'croppers';
$WDATA = $TBP . 'wdata';
$TBP = defined('TB_PREFIX') ? TB_PREFIX : 's1_';
$CROP_TABLE = $TBP . 'croppers';
$WDATA = $TBP . 'wdata';
// Count once if caller didn't
if ($countTotal <= 0) {
$row = mysqli_fetch_assoc(mysqli_query($this->dblink,
"SELECT COUNT(*) cnt FROM `$WDATA` WHERE fieldtype IN (1,6)"));
$countTotal = (int)($row['cnt'] ?? 0);
}
$total = 0;
$inTransaction = false;
if ($truncateFirst) {
if (!mysqli_query($this->dblink, "TRUNCATE TABLE `$CROP_TABLE`")) {
return ['ok'=>false,'msg'=>'TRUNCATE failed: '.mysqli_error($this->dblink)];
}
}
try {
if ($countTotal <= 0) {
$row = mysqli_fetch_assoc(mysqli_query($this->dblink, "SELECT COUNT(*) cnt FROM `$WDATA` WHERE fieldtype IN (1,6)"));
$countTotal = (int)($row['cnt'] ?? 0);
}
// Session-level speed knobs (local to this connection)
@mysqli_query($this->dblink, "SET innodb_flush_log_at_trx_commit=2");
@mysqli_query($this->dblink, "SET sync_binlog=0");
@mysqli_query($this->dblink, "SET unique_checks=0");
@mysqli_query($this->dblink, "SET foreign_key_checks=0");
if ($truncateFirst && !mysqli_query($this->dblink, "TRUNCATE TABLE `$CROP_TABLE`")) {
return ['ok'=>false,'msg'=>'TRUNCATE failed: '.mysqli_error($this->dblink)];
}
// Read big windows; write in safe slices to avoid max_allowed_packet
if ($batch < 1000) $batch = 1000;
if ($batch > 100000) $batch = 100000;
if($countTotal < 1000) $sliceSize = 200;
elseif($countTotal < 5000) $sliceSize = 500;
elseif($countTotal > 5000) $sliceSize = 1000;
$sessionTuningStatements = [
"SET SESSION innodb_flush_log_at_trx_commit=2",
"SET SESSION sync_binlog=0",
"SET SESSION unique_checks=0",
"SET SESSION foreign_key_checks=0",
];
foreach($sessionTuningStatements as $stmt){
try {
mysqli_query($this->dblink, $stmt);
} catch (Throwable $e) {
// Ignore tuning failures, they are not required for correctness.
}
}
$total = 0;
$lastId = 0;
if ($batch < 1000) $batch = 1000;
if ($batch > 100000) $batch = 100000;
if($countTotal < 1000) $sliceSize = 200;
elseif($countTotal < 5000) $sliceSize = 500;
else $sliceSize = 1000;
// Cursor pagination (no OFFSET)
while (true) {
$res = mysqli_query(
$this->dblink,
"SELECT id AS wref, x, y, fieldtype
FROM `$WDATA`
WHERE fieldtype IN (1,6) AND id > $lastId
ORDER BY id ASC
LIMIT $batch"
);
if (!$res) {
return ['ok'=>false,'msg'=>'SELECT failed: '.mysqli_error($this->dblink),'processed'=>$total,'target'=>$countTotal];
}
$lastId = 0;
while (true) {
$res = mysqli_query(
$this->dblink,
"SELECT id AS wref, x, y, fieldtype
FROM `$WDATA`
WHERE fieldtype IN (1,6) AND id > $lastId
ORDER BY id ASC
LIMIT $batch"
);
if (!$res) {
return ['ok'=>false,'msg'=>'SELECT failed: '.mysqli_error($this->dblink),'processed'=>$total,'target'=>$countTotal];
}
$rows = [];
while ($r = mysqli_fetch_assoc($res)) { $rows[] = $r; }
if (!$rows) break;
$rows = [];
while ($r = mysqli_fetch_assoc($res)) { $rows[] = $r; }
if (!$rows) break;
mysqli_begin_transaction($this->dblink);
mysqli_begin_transaction($this->dblink);
$inTransaction = true;
$n = count($rows);
for ($i = 0; $i < $n; $i += $sliceSize) {
$chunk = array_slice($rows, $i, $sliceSize);
$values = [];
$n = count($rows);
for ($i = 0; $i < $n; $i += $sliceSize) {
$chunk = array_slice($rows, $i, $sliceSize);
$values = [];
foreach ($chunk as $r) {
$x = (int)$r['x'];
$y = (int)$r['y'];
// Compute oasis crop bonus in batch for the whole chunk.
$bonusByWref = [];
$wrefs = [];
foreach ($chunk as $r) {
$wrefs[] = (int)$r['wref'];
}
// Your existing helper:
$bonus = (int)$this->getBestOasisCropBonus($x, $y);
if ($bonus < 0) $bonus = 0;
if ($bonus > 150) $bonus = 150;
if (!empty($wrefs)) {
$bonusSql = "SELECT
c.id AS wref,
LEAST(
150,
(50 * LEAST(SUM(CASE WHEN od.type = 12 THEN 1 ELSE 0 END), 3)) +
(25 * LEAST(
GREATEST(3 - LEAST(SUM(CASE WHEN od.type = 12 THEN 1 ELSE 0 END), 3), 0),
SUM(CASE WHEN od.type IN (4,9,10,11) THEN 1 ELSE 0 END)
))
) AS bonus
FROM `$WDATA` c
LEFT JOIN `$WDATA` o
ON o.fieldtype = 0
AND o.x BETWEEN (c.x - 3) AND (c.x + 3)
AND o.y BETWEEN (c.y - 3) AND (c.y + 3)
LEFT JOIN `{$TBP}odata` od
ON od.wref = o.id
AND od.type IN (12,4,9,10,11)
WHERE c.id IN (".implode(',', $wrefs).")
GROUP BY c.id";
$values[] = sprintf("(%d,%d,%d,%d,%d)",
(int)$r['wref'], $x, $y, (int)$r['fieldtype'], $bonus
);
}
$bonusRes = mysqli_query($this->dblink, $bonusSql);
if (!$bonusRes) {
mysqli_rollback($this->dblink);
$inTransaction = false;
return ['ok'=>false,'msg'=>'BONUS SELECT failed: '.mysqli_error($this->dblink),'processed'=>$total,'target'=>$countTotal];
}
if ($values) {
// ODKU is cheaper than REPLACE (no DELETE)
$sql = "INSERT INTO `$CROP_TABLE`
(`wref`,`x`,`y`,`fieldtype`,`best_oasis_bonus`)
VALUES ".implode(',', $values)."
ON DUPLICATE KEY UPDATE
`x`=VALUES(`x`),
`y`=VALUES(`y`),
`fieldtype`=VALUES(`fieldtype`),
`best_oasis_bonus`=VALUES(`best_oasis_bonus`)";
if (!mysqli_query($this->dblink, $sql)) {
mysqli_rollback($this->dblink);
return ['ok'=>false,'msg'=>'INSERT failed: '.mysqli_error($this->dblink),'processed'=>$total,'target'=>$countTotal];
}
}
while ($bonusRow = mysqli_fetch_assoc($bonusRes)) {
$bonusByWref[(int)$bonusRow['wref']] = (int)($bonusRow['bonus'] ?? 0);
}
}
// progress after each slice
$total += count($chunk);
if ($reporter) {
$pct = $countTotal ? min(100, (int)floor(($total / $countTotal) * 100)) : 0;
$reporter($total, $countTotal, $pct);
}
}
foreach ($chunk as $r) {
$x = (int)$r['x'];
$y = (int)$r['y'];
$bonus = (int)($bonusByWref[(int)$r['wref']] ?? 0);
if ($bonus < 0) $bonus = 0;
if ($bonus > 150) $bonus = 150;
$values[] = sprintf("(%d,%d,%d,%d,%d)", (int)$r['wref'], $x, $y, (int)$r['fieldtype'], $bonus);
}
mysqli_commit($this->dblink);
if ($values) {
$sql = "INSERT INTO `$CROP_TABLE`
(`wref`,`x`,`y`,`fieldtype`,`best_oasis_bonus`)
VALUES ".implode(',', $values)."
ON DUPLICATE KEY UPDATE
`x`=VALUES(`x`),
`y`=VALUES(`y`),
`fieldtype`=VALUES(`fieldtype`),
`best_oasis_bonus`=VALUES(`best_oasis_bonus`)";
if (!mysqli_query($this->dblink, $sql)) {
mysqli_rollback($this->dblink);
$inTransaction = false;
return ['ok'=>false,'msg'=>'INSERT failed: '.mysqli_error($this->dblink),'processed'=>$total,'target'=>$countTotal];
}
}
// advance cursor
$lastId = (int)$rows[$n - 1]['wref'];
}
$total += count($chunk);
if ($reporter) {
$pct = $countTotal ? min(100, (int)floor(($total / $countTotal) * 100)) : 0;
$reporter($total, $countTotal, $pct);
}
}
// Restore checks (optional)
@mysqli_query($this->dblink, "SET unique_checks=1");
@mysqli_query($this->dblink, "SET foreign_key_checks=1");
mysqli_commit($this->dblink);
$inTransaction = false;
$lastId = (int)$rows[$n - 1]['wref'];
}
// Analyze once at the end
@mysqli_query($this->dblink, "ANALYZE TABLE `$CROP_TABLE`");
foreach(["SET SESSION unique_checks=1", "SET SESSION foreign_key_checks=1"] as $stmt){
try {
mysqli_query($this->dblink, $stmt);
} catch (Throwable $e) {
// Ignore restore failures if the engine does not support the setting.
}
}
if ($reporter) { $reporter($total, $countTotal, 100); }
return ['ok'=>true,'msg'=>'Croppers populated','processed'=>$total,'target'=>$countTotal];
@mysqli_query($this->dblink, "ANALYZE TABLE `$CROP_TABLE`");
if ($reporter) { $reporter($total, $countTotal, 100); }
return ['ok'=>true,'msg'=>'Croppers populated','processed'=>$total,'target'=>$countTotal];
} catch (Throwable $e) {
if ($inTransaction) {
try {
mysqli_rollback($this->dblink);
} catch (Throwable $rollbackException) {
// Ignore rollback errors after fatal DB exceptions.
}
}
return ['ok'=>false,'msg'=>$e->getMessage(),'processed'=>$total,'target'=>$countTotal];
}
}
+6 -6
View File
@@ -194,7 +194,7 @@ define('WISHES_YOU', 'wishes you');
define('X_MAS', 'Merry Christmas');
define('NEW_YEAR', 'Happy New Year');
define('EASTER', 'Happy Easter');
define('PEACE', 'Peace');
if(!defined('PEACE')) define('PEACE', 'Peace');
define('GOLD', 'Gold');
define('GOLD_IMG', '<img src=\"/img/x.gif\" class=\"gold\" alt=\"'.GOLD.'\" title=\"'.GOLD.'\">');
@@ -626,8 +626,8 @@ define('MODERATOR', 'Moderator');
define('ACTIVE', 'Active');
define('ONLINE', 'Online');
define('TUTORIAL', 'Tutorial');
define('FAQ', 'Faq');
define('SPIELREGELN', 'Game Rules');
if(!defined('FAQ')) define('FAQ', 'Faq');
if(!defined('SPIELREGELN')) define('SPIELREGELN', 'Game Rules');
define('PLAYER_STATISTICS', 'Player statistics');
define('TOTAL_PLAYERS', PLAYERS.' in total');
define('ACTIVE_PLAYERS', 'Active players');
@@ -645,8 +645,8 @@ define('BECOME_COMUNITY', 'Become part of our community now!');
define('BECOME_COMUNITY2', 'Become a part of one of<br>the biggest gaming<br>communities in the<br>world.');
define('NEWS', 'News');
define('SCREENSHOTS', 'Screenshots');
define('FAQ', 'FAQ');
define('SPIELREGELN', 'Rules');
if(!defined('FAQ')) define('FAQ', 'FAQ');
if(!defined('SPIELREGELN')) define('SPIELREGELN', 'Rules');
define('AGB', 'Terms and Conditions');
define('LEARN1', 'Upgrade your fields and mines to increase your resource production. You will need resources to construct buildings and train soldiers.');
define('LEARN2', 'Construct and expand the buildings in your village. Buildings improve your overall infrastructure, increase your resource production and allow you to research, train and upgrade your troops.');
@@ -1331,7 +1331,7 @@ define('PLAYER_ADMIN', 'This player is Admin');
define('PLAYER_MH', 'This player is Multihunter');
define('PLAYER_BANNED', 'This player is BANNED');
define('PLAYER_VACATION', 'This player is on VACATION');
define('BANNED', 'Banned');
if(!defined('BANNED')) define('BANNED', 'Banned');
define('GENDER', 'Gender');
define('GENDER0', 'n/a');
define('MALE0', 'm');
+13 -5
View File
@@ -684,11 +684,19 @@ class Technology {
$resarray['iron'] = ${'r'.$id}['iron'];
$resarray['crop'] = ${'r'.$id}['crop'];
}
$rwtime = ($resarray['wood']-$village->awood) / $village->getProd("wood") * 3600;
$rcltime = ($resarray['clay']-$village->aclay) / $village->getProd("clay") * 3600;
$ritime = ($resarray['iron']-$village->airon) / $village->getProd("iron") * 3600;
$rctime = ($resarray['crop']-$village->acrop) / $village->getProd("crop") * 3600;
if($village->getProd("crop") >= 0) {
$woodNeed = $resarray['wood'] - $village->awood;
$clayNeed = $resarray['clay'] - $village->aclay;
$ironNeed = $resarray['iron'] - $village->airon;
$cropNeed = $resarray['crop'] - $village->acrop;
$woodProd = $village->getProd("wood");
$clayProd = $village->getProd("clay");
$ironProd = $village->getProd("iron");
$cropProd = $village->getProd("crop");
$rwtime = ($woodNeed <= 0) ? 0 : ($woodProd > 0 ? $woodNeed / $woodProd * 3600 : 0);
$rcltime = ($clayNeed <= 0) ? 0 : ($clayProd > 0 ? $clayNeed / $clayProd * 3600 : 0);
$ritime = ($ironNeed <= 0) ? 0 : ($ironProd > 0 ? $ironNeed / $ironProd * 3600 : 0);
$rctime = ($cropNeed <= 0) ? 0 : ($cropProd != 0 ? $cropNeed / $cropProd * 3600 : 0);
if($cropProd >= 0) {
$reqtime = max($rwtime,$rcltime,$ritime,$rctime) + time();
} else {
$reqtime = max($rwtime,$rcltime,$ritime);
+15 -3
View File
@@ -24,7 +24,7 @@ class Units {
switch($post['c']) {
case 1:
if (isset($post['a']) && $post['a'] == 533374) $this->sendTroops($post);
if (isset($post['a']) && $post['a'] == 533374 && empty($post['disabled'])) $this->sendTroops($post);
else
{
$post = $this->loadUnits($post);
@@ -51,7 +51,7 @@ class Units {
break;
case 4:
if (isset($post['a']) && $post['a'] == 533374) $this->sendTroops($post);
if (isset($post['a']) && $post['a'] == 533374 && empty($post['disabled'])) $this->sendTroops($post);
else
{
$post = $this->loadUnits($post);
@@ -97,12 +97,24 @@ class Units {
public function checkErrors(&$post){
global $database, $village, $session, $generator;
if(isset($post['x'])) $post['x'] = trim($post['x']);
if(isset($post['y'])) $post['y'] = trim($post['y']);
if(isset($post['x']) && isset($post['y']) && $post['x'] !== "" && $post['y'] !== ""){
if(!preg_match('/^-?\d+$/', $post['x']) || !preg_match('/^-?\d+$/', $post['y'])) return "Invalid coordinates";
$post['x'] = (int) $post['x'];
$post['y'] = (int) $post['y'];
if(abs($post['x']) > WORLD_MAX || abs($post['y']) > WORLD_MAX) return "Coordinates do not exist";
}
// Search by town name
// Coordinates and look confirm name people
if(isset($post['x']) && isset($post['y']) && $post['x'] != "" && $post['y'] != "") {
$vid = $database->getVilWref($post['x'], $post['y']);
unset($post['dname'], $post['dname']);
unset($post['dname']);
}
else if(isset($post['dname']) && !empty($post['dname'])) $vid = $database->getVillageByName(stripslashes($post['dname']));
+130 -71
View File
@@ -1,97 +1,156 @@
[![Code Triagers Badge](https://www.codetriage.com/shadowss/travianz/badges/users.svg)](https://www.codetriage.com/shadowss/travianz)
# TravianZ
[![Maintenance](https://img.shields.io/maintenance/yes/2025.svg)](https://github.com/Shadowss/TravianZ)
[![GitHub Release](https://img.shields.io/github/release/Shadowss/TravianZ/all.svg)](https://github.com/Shadowss/TravianZ)
[![Github All Releases](https://img.shields.io/github/downloads/Shadowss/TravianZ/total.svg)](https://github.com/Shadowss/TravianZ)
[![GitHub contributors](https://img.shields.io/github/contributors/Shadowss/TravianZ.svg)](https://github.com/Shadowss/TravianZ)
[![license](https://img.shields.io/github/license/Shadowss/TravianZ.svg)](https://github.com/Shadowss/TravianZ)
[![GitHub last commit](https://img.shields.io/github/last-commit/Shadowss/TravianZ.svg)](https://github.com/Shadowss/TravianZ)
[![Proudly Coded in PHPStorm](https://img.shields.io/badge/coded%20in-PHPStorm-BD5CF3.svg)](https://www.jetbrains.com/buy/opensource/?product=phpstorm)
[![Join the chat at https://gitter.im/TravianZ-V8/Lobby](https://badges.gitter.im/TravianZ-V8/Lobby.svg)](https://gitter.im/TravianZ-V8/Lobby?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge)
[![Watch how this was made on YouTube](https://img.shields.io/badge/The%20making%20of...-YouTube-FF0000.svg)](https://www.youtube.com/watch?v=1XiHhpGUmQg&list=PLzV5avt1FFHorlIeoL9YX0pdb9bj-FO84)
======
[![GitHub forks](https://img.shields.io/github/forks/Shadowss/TravianZ.svg?style=social&label=Fork)](https://github.com/Shadowss/TravianZ)
[![GitHub stars](https://img.shields.io/github/stars/Shadowss/TravianZ.svg?style=social&label=Stars)](https://github.com/Shadowss/TravianZ)
[![GitHub watchers](https://img.shields.io/github/watchers/Shadowss/TravianZ.svg?style=social&label=Watch)](https://github.com/Shadowss/TravianZ)
[![GitHub followers](https://img.shields.io/github/followers/Shadowss.svg?style=social&label=Follow)](https://github.com/Shadowss/TravianZ)
[![Twitter URL](https://img.shields.io/twitter/url/http/shields.io.svg?style=social)](https://twitter.com/cata7007)
======
TravianZ Version **v.8.3.5** - BETA 1
======
**Note:** this game is still in a pre-release state, although at this point it is very playable, tested and found to be fairly stable
TravianZ is an open-source browser strategy game inspired by classic Travian-like gameplay.
**WARNING:** please note that ***this is in no way an upgrade*** from the old 8.3.2 version, so please ***do not try to just copy your files over***,
since the installer logic has changed and you would just crash your old version
This repository currently targets modern local/server setups with PHP 8.x and MariaDB.
**Docker Quick Start:**
## Project Status
Get up and running quickly with Docker:
- Version line: `v8.3.5` (legacy naming)
- Stability: playable and actively maintained
- Migration note: this is not a drop-in upgrade over older `8.3.2` deployments
If you are upgrading from an older installation, do a fresh install and migrate data carefully.
## Quick Start (Docker)
```bash
# Clone the repository
git clone https://github.com/Shadowss/TravianZ.git
cd TravianZ
# Copy environment file
cp .env.example .env
# Start containers
docker-compose up -d
# Open browser to http://localhost:8080/install
docker compose up -d
```
For detailed Docker setup instructions, see [DOCKER_README.md](DOCKER_README.md)
Then open:
**Quick links:**
* [Download and Updates](https://github.com/Shadowss/TravianZ) &raquo;&raquo; https://github.com/Shadowss/TravianZ
* [Wiki](https://github.com/Shadowss/TravianZ/wiki)
* [Game Mechanics](http://travian.wikia.com/wiki/Travian_Wiki)
* [The Making Of](https://www.youtube.com/watch?v=1XiHhpGUmQg&list=PLzV5avt1FFHorlIeoL9YX0pdb9bj-FO84) - YouTube Videos about this project
* [Donate to TravianZ](https://www.paypal.me/cata7007)
- `http://localhost:8080/install`
**Minimum requirements:**
* [PHP](http://php.net/) 7.0.0+
* [MySQL Community Server](https://dev.mysql.com/downloads/mysql/) 5.5+
* or alternatively, [MariaDB](https://downloads.mariadb.org/) 5.5+
* Runs fine on Ubuntu 20, Apache2 2.4, MySQL Server 8.0 and PHP 7.4
Detailed container guide: [DOCKER_README.md](DOCKER_README.md)
**Dedicated or shared hosting?**
## System Requirements
We *strongly* recommend using a ***dedicated hosting*** for this game. Big Travian servers used to host
thousands of players and the original servers still had about 300ms page display time. The legacy code
in this clone (which was created in 2013) is right now doing about **400 MySQL queries** (questions
to the database) per single page refresh - which is usually well beyond what most shared hostings can support
with a big enough player base.
Recommended:
**Support & Bug reports**
- PHP `8.3+`
- MariaDB `latest stable` (or MySQL-compatible server)
- Apache or Nginx with PHP support
- Linux server with enough CPU/RAM for your expected player count
We are usually available to chat at our [Gitter channel](https://gitter.im/TravianZ-V8/Lobby), if you like to ask
any questions or just talk to the developers about how great their day is today :) As for bug reports, please use
the [Issues tab](https://github.com/Shadowss/TravianZ/issues) and create new issue, whether it's an error in game
or you have a feature request to be included in the play.
Notes:
**The team**
* [Shadowss](https://github.com/Shadowss) - project owner and an occasional developer / tester
* [lietuvis10](https://github.com/lietuvis10) - active developer
* [iopietro](https://github.com/iopietro) - alumni developer
* [AL-Kateb](https://github.com/AL-Kateb) - alumni developer
* [martinambrus](https://github.com/martinambrus) - alumni developer
* [Vladyslav](https://github.com/velhbxtyrj) - rigorous game tester
* [phaze1G](https://github.com/phaze1G) - game designer / css support
- The game is query-heavy by design (legacy architecture), so shared hosting can become a bottleneck quickly.
- For medium/large servers, prefer dedicated or well-sized VPS infrastructure.
**Thanks**
## Installation (Web Installer)
Many thanks to all those who recently played around with the project and tested it both, locally and on their
own servers. This includes [ZZJHONS](https://github.com/ZZJHONS), [DracoMilesX](https://github.com/DracoMilesX),
[phaze1G](https://github.com/phaze1G) , [martinambrus](https://github.com/martinambrus) and many others who were too shy to let us know that they use and enjoy
running this game :)
1. Start services (Docker) or prepare your web+DB stack.
2. Open `http://your-host/install`.
3. Fill database settings:
- Host: `db` (Docker) or your DB host
- Port: usually `3306`
- DB/User/Password from your environment
4. Complete installer steps:
- DB structure
- World data
- Croppers build
5. After success, access the game root.
Also, our thanks go to all both, the original and occasional developers, especially [yi12345](https://github.com/yi12345/),
[advocaite](https://github.com/advocaite/), [brainiacX](https://github.com/brainiacX/), [ronix](https://github.com/ronix/),
[MisterX](https://github.com/MisterX/), [Elio](https://github.com/eliopinho/) and many others who were part of this
project's history.
## Environment Configuration
Last but not least, our thanks go to [JetBrains](https://www.jetbrains.com/) for lending us a one-year full-featured
[open-source PHPStorm](https://www.jetbrains.com/buy/opensource/?product=phpstorm) (and other products) license!
Thanks guys, you're awesome :)
Use `.env` (copy from `.env.example`) to manage deployment values.
Main keys:
- `MARIADB_ROOT_PASSWORD`
- `MARIADB_DATABASE`
- `MARIADB_USER`
- `MARIADB_PASSWORD`
- `DB_HOST`
- `DB_PORT`
Legacy compatibility keys (`MYSQL_*`) are still supported and can inherit MariaDB values.
## Admin Panel
Admin entrypoint:
- `http://your-host/Admin/admin.php`
Recent improvements include:
- Users list under the `Users` menu
- Better null/undefined handling in admin templates
- Dynamic table prefix support in map tile queries
## Performance Notes
For large worlds (for example `400x400`), generation tasks can be expensive.
Recent optimizations include:
- world data generation tuning for bulk operations
- croppers generation batching and progress streaming
- safer DB/session handling during installer workflows
For production-like loads, monitor:
- DB CPU and slow queries
- PHP-FPM/Apache worker limits
- disk I/O during installer and reset operations
## Troubleshooting
Common checks:
1. If installer cannot connect to DB:
- verify `DB_HOST`, port, user and password
- in Docker, host should be `db`, not `localhost`
2. If permissions fail during install:
- ensure web user can write required runtime files/folders
3. If pages show warnings after PHP upgrade:
- ensure latest code is deployed
- clear opcode/cache and retry
For container-specific troubleshooting, see [DOCKER_README.md](DOCKER_README.md).
## Development
Useful commands:
```bash
# Start stack
docker compose up -d
# Logs
docker compose logs -f web
# Validate PHP files
find . -name '*.php' -not -path './var/*' -print0 | xargs -0 -n1 php -l
```
Repository references:
- Change history: [CHANGELOG.md](CHANGELOG.md)
- Contribution guide: [CONTRIBUTING.md](CONTRIBUTING.md)
- Code of conduct: [CODE_OF_CONDUCT.md](CODE_OF_CONDUCT.md)
## Community and Support
- Issues: https://github.com/Shadowss/TravianZ/issues
- Wiki: https://github.com/Shadowss/TravianZ/wiki
- Chat: https://gitter.im/TravianZ-V8/Lobby
## Credits
Thanks to the original and current maintainers, contributors, testers, and the TravianZ community.
Special acknowledgement to all legacy authors and maintainers who kept this project alive through multiple iterations.
## License
This project is licensed under the terms described in [LICENSE](LICENSE).
+5 -3
View File
@@ -71,13 +71,15 @@ class Security
if(self::$instance === NULL)
{
// Check for magic quotes
if(get_magic_quotes_runtime())
if(function_exists('get_magic_quotes_runtime') && get_magic_quotes_runtime())
{
// Dear lord!! This is bad and deprected. Sort it out ;)
set_magic_quotes_runtime(0);
if(function_exists('set_magic_quotes_runtime')) {
set_magic_quotes_runtime(0);
}
}
if(get_magic_quotes_gpc())
if(function_exists('get_magic_quotes_gpc') && get_magic_quotes_gpc())
{
// This is also bad and deprected. See http://php.net/magic_quotes for more information.
$this->magic_quotes_gpc = TRUE;
+25 -79
View File
@@ -52,6 +52,22 @@ $datetimedp = $golds['dp'];
// Retrieve the current date/time
$date2 = strtotime("NOW");
function formatRemainingTime($endTimestamp, $nowTimestamp) {
$remaining = (int)$endTimestamp - (int)$nowTimestamp;
if ($remaining <= 0) {
return '';
}
$days = intdiv($remaining, 86400);
$remaining %= 86400;
$hours = intdiv($remaining, 3600);
$remaining %= 3600;
$mins = intdiv($remaining, 60);
$secs = $remaining % 60;
return 'Remaining: <b>'.$days.'</b> days <b>'.$hours.'</b> hours <b>'.$mins.'</b> mins <b>'.$secs.'</b> secs (until '.date('H:i:s', (int)$endTimestamp).')';
}
if ($datetimep == 0) echo "get PLUS<br>";
else
{
@@ -59,17 +75,7 @@ else
print "Your PLUS advantage has ended.<br>";
mysqli_query($database->dblink, "UPDATE " . TB_PREFIX . "users set plus = '0' where `id`='" . $session->uid . "'") or die(mysqli_error($database->dblink));
} else {
$holdtotmin = (($datetimep - $date2) / 60);
$holdtothr = (($datetimep - $date2) / 3600);
$holdtotday = intval(($datetimep - $date2) / 86400);
echo "<font color='#B3B3B3' size='1'>Remaining: <b>" . $holdtotday . "</b> days";
$holdhr = intval($holdtothr - ($holdtotday * 24));
echo " <b>" . ($holdhr) . "</b> hours ";
$holdmr = intval($holdtotmin - (($holdhr * 60) + ($holdtotday * 1440)));
echo "<b> " . ($holdmr) . "</b> mins</font>";
echo "<font color='#B3B3B3' size='1'>" . formatRemainingTime($datetimep, $date2) . "</font>";
}
}
?>
@@ -120,23 +126,8 @@ if (PLUS_TIME >= 86400) {
<?php
$tl_b1 = $golds['b1'];
if ($tl_b1 < $date2) {
print " ";
} else {
$holdtotmin1 = (($tl_b1 - $date2) / 60);
$holdtothr1 = (($tl_b1 - $date2) / 3600);
$holdtotday1 = intval(($tl_b1 - $date2) / 86400);
$holdhr1 = intval($holdtothr1 - ($holdtotday1 * 24));
$holdmr1 = intval($holdtotmin1 - (($holdhr1 * 60) + ($holdtotday1 * 1440)));
}
if ($tl_b1 < $date2) {
print " ";
} else {
echo "<font color='#B3B3B3' size='1'>Remaining <b> " . $holdtotday1 . "</b> days ";
echo "<b> " . ($holdhr1) . "</b> hours ";
echo "<b> " . ($holdmr1) . "</b> mins</font> ";
if ($tl_b1 >= $date2) {
echo "<font color='#B3B3B3' size='1'>" . formatRemainingTime($tl_b1, $date2) . "</font> ";
}
?>
&nbsp; </span>
@@ -189,23 +180,8 @@ if ($session->access != BANNED) {
<?php
$tl_b2 = $golds['b2'];
if ($tl_b2 < $date2) {
print " ";
} else {
$holdtotmin2 = (($tl_b2 - $date2) / 60);
$holdtothr2 = (($tl_b2 - $date2) / 3600);
$holdtotday2 = intval(($tl_b2 - $date2) / 86400);
$holdhr2 = intval($holdtothr2 - ($holdtotday2 * 24));
$holdmr2 = intval($holdtotmin2 - (($holdhr2 * 60) + ($holdtotday2 * 1440)));
}
if ($tl_b2 < $date2) {
print " ";
} else {
echo "<font color='#B3B3B3' size='1'>Remaining: <b> " . $holdtotday2 . "</b> days ";
echo "<b> " . ($holdhr2) . "</b> hours ";
echo "<b> " . ($holdmr2) . "</b> mins<font>";
if ($tl_b2 >= $date2) {
echo "<font color='#B3B3B3' size='1'>" . formatRemainingTime($tl_b2, $date2) . "</font>";
}
?>
&nbsp; </span>
@@ -259,23 +235,8 @@ if ($session->access != BANNED) {
<?php
$tl_b3 = $golds['b3'];
if ($tl_b3 < $date2) {
print " ";
} else {
$holdtotmin3 = (($tl_b3 - $date2) / 60);
$holdtothr3 = (($tl_b3 - $date2) / 3600);
$holdtotday3 = intval(($tl_b3 - $date2) / 86400);
$holdhr3 = intval($holdtothr3 - ($holdtotday3 * 24));
$holdmr3 = intval($holdtotmin3 - (($holdhr3 * 60) + ($holdtotday3 * 1440)));
}
if ($tl_b3 < $date2) {
print " ";
} else {
echo "<font color='#B3B3B3' size='1'>Remaining: <b> " . $holdtotday3 . "</b> days ";
echo "<b> " . ($holdhr3) . "</b> hours ";
echo "<b> " . ($holdmr3) . "</b> mins</font>";
if ($tl_b3 >= $date2) {
echo "<font color='#B3B3B3' size='1'>" . formatRemainingTime($tl_b3, $date2) . "</font>";
}
?>
&nbsp; </span>
@@ -327,23 +288,8 @@ if ($session->access != BANNED) {
<?php
$tl_b4 = $golds['b4'];
if ($tl_b4 < $date2) {
print " ";
} else {
$holdtotmin4 = (($tl_b4 - $date2) / 60);
$holdtothr4 = (($tl_b4 - $date2) / 3600);
$holdtotday4 = intval(($tl_b4 - $date2) / 86400);
$holdhr4 = intval($holdtothr4 - ($holdtotday4 * 24));
$holdmr4 = intval($holdtotmin4 - (($holdhr4 * 60) + ($holdtotday4 * 1440)));
}
if ($tl_b4 < $date2) {
print " ";
} else {
echo "<font color='#B3B3B3' size='1'>Remaining: <b> " . $holdtotday4 . "</b> days ";
echo "<b> " . ($holdhr4) . "</b> hours ";
echo "<b> " . ($holdmr4) . "</b> mins</font>";
if ($tl_b4 >= $date2) {
echo "<font color='#B3B3B3' size='1'>" . formatRemainingTime($tl_b4, $date2) . "</font>";
}
?>
&nbsp; </span>
+1 -1
View File
@@ -6,7 +6,7 @@
if(!isset($_GET['id']) && @(basename($_SERVER['REQUEST_URI']) !== 'a2b2.php')) {
echo "class=\"selected\"";
}
if(isset($_GET['id']) && ($_GET['id'] == 1) || strlen($_GET['id']) === 3) {
if(isset($_GET['id']) && (($_GET['id'] == 1) || strlen((string)$_GET['id']) === 3)) {
echo "class=\"selected\"";
}
+31 -10
View File
@@ -42,6 +42,27 @@ $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,
@@ -53,10 +74,10 @@ mysqli_query($database->dblink, "CREATE TABLE IF NOT EXISTS `$CROP_TABLE` (
CHECK (`best_oasis_bonus` IN (0,25,50,75,100,125,150))
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4");
// Helpful indexes (ignore errors if already exist)
@mysqli_query($database->dblink, "CREATE INDEX `idx_ft_bonus_xy` ON `$CROP_TABLE` (`fieldtype`, `best_oasis_bonus`, `x`, `y`)");
@mysqli_query($database->dblink, "CREATE INDEX `idx_xy` ON `$CROP_TABLE` (`x`, `y`)");
@mysqli_query($database->dblink, "CREATE INDEX `idx_bonus` ON `$CROP_TABLE` (`best_oasis_bonus`)");
// 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 {
@@ -110,12 +131,12 @@ if ($action === 'truncate') {
$notice = "Croppers table truncated.";
}
if ($action === 'reindex') {
@mysqli_query($database->dblink, "DROP INDEX `idx_ft_bonus_xy` ON `$CROP_TABLE`");
@mysqli_query($database->dblink, "DROP INDEX `idx_xy` ON `$CROP_TABLE`");
@mysqli_query($database->dblink, "DROP INDEX `idx_bonus` ON `$CROP_TABLE`");
@mysqli_query($database->dblink, "CREATE INDEX `idx_ft_bonus_xy` ON `$CROP_TABLE` (`fieldtype`, `best_oasis_bonus`, `x`, `y`)");
@mysqli_query($database->dblink, "CREATE INDEX `idx_xy` ON `$CROP_TABLE` (`x`, `y`)");
@mysqli_query($database->dblink, "CREATE INDEX `idx_bonus` ON `$CROP_TABLE` (`best_oasis_bonus`)");
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') {
+7 -10
View File
@@ -1,5 +1,3 @@
version: '3.8'
services:
web:
build:
@@ -10,7 +8,6 @@ services:
- "8080:80"
volumes:
- ./:/var/www/html
- ./var:/var/www/html/var
environment:
- APACHE_DOCUMENT_ROOT=/var/www/html
depends_on:
@@ -20,13 +17,13 @@ services:
restart: unless-stopped
db:
image: mysql:5.7
image: mariadb:latest
container_name: travianz-db
environment:
MYSQL_ROOT_PASSWORD: ${MYSQL_ROOT_PASSWORD:-rootpassword}
MYSQL_DATABASE: ${MYSQL_DATABASE:-travian}
MYSQL_USER: ${MYSQL_USER:-travianz}
MYSQL_PASSWORD: ${MYSQL_PASSWORD:-travianzpass}
MARIADB_ROOT_PASSWORD: ${MARIADB_ROOT_PASSWORD:-${MYSQL_ROOT_PASSWORD:-rootpassword}}
MARIADB_DATABASE: ${MARIADB_DATABASE:-${MYSQL_DATABASE:-travian}}
MARIADB_USER: ${MARIADB_USER:-${MYSQL_USER:-travianz}}
MARIADB_PASSWORD: ${MARIADB_PASSWORD:-${MYSQL_PASSWORD:-travianzpass}}
volumes:
- db-data:/var/lib/mysql
- ./var/db:/docker-entrypoint-initdb.d:ro
@@ -35,7 +32,7 @@ services:
networks:
- travianz-network
restart: unless-stopped
command: --default-authentication-plugin=mysql_native_password --sql_mode=""
command: --sql_mode=""
phpmyadmin:
image: phpmyadmin/phpmyadmin:latest
@@ -44,7 +41,7 @@ services:
PMA_HOST: db
PMA_PORT: 3306
PMA_USER: root
PMA_PASSWORD: ${MYSQL_ROOT_PASSWORD:-rootpassword}
PMA_PASSWORD: ${MARIADB_ROOT_PASSWORD:-${MYSQL_ROOT_PASSWORD:-rootpassword}}
ports:
- "8081:80"
depends_on:
+3 -2
View File
@@ -67,8 +67,8 @@ $reporter = function($done, $target, $pct) use (&$lastPing) {
if (connection_aborted()) { exit; } // client left
};
// Run it (fresh world => truncateFirst=true; big batch on dedicated server)
$out = $database->populateCroppers($total, true, 20000, $reporter);
// Run it (fresh world => truncateFirst=true). Slightly smaller batch keeps progress updates more frequent.
$out = $database->populateCroppers($total, true, 2000, $reporter);
if (!empty($out['ok'])) {
sse_send([
@@ -79,6 +79,7 @@ if (!empty($out['ok'])) {
]);
} else {
sse_send([
'error'=>true,
'pct'=>0,
'done'=>0,
'total'=>(int)$total,
+6 -3
View File
@@ -7,7 +7,7 @@ include("templates/script.tpl");
if(!isset($_GET['s'])) {
$_GET['s']=0;
}
$tz=(isset($_GET['t']))? $_GET['t']:1;
$tz=(isset($_GET['t']))? (int)$_GET['t'] : 13;
switch($tz) {
case 1: $t_zone="Africa/Dakar";break;
case 2: $t_zone="America/New_York";break;
@@ -18,8 +18,11 @@ $tz=(isset($_GET['t']))? $_GET['t']:1;
case 7: $t_zone="Australia/Melbourne";break;
case 8: $t_zone="Europe/Bucharest";break;
case 9: $t_zone="Europe/London";break;
case 10: $t_zone="Indian/Maldives";break;
case 11: $t_zone="Pacific/Fiji";break;
case 10: $t_zone="Europe/Bratislava";break;
case 11: $t_zone="Indian/Maldives";break;
case 12: $t_zone="Pacific/Fiji";break;
case 13: $t_zone="America/Sao_Paulo";break;
default: $t_zone="America/Sao_Paulo";break;
}
date_default_timezone_set($t_zone);
?>
+65 -6
View File
@@ -19,6 +19,64 @@
if(isset($_GET['c']) && $_GET['c'] == 1) {
echo "<div class=\"headline\"><span class=\"f10 c5\">Error creating constant.php check cmod.</span></div><br>";
}
@session_start();
$envPath = dirname(__DIR__, 2) . '/.env';
$envDefaults = [];
if(file_exists($envPath)) {
$lines = @file($envPath, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES);
if(is_array($lines)) {
foreach($lines as $line) {
$line = trim($line);
if($line === '' || $line[0] === '#') {
continue;
}
$eqPos = strpos($line, '=');
if($eqPos === false) {
continue;
}
$key = trim(substr($line, 0, $eqPos));
$value = trim(substr($line, $eqPos + 1));
if($key === '') {
continue;
}
if((strlen($value) >= 2) && (($value[0] === '"' && substr($value, -1) === '"') || ($value[0] === "'" && substr($value, -1) === "'"))) {
$value = substr($value, 1, -1);
}
$envDefaults[$key] = $value;
}
// Resolve ${VAR} references using parsed values first, then process env.
foreach($envDefaults as $key => $value) {
$envDefaults[$key] = preg_replace_callback('/\$\{([A-Z0-9_]+)\}/i', function($m) use ($envDefaults) {
$ref = $m[1];
if(isset($envDefaults[$ref])) return $envDefaults[$ref];
$fromEnv = getenv($ref);
return ($fromEnv !== false) ? $fromEnv : '';
}, $value);
}
}
}
$dbHost = $envDefaults['DB_HOST'] ?? 'localhost';
$dbPort = $envDefaults['DB_PORT'] ?? '3306';
$dbUser = $envDefaults['MARIADB_USER'] ?? ($envDefaults['MYSQL_USER'] ?? '');
$dbPass = $envDefaults['MARIADB_PASSWORD'] ?? ($envDefaults['MYSQL_PASSWORD'] ?? '');
$dbName = $envDefaults['MARIADB_DATABASE'] ?? ($envDefaults['MYSQL_DATABASE'] ?? '');
if(empty($_SESSION['install_random_prefix'])) {
try {
$_SESSION['install_random_prefix'] = 's' . substr(bin2hex(random_bytes(2)), 0, 4) . '_';
} catch (Throwable $e) {
$_SESSION['install_random_prefix'] = 's' . str_pad((string) mt_rand(0, 9999), 4, '0', STR_PAD_LEFT) . '_';
}
}
$dbPrefix = $_SESSION['install_random_prefix'];
?>
<form action="process.php" method="post" id="dataform">
@@ -41,6 +99,7 @@ echo "<div class=\"headline\"><span class=\"f10 c5\">Error creating constant.php
<select name="tzone" onChange="refresh(this.value)">
<option value="1,Africa/Dakar" <?php if ($tz==1) echo "selected";?>>Africa</option>
<option value="2,America/New_York" <?php if ($tz==2) echo "selected";?>>America</option>
<option value="13,America/Sao_Paulo" <?php if ($tz==13) echo "selected";?>>Brazil (Sao Paulo)</option>
<option value="3,Antarctica/Casey" <?php if ($tz==3) echo "selected";?>>Antarctica</option>
<option value="4,Arctic/Longyearbyen" <?php if ($tz==4) echo "selected";?>>Arctic</option>
<option value="5,Asia/Kuala_Lumpur" <?php if ($tz==5) echo "selected";?>>Asia</option>
@@ -416,27 +475,27 @@ echo "<div class=\"headline\"><span class=\"f10 c5\">Error creating constant.php
<table>
<tr>
<td><span class="f9 c6">Hostname:</span></td>
<td><input name="sserver" type="text" id="sserver" value="localhost"></td>
<td><input name="sserver" type="text" id="sserver" value="<?php echo htmlspecialchars($dbHost, ENT_QUOTES, 'UTF-8'); ?>"></td>
</tr>
<tr>
<td><span class="f9 c6">Port:</span></td>
<td><input name="sport" type="text" id="sport" value="3306"></td>
<td><input name="sport" type="text" id="sport" value="<?php echo htmlspecialchars($dbPort, ENT_QUOTES, 'UTF-8'); ?>"></td>
</tr>
<tr>
<td><span class="f9 c6">Username:</span></td>
<td><input name="suser" type="text" id="suser" value=""></td>
<td><input name="suser" type="text" id="suser" value="<?php echo htmlspecialchars($dbUser, ENT_QUOTES, 'UTF-8'); ?>"></td>
</tr>
<tr>
<td><span class="f9 c6">Password:</span></td>
<td><input type="password" name="spass" id="spass"></td>
<td><input type="password" name="spass" id="spass" value="<?php echo htmlspecialchars($dbPass, ENT_QUOTES, 'UTF-8'); ?>"></td>
</tr>
<tr>
<td><span class="f9 c6">DB name:</span></td>
<td><input type="text" name="sdb" id="sdb"></td>
<td><input type="text" name="sdb" id="sdb" value="<?php echo htmlspecialchars($dbName, ENT_QUOTES, 'UTF-8'); ?>"></td>
</tr>
<tr>
<td><span class="f9 c6">Prefix:</span></td>
<td><input type="text" name="prefix" id="prefix" value="s1_" size="5"></td>
<td><input type="text" name="prefix" id="prefix" value="<?php echo htmlspecialchars($dbPrefix, ENT_QUOTES, 'UTF-8'); ?>" size="7"></td>
</tr>
<td><span class="f9 c6">Type:</span></td>
<td>
+15 -15
View File
@@ -24,22 +24,22 @@ if($_POST && count($_POST))
$strMailtext = "";
while(list($strName,$value) = each($_POST))
{
if(is_array($value))
{
foreach($value as $value_array)
{
$strMailtext .= $strName.$strDelimiter.$value_array."\n";
}
}
else
{
$strMailtext .= $strName.$strDelimiter.$value."\n";
}
}
foreach($_POST as $strName => $value)
{
if(is_array($value))
{
foreach($value as $value_array)
{
$strMailtext .= $strName.$strDelimiter.$value_array."\n";
}
}
else
{
$strMailtext .= $strName.$strDelimiter.$value."\n";
}
}
if(get_magic_quotes_gpc())
if(function_exists('get_magic_quotes_gpc') && get_magic_quotes_gpc())
{
$strMailtext = stripslashes($strMailtext);
}
+5
View File
@@ -17,6 +17,11 @@ AccessLogger::logRequest();
$max_per_pass = 1000;
// Default flow flags used later in template conditions.
$NextStep = false;
$NextStep2 = false;
$done = false;
if (mysqli_num_rows(mysqli_query($database->dblink,"SELECT id FROM ".TB_PREFIX."users WHERE access = 9 AND id = ".(int) $session->uid)) != '1') die("Hacking attemp!");
if (@$_POST['submit'] == "Send")
+7
View File
@@ -15,6 +15,13 @@ include_once("GameEngine/Account.php");
AccessLogger::logRequest();
$max_per_pass = 1000;
// Default flow flags used later in template conditions.
$NextStep = false;
$NextStep2 = false;
$done = false;
$Interupt = false;
if (mysqli_num_rows(mysqli_query($database->dblink,"SELECT id FROM ".TB_PREFIX."users WHERE access = 9 AND id = ".$session->uid)) != '1') die("Hacking attempt!");
if(isset($_GET['del'])){