Skip to content

Source logic and ambience

Research This page collects what was measured on 2026-10-04 so the decisions at the end can be made from facts. The importer’s first two logic phases are built; As built: phase 1 and phase 2 say what they took from them. The rest is open.

The Source importer brings a map’s geometry, lighting and collision across, and leaves its behavior behind: doors, buttons, trains and triggers arrive as static brush nodes, and soundscapes are only counted. The goal of this note is an imported Source map that is interactive: its doors open, its buttons press, its trains run, and it sounds like itself.

The rule for the design is the one the rest of DigitalHeaven follows: a DigitalHeaven concept, not a Source clone. Where Valve’s model and ours disagree, we lead with Valve’s. The wire format is Source’s own and is the thing every imported map is written in.

Everything below was read from code and the map type docs, not from memory.

FactEvidence
An object’s outputs are bound to an ordered action list; the list runs top to bottom, a delay blocks the rest of itLogicAction.cs (the action model, call at 37, delay at 71), LogicRuntime.RunActionList (LogicRuntime.cs:354)
A call names its target by the target’s unique map name; one target per call, no wildcard, no class fallbackLogicCallAction.Target (LogicAction.cs:39), _byName lookup (LogicRuntime.cs:373)
A call carries nothing but a static boolean or one number or one color, written in the map. An output carries no valueLogicParamType (LogicSchema.cs:12), LogicCallAction.Param and Value (LogicAction.cs:47)
The activator flows through a whole run, including across delays, as a wire id. It cannot be named as a targetRunActionList(... activator ...), LastActivator (LogicRuntime.cs:419)
Calls are synchronous. A chain deeper than 8 hops is dropped with a warningLogicConstants.MaxCallDepth (LogicConstants.cs:18), ApplyInput (LogicRuntime.cs:400)
There is no per-connection delay or fire count. A delay is an action in the list. There is no “fire once”LogicConnection (LogicAction.cs:104) has output, actions and nothing else
Delays are quantized up to whole simulation ticksSecondsToTicks (LogicRuntime.cs:332)
An object carrying a dh.trigger is a logic source with the two touch outputs and the inputs enable, disable and toggle. A trigger collider with no dh.trigger fires nothingdh.trigger, TriggerBehavior (LogicBehaviors.cs)

The whole list of kinds that take part in wiring is ComponentDescriptions.BuiltIn (ComponentDescriptions.cs:430). The runtime creates a node for each in CreateKindNode (LogicRuntime.cs:464).

DH kindInputsOutputsEvidence
buttonUse (E)onPressedButtonEntity (LogicEntities.cs:15), ButtonNode (LogicRuntime.cs:627)
dooropen, close, toggleonFullyOpen, onFullyClosedDoorEntity (LogicEntities.cs:39), DoorNode (LogicRuntime.cs:661)
timerfireonTimerTimerEntity (LogicEntities.cs:79), TimerNode (LogicRuntime.cs:756)
mapStartnoneonMapStartMapStartEntity (LogicEntities.cs:97), MapStartNode (LogicRuntime.cs:777)
andGate, orGate, notGatesetA, setB (booleans)onTrue, onFalse (edges only)LogicEntities.cs:111-144, GateNode (LogicRuntime.cs:788)
pressurePlatenoneonStartTouch, onEndTouch (occupancy 0 to 1, and 1 to 0)LogicEntities.cs:154
lightturnOn, turnOff, togglenoneLogicEntities.cs:587
lightProbeVolume, lightProbeGroupon/off, setTint, setIntensitynoneComponentDescriptions.cs:268-333
physicsProp (and an objectInstance carrying one)nonenoneLogicEntities.cs:185
objectInstance, voxelVolumenonenoneComponentDescriptions.cs:378-427

There is no counter, compare, case, branch, random or relay kind. The nearest things are the boolean gates (a level, not a number) and the timer, which is the only relay: it delays fire by a fixed time and is the only way to make one wire out of another.

mltn’s belief was that a door attaches to arbitrary geometry and objects, and that moving platforms use the same tech. Half of that is true.

