← Home SEEBIE90 DOCS
The SOFLAM Integration Core mod is published under the Arma Public License (APL)Example code is free to useSuggestions: Discord to the author

SOFLAM Integration Core

Assembling and the gunner seat Changed

Build the turret from two parts and let the engine do the spawning. And a fake door that has to be placed on real ground.

In plain wordsTwo items become one turret in a single engine action; the player sits in a seat; the seat needs a door, and the door must stand on real ground.

Two parts, one engine action

The folded tripod (GTG_SOFLAM_Tripod_Carried.et) is a vanilla multi-part deployable. You drop it, look at it, and “Assemble” builds it together with a SOFLAM from your inventory (PEQ1_SOFLAM.et, part name “SOFLAM Designator”). A ghost shows the result first. Both parts are consumed and the engine spawns GTG_SOFLAM_Tripod_Turret.et. The component on the carried tripod:

Example code – free to useGTG_SOFLAM_Tripod_Carried.et (excerpt)
SCR_MultiPartDeployableItemComponent "{61B46B9C8F38F759}" {
 m_aVariants {
  SCR_DeployableVariantContainer "{6A48B5C100000310}" {
   m_sReplacementPrefab "{6A48B5E000005010}Prefabs/Items/Tripod/GTG_SOFLAM_Tripod_Turret.et"
   m_sPreviewObject "{6A48B5E000005020}Prefabs/Items/Tripod/GTG_SOFLAM_TripodDeployPreview.et"
   m_aRequiredElements {
    SCR_RequiredDeployablePart "{6A48B5C100000311}" {
     m_sPrefab "{F2A55A212EBAF03F}Prefabs/Items/SOFLAM/PEQ1_SOFLAM.et"
     m_sPartName "SOFLAM Designator"
    }
   }
   m_fMaxAllowedTilt 35
  }
 }
}

“Pack Up Tripod” gives both back, separately. Vanilla solves that with a second multi-part component on the assembled object: the replacement prefab is the main part, m_aAdditionalPrefabs are the others (copied from the vanilla M3 tripod with the M2HB):

Example code – free to useGTG_SOFLAM_Tripod_Turret.et (excerpt)
SCR_MultiPartDeployableItemComponent "{6A48B5C000006210}" {
 m_bRemoveAllItemsWhenDeleted 1
 m_aVariants {
  SCR_DeployableVariantContainer "{6A48B5C000006211}" {
   m_sReplacementPrefab "{6A48B5E000005009}Prefabs/Items/Tripod/GTG_SOFLAM_Tripod_Carried.et"
   m_aAdditionalPrefabs {
    SCR_AdditionalDeployablePart "{6A48B5C000006062}" {
     m_sPrefab "{F2A55A212EBAF03F}Prefabs/Items/SOFLAM/PEQ1_SOFLAM.et"
    }
   }
   m_bUsePartRotationAndPosition 1
  }
 }
 m_eSurfaceObservationBehaviour DISMANTLE
}
What changedThe earlier route had two steps: deploy the tripod, then “Mount Designator”, and “Collect Designator” to take it off again. Both actions are gone from the shipped prefabs. Collecting left a tripod with no device on it, and in that state “Use SOFLAM” disappears. The classes (GTG_SOFLAM_MountAction, GTG_SOFLAM_PickUpAction, GTG_SOFLAM_SwapPrefabAction, GTG_SOFLAM_UseAction) are still in the script folder; no shipped prefab uses them.

Why a weapon spawned by script arrives empty

The turret holds the device in its weapon slot: WeaponTemplate points to PEQ1_SOFLAM_Mounted.et, and the engine creates it on every machine. The route through a script first (spawn the weapon prefab, put it in the slot) produced a device that existed for the server and for nobody else. The cause was not the spawn but the prefab: the mounted device inherited Weapon_Base, an abstract base with a weapon animation component but no animation graph. Without a graph the entity initialisation aborts and the engine returns an empty shell: no model, no components, and no replication component. Nothing to replicate, so no client ever saw it.

