fix(drill): staging 26341433's swap fields; a failed group no longer ends the harness (#20)

The staging drill's run 47 failed on Rust staging 26341433:

- Rust added BasePlayer.timeSinceAiming, BaseEntity.ServerPreProcessed and
  BaseNetworkable.ignoreNetworkLod to the swap's field list (D232): 55 -> 58.
  On edge now; main at the forced wipe of 2026-11-05.
- Harness 0.7.4: place.set.each was refused and the refusal was thrown
  away; place.spawned failed; reading each[0] of an empty list then killed
  the coroutine, so the drill waited 45 minutes for a run that had stopped
  after 10. Place puts its spots on the navmesh, the set checks carry the
  refusal, a failed place.spawned ends the group, and any group that throws
  is a <group>.exception FAIL line and the run goes on.
- drill.js: rust-staging boots from empty Kits and RunicNPC data, so the
  write-once example kits match this Rust (s6.examples.fullCondition); the
  wait ends after 5 minutes without a new harness line; latest.log's tail
  goes into the issue when the harness did not finish. drill.test.js
  covers the stall logic.

Refs #20, #21

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01E14m6SuuY6i1vASFeGDBeY
This commit is contained in:
2026-11-01 01:30:10 -06:00
parent 27bafedb03
commit bad19a668d
5 changed files with 167 additions and 28 deletions

View File

@@ -45,7 +45,7 @@ namespace Oxide.Plugins
/// a made-up user id (11400000001 and up), so <c>IsNpc</c> is false. They take no damage.
/// </para>
/// </summary>
[Info("RunicNpcTest", "Runic Gateway", "0.7.3")]
[Info("RunicNpcTest", "Runic Gateway", "0.7.4")]
[Description("RunicNPC stage 2 to 7 tests, the stage 6 spike and stage 9's cost check. A developer tool: never ship it.")]
internal class RunicNpcTest : RustPlugin
{
@@ -753,7 +753,25 @@ namespace Oxide.Plugins
continue;
}
while (inner.MoveNext()) yield return inner.Current;
// A group that throws is a FAIL line, not the end of the run: yield cannot sit inside a try, so
// each step is taken inside one and the value is yielded outside it.
while (true)
{
object step;
try
{
if (!inner.MoveNext()) break;
step = inner.Current;
}
catch (Exception e)
{
Check($"{g}.exception", false, $"{e.GetType().Name}: {e.Message} at {(e.StackTrace ?? "").Split('\n').FirstOrDefault()?.Trim()}");
break;
}
yield return step;
}
Api<int>("RunicNpc_DespawnOwner", Owner);
ClearWorld();
yield return new WaitForSeconds(1f);
@@ -1095,16 +1113,27 @@ namespace Oxide.Plugins
private IEnumerator Place(Vector3 field)
{
Vector3 a = field + new Vector3(-40, 0, 0);
Vector3 b = field + new Vector3(-40, 0, 30);
StandIn(field + new Vector3(-40, 0, 110), 8);
Check("place.set.each", Api<string>("RunicNpc_SetPlacement", "t_each", Placement("t_each", a, 3, 20f, "each")) == null, "");
Check("place.set.group", Api<string>("RunicNpc_SetPlacement", "t_group", Placement("t_roam", b, 3, 20f, "group")) == null, "");
// A roamer's spot must be on the navmesh when it is set (D219), so both spots are put there.
Vector3 a = OnMesh(field + new Vector3(-40, 0, 0));
Vector3 b = OnMesh(field + new Vector3(-40, 0, 30));
StandIn(OnMesh(field + new Vector3(-40, 0, 110)), 8);
string eachRefusal = Api<string>("RunicNpc_SetPlacement", "t_each", Placement("t_each", a, 3, 20f, "each"));
Check("place.set.each", eachRefusal == null, eachRefusal ?? "");
string groupRefusal = Api<string>("RunicNpc_SetPlacement", "t_group", Placement("t_roam", b, 3, 20f, "group"));
Check("place.set.group", groupRefusal == null, groupRefusal ?? "");
yield return new WaitForSeconds(4f);
List<Dictionary<string, object>> each = List("placement:t_each");
List<Dictionary<string, object>> group = List("placement:t_group");
Check("place.spawned", each.Count == 3 && group.Count == 3, $"each {each.Count}, group {group.Count}");
if (each.Count != 3 || group.Count != 3)
{
// Everything below needs both sets alive. Take back what was set and end the group, so the run goes on.
Api<bool>("RunicNpc_RemovePlacement", "t_each");
Api<bool>("RunicNpc_RemovePlacement", "t_group");
Note("place: the placements did not spawn, so the rest of the group is skipped");
yield break;
}
// each: kill one; the other two stay, and it alone returns after ~20 s.
var survivors = new HashSet<ulong>(each.Skip(1).Select(x => (ulong)x["netId"]));