QuestionAnswerEvidence
Can a door attach to any node or mesh?No. A door is its own entity that renders a textured box: size and a material slot, installed as a kinematic box collider. It cannot be placed on a GLB node, a mesh, or an objectDoorEntity.Size and Material (LogicEntities.cs:47-54), DoorNode builds its visual from door.Size (LogicRuntime.cs:679)
Does anything ride a door?Yes. Every object under it in the hierarchy is carried, by any path (folders, brushes, plates, buttons, lights, other doors). That is how a lift plate or a lit platform worksRiding a door, EntityHierarchy (EntityHierarchy.cs:11-31), MoverRides (MoverRides.cs:8-15)
Can a GLB node ride a door?Not when this was written; yes since step 8d. A part whose override parent names a mover rides it, drawn, collided and ridden on. The carry was computed over the logic order (entities with a wire index). A manifest node of an imported GLB is not in it, and dh.editNode has no parentEntityHierarchy.Of walks LogicOrder (EntityHierarchy.cs:99-129); the dh.setParent and dh.importObject patches parent objects, not manifest nodes (Patch.cs:104-119, Patch.cs:267)
Is there a moving-platform kind?A door is it. A door wired to reverse itself at each end is the bouncing platform the logic-map demo shipsEntity kinds
Is there a rotation mover?Not when this was written; yes since step 8d (motion: "rotate", an axis in the object’s frame and an angle about its origin, and the carry is a pose). A door’s whole motion was axis * distance * fraction. The carry that children take is a Vector3, so a rotation could not be carried even if one existedMoverPose.Slide (MoverPose.cs:23), EntityHierarchy.Carried returns Vector3 (EntityHierarchy.cs:157)
Is there a path or track mover?No. There is no waypoint, path or track object anywhere in Core or the engineComponentDescriptions.BuiltIn (ComponentDescriptions.cs:430)
What may move at all?EntityHierarchy.MovesOnItsOwn is entity is DoorEntity, with a comment that the next self-moving kind is added there and nowhere elseEntityHierarchy.cs:89
Does a mover have speed control, locks, blocked outputs?No. One duration, no setSpeed, no lock, no push-back when blockedDoorEntity.MoveDurationSeconds (LogicEntities.cs:70)

The good news is the trajectory machinery: a door replicates its open fraction and the signed rate it changes by, the client predicts the next ticks from them, the collider is kinematic, and a rider stands on it with a real velocity (MoverPose.cs:11-56). A rotating or path mover can reuse every piece of that. The part that has to change is what a mover moves (a node as well as a box) and what the carry is (a transform as well as a displacement).

QuestionAnswerEvidence
Is there an ambient sound or soundscape entity?No. No map entity plays a sound. dh.sound is “designed but not yet implemented”Planned: dh.sound, ComponentDescriptions.BuiltIn has no audio kind
Is there a sound cue entity?No. SoundCueOverlay is a developer readout of the movement feel’s cues, not a map objectSoundCueOverlay.cs:70
What does the audio stack do?A device and mixer, 2D one-shots, and one 3D spatialized looping source (CreateSource)audio.mdx line 15, IAudioSystem.CreateSource (IAudioSystem.cs:90)
Is there reverb, occlusion, DSP presets, a 3D one-shot?No. “Ambisonics, occlusion/reverb … streaming” are listed as plannedaudio.mdx:190; AudioOneShots notes there is no positional one-shot API (AudioOneShots.cs:9)
Can a map name a sound at all?No. No field of the map format, and no entity, references an audio fileMapDefinition.cs has no sound member; ComponentDescriptions.BuiltIn has no audio kind

An entity’s outputs are keys in its entity lump entry. The value is one connection:

OnPressed "slidingdoor,Open,,0,-1"

The five fields are target, input, parameter, delay, timesToFire. CEventAction::CEventAction parses them (src/game/server/cbase.cpp:117, in the Source SDK 2013 repository, hereafter the SDK). Facts that matter here:

  • Separator. Older maps use a comma. Newer compilers use the escape character 0x1B (VMF_IOPARAM_STRING_DELIMITER), so a target name can contain a comma. The parser looks for 0x1B first and falls back to a comma (cbase.cpp:133-136). Among the thirteen maps both occur: 1486 ESC connections and 1147 comma ones.
  • An empty input becomes Use. An empty parameter means “pass the output’s own value on”. An empty delay is 0. timesToFire of 0 or empty means “always”, and -1 is the same (EVENT_FIRE_ALWAYS) (cbase.cpp:126-190).
  • Firing posts to a queue. CBaseEntityOutput::FireOutput (cbase.cpp:306) adds one event per connection to g_EventQueue at delay (a zero delay is still queued, and runs at the next queue service), carrying the activator and the caller (the entity that owns the output). A connection with a finite timesToFire counts down and is removed when it reaches zero (cbase.cpp:384-391). So delays are per connection and independent, not a blocking sequence.
  • Targets are late-bound and fan out. When an event is due, the queue calls FindEntityByName in a loop and delivers the input to every match, then falls back to treating the target as a classname if nothing matched, and logs “target entity not found” otherwise (cbase.cpp:1005-1070).
  • Special target names are resolved by FindEntityProcedural (src/game/server/entitylist.cpp): !player (player 1), !pvsplayer, !activator, !caller, !picker (what the player is looking at) and !self (the entity searching). Any other !name is an error. (!speechtarget, !enemy and !friend exist for NPC code and are not in this function.) Wildcards (*) are matched by NameMatches, which is not in the files read here.
  • No recursion cap. Because everything goes through the queue, a long chain is not stack-limited the way DigitalHeaven’s synchronous 8-deep chain is.