Example code – free to usefrom the server log of 2026-09-06 (shortened)
 SpawnEntityPrefab  {6A48B5E000005012}PEQ1_SOFLAM_Mounted.et
  Create entity ('GenericEntity')            <- without a model
   ANIMATION (E): anim controller initialization data is invalid (graph , node MasterControl, instance )

 Server  slot 1 | weapon 1 | designator false | Rpl false | children 9
 Client  slot 1 | weapon 0 | prefab -         |           | children 8

The fix is to give the device the animation set the turret already runs. Weapon and turret must use the same set, or grips and seat do not match:

Example code – free to usePEQ1_SOFLAM_Mounted.et (excerpt)
WeaponAnimationComponent "{60B4EA76EB15F6E0}" {
 AnimGraph "{C727601154428730}Assets/Weapons/HeavyWeapons/workspaces/M2HB_tripod.agr"
 AnimInstance "{26749793D7EDEB6D}Assets/Weapons/HeavyWeapons/workspaces/M2HB_Weapon_tripod.asi"
 StartNode "WeaponMasterControl"
 AutoVariablesBind 1
}
NoteRule that came out of it: if a freshly created weapon has no components on a dedicated server, look at its inheritance chain and its animation graph before you blame the server or the spawn path.

The legacy route: re-parent the same entity

The code that mounts a device by hand is still in GTG_SOFLAM_MountAction, and the technique is worth knowing. The carried device leaves the inventory and becomes a child of the ball head. It keeps everything: laser code, vision mode, a running target report, its replication id. The obvious alternative – delete the carried device and spawn a “mounted” prefab – is simpler, but throws exactly that state away and needs a second prefab.

Example code – free to useGTG_SOFLAM_MountAction.c (excerpt)
item.SetWorldTransform(worldMat);							// 1. world position of the seat first

// 2. then attach, and let the engine convert that world transform to a local one
support.AddChild(item, -1, EAddChildFlags.RECALC_LOCAL_TRANSFORM);

SetItemPhysicsActive(item, false);							// SimulationState.NONE: otherwise it falls off the head
PitfallThe order matters. With AUTO_TRANSFORM first the transform you just set is immediately overwritten; with NONE the device never follows the tripod. RECALC_LOCAL_TRANSFORM after setting the world position is the combination that does both.

Do it on the authority only (if (m_RplComponent && m_RplComponent.IsProxy()) return;). If a proxy also did it, the device would hang on the tripod on one machine and not on the others. And switch the physics off with SCR_PhysicsHelper.ChangeSimulationState(item, SimulationState.NONE), not just to inactive: an inactive body stays in the collision world and its position still belongs to the physics, not to the hierarchy.

Aiming from the gunner seat

