Skip to content

Multiplayer Compatibility

QinShenYu edited this page Jun 16, 2026 · 1 revision

Namespace:​ ScavLib.compat.krokmp

ScavLib ships a built-in compatibility bridge for the KrokoshaCasualtiesMP multiplayer plugin. The bridge is a soft dependency: if KrokMP is present at startup, the bridge activates automatically; if not, ScavLib continues to operate normally in single-player. There is no opt-in for mod authors and no opt-out for players.

The goal of this page is to describe what the bridge fixes, what it guarantees, and the limitations you need to design around when targeting multiplayer.


What the Bridge Does

KrokMP synchronizes networked GameObjects by passing their resource_stringid to Resources.Load(...) on every client. That works for vanilla items, whose prefabs live in Resources/, but custom items registered through CustomItemBuilder have no Resources/ prefab — ScavLib spawns them by cloning a vanilla template at runtime. Without intervention, every custom item would either fail to spawn on remote clients or trigger an infinite-spawn loop on the host.

The bridge installs three Harmony patches at startup:

KrokMpGoSyncPatch — Patches GOSyncPacket.Apply

The vanilla MP path resolves an incoming sync packet by calling Resources.Load(resource_stringid). The bridge intercepts the call:

  • If the id is not a registered custom item, vanilla logic runs unchanged.
  • If the id is a registered custom item, the bridge instantiates the cached clone template (the same one Utils.Create would use), stamps the mod id onto Item.id, attaches a CustomItemTag, runs the registered OnSpawn hook on this client's instance, and registers the GameObject with NetObjectRegistry._RegisterGO.

End result: every client sees the custom item materialise correctly, including its sprite, tags, and component-level configuration.

KrokMpConSpawnPatch — Patches Con.SpawnThingOnPlayer

/spawn on a dedicated server uses Con.SpawnThingOnPlayer, which has its own setup path independent of the normal world spawn. The bridge re-implements that path for custom ids: it routes through Utils.Create, fills ammo / chamber state for guns, and chooses between container insertion and auto-pickup based on the original arguments.

KrokMpValidationPatch — Patches NetObjectRegistry.ObjectCanBeIgnoredForNetwork

KrokMP periodically asks "can this object be ignored for network sync?" and answers true for objects whose resource id cannot be located via Resources. For custom items, that check would always fail — repeatedly — and KrokMP would respond by destroying and re-creating the object, producing an infinite spawn loop that was visible in the wild.

The bridge forces objects carrying CustomItemTag to report canBeIgnored = false regardless of Resources resolution, breaking the loop.


Save Directory Resolution

