Refactor GameEngine files

fix some GameEngine bugs and new improovements
This commit is contained in:
novgorodschi catalin
2026-07-20 07:09:06 +03:00
parent 94a7a1c245
commit 8db3032293
11 changed files with 951 additions and 1002 deletions
+613 -870
View File
File diff suppressed because it is too large Load Diff
+5
View File
@@ -383,6 +383,11 @@ if (!isset($SAJAX_INCLUDED)) {
function get_data() {
global $session,$database;
// FIX: $data initializat inainte de concatenare - fara asta, PHP 8+
// arunca "Warning: Undefined variable $data" la fiecare refresh de chat
// (umple error log-ul; pe PHP 9 devine mai strict).
$data = '';
$alliance = $database->escape($session->alliance);
$query = mysqli_query($database->dblink,"select id_user, name, date, msg from ".TB_PREFIX."chat where alli='$alliance' order by id desc limit 0,13");
while ($r = mysqli_fetch_array($query)) {
@@ -30,6 +30,91 @@ trait DatabaseBuildingQueries {
return mysqli_query($this->dblink,$q);
}
/**
* B-W2: finalizeaza o cladire (nivel + tip) in fdata SI invalideaza cache-ul
* de resource levels. Inlocuieste UPDATE-ul raw din Building::finishAll()
* care ocolea invalidarea din Faza B - recountCP() citea nivelele vechi din
* cache si persista un CP gresit in vdata.
*/
function finishBuildingLevel($wid, $field, $level, $type) {
list($wid, $field, $level, $type) = $this->escape_input((int) $wid, (int) $field, (int) $level, (int) $type);
$q = "UPDATE " . TB_PREFIX . "fdata SET f" . $field . " = " . $level . ", f" . $field . "t = " . $type . " WHERE vref = " . $wid;
$result = mysqli_query($this->dblink, $q);
// invalidare: urmatorul getResourceLevel()/getFieldLevel() reciteste din DB
self::clearResourseLevelsCache();
return $result;
}
/**
* B-W2: sterge joburi din bdata dupa id-uri SI invalideaza cache-ul de
* joburi al satului. Inlocuieste DELETE-ul raw din Building::finishAll().
*/
function deleteBuildings($wid, array $ids) {
$wid = (int) $wid;
$ids = array_map('intval', $ids);
if (empty($ids)) {
return true;
}
$result = mysqli_query($this->dblink, "DELETE FROM " . TB_PREFIX . "bdata WHERE id IN(" . implode(',', $ids) . ")");
unset(self::$buildingsUnderConstructionCache[$wid]);
return $result;
}
/**
* B-W2: promoveaza un job din coada de asteptare (loopcon = 1) in coada
* activa SI invalideaza cache-ul de joburi al satului.
*/
function promoteLoopJob($wid, $id) {
list($wid, $id) = $this->escape_input((int) $wid, (int) $id);
$result = mysqli_query($this->dblink, "UPDATE " . TB_PREFIX . "bdata SET loopcon = 0 WHERE id = " . $id);
unset(self::$buildingsUnderConstructionCache[$wid]);
return $result;
}
/**
* B-W2: actualizeaza numarul maxim de membri ai aliantei conduse de $leader
* (efectul Embassy-ului). Inlocuieste UPDATE-ul raw din Building::finishAll().
*/
function updateAllianceMax($leader, $max) {
list($leader, $max) = $this->escape_input((int) $leader, (int) $max);
return mysqli_query($this->dblink, "UPDATE " . TB_PREFIX . "alidata SET max = " . $max . " WHERE leader = " . $leader);
}
/**
* B-W2: log de gold (prepared statement). Inlocuieste INSERT-ul raw din
* Building::finishAll(); acelasi format ca logger-ul din UserQueries.
*/
function addGoldFinLog($wid, $uid, $action, $gold, $details) {
$stmt = $this->dblink->prepare(
"INSERT INTO `" . TB_PREFIX . "gold_fin_log` (wid, uid, action, gold, time, details) VALUES (?, ?, ?, ?, ?, ?)"
);
if (!$stmt) {
return false;
}
$wid = (int) $wid;
$uid = (int) $uid;
$gold = (int) $gold;
$now = time();
$stmt->bind_param("iisiis", $wid, $uid, $action, $gold, $now, $details);
$ok = $stmt->execute();
$stmt->close();
return $ok;
}
function getBuildLock($wid) {
$wid = (int) $wid;
$result = mysqli_query($this->dblink, "SELECT GET_LOCK('build_village_$wid', 10) AS locked");
@@ -288,4 +288,44 @@ References: User ID/Message ID, Mode
$result = mysqli_query($this->dblink,$q);
return $this->mysqli_fetch_all($result);
}
/**
* UM-W1: numarul de mesaje trimise de $uid in ultimele $seconds secunde.
* Muta flood-check-ul inline din Message::sendMessage()/sendAMessage().
*/
function countRecentMessages($uid, $seconds = 60) {
list($uid, $seconds) = $this->escape_input((int) $uid, (int) $seconds);
$q = "SELECT COUNT(*) AS Total FROM " . TB_PREFIX . "mdata WHERE owner = $uid AND time > " . (time() - $seconds);
$row = mysqli_fetch_assoc(mysqli_query($this->dblink, $q));
return (int) ($row['Total'] ?? 0);
}
/**
* UM-W1: id-urile membrilor unei aliante. Muta query-ul inline din
* Message::sendAMessage().
*/
function getAllianceMemberIds($alliance) {
list($alliance) = $this->escape_input((int) $alliance);
$q = "SELECT id FROM " . TB_PREFIX . "users WHERE alliance = $alliance";
$result = mysqli_query($this->dblink, $q);
$ids = [];
while ($row = mysqli_fetch_assoc($result)) {
$ids[] = $row['id'];
}
return $ids;
}
/**
* UM-W1: target + owner ale unui mesaj (pentru a decide modul de stergere).
* Muta SELECT-ul inline din Message::removeMessage().
*/
function getMessageOwnership($id) {
list($id) = $this->escape_input((int) $id);
$q = "SELECT target, owner FROM " . TB_PREFIX . "mdata WHERE id = $id LIMIT 1";
$row = mysqli_fetch_assoc(mysqli_query($this->dblink, $q));
return $row ?: ['target' => 0, 'owner' => 0];
}
}
+9 -4
View File
@@ -299,11 +299,16 @@ class Market
? PUSH_PROTECTION_LIMIT
: 'This account has reached its resource-transfer limit for now and cannot receive that many resources. Send less, or wait for the limit to recover.');
} elseif (
$availableWood >= $post['r1'] &&
$availableClay >= $post['r2'] &&
$availableIron >= $post['r3'] &&
$availableCrop >= $post['r4']
$availableWood >= $wtrans &&
$availableClay >= $ctrans &&
$availableIron >= $itrans &&
$availableCrop >= $crtrans
) {
// UM-D1: comparatia foloseste valorile curatate de semnul minus
// ($wtrans/... in loc de $post['r1']/...). Inainte, comparatia se
// facea pe POST-ul brut (potential negativ), dar deducerea se facea
// pe valoarea pozitiva - un "r1" negativ trecea verificarea si scadea
// resurse pozitive.
$reqMerc = ceil((array_sum($resource) - 0.1) / $this->maxcarry);
// Acquire merchant lock to prevent race conditions
+28 -58
View File
@@ -307,7 +307,6 @@ class Message
private function removeMessage($post)
{
global $database, $session;
$post = $database->escape($post);
$mode5updates = [];
$mode7updates = [];
$mode8updates = [];
@@ -316,14 +315,9 @@ class Message
continue;
}
$messageId = (int)$post['n' . $i];
$query = mysqli_query(
$database->dblink,
"SELECT target, owner
FROM " . TB_PREFIX . "mdata
WHERE id = " . $messageId . "
LIMIT 1"
);
$message = mysqli_fetch_array($query);
// UM-W1: SELECT-ul de ownership mutat in DB (getMessageOwnership).
// $messageId e deja cast la int, deci nu mai e nevoie de escape pe tot $post.
$message = $database->getMessageOwnership($messageId);
if (
$message['target'] == $session->uid &&
$message['owner'] == $session->uid
@@ -595,34 +589,15 @@ class Message
{
global $session, $database;
// Flood protection
$q = "
SELECT COUNT(*) AS Total
FROM " . TB_PREFIX . "mdata
WHERE owner='" . $session->uid . "'
AND time > " . (time() - 60);
$res = mysqli_fetch_array(
mysqli_query($database->dblink, $q),
MYSQLI_ASSOC
);
if ($res['Total'] > 5) {
// Flood protection (UM-W1: query mutat in DB)
if ($database->countRecentMessages($session->uid, 60) > 5) {
return;
}
$allmembersQ = mysqli_query(
$database->dblink,
"SELECT id
FROM " . TB_PREFIX . "users
WHERE alliance='" . $session->alliance . "'"
);
$userally = $database->getUserField($session->uid, "alliance", 0);
$permission = mysqli_fetch_array(
mysqli_query(
$database->dblink,
"SELECT opt7
FROM " . TB_PREFIX . "ali_permission
WHERE uid='" . $session->uid . "'"
)
);
$allmembers = $database->getAllianceMemberIds($session->alliance);
$userally = $database->getUserField($session->uid, "alliance", 0);
$permission = $database->getAllyMessagePermission($session->uid);
if (defined('WORD_CENSOR')) {
$topic = $this->wordCensor($topic);
$text = $this->wordCensor($text);
@@ -646,11 +621,11 @@ class Message
$coor,
$report
);
if ($permission['opt7'] == 1) {
if ($permission == 1) {
if ($userally > 0) {
while ($allmembers = mysqli_fetch_array($allmembersQ)) {
foreach ($allmembers as $memberId) {
$database->sendMessage(
$allmembers['id'],
$memberId,
$session->uid,
htmlspecialchars(addslashes($topic)),
htmlspecialchars(addslashes($text)),
@@ -671,19 +646,9 @@ class Message
global $session, $database;
$user = $database->getUserField($recieve, "id", 1);
// Flood protection
// Flood protection (UM-W1: query mutat in DB)
if ($security_check) {
$q = "
SELECT COUNT(*) AS Total
FROM " . TB_PREFIX . "mdata
WHERE owner='" . $session->uid . "'
AND time > " . (time() - 60);
$res = mysqli_fetch_array(
mysqli_query($database->dblink, $q),
MYSQLI_ASSOC
);
if ($res['Total'] > 5) {
if ($database->countRecentMessages($session->uid, 60) > 5) {
return;
}
}
@@ -842,7 +807,12 @@ class Message
public function addFriends($post)
{
global $database;
global $database, $session;
// FIX securitate (UM-D1): identitatea proprie se ia din sesiune, NU din
// $post['myid'] (hidden field, deci falsificabil de un client modificat).
// Inainte, un POST forjat cu alt myid modifica lista de prieteni a altui
// jucator (IDOR) - SQL-ul era escapat, dar ownership-ul nu era verificat.
$myid = (int) $session->uid;
for ($i = 0; $i <= 19; $i++) {
if (empty($post['addfriends' . $i])) {
continue;
@@ -859,12 +829,12 @@ class Message
continue;
}
$user = $database->getUserField(
$post['myid'],
$myid,
"friend" . $j,
0
);
$userwait = $database->getUserField(
$post['myid'],
$myid,
"friend" . $j . "wait",
0
);
@@ -872,11 +842,11 @@ class Message
for ($k = 0; $k <= 19; $k++) {
$user1 = $database->getUserField(
$post['myid'],
$myid,
"friend" . $k,
0
);
if ($user1 == $uid || $uid == $post['myid']) {
if ($user1 == $uid || $uid == $myid) {
$exist = 1;
}
}
@@ -905,18 +875,18 @@ class Message
$database->addFriend(
$uid,
"friend" . $l . "wait",
$post['myid']
$myid
);
$added1 = 1;
}
}
$database->addFriend(
$post['myid'],
$myid,
"friend" . $j,
$uid
);
$database->addFriend(
$post['myid'],
$myid,
"friend" . $j . "wait",
$uid
);
+11 -2
View File
@@ -432,10 +432,19 @@ class Profile {
private function removeSitter($get) {
global $database, $session;
// UM-W1: validam type in {1,2} inainte de a-l folosi in numele coloanei
// (sit1/sit2). DB escapeaza campul, deci nu era injectabil, dar un type
// invalid ar fi tintit o coloana inexistenta.
$type = isset($get['type']) ? (int)$get['type'] : 0;
if ($type !== 1 && $type !== 2) {
header("Location: spieler.php?s=" . $get['s']);
exit;
}
if ($get['a'] == $session->checker) {
if ($session->userinfo['sit' . $get['type']] == $get['id']) {
$database->updateUserField($session->uid, "sit" . $get['type'], 0, 1);
if ($session->userinfo['sit' . $type] == $get['id']) {
$database->updateUserField($session->uid, "sit" . $type, 0, 1);
// Invalidate the 30s session user-cache (see Session::PopulateVar) so the
// removed sitter disappears immediately, without a re-login.
unset($_SESSION['cache_user_' . ($_SESSION['username'] ?? '')]);
+102 -55
View File
@@ -399,40 +399,16 @@ class Units {
$rivalsGreatConfusion = $database->getArtifactsSumByKind($to_owner, $data['to_vid'], 7);
$rallyPointLevel = ($village->resarray)['f39'];
$invalidBuildings = [];
$invalidBuildings = $this->catapultInvalidBuildings($rallyPointLevel);
// fill the array with the invalid buildings
if($rallyPointLevel >= 3 && $rallyPointLevel < 5){
for($i = 1; $i <= 50; $i++){
if(!in_array($i, [10, 11])) $invalidBuildings[] = $i;
}
}
else if($rallyPointLevel >= 5 && $rallyPointLevel < 10){
for($i = 12; $i <= 50; $i++) $invalidBuildings[] = $i;
}
else if($rallyPointLevel >= 10){
// zidurile nu pot fi tinta directa (31,32,33 + cele noi 42,43,47,50)
$invalidBuildings = [23, 31, 32, 33, 34, 36, 42, 43, 47, 50];
}
if(isset($post['ctar1']) && $post['ctar1'] != 0){
// check if the player has selected a valid building
if($rallyPointLevel < 3 || $data['u8'] == 0 || in_array($post['ctar1'], $invalidBuildings) || $post['ctar1'] < 0 || $post['ctar1'] > 50){
$post['ctar1'] = 0;
}
}
if(isset($post['ctar2']) && $post['ctar2'] != 0){
// check if there are atleast 20 catapults
if($data['u8'] < 20 || $rallyPointLevel != 20){
$post['ctar2'] = 0;
}else{
// check if the player has selected a valid building
if(in_array($post['ctar2'], $invalidBuildings) || ($post['ctar2'] < 0 || $post['ctar2'] > 50 && $post['ctar2'] != 99)){
$post['ctar2'] = 99;
}
}
}
// UM-W1: validarea per-tinta extrasa in catapultTargetAllowed(); comportament
// identic cu vechile blocuri inline (aceleasi praguri, aceleasi fallback-uri).
$post['ctar1'] = $this->normalizeCatapultTarget(
$post['ctar1'] ?? 0, 1, $data, $rallyPointLevel, $invalidBuildings
);
$post['ctar2'] = $this->normalizeCatapultTarget(
$post['ctar2'] ?? 0, 2, $data, $rallyPointLevel, $invalidBuildings
);
// Bug fix: Brewery (35) is Teuton-only, capital-only, but empire-wide —
// the catapult-randomization side effect must be checked on the SENDER'S
@@ -447,31 +423,96 @@ class Units {
&& $database->getFieldLevelInVillage($senderCapital['wref'], 35) > 0;
}
if(isset($post['ctar1'])) {
//Is the Mead-Festival active?
if(!$hasActiveBrewery){
if($rivalsGreatConfusion['totals'] > 0) {
if($post['ctar1'] != 40 && ($post['ctar1'] != 27 || ($post['ctar1'] == 27 && $rivalsGreatConfusion['unique'] > 0))) {
$post['ctar1'] = 0;
}
}
}
else $post['ctar1'] = 0;
}
else $post['ctar1'] = 0;
$post['ctar1'] = $this->applyConfusion(
$post['ctar1'] ?? 0, 1, $hasActiveBrewery, $rivalsGreatConfusion
);
$post['ctar2'] = $this->applyConfusion(
$post['ctar2'] ?? 0, 2, $hasActiveBrewery, $rivalsGreatConfusion
);
}
if(isset($post['ctar2']) && $post['ctar2'] > 0) {
//Is the Mead-Festival active?
if(!$hasActiveBrewery){
if($rivalsGreatConfusion['totals'] > 0) {
if ($post['ctar2'] != 40 && ($post['ctar2'] != 27 || ($post['ctar2'] == 27 && $rivalsGreatConfusion['unique'] > 0))) {
$post['ctar2'] = 99;
}
/**
* UM-W1: lista cladirilor care nu pot fi tinta directa de catapulta, in
* functie de nivelul Punctului de Adunare. Extrasa 1:1 din vechiul switch.
*/
private function catapultInvalidBuildings($rallyPointLevel) {
if ($rallyPointLevel >= 3 && $rallyPointLevel < 5) {
$invalid = [];
for ($i = 1; $i <= 50; $i++) {
if (!in_array($i, [10, 11])) $invalid[] = $i;
}
return $invalid;
}
if ($rallyPointLevel >= 5 && $rallyPointLevel < 10) {
$invalid = [];
for ($i = 12; $i <= 50; $i++) $invalid[] = $i;
return $invalid;
}
if ($rallyPointLevel >= 10) {
// zidurile nu pot fi tinta directa (31,32,33 + cele noi 42,43,47,50)
return [23, 31, 32, 33, 34, 36, 42, 43, 47, 50];
}
return [];
}
/**
* UM-W1: validarea de baza a unei tinte de catapulta (ctar1 sau ctar2),
* inainte de efectul de confuzie. $slot = 1 sau 2. Pastreaza exact regulile
* vechi: ctar1 cere RP>=3, minim 1 catapulta si tinta valida; ctar2 cere
* minim 20 catapulte, RP == 20 si cade pe 99 (random) cand tinta e invalida.
*/
private function normalizeCatapultTarget($target, $slot, $data, $rallyPointLevel, array $invalidBuildings) {
if ($slot == 1) {
if ($target != 0) {
if ($rallyPointLevel < 3 || $data['u8'] == 0 || in_array($target, $invalidBuildings) || $target < 0 || $target > 50) {
return 0;
}
}
else $post['ctar2'] = 99;
return $target;
}
else $post['ctar2'] = 0;
// slot 2
if ($target != 0) {
if ($data['u8'] < 20 || $rallyPointLevel != 20) {
return 0;
}
if (in_array($target, $invalidBuildings) || ($target < 0 || $target > 50 && $target != 99)) {
return 99;
}
}
return $target;
}
/**
* UM-W1: efectul artefactului "Rivals' Great Confusion" / festivalul de bere.
* ctar1 -> 0 (random), ctar2 -> 99 (random) cand tinta nu e WW(40) sau
* Trezorerie(27, exceptata doar de artefactul unic). Comportament 1:1.
*/
private function applyConfusion($target, $slot, $hasActiveBrewery, $rivalsGreatConfusion) {
$random = ($slot == 1) ? 0 : 99;
if ($slot == 2 && $target <= 0) {
return 0;
}
if ($hasActiveBrewery) {
return $random;
}
// Conditie pastrata IDENTIC cu originalul (verificata prin test de
// echivalenta). Nota: ramura "$target == 27 && unique > 0" nu schimba
// rezultatul - orice tinta != 40 ajunge random - dar o pastram 1:1 ca
// sa nu introducem o schimbare de comportament subtila.
if ($rivalsGreatConfusion['totals'] > 0) {
if ($target != 40 && ($target != 27 || ($target == 27 && $rivalsGreatConfusion['unique'] > 0))) {
return $random;
}
}
return $target;
}
private function sendTroopsBack($post) {
@@ -575,10 +616,16 @@ class Units {
public function Settlers($post) {
global $form, $database, $village, $session;
// FIX securitate (UM-D1): tablourile de CP (cp0..cpN) sunt globale, definite
// in GameEngine/Data/cp.php. Fara aceste "global", ${'cp'.$mode} era null in
// scope-ul metodei, deci $need_cps = null si "$cps >= $need_cps" trecea mereu -
// verificarea de puncte de cultura la intemeierea satului era ocolita
// server-side (doar UI-ul o respecta). Importam exact tabloul folosit.
global ${'cp'.CP};
$mode = CP;
$total = count($database->getProfileVillages($session->uid));
$need_cps = ${'cp'.$mode}[$total + 1];
$need_cps = isset(${'cp'.$mode}[$total + 1]) ? ${'cp'.$mode}[$total + 1] : PHP_INT_MAX;
$cps = $session->cp;
$rallypoint = $database->getResourceLevel($village->wid);