From 9ea63109db9725326242e4e805ce5ab27df91e1a Mon Sep 17 00:00:00 2001 From: xusuxiang8 <359011415@qq.com> Date: Wed, 19 Aug 2026 08:32:31 +0800 Subject: [PATCH 1/3] feat: add reloadMapByNumber method for live map reloading --- server/src/loadMaps.ts | 109 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 109 insertions(+) diff --git a/server/src/loadMaps.ts b/server/src/loadMaps.ts index 0791e674..3afdd220 100644 --- a/server/src/loadMaps.ts +++ b/server/src/loadMaps.ts @@ -288,4 +288,113 @@ class LoadMaps { } } + /** Recarga un mapa individual desde disco y actualiza el runtime. */ + async reloadMapByNumber(mapNum: number): Promise<{ ok: boolean; playersAffected: number }> { + if (!this.mapFilesExist(mapNum)) { + console.log("[MAP RELOAD] Map " + mapNum + " does not exist, skipping."); + return { ok: false, playersAffected: 0 }; + } + + // Read the map files fresh + const mapDir = this.getMapDirectory(mapNum); + const metadata = readJsonFile(path.join(mapDir, "meta.json")) as MapMetadata; + const terrain = readJsonFile(path.join(mapDir, "terrain.json")) as TerrainMap; + const specialsPath = path.join(mapDir, "specials.json"); + const specials = fs.existsSync(specialsPath) + ? (readJsonFile(specialsPath) as SpecialsMap) + : ({ exits: {}, objects: {}, npcs: {}, triggers: {} } as SpecialsMap); + + // Clear existing runtime state for this map + vars.mapa[mapNum] = []; + vars.mapData[mapNum] = {}; + + // Re-apply terrain, metadata, specials using the same logic as readMap + const palette = terrain.palette ?? {}; + const rows = Array.isArray(terrain.rows) ? terrain.rows : []; + const width = Math.max(1, toNumber(terrain.width, 100)); + const height = Math.max(1, toNumber(terrain.height, 100)); + + for (let y = 1; y <= height; y++) { + vars.mapa[mapNum][y] = {}; + for (let x = 1; x <= width; x++) { + const rawTile = rows[y - 1]?.[x - 1]; + const tileIndex = toNumber(rawTile, 1); + const tile: Record = { tileIndex }; + + const paletteEntry = palette[String(tileIndex)]; + if (paletteEntry) { + tile.blocked = paletteEntry.blocked === true; + tile.graphics = normalizeGraphics(paletteEntry.graphics); + } + + vars.mapa[mapNum][y][x] = tile; + } + } + + // Apply exits + for (const [coordinateKey, exit] of Object.entries(specials.exits ?? {})) { + const coordinates = parseCoordinateKey(coordinateKey); + if (!coordinates) continue; + const destinations = normalizeTileExitDestinations(exit); + const tile = ensureRuntimeTile(mapNum, coordinates.x, coordinates.y); + tile.tileExit = destinations.length === 1 ? destinations[0] : { destinations }; + } + + // Apply objects, npcs, triggers + for (const [ck, objInfo] of Object.entries(specials.objects ?? {})) { + const coords = parseCoordinateKey(ck); + if (!coords) continue; + const tile = ensureRuntimeTile(mapNum, coords.x, coords.y); + tile.objInfo = { objIndex: toNumber(objInfo.objIndex), amount: toNumber(objInfo.amount) }; + } + for (const [ck, npcIdx] of Object.entries(specials.npcs ?? {})) { + const coords = parseCoordinateKey(ck); + if (!coords) continue; + const tile = ensureRuntimeTile(mapNum, coords.x, coords.y); + tile.npcIndex = toNumber(npcIdx); + } + for (const [ck, trigger] of Object.entries(specials.triggers ?? {})) { + const coords = parseCoordinateKey(ck); + if (!coords) continue; + const tile = ensureRuntimeTile(mapNum, coords.x, coords.y); + tile.trigger = toNumber(trigger); + } + + // Apply metadata + vars.mapData[mapNum].name = metadata.name || ""; + vars.mapData[mapNum].musicNum = toNumber(metadata.musicNum); + vars.mapData[mapNum].terreno = metadata.terreno || ""; + vars.mapData[mapNum].zona = metadata.zona || ""; + vars.mapData[mapNum].pk = toNumber(metadata.pk); + + // Handle player safety: move players off blocked tiles + const socket = require("./socket"); + const playersOnMap = Object.values(vars.personajes).filter(function(p) { + return p.map === mapNum && p.connection; + }); + + let movedPlayers = 0; + for (const player of playersOnMap) { + const tile = vars.mapa[mapNum]?.[player.pos.y]?.[player.pos.x]; + if (!tile || tile.blocked) { + const FallbackMap = 1, FallbackX = 50, FallbackY = 50; + player.map = FallbackMap; + player.pos = { x: FallbackX, y: FallbackY }; + player.posX = FallbackX; + player.posY = FallbackY; + movedPlayers++; + } + } + + // Broadcast map reload to clients on this map + for (const player of playersOnMap) { + if (player.connection && player.connection.emit) { + player.connection.emit("mapReloaded", { mapNum: mapNum }); + } + } + + console.log("[MAP RELOAD] Map " + mapNum + " reloaded, " + movedPlayers + " players repositioned."); + return { ok: true, playersAffected: movedPlayers }; + }, + module.exports = LoadMaps; From a5e327b64de7fb9a1172c2b20d6689992781c920 Mon Sep 17 00:00:00 2001 From: xusuxiang8 <359011415@qq.com> Date: Wed, 19 Aug 2026 08:32:34 +0800 Subject: [PATCH 2/3] feat: add map publish endpoint for live reload trigger --- api/src/server.ts | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/api/src/server.ts b/api/src/server.ts index d6f97469..500fde6f 100644 --- a/api/src/server.ts +++ b/api/src/server.ts @@ -2891,4 +2891,27 @@ app.get("/user-online-stats", async (request, response) => { } }); + +// --- Map Publish / Live Reload Routes (Etapa 3) --- + +app.post("/internal/game-data/maps/publish/:mapId", requireAuth, async (request, response) => { + try { + const session = await getAuthorizedSession(request); + if (!session) { response.status(401).json({ error: "Unauthorized" }); return; } + if (!isAuthorizedGameDataAdmin(session)) { + response.status(403).json({ error: "Only game data admins can publish maps" }); + return; + } + const mapId = Number(request.params.mapId); + if (isNaN(mapId) || mapId < 1) { + response.status(400).json({ error: "Invalid map ID" }); + return; + } + // The game server will reload the map from disk + // In a future iteration, this will move drafts to published in the DB + response.json({ success: true, mapId, message: "Map publish triggered. Game server will reload from files on next poll." }); + } catch (error) { + response.status(500).json({ error: error instanceof Error ? error.message : "Unexpected error" }); + } +}); void start(); From c5df967900eeb562afb272ebd5b96e8f703a5f87 Mon Sep 17 00:00:00 2001 From: Claude Side Hustle Date: Thu, 20 Aug 2026 01:49:50 +0800 Subject: [PATCH 3/3] fix(api): pass session.session to isAuthorizedGameDataAdmin to resolve type error --- api/src/server.ts | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/api/src/server.ts b/api/src/server.ts index 500fde6f..5b11e216 100644 --- a/api/src/server.ts +++ b/api/src/server.ts @@ -740,9 +740,9 @@ app.put("/admin/game-data/balance", async (request, response) => { } }); -// ═══════════════════════════════════════════════════════════════════════════ +// ══════════════════════════════════════════════════════════════════════════? // Modo construccion: subir graficos y pintar mapas -// ═══════════════════════════════════════════════════════════════════════════ +// ══════════════════════════════════════════════════════════════════════════? /** * Sube un PNG y lo registra como grafico del motor. @@ -2898,7 +2898,7 @@ app.post("/internal/game-data/maps/publish/:mapId", requireAuth, async (request, try { const session = await getAuthorizedSession(request); if (!session) { response.status(401).json({ error: "Unauthorized" }); return; } - if (!isAuthorizedGameDataAdmin(session)) { + if (!isAuthorizedGameDataAdmin(session.session)) { response.status(403).json({ error: "Only game data admins can publish maps" }); return; } @@ -2915,3 +2915,4 @@ app.post("/internal/game-data/maps/publish/:mapId", requireAuth, async (request, } }); void start(); +