Inputs and outputs below were read from DEFINE_INPUTFUNC and DEFINE_OUTPUT in the SDK (doors.cpp, buttons.cpp, func_movelinear.cpp, trains.cpp, pathtrack.cpp, triggers.cpp, logicentities.cpp, logicrelay.cpp, logicauto.cpp, soundscape.cpp, sound.cpp, fire.cpp). Rows marked with an asterisk are not read from the SDK: their inputs are the ones the thirteen maps actually send. Kill is a base entity input every class takes, and is left out of the lists.

ClassWhat it doesKey inputsKey outputs
func_doorBrush that slides along a direction by a lip-adjusted distanceOpen, Close, Toggle, Lock, Unlock, SetSpeed, SetToggleStateOnOpen, OnClose, OnFullyOpen, OnFullyClosed, OnBlockedOpening, OnBlockedClosing, OnUnblockedOpening, OnUnblockedClosing, OnLockedUse
func_door_rotatingThe same, rotating about the brush’s origin by an angle and axisas func_dooras func_door
prop_door_rotating*A model door that swings (the maps set a hinge axis, distance, ajarangles, opendir, speed)Open, Close, Lock, UnlockOnOpen, OnLockedUse
func_movelinearSlides along an angle for a distance; settable positionOpen, Close, SetPosition (0 to 1), SetSpeedOnFullyOpen, OnFullyClosed
func_rotating*Spins continuously (maxspeed, fanfriction)Stop, Toggle, SetSpeednone
func_tracktrainRides a path_track chain with accelerationStartForward, StartBackward, Stop, Resume, Reverse, Toggle, SetSpeed, SetSpeedDir, SetSpeedReal, SetSpeedDirAccel, TeleportToPathTrackOnStart, OnNextPoint
func_trainThe older train: moves from path_corner to path_cornerStart, Stop, ToggleOnStart
path_trackA waypoint, with a speed change, an alternate path and a branchInPass, InTeleport, EnablePath, DisablePath, TogglePath, EnableAlternatePath, DisableAlternatePath, ToggleAlternatePathOnPass, OnTeleport
path_cornerA waypoint for func_train and NPCsnoneOnPass
func_buttonA pressed brush, with a lock and a return delayPress, PressIn, PressOut, Lock, UnlockOnPressed, OnIn, OnOut, OnDamaged, OnUseLocked
func_rot_button, momentary_rot_buttonA wheel or lever that turns; the momentary one reports a positionSetPosition, SetPositionImmediately, Enable, Disable, Lock, UnlockOnPressed, OnUnpressed, OnFullyOpen, OnFullyClosed, Position (a value), OnReachedPosition
trigger_onceA volume that fires once and removes itselfthe base trigger’s: Enable, Disable, DisableAndEndTouch, Toggle, TouchTest, StartTouch, EndTouchOnTrigger, plus the base trigger’s touch outputs below
trigger_multipleA volume that fires on entry and exit, with a delay between triggersas trigger_onceOnTrigger, OnStartTouch (per toucher), OnStartTouchAll (first in), OnEndTouch (per toucher), OnEndTouchAll (last out), OnTouching, OnNotTouching
trigger_hurtDamages what stands in itthe base trigger’sOnHurt, OnHurtPlayer, and the touch outputs
trigger_teleportMoves what enters to an info_teleport_destinationthe base trigger’sthe touch outputs
trigger_pushPushes what is inside along a directionthe base trigger’sthe touch outputs
logic_autoFires when the map loadsnoneOnMapSpawn, OnNewGame, OnLoadGame, OnMapTransition, OnBackgroundMap
logic_relayA relay that can be disabled and cancelledTrigger, Enable, Disable, Toggle, CancelPendingOnTrigger, OnSpawn
logic_timerFires at an interval, fixed or randomEnable, Disable, Toggle, ResetTimer, RefireTime, FireTimerOnTimer, OnTimerHigh, OnTimerLow
logic_branchA boolean to testSetValue, SetValueTest, Toggle, ToggleTest, TestOnTrue, OnFalse
logic_compareCompares a value with a referenceSetValue, SetValueCompare, SetCompareValue, CompareOnLessThan, OnEqualTo, OnNotEqualTo, OnGreaterThan, OutValue
logic_caseRoutes a value to one of sixteen outputs, or picks one at randomInValue, PickRandom, PickRandomShuffleOnCase01 to OnCase16, OnDefault
math_counterA bounded numberAdd, Subtract, Multiply, Divide, SetValue, SetValueNoFire, SetHitMax, SetHitMin, GetValue, Enable, DisableOutValue, OnHitMax, OnHitMin, OnGetValue
logic_randomNot in the SDK 2013 (later branches have logic_random_outputs). logic_case’s PickRandom is the 2013 way
filter_*An object that accepts or rejects an activator by name, class, team, mass, damage type, or a combination (filter_multi)OnPass, OnFail
env_soundscapeA sound environment that the nearest one (in range, with a line of sight) selectsEnable, Disable, ToggleEnabledOnPlay
ambient_genericA positional or global sound, loop or one-shotPlaySound, StopSound, ToggleSound, FadeIn, FadeOut, Pitch, Volumenone
env_sprite*A glow or spriteShowSprite, HideSprite, ToggleSprite, Colornone
env_spark*A burst of sparksnone sent by these mapsnone
env_fireA fire that burns and spreadsEnable, Disable, StartFire, Extinguish, ExtinguishTemporaryOnIgnited, OnExtinguished
point_spotlightA beam that lights nothing but draws a coneLightOn, LightOffnone

