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)