一键导入
lvl-format
Subspace/Continuum .lvl map file format — binary layout, embedded tileset, eLVL metadata, tile encoding, and how the existing loaders work.
用 Codex 或 Claude 帮你安装 复制这段 Prompt,粘贴到 Codex、Claude 或其他助手里,让它检查 Skill 页面并帮你完成安装。
菜单
Subspace/Continuum .lvl map file format — binary layout, embedded tileset, eLVL metadata, tile encoding, and how the existing loaders work.
用 Codex 或 Claude 帮你安装 复制这段 Prompt,粘贴到 Codex、Claude 或其他助手里,让它检查 Skill 页面并帮你完成安装。
基于 SOC 职业分类
ArenaModule interface (api/) — arena-composition contract per ADR-0008. ModuleCatalog, ModuleLoader, and ArenaModuleSystem are live. Author new ArenaModule classes under infinity-server/src/main/java/infinity/modules/<category>/ and register them in ModuleCatalog.
Work with Subspace Infinity arena settings — the per-arena `arena.groovy` files under `zone/arenas/`, the Groovy `conf/` preset fragment library (section/shipSection/shipSections DSL), the recursive `include` directive, the `SettingsSystem` typed accessors, and the `~loadArena`/`~swapMap` commands. Use when adding or reading settings, creating a new arena, or splitting settings fragments.
Explains the api ↔ server ↔ client layering of Subspace Infinity — which module new code belongs in, how data flows between layers, which packages live in which Gradle module, and the SimEthereal + RMI boundaries. Use when deciding where a new file should live, tracing data across layers, or explaining the project structure.
Overview of Subspace Infinity project structure, tech stack, and conventions. Use when understanding the codebase, finding files, or learning project patterns.
Create Zay-ES EntityComponent classes for the ECS architecture. Use when creating new components, data holders, or entity attributes.
Debug Entity-Component-System issues including EntitySet problems, memory leaks, and component queries. Use when troubleshooting ECS bugs or entity processing issues.
| name | lvl-format |
| description | Subspace/Continuum .lvl map file format — binary layout, embedded tileset, eLVL metadata, tile encoding, and how the existing loaders work. |
Source of truth: infinity-server/src/main/java/infinity/map/ — BitMap.java, LevelFile.java, LevelLoader.java.
[BMP tileset] optional — 304×160 px, 8-bit indexed, RLE-8 compressed
[eLVL metadata] optional — only present when BMP reserved field == 49720
[tile data] required — 4 bytes per non-empty tile, until EOF
BitMap.readBitMap() reads the first 14-byte file header:
BM → file has an embedded BMP tilesetelvl → file has ONLY eLVL data (no BMP); ELvlOffset = 0m_size == 49718 (normal BMP, no eLVL)reserved field == 49720 → eLVL follows the BMP; ELvlOffset = 49720Standard Windows BMP, 8-bit color, RLE-8 compression:
Tile layout in atlas:
n (1-indexed) → tileIndex = n - 1col = tileIndex % 19, row = tileIndex / 19(col * 16, row * 16)BitMap.getImageData() returns int[] in ARGB format, stored top-to-bottom
(index 0 = top row of image, index (height-1)*width = bottom row).
Starts at offset ELvlOffset with a 12-byte header:
"elvl" 4 bytes magic tag
total_size 4 bytes LE int — total metadata section size in bytes
reserved 4 bytes (0)
Followed by variable-length chunks until total_size bytes consumed:
type 4 bytes ASCII tag ("ATTR", "REGN", or unknown)
chunk_len 4 bytes LE int
data chunk_len bytes
padding 0–3 bytes to align to 4-byte boundary
ATTR chunk — key=value string, e.g. NAME=Trench Wars
REGN chunk — RLE-encoded region bitmask (see Region.java)
After the BMP (and optional eLVL), remaining bytes are tile entries:
4 bytes per tile (little-endian int):
bits 31–24 tile ID (1–190; 0 = empty, skip)
bits 23–12 y coordinate (0–1023)
bits 11–0 x coordinate (0–1023)
Decode:
int i = readLittleEndianInt();
int tile = (i >> 24) & 0xFF;
int y = (i >> 12) & 0x3FF;
int x = i & 0x3FF;
Map is 1024×1024 cells. Cells with tile == 0 are empty space.
| Class | Role |
|---|---|
BitMap | Parses BMP header + RLE-8 pixel data; getImageData() → int[] ARGB; readBitMap(trans) — pass true to make black transparent |
LevelLoader | JME3 AssetLoader for .lvl; returns LevelFile; falls back to Textures/Tilesets/default.bmp if no embedded BMP |
LevelFile | Holds parsed map: getMap() → short[1024][1024], getTileSet() → java.awt.Image, getTileSetPixels() → int[] ARGB, tile write/save support |
BitMapLoader | JME3 AssetLoader for standalone .bmp files |
// In MapSystem.initialize():
assetLoader.registerLoader(LevelLoader.class, "lvl", "lvz");
LevelFile res = (LevelFile) assetLoader.loadAsset("Maps/trench.lvl");
short[][] map = res.getMap(); // [x][y] tile IDs
assets.registerLoader(LevelLoader.class, "lvl");
LevelFile lvl = (LevelFile) assets.loadAsset("Maps/trench.lvl");
int[] pixels = lvl.getTileSetPixels(); // ARGB, top-to-bottom
int width = lvl.getTileSetImageWidth(); // 304
int height = lvl.getTileSetImageHeight(); // 160
// Flip vertically (JME3 V=0 is bottom; pixels[0] is top row)
// Treat black as transparent
ByteBuffer buf = BufferUtils.createByteBuffer(width * height * 4);
for (int row = height - 1; row >= 0; row--) {
for (int col = 0; col < width; col++) {
int argb = pixels[row * width + col];
int r = (argb >> 16) & 0xFF;
int g = (argb >> 8) & 0xFF;
int b = argb & 0xFF;
int a = (r == 0 && g == 0 && b == 0) ? 0 : ((argb >> 24) & 0xFF);
buf.put((byte)r).put((byte)g).put((byte)b).put((byte)a);
}
}
buf.flip();
Image jmeImage = new Image(Image.Format.RGBA8, width, height, buf, ColorSpace.sRGB);
Texture2D tex = new Texture2D(jmeImage);
tex.setMinFilter(Texture.MinFilter.NearestNoMipMaps);
tex.setMagFilter(Texture.MagFilter.Nearest);
See BlockGeometryIndex.registerTileMaterialFromLevel() for the live implementation.
.lvl stores coordinates as (x, y) both 0–1023worldOffset (multiple of 1024) by MapSystem(x, y) in the lvl → single world cell at (worldOffset.x + x, 1, worldOffset.z + y)GameServer.expandCollidersForTiles so ships pass through them.Constants live in infinity.map.MapTypes. Each category is handled in LegacyMapProjector (extracted from MapSystem in the backlog cleanup bundle).
| IDs | Category | How it's placed |
|---|---|---|
| 0 | empty | skipped |
| 1–161 (except 20) | normal visible tile | cell: TILE_TYPE_BASE + id - 1, solid |
| 20 | border | same cell treatment as normal tiles (rarely present in .lvl data) |
| 162–165 | vertical door | entity: MapFactory.createDoor |
| 166–169 | horizontal door | entity: MapFactory.createDoor |
| 170 | turf flag | entity: MapFactory.createTurfStationaryFlag |
| 171 | safe zone | cell, visible, solid |
| 172 | goal area | cell, visible, solid |
| 173–175 | flyover | cell, visible, passthrough (null collider) |
| 176–190 | flyunder | cell, visible, passthrough (null collider) |
| 216 | small asteroid | entity: MapFactory.createAsteroidSmall |
| 217 | medium asteroid | entity: MapFactory.createAsteroidMedium |
| 218 | asteroid end / wormhole2 | entity: MapFactory.createWormhole2 |
| 219 | space station | cell, visible, solid |
| 220 | wormhole | entity: MapFactory.createWormhole |
| 221–228 | team/enemy bricks, goals, flags, prizes | SSB runtime-only — not in .lvl data |
| 240–255 | invisible / special behavior tiles | cell: INVISIBLE_BLOCK_TYPE, collidable but no visuals |
Entity-backed tiles don't write a world cell — the entity owns its shape, rendering, and behavior. Their placement Vec3d is still tracked in the returned coordinates set so removeBlocksFromLegacyMap can zero the slot regardless of what happens to be there.
assets/Maps/ — maps are on the JME3 asset path, loadable by both server (AssetLoaderService) and client (app.getAssetManager()).