← 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

Who may send Changed

The bug that took the longest: with a dedicated server, no laser and no aiming worked, and not a single error appeared. The server simply never heard the client.

In plain wordsOn a server a client may only send to the server for things it owns. Otherwise the message vanishes without a trace. A relay on the player's character gets around that.
How to hook this up in your own modIf you write a device of your own, you need this page: Report your own designator uses the pattern. If a dial or a key does nothing on a server, start with the checklist.

The rule

An Rpc addressed to the server is delivered only if the sending client owns the entity the component sits on. If it does not, the message is dropped – no error, no log line. The engine's routing table says so (RplDocs.c, “RPC invoke from the client”: Not Owner → Dropped). In single-player nothing shows, because client and server are one machine and the question of ownership is never asked.

The log of the first real server test looked like this on the client, from the moment the laser was switched on (shortened from the comment in GTG_SOFLAM_Relay.c; the Core prints its messages in German, translation below):

Example code – free to use
(W) Designator sendet ohne Besitz der Traegerentitaet -- der Server verwirft diese Nachrichten
    [NETZ] SENDE  Ziel anlegen        | Rolle Proxy-ohne-Besitz | Laser true
    [NETZ] SENDE  Ziel aktualisieren  | Rolle Proxy-ohne-Besitz | Laser true
(W) Kein Strahl seit einer Sekunde: kein Ziel unter dieser Ersteller-Id
    [NETZ] SENDE  Ziel aktualisieren  (weiter im Sekundentakt)
    (not a single "WIRKT" line - "takes effect" - after switch-on)

Translation: “Designator sends without owning the carrier entity – the server drops these messages”; “SENDE” is send, “Rolle Proxy-ohne-Besitz” is role proxy without owner; “no beam for one second: no target under this creator id”. The proof that the way back was fine stood three minutes earlier in the same log: a “WIRKT Ziel entfernen” arrived with no “SENDE” before it – so it came over the network. Only the direction client → server failed.

PitfallHolding the device in the hand does not give the client ownership of the item entity. The role stayed “proxy without owner” while the device lay in the inventory. An early comment in the code asserted the opposite – and was the reason neither laser nor ball head reacted on the server.

A relay on the character

You need an entity the player owns. The character is always his. Vanilla already hangs server-bound methods on the character controller component (SCR_CharacterControllerComponent carries four RplRcver.Server methods), so the route is not just allowed, it is the one in use. The SCR_PlayerController itself carries no RPC of its own in vanilla; reaching it would need a prefab override, which is a load-order lottery with every other mod that does the same.

So the Core adds eight server-bound methods to the character controller (GTG_SOFLAM_Relais*) and a public method for each that sends it (GTG_SOFLAM_Sende*). Every pair is the same shape. The designator picks the route per message:

Example code – free to useGTG_SOFLAM_Relay.c and GTG_SOFLAM_LaserDesignatorComponent.c (excerpts)
modded class SCR_CharacterControllerComponent
{
	// runs on the SERVER, with an id that came from a client - so with every check that belongs to that
	[RplRpc(RplChannel.Reliable, RplRcver.Server)]
	void GTG_SOFLAM_RelaisZielAktualisieren(RplId geraet, string code, vector position)
	{
		GTG_SOFLAM_LaserDesignatorComponent designator = GTG_SOFLAM_Relay.FindeGeraet(geraet);
		if (designator)
			designator.GTG_ServerZielAktualisieren(code, position);
	}

	// Rpc() is protected: the call must stand inside THIS class, so the designator calls this public method
	void GTG_SOFLAM_SendeZielAktualisieren(RplId geraet, string code, vector position)
	{
		Rpc(GTG_SOFLAM_RelaisZielAktualisieren, geraet, code, position);
	}
}

// in the designator
protected bool GTG_BrauchtRelais()
{
	if (!m_RplComponent)
		return false;

	if (Replication.IsServer())
		return false;				// the server sends directly

	return !m_RplComponent.IsOwner();	// an owner sends directly, anybody else needs the relay
}

SCR_CharacterControllerComponent relais;
if (GTG_BrauchtRelais())
	relais = GTG_SOFLAM_Relay.Sender();

if (relais)
	relais.GTG_SOFLAM_SendeZielAktualisieren(GetCreatorId(), m_sLaserCode, targetPos);
else
	Rpc(GTG_RpcAsk_UpdateTarget, m_sLaserCode, targetPos);

If the relay is not available, the message goes the direct way anyway. It is lost then, but the behaviour is the same as before and the one-time warning below names the reason. A silent special route would be the worse of two bad options.

