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.
Do step 1 and read how a weapon follows the laser first.
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:
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:
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):
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;
}
}
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):
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
}
ParentContextListnames an interaction context that already exists on the vehicle (Radio_Gunnercomes from the WCS AH-64D). It decides where the player stands to use it. Pick one that exists – the AH-64 mod chose the gunner's radio panel instead of inventing a position.- The action and the holder are on different entities (cockpit and turret). The action therefore looks for the holder from the vehicle root downwards, and only when it is first needed, not in
Init: atInitthe instrument panel may not hang on the vehicle yet, and a failure there would be final. - The AH-1Z mod needed
AllowCrossHierarchy 1on the actions manager when action and holder are on different entities, and it puts one context point per digit (GTG_LSS_Digit_1to_4) on the vehicle.
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:
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:
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:
| Aircraft | Weapon seat (compartment name) | Where it is set |
|---|---|---|
| AH-64D | gunner | m_sPilotCompartmentUniqueName "gunner" in the turret prefab; IsInCompartment(..., "gunner") in the marker component |
| AH-1Z | copilot | CompartmentUniqueName "copilot" in the turret prefab and in the marker component |
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
- HUD marker. The AH-64 and AH-1Z mods draw a diamond and the code per laser point (
GTG_AH64_LaserSpotIndicatorComponent), reading the whole list withGetAllTargets(array). Show it only while the player sits in the weapon seat. - Show the code where the crew looks. A text overlay follows the head; the AH-64 mod moved the code onto a cockpit screen.
- Operators far away. Laser points of an operator more than about 1600 m from the gunner vanished, because the operator's character was not streamed that far. The AH-64 mod fixes it by
streaming every character to every client:
RplComponent.EnableSpatialRelevancy(true)plusEnableStreamingConNode(identity, false)per connected player, on the server, in amodded SCR_ChimeraCharacterand amodded SCR_BaseGameMode.OnPlayerConnected(classGTG_SoflameCharacterRelevancy, technique taken from a view-distance addon). The AH-1Z mod has the same hook; the Core itself does not contain it. If two of your mods carry it, load only one copy.