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.
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:
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):
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
}
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.
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:
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
}
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.
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
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).
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.
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.
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.
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.
[GTG_SOFLAM][ZUSAMMENBAU] Spurpunkt <position> | Getroffen <entity> | Normale <vector> | Neigung <angle> Grad | Grenze 35