diff --git a/MosswartMassacre/MosswartMassacre.csproj b/MosswartMassacre/MosswartMassacre.csproj index 49bb9f0..0304df2 100644 --- a/MosswartMassacre/MosswartMassacre.csproj +++ b/MosswartMassacre/MosswartMassacre.csproj @@ -347,6 +347,7 @@ + diff --git a/MosswartMassacre/NearbyObjectsTracker.cs b/MosswartMassacre/NearbyObjectsTracker.cs new file mode 100644 index 0000000..1d2b8cd --- /dev/null +++ b/MosswartMassacre/NearbyObjectsTracker.cs @@ -0,0 +1,175 @@ +using System; +using System.Collections.Generic; +using System.Globalization; +using Decal.Adapter; +using Decal.Adapter.Wrappers; +using Newtonsoft.Json; + +namespace MosswartMassacre +{ + /// + /// On-demand tracker that polls nearby world objects and streams them + /// to the backend via WebSocket. Activated/deactivated by start_radar + /// and stop_radar commands from the browser. + /// + /// Uses System.Windows.Forms.Timer so the tick fires on the main UI + /// thread — DECAL COM objects are apartment-threaded (STA) and must + /// only be accessed from the game's main thread. + /// + public class NearbyObjectsTracker : IDisposable + { + private const int PollIntervalMs = 1000; + + private readonly IPluginLogger _logger; + private System.Windows.Forms.Timer _timer; + private bool _active; + private bool _disposed; + + public bool IsActive => _active; + + public NearbyObjectsTracker(IPluginLogger logger) + { + _logger = logger; + } + + public void Start() + { + if (_active) return; + _active = true; + + _timer = new System.Windows.Forms.Timer(); + _timer.Interval = PollIntervalMs; + _timer.Tick += OnTick; + _timer.Start(); + + _logger?.Log("[Radar] Started nearby objects tracker"); + } + + public void Stop() + { + if (!_active) return; + _active = false; + + if (_timer != null) + { + _timer.Stop(); + _timer.Tick -= OnTick; + _timer.Dispose(); + _timer = null; + } + + _logger?.Log("[Radar] Stopped nearby objects tracker"); + } + + private async void OnTick(object sender, EventArgs e) + { + try + { + var payload = BuildNearbyObjectsPayload(); + if (payload == null) return; + + var json = JsonConvert.SerializeObject(payload); + await WebSocket.SendNearbyObjectsAsync(json); + } + catch (Exception ex) + { + _logger?.Log($"[Radar] Tick error: {ex.Message}"); + } + } + + private object BuildNearbyObjectsPayload() + { + try + { + var playerCoords = Coordinates.Me; + double playerHeading = CoreManager.Current.Actions.Heading; + string characterName = CoreManager.Current.CharacterFilter.Name; + int playerId = CoreManager.Current.CharacterFilter.Id; + + var objects = new List(); + + using (var landscape = CoreManager.Current.WorldFilter.GetLandscape()) + { + foreach (WorldObject wo in landscape) + { + try + { + if (wo.Id == playerId) continue; + if (!CoreManager.Current.Actions.IsValidObject(wo.Id)) continue; + + // Skip wielded items (equipped weapons/shields on other players) + int wielder = 0; + try { wielder = wo.Values(LongValueKey.Wielder, 0); } catch { } + if (wielder != 0) continue; + + string objectClass = ClassifyObject(wo.ObjectClass); + if (objectClass == null) continue; + + // Get coordinates + var coords = Utils.GetWorldObjectCoordinates(wo); + if (coords == null || (coords.EW == 0 && coords.NS == 0)) continue; + + objects.Add(new + { + id = wo.Id, + name = wo.Name, + object_class = objectClass, + ew = Math.Round(coords.EW, 7), + ns = Math.Round(coords.NS, 7), + z = Math.Round(coords.Z, 2) + }); + } + catch + { + // Skip individual objects that fail + } + } + } + + return new + { + type = "nearby_objects", + character_name = characterName, + timestamp = DateTime.UtcNow.ToString("o"), + player_ew = Math.Round(playerCoords.EW, 7), + player_ns = Math.Round(playerCoords.NS, 7), + player_z = Math.Round(playerCoords.Z, 2), + player_heading = Math.Round(playerHeading, 1), + objects + }; + } + catch (Exception ex) + { + _logger?.Log($"[Radar] Build payload error: {ex.Message}"); + return null; + } + } + + /// + /// Maps DECAL ObjectClass to a string label for the radar. + /// Returns null for object classes we don't want to track. + /// + private static string ClassifyObject(ObjectClass oc) + { + switch (oc) + { + case ObjectClass.Monster: return "Monster"; + case ObjectClass.Player: return "Player"; + case ObjectClass.Npc: return "NPC"; + case ObjectClass.Vendor: return "Vendor"; + case ObjectClass.Portal: return "Portal"; + case ObjectClass.Corpse: return "Corpse"; + case ObjectClass.Container: return "Container"; + case ObjectClass.Door: return "Door"; + default: return null; + } + } + + public void Dispose() + { + if (_disposed) return; + _disposed = true; + Stop(); + } + } +} diff --git a/MosswartMassacre/PluginCore.cs b/MosswartMassacre/PluginCore.cs index 2f5fc21..d607624 100644 --- a/MosswartMassacre/PluginCore.cs +++ b/MosswartMassacre/PluginCore.cs @@ -146,6 +146,7 @@ namespace MosswartMassacre private CommandRouter _commandRouter; private LiveInventoryTracker _liveInventoryTracker; private EquipmentCantripStateTracker _equipmentCantripStateTracker; + private NearbyObjectsTracker _nearbyObjectsTracker; protected override void Startup() { @@ -305,6 +306,9 @@ namespace MosswartMassacre // Initialize navigation visualization system navVisualization = new NavVisualization(); + // Initialize nearby objects tracker (radar) + _nearbyObjectsTracker = new NearbyObjectsTracker(this); + // Initialize command router _commandRouter = new CommandRouter(); RegisterCommands(); @@ -410,6 +414,10 @@ namespace MosswartMassacre questManager = null; } + // Stop radar tracker + _nearbyObjectsTracker?.Dispose(); + _nearbyObjectsTracker = null; + // Clean up the view ViewManager.ViewDestroy(); //Disable vtank interface @@ -902,6 +910,18 @@ namespace MosswartMassacre { try { + // Handle radar commands directly (don't send to chat box) + if (command == "start_radar") + { + _nearbyObjectsTracker?.Start(); + return; + } + if (command == "stop_radar") + { + _nearbyObjectsTracker?.Stop(); + return; + } + // Execute ALL WebSocket commands on main thread - fast and reliable DispatchChatToBoxWithPluginIntercept(command); } diff --git a/MosswartMassacre/WebSocket.cs b/MosswartMassacre/WebSocket.cs index dfdbf23..4da2326 100644 --- a/MosswartMassacre/WebSocket.cs +++ b/MosswartMassacre/WebSocket.cs @@ -349,6 +349,11 @@ namespace MosswartMassacre await SendEncodedAsync(json, CancellationToken.None); } + public static async Task SendNearbyObjectsAsync(string json) + { + await SendEncodedAsync(json, CancellationToken.None); + } + public static async Task SendQuestDataAsync(string questName, string countdown) { var envelope = new diff --git a/MosswartMassacre/bin/Release/MosswartMassacre.dll b/MosswartMassacre/bin/Release/MosswartMassacre.dll index 4b652fc..53cbf3a 100644 Binary files a/MosswartMassacre/bin/Release/MosswartMassacre.dll and b/MosswartMassacre/bin/Release/MosswartMassacre.dll differ