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.
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:
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
}
GetAllTargets().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).
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:
SystemSettings : "{45C53F06BA17238D}configs/Systems/SystemsConfig.conf" {
Systems +{
GTG_SOFLAM_LaserTargetManager "{6A48B5D000000201}" {
}
}
}
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.
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:
| Method | What 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.
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).
[GTG_SOFLAM][VERFALL] Ziel ... seit ... s ohne Meldung -- entfernt – is off in regular operation (GTG_LOG_VERFALL = false).