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\VVSTabbedMainView.cs" />
|
||||
<Compile Include="CharacterStats.cs" />
|
||||
<Compile Include="NearbyObjectsTracker.cs" />
|
||||
<Compile Include="WebSocket.cs" />
|
||||
</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 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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
Binary file not shown.
Loading…
Add table
Add a link
Reference in a new issue