These are the maps in the workspace: gm_construct, gm_flatgrass, gm_bigcity_improved, gm_pole, gm_prison, gm_suburb_construct_v1, gm_johnnyisland, gm_fork, gm_mallparking, squidgame_facility, summer morning 2004, d1_canals_01 and d1_trainstation_02. Every count was read from each BSP’s entity lump through the importer’s own BspFile, mounted over Garry’s Mod (which carries the Half-Life 2 content).

ClassInstancesHeaviest maps
prop_door_rotating620gm_suburb 217, squidgame 214, gm_fork 63, gm_prison 50
func_door_rotating230gm_prison 123, squidgame 36, gm_suburb 31, summer morning 20
func_door116squidgame 67, gm_prison 30, trainstation 10
func_movelinear5squidgame 5
func_rotating5gm_pole 3, squidgame 2
func_tracktrain7d1_canals 4, gm_prison 2, gm_mallparking 1
func_train0
path_track148gm_bigcity 60, d1_canals 35, gm_suburb 32, gm_construct 14
path_corner84d1_canals 52, trainstation 24, gm_construct 8
func_button216gm_bigcity 93, squidgame 58, summer morning 39, gm_prison 10
func_rot_button0
momentary_rot_button3squidgame 3
trigger_multiple99trainstation 30, gm_suburb 26, squidgame 20, summer morning 12
trigger_once51d1_canals 31, trainstation 19
trigger_hurt18gm_bigcity 9, squidgame 4
trigger_teleport97gm_bigcity 92
trigger_push7squidgame 5, d1_canals 2
trigger_soundscape83gm_bigcity 82
logic_auto19d1_canals 5, trainstation 4, squidgame 3
logic_relay42d1_canals 16, trainstation 14, squidgame 12
logic_timer25summer morning 15, trainstation 4
logic_branch9squidgame 5, gm_fork 2
logic_compare5squidgame 5
logic_case41squidgame 19, summer morning 14
math_counter17squidgame 11, d1_canals 4
logic_random0
filter_*10trainstation 5, d1_canals 4
env_soundscape199gm_fork 99, squidgame 27, summer morning 28, gm_construct 19
env_soundscape_proxy44squidgame 44
env_soundscape_triggerable9gm_bigcity 9
ambient_generic439summer morning 296, squidgame 41, d1_canals 32, mallparking 27
env_sprite1270squidgame 489, gm_prison 301, summer morning 294
point_spotlight309summer morning 263, squidgame 27
env_spark, env_fire3, 3d1_canals, squidgame

Also present, and out of scope here: 2206 prop_dynamic, 567 prop_physics, 391 prop_physics_multiplayer, 213 func_areaportal, npc_template_maker 36, logic_choreographed_scene 30, scripted_sequence 31, and the Half-Life 2 NPC and AI entities.

2633 connections in total. 613 carry a delay, 122 fire exactly once, 1486 use the ESC separator and 1147 a comma.

OutputCountInput (into any class)Count
func_button.OnPressed585ambient_generic.PlaySound226
trigger_once.OnTrigger163func_areaportal.Close and Open108, 107
logic_relay.OnTrigger153trigger_teleport.Enable and Disable91, 91
trigger_multiple.OnEndTouchAll129ambient_generic.StopSound78
trigger_multiple.OnStartTouchAll114func_door.Open, Close, Toggle66, 47, 10
path_track.OnPass105logic_case.PickRandom, PickRandomShuffle82, 22
logic_auto.OnMapSpawn88env_sprite.ShowSprite, ToggleSprite, HideSprite43, 39, 38
trigger_multiple.OnStartTouch69light.Toggle, TurnOn, TurnOff35, 5, 5
logic_case.OnCase01 to OnCase16290func_tracktrain.SetSpeed, StartForward, Stop26, 16, 9
func_door.OnClose, OnFullyOpen, OnOpen17, 16, 15math_counter.Add, SetValue, Subtract15, 14, 7
math_counter.OnHitMax, OutValue, OnHitMin20, 14, 11logic_relay.Trigger, Enable, Disable68, 14, 6
func_movelinear.OnFullyClosed, OnFullyOpen15, 11logic_timer.Enable, Disable, ResetTimer24, 11, 7
logic_compare.OnEqualTo20func_button.Unlock, Press, Lock23, 16, 12

Target forms: 57 use !self, 47 use !activator (16 AddContext, 14 Kill, 12 SetTrack), 112 name a wildcard, and none use !player, !caller or !picker. 52 name something that is not an entity in the lump (a classname target, or an entity another map or a template spawns). Seventeen inputs go to lua_run, a Garry’s Mod entity that runs Lua and cannot be ported.

