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;
/// The nearby_objects and dungeon_map frames as the original built them.
public sealed class RadarTests
{
private const string Stamp = "2026-09-25T08:30:15.1230000Z";
private static string D(double value) => OpenAC.MosswartMassacre.Backend.FrameworkNumberConverter.FrameworkRoundTrip(value);
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().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();
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 cells) : IDungeonCellSource
{
public int Reads { get; private set; }
public IReadOnlyList? ReadCells(uint landblock)
{
Reads++;
return cells;
}
}
}