“Use SOFLAM” puts the player into the turret seat. GTG_SOFLAM_TurretGetInAction derives from the vanilla SCR_GetInUserAction and not from a modded class, because the vanilla action sits on every vehicle and turret in the game. It hides the action when no device is installed (an unavailable entry would look like a missing permission; the reason text is #AR-UserAction_SeatObstructed), and selects the turret weapon a moment after getting in if the engine did not.

The turret is set up so that you look around freely and the aiming axis follows the mouse only while looking through the optic: CanAimOnlyInADS 1, limits LimitsHoriz -180 180 and LimitsVert -45 45 (also for prone), and UseWeaponSights 1 so that the optic of the device counts. The ball head follows the turret locally (the tripod page).

Radians, not degreesThe engine call SetAimingAngles takes radians. It was given degrees from the tracking code. Both axes ran into their stops: elevation read exactly 45.000, the stop value, and the mount stood 180° off. From outside it looked right, because the ball head followed through a second call that already used radians. Measured: sight axis off by 133.3° before, 0.13° after.

The seat needs a door

A vehicle-style seat needs a door the engine can resolve. Without one the get-in check finds no valid door index and reports the seat as obstructed – “Obstructed” on completely free ground. A fake door with an entry, an exit and an exit-teleport point fixes that. Do not override the door index lookup to a constant: the stock method does a real query for exactly the case you care about (the local player getting in). An earlier override that returned −1 replaced that query for every caller, the fake door was never looked at, and it was the real cause of “obstructed” on flat ground.

Example code – free to useGTG_SOFLAM_Tripod_Turret.et (excerpt)
CompartmentDoorInfo "{6A48B5C000006050}" {
 ContextName "tripod_head"
 EntryPositionInfo PointInfo "{6A48B5C000006051}" {
  Offset 0 0 -1.5
 }
 ExitPositionInfo PointInfo "{6A48B5C000006052}" {
  Offset 0 0 -1.5
 }
 ExitTeleportPositionInfo PointInfo "{6A48B5C000006053}" {
  Offset 0.5 0 -1.5
  Angles 0 -30 0
 }
 GetInTeleport 1
 GetOutTeleport 1
 FakeDoor 1
 StanceChangeOnExit "Change from turret height"
}

A door point on a hillside

The fake door's points are fixed offsets from the root, 1.5 m behind it. That silently assumes the ground there is as high as at the root. On a slope it is not, and the engine's native check at that point can answer “obstructed” although the player stands right in front of it.

So place the points at runtime on the ground, keeping direction and angle from the prefab and adjusting only the height. And use the real surface for that height, with a little air (5 cm), as the previous page warns: a point exactly on the surface or inside it counts as blocked. The point is changed with PointInfo.Set(owner, boneName, matrix), which the engine header allows at runtime.

Example code – free to useGTG_SOFLAM_TripodComponent.c (excerpt, log lines left out)
protected void GTG_ZiehePunktAufBoden(PointInfo punkt, IEntity owner, vector ownerMat[4], BaseWorld welt)
{
	vector lokal[4];
	punkt.GetLocalTransform(lokal);

	vector weltMat[4];
	Math3D.MatrixMultiply4(ownerMat, lokal, weltMat);

	// the real surface by trace, plus 5 cm of air; the trace starts 1 m above the point
	float boden = GTG_BodenY(welt, Vector(weltMat[3][0], weltMat[3][1] + 1.0, weltMat[3][2])) + 0.05;
	float differenz = boden - weltMat[3][1];

	if (Math.AbsFloat(differenz) < 0.02)
		return;											// no write without a reason

	lokal[3][1] = lokal[3][1] + differenz;
	punkt.Set(owner, punkt.GetBoneName(), lokal);
}

It is applied to the entry, exit and exit-teleport point of door 0, on every machine, from the same code that places the legs, and only when the geometry key changed. No RPC is needed because the terrain read is the same everywhere.

NoteThe diagnostic that made this findable hung on the moment of refusal: the log (GTG_LOG_SPERRE, off by default) writes the reason text, the player and root positions with the ground under each, and the entry point's height above the ground. Without it the question “why obstructed?” has no answer, because the verdict comes from a native function.

“Too steep” without guessing

When assembly refuses with “too steep” on a meadow that looks flat, do not replace the check – measure it. GTG_SOFLAM_TripodAssembleAction derives from the vanilla SCR_DeployMultiPartInventoryItemAction, overrides SetCannotPerformReason, and repeats the base class's trace (throttled to every 2 s) only when exactly the too-steep text was set. Anything else that makes the action fail would be noise in the log. The limit is m_fMaxAllowedTilt 35 degrees on the carried prefab.

Example code – free to usethe measurement line
[GTG_SOFLAM][ZUSAMMENBAU] Spurpunkt <position> | Getroffen <entity> | Normale <vector> | Neigung <angle> Grad | Grenze 35
Example code on this page is free to use in your own projects (credit appreciated). The SOFLAM Integration Core mod is published under the Arma Public License (APL). Suggestions: Discord. Arma Reforger / Enfusion are trademarks of Bohemia Interactive.
Impressum · Privacy Policy