Relay method (server-bound)ChannelCarries
GTG_SOFLAM_RelaisZielAnlegenReliablecreate the target (device id, code)
GTG_SOFLAM_RelaisZielAktualisierenReliableposition and code. Unreliable until 2026-09-17, then changed: entries stood for minutes as corpses.
GTG_SOFLAM_RelaisZielEntfernenReliableremove the target
GTG_SOFLAM_RelaisCode, GTG_SOFLAM_RelaisStaerkeReliablelaser code, laser power
GTG_SOFLAM_RelaisZielenUnreliableaim angles of the ball head. A lost angle heals itself on the next tick; reliability would only build a queue.
GTG_SOFLAM_RelaisZurueckReliableput the device back on the mount
GTG_SOFLAM_RelaisVerfolgungReliabletracking on/off with target id and local point. A lost on/off flip stays wrong; there is no next tick that heals it.

Every send looks the sender up again from the controlled entity (GTG_SOFLAM_Relay.Sender()), never from a remembered reference: the character changes at a respawn, and an old reference points at a corpse the player no longer owns. On the server, FindeGeraet turns the device's RplId into a designator – with GTG_SOFLAM_LaserDesignatorComponent.FindOn, because a search for the base class does not find the subclass on the SOFLAM.

When there is neither ownership nor a relay, the designator says so once, so that the laser does not just silently not work:

Example code – free to use
[GTG_SOFLAM] Designator sendet ohne Besitz der Traegerentitaet UND ohne erreichbares Relais -- der Server verwirft diese Nachrichten
(RplDocs: RPC invoke from the client, Not Owner -> Dropped). GTG_SOFLAM_Relay.Sender() liefert nichts: kein PlayerController oder kein gesteuerter Charakter.
NoteAdd only. The relay is a class that adds methods; it overrides nothing and calls nothing of vanilla's. And every name carries the prefix. A second mod that adds methods to the same vanilla class is fine as long as the names differ.

RPCs live in your own component

An earlier approach put all four RPC blocks into modded class SCR_ChimeraCharacter. Everybody who loaded the mod inherited that extension unasked. A component becomes a replication item simply by having [RplRpc] methods. The second gain: the broadcast lands on the same component on every machine, so the creator id does not have to travel with every packet – each machine derives it from its own carrier entity (GetCreatorId() is the id of the replication component). Only the relay, which sits on the character, has to carry the device's id.

Ask, Do, Apply: the receive path must not send

Every state change has three methods: Ask (client to server), Do (broadcast) and Apply (the effect). The server applies a change itself and then broadcasts (a broadcast does not deliver to its own sender). If the method that applies a received state also reported that state, a client that receives a broadcast would immediately send it back to the server, and the loop closes. A commented-out line used to hide exactly that. The public path reports, the receive path applies and stays silent.

Example code – free to useGTG_SOFLAM_LaserDesignatorComponent.c (excerpt)
[RplRpc(RplChannel.Reliable, RplRcver.Server)]
protected void GTG_RpcAsk_UpdateTarget(string code, vector position)
{
	GTG_ServerZielAktualisieren(code, position);
}

void GTG_ServerZielAktualisieren(string code, vector position)
{
	GTG_ApplyUpdateTarget(code, position);			// the server's own copy
	Rpc(GTG_RpcDo_UpdateTarget, code, position);	// everybody else
}

[RplRpc(RplChannel.Reliable, RplRcver.Broadcast)]
protected void GTG_RpcDo_UpdateTarget(string code, vector position)
{
	GTG_ApplyUpdateTarget(code, position);			// never sends
}

The server-side entry points (GTG_ServerZielAnlegen, GTG_ServerZielAktualisieren, GTG_ServerZielEntfernen, GTG_ServerCode, GTG_ServerStaerke) are public on purpose: the relay calls them from the outside. The seeker uses the same split (GTG_RpcAsk_SetState / GTG_RpcDo_SetState / GTG_ApplySearchState).

PitfallThe Ask method must not start with “if this is the server, return”. A state change on the server would then never reach anybody else. It was a bug in the seeker of the code the Core replaced: no client ever learned whether a seeker was on or off.

Late joiners

State that arrives only as messages is gone for everybody who connects later. The designator implements RplSave and RplLoad for the state that matters – laser on or off, code, last sent position, laser power – or a late client sees nothing until the next report. On loading, a running laser re-enters the registry through the same GTG_ApplyUpdateTarget.

When you can see the problem

The Core has a switch for exactly this, GTG_LOG_NETZ (off in the published version). It prints one line per send and per effect with the role of the machine: [GTG_SOFLAM][NETZ] SENDE <message> | Rolle <Server|Besitzer|Proxy-ohne-Besitz> | Code ... | Laser ... and ... WIRKT <message> | .... A SENDE that is never followed by a WIRKT on the server was dropped; a WIRKT without a SENDE on the same machine came over the network. Put a tag on every line of your own mod and write the role into every network line.

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