Skip to main content
Glama
maps.ts6.37 kB
import { LRUCache } from 'lru-cache'; import { readYAML } from './yaml.js'; import { getString } from './strings.js'; import { readDVPLFile } from '../dvpl/reader.js'; import { Sc2ReadStream, extractMapEntities } from '../streams/sc2.js'; import type { Config } from '../config.js'; import type { GameMap, MapSpawnPoint, MapEntity, TerrainSummary, MapBounds } from '../data/types.js'; import { existsSync, readdirSync } from 'fs'; import { join } from 'path'; interface RawSpawnPoint { respawnNumber: number; points: number[][]; } interface RawMap { id: number; tags?: string; localName: string; avaliableInTrainingRoom: boolean; spriteFrame: number; supremacyPointsThreshold?: number; availableModes: number[]; shadowMapsAvailable?: boolean; assaultRespawnPoints: { allies: RawSpawnPoint[]; enemies: RawSpawnPoint[]; }; levels?: number[]; } interface MapsYAML { maps: Record<string, RawMap>; } interface CachedTerrain { entities: MapEntity[]; summary: TerrainSummary; bounds: MapBounds; } const terrainCache = new LRUCache<number, CachedTerrain>({ max: 20 }); let mapsCache: Map<number, GameMap> | null = null; let maps3dPath: string = ''; const VALID_MAP_TAG = /^[a-zA-Z0-9_-]+$/; export async function loadMaps(config: Config): Promise<void> { if (mapsCache) return; mapsCache = new Map(); maps3dPath = config.paths.maps3d; const data = await readYAML<MapsYAML>(config.paths.maps); for (const [mapTag, rawMap] of Object.entries(data.maps)) { const stringKey = `#maps:${mapTag}:${rawMap.localName}`; const name = getString(stringKey); const gameMap: GameMap = { id: rawMap.id, name: name !== stringKey ? name : formatMapName(mapTag), tag: mapTag, modes: rawMap.availableModes, alliesSpawns: rawMap.assaultRespawnPoints.allies.map(parseSpawnPoint), enemiesSpawns: rawMap.assaultRespawnPoints.enemies.map(parseSpawnPoint), }; mapsCache.set(rawMap.id, gameMap); } } function parseSpawnPoint(raw: RawSpawnPoint): MapSpawnPoint { return { respawnNumber: raw.respawnNumber, points: raw.points, }; } function formatMapName(tag: string): string { return tag .split('_') .map(word => word.charAt(0).toUpperCase() + word.slice(1)) .join(' '); } function calculateBounds(entities: MapEntity[]): MapBounds { let minX = Infinity, maxX = -Infinity; let minZ = Infinity, maxZ = -Infinity; for (const e of entities) { minX = Math.min(minX, e.position[0]); maxX = Math.max(maxX, e.position[0]); minZ = Math.min(minZ, e.position[2]); maxZ = Math.max(maxZ, e.position[2]); } return { minX: Math.round(minX), maxX: Math.round(maxX), minZ: Math.round(minZ), maxZ: Math.round(maxZ), width: Math.round(maxX - minX), height: Math.round(maxZ - minZ), }; } function calculateSummary(entities: MapEntity[]): TerrainSummary { const counts: Record<string, number> = {}; for (const e of entities) { counts[e.type] = (counts[e.type] || 0) + 1; } return { bushes: counts['bush'] || 0, trees: counts['tree'] || 0, buildings: counts['building'] || 0, rocks: counts['rock'] || 0, walls: counts['wall'] || 0, bridges: counts['bridge'] || 0, capturePoints: counts['capture_point'] || 0, totalCover: (counts['bush'] || 0) + (counts['tree'] || 0) + (counts['building'] || 0) + (counts['rock'] || 0), }; } function findSc2Path(mapTag: string): string | null { if (!VALID_MAP_TAG.test(mapTag)) { return null; } const folders = [ `${mapTag.split('_').slice(0, 2).join('_')}_${mapTag.split('_').slice(-1)[0]}`, mapTag, ]; for (const folder of folders) { if (!VALID_MAP_TAG.test(folder)) continue; const sc2Path = join(maps3dPath, folder, `${folder}.sc2`); if (existsSync(sc2Path + '.dvpl')) { return sc2Path; } } const entries = readdirSync(maps3dPath); for (const entry of entries) { if (!VALID_MAP_TAG.test(entry)) continue; if (entry.toLowerCase().includes(mapTag.split('_').slice(-1)[0])) { const sc2Path = join(maps3dPath, entry, `${entry}.sc2`); if (existsSync(sc2Path + '.dvpl')) { return sc2Path; } } } return null; } export async function loadMapTerrain(mapId: number): Promise<CachedTerrain | null> { const map = mapsCache?.get(mapId); if (!map) return null; const cached = terrainCache.get(mapId); if (cached) return cached; const sc2Path = findSc2Path(map.tag); if (!sc2Path) return null; try { const buffer = await readDVPLFile(sc2Path); const arrayBuffer = buffer.buffer.slice(buffer.byteOffset, buffer.byteOffset + buffer.byteLength) as ArrayBuffer; const stream = new Sc2ReadStream(arrayBuffer); const data = stream.sc2(); const entities = extractMapEntities(data); const terrainData: CachedTerrain = { entities, summary: calculateSummary(entities), bounds: calculateBounds(entities), }; terrainCache.set(mapId, terrainData); return terrainData; } catch { return null; } } export function getMaps(): GameMap[] { return mapsCache ? Array.from(mapsCache.values()) : []; } export function getMap(id: number): GameMap | undefined { return mapsCache?.get(id); } export function getMapByName(name: string): GameMap | undefined { if (!mapsCache) return undefined; const lowerName = name.toLowerCase(); for (const map of mapsCache.values()) { if (map.name.toLowerCase() === lowerName || map.tag.toLowerCase() === lowerName) { return map; } } return undefined; } export interface MapWithTerrain extends GameMap { terrain: MapEntity[]; terrainSummary: TerrainSummary; bounds: MapBounds; } export async function getMapWithTerrain(nameOrId: string | number): Promise<MapWithTerrain | undefined> { let map: GameMap | undefined; if (typeof nameOrId === 'number') { map = getMap(nameOrId); } else { map = getMapByName(nameOrId); } if (!map) return undefined; const terrainData = await loadMapTerrain(map.id); if (!terrainData) return undefined; return { ...map, terrain: terrainData.entities, terrainSummary: terrainData.summary, bounds: terrainData.bounds, }; } export function getTerrainCacheStats(): { size: number; max: number } { return { size: terrainCache.size, max: 20 }; }

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/Revenant30102000/wotblitz-mcp'

If you have feedback or need assistance with the MCP directory API, please join our Discord server