← 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

For other modders

3. Aircraft, vehicles and turrets New

An aircraft is a consumer: the crew sets a code, fires a missile, and the missile asks the registry. The parts in between are yours. This page follows the AH-64 mod; the AH-1Z mod is a cleaned-up copy of it.

In plain wordsThe crew of an aircraft needs a place that holds the code, buttons to change it, and something that copies it onto each missile. The Core does not ship these; the AH-64 and AH-1Z mods built them, and you can copy them.
1. Code holder→2. Four dials→3. Injector→4. Missile prefab→5. Seat name→6. Test

Do step 1 and read how a weapon follows the laser first.

What is the Core's and what is notThe names below belong to the AH-64 mod and start with GTG_AH64_. In your mod, use your own prefix. Projectile hooks such as GetOnProjectileLaunched() and the base classes WCS_Armament_* belong to WCS Armaments, which both aircraft mods use for the missile itself; they are not part of the Core. If your weapons are not WCS weapons, use whatever launch hook they have.

Step 1 – A code holder on the turret

The code is a property of the aircraft, not of the viewer: pilot and gunner must see the same one, a late joiner must get it, and the missile takes it along. So it lives in a component that replicates it. This is the AH-64's, shortened. It holds the code and nothing else:

Example code – free to useGTG_AH64_LaserCodeComponent.c (excerpt; logging left out)
class GTG_AH64_LaserCodeComponent : ScriptComponent
{
	[Attribute("1688", UIWidgets.Auto, "Laser code at start, four digits 1..8")]
	protected string m_sDefaultCode;

	protected string m_sCode;
	protected RplComponent m_RplComponent;

	override void OnPostInit(IEntity owner)
	{
		super.OnPostInit(owner);
		m_sCode = m_sDefaultCode;
		m_RplComponent = RplComponent.Cast(owner.FindComponent(RplComponent));
	}

	string GetCode() { return m_sCode; }

	void SetCode(string newCode)
	{
		if (newCode == string.Empty || newCode == m_sCode)
			return;

		m_sCode = newCode;
		if (m_RplComponent)
			Rpc(GTG_RpcAsk_SetCode, m_sCode);		// client asks, the server distributes
	}

	[RplRpc(RplChannel.Reliable, RplRcver.Server)]
	protected void GTG_RpcAsk_SetCode(string code)
	{
		GTG_ApplySetCode(code);						// the server's own copy
		Rpc(GTG_RpcDo_SetCode, code);				// everybody else
	}

	[RplRpc(RplChannel.Reliable, RplRcver.Broadcast)]
	protected void GTG_RpcDo_SetCode(string code)
	{
		GTG_ApplySetCode(code);
	}

	protected void GTG_ApplySetCode(string code)	// shared effect; never sends
	{
		if (code != string.Empty)
			m_sCode = code;
	}

	override bool RplSave(ScriptBitWriter writer)	// late joiners
	{
		writer.WriteString(m_sCode);
		return true;
	}

	override bool RplLoad(ScriptBitReader reader)
	{
		string code;
		if (!reader.ReadString(code))
			return false;

		if (code != string.Empty)
			m_sCode = code;

		return true;
	}
}

Why it looks like this – the Ask/Do pair, the shared effect method that never sends, and RplSave / RplLoad – is explained on Who may send; it is the same design as the Core's designator.

In the turret prefab the component needs no attributes. It looks for a RplComponent on its own entity; without one it only works locally and sends nothing:

Example code – free to useAH64D_Turret_X4_ATGM.et
GTG_AH64_LaserCodeComponent "{9E87DDFAC4FF0E35}" {
}

Step 2 – Four dials in the cockpit

Four instances of one user action, one per digit, each counting 1 to 8 and then back to 1. This is the AH-64's version (shortened):

Example code – free to useGTG_AH64_CockpitLaserTuneAction.c (excerpt)
class GTG_AH64_CockpitLaserTuneAction : ScriptedUserAction
{
	[Attribute("0", UIWidgets.Auto, "Which digit of the code, 0..3")]
	protected int m_iLaserCodeDigit;

	protected GTG_AH64_LaserCodeComponent m_CodeComp;		// found lazily, from the vehicle root downwards

	override void PerformAction(IEntity pOwnerEntity, IEntity pUserEntity)
	{
		if (!GTG_FindeCodeHalter())
			return;

		string code = m_CodeComp.GetCode();
		if (code.Length() != 4)							// never count on a code that is not four characters
			return;

		array<string> digits = {};
		for (int i = 0; i < 4; i++)
			digits.Insert(code.Get(i));

		int digit = digits.Get(m_iLaserCodeDigit).ToInt() + 1;
		if (digit > 8)
			digit = 1;

		digits.Set(m_iLaserCodeDigit, digit.ToString());
		m_CodeComp.SetCode(SCR_StringHelper.Join("", digits, true));
	}

	// LOCAL EFFECT ONLY, on purpose: see the pit below
	override bool HasLocalEffectOnlyScript()
	{
		return true;
	}
}
A digit that jumps by twoThe action returned false from HasLocalEffectOnlyScript() at first, so the engine replicated the action and ran PerformAction on the server and the clients, and the code holder sent the change as well. A relative step (“+1”) through two channels counts twice: the digit jumped by two. It does not show in a self-hosted game, because player and server are one machine. Let the holder be the only way the change travels.

