feat: add on-demand nearby objects radar streaming
New NearbyObjectsTracker polls WorldFilter.GetLandscape() every 1s when activated by start_radar/stop_radar commands from the browser. Streams all visible objects (monsters, players, NPCs, portals, etc.) with coordinates to the backend for real-time radar display. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
98c43c4c61
commit
5f20d395a6
5 changed files with 201 additions and 0 deletions
|
|
@ -347,6 +347,7 @@
|
||||||
<Compile Include="Views\VVSBaseView.cs" />
|
<Compile Include="Views\VVSBaseView.cs" />
|
||||||
<Compile Include="Views\VVSTabbedMainView.cs" />
|
<Compile Include="Views\VVSTabbedMainView.cs" />
|
||||||
<Compile Include="CharacterStats.cs" />
|
<Compile Include="CharacterStats.cs" />
|
||||||
|
<Compile Include="NearbyObjectsTracker.cs" />
|
||||||
<Compile Include="WebSocket.cs" />
|
<Compile Include="WebSocket.cs" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
|
|
||||||
175
MosswartMassacre/NearbyObjectsTracker.cs
Normal file
175
MosswartMassacre/NearbyObjectsTracker.cs
Normal file
|
|
@ -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
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 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.
|
||||||
|
/// </summary>
|
||||||
|
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<object>();
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Maps DECAL ObjectClass to a string label for the radar.
|
||||||
|
/// Returns null for object classes we don't want to track.
|
||||||
|
/// </summary>
|
||||||
|
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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -146,6 +146,7 @@ namespace MosswartMassacre
|
||||||
private CommandRouter _commandRouter;
|
private CommandRouter _commandRouter;
|
||||||
private LiveInventoryTracker _liveInventoryTracker;
|
private LiveInventoryTracker _liveInventoryTracker;
|
||||||
private EquipmentCantripStateTracker _equipmentCantripStateTracker;
|
private EquipmentCantripStateTracker _equipmentCantripStateTracker;
|
||||||
|
private NearbyObjectsTracker _nearbyObjectsTracker;
|
||||||
|
|
||||||
protected override void Startup()
|
protected override void Startup()
|
||||||
{
|
{
|
||||||
|
|
@ -305,6 +306,9 @@ namespace MosswartMassacre
|
||||||
// Initialize navigation visualization system
|
// Initialize navigation visualization system
|
||||||
navVisualization = new NavVisualization();
|
navVisualization = new NavVisualization();
|
||||||
|
|
||||||
|
// Initialize nearby objects tracker (radar)
|
||||||
|
_nearbyObjectsTracker = new NearbyObjectsTracker(this);
|
||||||
|
|
||||||
// Initialize command router
|
// Initialize command router
|
||||||
_commandRouter = new CommandRouter();
|
_commandRouter = new CommandRouter();
|
||||||
RegisterCommands();
|
RegisterCommands();
|
||||||
|
|
@ -410,6 +414,10 @@ namespace MosswartMassacre
|
||||||
questManager = null;
|
questManager = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Stop radar tracker
|
||||||
|
_nearbyObjectsTracker?.Dispose();
|
||||||
|
_nearbyObjectsTracker = null;
|
||||||
|
|
||||||
// Clean up the view
|
// Clean up the view
|
||||||
ViewManager.ViewDestroy();
|
ViewManager.ViewDestroy();
|
||||||
//Disable vtank interface
|
//Disable vtank interface
|
||||||
|
|
@ -902,6 +910,18 @@ namespace MosswartMassacre
|
||||||
{
|
{
|
||||||
try
|
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
|
// Execute ALL WebSocket commands on main thread - fast and reliable
|
||||||
DispatchChatToBoxWithPluginIntercept(command);
|
DispatchChatToBoxWithPluginIntercept(command);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -349,6 +349,11 @@ namespace MosswartMassacre
|
||||||
await SendEncodedAsync(json, CancellationToken.None);
|
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)
|
public static async Task SendQuestDataAsync(string questName, string countdown)
|
||||||
{
|
{
|
||||||
var envelope = new
|
var envelope = new
|
||||||
|
|
|
||||||
Binary file not shown.
Loading…
Add table
Add a link
Reference in a new issue