The biggest single pattern is not a game mechanic. 107 trigger_multiple pairs each close and open a func_areaportal, which is Source’s visibility culling and has no meaning here. The next is 85 buttons that enable and disable a teleport trigger.

60 soundscape names (counted per map) are referenced across nine of the maps (the other four reference none). Every one resolves in scripts/soundscapes_manifest.txt and its files, or in the map’s own pakfile (scripts/soundscapes_<map>.txt), except Automatic in d1_canals_01 and one unnamed or Nothing entry in squidgame.

MapSoundscapes referenced
gm_constructconstruct.outside, construct.lake, construct.garage, construct.inside
gm_bigcity_improvedcity.park, city.windy, city.util_plaza, city.warehouse, city.under, city.canals, city.wasteplant_inside, city.util_goop
gm_polepole
gm_johnnyislandcoast.generic_ambient_01
gm_forkfork.main, fork.shore, fork.high_altitude, and six interior and tunnel scapes
squidgame_facility19, squid.lobby, squid.hopscotch, squid.divingcave and so on
summer morning 2004nt, amb, d1_canals.general_watery_tunnel
d1_canals_01d1_canals_citystart, watercanal, traintunnel, traincanal, citadel, tunnel01, junkyard, general.concrete_quiet
d1_trainstation_02d1_trainstation.Turnstyle, Interrogation, TerminalSquare, AppartmentCourtyard, Appartments, QuietCourtyard

Their rules, counted across the referenced soundscapes: 80 playlooping, 31 playrandom, 44 playsoundscape (one soundscape nesting another), 53 dsp, 36 with a position (a sound from a named point), 154 volume, 111 pitch, 39 soundlevel. 930 soundscapes are defined in the manifest and map files read.

The ambient sounds those maps wire up are a mix of file paths (ambient/atmosphere/indoor1.wav, ambient/levels/canals/tunnel_wind_loop1.wav, a few .mp3 music files from a map’s own folder) and game-sound names that a game_sounds script resolves (Canals.d1_canals_01_watchit, d3_citadel.shield_loop, song_plaza). Of 439 ambient_generic entities, 125 share one file (ambient/misc/flour_light.wav, a fluorescent hum) and the rest are mostly one of a kind.

Status is what DigitalHeaven does with it today: ✅ direct, 🟡 partial, ❌ none. The last column counts instances across the thirteen maps.

Source classWhat it doesDH todayWhat DH needsUsed
func_doorSliding brush door🟡 a door slides, but it is a textured box, not the brush’s meshA mover that takes a node’s mesh; lock, speed, onOpen and onClose outputs116
func_door_rotatingSwinging brush door❌ no rotationA mover that rotates about a pivot230
prop_door_rotatingSwinging model door❌ no rotation, and no mover on a modelThe same rotation mover, on a mesh object620
func_movelinearSlide with a settable position🟡 door slides between two ends onlyA mover with a setPosition fraction input5
func_rotatingContinuous spin🟡 a spin mover (a rate and a spin-up time) built; SetSpeed and Reverse have no inputA rate input, if a map needs one5
func_tracktrain, func_trainRides a waypoint chain❌A path mover over waypoints, with speed and direction inputs7
path_track, path_cornerWaypoint, with pass output and branches❌A waypoint object and a onPass output; branches can wait232
func_buttonPressed brush🟡 a button is a textured box, and has no lockA button that takes a node; lock and unlock; onIn, onOut can wait216
func_rot_button, momentary_rot_buttonTurning lever or wheel, with a position❌A held, graded interaction that writes a number3
trigger_onceOne-shot volume🟡 a dh.trigger fires touch edges and takes enable, disable and toggle, with no onceonce on a connection; a kill input51
trigger_multipleRepeating volume🟡 a dh.trigger’s onStartTouch and onEndTouch fire per toucher, as Source’s OnStartTouch and OnEndTouch do, and it takes enable, disable and toggleA filter; the OnStartTouchAll edge99
trigger_hurtDamage volume❌ no damage action in the logic schemaA damage action or component, if DH has health18
trigger_teleportMoves the toucher❌ no teleport actionA teleport action, and an object that is a destination97
trigger_pushPushes the toucher❌ no pushA force volume7
trigger_soundscapeForces a soundscape inside❌A soundscape zone (below)83
logic_autoFires on load✅ mapStartNothing (OnNewGame, OnLoadGame have no DH meaning)19
logic_relayRelay with enable, disable, cancel🟡 timer delays; andGate can disable by levelA relay with enable, disable, cancelPending (the DH timer cannot cancel)42
logic_timerRepeating timer, fixed or random🟡 timer is one-shot per fireA repeating mode, a random range, enable, disable, reset25
logic_branchA boolean to test🟡 the gates hold levels but compare nothingA boolean branch with set and test9
logic_compareNumber against reference❌ no number stateA number, with comparison outputs5
logic_caseRoute a value, or pick at random❌A case or random object with N outputs41
math_counterBounded number❌A counter17
filter_*Accept or reject an activator❌ the activator is always a playerOnly if DH gets more kinds of activator10
env_soundscape and friendsAmbient bed❌A soundscape (below)252
ambient_genericPositional or global sound❌ no map sound objectAn emitter object with play, stop, toggle, fade439
env_spriteLight glow🟡 a light fixture can swap materialA billboard or glow, with show, hide, toggle1270
env_spark, env_fireEffects❌A particle effect object6
point_spotlightVisible beam🟡 a spot light lights, but draws no beamA volumetric cone or a beam sprite309
func_areaportalVisibility culling❌ and not wantedDrop at import213

