fix(drill): place spots found up to 15 m from the navmesh; count only this run's Oxide errors (#22)
All checks were successful
PR Checks / plugin-checks (pull_request) Successful in 1m28s

Run 49: place.set.each was refused "4.4 m from the navmesh", because
OnMesh searched 3 m and returned the unsnapped point; Place now searches
15 m. The drill reported run 48's four error lines again: oxideErrors
read the whole day's Oxide log, and now reads only what was added after
the drill started the rig.

Refs #20, #22

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 02:15:18 -06:00
parent 4fb935d9dd
commit cf3997bbce
2 changed files with 18 additions and 9 deletions

View File

@@ -1113,9 +1113,10 @@ namespace Oxide.Plugins
private IEnumerator Place(Vector3 field) private IEnumerator Place(Vector3 field)
{ {
// A roamer's spot must be on the navmesh when it is set (D219), so both spots are put there. // A roamer's spot must be on the navmesh when it is set (D219), so both spots are put there; the
Vector3 a = OnMesh(field + new Vector3(-40, 0, 0)); // wider search because a fixed offset from the field can land a few metres off it (run 49: 4.4 m).
Vector3 b = OnMesh(field + new Vector3(-40, 0, 30)); Vector3 a = OnMesh(field + new Vector3(-40, 0, 0), 15f);
Vector3 b = OnMesh(field + new Vector3(-40, 0, 30), 15f);
StandIn(OnMesh(field + new Vector3(-40, 0, 110)), 8); StandIn(OnMesh(field + new Vector3(-40, 0, 110)), 8);
string eachRefusal = Api<string>("RunicNpc_SetPlacement", "t_each", Placement("t_each", a, 3, 20f, "each")); string eachRefusal = Api<string>("RunicNpc_SetPlacement", "t_each", Placement("t_each", a, 3, 20f, "each"));
Check("place.set.each", eachRefusal == null, eachRefusal ?? ""); Check("place.set.each", eachRefusal == null, eachRefusal ?? "");
@@ -1569,11 +1570,11 @@ namespace Oxide.Plugins
return floor; return floor;
} }
private static Vector3 OnMesh(Vector3 p) private static Vector3 OnMesh(Vector3 p, float radius = 3f)
{ {
p.y = TerrainMeta.HeightMap.GetHeight(p); p.y = TerrainMeta.HeightMap.GetHeight(p);
NavMeshHit hit; NavMeshHit hit;
return SampleNavmesh(p, 3f, out hit) ? hit.position : p; return SampleNavmesh(p, radius, out hit) ? hit.position : p;
} }
private static JObject Placement(string profile, Vector3 at, int count, float respawn, string mode) => private static JObject Placement(string profile, Vector3 at, int count, float respawn, string mode) =>

View File

@@ -281,6 +281,7 @@ async function live(a) {
for (const f of FRESH_DATA) await p.rm(rig, f) // already gone is fine: rm swallows it for (const f of FRESH_DATA) await p.rm(rig, f) // already gone is fine: rm swallows it
await p.write(rig, KITS_FILE, JSON.stringify(harnessKits(), null, 2)) await p.write(rig, KITS_FILE, JSON.stringify(harnessKits(), null, 2))
const logMark = await oxideLogMark(p, rig)
log(`starting ${RIG} on ${branch}`) log(`starting ${RIG} on ${branch}`)
const started = Date.now() const started = Date.now()
await p.power(rig, 'start') await p.power(rig, 'start')
@@ -298,7 +299,7 @@ async function live(a) {
const status = await waitStatus(p, rig, 30 * 60000) const status = await waitStatus(p, rig, 30 * 60000)
if (!status) { if (!status) {
finding('RunicNPC did not load: `rnpc.status` never answered.') finding('RunicNPC did not load: `rnpc.status` never answered.')
result.oxideErrors = await oxideErrors(p, rig) result.oxideErrors = await oxideErrors(p, rig, logMark)
return return
} }
result.status = status result.status = status
@@ -322,7 +323,7 @@ async function live(a) {
} }
if (!accepted) { if (!accepted) {
finding('The test harness did not load (`rnt.run all` was not answered).') finding('The test harness did not load (`rnt.run all` was not answered).')
result.oxideErrors = await oxideErrors(p, rig) result.oxideErrors = await oxideErrors(p, rig, logMark)
return return
} }
result.ran = true result.ran = true
@@ -337,7 +338,7 @@ async function live(a) {
} }
for (const f of result.harness.failed) finding(`Harness: ${f}`) for (const f of result.harness.failed) finding(`Harness: ${f}`)
log(`harness: ${result.harness.pass} pass, ${result.harness.fail} fail`) log(`harness: ${result.harness.pass} pass, ${result.harness.fail} fail`)
result.oxideErrors = await oxideErrors(p, rig) result.oxideErrors = await oxideErrors(p, rig, logMark)
if (result.oxideErrors.length) finding(`RunicNPC logged ${result.oxideErrors.length} error line(s) in Oxide's log during the run.`) if (result.oxideErrors.length) finding(`RunicNPC logged ${result.oxideErrors.length} error line(s) in Oxide's log during the run.`)
await p.command(rig, 'rnt.clear').catch(() => {}) await p.command(rig, 'rnt.clear').catch(() => {})
} catch (e) { } catch (e) {
@@ -490,10 +491,17 @@ function summarise(lines) {
} }
/** RunicNPC's and the harness's error lines from today's Oxide log. */ /** RunicNPC's and the harness's error lines from today's Oxide log. */
async function oxideErrors(p, id) { /** Where Oxide's log of today ends now, so oxideErrors reads only what a run adds (run 49 counted run 48's). */
async function oxideLogMark(p, id) {
const day = now().slice(0, 10)
return { day, length: ((await p.readOr(id, `/oxide/logs/oxide_${day}.txt`, '')) || '').length }
}
async function oxideErrors(p, id, since = null) {
const day = now().slice(0, 10) const day = now().slice(0, 10)
const text = (await p.readOr(id, `/oxide/logs/oxide_${day}.txt`, '')) || '' const text = (await p.readOr(id, `/oxide/logs/oxide_${day}.txt`, '')) || ''
return text return text
.slice(since && since.day === day ? since.length : 0)
.split('\n') .split('\n')
.filter((l) => /RunicNPC|RunicNpcTest/.test(l) && /\[Error\]|Exception|Failed|error CS/.test(l)) .filter((l) => /RunicNPC|RunicNpcTest/.test(l) && /\[Error\]|Exception|Failed|error CS/.test(l))
.slice(-40) .slice(-40)