diff --git a/.gitignore b/.gitignore
index 1e1e7ac..a86280e 100644
--- a/.gitignore
+++ b/.gitignore
@@ -399,3 +399,6 @@ FodyWeavers.xsd
# JetBrains Rider
*.sln.iml
/Server
+
+# Plan files (local only)
+plan.md
diff --git a/BossNotifier.csproj b/BossNotifier.csproj
index 71e262d..0cbfda3 100644
--- a/BossNotifier.csproj
+++ b/BossNotifier.csproj
@@ -1,6 +1,5 @@
-
-
-
+
+
Debug
AnyCPU
@@ -9,10 +8,12 @@
Properties
BossNotifier
BossNotifier
- v4.7.1
+ netstandard2.1
512
true
-
+ latest
+ false
+ $(DefaultItemExcludes);Fika\**
true
@@ -36,10 +37,6 @@
Always
-
-
-
-
References\0Harmony.dll
@@ -56,10 +53,12 @@
False
References\Comfort.dll
+
+ References\Comfort.Unity.dll
+
References\spt-reflection.dll
-
References\UnityEngine.dll
@@ -67,11 +66,14 @@
References\UnityEngine.CoreModule.dll
-
- mkdir $(TargetDir)BepInEx\plugins\
-copy /Y "$(TargerDir)$(TargetFileName)" "C:\Games\SPT 3.10.3\BepInEx\plugins\$(TargetFileName)"
-copy /Y "$(TargetDir)$(TargetFileName)" "$(TargetDir)BepInEx\plugins\$(TargetFileName)"
-powershell.exe -command Compress-Archive -Force -Path BepInEx $(TargetName).zip
+ C:\SPT
-
\ No newline at end of file
+
+
+
+
+
+
+
+
diff --git a/Fika/BossNotifier.Fika.csproj b/Fika/BossNotifier.Fika.csproj
new file mode 100644
index 0000000..d2d1a2f
--- /dev/null
+++ b/Fika/BossNotifier.Fika.csproj
@@ -0,0 +1,50 @@
+
+
+
+ netstandard2.1
+ latest
+ false
+
+
+
+
+ ..\References\0Harmony.dll
+
+
+ ..\References\Assembly-CSharp.dll
+
+
+ ..\References\BepInEx.dll
+
+
+ ..\References\Comfort.dll
+
+
+ ..\References\Comfort.Unity.dll
+
+
+ ..\References\Fika.Core.dll
+
+
+ ..\References\UnityEngine.dll
+
+
+ ..\References\UnityEngine.CoreModule.dll
+
+
+
+
+
+ ..\bin\Release\netstandard2.1\BossNotifier.dll
+
+
+
+
+ C:\SPT
+
+
+
+
+
+
+
diff --git a/Fika/FikaIntegration.cs b/Fika/FikaIntegration.cs
new file mode 100644
index 0000000..5862bc1
--- /dev/null
+++ b/Fika/FikaIntegration.cs
@@ -0,0 +1,301 @@
+using Fika.Core.Networking;
+using Fika.Core.Modding;
+using Fika.Core.Modding.Events;
+using Comfort.Common;
+using Fika.Core.Networking.LiteNetLib;
+using Fika.Core.Networking.LiteNetLib.Utils;
+using BepInEx.Logging;
+using System;
+using System.Collections.Generic;
+using EFT.Communications;
+using EFT;
+
+namespace BossNotifier
+{
+ public static class FikaIntegration
+ {
+ public static void Initialize()
+ {
+ BossNotifierPlugin.Log(LogLevel.Info, "[BossNotifier] [Fika] Initializing Fika integration...");
+
+ try
+ {
+ // Subscribe to Fika network manager creation event
+ FikaEventDispatcher.SubscribeEvent(OnNetworkManagerCreated);
+ BossNotifierPlugin.Log(LogLevel.Info, "[BossNotifier] [Fika] Subscribed to FikaNetworkManagerCreatedEvent");
+ }
+ catch (Exception ex)
+ {
+ BossNotifierPlugin.Log(LogLevel.Error, $"[BossNotifier] [Fika] Failed to subscribe to event: {ex}");
+ throw;
+ }
+ }
+
+ private static void OnNetworkManagerCreated(FikaNetworkManagerCreatedEvent e)
+ {
+ BossNotifierPlugin.Log(LogLevel.Info, "[BossNotifier] [Fika] FikaNetworkManagerCreatedEvent received!");
+
+ try
+ {
+ // Register packet handlers
+ BossNotifierPlugin.Log(LogLevel.Debug, "[BossNotifier] [Fika] Registering RequestBossInfoPacket...");
+ e.Manager.RegisterPacket(OnRequestBossInfo);
+
+ BossNotifierPlugin.Log(LogLevel.Debug, "[BossNotifier] [Fika] Registering BossInfoPacket...");
+ e.Manager.RegisterPacket(OnBossInfoReceived);
+
+ BossNotifierPlugin.Log(LogLevel.Debug, "[BossNotifier] [Fika] Registering VicinityNotificationPacket...");
+ e.Manager.RegisterPacket(OnVicinityNotificationReceived);
+
+ BossNotifierPlugin.Log(LogLevel.Info, "[BossNotifier] [Fika] Packets registered successfully!");
+ }
+ catch (Exception ex)
+ {
+ BossNotifierPlugin.Log(LogLevel.Error, $"[BossNotifier] [Fika] Error registering packets: {ex}");
+ throw;
+ }
+ }
+
+ // Handler: Client requests boss info (Host-side)
+ private static void OnRequestBossInfo(RequestBossInfoPacket packet, NetPeer peer)
+ {
+ BossNotifierPlugin.Log(LogLevel.Info, $"[BossNotifier] [Fika] RequestBossInfo received from peer {peer?.Id ?? -1}");
+
+ try
+ {
+ // Check if we're the host
+ bool isHost = BossNotifierPlugin.IsHost();
+ BossNotifierPlugin.Log(LogLevel.Debug, $"[BossNotifier] [Fika] IsHost: {isHost}");
+
+ if (!isHost)
+ {
+ BossNotifierPlugin.Log(LogLevel.Warning, "[BossNotifier] [Fika] Non-host received request, ignoring");
+ return;
+ }
+
+ if (BossNotifierMono.Instance == null)
+ {
+ BossNotifierPlugin.Log(LogLevel.Warning, "[BossNotifier] [Fika] BossNotifierMono not initialized, cannot respond");
+ return;
+ }
+
+ BossNotifierPlugin.Log(LogLevel.Debug, $"[BossNotifier] [Fika] Preparing response...");
+ BossNotifierPlugin.Log(LogLevel.Debug, $"[BossNotifier] [Fika] Bosses in raid: {BossLocationSpawnPatch.bossesInRaid.Count}");
+ BossNotifierPlugin.Log(LogLevel.Debug, $"[BossNotifier] [Fika] Spawned bosses: {BotBossPatch.spawnedBosses.Count}");
+
+ // Create response packet with current boss data
+ BossInfoPacket response = new BossInfoPacket
+ {
+ bossesInRaid = new Dictionary(BossLocationSpawnPatch.bossesInRaid),
+ spawnedBosses = new HashSet(BotBossPatch.spawnedBosses)
+ };
+
+ // Send response ONLY to the requesting peer
+ var manager = Singleton.Instance;
+ if (manager == null)
+ {
+ BossNotifierPlugin.Log(LogLevel.Error, "[BossNotifier] [Fika] Network manager is null, cannot send response");
+ return;
+ }
+
+ BossNotifierPlugin.Log(LogLevel.Info, $"[BossNotifier] [Fika] Sending BossInfoPacket to peer {peer.Id}...");
+ manager.SendDataToPeer(ref response, DeliveryMethod.ReliableOrdered, peer);
+ BossNotifierPlugin.Log(LogLevel.Info, $"[BossNotifier] [Fika] Response sent successfully!");
+ }
+ catch (Exception ex)
+ {
+ BossNotifierPlugin.Log(LogLevel.Error, $"[BossNotifier] [Fika] Error handling boss info request: {ex}");
+ throw;
+ }
+ }
+
+ // Handler: Host sends boss info (Client-side)
+ private static void OnBossInfoReceived(BossInfoPacket packet)
+ {
+ BossNotifierPlugin.Log(LogLevel.Info, "[BossNotifier] [Fika] BossInfoPacket received!");
+
+ try
+ {
+ // Check if we're a client
+ bool isClient = BossNotifierPlugin.IsClient();
+ BossNotifierPlugin.Log(LogLevel.Debug, $"[BossNotifier] [Fika] IsClient: {isClient}");
+
+ if (!isClient)
+ {
+ BossNotifierPlugin.Log(LogLevel.Warning, "[BossNotifier] [Fika] Non-client received BossInfoPacket, ignoring");
+ return;
+ }
+
+ if (BossNotifierMono.Instance == null)
+ {
+ BossNotifierPlugin.Log(LogLevel.Warning, "[BossNotifier] [Fika] BossNotifierMono not initialized, cannot display");
+ return;
+ }
+
+ BossNotifierPlugin.Log(LogLevel.Debug, $"[BossNotifier] [Fika] Packet contains {packet.bossesInRaid?.Count ?? 0} bosses, {packet.spawnedBosses?.Count ?? 0} spawned");
+
+ // Process boss info directly (avoid passing Fika types to Plugin.cs)
+ ProcessReceivedBossInfo(packet);
+ BossNotifierPlugin.Log(LogLevel.Info, "[BossNotifier] [Fika] Boss info processed and displayed");
+ }
+ catch (Exception ex)
+ {
+ BossNotifierPlugin.Log(LogLevel.Error, $"[BossNotifier] [Fika] Error receiving boss info: {ex}");
+ throw;
+ }
+ }
+
+ // Client method: Send boss info request to host
+ public static void SendBossInfoRequest()
+ {
+ BossNotifierPlugin.Log(LogLevel.Info, "[BossNotifier] [Fika] SendBossInfoRequest() called");
+
+ var manager = Singleton.Instance;
+ if (manager == null)
+ {
+ BossNotifierPlugin.Log(LogLevel.Error, "[BossNotifier] [Fika] Network manager not available, cannot request boss info");
+ return;
+ }
+
+ BossNotifierPlugin.Log(LogLevel.Info, "[BossNotifier] [Fika] Sending RequestBossInfoPacket to host...");
+ RequestBossInfoPacket request = new RequestBossInfoPacket();
+ manager.SendData(ref request, DeliveryMethod.ReliableOrdered);
+ BossNotifierPlugin.Log(LogLevel.Info, "[BossNotifier] [Fika] Request sent!");
+ }
+
+ // Process received boss info from host (Client-side)
+ private static void ProcessReceivedBossInfo(BossInfoPacket packet)
+ {
+ BossNotifierPlugin.Log(LogLevel.Info, "[BossNotifier] [Fika] ProcessReceivedBossInfo() called");
+
+ // Check intel center requirement (using client's own level)
+ if (BossNotifierMono.Instance.intelCenterLevel < BossNotifierPlugin.intelCenterUnlockLevel.Value)
+ {
+ BossNotifierPlugin.Log(LogLevel.Info, $"[BossNotifier] [Fika] Intel center requirement not met ({BossNotifierMono.Instance.intelCenterLevel} < {BossNotifierPlugin.intelCenterUnlockLevel.Value})");
+ return;
+ }
+
+ // If no bosses, show that
+ if (packet.bossesInRaid == null || packet.bossesInRaid.Count == 0)
+ {
+ BossNotifierPlugin.Log(LogLevel.Info, "[BossNotifier] [Fika] No bosses in raid");
+ NotificationManagerClass.DisplayMessageNotification("No Bosses Located", ENotificationDurationType.Long);
+ return;
+ }
+
+ // Display boss notifications from received data
+ BossNotifierPlugin.Log(LogLevel.Info, "[BossNotifier] [Fika] Displaying boss notifications from received data");
+ DisplayBossNotificationsFromReceivedData(packet.bossesInRaid, packet.spawnedBosses);
+ }
+
+ // Display boss notifications from received packet data (Client-side)
+ private static void DisplayBossNotificationsFromReceivedData(Dictionary bosses, HashSet spawned)
+ {
+ BossNotifierPlugin.Log(LogLevel.Info, "[BossNotifier] [Fika] DisplayBossNotificationsFromReceivedData() called");
+
+ // Use client's own intel center level (not host's)
+ int intelLevel = BossNotifierMono.Instance.intelCenterLevel;
+
+ // Check if it's daytime to prevent showing Cultist notif (with null checks)
+ bool isDayTime = false;
+ try {
+ if (Singleton.Instance != null &&
+ Singleton.Instance.BotsController != null &&
+ Singleton.Instance.BotsController.ZonesLeaveController != null)
+ {
+ isDayTime = Singleton.Instance.BotsController.ZonesLeaveController.IsDay();
+ }
+ } catch (Exception ex) {
+ BossNotifierPlugin.Log(LogLevel.Warning, $"[BossNotifier] [Fika] Could not determine day/night: {ex.Message}");
+ }
+ BossNotifierPlugin.Log(LogLevel.Debug, $"[BossNotifier] [Fika] Is daytime: {isDayTime}");
+
+ // Get whether location is unlocked
+ bool isLocationUnlocked = intelLevel >= BossNotifierPlugin.intelCenterLocationUnlockLevel.Value;
+ BossNotifierPlugin.Log(LogLevel.Debug, $"[BossNotifier] [Fika] Location unlocked: {isLocationUnlocked} ({intelLevel} >= {BossNotifierPlugin.intelCenterLocationUnlockLevel.Value})");
+
+ // Get whether detection is unlocked
+ bool isDetectionUnlocked = intelLevel >= BossNotifierPlugin.intelCenterDetectedUnlockLevel.Value;
+ BossNotifierPlugin.Log(LogLevel.Debug, $"[BossNotifier] [Fika] Detection unlocked: {isDetectionUnlocked} ({intelLevel} >= {BossNotifierPlugin.intelCenterDetectedUnlockLevel.Value})");
+
+ int displayedCount = 0;
+ foreach (var bossSpawn in bosses)
+ {
+ // If it's daytime then cultists don't spawn
+ if (isDayTime && bossSpawn.Key.Equals("Cultists"))
+ {
+ BossNotifierPlugin.Log(LogLevel.Debug, $"[BossNotifier] [Fika] Skipping Cultists (daytime)");
+ continue;
+ }
+
+ // If boss has been spawned/detected
+ bool isDetected = spawned != null && spawned.Contains(bossSpawn.Key);
+
+ string notificationMessage;
+ // If we don't have locations or value is null/whitespace
+ if (!isLocationUnlocked || bossSpawn.Value == null || bossSpawn.Value.Equals(""))
+ {
+ // Then just show that they spawned and nothing else
+ notificationMessage = $"{bossSpawn.Key} {(BossNotifierPlugin.pluralBosses.Contains(bossSpawn.Key) ? "have" : "has")} been located.{(isDetectionUnlocked && isDetected ? $" ✓" : "")}";
+ }
+ else
+ {
+ // Location is unlocked and location isnt null
+ notificationMessage = $"{bossSpawn.Key} {(BossNotifierPlugin.pluralBosses.Contains(bossSpawn.Key) ? "have" : "has")} been located near {bossSpawn.Value}{(isDetectionUnlocked && isDetected ? $" ✓" : "")}";
+ }
+
+ BossNotifierPlugin.Log(LogLevel.Info, $"[BossNotifier] [Fika] Displaying: {notificationMessage}");
+ NotificationManagerClass.DisplayMessageNotification(notificationMessage, ENotificationDurationType.Long);
+ displayedCount++;
+ }
+
+ BossNotifierPlugin.Log(LogLevel.Info, $"[BossNotifier] [Fika] Displayed {displayedCount} boss notifications");
+ }
+
+ // Handler: Vicinity notification received (Client-side)
+ private static void OnVicinityNotificationReceived(VicinityNotificationPacket packet)
+ {
+ BossNotifierPlugin.Log(LogLevel.Info, $"[BossNotifier] [Fika] VicinityNotificationPacket received: {packet.message}");
+
+ try
+ {
+ // Check if we're a client
+ bool isClient = BossNotifierPlugin.IsClient();
+ BossNotifierPlugin.Log(LogLevel.Debug, $"[BossNotifier] [Fika] IsClient: {isClient}");
+
+ if (!isClient)
+ {
+ BossNotifierPlugin.Log(LogLevel.Warning, "[BossNotifier] [Fika] Non-client received VicinityNotificationPacket, ignoring");
+ return;
+ }
+
+ // Enqueue the message for display
+ BossNotifierPlugin.Log(LogLevel.Debug, $"[BossNotifier] [Fika] Enqueueing vicinity notification: {packet.message}");
+ BotBossPatch.vicinityNotifications.Enqueue(packet.message);
+ }
+ catch (Exception ex)
+ {
+ BossNotifierPlugin.Log(LogLevel.Error, $"[BossNotifier] [Fika] Error receiving vicinity notification: {ex}");
+ throw;
+ }
+ }
+
+ // Host method: Send vicinity notification to all clients
+ public static void SendVicinityNotificationToClients(string message)
+ {
+ BossNotifierPlugin.Log(LogLevel.Info, $"[BossNotifier] [Fika] SendVicinityNotificationToClients() called: {message}");
+
+ var manager = Singleton.Instance;
+ if (manager == null)
+ {
+ BossNotifierPlugin.Log(LogLevel.Error, "[BossNotifier] [Fika] Network manager not available, cannot send vicinity notification");
+ return;
+ }
+
+ BossNotifierPlugin.Log(LogLevel.Info, "[BossNotifier] [Fika] Sending VicinityNotificationPacket to all clients...");
+ VicinityNotificationPacket packet = new VicinityNotificationPacket { message = message };
+ manager.SendData(ref packet, DeliveryMethod.ReliableOrdered);
+ BossNotifierPlugin.Log(LogLevel.Info, "[BossNotifier] [Fika] Vicinity notification sent!");
+ }
+ }
+}
diff --git a/Fika/FikaPackets.cs b/Fika/FikaPackets.cs
new file mode 100644
index 0000000..fea5266
--- /dev/null
+++ b/Fika/FikaPackets.cs
@@ -0,0 +1,110 @@
+using Fika.Core.Networking.LiteNetLib.Utils;
+using System.Collections.Generic;
+using BepInEx.Logging;
+
+namespace BossNotifier
+{
+ // Client → Host: Request boss information
+ public class RequestBossInfoPacket : INetSerializable
+ {
+ // Empty packet - just signals request
+ public void Serialize(NetDataWriter writer)
+ {
+ BossNotifierPlugin.Log(LogLevel.Debug, "[BossNotifier] [Packet] Serializing RequestBossInfoPacket");
+ }
+
+ public void Deserialize(NetDataReader reader)
+ {
+ BossNotifierPlugin.Log(LogLevel.Debug, "[BossNotifier] [Packet] Deserializing RequestBossInfoPacket");
+ }
+ }
+
+ // Host → Client: Response with boss data
+ public class BossInfoPacket : INetSerializable
+ {
+ public Dictionary bossesInRaid;
+ public HashSet spawnedBosses;
+
+ public void Serialize(NetDataWriter writer)
+ {
+ BossNotifierPlugin.Log(LogLevel.Debug, $"[BossNotifier] [Packet] Serializing BossInfoPacket: {bossesInRaid?.Count ?? 0} bosses, {spawnedBosses?.Count ?? 0} spawned");
+
+ // Serialize bossesInRaid
+ if (bossesInRaid == null)
+ {
+ writer.Put(0);
+ }
+ else
+ {
+ writer.Put(bossesInRaid.Count);
+ foreach (var kvp in bossesInRaid)
+ {
+ writer.Put(kvp.Key);
+ writer.Put(kvp.Value);
+ BossNotifierPlugin.Log(LogLevel.Debug, $"[BossNotifier] [Packet] Boss: {kvp.Key} @ {kvp.Value}");
+ }
+ }
+
+ // Serialize spawnedBosses
+ if (spawnedBosses == null)
+ {
+ writer.Put(0);
+ }
+ else
+ {
+ writer.Put(spawnedBosses.Count);
+ foreach (var boss in spawnedBosses)
+ {
+ writer.Put(boss);
+ BossNotifierPlugin.Log(LogLevel.Debug, $"[BossNotifier] [Packet] Spawned: {boss}");
+ }
+ }
+ }
+
+ public void Deserialize(NetDataReader reader)
+ {
+ BossNotifierPlugin.Log(LogLevel.Debug, "[BossNotifier] [Packet] Deserializing BossInfoPacket");
+
+ // Deserialize bossesInRaid
+ int bossCount = reader.GetInt();
+ bossesInRaid = new Dictionary();
+ BossNotifierPlugin.Log(LogLevel.Debug, $"[BossNotifier] [Packet] Reading {bossCount} bosses");
+ for (int i = 0; i < bossCount; i++)
+ {
+ string key = reader.GetString();
+ string value = reader.GetString();
+ bossesInRaid[key] = value;
+ BossNotifierPlugin.Log(LogLevel.Debug, $"[BossNotifier] [Packet] {key} @ {value}");
+ }
+
+ // Deserialize spawnedBosses
+ int spawnedCount = reader.GetInt();
+ spawnedBosses = new HashSet();
+ BossNotifierPlugin.Log(LogLevel.Debug, $"[BossNotifier] [Packet] Reading {spawnedCount} spawned bosses");
+ for (int i = 0; i < spawnedCount; i++)
+ {
+ string boss = reader.GetString();
+ spawnedBosses.Add(boss);
+ BossNotifierPlugin.Log(LogLevel.Debug, $"[BossNotifier] [Packet] {boss}");
+ }
+ }
+ }
+
+ // Host → Clients: Vicinity notification when boss spawns during raid
+ public class VicinityNotificationPacket : INetSerializable
+ {
+ public string message;
+
+ public void Serialize(NetDataWriter writer)
+ {
+ BossNotifierPlugin.Log(LogLevel.Debug, $"[BossNotifier] [Packet] Serializing VicinityNotificationPacket: {message}");
+ writer.Put(message ?? "");
+ }
+
+ public void Deserialize(NetDataReader reader)
+ {
+ message = reader.GetString();
+ BossNotifierPlugin.Log(LogLevel.Debug, $"[BossNotifier] [Packet] Deserializing VicinityNotificationPacket: {message}");
+ }
+ }
+}
diff --git a/Plugin.cs b/Plugin.cs
index 2012901..22b7249 100644
--- a/Plugin.cs
+++ b/Plugin.cs
@@ -2,6 +2,7 @@
using SPT.Reflection.Patching;
using SPT.Reflection.Utils;
using System.Reflection;
+using System.IO;
using UnityEngine;
using EFT.Communications;
using EFT;
@@ -18,10 +19,11 @@
#pragma warning disable IDE0051 // Remove unused private members
namespace BossNotifier {
- [BepInPlugin("Mattdokn.BossNotifier", "BossNotifier", "1.5.4")]
+ [BepInPlugin("Mattdokn.BossNotifier", "BossNotifier", "2.0.1")]
[BepInDependency("com.fika.core", BepInDependency.DependencyFlags.SoftDependency)]
public class BossNotifierPlugin : BaseUnityPlugin {
- public static FieldInfo FikaIsPlayerHost;
+ public static PropertyInfo FikaIsPlayerHost;
+ private static Type FikaIntegrationType;
// Configuration entries
@@ -109,10 +111,55 @@ public static void Log(LogLevel level, string msg) {
private void Awake() {
logger = Logger;
+ Log(LogLevel.Info, "[BossNotifier] Awake() called");
- Type FikaUtilExternalType = Type.GetType("Fika.Core.Coop.Utils.FikaBackendUtils, Fika.Core", false);
+ // Detect Fika EARLY to subscribe to network events before they fire
+ Type FikaUtilExternalType = Type.GetType("Fika.Core.Main.Utils.FikaBackendUtils, Fika.Core", false);
if (FikaUtilExternalType != null) {
- FikaIsPlayerHost = AccessTools.Field(FikaUtilExternalType, "MatchingType");
+ // Search for Fika.Core assembly
+ System.Reflection.Assembly fikaAssembly = null;
+ foreach (var assembly in AppDomain.CurrentDomain.GetAssemblies()) {
+ if (assembly.GetName().Name == "Fika.Core") {
+ fikaAssembly = assembly;
+ break;
+ }
+ }
+
+ if (fikaAssembly != null) {
+ Type fikaBackendUtils = fikaAssembly.GetType("Fika.Core.Main.Utils.FikaBackendUtils");
+ if (fikaBackendUtils != null) {
+ FikaIsPlayerHost = AccessTools.Property(fikaBackendUtils, "ClientType");
+ Log(LogLevel.Info, "[BossNotifier] ✓ Fika detected in Awake! Initializing integration...");
+
+ try {
+ // Load BossNotifier.FikaOptional.dll.bin from same directory as main DLL
+ string pluginDir = Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location);
+ string fikaPath = Path.Combine(pluginDir, "BossNotifier.FikaOptional.dll.bin");
+
+ if (File.Exists(fikaPath)) {
+ Log(LogLevel.Info, $"[BossNotifier] Loading Fika integration from {fikaPath}");
+ Assembly fikaAsm = Assembly.LoadFrom(fikaPath);
+ FikaIntegrationType = fikaAsm.GetType("BossNotifier.FikaIntegration");
+
+ if (FikaIntegrationType != null) {
+ MethodInfo initMethod = FikaIntegrationType.GetMethod("Initialize", BindingFlags.Public | BindingFlags.Static);
+ if (initMethod != null) {
+ initMethod.Invoke(null, null);
+ Log(LogLevel.Info, "[BossNotifier] ✓ Fika integration initialized in Awake");
+ } else {
+ Log(LogLevel.Error, "[BossNotifier] Could not find Initialize method in FikaIntegration");
+ }
+ } else {
+ Log(LogLevel.Error, "[BossNotifier] Could not load FikaIntegration type from assembly");
+ }
+ } else {
+ Log(LogLevel.Info, "[BossNotifier] BossNotifier.FikaOptional.dll.bin not found, running in singleplayer mode");
+ }
+ } catch (Exception ex) {
+ Log(LogLevel.Error, $"[BossNotifier] Failed to initialize Fika integration: {ex}");
+ }
+ }
+ }
}
// Initialize configuration entries
@@ -176,6 +223,78 @@ public static string GetZoneName(string zoneId) {
}
return sb.ToString().Replace("_", " ").Trim();
}
+
+ // Fika helper methods
+ public static bool IsFikaInstalled() {
+ bool installed = FikaIsPlayerHost != null;
+ Log(LogLevel.Debug, $"[BossNotifier] IsFikaInstalled: {installed}");
+ return installed;
+ }
+
+ public static bool IsHost() {
+ if (!IsFikaInstalled()) {
+ Log(LogLevel.Debug, "[BossNotifier] IsHost: false (Fika not installed)");
+ return false;
+ }
+ int clientType = (int)FikaIsPlayerHost.GetValue(null);
+ bool isHost = clientType == 2; // 2 = Host
+ Log(LogLevel.Debug, $"[BossNotifier] IsHost: {isHost} (ClientType: {clientType})");
+ return isHost;
+ }
+
+ public static bool IsClient() {
+ if (!IsFikaInstalled()) {
+ Log(LogLevel.Debug, "[BossNotifier] IsClient: false (Fika not installed)");
+ return false;
+ }
+ int clientType = (int)FikaIsPlayerHost.GetValue(null);
+ bool isClient = clientType == 1; // 1 = Client
+ Log(LogLevel.Debug, $"[BossNotifier] IsClient: {isClient} (ClientType: {clientType})");
+ return isClient;
+ }
+
+ public static bool IsSingleplayer() {
+ bool isSP = !IsFikaInstalled();
+ Log(LogLevel.Debug, $"[BossNotifier] IsSingleplayer: {isSP}");
+ return isSP;
+ }
+
+ // Reflection-based helper methods to call FikaIntegration
+ public static void SendVicinityNotificationToClients(string message) {
+ if (FikaIntegrationType == null) {
+ Log(LogLevel.Warning, "[BossNotifier] FikaIntegration type not loaded, cannot send vicinity notification");
+ return;
+ }
+
+ try {
+ MethodInfo method = FikaIntegrationType.GetMethod("SendVicinityNotificationToClients", BindingFlags.Public | BindingFlags.Static);
+ if (method != null) {
+ method.Invoke(null, new object[] { message });
+ } else {
+ Log(LogLevel.Error, "[BossNotifier] Could not find SendVicinityNotificationToClients method");
+ }
+ } catch (Exception ex) {
+ Log(LogLevel.Error, $"[BossNotifier] Error calling SendVicinityNotificationToClients: {ex}");
+ }
+ }
+
+ public static void SendBossInfoRequest() {
+ if (FikaIntegrationType == null) {
+ Log(LogLevel.Warning, "[BossNotifier] FikaIntegration type not loaded, cannot send boss info request");
+ return;
+ }
+
+ try {
+ MethodInfo method = FikaIntegrationType.GetMethod("SendBossInfoRequest", BindingFlags.Public | BindingFlags.Static);
+ if (method != null) {
+ method.Invoke(null, null);
+ } else {
+ Log(LogLevel.Error, "[BossNotifier] Could not find SendBossInfoRequest method");
+ }
+ } catch (Exception ex) {
+ Log(LogLevel.Error, $"[BossNotifier] Error calling SendBossInfoRequest: {ex}");
+ }
+ }
}
// Patch for tracking boss location spawns
@@ -188,7 +307,7 @@ internal class BossLocationSpawnPatch : ModulePatch {
// Add boss spawn if not already present
private static void TryAddBoss(string boss, string location) {
if (location == null) {
- Logger.LogError("Tried to add boss with null location.");
+ BossNotifierPlugin.Log(LogLevel.Error, "Tried to add boss with null location.");
return;
}
// If boss is already added
@@ -212,27 +331,32 @@ private static void TryAddBoss(string boss, string location) {
// Handle boss location spawns
[PatchPostfix]
private static void PatchPostfix(BossLocationSpawn __instance) {
- // If the boss will spawn
- if (__instance.ShallSpawn) {
- // Get it's name, if no name found then return.
- string name = BossNotifierPlugin.GetBossName(__instance.BossType);
- if (name == null) return;
-
- // Get the spawn location
- string location = BossNotifierPlugin.GetZoneName(__instance.BornZone);
-
- BossNotifierPlugin.Log(LogLevel.Info, $"Boss {name} @ zone {__instance.BornZone} translated to {(location == null ? __instance.BornZone.Replace("Bot", "").Replace("Zone", ""): location)}");
-
- if (location == null) {
- // If it's null then use cleaned up BornZone
- TryAddBoss(name, __instance.BornZone.Replace("Bot", "").Replace("Zone", ""));
- } else if (location.Equals("")) {
- // If it's empty location (Factory Spawn)
- TryAddBoss(name, "");
- } else {
- // Location is valid
- TryAddBoss(name, location);
+ try {
+ // If the boss will spawn
+ if (__instance.ShallSpawn) {
+ // Get it's name, if no name found then return.
+ string name = BossNotifierPlugin.GetBossName(__instance.BossType);
+ if (name == null) return;
+
+ // Get the spawn location
+ string location = BossNotifierPlugin.GetZoneName(__instance.BornZone);
+
+ BossNotifierPlugin.Log(LogLevel.Info, $"Boss {name} @ zone {__instance.BornZone} translated to {(location == null ? __instance.BornZone.Replace("Bot", "").Replace("Zone", ""): location)}");
+
+ if (location == null) {
+ // If it's null then use cleaned up BornZone
+ TryAddBoss(name, __instance.BornZone.Replace("Bot", "").Replace("Zone", ""));
+ } else if (location.Equals("")) {
+ // If it's empty location (Factory Spawn)
+ TryAddBoss(name, "");
+ } else {
+ // Location is valid
+ TryAddBoss(name, location);
+ }
}
+ } catch (Exception ex) {
+ BossNotifierPlugin.Log(LogLevel.Error, $"[BossNotifier] Error in BossLocationSpawnPatch: {ex}");
+ throw;
}
}
}
@@ -249,37 +373,53 @@ internal class BotBossPatch : ModulePatch {
[PatchPostfix]
private static void PatchPostfix(BotBoss __instance) {
- WildSpawnType role = __instance.Owner.Profile.Info.Settings.Role;
- // Get it's name, if no name found then return.
- string name = BossNotifierPlugin.GetBossName(role);
- if (name == null) return;
-
- // Get the spawn location
- Vector3 positionVector = __instance.Player().Position;
- string position = $"{(int)positionVector.x}, {(int)positionVector.y}, {(int)positionVector.z}";
- // {name} has spawned at (x, y, z) on {map}
- BossNotifierPlugin.Log(LogLevel.Info, $"{name} has spawned at {position} on {Singleton.Instance.LocationId}");
-
- // Add boss to spawnedBosses
- spawnedBosses.Add(name);
-
- vicinityNotifications.Enqueue($"{name} {(BossNotifierPlugin.pluralBosses.Contains(name) ? "have" : "has")} been detected in your vicinity.");
-
- //if (BossNotifierMono.Instance.intelCenterLevel >= BossNotifierPlugin.intelCenterDetectedUnlockLevel.Value) {
- // NotificationManagerClass.DisplayMessageNotification($"{name} {(BossNotifierPlugin.pluralBosses.Contains(name) ? "have" : "has")} been detected in your vicinity.", ENotificationDurationType.Long);
- // BossNotifierMono.Instance.GenerateBossNotifications();
- //}
+ try {
+ WildSpawnType role = __instance.Owner.Profile.Info.Settings.Role;
+ // Get it's name, if no name found then return.
+ string name = BossNotifierPlugin.GetBossName(role);
+ if (name == null) return;
+
+ // Get the spawn location
+ Vector3 positionVector = __instance.Player().Position;
+ string position = $"{(int)positionVector.x}, {(int)positionVector.y}, {(int)positionVector.z}";
+ // {name} has spawned at (x, y, z) on {map}
+ BossNotifierPlugin.Log(LogLevel.Info, $"{name} has spawned at {position} on {Singleton.Instance.LocationId}");
+
+ // Add boss to spawnedBosses
+ spawnedBosses.Add(name);
+
+ // Create vicinity notification message
+ string vicinityMessage = $"{name} {(BossNotifierPlugin.pluralBosses.Contains(name) ? "have" : "has")} been detected in your vicinity.";
+
+ // Enqueue for local display
+ vicinityNotifications.Enqueue(vicinityMessage);
+
+ // If Fika is installed and we're the host, send to all clients
+ if (BossNotifierPlugin.IsFikaInstalled() && BossNotifierPlugin.IsHost())
+ {
+ BossNotifierPlugin.Log(LogLevel.Info, $"[BossNotifier] Host detected, sending vicinity notification to clients: {vicinityMessage}");
+ BossNotifierPlugin.SendVicinityNotificationToClients(vicinityMessage);
+ }
+ } catch (Exception ex) {
+ BossNotifierPlugin.Log(LogLevel.Error, $"[BossNotifier] Error in BotBossPatch: {ex}");
+ throw;
+ }
}
}
// Patch for hooking when a raid is started
internal class NewGamePatch : ModulePatch {
- protected override MethodBase GetTargetMethod() => typeof(GameWorld).GetMethod("OnGameStarted");
+ protected override MethodBase GetTargetMethod() => typeof(GameWorld).GetMethod(nameof(GameWorld.OnGameStarted));
- [PatchPrefix]
- public static void PatchPrefix() {
- // Start BossNotifierMono
- BossNotifierMono.Init();
+ [PatchPostfix]
+ public static void PatchPostfix() {
+ try {
+ // Start BossNotifierMono
+ BossNotifierMono.Init();
+ } catch (Exception ex) {
+ BossNotifierPlugin.Log(LogLevel.Error, $"[BossNotifier] Error in NewGamePatch: {ex}");
+ throw;
+ }
}
}
@@ -321,9 +461,72 @@ public static void Init() {
}
public void Start() {
+ BossNotifierPlugin.Log(LogLevel.Info, "[BossNotifier] BossNotifierMono.Start() called");
+
+ // Log Fika state for diagnostics (detection already done in Awake)
+ BossNotifierPlugin.Log(LogLevel.Info, $"[BossNotifier] Fika installed: {BossNotifierPlugin.IsFikaInstalled()}");
+ if (BossNotifierPlugin.IsFikaInstalled()) {
+ int clientType = (int)BossNotifierPlugin.FikaIsPlayerHost.GetValue(null);
+ string clientTypeStr = clientType == 0 ? "None" : (clientType == 1 ? "Client" : (clientType == 2 ? "Host" : "Unknown"));
+ BossNotifierPlugin.Log(LogLevel.Info, $"[BossNotifier] ClientType at Start(): {clientType} ({clientTypeStr})");
+ }
+
GenerateBossNotifications();
+ BossNotifierPlugin.Log(LogLevel.Debug, "[BossNotifier] After GenerateBossNotifications, checking config...");
+ BossNotifierPlugin.Log(LogLevel.Debug, $"[BossNotifier] showNotificationsOnRaidStart = {BossNotifierPlugin.showNotificationsOnRaidStart.Value}");
+
+ if (!BossNotifierPlugin.showNotificationsOnRaidStart.Value) {
+ BossNotifierPlugin.Log(LogLevel.Warning, "[BossNotifier] Auto-notifications disabled in config!");
+ return;
+ }
- if (!BossNotifierPlugin.showNotificationsOnRaidStart.Value) return;
+ // Use retry mechanism to handle Fika initialization timing
+ BossNotifierPlugin.Log(LogLevel.Info, "[BossNotifier] Starting automatic notification scheduling with retries...");
+ StartCoroutine(TryScheduleAutomaticNotification());
+ }
+
+ // Coroutine to retry scheduling automatic notifications until Fika is ready
+ private System.Collections.IEnumerator TryScheduleAutomaticNotification()
+ {
+ int maxRetries = 10;
+ int retryCount = 0;
+ float retryInterval = 0.5f; // Check every 0.5 seconds
+
+ while (retryCount < maxRetries)
+ {
+ BossNotifierPlugin.Log(LogLevel.Debug, $"[BossNotifier] Attempt {retryCount + 1}/{maxRetries} to determine client/host mode...");
+ BossNotifierPlugin.Log(LogLevel.Debug, $"[BossNotifier] IsClient() = {BossNotifierPlugin.IsClient()}");
+ BossNotifierPlugin.Log(LogLevel.Debug, $"[BossNotifier] IsHost() = {BossNotifierPlugin.IsHost()}");
+
+ if (BossNotifierPlugin.IsClient())
+ {
+ // Client: Request boss info after delay
+ BossNotifierPlugin.Log(LogLevel.Info, "[BossNotifier] ✓ Client mode detected: Scheduling boss info request in 3.5s");
+ Invoke("RequestBossInfoFromHost", 3.5f);
+ yield break; // Success, exit coroutine
+ }
+ else if (BossNotifierPlugin.IsHost())
+ {
+ // Host: Show after 2 seconds
+ BossNotifierPlugin.Log(LogLevel.Info, "[BossNotifier] ✓ Host mode detected: Scheduling notifications in 2s");
+ Invoke("SendBossNotifications", 2f);
+ yield break; // Success, exit coroutine
+ }
+ else if (!BossNotifierPlugin.IsFikaInstalled())
+ {
+ // Singleplayer: Show after 2 seconds
+ BossNotifierPlugin.Log(LogLevel.Info, "[BossNotifier] ✓ Singleplayer mode detected: Scheduling notifications in 2s");
+ Invoke("SendBossNotifications", 2f);
+ yield break; // Success, exit coroutine
+ }
+
+ retryCount++;
+ BossNotifierPlugin.Log(LogLevel.Debug, $"[BossNotifier] Client/Host mode not determined yet, waiting {retryInterval}s...");
+ yield return new UnityEngine.WaitForSeconds(retryInterval);
+ }
+
+ // If we get here, we failed to determine mode after all retries
+ BossNotifierPlugin.Log(LogLevel.Warning, $"[BossNotifier] Failed to determine client/host mode after {maxRetries} retries, defaulting to singleplayer behavior");
Invoke("SendBossNotifications", 2f);
}
@@ -337,7 +540,16 @@ public void Update() {
}
if (IsKeyPressed(BossNotifierPlugin.showBossesKeyCode.Value)) {
- SendBossNotifications();
+ BossNotifierPlugin.Log(LogLevel.Info, "[BossNotifier] Hotkey pressed!");
+ if (BossNotifierPlugin.IsClient()) {
+ // Client: Request from host
+ BossNotifierPlugin.Log(LogLevel.Info, "[BossNotifier] Client mode: Requesting boss info from host");
+ RequestBossInfoFromHost();
+ } else {
+ // Host/Singleplayer: Show directly
+ BossNotifierPlugin.Log(LogLevel.Info, "[BossNotifier] Host/SP mode: Showing notifications directly");
+ SendBossNotifications();
+ }
}
}
@@ -349,17 +561,28 @@ public void OnDestroy() {
}
public bool ShouldFunction() {
- if (BossNotifierPlugin.FikaIsPlayerHost == null) return true;
- return (int)BossNotifierPlugin.FikaIsPlayerHost.GetValue(null) == 2;
+ // NEW: Always return true (both host and client can function now)
+ // Clients will request data from host via packets
+ BossNotifierPlugin.Log(LogLevel.Debug, "[BossNotifier] ShouldFunction: true (clients now supported)");
+ return true;
}
public void GenerateBossNotifications() {
// Clear out boss notification cache
bossNotificationMessages = new List();
- // Check if it's daytime to prevent showing Cultist notif.
- // This is the same method that DayTimeCultists patches so if that mod is installed then this always returns false
- bool isDayTime = Singleton.Instance.BotsController.ZonesLeaveController.IsDay();
+ // Check if it's daytime to prevent showing Cultist notif (with null checks for Fika clients)
+ bool isDayTime = false;
+ try {
+ if (Singleton.Instance != null &&
+ Singleton.Instance.BotsController != null &&
+ Singleton.Instance.BotsController.ZonesLeaveController != null)
+ {
+ isDayTime = Singleton.Instance.BotsController.ZonesLeaveController.IsDay();
+ }
+ } catch (Exception ex) {
+ BossNotifierPlugin.Log(LogLevel.Warning, $"[BossNotifier] Could not determine day/night in GenerateBossNotifications: {ex.Message}");
+ }
// Get whether location is unlocked or not.
bool isLocationUnlocked = intelCenterLevel >= BossNotifierPlugin.intelCenterLocationUnlockLevel.Value;
@@ -389,6 +612,19 @@ public void GenerateBossNotifications() {
}
}
+ // Client method: Request boss info from host
+ private void RequestBossInfoFromHost() {
+ BossNotifierPlugin.Log(LogLevel.Info, "[BossNotifier] RequestBossInfoFromHost() called");
+
+ if (!BossNotifierPlugin.IsClient()) {
+ BossNotifierPlugin.Log(LogLevel.Warning, "[BossNotifier] RequestBossInfoFromHost called but not a client!");
+ return;
+ }
+
+ // Call into FikaIntegration via reflection to avoid loading Fika types when not installed
+ BossNotifierPlugin.SendBossInfoRequest();
+ }
+
// Credit to DrakiaXYZ, thank you!
bool IsKeyPressed(KeyboardShortcut key) {
if (!UnityInput.Current.GetKeyDown(key.MainKey)) {
diff --git a/Properties/AssemblyInfo.cs b/Properties/AssemblyInfo.cs
index 187667f..9a6fd49 100644
--- a/Properties/AssemblyInfo.cs
+++ b/Properties/AssemblyInfo.cs
@@ -10,7 +10,7 @@
[assembly: AssemblyConfiguration("")]
[assembly: AssemblyCompany("")]
[assembly: AssemblyProduct("BossNotifier")]
-[assembly: AssemblyCopyright("Copyright © 2024")]
+[assembly: AssemblyCopyright("Copyright © 2024-2025")]
[assembly: AssemblyTrademark("")]
[assembly: AssemblyCulture("")]
@@ -32,5 +32,8 @@
// You can specify all the values or you can default the Build and Revision Numbers
// by using the '*' as shown below:
// [assembly: AssemblyVersion("1.0.*")]
-[assembly: AssemblyVersion("1.0.0.0")]
-[assembly: AssemblyFileVersion("1.0.0.0")]
+[assembly: AssemblyVersion("2.0.1.0")]
+[assembly: AssemblyFileVersion("2.0.1.0")]
+
+// Make internal types visible to Fika integration assembly
+[assembly: InternalsVisibleTo("BossNotifier.Fika")]
diff --git a/References/Assembly-CSharp.dll b/References/Assembly-CSharp.dll
index 9561a2b..c7c12d9 100644
Binary files a/References/Assembly-CSharp.dll and b/References/Assembly-CSharp.dll differ
diff --git a/References/BepInEx.dll b/References/BepInEx.dll
index 826b479..8235e6f 100644
Binary files a/References/BepInEx.dll and b/References/BepInEx.dll differ
diff --git a/References/Comfort.Unity.dll b/References/Comfort.Unity.dll
new file mode 100644
index 0000000..d020a23
Binary files /dev/null and b/References/Comfort.Unity.dll differ
diff --git a/References/Comfort.dll b/References/Comfort.dll
index 5eeb3c6..918c4ef 100644
Binary files a/References/Comfort.dll and b/References/Comfort.dll differ
diff --git a/References/Fika.Core.dll b/References/Fika.Core.dll
new file mode 100644
index 0000000..67a6fc9
Binary files /dev/null and b/References/Fika.Core.dll differ
diff --git a/References/UnityEngine.CoreModule.dll b/References/UnityEngine.CoreModule.dll
index d8a5448..5543d8e 100644
Binary files a/References/UnityEngine.CoreModule.dll and b/References/UnityEngine.CoreModule.dll differ
diff --git a/References/UnityEngine.dll b/References/UnityEngine.dll
index 6ff1d0f..0f82d88 100644
Binary files a/References/UnityEngine.dll and b/References/UnityEngine.dll differ
diff --git a/References/spt-reflection.dll b/References/spt-reflection.dll
index eda4cda..f1ee89c 100644
Binary files a/References/spt-reflection.dll and b/References/spt-reflection.dll differ
diff --git a/build.ps1 b/build.ps1
new file mode 100644
index 0000000..ad16f47
--- /dev/null
+++ b/build.ps1
@@ -0,0 +1,35 @@
+Write-Host "Building BossNotifier (2-DLL solution)..." -ForegroundColor Cyan
+Write-Host ""
+
+Write-Host "[1/2] Building BossNotifier.csproj..." -ForegroundColor Yellow
+dotnet build BossNotifier.csproj -c Release
+if ($LASTEXITCODE -ne 0) {
+ Write-Host "Failed to build main project!" -ForegroundColor Red
+ exit $LASTEXITCODE
+}
+Write-Host ""
+
+Write-Host "[2/2] Building BossNotifier.Fika.csproj..." -ForegroundColor Yellow
+dotnet build Fika\BossNotifier.Fika.csproj -c Release
+if ($LASTEXITCODE -ne 0) {
+ Write-Host "Failed to build Fika integration!" -ForegroundColor Red
+ exit $LASTEXITCODE
+}
+Write-Host ""
+
+Write-Host "[3/3] Rebuilding main project to package .bin file..." -ForegroundColor Yellow
+dotnet build BossNotifier.csproj -c Release --no-restore
+if ($LASTEXITCODE -ne 0) {
+ Write-Host "Failed to rebuild main project!" -ForegroundColor Red
+ exit $LASTEXITCODE
+}
+Write-Host ""
+
+Write-Host "======================================" -ForegroundColor Green
+Write-Host "Build complete!" -ForegroundColor Green
+Write-Host "======================================" -ForegroundColor Green
+Write-Host "Output: bin\Release\netstandard2.1\BossNotifier.zip"
+Write-Host " BepInEx/plugins/BossNotifier/"
+Write-Host " - BossNotifier.dll"
+Write-Host " - BossNotifier.FikaOptional.dll.bin"
+Write-Host "======================================" -ForegroundColor Green
diff --git a/projectstructure.txt b/projectstructure.txt
new file mode 100644
index 0000000..4fe0848
--- /dev/null
+++ b/projectstructure.txt
@@ -0,0 +1,13 @@
+BossNotifier Two-DLL Architecture
+==================================
+
+The mod is split into two assemblies to support optional Fika integration:
+
+1. BossNotifier.dll - Main plugin with no Fika dependencies, works standalone in singleplayer
+2. BossNotifier.FikaOptional.dll.bin - Fika integration loaded conditionally when Fika.Core is present
+
+The .bin extension prevents BepInEx from auto-loading it. The main plugin loads it via Assembly.LoadFrom()
+only when Fika is detected, ensuring no errors occur when Fika is absent. This approach eliminates the need
+for complex reflection throughout the Fika integration code - FikaIntegration.cs uses direct references.
+
+Build: Use build.ps1 (handles build order and ensures .bin is packaged in zip)