← Home SEEBIE90 DOCS
Download PDF
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

Target registry Changed

The registry is the contract between everything that marks and everything that seeks. It stores one point per designator, matches by code, and forgets entries nobody removed.

In plain wordsOne place that stores every active laser point. Anyone who searches asks it for points with a matching code.
How to hook this up in your own modTo use the registry from your own mod, see Make a weapon follow the laser (reading) and Report your own designator (writing).

Match by code, not by entity

A seeker does not know which device is lasing it. It knows a four-digit code and asks for the nearest point that carries it. That is what lets a handheld device, a tripod, a vehicle turret and an aircraft all designate for the same missile – and it is what makes a code a real thing the player sets rather than an internal link.

One entry is a GTG_SOFLAM_LaserTarget. It is deliberately small:

Example code – free to useGTG_SOFLAM_LaserTargetManager.c (excerpt)
class GTG_SOFLAM_LaserTarget
{
	protected RplId  m_CreatorId;		// who reported it
	protected vector m_vPosition;
	protected string m_sCode;

	//! World time of the last report. The basis of the expiry, see below.
	protected float  m_fLetzteMeldung;

	void GTG_SOFLAM_LaserTarget(RplId creatorId, string code) { m_CreatorId = creatorId; m_sCode = code; }

	RplId  GetCreatorId()    { return m_CreatorId; }
	vector GetPosition()     { return m_vPosition; }
	string GetCode()         { return m_sCode; }
	float  GetLetzteMeldung() { return m_fLetzteMeldung; }
	// plus SetPosition(), SetCode() and MeldungGesehen(time) for the registry itself
}
NoteThe creator id sits inside the entry. If it only lived as the map key, every consumer would need to know the map's structure to reach it, and the structure could never change again. A consumer only needs GetAllTargets().
NamesSome member names are German: GetLetzteMeldung is “last report”. They are public; they are what you have to type.

A game system, not a singleton

A static singleton survives a world change. Someone then has to remember to empty it on every start, and if they forget, seekers of the new round home in on points from the old one. A GameSystem hangs on the world: the engine creates it on load and throws it away on unload, with nothing left over. The pattern is taken from a vanilla system (SCR_ScenarioFrameworkCallQueueSystem).

Example code – free to useGTG_SOFLAM_LaserTargetManager.c (excerpt)
class GTG_SOFLAM_LaserTargetManager : GameSystem
{
	protected ref map<RplId, ref GTG_SOFLAM_LaserTarget> m_mTargets = new map<RplId, ref GTG_SOFLAM_LaserTarget>();

	override static void InitInfo(WorldSystemInfo outInfo)
	{
		outInfo
			.SetAbstract(false)
			.AddPoint(ESystemPoint.FixedFrame);		// without any update point the engine never creates the system
	}

	// can be null when no world is loaded: EVERY caller must check
	static GTG_SOFLAM_LaserTargetManager GetInstance()
	{
		World world = GetGame().GetWorld();
		if (!world)
			return null;

		return GTG_SOFLAM_LaserTargetManager.Cast(world.FindSystem(GTG_SOFLAM_LaserTargetManager));
	}
}

The second half is the systems config. It extends the vanilla file, so the file's .meta carries the vanilla GUID {86E953538A28A98D} for Configs/Systems/ChimeraSystemsConfig.conf; with a GUID of its own the file would be packed and never read:

Example code – free to useConfigs/Systems/ChimeraSystemsConfig.conf
SystemSettings : "{45C53F06BA17238D}configs/Systems/SystemsConfig.conf" {
 Systems +{
  GTG_SOFLAM_LaserTargetManager "{6A48B5D000000201}" {
  }
 }
}
PitfallTwo things have to be true before FindSystem() returns anything. The class needs at least one update point, even if it never uses it, and it has to be listed in the systems config. The first version of this registry was commented as “deliberately without an update point” – the engine then never created it, FindSystem() returned null, and the laser did nothing for days. The only symptom was the beam component's Zielablage nicht gefunden (“registry not found”).

Every machine has its own copy

The registry replicates nothing by itself. Every machine fills its own copy from the broadcast RPCs of the designators (see Who may send). That is deliberate: the entries are small and change often, and a replication component of their own would not pay off. The consequence for you: anything that writes into the registry has to run on every machine, not only on the server.

One target per designator, and update before create

