Skip to content

Server Facility Authoring

Use an ordinary LabAPI plugin to author the next facility. Load the Anomaly core at server startup, before the first map is generated, and perform all edits on the server game thread. Facility authoring covers Light Containment, Heavy Containment, and Entrance; Surface generation stays native.

EventWhat to author
LabAPI ServerEvents.MapGeneratingSet generation inputs and stage atlas or tile operations with Anomaly.Server.Api.Map.FacilityLayout.
Anomaly MapEvents.LayoutFinalizingInspect the complete selected facility and move, rotate, or remove selected rooms through FacilityRoomPlacement descriptors.
Anomaly MapEvents.RecipeAppliedObserve the applied server layout during native MapGenerated. This event has no arguments and does not permit edits.

Each generation starts with an empty recipe. FacilityLayout staging freezes after all native MapGenerating handlers return. Changes after that boundary throw InvalidOperationException.

LayoutFinalizing runs for unchanged and custom layouts, before final placement is frozen, the authenticated snapshot is published, and native MapGenerated runs. No room-change preregistration is required. Subscribe in your plugin’s Enable and unsubscribe in Disable.

RecipeApplied reports that the frozen server layout has been applied. Remaining native registration stages can still fail and reject the generation. It is not a client-readiness acknowledgement.

This example can stage a tile offset, then inspect a selected room by name and move or remove it. Both examples default to disabled. Choose a tested tile or room and validate the resulting layout before enabling either option.

using System;
using Anomaly.Server.Api.Events.Arguments;
using Anomaly.Server.Api.Events.Handlers;
using Anomaly.Server.Api.Map;
using LabApi.Events.Arguments.ServerEvents;
using LabApi.Events.Handlers;
using LabApi.Features;
using LabApi.Loader.Features.Plugins;
using MapGeneration;
using UnityEngine;
using Logger = LabApi.Features.Console.Logger;
namespace FacilityCompanion;
public sealed class Plugin : Plugin<Config>
{
public override string Name => "Facility Companion";
public override string Description => "Authors facility recipes and selected placements.";
public override string Author => "Example";
public override Version Version => new Version(1, 0, 0);
public override Version RequiredApiVersion => new Version(LabApiProperties.CompiledVersion);
public override void Enable()
{
ServerEvents.MapGenerating += OnGenerating;
MapEvents.LayoutFinalizing += OnFinalizing;
MapEvents.RecipeApplied += OnApplied;
}
public override void Disable()
{
ServerEvents.MapGenerating -= OnGenerating;
MapEvents.LayoutFinalizing -= OnFinalizing;
MapEvents.RecipeApplied -= OnApplied;
}
private void OnGenerating(MapGeneratingEventArgs ev)
{
if (!ev.IsAllowed || !Config.TransformTile) return;
var tile = new Vector2Int(Config.TileX, Config.TileY);
FacilityLayout.MoveRoom(Config.Zone, tile, new Vector3(15f, 0f, 0f));
FacilityLayout.RotateRoom(Config.Zone, tile, 90f);
}
private void OnFinalizing(FacilityLayoutFinalizingEventArgs ev)
{
if (!Config.EditSelectedRoom) return;
foreach (var room in ev.Rooms)
{
if (room.Name != Config.SelectedRoom || room.IsRemoved) continue;
if (Config.RemoveSelectedRoom)
room.IsRemoved = true;
else
room.Position += new Vector3(Config.OffsetX, 0f, 0f);
Logger.Info($"Authored {room.Name} at {room.Zone} {room.Coords} for generation {ev.GenerationId}.");
return;
}
}
private static void OnApplied() => Logger.Info("The server facility layout was applied.");
}
public sealed class Config
{
public bool TransformTile { get; set; }
public FacilityZone Zone { get; set; } = FacilityZone.LightContainment;
public int TileX { get; set; }
public int TileY { get; set; }
public bool EditSelectedRoom { get; set; }
public RoomName SelectedRoom { get; set; } = RoomName.LczArmory;
public bool RemoveSelectedRoom { get; set; }
public float OffsetX { get; set; } = 15f;
}

FacilityLayoutFinalizingEventArgs exposes GenerationId, Seed, and a fixed Rooms list in generation order. The descriptors contain selected-room data, without live LabAPI room wrappers or Unity objects.

