start_radar/stop_radar from the backend drive it, as before. nearby_objects keeps the original's shape: map coordinates on the surface, landblock-local raw positions with zero map coordinates in a dungeon, ids as signed ints, the same eight object classes, no wielded items or self. dungeon_map is built the original's way from the landblock's cells (x a multiple of 10, whole metres, 6-metre floors, rotation = the orientation's stored W) and sent once per landblock per session; the cells come through a small reader that the plugin API's dungeon-cell data will fill (until then the map is not sent). Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
119 lines
5.7 KiB
C#
119 lines
5.7 KiB
C#
using System.Globalization;
|
|
using AcDream.Plugin.Abstractions;
|
|
using OpenAC.MosswartMassacre.Backend;
|
|
using OpenAC.MosswartMassacre.Radar;
|
|
using OpenAC.MosswartMassacre.Tests.Streams;
|
|
|
|
namespace OpenAC.MosswartMassacre.Tests.Radar;
|
|
|
|
/// <summary>The nearby_objects and dungeon_map frames as the original built them.</summary>
|
|
public sealed class RadarTests
|
|
{
|
|
private const string Stamp = "2026-09-25T08:30:15.1230000Z";
|
|
|
|
private static string D(double value) => value.ToString("R", CultureInfo.InvariantCulture);
|
|
|
|
private static PluginWorldObject Landscape(uint id, string name, PluginObjectClass cls, PluginNavigationPosition position, uint wielder = 0) =>
|
|
new(id, 1u, name, cls, 0u, 0u, wielder) { IsLandscape = true, HasPosition = true, Position = position };
|
|
|
|
[Fact]
|
|
public void Nearby_objects_on_the_surface()
|
|
{
|
|
// NearbyObjectsTracker.cs:124-231: objects with ew/ns to 7 places and
|
|
// z to 2; the player's own position both as map coordinates and as
|
|
// landblock-local metres; ids as signed ints; only the eight classes;
|
|
// no wielded items, no self.
|
|
using var harness = new PluginHarness("Mossy");
|
|
harness.Host.Automation.Navigation.Snapshot = new PluginNavigationSnapshot(
|
|
true, false, 1u, StreamGoldenTests.Position(), false, false);
|
|
harness.Host.Automation.Objects.Objects.AddRange(
|
|
[
|
|
Landscape(0x80001234, "Drudge Skulker", PluginObjectClass.Monster, StreamGoldenTests.Position(x: 108f, y: 120f, z: 44.25f)),
|
|
Landscape(0x80001235, "Sword", PluginObjectClass.MeleeWeapon, StreamGoldenTests.Position()),
|
|
Landscape(0x80001236, "Held", PluginObjectClass.Monster, StreamGoldenTests.Position(), wielder: 5),
|
|
Landscape(0x50000001, "Mossy", PluginObjectClass.Player, StreamGoldenTests.Position()),
|
|
]);
|
|
harness.Login();
|
|
|
|
string json = Wire.Serialize(harness.Feature<RadarStream>().BuildNearbyObjects());
|
|
|
|
double monsterEw = StreamGoldenTests.Position(x: 108f).EastWest;
|
|
double monsterNs = StreamGoldenTests.Position(y: 120f).NorthSouth;
|
|
Assert.Equal(
|
|
"{\"type\":\"nearby_objects\",\"character_name\":\"Mossy\",\"timestamp\":\"" + Stamp + "\","
|
|
+ "\"player_ew\":" + D(Math.Round((double)33.65f, 7)) + ",\"player_ns\":" + D(Math.Round((double)42.55f, 7))
|
|
+ ",\"player_z\":42.5,\"player_heading\":90.0,\"is_dungeon\":false,\"landblock\":\"0xA9B40000\","
|
|
+ "\"player_x\":96.0,\"player_y\":120.0,\"player_raw_z\":42.5,"
|
|
+ "\"objects\":[{\"id\":-2147478988,\"name\":\"Drudge Skulker\",\"object_class\":\"Monster\","
|
|
+ "\"ew\":" + D(Math.Round(monsterEw, 7)) + ",\"ns\":" + D(Math.Round(monsterNs, 7)) + ",\"z\":44.25}]}",
|
|
json);
|
|
}
|
|
|
|
[Fact]
|
|
public void In_a_dungeon_objects_carry_raw_positions_and_the_map_goes_out_once()
|
|
{
|
|
const uint cell = 0x01D90105u;
|
|
var here = StreamGoldenTests.Position(x: 30f, y: 40f, z: -12f, cell: cell);
|
|
var cells = new FakeCells(
|
|
[
|
|
new DungeonCellRecord(0x0123, 10.2f, 20f, 0.4f, 1f),
|
|
new DungeonCellRecord(0x0456, 15f, 20f, 0f, 0.7071f),
|
|
new DungeonCellRecord(0x0789, 30f, -10f, -6.4f, 0f),
|
|
]);
|
|
using var harness = new PluginHarness("Mossy");
|
|
harness.Host.Automation.Navigation.Snapshot = new PluginNavigationSnapshot(true, false, 1u, here, false, false);
|
|
harness.Host.Automation.DungeonMap.SealedLandblocks.Add(0x01D90000u);
|
|
harness.Host.Automation.Objects.Objects.Add(
|
|
Landscape(0x80000001, "Lich", PluginObjectClass.Monster, StreamGoldenTests.Position(x: 31.5f, y: 41.25f, z: -12f, cell: cell)));
|
|
harness.Login();
|
|
var radar = new RadarStream(harness.Context, cells) ;
|
|
radar.Start();
|
|
|
|
radar.OnTick();
|
|
radar.OnTick();
|
|
|
|
Assert.Equal(
|
|
"{\"type\":\"dungeon_map\",\"character_name\":\"Mossy\",\"landblock\":\"0x01D90000\","
|
|
+ "\"bounds\":{\"min_x\":10,\"max_x\":30,\"min_y\":-10,\"max_y\":20},"
|
|
+ "\"z_levels\":[{\"z\":-6,\"cells\":[{\"env_id\":1929,\"x\":30,\"y\":-10,\"z\":-6,\"rotation\":0.0}]},"
|
|
+ "{\"z\":0,\"cells\":[{\"env_id\":291,\"x\":10,\"y\":20,\"z\":0,\"rotation\":1.0}]}]}",
|
|
Assert.Single(harness.Frames, f => f.Contains("dungeon_map", StringComparison.Ordinal)));
|
|
string objects = harness.Frames.First(f => f.Contains("nearby_objects", StringComparison.Ordinal));
|
|
Assert.Contains("\"is_dungeon\":true", objects, StringComparison.Ordinal);
|
|
Assert.Contains(
|
|
"{\"id\":-2147483647,\"name\":\"Lich\",\"object_class\":\"Monster\",\"ew\":0.0,\"ns\":0.0,\"z\":0.0,"
|
|
+ "\"raw_x\":31.5,\"raw_y\":41.25,\"raw_z\":-12.0}",
|
|
objects,
|
|
StringComparison.Ordinal);
|
|
}
|
|
|
|
[Fact]
|
|
public void The_backend_turns_the_radar_on_and_off()
|
|
{
|
|
using var harness = new PluginHarness("Mossy");
|
|
harness.Login();
|
|
var radar = harness.Feature<RadarStream>();
|
|
|
|
harness.Receive("{\"player_name\":\"Mossy\",\"command\":\"start_radar\"}");
|
|
harness.Tick(0.01);
|
|
Assert.True(radar.IsActive);
|
|
|
|
harness.Run(2.05);
|
|
Assert.Equal(2, harness.FramesOfType("nearby_objects").Count());
|
|
|
|
harness.Receive("{\"player_name\":\"Mossy\",\"command\":\"stop_radar\"}");
|
|
harness.Tick(0.01);
|
|
Assert.False(radar.IsActive);
|
|
}
|
|
|
|
private sealed class FakeCells(IReadOnlyList<DungeonCellRecord> cells) : IDungeonCellSource
|
|
{
|
|
public int Reads { get; private set; }
|
|
|
|
public IReadOnlyList<DungeonCellRecord>? ReadCells(uint landblock)
|
|
{
|
|
Reads++;
|
|
return cells;
|
|
}
|
|
}
|
|
}
|