diff --git a/install/process_instance.php b/install/process_instance.php new file mode 100644 index 00000000..72ff1795 --- /dev/null +++ b/install/process_instance.php @@ -0,0 +1,367 @@ +root = dirname(__DIR__); + $this->instancesFile = $this->root . '/instances/registry.json'; + } + + public function run(): void + { + if ($_SERVER['REQUEST_METHOD'] !== 'POST') { + $this->fail('Invalid request method.'); + } + + $instanceId = strtolower(trim((string) ($_POST['instance_id'] ?? ''))); + if (!TravianZInstanceResolver::isValid($instanceId)) { + $this->fail('Invalid instance ID.'); + } + + // Select this instance before Bootstrap/Database are loaded. + putenv('TRAVIANZ_INSTANCE=' . $instanceId); + + $action = (string) ($_POST['action'] ?? 'config'); + + try { + switch ($action) { + case 'config': + $this->writeConfig($instanceId); + break; + + case 'structure': + $this->loadInstanceBootstrap(); + $this->createStructure($instanceId); + break; + + case 'world': + $this->loadInstanceBootstrap(); + $this->createWorld($instanceId); + break; + + case 'finalize': + $this->finalize($instanceId); + break; + + default: + $this->fail('Unknown installation action.'); + } + } catch (Throwable $e) { + $this->fail($e->getMessage()); + } + } + + /** + * Converts the existing installer POST fields into the placeholders used + * by constant_format.tpl. This deliberately mirrors the normal installer + * naming convention, with aliases only where the old form uses a different + * name (for example tzone -> STIMEZONE and paypal-email -> PAYPAL_EMAIL). + */ + private function buildReplacements(): array + { + $templatePath = $this->root . '/install/data/constant_format.tpl'; + $text = file_get_contents($templatePath); + if ($text === false) { + $this->fail('Cannot read installer configuration template.'); + } + + preg_match_all('/%([A-Z0-9_]+)%/', $text, $matches); + $placeholders = array_unique($matches[0] ?? []); + + $aliases = [ + '%STIMEZONE%' => 'tzone', + '%SERVERNAME%' => 'servername', + '%SSTARTDATE%' => 'start_date', + '%SSTARTTIME%' => 'start_time', + '%PAYPAL_EMAIL%' => 'paypal-email', + '%PAYPAL_CURRENCY%' => 'paypal-currency', + '%PLUS_PACKAGE_A_GOLD%' => 'plus-a-gold', + '%PLUS_PACKAGE_A_PRICE%' => 'plus-a-price', + '%PLUS_PACKAGE_B_GOLD%' => 'plus-b-gold', + '%PLUS_PACKAGE_B_PRICE%' => 'plus-b-price', + '%PLUS_PACKAGE_C_GOLD%' => 'plus-c-gold', + '%PLUS_PACKAGE_C_PRICE%' => 'plus-c-price', + '%PLUS_PACKAGE_D_GOLD%' => 'plus-d-gold', + '%PLUS_PACKAGE_D_PRICE%' => 'plus-d-price', + '%PLUS_PACKAGE_E_GOLD%' => 'plus-e-gold', + '%PLUS_PACKAGE_E_PRICE%' => 'plus-e-price', + '%GP_LOCATE%' => 'gp_locate', + '%CRONKEY%' => null, + '%STARTTIME%' => null, + '%ERROR%' => 'error', + ]; + + $defaults = [ + '%SERVERNAME%' => 'TravianZ', + '%SPEED%' => '1', + '%INCSPEED%' => '1', + '%EVASIONSPEED%' => '1', + '%TRADERCAP%' => '1', + '%CRANNYCAP%' => '1', + '%TRAPPERCAP%' => '1', + '%STORAGE_MULTIPLIER%' => '1', + '%MAX%' => '100', + '%LANG%' => 'en', + '%STIMEZONE%' => 'Europe/Bucharest', + '%BEGINNER%' => '43200', + '%REG_OPEN%' => 'true', + '%TS_THRESHOLD%' => '20', + '%MEDALINTERVAL%' => '0', + '%GREAT_WKS%' => 'false', + '%WW%' => 'false', + '%SHOW_NATARS%' => 'false', + '%NATARS_UNITS%' => '100', + '%NATARS_SPAWN_TIME%' => '260', + '%NATARS_WW_SPAWN_TIME%' => '260', + '%NATARS_WW_BUILDING_PLAN_SPAWN_TIME%' => '260', + '%NATARS_WW_START_DELAY%' => '10', + '%NATURE_REGTIME%' => '43200', + '%OASIS_WOOD_MULTIPLIER%' => '40', + '%OASIS_CLAY_MULTIPLIER%' => '40', + '%OASIS_IRON_MULTIPLIER%' => '40', + '%OASIS_CROP_MULTIPLIER%' => '40', + '%CRONLOOP%' => '300', + '%CRONTICK%' => '60', + '%CLEANUPREPORTS%' => '14', + '%CLEANUPCHAT%' => '7', + '%CLEANUPMESSAGES%' => '0', + '%HEROBASEREGEN%' => '10', + '%HEROSILVERPERGOLD%' => '10', + '%HEROSILVERTOGOLD%' => '25', + '%HERORESALL%' => '3', + '%HERORESONE%' => '10', + '%ERRORREPORT%' => '0', + '%GP%' => 'false', + '%GP_LOCATE%' => 'gpack/travian_default/', + '%ALLIANCEBONUSES%' => 'false', + '%PLUSSTATS%' => 'true', + '%PLUSSTATSHOURS%' => '6', + '%PLUSSTATSKEEP%' => '0', + '%USRNMSPECIAL%' => 'true', + '%USRNMMIN%' => '3', + '%USRNMMAX%' => '15', + '%PWMIN%' => '4', + '%WWIMAGE%' => 'true', + '%PROTECTEDPLAYERS%' => '', + '%ASUPPMSGS%' => 'true', + '%ARAIDS%' => 'false', + '%ARANK%' => 'false', + '%CONNECTT%' => '1', + '%DOMAIN%' => '', + '%HOMEPAGE%' => '', + '%SERVER%' => '', + '%LIMIT_MAILBOX%' => '0', + '%MAX_MAILS%' => '100', + '%DEMOLISH%' => 'true', + '%BOX1%' => 'false', + '%BOX2%' => 'false', + '%BOX3%' => 'false', + '%VILLAGE_EXPAND%' => '0', + '%PLUS_TIME%' => '0', + '%PLUS_PRODUCTION%' => '0', + '%PAYPAL_EMAIL%' => '', + '%PAYPAL_CURRENCY%' => 'EUR', + '%PEACE%' => '0', + '%T4_COMING%' => 'false', + ]; + + $replacements = []; + foreach ($placeholders as $placeholder) { + if (array_key_exists($placeholder, $aliases) && $aliases[$placeholder] !== null) { + $field = $aliases[$placeholder]; + } else { + $field = strtolower(trim($placeholder, '%')); + } + + if (isset($_POST[$field]) && !is_array($_POST[$field])) { + $value = (string) $_POST[$field]; + } elseif (array_key_exists($placeholder, $defaults)) { + $value = (string) $defaults[$placeholder]; + } else { + // Preserve the old installer's behavior for optional fields: + // an absent option becomes an empty string rather than leaving + // an unresolved %PLACEHOLDER% in the generated PHP file. + $value = ''; + } + + if ($placeholder === '%STIMEZONE%') { + $tz = explode(',', $value, 2); + $value = $tz[1] ?? $value; + } + + $replacements[$placeholder] = $value; + } + + $replacements['%STARTTIME%'] = time(); + $replacements['%CRONKEY%'] = bin2hex(random_bytes(24)); + + // The registry owns these values; a form cannot redirect an instance + // to another database or table prefix. + $registry = $this->readRegistry(); + if (isset($registry[(string) $_POST['instance_id']])) { + $instance = $registry[(string) $_POST['instance_id']]; + $replacements['%SERVERNAME%'] = (string) ($instance['name'] ?? $replacements['%SERVERNAME%']); + $replacements['%SDB%'] = (string) ($instance['database'] ?? ''); + $replacements['%PREFIX%'] = (string) ($instance['prefix'] ?? ''); + $replacements['%SSERVER%'] = (string) ($_POST['sserver'] ?? 'localhost'); + $replacements['%SPORT%'] = (string) ($_POST['sport'] ?? '3306'); + $replacements['%SUSER%'] = (string) ($_POST['suser'] ?? ''); + $replacements['%SPASS%'] = (string) ($_POST['spass'] ?? ''); + } + + return $replacements; + } + + private function writeConfig(string $instanceId): void + { + $replacements = $this->buildReplacements(); + $path = TravianZInstanceConfigWriter::write($instanceId, $replacements); + + $this->updateStatus($instanceId, 'configured', [ + 'config' => $path, + 'configured_at' => date('c'), + ]); + + $this->redirect('multi.php?instance=' . rawurlencode($instanceId)); + } + + private function loadInstanceBootstrap(): void + { + require_once $this->root . '/GameEngine/Instance/Bootstrap.php'; + } + + private function createStructure(string $instanceId): void + { + global $database; + + require_once $this->root . '/GameEngine/Database.php'; + require_once $this->root . '/GameEngine/Admin/database.php'; + + $result = $database->createDbStructure(); + if ($result === false) { + $this->fail('Database structure creation failed.'); + } + if ($result === -1) { + $this->fail('Database structure already exists or installation requires attention.'); + } + + $this->updateStatus($instanceId, 'structure_created', ['structure_at' => date('c')]); + $this->redirect('multi.php?instance=' . rawurlencode($instanceId)); + } + + private function createWorld(string $instanceId): void + { + global $database; + + require_once $this->root . '/GameEngine/Database.php'; + require_once $this->root . '/GameEngine/Admin/database.php'; + + $result = $database->populateWorldData(); + if ($result === false) { + $this->fail('World data creation failed.'); + } + if ($result === -1) { + $this->fail('World data already exists or installation requires attention.'); + } + + $this->updateStatus($instanceId, 'world_created', ['world_at' => date('c')]); + $this->redirect('multi.php?instance=' . rawurlencode($instanceId)); + } + + private function finalize(string $instanceId): void + { + $registry = $this->readRegistry(); + if (!isset($registry[$instanceId])) { + $this->fail('Instance not found.'); + } + + $instancePath = $this->root . '/instances/' . $instanceId; + if (!is_dir($instancePath) && !mkdir($instancePath, 0775, true) && !is_dir($instancePath)) { + $this->fail('Cannot create instance directory.'); + } + + $installedMarker = $instancePath . '/installed'; + if (file_put_contents($installedMarker, date('c') . PHP_EOL, LOCK_EX) === false) { + $this->fail('Cannot create instance installed marker.'); + } + + $this->updateStatus($instanceId, 'installed', ['installed_at' => date('c')]); + $this->redirect('multi.php?instance=' . rawurlencode($instanceId)); + } + + private function readRegistry(): array + { + if (!is_file($this->instancesFile)) { + return []; + } + + $data = json_decode((string) file_get_contents($this->instancesFile), true); + return is_array($data) ? $data : []; + } + + private function updateStatus(string $instanceId, string $status, array $extra = []): void + { + $registry = $this->readRegistry(); + if (!isset($registry[$instanceId]) || !is_array($registry[$instanceId])) { + return; + } + + $registry[$instanceId]['status'] = $status; + $registry[$instanceId]['updated_at'] = date('c'); + foreach ($extra as $key => $value) { + $registry[$instanceId][$key] = $value; + } + + $dir = dirname($this->instancesFile); + if (!is_dir($dir)) { + @mkdir($dir, 0775, true); + } + + $tmp = $this->instancesFile . '.tmp'; + if (file_put_contents($tmp, json_encode($registry, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES) . PHP_EOL, LOCK_EX) === false) { + $this->fail('Cannot update instance registry.'); + } + if (!rename($tmp, $this->instancesFile)) { + @unlink($tmp); + $this->fail('Cannot activate instance registry update.'); + } + } + + private function redirect(string $location): void + { + header('Location: ' . $location); + exit; + } + + private function fail(string $message): void + { + http_response_code(500); + echo '
' . htmlspecialchars($message, ENT_QUOTES, 'UTF-8') . '
'; + exit; + } +} + +(new TravianZInstanceInstallProcess())->run();