For other modders
4. Report your own designator New
Anything that can point at the ground can be a designator: it creates one registry entry, keeps its position fresh, and removes it. The hard part is not the three calls. It is getting them from the player's machine to everybody else's.
Do step 1 first.
Which way: the Core's designator, or your own?
| Core designator component | Your own, calling the registry | |
|---|---|---|
| What it is | GTG_SOFLAM_LaserDesignatorComponent on your device: keys, rangefinder, tracking, laser power, relay, replication all included. | Three registry calls and a relay you write. |
| Fits | A device with an optic that a player looks through, like the SOFLAM. | Anything else: a drone camera, a turret. |
| Proven in | The SOFLAM prefabs of the Core. | The test emitters of the AH-64 and AH-1Z mods (without a relay). |
| Needs | A RplComponent on the device; the target position comes from the camera (or, in the SOFLAM subclass, from the optic). | Any entity with a RplComponent as creator id. |
Way 1 – the Core's designator component
- The device needs a
RplComponent. Without it the component prints[GTG_SOFLAM] Designator ohne RplComponent an der Traegerentitaet -- ohne die gibt es keine Ersteller-Id und keine Replikation.once and stays quiet. - Add
GTG_SOFLAM_LaserDesignatorComponent(or inherit a SOFLAM prefab). The defaults give you the keys T (designate), Y (range only) and U (track), code"1688", 10 reports per second at most. The attributes are on the designator page. - Tell it when the player operates the device:
designator.SetActionsEnabled(true)hooks the keys up,SetActionsEnabled(false)removes them (and switches the laser off, unless you passtrueas the second argument). The SOFLAM's optic calls these when the player starts and stops looking through it. - Tell it where the camera is, if it is not the player's:
SetCameraEntity(entity). Without one, it measures from the active game camera. - For a different beam geometry, derive and override
ComputeTargetPosition()– and askGTG_TrackingPoint()first, as the Core's own two versions do, or tracking silently stops working in yours.
GTG_SOFLAM_LaserDesignatorComponent.FindOn(entity), which looks for the base class and then for GTG_SOFLAM_SoflamDesignatorComponent. A search for the base class alone does not find a derived class on the SOFLAM. Three symptoms – code wheel stuck, laser indicator stuck on “on”, designate key does nothing – had one cause, and none of them printed anything. If you derive your own, FindOn will not know it: call your own search, or make sure the code that needs it finds yours.The Core's designator has no shipped example outside the SOFLAM prefabs. Test it on a dedicated server before you rely on it for a vehicle.
Way 2 – call the registry yourself
A designator that is not the SOFLAM reports into the registry with three calls on GTG_SOFLAM_LaserTargetManager. Everything else is networking.
Step 1 – Pick the creator id
The registry keys each entry by a RplId: the replication id of the entity that creates the laser point. One creator has exactly one entry; creating again replaces it.
The Core's designator uses the id of its own device (GetCreatorId() returns RplComponent.Id()). A drone would use the drone's id.
It must be valid (creatorId.IsValid()), otherwise CreateTarget and UpdateTarget do nothing. On an entity that exists in the world file from the start the id may not
be valid yet in EOnInit – the AH-64 test emitter waits for the first POSTFRAME.
Step 2 – Three calls
GTG_SOFLAM_LaserTargetManager manager = GTG_SOFLAM_LaserTargetManager.GetInstance();
if (!manager)
return; // no world: nothing to do, ask again next time
manager.CreateTarget(creatorId, code); // the entry exists, but with NO position yet
manager.UpdateTarget(creatorId, position, code); // position and code; creates the entry if it is missing
manager.RemoveTarget(creatorId);
Call UpdateTarget straight away; it creates the entry when there is none (an update can even reach a late-joining client before the create). A point that was only created sits at the zero vector
and every reader has to skip it. Mind the order of the arguments: UpdateTarget(creatorId, position, code), not code first.
Step 3 – Send it through the server, from a place the player owns
The registry replicates nothing. Every machine fills its own copy, from a broadcast. So the three calls must run on every machine, and the way there is: the player's machine asks the server, the server applies it itself and broadcasts. And
a client may only call a server RPC on something it owns. The Core's relay on SCR_CharacterControllerComponent is the player's own character – but it only resolves the id to a GTG_SOFLAM_LaserDesignatorComponent, so a drone cannot use it. You need a second one with your own names:
modded class SCR_CharacterControllerComponent
{
// the server: apply here, then tell everybody else
void MyMod_ServerUpdate(RplId creator, string code, vector position)
{
MyMod_ApplyUpdate(creator, code, position);
Rpc(MyMod_DoUpdate, creator, code, position);
}
protected void MyMod_ApplyUpdate(RplId creator, string code, vector position)
{
GTG_SOFLAM_LaserTargetManager manager = GTG_SOFLAM_LaserTargetManager.GetInstance();
if (manager)
manager.UpdateTarget(creator, position, code);
}
[RplRpc(RplChannel.Reliable, RplRcver.Server)]
void MyMod_AskUpdate(RplId creator, string code, vector position)
{
MyMod_ServerUpdate(creator, code, position);
}
[RplRpc(RplChannel.Reliable, RplRcver.Broadcast)]
void MyMod_DoUpdate(RplId creator, string code, vector position)
{
MyMod_ApplyUpdate(creator, code, position);
}
// Rpc() is protected: the call must stand INSIDE this class, so callers use this public method
void MyMod_SendUpdate(RplId creator, string code, vector position)
{
Rpc(MyMod_AskUpdate, creator, code, position);
}
}
And the sender, which the player's machine runs:
static SCR_CharacterControllerComponent MyMod_Sender()
{
PlayerController controller = GetGame().GetPlayerController();
if (!controller)
return null; // dedicated server: no local player
IEntity character = controller.GetControlledEntity();
if (!character)
return null;
return SCR_CharacterControllerComponent.Cast(
character.FindComponent(SCR_CharacterControllerComponent));
}
// ...
SCR_CharacterControllerComponent relay = MyMod_Sender();
if (relay)
{
if (Replication.IsServer())
relay.MyMod_ServerUpdate(creatorId, code, position); // a broadcast does not deliver to its own sender
else
relay.MyMod_SendUpdate(creatorId, code, position);
}
- Look the sender up every time, from the controlled entity. The character changes at a respawn; a remembered reference points at a corpse the player no longer owns.
- All of them
Reliable, including the position. A position still on its way could otherwise arrive after the remove and create the point again: a laser that never goes out. The Core's relay used an unreliable channel for the position for a while and had exactly this; entries stood for minutes, three at a time. - Do not use the Core's
GTG_SOFLAM_SendeZielAktualisierenand friends for a foreign device: their server side only knowsGTG_SOFLAM_LaserDesignatorComponent. The call would arrive and do nothing. - Give every method a prefix. You are adding to a vanilla class that every other mod can add to, too.
Step 4 – Keep it alive, but not too hard
The registry throws away any entry that has not been updated for 5 seconds (checked once a second). So you must keep reporting, whether the point moves or not. The Core's designator reports:
- at most every
m_fUpdateInterval= 0.1 s (ten per second), - only if the point moved at least
m_fMinMoveDistance= 0.5 m, - but at least once every 2 seconds even if it did not move (a “life sign”). Without that, a steady laser would vanish after five seconds, just when it is needed.
Sending every frame also works, but on a reliable channel it is a lot of traffic. Take the Core's rhythm unless you have a reason.
When the operator can no longer see the target (looks away), keep sending the last valid point rather than stopping, so that clients who only come into range later still get a position.
Step 5 – Clean up
- Remove the entry when the laser goes off:
RemoveTarget(creatorId), through the same route. - Remove it when the operator leaves. A first version often forgets this: a pilot who leaves a drone without switching off leaves a stale entry that survives until the world is reloaded, even a respawn. Remove it as soon as the device no longer belongs to the local player.
- Remove it when your entity is deleted (
OnDelete). The Core's designator does this on every machine, not just the operator's. - Do it locally first, then send. If the send fails, a removal that only travels over the network leaves the entry standing on every machine, including yours. The expiry is the net under this, not the plan.