A designator has exactly one target, so reporting again replaces the old one. The update path creates the entry when it does not exist yet. That is not tidiness: on a client that joins late, the first message can be an update that arrives before the create. A registry that drops it leaves the client blind until the next create.

Example code – free to useGTG_SOFLAM_LaserTargetManager.c (excerpt)
void UpdateTarget(RplId creatorId, vector position, string code)
{
	if (!creatorId.IsValid())
		return;

	GTG_SOFLAM_LaserTarget target = m_mTargets.Get(creatorId);
	if (!target)
	{
		target = new GTG_SOFLAM_LaserTarget(creatorId, code);		// an update may reach us before the create
		m_mTargets.Set(creatorId, target);
	}

	target.SetPosition(position);
	target.SetCode(code);
	target.MeldungGesehen(GTG_Weltzeit());
}

CreateTarget(creatorId, code), on the other hand, stores no position. An entry that was created and never updated sits at the zero vector. Readers must skip it (how); GetClosestTargetByCode does not.

Keep the map private

A public map is manipulated from outside, including by mods you have never seen. After that the data structure cannot change without breaking all of them. The map is protected; the public surface is a set of methods, and the Core's design notes call their names and signatures a promise:

MethodWhat it does
static GetInstance()The registry of the current world, or null.
CreateTarget(RplId creatorId, string code)New entry for a designator, replacing its old one. No position yet.
UpdateTarget(RplId creatorId, vector position, string code)Position and code; creates the entry if missing; stamps the time.
RemoveTarget(RplId creatorId)Removes the entry.
GetTarget(RplId creatorId)The entry of one designator, or null.
int GetAllTargets(notnull out array<GTG_SOFLAM_LaserTarget> outTargets)All entries; returns the count.
int GetTargetsByCode(string code, notnull out array<GTG_SOFLAM_LaserTarget> outTargets)All entries with this code. An empty code returns 0.
GetClosestTargetByCode(string code, vector fromPosition)The nearest entry with this code, or null. Selecting the nearest belongs here, otherwise every seeker writes it a little differently.
int GetTargetCount(), ClearTargets()Count; clear all.

Expiry: the last safety net

A target is created over the network and removed over the network. When the return path fails, it stays. In one server run three entries stood unchanged for minutes in an open field, their device long gone. A seeker takes the nearest match, so a corpse right in front of the helicopter beats the real target at 1600 m.

Every report stamps the entry. The registry's own update point sweeps once a second and removes everything that has not been reported for five seconds (GTG_VERFALL_MS = 5000, GTG_VERFALL_TAKT_MS = 1000). The individual leaks were fixed one by one; the expiry is the net under all of them.

Example code – free to useGTG_SOFLAM_LaserTargetManager.c (excerpt; the log line is left out)
protected static const float GTG_VERFALL_MS = 5000;			// five seconds without a report
protected static const float GTG_VERFALL_TAKT_MS = 1000;	// sweep once a second

override event protected void OnUpdatePoint(WorldUpdatePointArgs args)
{
	float jetzt = GTG_Weltzeit();
	if (jetzt < m_fNaechsterVerfallslauf)
		return;

	m_fNaechsterVerfallslauf = jetzt + GTG_VERFALL_TAKT_MS;

	// count BACKWARDS: removing shifts the following elements, going forwards would skip entries
	for (int i = m_mTargets.Count() - 1; i >= 0; i--)
	{
		GTG_SOFLAM_LaserTarget target = m_mTargets.GetElement(i);
		if (!target)
		{
			m_mTargets.Remove(m_mTargets.GetKey(i));
			continue;
		}

		if (jetzt - target.GetLetzteMeldung() < GTG_VERFALL_MS)
			continue;

		m_mTargets.Remove(target.GetCreatorId());
	}
}

Why time, and not “does the entity still exist”: a check with Replication.FindItem would be wrong on a client. A remote device is often not streamed there; its id cannot be resolved although the device lives and lases. The time since the last report is the same question on every machine.

What this means for a publisher: report at least every few seconds, even when the point does not move. The Core's designator sends a “life sign” every 2 seconds for exactly this reason (The designator).

NoteSwitching the laser off is the normal way an entry expires, so the log line for an expired target – [GTG_SOFLAM][VERFALL] Ziel ... seit ... s ohne Meldung -- entfernt – is off in regular operation (GTG_LOG_VERFALL = false).
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