Three conclusions follow from the counts.

  1. Rotation is the largest gap. 850 of the 971 doors rotate, and 620 of those are models. A slide-only door covers 121.
  2. A mover must take a node, not a box. Every one of those 1,100-odd doors and buttons is a brush or a model that already exists in the imported geometry. A mover that draws its own box would leave the original static mesh behind.
  3. The logic itself is small. The relays, timers, counters and cases total about 140 entities in 13 maps, and the pattern that matters (button, relay, door, sound) uses only Trigger, Open, Close, PlaySound and a delay.

Each is open. The recommendation follows Valve where the two models disagree, and prefers a generic DigitalHeaven component to a Source-shaped one.

1. How a connection is written Open

Section titled “1. How a connection is written ”

DigitalHeaven calls one entity by a unique name with a static parameter, in a blocking list, with no fire count. Source posts independent connections, each with its own delay and count, late-bound to every entity of a name.

OptionCostCost to the importer
A. Keep the model; resolve everything at import. !self becomes the owner’s name, wildcards and classname targets expand to one call per match, each Source connection becomes one DH connection [delay, call]none in the enginea map of imported wires can be large (112 wildcard targets, some matching many)
B. Add a target pattern to call (door*), resolved when the call runs, and a reserved activator tokena small change in RunActionListnone
C. A group or tag on objects that a call can namea new concept, authored in the editorthe importer writes tags

Add times (a fire count, omitted for unlimited) to a connection in any case: 122 connections need exactly one fire and nothing here can say it.

Recommendation: A, plus times, plus a reserved activator target. The map is static, so resolving at import costs nothing at runtime and matches the “validated at compile time” rule the wire model already has. Only SetTrack on !activator (12 uses) needs a runtime name, and a reserved activator token is cheap, since the activator already flows through every run. Revisit B when something spawns at runtime (point_template, 13 uses, is out of scope). Also decide whether the delay-0 queue matters: Source never overflows on a deep chain and DigitalHeaven stops at 8. A logic_case or logic_relay chain in a real map can reach that, so the importer should count chain depth and warn.

2. Numbers: counter, compare, case Open

Section titled “2. Numbers: counter, compare, case ”

DigitalHeaven carries booleans and edges, never a number.

OptionShape
A. Three kinds: counter, compare, case, each a Source cloneeasy to import, three things to learn
B. One counter kind: a number with min, max, start, inputs add, subtract, set, reset, and outputs onHitMin, onHitMax, onChanged, onEqual, onLess, onGreater against a compareTo field. A separate random kind (N outputs, pick and shuffle)two kinds, covers math_counter, logic_compare and logic_case’s random half
C. Map variables and the reserved set and if actions the action model already leaves room forthe most general, and a programming surface the schema says it avoids

Whether an output carries a value is a separate question. Source passes it (OutValue, InValue), and the maps use it only about 30 times (14 OutValue, 11 InValue, 2 Position). Without it, compare can only compare to a fixed field.

Recommendation: B, with no output values yet. logic_case’s 16-way value routing (the 290 OnCaseNN connections) is almost entirely PickRandom, so a random kind covers it. A number-valued output can be added later without changing a map written before it. Keep branch as the gates’ boolean level plus a test input, which is what logic_branch is.

3. Movers: slide, rotate, path Open

Section titled “3. Movers: slide, rotate, path ”

Today’s door is slide-only and moves its own box. The carry is a displacement.

OptionShape
A. Grow door in place: add rotation and a pivot to its fieldssmallest change, keeps the box
B. Add a kind per motion: rotor, trackTrain, beside dooreasy to read, three near-copies of the trajectory code
C. One generic mover component that moves whatever it sits on (a box, a node, an object), whose motion is one of slide, rotate or path, reusing the existing fraction-and-rate replication; door is a mover with a box, and the carry becomes a transformone concept, one replication, and it takes a node

Rotation forces the carry to become a full transform (EntityHierarchy.Carried returns a Vector3 now), and a node must be able to hold a mover or be parented to an entity, because the doors Source maps ship are already in the geometry.

