Compatibility helper that converts MissingComponent stubs of type TerrorTown.FuncDoorRotating into the engine Door component. It reads serialized JSON from stubs, maps properties (lock, auto-close, pivot, target angle, partner link) onto a new Door component, and wires paired doors by GameObject id.
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Text.Json;
using System.Text.Json.Nodes;
using Sandbox.Mapping;
namespace Sandbox;
/// <summary>
/// Converts TerrorTown <c>FuncDoorRotating</c> stubs (loaded as <see cref="MissingComponent"/>)
/// into <see cref="Door"/> so maps work without the TerrorTown package.
/// </summary>
public static class TerrorTownDoorCompat
{
const string FuncDoorRotatingType = "TerrorTown.FuncDoorRotating";
public static void ConvertAll( GameObject mapRoot )
{
if ( !mapRoot.IsValid() )
return;
var pendingPartners = new List<(Door Door, string PartnerId)>();
var convertedByGoId = new Dictionary<string, Door>();
var missing = mapRoot.Components
.GetAll<MissingComponent>( FindMode.EverythingInSelfAndDescendants )
.ToArray();
foreach ( var stub in missing )
{
if ( !TryConvert( stub, out var door, out var partnerId ) )
continue;
convertedByGoId[door.GameObject.Id.ToString()] = door;
if ( !string.IsNullOrWhiteSpace( partnerId ) )
pendingPartners.Add( (door, partnerId) );
}
foreach ( var (door, partnerId) in pendingPartners )
{
if ( convertedByGoId.TryGetValue( partnerId, out var partner ) )
door.LinkedDoor = partner;
}
}
static bool TryConvert( MissingComponent stub, out Door door, out string partnerId )
{
door = null;
partnerId = null;
var json = stub.GetJson();
if ( json is null )
return false;
var type = json["__type"]?.GetValue<string>();
if ( type != FuncDoorRotatingType )
return false;
var go = stub.GameObject;
if ( !go.IsValid() )
return false;
if ( go.GetComponent<Door>().IsValid() )
{
stub.Destroy();
return false;
}
door = go.AddComponent<Door>();
door.Mode = Door.DoorMode.Rotating;
door.IsUsable = GetBool( json, "AllowDirectUse", true );
door.IsLocked = GetBool( json, "StartsLocked", false );
door.StartOpen = GetBool( json, "StartsOpen", false );
door.AutoClose = GetBool( json, "AutoReset", false );
door.AutoCloseDelay = GetFloat( json, "ResetTime", 1f );
door.OpenAwayFromPlayer = GetBool( json, "OpenAwayFromActivator", true );
var hint = json["HintText"]?.GetValue<string>();
if ( !string.IsNullOrWhiteSpace( hint ) )
door.OpenTooltipTitle = hint;
var targetAngle = TargetAngleFromRotation( GetVector3( json, "TargetRotation", new Vector3( 0, 90, 0 ) ) );
door.TargetAngle = targetAngle;
var openDuration = GetFloat( json, "OpenDuration", 1f );
door.Speed = openDuration > 0f ? targetAngle / openDuration : targetAngle;
var pivotLocal = GetVector3( json, "Pivot", Vector3.Zero );
if ( !pivotLocal.IsNearlyZero( 0.001f ) )
{
var pivotGo = new GameObject( true, "DoorPivot" );
pivotGo.SetParent( go );
pivotGo.LocalPosition = pivotLocal;
door.Pivot = pivotGo;
}
partnerId = TryGetIdString( json["PartnerObject"] );
stub.Destroy();
return true;
}
static float TargetAngleFromRotation( Vector3 targetRotation )
{
var absX = MathF.Abs( targetRotation.x );
var absY = MathF.Abs( targetRotation.y );
var absZ = MathF.Abs( targetRotation.z );
var angle = MathF.Max( absX, MathF.Max( absY, absZ ) );
return angle > 0f ? angle : 90f;
}
static bool GetBool( JsonObject json, string key, bool fallback )
{
var node = json[key];
if ( node is null )
return fallback;
try
{
return node.GetValue<bool>();
}
catch
{
return fallback;
}
}
static float GetFloat( JsonObject json, string key, float fallback )
{
var node = json[key];
if ( node is null )
return fallback;
try
{
return node.GetValue<float>();
}
catch
{
return fallback;
}
}
static Vector3 GetVector3( JsonObject json, string key, Vector3 fallback )
{
var node = json[key];
if ( node is null )
return fallback;
if ( node is JsonValue value )
{
var text = value.GetValue<string>();
if ( TryParseVector3( text, out var parsed ) )
return parsed;
}
return fallback;
}
static bool TryParseVector3( string text, out Vector3 result )
{
result = default;
if ( string.IsNullOrWhiteSpace( text ) )
return false;
var parts = text.Split( ',' );
if ( parts.Length < 3 )
return false;
if ( !float.TryParse( parts[0].Trim(), NumberStyles.Float, CultureInfo.InvariantCulture, out var x ) )
return false;
if ( !float.TryParse( parts[1].Trim(), NumberStyles.Float, CultureInfo.InvariantCulture, out var y ) )
return false;
if ( !float.TryParse( parts[2].Trim(), NumberStyles.Float, CultureInfo.InvariantCulture, out var z ) )
return false;
result = new Vector3( x, y, z );
return true;
}
static string TryGetIdString( JsonNode node )
{
if ( node is null )
return null;
if ( node is JsonValue value )
{
if ( value.GetValueKind() == JsonValueKind.Null )
return null;
try
{
var text = value.GetValue<string>();
return string.IsNullOrWhiteSpace( text ) ? null : text;
}
catch
{
return null;
}
}
return null;
}
}