[wcid|t] mint n real items into it (t31 = treasure profile 31)\n" +
"mule-test deposit deposit n of them\n" +
"mule-test withdraw take n back out\n" +
"mule-test race two threads deposit at once - proves the lock\n" +
"mule-test count memory against the shard, both sides\n" +
"mule-test audit where every item it ever made is now\n" +
"mule-test pause debug: ms to sleep between the steps of a movement\n" +
"mule-test clear destroy the stand-in pack and its items")]
public static void HandleTest(Session session, params string[] parameters)
{
if (!Mod.Settings.AllowTestHarness)
{
Say(session, "mule-test is off. It mints and destroys real items; set AllowTestHarness true in Settings.json (staging only) and `mule-admin reload`.");
return;
}
var args = parameters ?? Array.Empty();
if (args.Length < 2 || !uint.TryParse(args[1], out var accountId))
{
Say(session, "mule-test [...]");
return;
}
try
{
Say(session, args[0].ToLowerInvariant() switch
{
"pack" => MuleTest.Pack(accountId, out var e) is { } p
? $"stand-in pack 0x{p.Guid.Full:X8}, {p.Inventory.Count} item(s), {Mod.Settings.SlotsPerContainer} slots"
: $"no stand-in pack: {e}",
"fill" => MuleTest.Fill(accountId, Int(args, 2, 10), Wcid(args, 3), Profile(args, 3)),
"deposit" => MuleTest.Deposit(accountId, Int(args, 2, 1)),
"withdraw" => MuleTest.Withdraw(accountId, Int(args, 2, 1)),
"race" => MuleTest.Race(accountId, Int(args, 2, 10)),
"count" => MuleTest.Count(accountId),
// The crash-test aiming knobs. Two gaps, set separately so a kill can be
// aimed at one of them and the other ruled out.
"pause" => Pause(Int(args, 2, 0), Int(args, 3, 0)),
"audit" => MuleTest.Audit(accountId),
"clear" => MuleTest.Clear(accountId),
_ => $"'{args[0]}' is not a mule-test verb",
});
}
catch (Exception ex)
{
Say(session, $"mule-test threw: {ex.Message}");
ModManager.Log($"[{Mod.Name}] mule-test threw: {ex}", ModManager.LogLevel.Error);
}
}
private static string Pause(int afterRemove, int afterAdd)
{
Movement.DebugPauseAfterRemoveMs = Math.Clamp(afterRemove, 0, 5000);
Movement.DebugPauseAfterAddMs = Math.Clamp(afterAdd, 0, 5000);
return $"movements will now sleep {Movement.DebugPauseAfterRemoveMs}ms between leaving the source and arriving in the target, " +
$"and {Movement.DebugPauseAfterAddMs}ms between arriving and the commit. `mule-test pause 0 0` puts it back.";
}
private static int Int(string[] args, int index, int fallback) =>
args.Length > index && int.TryParse(args[index], out var v) ? v : fallback;
/// `fill`'s third argument: a bare number is a wcid, t<n> is a treasure profile.
private static uint Wcid(string[] args, int index) =>
args.Length > index && !args[index].StartsWith('t') && uint.TryParse(args[index], out var v) ? v : 3000001;
private static int Profile(string[] args, int index) =>
args.Length > index && args[index].StartsWith('t') && int.TryParse(args[index][1..], out var v) ? v : 0;
private static bool TryGuid(string s, out uint guid)
{
s = (s ?? "").Trim();
if (s.StartsWith("0x", StringComparison.OrdinalIgnoreCase))
return uint.TryParse(s[2..], System.Globalization.NumberStyles.HexNumber, null, out guid);
return uint.TryParse(s, out guid);
}
///
/// Console or chat, whichever asked. A console command gets a null session, and
/// ModManager.Log is what RemoteConsole reads back out of the journal.
///
private static void Say(Session? session, string message)
{
foreach (var line in (message ?? "").Split('\n'))
{
var text = line.TrimEnd('\r');
if (string.IsNullOrWhiteSpace(text))
continue;
if (session?.Player is not null)
session.Player.SendMessage(text);
else
ModManager.Log($"[{Mod.Name}] {text}");
}
}
}