Recommendation: C. It is the only option that reaches the 850 rotating doors, since it is the only one that moves the existing mesh. Build it in this order: (1) a mover that takes a node and rotates about a pivot (covers func_door_rotating, prop_door_rotating, func_rotating and momentary_rot_button), (2) slide on a node, (3) a setPosition fraction input, (4) a path over waypoints with speed and direction inputs for the 7 trains. The same decision applies to button: it should also sit on a node.

TargetUsesOptionRecommendation
!self57A: rewrite to the owner’s name at importA
wildcard name*112A: expand at import. B: a pattern in callA
a classname52 unresolvedA: expand at import over entities of that classA
!activator47reserved activator token, or dropA reserved token, once something uses it
!caller0reserved tokenAdd with activator, since it costs the same
!player, !picker0player token laterNot now

5. Soundscapes and stock tracks Open

Section titled “5. Soundscapes and stock tracks ”

Source selects one soundscape for the player: the nearest in range with a line of sight, or the one a trigger_soundscape forces, and it crossfades. A soundscape is a script of rules: loops (playlooping), random one-shots (playrandom), other soundscapes (playsoundscape), and a DSP preset.

DigitalHeaven has no sound object, no dh.sound type, and one 3D source.

OptionShape
A. A generic ambience zone: a sound set (loops, random one-shots with a time range, nested sets), a position and radius, and a rule that the nearest or forced zone wins and crossfades. A positional emitter for ambient_generic with play, stop, toggle and fade inputsone new asset type plus two components; the importer translates the script
B. Flatten: every env_soundscape becomes one looping emitter, rules droppedsmall, and 80 of the rules are loops, but it loses randomness and nesting, which 75 of the rules use
C. Leave ambience outnothing built, and Source maps are silent

Stock tracks. The sounds are the game’s files, so DigitalHeaven must not ship them. Options: (A) the importer copies the referenced files out of the user’s mounted game into their local pallet, the way materials already import, never into anything distributed; (B) reference them by mount at runtime; (C) support only sounds the map’s own pakfile carries.

Recommendation: A for the zone, with B’s emitter shipped first, and A for tracks. dh.sound is a prerequisite either way, and the emitter is the part 439 entities need now. The importer then resolves a soundscape’s file paths and game_sounds names against the mount into a local pallet, so the recipe ships and the audio does not.

StepCovers
1. Mover on a node: rotate, slide, setPosition; button on a node; times; lockfunc_door, func_door_rotating, prop_door_rotating, func_button, 5 func_movelinear, 5 func_rotating
2. Importer writes the wiring: triggers, relays, timers, logic_auto, the static resolution of Decision 1everything that is a button, a door and a delay
3. counter, random, branchmath_counter, logic_case, logic_branch, logic_compare
4. dh.sound, the emitter, then the zoneambient_generic, env_soundscape
5. Path mover and waypoints7 trains
6. Teleport, damage, push97, 18 and 7 triggers

Left out on purpose: NPC, AI, choreography and scripted_sequence; func_areaportal; lua_run; filter_* until an activator can be something other than a player.

Built 2026-10-05, after stage 3 step 6 made the logic kinds components. It is the order of work’s step 2, the importer writing the wiring, over the components DigitalHeaven already has. The platform page says what each class becomes.

  • Decision 1 is A. Targets are resolved at import: a name fans out to every match, a trailing * is a prefix, a name that matches nothing is a class name, and !self is the entity firing. !activator and the other runtime names are dropped. No fire count and no activator token were added.
  • A door is a bodiless mover over its piece. The planner’s shape: the mover object carries nothing but the dh.mover and the wiring, and the brush piece stays a part of the world instance with parent naming the mover, static: false and its own hulls, so the door stays solid and shut and needs no re-import once a mover carries a model part (step 8). The compiler refused a mover with no body until an engine fix lets one stand when something is parented under it. SourceLogic.MoverBody is the one switch to the other shape, a box collider on the mover and no hulls on the piece, which builds today but leaves the drawn door where it is while its collision moves.
  • Gates stand in for the missing state. A once-only thing (a trigger_once, a relay with flag 1, a button with wait -1) fires through an or gate whose A it sets true: the first edge passes, and no later write is an edge. An enable is an and gate’s A, and a pulse on B (true, then false) passes only while A is on. That gives a disabled relay and a timer exact Enable and Disable. A relay that starts enabled is a zero-delay timer instead, because a gate starts with A off and setting it on the first logic tick would race a logic_auto that triggers the relay on the same tick.
  • Not built: a lock, a trigger’s enable, a fire count, per-toucher trigger edges, OnOpen and OnClose (DigitalHeaven fires on arrival), a button’s in and out positions, random timer intervals, and every class in the engine asks below.

What the dropped and left-static entries need, counted per map. Entities are left static; connections are dropped.