The prefab text for the four dials (the same four lines for digit 0 to 3):

Example code – free to useAH64D_Turret_X4_ATGM.et (digits 0 and 1; 2 and 3 follow)
GTG_AH64_CockpitLaserTuneAction "{7C4E1A0000000140}" {
 ParentContextList {
  "Radio_Gunner"
 }
 "Sort Priority" 20
 m_iLaserCodeDigit 0
}
GTG_AH64_CockpitLaserTuneAction "{7C4E1A0000000141}" {
 ParentContextList {
  "Radio_Gunner"
 }
 "Sort Priority" 21
 m_iLaserCodeDigit 1
}

Step 3 – An injector that copies the code onto each missile

The missile is a new entity every time. Something has to give it the code at the moment it leaves the rail. The AH-64 mod does it on the turret:

Example code – free to useGTG_AH64_LaserCodeInjectorComponent.c (excerpt)
class GTG_AH64_LaserCodeInjectorComponent : ScriptComponent
{
	protected GTG_AH64_LaserCodeComponent m_CodeHolder;

	override protected void OnPostInit(IEntity owner)
	{
		super.OnPostInit(owner);
		SetEventMask(owner, EntityEvent.POSTFRAME);		// look things up one tick later, not in init
	}

	override event void EOnPostFrame(IEntity owner, float timeSlice)
	{
		ClearEventMask(owner, EntityEvent.POSTFRAME);
		m_CodeHolder = GTG_AH64_LaserCodeComponent.FindOn(owner);
		// ... subscribe to the launch event of every weapon of the loadout:
		//     weapon.GetOnProjectileLaunched().Insert(OnProjectileLaunched);   (WCS)
	}

	void OnProjectileLaunched(Projectile projectile)
	{
		if (!projectile || !m_CodeHolder)
			return;

		GTG_AH64_LaserSeekerComponent seeker = GTG_AH64_LaserSeekerComponent.Cast(
			projectile.FindComponent(GTG_AH64_LaserSeekerComponent));
		if (seeker)
			seeker.SetTargetCode(m_CodeHolder.GetCode());
	}
}

The two ways a missile can leave without a code look identical from outside – it falls – so the injector reports them separately, as warnings: [GTG_AH64][TELEMETRIE] Start OHNE Codehalter -- GTG_AH64_LaserCodeComponent am Turm nicht gefunden. (“launch without code holder”: the component is missing from the turret prefab) and [GTG_AH64][TELEMETRIE] Start OHNE Suchkopf -- GTG_AH64_LaserSeekerComponent nicht an der Rakete. (“launch without seeker”: it is missing from the ammo prefab).

Step 4 – The missile prefab

The AH-64 mod switches the stock WCS laser seeker and flight computer off and puts its own on the ammo prefab:

Example code – free to useAmmo_Missile_AGM-114L_Laser.et (excerpt)
WCS_Armament_LaserSeekerComponent "{62A51932B80C9D78}" {
 Enabled 0
}
GTG_AH64_LaserSeekerComponent "{33BAD89EB72B67ED}" {
 m_fTargetCheckInterval 0.05
 m_fLaserAcquisitionFov 180
}
WCS_Armament_LaserGroundMissileFlightComputerComponent "{62B2C7B3F8499C97}" {
 Enabled 0
}

The check interval matters: the prefab comment explains that 0.5 s let a missile at 150 - 250 m/s travel 75 - 125 m between two target updates, which limited the final accuracy hard. 0.05 s fixed that.

The turret and pod prefabs are overrides: they claim the same resource GUID as the WCS originals (through the .et.meta file), so the unchanged vehicle prefab, which already refers to turret and pod by GUID, now points to the mod's version. That is how the AH-64 mod got its code dials onto the existing Apache without a new vehicle variant.

Step 5 – Get the seat name right

Several things in an aircraft mod depend on which seat the player is in – HUD markers, the attack-mode listener. The name is not the same in every aircraft:

AircraftWeapon seat (compartment name)Where it is set
AH-64Dgunnerm_sPilotCompartmentUniqueName "gunner" in the turret prefab; IsInCompartment(..., "gunner") in the marker component
AH-1ZcopilotCompartmentUniqueName "copilot" in the turret prefab and in the marker component
Seat is not contextThe compartment name (where the player sits) and the interaction context (where an action can be used) are two different things. In one helicopter port the dials hung on the context copilot and never worked; the context name comes from the shared turret base and is gunner whatever the seat is called. Read both from the vehicle prefab; do not assume.

Step 6 – Know who owns the vehicle

The code holder sends its Ask straight from the component on the vehicle. That worked on a dedicated server in the AH-64 mod (its changelog says it was tested there). An RPC to the server only arrives if the sender owns the entity (see Who may send). The Core's own design notes list as still to be checked whether a turret gunner owns the vehicle. If a dial does nothing on a server and the log is silent, this is the first suspect; the cure is the relay pattern of the next page (the message goes through the player's character).

Optional – markers, screens, and far-away operators

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