SCPSL Map Template Exporter is a LabAPI server plugin for SCP: Secret Laboratory. After the server finishes generating a facility, the plugin exports the runtime map-generation inputs and the resulting room layout as JSON files.
The exported data is intended for offline tools that need to inspect, visualize, compare, or reproduce the room-generation process without running the game server.
The plugin creates up to two files after map generation:
mapgen-raw-<gameVersion>.json
mapgen-snapshot-<gameVersion>-seed-<seed>.json
For example:
mapgen-raw-14.2.7.json
mapgen-snapshot-14.2.7-seed-123456.json
The raw file contains the version-specific inputs used by the facility generator:
- game version, build type, and active holiday;
- facility grid scale;
- glyph colors, room shapes, specific-room constraints, and allowed rotations;
- zone generators in their original runtime order;
- atlas textures encoded as bottom-left-origin RGBA8 data;
- atlas SHA-256 hashes;
- compatible room candidates in their original order;
- room selection limits and probability weights;
- single-room generator positions and rotations;
- holiday room variants, including serialized null variants;
- every room prefab's connector spawnpoints, including inactive spawnpoints.
Simplified structure:
{
"schemaVersion": 1,
"game": {
"version": "14.2.7",
"buildType": "Release",
"activeHoliday": "None"
},
"exportedAtUtc": "2026-08-06T00:00:00.0000000Z",
"seedUsedForExport": 123456,
"pixelLayout": "RGBA8,row-major,bottom-left-origin",
"gridScale": { "x": 15, "y": 100, "z": 15 },
"glyphShapePairs": [],
"generators": [],
"roomTemplates": []
}An atlas texture is stored as follows:
{
"index": 0,
"name": "HCZ_Atlas_00",
"width": 32,
"height": 32,
"isReadable": true,
"encoding": "rgba8-base64",
"origin": "bottom-left",
"rgbaBase64": "...",
"sha256": "..."
}Decode rgbaBase64 into exactly width * height * 4 bytes. Pixel (x, y) starts
at this byte offset:
(y * width + x) * 4
The four bytes are red, green, blue, and alpha. The first row is the bottom row of the Unity texture.
An atlas generator contains its source atlases and ordered room candidates:
{
"index": 0,
"kind": "atlas",
"runtimeType": "MapGeneration.AtlasZoneGenerator",
"targetZone": { "name": "HeavyContainment", "value": 2 },
"zoneHeight": 0,
"atlases": [],
"compatibleRoomTemplateIds": [
"room-template-000",
"room-template-001"
],
"compatibleRooms": [
{ "index": 0, "templateId": "room-template-000" },
{ "index": 1, "templateId": "room-template-001" }
]
}Single-room generators preserve the serialized rotation and also provide a usable normalized rotation:
{
"index": 3,
"kind": "single-room",
"prefabTemplateId": "room-template-042",
"spawnPosition": { "x": 0, "y": 1000, "z": 0 },
"spawnRotation": { "x": 0, "y": 0, "z": 0, "w": 0 },
"effectiveRotation": { "x": 0, "y": 0, "z": 0, "w": 1 },
"effectiveRotationY": 0
}spawnRotation is the original serialized value. effectiveRotation is normalized.
A zero quaternion is interpreted as the identity quaternion. effectiveRotationY
is the corresponding yaw angle in degrees.
Holiday variants explicitly distinguish a serialized null reference from a resolved room template:
{
"index": 0,
"holiday": { "name": "Christmas", "value": 1 },
"resultState": "serialized-null",
"templateId": null
}Non-null variants use "resultState": "resolved". If a non-null result cannot be
mapped to an exported room template, the export fails instead of writing an ambiguous
null reference.
Each unique room template contains its connector geometry. localPosition and
localRotation are relative to the SpawnableRoom prefab root, even when the
connector is nested below intermediate child objects:
{
"id": "room-template-000",
"prefabName": "LCZ_Straight",
"connectorPoints": [
{
"index": 0,
"runtimeType": "MapGeneration.RoomConnectors.Spawners.SimpleRoomConnectorSpawnpoint",
"activeSelf": true,
"localPosition": { "x": 0, "y": 0, "z": 7.5 },
"localRotation": { "x": 0, "y": 0, "z": 0, "w": 1 },
"fallbackType": { "name": "LczStandardDoor", "value": 3 },
"connectorPriority": 0,
"spawnPriority": 0,
"allowedTypes": [
{ "name": "LczStandardDoor", "value": 3 },
{ "name": "OpenHallway", "value": 6 }
]
}
]
}Advanced spawnpoints use a connectors array. Each entry preserves its serialized
order, chance weight, and allowed connector types:
{
"connectors": [
{
"index": 0,
"chance": 1.5,
"allowedTypes": [
{ "name": "HczStandardDoor", "value": 2 }
]
}
]
}Room templates are collected from every atlas generator, every single-room generator, and all recursively referenced holiday variants. Connector collection does not invoke map generation, atlas interpretation, or random-number APIs. A missing required connector field fails the export instead of publishing an empty placeholder array.
The snapshot file records the facility that the server actually created for one seed. It is the authoritative expected result for testing an offline generator.
It contains:
- the exact seed;
- every runtime
RoomIdentifier; - world position and quaternion rotation;
- normalized Y rotation;
- facility grid coordinates;
- room name, shape, and zone;
- source
SpawnableRoomname and duplicate ID; - every runtime connector's world position and quaternion rotation;
- room-to-room connections;
- the ordered spawn trace for every atlas generator.
Simplified structure:
{
"schemaVersion": 1,
"game": {
"version": "14.2.7",
"buildType": "Release",
"activeHoliday": "None"
},
"exportedAtUtc": "2026-08-06T00:00:00.0000000Z",
"seed": 123456,
"rooms": [],
"spawnTrace": []
}A room entry looks like this:
{
"id": "room-000",
"position": { "x": 0, "y": 0, "z": 15 },
"rotationY": 90,
"rotation": { "x": 0, "y": 0.7071068, "z": 0, "w": 0.7071068 },
"mainCoords": { "x": 0, "y": 0, "z": 1 },
"name": { "name": "HczCrossing", "value": 20 },
"shape": { "name": "XShape", "value": 4 },
"zone": { "name": "HeavyContainment", "value": 2 },
"spawnableRoomName": "HCZ_Crossing",
"duplicateId": 0,
"connectorInstances": [
{
"index": 0,
"runtimeType": "MapGeneration.RoomConnectors.Spawners.SimpleRoomConnectorSpawnpoint",
"worldPosition": { "x": 0, "y": 0, "z": 22.5 },
"worldRotation": { "x": 0, "y": 0.7071068, "z": 0, "w": 0.7071068 },
"connectedRoomIds": ["room-001"]
},
{
"index": 1,
"runtimeType": "MapGeneration.RoomConnectors.Spawners.SimpleRoomConnectorSpawnpoint",
"worldPosition": { "x": 7.5, "y": 0, "z": 15 },
"worldRotation": { "x": 0, "y": 1, "z": 0, "w": 0 },
"connectedRoomIds": ["room-004"]
}
],
"connectedRoomIds": ["room-001", "room-004"]
}Snapshot room IDs are stable within the file and do not use Unity instance IDs.
The exporter verifies that RoomIdentifier.ConnectedRooms is bidirectional and that
the union of each room's connector-level connectedRoomIds matches the room-level
connectedRoomIds.
The spawn trace records the generator's actual ordered decisions:
{
"generatorIndex": 0,
"spawnedIndex": 0,
"coords": { "x": 0, "y": 1 },
"roomShape": { "name": "Straight", "value": 2 },
"specificRooms": [],
"rotationY": 90,
"chosenCandidateTemplateId": "room-template-005",
"instanceRoomId": "room-000"
}Use the raw template file to transform each connector into world space:
worldConnector = room.position + room.rotation * connector.localPosition
Two active connector points represent the same opening when their squared world-space
distance is at most 1. Build bidirectional room edges from those matches and compare
them with both connectorInstances and connectedRoomIds in the seed snapshot. The
connector's X/Z direction from the room center can then be mapped to north, east,
south, or west for visualization.
RoomShape, grid adjacency, and rotationY alone are not authoritative opening data.
Do not hard-code T-shape, curve, or crossing directions from those fields.
- Stop the dedicated server.
- Copy
SCPSL_Template_export.dllinto the LabAPI global plugin directory. - Start the server and check for this log message:
[MapTemplateExporter] Waiting for map generation to finish.
- Allow the server to generate a map. No command is required.
- Read the final file paths from the server console.
On Windows, the global plugin directory is normally under:
%APPDATA%/SCP Secret Laboratory/LabAPI/plugins/global/
The plugin writes files only after SeedSynchronizer.OnGenerationFinished, when room
coordinates and connections have been registered.
LabAPI creates the plugin configuration on first load:
enabled: true
export_raw_templates: true
export_seed_snapshot: true
overwrite_existing_raw: false
pretty_print: false
output_directory: ''| Setting | Meaning |
|---|---|
enabled |
Enables event subscription and exporting. |
export_raw_templates |
Writes the version-level raw template file. |
export_seed_snapshot |
Writes the current seed snapshot. |
overwrite_existing_raw |
Allows replacing an existing raw file for the same game version. |
pretty_print |
Produces indented JSON for manual inspection. |
output_directory |
Uses a custom output directory. Relative paths are resolved from the plugin configuration directory. |
When output_directory is empty, files are written to the plugin configuration
directory's exports subdirectory.
Raw files are not overwritten by default. Snapshot files for the same game version and seed are replaced atomically. Temporary files are removed if serialization or validation fails.