When the bridge is active, it installs itself as the SaveDirResolver on SaveCompanionFile. Resolution order:

  1. If KrokMP's network system is not running (single-player or menu) — return Application.persistentDataPath.
  2. If SavesystemPatch.savedatapathreplacement is non-empty — use it (this is KrokMP's own override hook).
  3. If SavesystemPatch.HasMultiplayerSaveFile() returns true — use SavesystemPatch.mpsavefolder.
  4. Otherwise fall back to Application.persistentDataPath.

So companion files written by SaveCompanionFile automatically follow the active save context: single-player saves, dedicated-server saves, and KrokMP's own save-path overrides are all respected without any per-mod plumbing.


What You Need to Do as a Mod Author

Nothing.​ Anything you register through CustomItemBuilder is MP-aware as soon as KrokMP is installed. Your Awake() looks the same whether or not KrokMP is present.

private void Awake()
{
    CustomItemBuilder.Create("mymod_salve", "MyMod", ItemTemplate.Bandage)
        .Register(out _);
}

If you want to detect MP at runtime (e.g. to skip a UI flow that only makes sense in single-player), check the bridge's enabled flag:

if (ScavLib.compat.krokmp.KrokMpBridge.IsEnabled)
{
    // KrokMP is loaded; we may or may not be currently in an MP session.
}

For the in-session check, query KrokoshaCasualtiesMP.KrokoshaScavMultiplayer.network_system_is_running directly. ScavLib does not wrap that bool — it's KrokMP's source of truth.


Behaviour Guarantees and Limitations

The bridge does several things, but it does not silently turn every custom item into a fully-synchronised state machine. The constraints below come from how OnSpawn and CustomItemTag are designed, not from the bridge itself.

Guaranteed

  • The same custom item id resolves to the same clone template on every client.
  • Every spawned instance carries an identical CustomItemTag with the correct CustomItemId and Owner.
  • Vanilla item systems that read ItemInfo (display name, weight, tags, container capacity, etc.) see consistent values everywhere, because ItemInfo lives in each client's Item.GlobalItems after registration.
  • Save companion files follow the active save directory.

Per-client (not synced)

  • OnSpawn runs on every client independently.​ Each instance of the hook fires on its own client's GameObject. Side effects must either be deterministic (computed from inputs that all clients see) or you must explicitly synchronize them via your own packet.
  • CustomItemTag.InstanceData is not automatically replicated. It is a plain in-memory dictionary on the local MonoBehaviour. Two clients who both see the same item see two unrelated bags. If you rely on per-instance state, treat InstanceData as client-local cache and synchronise the source of truth through GOSyncPacket or another KrokMP primitive yourself.
  • Anything you mutate inside OnSpawn based on world state (for example, deriving stats from "current biome depth") will diverge if the world state can differ across clients.

Other notes

  • OnSpawn runs after Unity's Awake on the spawned clone (this was a behaviour change in the 0.7.2 spawn rewrite). On both single-player and MP, you can rely on every component's Awake-time fields being initialised before your hook reads them.
  • The bridge hooks GOSyncPacket.Apply at Harmony Prefix returning false for handled ids. If another mod also patches GOSyncPacket.Apply, test the combination — ScavLib does not intercept ids it does not own, but ordering can still matter.
  • The bridge runs at default priority. ScavLib's own Utils.Create patch is Priority.Low (other frameworks run first), but the KrokMP patches are not — they are intentionally normal priority because they are cooperating with KrokMP, not competing with it.

Diagnosing MP Issues

If a custom item misbehaves in MP, work through this list before filing a bug:

  1. Run scavlib check on every client. The output should list your item id under "Commands (via ScavLib)" — sorry, "items" — with a consistent owner mod name. A divergent owner across clients means the mod was registered on different versions or with different ownership.
  2. Confirm the bridge activated. The startup log line [KrokMpBridge] Krokosha MP bridge initialized with ValidationFix. should appear on every client. If it only appears on the host, KrokMP is missing on the other client.
  3. If a RivalFrameworkDetector warning appears, another custom-item framework is loaded alongside ScavLib. Cross-framework conflicts in MP are not auto-resolvable; mention the warning in your bug report.
  4. Check for Failed to resolve template '<id>' errors. Those mean the clone template is not in Resources/ — usually a typo in the template id passed to CustomItemBuilder.Create(...).

Known Failure Modes

  • Cross-framework coexistence (RshLib + ScavLib).​ Both libraries patch Utils.Create. ScavLib runs at Priority.Low and will not hijack ids it does not own, but if both libraries register the same id, the result is undefined and the RivalFrameworkDetector warning is the only signal. Use mod-prefixed ids.
  • Late-joining clients with newer mod content.​ A client that joins a session running a different version of your mod may have different ItemInfo for the same id. KrokMP will sync the GameObject, but every client renders it according to its own Item.GlobalItems. Ship matched mod versions to your players.
  • Hot-disabling mods mid-session.​ ScavLib does not currently support unregistering custom items at runtime in MP. The single-player unregister path exists but is not synchronised to remote clients; unloading a mod mid-session leaves orphan instances that, on re-registration, are handled by MissingItemTag for save-game recovery — see the SaveCompanionFile page.

Clone this wiki locally