Subnautica II Co-op BepInEx Mod
Skill by ara.so — Devtools Skills collection.
Overview
The Deep Synergy Multiplayer Mod transforms Subnautica 2 into a synchronized cooperative experience. Built on BepInEx, it implements deterministic session synchronization, adaptive difficulty scaling, shared inventory via Merkle tree verification, and optional AI-driven narrative features using OpenAI/Claude APIs.
Core Architecture:
- BepInEx 6.0.x plugin framework (IL2CPP hooks, no game file modification)
- WebRTC P2P for decentralized multiplayer (NAT punch-through)
- Merkle tree inventory sync for distributed state integrity
- Adaptive scaling based on player count
- API integrations for procedural narrative generation
Installation
Prerequisites
- Subnautica 2 (Steam/GOG version)
- BepInEx 6.0.x framework installed
Setup Steps
# 1. Install BepInEx first (if not already installed)
# Download from https://github.com/BepInEx/BepInEx/releases
# Extract to Subnautica 2 game directory
# 2. Download Deep Synergy mod
# Extract to: <Subnautica2>/BepInEx/plugins/DeepSynergy/
# 3. Verify directory structure:
# BepInEx/
# plugins/
# DeepSynergy/
# DeepSynergy.dll
# Newtonsoft.Json.dll
# config/
# synergy_profile.json
File Structure
<Subnautica2>/
├── BepInEx/
│ ├── core/
│ ├── plugins/
│ │ └── DeepSynergy/
│ │ ├── DeepSynergy.dll
│ │ └── libs/
│ └── config/
│ ├── synergy_profile.json
│ └── session_config.xml
└── Subnautica2.exe
Configuration
Session Profile (BepInEx/config/synergy_profile.json)
{
"session_name": "Ocean Explorers",
"max_players": 4,
"difficulty_scale": "adaptive",
"resource_multiplier": 1.0,
"oxygen_consumption": 1.0,
"creature_spawn_divider": 1,
"enable_pvp": false,
"friendly_fire": false,
"shared_blueprints": true,
"ping_locations_shared": true,
"time_of_day_sync": "host",
"voice_chat_integration": "none",
"network": {
"port": 25000,
"max_latency_ms": 200,
"reconnect_timeout_s": 30,
"stun_server": "stun:stun.l.google.com:19302"
},
"api_integration": {
"openai": {
"enabled": false,
"api_key_env": "OPENAI_API_KEY",
"model": "gpt-4",
"role": "narrator"
},
"claude": {
"enabled": false,
"api_key_env": "ANTHROPIC_API_KEY",
"model": "claude-3-sonnet-20240229",
"role": "lore_engine"
}
},
"locale": "en-US",
"debug_mode": false
}
Configuration Fields
| Field | Type | Description |
|---|---|---|
session_name |
string | Display name for session |
max_players |
int | 2-8 player limit |
difficulty_scale |
enum | fixed, adaptive, manual |
resource_multiplier |
float | Multiplies harvestable resources (0.5-2.0) |
oxygen_consumption |
float | Oxygen drain rate (0.5-1.5) |
creature_spawn_divider |
int | Reduces creature count (1=normal, 2=half) |
shared_blueprints |
bool | Blueprint unlocks sync across players |
time_of_day_sync |
enum | host, all, none |
Network Configuration
{
"network": {
"port": 25000,
"max_latency_ms": 200,
"reconnect_timeout_s": 30,
"stun_server": "stun:stun.l.google.com:19302",
"use_relay": false,
"encryption": true
}
}
Console Commands
Access via BepInEx terminal (F1 key by default):
# Host a new session
/start_server
# Join existing session
/join_session <session-code>
# Example: /join_session 9B2A-4C7D-E8F1
# Check connection status
/synergy_status
# Adjust difficulty scaling mid-session
/synergy_scale <multiplier>
# Example: /synergy_scale 1.5
# Force world seed synchronization
/seed_override <seed>
# Example: /seed_override 8251
# Trigger AI narration (requires API config)
/api_narrate "<context>"
# Example: /api_narrate "exploring deep caves"
# Disconnect gracefully
/disconnect
# Force inventory resync
/sync_inventory
# List connected peers
/list_peers
Code Examples
Plugin Entry Point (C#)
using BepInEx;
using BepInEx.IL2CPP;
using HarmonyLib;
using UnityEngine;
namespace DeepSynergy
{
[BepInPlugin(GUID, Name, Version)]
public class DeepSynergyPlugin : BasePlugin
{
public const string GUID = "com.deepsynergy.subnautica2";
public const string Name = "Deep Synergy Multiplayer";
public const string Version = "1.0.0";
private SessionManager _sessionManager;
private StateSync _stateSync;
public override void Load()
{
Log.LogInfo("Loading Deep Synergy...");
// Load configuration
var config = ConfigLoader.LoadProfile();
// Initialize core systems
_sessionManager = new SessionManager(config);
_stateSync = new StateSync(config);
// Apply Harmony patches
var harmony = new Harmony(GUID);
harmony.PatchAll();
Log.LogInfo("Deep Synergy loaded successfully");
}
}
}
Session Creation Pattern
using DeepSynergy.Core;
using System;
namespace DeepSynergy.Session
{
public class SessionManager
{
private SynergyConfig _config;
private WebRTCChannel _channel;
public SessionManager(SynergyConfig config)
{
_config = config;
}
public string CreateSession()
{
var sessionCode = GenerateSessionCode();
_channel = new WebRTCChannel(_config.Network);
_channel.OnPeerConnected += HandlePeerConnect;
_channel.OnDataReceived += HandleStateUpdate;
_channel.StartHost(_config.Network.Port);
Console.WriteLine($"Session created: {sessionCode}");
return sessionCode;
}
public bool JoinSession(string sessionCode)
{
_channel = new WebRTCChannel(_config.Network);
_channel.OnConnected += () => {
Console.WriteLine("Connected to session");
SyncInitialState();
};
return _channel.Connect(sessionCode);
}
private void HandleStateUpdate(byte[] data)
{
var update = StateUpdate.Deserialize(data);
_stateSync.ApplyUpdate(update);
}
private string GenerateSessionCode()
{
var guid = Guid.NewGuid();
return $"{guid.ToString().Substring(0, 4).ToUpper()}-" +
$"{guid.ToString().Substring(4, 4).ToUpper()}-" +
$"{guid.ToString().Substring(8, 4).ToUpper()}";
}
}
}
Inventory Synchronization
using System.Security.Cryptography;
using System.Text;
namespace DeepSynergy.Sync
{
public class InventorySync
{
private Dictionary<string, InventoryState> _playerInventories;
public string ComputeMerkleRoot(InventoryState inventory)
{
var hashes = new List<string>();
foreach (var item in inventory.Items.OrderBy(i => i.Id))
{
var itemData = $"{item.Id}:{item.Quantity}:{item.Metadata}";
hashes.Add(ComputeHash(itemData));
}
return BuildMerkleTree(hashes);
}
public void SyncInventory(string playerId, InventoryState state)
{
var localHash = ComputeMerkleRoot(state);
var remoteHash = RequestRemoteHash(playerId);
if (localHash != remoteHash)
{
// Resolve conflict using timestamp authority
var resolvedState = ConflictResolver.Resolve(
state,
GetRemoteState(playerId)
);
ApplyInventoryState(playerId, resolvedState);
BroadcastUpdate(playerId, resolvedState);
}
}
private string ComputeHash(string data)
{
using (var sha256 = SHA256.Create())
{
var bytes = Encoding.UTF8.GetBytes(data);
var hash = sha256.ComputeHash(bytes);
return BitConverter.ToString(hash).Replace("-", "");
}
}
private string BuildMerkleTree(List<string> hashes)
{
if (hashes.Count == 1) return hashes[0];
var newLevel = new List<string>();
for (int i = 0; i < hashes.Count; i += 2)
{
var combined = i + 1 < hashes.Count
? hashes[i] + hashes[i + 1]
: hashes[i];
newLevel.Add(ComputeHash(combined));
}
return BuildMerkleTree(newLevel);
}
}
}
AI Narrative Integration
using System.Net.Http;
using System.Text.Json;
using System.Threading.Tasks;
namespace DeepSynergy.AI
{
public class NarrativeEngine
{
private readonly HttpClient _client;
private readonly string _apiKey;
private readonly string _model;
public NarrativeEngine(string apiKeyEnv, string model)
{
_apiKey = Environment.GetEnvironmentVariable(apiKeyEnv);
_model = model;
_client = new HttpClient();
}
public async Task<string> GenerateNarration(string context)
{
if (string.IsNullOrEmpty(_apiKey))
{
return "API key not configured";
}
var request = new
{
model = _model,
messages = new[]
{
new { role = "system", content = "You are a narrator for a co-op ocean survival game. Generate brief, atmospheric journal entries based on player actions." },
new { role = "user", content = $"Context: {context}" }
},
max_tokens = 150,
temperature = 0.7
};
_client.DefaultRequestHeaders.Clear();
_client.DefaultRequestHeaders.Add("Authorization", $"Bearer {_apiKey}");
var response = await _client.PostAsJsonAsync(
"https://api.openai.com/v1/chat/completions",
request
);
var result = await response.Content.ReadAsStringAsync();
var json = JsonDocument.Parse(result);
return json.RootElement
.GetProperty("choices")[0]
.GetProperty("message")
.GetProperty("content")
.GetString();
}
}
}
Adaptive Scaling Implementation
namespace DeepSynergy.Scaling
{
public class DifficultyScaler
{
private SynergyConfig _config;
private int _playerCount;
public void UpdatePlayerCount(int count)
{
_playerCount = count;
if (_config.DifficultyScale == "adaptive")
{
AdjustGameParameters();
}
}
private void AdjustGameParameters()
{
// Scale creature spawns inversely with player count
var creatureMultiplier = 1.0f / (_playerCount * _config.CreatureSpawnDivider);
Game.CreatureSpawner.SetGlobalMultiplier(creatureMultiplier);
// Scale resource availability
var resourceMultiplier = _config.ResourceMultiplier *
Mathf.Lerp(1.0f, 1.5f, (_playerCount - 1) / 7f);
Game.ResourceManager.SetMultiplier(resourceMultiplier);
// Adjust oxygen consumption (more players = slightly more efficient)
var oxygenEfficiency = _config.OxygenConsumption *
Mathf.Lerp(1.0f, 0.85f, (_playerCount - 1) / 7f);
Game.Player.SetOxygenConsumption(oxygenEfficiency);
}
}
}
Common Patterns
Session Lifecycle
// Host workflow
var session = new SessionManager(config);
var sessionCode = session.CreateSession();
Console.WriteLine($"Share code: {sessionCode}");
// Client workflow
var session = new SessionManager(config);
session.JoinSession("9B2A-4C7D-E8F1");
// Graceful disconnect
session.Disconnect();
State Synchronization
// Send state update
var update = new StateUpdate
{
Timestamp = DateTime.UtcNow.Ticks,
PlayerId = localPlayerId,
Type = UpdateType.InventoryChange,
Data = SerializeInventory()
};
_channel.Broadcast(update.Serialize());
// Receive and apply update
_channel.OnDataReceived += (data) => {
var update = StateUpdate.Deserialize(data);
if (ConflictResolver.ShouldApply(update))
{
ApplyStateUpdate(update);
}
};
Error Handling
try
{
session.JoinSession(sessionCode);
}
catch (SessionNotFoundException)
{
Console.WriteLine("Session not found. Check code.");
}
catch (NetworkTimeoutException)
{
Console.WriteLine("Connection timeout. Check firewall/NAT.");
}
catch (VersionMismatchException ex)
{
Console.WriteLine($"Mod version mismatch: {ex.Message}");
}
Troubleshooting
Connection Issues
Symptom: Cannot connect to session
Solutions:
- Verify port forwarding (default 25000)
- Check firewall rules for Subnautica2.exe and BepInEx
- Confirm both players have identical mod versions
- Enable debug mode:
"debug_mode": truein config - Try changing STUN server in network config
# Check logs
tail -f BepInEx/LogOutput.log | grep DeepSynergy
# Common error patterns:
# "WebRTC connection timeout" → NAT/firewall issue
# "Version mismatch" → Update mod to matching version
# "Session not found" → Invalid session code
Inventory Desync
Symptom: Players see different inventories
Solutions:
# Force resync via console
/sync_inventory
# Or programmatically
inventorySync.ForceFullSync();
Check for conflicting mods:
# Disable other inventory mods temporarily
# Verify DeepSynergy.dll is latest version
API Integration Not Working
Symptom: AI narration not generating
Solutions:
# Verify environment variables
echo $OPENAI_API_KEY # Linux/Mac
echo %OPENAI_API_KEY% # Windows
# Test API key
curl https://api.openai.com/v1/models \
-H "Authorization: Bearer $OPENAI_API_KEY"
Check config:
{
"api_integration": {
"openai": {
"enabled": true,
"api_key_env": "OPENAI_API_KEY", // Must match env var name
"model": "gpt-4"
}
}
}
Performance Issues
Symptom: Lag/stuttering with 3+ players
Solutions:
- Reduce creature spawn rate:
{
"creature_spawn_divider": 2 // Halves creature count
}
- Lower sync frequency (advanced):
StateSync.SetUpdateInterval(100); // ms, default 50
- Disable API features:
{
"api_integration": {
"openai": { "enabled": false },
"claude": { "enabled": false }
}
}
Log Analysis
# Enable debug logging
# In synergy_profile.json:
"debug_mode": true
# Key log patterns:
# "[StateSync] Merkle mismatch" → Inventory desync
# "[Network] Peer disconnected" → Connection loss
# "[API] Rate limit exceeded" → Reduce API calls
Advanced Configuration
Custom Scaling Formula
// Override default scaling in plugin
public class CustomScaler : DifficultyScaler
{
protected override float CalculateCreatureMultiplier(int players)
{
// Custom formula: exponential reduction
return Mathf.Pow(0.8f, players - 1);
}
}
Session Migration
// Implement host migration on disconnect
_channel.OnHostDisconnected += () => {
var survivingPeer = _peers.OrderBy(p => p.Latency).First();
if (survivingPeer.Id == localPlayerId)
{
PromoteToHost();
}
};
private void PromoteToHost()
{
_channel.BecomeHost();
BroadcastFullState();
}
Blueprint Sharing
// Sync blueprint unlocks
Game.BlueprintManager.OnUnlock += (blueprint) => {
if (_config.SharedBlueprints)
{
var update = new StateUpdate
{
Type = UpdateType.BlueprintUnlock,
Data = Encoding.UTF8.GetBytes(blueprint.Id)
};
_channel.Broadcast(update.Serialize());
}
};
Environment Variables
# API keys (never commit these)
export OPENAI_API_KEY="sk-..."
export ANTHROPIC_API_KEY="sk-ant-..."
# Optional network overrides
export SYNERGY_PORT=25000
export SYNERGY_STUN_SERVER="stun:custom.server.com:3478"
Compatibility
- OS: Windows 10/11, macOS 11+, Linux (Ubuntu 20.04+, Steam Deck)
- BepInEx: 6.0.x (IL2CPP)
- Game Version: Subnautica 2 (2026 release)
- Conflicting Mods: Avoid other multiplayer/inventory mods