For other modders
5. Optional integration without a hard dependency New
A script cannot say “use this class if it exists”: a reference to a class that is not there stops the compile. What does work is to keep the reference in an addon that only exists when both sides do.
What does not work
A reference to GTG_SOFLAM_LaserTargetManager in your main mod makes the Core a hard dependency, because the compiler resolves it. A runtime
“if the Core is present” around it does not help – the line must compile first. A missing class breaks the script module, and
"ClassName".ToType() can tell you whether a class exists but cannot call a method on it.
What works: a bridge addon
Your main mod does not depend on the Core. It has a small base class, a provider, that answers one question: where is the laser point for this code? A separate addon, the bridge, depends on your mod and the Core and fills that hook. If a player loads the bridge, your weapons follow SOFLAM lasers. If not, nothing changes.
- In your main mod, define the question without knowing the Core: a base class with a static register/get and a few virtual methods that answer “no” by default.
Your code asks the registered provider, and does something sensible when there is none.
Example code – free to useyour main mod (the names are yours)
class MyMod_LaserProvider { protected static ref MyMod_LaserProvider s_Instance; static void Register(MyMod_LaserProvider provider) { s_Instance = provider; } static MyMod_LaserProvider Get() { return s_Instance; } bool IsAvailable() { return false; } bool FindPointByCode(string code, vector fromPosition, out vector point) { return false; } } - In the bridge addon, list both in
addon.gproj: your mod's GUID and the Core's6A48B52029EA0F9A. Derive the provider and answer from the registry:Example code – free to usethe bridge addon (the registry calls are the Core's, the rest are your names)class MyMod_SoflamLaserProvider : MyMod_LaserProvider { override bool IsAvailable() { return GTG_SOFLAM_LaserTargetManager.GetInstance() != null; // ready once the world has a registry } override bool FindPointByCode(string code, vector fromPosition, out vector point) { point = vector.Zero; if (code.IsEmpty()) return false; GTG_SOFLAM_LaserTargetManager manager = GTG_SOFLAM_LaserTargetManager.GetInstance(); if (!manager) return false; array<GTG_SOFLAM_LaserTarget> targets = {}; if (manager.GetTargetsByCode(code, targets) == 0) return false; bool found = false; float bestDistSq = 0; foreach (GTG_SOFLAM_LaserTarget target : targets) { if (!target) continue; vector position = target.GetPosition(); if (position == vector.Zero) continue; // created, never reported float distSq = vector.DistanceSq(fromPosition, position); if (!found || distSq < bestDistSq) { found = true; bestDistSq = distSq; point = position; } } return found; } } - Register it, and do it twice: once when the game mode starts (
modded SCR_BaseGameMode.OnGameStart), once when the first character is created (modded SCR_ChimeraCharacter.EOnInit). An empty Workbench test world has no game mode, so only the second path runs there. Make the registration safe to repeat: do nothing if a provider of your kind already stands. - Ask every time. The provider keeps no world state and fetches the registry for each call, so it survives a world change.
The other direction: your mod calls an optional mod by name
What can be done at runtime is calling a method on an object by its name. The Core does it to be friendly to RHS night-vision goggles without depending on RHS:
static bool CanLocalPlayerSeeIR()
{
if (s_bTemporaryNV)
return true;
SCR_PlayerController playerController = SCR_PlayerController.Cast(GetGame().GetPlayerController());
if (!playerController)
return false;
ScriptModule scriptModule = GetGame().GetScriptModule();
if (!scriptModule)
return false;
bool nvOff = true;
if (scriptModule.Call(playerController, "RHS_IsNVOff", false, nvOff)) // false if the method does not exist
return !nvOff;
return false;
}
If RHS is loaded, the call returns the real answer; if not, it fails and only the Core's own state counts. The vanilla scripts use the same call (the AI behaviour node
SCR_AIDecoIsArrayEmpty calls IsEmpty on an object by name). Limits:
- You need an object to call it on. For a static method or a class you cannot name, this gives you nothing.
- It returns whether the call worked; the result comes back through the last argument.
- The Core asks on every call and does not remember a failure. If you copy the pattern into something that runs every frame, remember the first failure and stop asking.
- The AH-64 mod looks classes up by name for RHS, too (
"RHS_VehicleNightVisionComponent".ToType()) to find out whether they exist.
GTG_SOFLAM_LaserTargetManager without a compile reference could not be shown from the code of the published mods; both use a dependency. If you find one, test it on a dedicated server first.