Descriptor propertyMeaning
Zone, CoordsStable atlas identity; moving the room does not change it.
Name, ShapeSelected vanilla room type and shape.
OriginalPosition, OriginalRotationYPose at event entry, including earlier recipe edits.
PositionEditable absolute final world position.
RotationYEditable absolute world Y rotation in degrees.
IsRemovedWhether to omit this room and its vanilla contents.

Assign finite positions and rotations synchronously inside the event. Later handlers see earlier edits; the last assignment wins, including setting IsRemoved back to false. LabAPI logs handler exceptions and continues, so valid assignments made before an exception remain. Keep related edits in one handler and coordinate plugins that target the same rooms.

After the event returns, retained descriptors remain readable but their setters reject changes. Tasks, delayed callbacks, and MapGenerated handlers cannot edit them. Selected identities of unaffected rooms remain stable: moving or removing a room does not reroll later room choices.

A removed room does not instantiate its geometry, lights, colliders, or owned network objects. Its space remains available for your authored layout; removal does not deliver a replacement model. Custom geometry, colliders, and gameplay objects require their own implementation. Surviving neighbours still run their connector and fallback-door logic.

FacilityLayout room operations address a zone and its interpreted two-dimensional atlas tile coordinates. These coordinates are not world positions or indices from a live room list. Choose them from the matching game’s atlas/profile. CurrentEntries enumerates staged operation keys, rather than every selected tile.

MoveRoom sets a world-space offset from the tile’s vanilla placement; repeated calls replace that offset. RotateRoom sets an absolute world-space Y rotation, including in Entrance. Position and rotation changes to the same tile combine. RemoveRoom skips the tile. These operations do not repair doors, elevators, navigation, or room-specific scripts.

SetAtlasPixel accepts a RoomShape, a specific RoomName, or a raw Color32 glyph. Typed overloads reject shapes or rooms without a known glyph. ClearAtlasPixel writes the empty glyph. Atlas edits operate on temporary copies; the source assets are restored afterward. The atlas index must exist and pixel coordinates must fit its actual dimensions. Editing one alternative atlas does not force the seeded generator to choose it.

FacilityGlyphs.TryGetColor resolves a shape or room to its first matching glyph. AllPairs exposes detached GlyphPairInfo values containing Color, Shape, SpecificRooms, and Rotations; inspect these when multiple glyph variants exist. LogPalette writes the live palette to the server log. Read the palette during MapGenerating or later, after the facility scene creates its interpreter.

ClearRecipe clears both staged room operations and atlas edits. ClearAtlasEdits clears only pixel edits. Staging is shared by all companions, so coordinate ownership and handler ordering before using either clear operation.

Room coordinates fit signed 16-bit integers; atlas indices fit an unsigned byte; pixel coordinates fit unsigned 16-bit integers and the actual texture dimensions. Transform values must be finite. A snapshot supports at most 4,096 recipe entries, 4,096 pixel edits, and 4,096 planned rooms, within a combined 132 KiB payload cap.

Application protocol v5 authenticates the generation ID, seed, generation mode, recipe, atlas edits, complete selected placement plan when required, expected room descriptors digest, and snapshot digest. Install matching client and server builds. The client waits for the authenticated snapshot and matching native seed before generating its map. Initial and late clients receive the same frozen layout. This bootstrap is independent of optional PNG/OGG content consent.

A missing bootstrap times out after 30 seconds. Invalid or conflicting snapshots, invalid atlas targets, and generation failures reject the affected connection. A valid snapshot establishes agreed generation data; it does not establish that an arbitrary authored layout is playable.

Entrance alignment is derived from the two HCZ/Entrance linking checkpoints in each zone. During the recipe stage, both linking checkpoints must remain present so that alignment can be resolved. In LayoutFinalizing, they may be removed after alignment has been calculated; other planned placements retain that alignment.

The game’s native room lookup rounds positions onto a 15 by 100 by 15 metre grid, then tries limited vertical raycasts. Whole-zone checkpoint shifts should preserve the 15-metre horizontal grid. A fractional shift can leave SCP-079 speakers or interactables without a room and cause native generation to fail.

Final placements support arbitrary yaw, and structure objects use the full planned rotation. Grid-aligned shifts and quarter-turn rotations are useful starting constraints, but they do not guarantee connected doors, unobstructed geometry, or a playable map. Room edits do not move neighbouring rooms or repair door topology.

Validate authored layouts with matching native client/server runs, including Entrance, native round restart, and late join. Check generation, matching placements, and ordinary room interactions. Compilation and synchronized digests do not establish those gameplay properties.

Continue to the API Index or return to Server Companion Quickstart.