Planetengenerierung

This commit is contained in:
2026-06-15 21:35:25 +02:00
parent 2623da3d39
commit 7a39007712
27 changed files with 2465 additions and 141 deletions
@@ -0,0 +1,74 @@
import cron from 'node-cron';
import pool from '../db.js';
/**
* Initialisiert alle zeitgesteuerten Hintergrund-Aufgaben (Cronjobs) für die Lobby.
* Wird einmalig beim Serverstart aufgerufen.
*/
export const initLobbyCronJobs = () => {
console.log('⏰ Initialisiere Lobby Cronjobs...');
// --------------------------------------------------------------------------
// CRON: Prüfung geschlossener Sektoren auf freie Kapazitäten
// Format: 'Minute Stunde Tag Monat Wochentag'
// '0 4 * * *' bedeutet: Jeden Tag exakt um 04:00 Uhr nachts.
// --------------------------------------------------------------------------
cron.schedule('0 4 * * *', async () => {
console.log('🔍 [CRON] Prüfe geschlossene Sektoren auf freie Startplätze...');
const client = await pool.connect();
try {
// Alle Sektoren finden, deren Registrierung aktuell geschlossen ist
const closedSectorsQuery = await client.query(`
SELECT id, internal_url, name
FROM sectors
WHERE registration_open = false
`);
const closedSectors = closedSectorsQuery.rows;
if (closedSectors.length === 0) {
console.log('✅ [CRON] Keine geschlossenen Sektoren gefunden. Nichts zu tun.');
return;
}
// Jeden geschlossenen Sektor einzeln über das Netzwerk abfragen
for (const sector of closedSectors) {
try {
const response = await fetch(`${sector.internal_url}/api/internal/slots`);
if (!response.ok) {
console.warn(`⚠️ [CRON] Sektor ${sector.name} hat einen Fehler gemeldet (Status: ${response.status}). Überspringe.`);
continue;
}
const data = await response.json();
const freeSlots = data.freeSlots;
// Auswertung der freien Plätze
// Wenn der Sektor wieder mindestens 150 freie Plätze hat, machen wir ihn auf.
if (freeSlots >= 150) {
console.log(`🔓 [CRON] Sektor ${sector.name} hat ${freeSlots} freie Plätze. Öffne Sektor für Neuregistrierungen!`);
await client.query(`
UPDATE sectors
SET registration_open = true
WHERE id = $1
`, [sector.id]);
} else {
console.log(`🔒 [CRON] Sektor ${sector.name} hat nur ${freeSlots} freie Plätze (unter 150). Bleibt geschlossen.`);
}
} catch (fetchError) {
console.warn(`⚠️ [CRON] Netzwerkfehler bei Abfrage von Sektor ${sector.name}:`, fetchError.message);
}
}
} catch (error) {
console.error('❌ [CRON] Kritischer Datenbank-Fehler beim Prüfen der Sektoren:', error);
} finally {
client.release();
}
});
};
@@ -1,5 +1,3 @@
// === File: lobby-backend/src/services/heartbeat.service.js ===
import pool from '../db.js';
// ==============================================================================
@@ -9,49 +7,77 @@ import pool from '../db.js';
export const pingSectors = async () => {
const client = await pool.connect();
try {
const result = await client.query('SELECT id, name, api_url FROM sectors WHERE api_url IS NOT NULL');
const sectors = result.rows;
for (const sector of sectors) {
try {
// Wir nutzen einen AbortController, um blockierende Anfragen
// nach 3 Sekunden hart abzubrechen (Timeout).
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), 3000);
// Nativer, sauberer fetch-Aufruf (Dank NODE_OPTIONS in Docker nun bugfrei)
const response = await fetch(sector.api_url + '/api/health', {
signal: controller.signal
});
clearTimeout(timeoutId);
if (response.ok) {
const data = await response.json();
if (data.status === 'ok') {
// Sektor antwortet korrekt -> Status auf online setzen
await client.query(
"UPDATE sectors SET status = 'online' WHERE id = $1",
[sector.id]
`UPDATE sectors SET
status = 'online',
online_players = $1,
current_players = $2,
planets_count = $3,
max_galaxies = $4,
speed_economy = $5,
research_speed = $6,
fleet_speed_hostile = $7,
fleet_speed_peaceful = $8,
fleet_speed_friendly = $9,
fuel_consumption_factor = $10,
fleet_to_debris_percentage = $11,
defense_to_debris_percentage = $12,
fuel_included_in_debris = $13,
initial_res_premium = $14,
bonus_fields = $15
WHERE id = $16`,
[
data.stats.online_players,
data.stats.total_players,
data.stats.planets_count,
data.config.max_galaxies,
data.config.speed_economy,
data.config.research_speed,
data.config.fleet_speed_hostile,
data.config.fleet_speed_peaceful,
data.config.fleet_speed_friendly,
data.config.fuel_consumption_factor,
data.config.fleet_to_debris_percentage,
data.config.defense_to_debris_percentage,
data.config.fuel_included_in_debris,
data.config.initial_res_premium,
data.config.bonus_fields,
sector.id
]
);
// Log-Ausgabe nur für unseren aktiven Entwicklungs-Sektor,
// um die Konsole sauber zu halten.
} else {
if (sector.name === 'Development') {
//console.log(`[Heartbeat] ✅ Sektor '${sector.name}' ist online!`);
console.warn(`[Heartbeat-Trace] ⚠️ Fehler: JSON-Status war nicht 'ok', sondern:`, data.status);
}
await setSectorOffline(client, sector.id);
}
} else {
// Server erreichbar, liefert aber keinen 2xx Status
if (sector.name === 'Development') {
console.warn(`[Heartbeat-Trace] ⚠️ HTTP-Response nicht OK! Status-Code:`, response.status);
}
await setSectorOffline(client, sector.id);
}
} catch (error) {
// Sektor gar nicht erreichbar (Container down, Netzwerkfehler, Timeout)
if (sector.name === 'Development') {
console.error(`[Heartbeat] ❌ Sektor '${sector.name}' nicht erreichbar:`, error.message);
console.error(`[Heartbeat-Trace] ❌ Crash bei Sektor 'Development'. Grund:`, error.message);
}
await setSectorOffline(client, sector.id);
}
@@ -11,11 +11,18 @@ import pool from '../db.js';
*/
export const getSectorsList = async (accountId) => {
const client = await pool.connect();
try {
// WURDE ERWEITERT: Wir selektieren nun die echten Cache-Werte (online_players, planets_count)
// sowie Konfigurationswerte (initial_res_premium, fleet_speed_hostile) und das Erstelldatum (created_at).
const mySectorsResult = await client.query(`
SELECT
s.id, s.name, s.status, s.speed_fleet, s.speed_economy,
s.id, s.name, s.status, s.is_registration_open,
s.online_players, s.current_players, s.planets_count, s.created_at,
s.speed_economy, s.research_speed, s.max_galaxies,
s.fleet_speed_hostile, s.fleet_speed_peaceful, s.fleet_speed_friendly,
s.fuel_consumption_factor, s.fleet_to_debris_percentage,
s.defense_to_debris_percentage, s.fuel_included_in_debris,
s.initial_res_premium, s.bonus_fields,
las.local_username, las.gender,
las.created_at as joined_at,
las.updated_at as last_played
@@ -27,31 +34,35 @@ export const getSectorsList = async (accountId) => {
const newSectorsResult = await client.query(`
SELECT
id, name, status, speed_fleet, speed_economy
id, name, status, is_registration_open,
online_players, current_players, planets_count, created_at,
speed_economy, research_speed, max_galaxies,
fleet_speed_hostile, fleet_speed_peaceful, fleet_speed_friendly,
fuel_consumption_factor, fleet_to_debris_percentage,
defense_to_debris_percentage, fuel_included_in_debris,
initial_res_premium, bonus_fields
FROM sectors
WHERE id NOT IN (
SELECT sector_id
FROM lobby_accounts_sectors
WHERE lobby_account_id = $1
)
-- Wir zeigen bei den "Neuen Sektoren" nur die an,
-- bei denen die Registrierung noch nicht serverseitig geschlossen wurde.
AND is_registration_open = true
ORDER BY created_at ASC
`, [accountId]);
const mapSectorData = (sector) => ({
...sector,
online_players: Math.floor(Math.random() * 150) + 10,
planets_count: (Math.floor(Math.random() * 150) + 10) * 12
});
// ALTE LOGIK ENTFERNT: Wir brauchen kein mapSectorData mit Math.random() mehr!
// Wir geben die echten Datenbank-Ergebnisse direkt ans Frontend weiter.
return {
mySectors: mySectorsResult.rows.map(mapSectorData),
newSectors: newSectorsResult.rows.map(mapSectorData)
mySectors: mySectorsResult.rows,
newSectors: newSectorsResult.rows
};
} catch (error) {
console.error('[SectorService] Fehler beim Auslesen der Sektordaten:', error);
throw error;
} finally {
client.release();
}
@@ -59,8 +70,8 @@ export const getSectorsList = async (accountId) => {
/**
* Trägt einen Spieler in einen neuen Sektor ein.
* Führt die eigentliche Datenbankoperation durch und wirft spezifische
* Fehler-Objekte, falls Constraints (UNIQUE-Regeln) verletzt werden.
* Komuniziert zuest mit dem Ziel-Sektor und speichert den Psielstand in der lobby erst,
* wenn des Sektor-Backend den Account erfolgreich akzeptiert hat.
*
* @param {string} accountId - Die UUID des Nutzers
* @param {string} sectorId - Die UUID des Sektors
@@ -73,6 +84,61 @@ export const joinSector = async (accountId, sectorId, localUsername, gender) =>
const client = await pool.connect();
try {
const sectorQuery = await client.query(
'SELECT name, api_url, status FROM sectors WHERE id = $1',
[sectorId]
);
if (sectorQuery.rows.length === 0) {
const customErr = new Error('Sector not found');
customErr.code = 'ERR_SECTOR_NOT_FOUND';
throw customErr;
}
const sector = sectorQuery.rows[0];
if (sector.status !== 'online') {
const customErr = new Error('Sector is offline or under maintenance');
customErr.code = 'ERR_SECTOR_OFFLINE';
throw customErr;
}
// Ziel-Endpunkt des Sektors: POST /api/internal/accounts
const sectorInternalUrl = `${sector.api_url}/api/internal/accounts`;
console.log(`[Lobby-to-Server] Sende Registierung für '${localUsername}' an Sektor '${sector.name}' (${sectorInternalUrl})...`);
const response = await fetch(sectorInternalUrl, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
lobby_uuid: accountId,
username: localUsername,
gender: gender
})
});
if (!response.ok) {
const errorData = await response.json().catch(() => ({}));
console.error(`[Lobby-to-Server] Sektor meldet Fehler (Status ${response.status}):`, errorData);
if (response.status == 409) {
const customErr = new Error(errorData.error || 'Conflict in Sector');
customErr.code = errorData.error || 'ERR_LOCAL_USERNAME_TAKEN';
throw customErr;
}
const customErr = new Error('Sector communication failed');
customErr.code = 'ERR_SECTOR_CONNECTION_FAILED';
throw customErr;
}
const sectorResultData = await response.json();
console.log(`[Lobby-to-Server] ✅ Sektor-Account erfolgreich angelegt! Local-UUID: ${sectorResultData.account?.local_uuid}`);
// Wir versuchen, den Datensatz direkt einzufügen.
await client.query(`
INSERT INTO lobby_accounts_sectors (lobby_account_id, sector_id, local_username, gender)