Featuregm_prisongm_mallparkingsquidgame_facilityd1_trainstation_02gm_forkgm_bigcity_improvedgm_construct
Continuous rotate (func_rotating), entities only, measured in phase 2; built since, so none is left static0000000
Path mover (func_tracktrain, path_track, path_corner)6 / 104 / 50 / 024 / 20 / 060 / 6022 / 13
Teleport0 / 00 / 01 / 04 / 40 / 092 / 1700 / 0
Damage, push0 / 03 / 09 / 80 / 00 / 09 / 00 / 0
Counter, case, branch, compare0 / 00 / 040 / 2425 / 212 / 101 / 51 / 5
Sound11 / 627 / 041 / 2315 / 139 / 28 / 10 / 0
Model animation (SetAnimation)0 / 00 / 00 / 120 / 10 / 20 / 00 / 0
Lock (Lock, Unlock)0 / 00 / 00 / 440 / 00 / 00 / 20 / 0
Switching on and off (Enable, Disable on a trigger or an enabled relay)0 / 40 / 03 / 91 / 130 / 00 / 00 / 1
Removing an entity (Kill)0 / 00 / 00 / 10 / 100 / 00 / 00 / 0
Graded interaction, position input0 / 00 / 03 / 190 / 00 / 00 / 00 / 0
Fire count0005000

Each cell reads entities / connections. The fire count row counts connections that were mapped and fire every time.

Built 2026-10-06, after stage 3 step 8d gave dh.mover its rotate motion and let a mover carry model parts and instances. It is the rotating half of Decision 3’s step 1. The platform page has the rows.

  • A func_door_rotating is the phase 1 door with a turn. The same bodiless mover at the entity origin carries its piece, wired by the same code (SourceLogic.BrushDoor). The axis is AxisDir’s, and Source adds the turn to one of the entity’s angles, which is exactly a turn about a fixed axis in the entity’s frame: world Z for a yaw, the entity’s X for a roll, and its unrolled Y for a pitch (SourceRotaryMotions.LocalAxis, proved for arbitrary angles in the tests). A door that spawns open stands where the map built it and spawns open with the mover’s startOpen; only the obsolete start-open flag, which calls the spawn end closed, stands the mover and its piece at the far end.
  • A prop_door_rotating is a mover over its instance. The model is built around its hinge, so the mover stands at the prop’s origin and the instance is filed under it. Its hulls leave the map’s collision object for a dh.collider on the instance, in the instance’s own frame, so the carry moves them. The same is true of every prop a mover carries, a prop parented to a func_door included, which before this phase stood solid where it was placed while its picture moved.
  • One open direction. A two-way door opens away from the player in Source. DigitalHeaven’s mover has one open pose, so each door opens the way Source opens it with no activator: a brush door by its distance, a prop door forward. The one exception is a double door: a slave that opens both ways turns to its master’s side, since a use opens both away from the player. Two unlinked brush leaves still swing opposite ways (as on gm_prison’s double doors), because nothing in the map pairs them.
  • func_rotating was left static here. A mover wired to restart itself either turns back the way it came, or sweeps a whole turn backwards in one tick, which the client draws as a flicker through the interpolated half turn and which flings anyone standing on it. It needed a continuous rotate mode, and has one now: see the next section.
  • Not built: opening away from the player, ajar prop doors (they spawn closed), the hardware bodygroup, SetRotationDistance and SetSpeed, blocked doors, func_rot_button, and a lock.
MapBrush moversOf which rotatingProp doorsDouble doorsTwo-way doorsfunc_rotating
gm_prison1531235041040
gm_mallparking221826——0
squidgame_facility10836214——2
d1_trainstation_021006260
gm_fork0063——0
gm_bigcity_improved2119——0
gm_construct000000

Built 2026-10-06, once dh.mover gained its spin motion (a rate in degrees a second and a duration that is the spin-up and spin-down time). It is the continuous half of Decision 3’s step 1; the platform page has the row.

  • A func_rotating is a rotating door with a rate instead of an angle. The same bodiless mover at the entity origin carries its piece and its hulls (SourceLogic.CarryingMover, shared with the doors), and its axis is the rotating doors’ (SourceRotaryMotions.LocalAxis): CFuncRotating::Spawn sets a unit QAngle angular velocity, roll for flag 4, pitch for 8, yaw otherwise.
  • Spin-up is fanfriction, under flag 16 only. SpinUpMove adds 0.2 * maxspeed * friction per 0.1 s step, so it takes 1 / (2 * friction) seconds, with friction fanfriction / 100 (1 when 0). Without Acc/Dcc Source changes speed at once, so the duration is the minimum. SpinDown is half as fast (0.1 * maxspeed * friction), which DigitalHeaven’s single time cannot say.
  • Start on is the map start. Flag 1 wires a dh.mapStart to open. Source waits 0.2 s first; DigitalHeaven does not.
  • Inputs. Start, Stop and Toggle map. SetSpeed and Reverse are dropped, and counted in connections.droppedByInput.
  • Not built: a rate or a direction input, the hurt-on-touch flag (noted), and the fan sound.

On squidgame_facility it maps both func_rotatings, and none is left static.

gm_prison and d1_trainstation_02 were imported with their props; the other five were counted without them, so their prop doors are counted but not paired (—), and their two-way counts would add the prop doors.