diff --git a/CHANGELOG.md b/CHANGELOG.md index d1d8129..114aa24 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,6 +14,9 @@ pre-`1.0.0` policy documented in [COMPATIBILITY.md](COMPATIBILITY.md#version-pol roof 20 m above where it began, or 10 m up in the air, was fatal. Leaving no-clip now calls `LocalPlayer.resetFall`, so the landing costs no health. It needs a HogwartsMP host with `LocalPlayer.resetFall`; on an older host the landing is unchanged. +- `hmp-npcs` `destroy` removes the NPC from the registry when its id arrives as a numeric string. It + looked the entry up by the normalized id but deleted it by the raw one, so the destroyed NPC stayed + counted against the resource's limit and was destroyed again by `clear` and on stop. ## [0.9.1] - 2026-10-16 diff --git a/resources/hmp-npcs/server/service.ts b/resources/hmp-npcs/server/service.ts index 677208f..3706088 100644 --- a/resources/hmp-npcs/server/service.ts +++ b/resources/hmp-npcs/server/service.ts @@ -63,9 +63,10 @@ export function createNpcsService(options: NpcsServiceOptions): NpcsService { function destroy(id: number, resource: string): boolean { ensureReady(); const owner = resourceName(resource); - const entry = managed.get(integer(id, "NPC id", 0)); + const key = integer(id, "NPC id", 0); + const entry = managed.get(key); if (!entry || entry.resource !== owner) return false; - managed.delete(id); + managed.delete(key); try { entry.npc.destroy(); } catch { /* Native actor may already be gone. */ } return true; } diff --git a/resources/hmp-npcs/test/npcs.test.ts b/resources/hmp-npcs/test/npcs.test.ts index a17726a..75f98e9 100644 --- a/resources/hmp-npcs/test/npcs.test.ts +++ b/resources/hmp-npcs/test/npcs.test.ts @@ -37,8 +37,13 @@ assert.strictEqual(service.destroy(other.id, "arena"), false); assert.strictEqual(service.died(first.id), true); assert.strictEqual(service.clear("arena"), 1); assert.deepStrictEqual(destroyed, [2]); +// An id that crosses a resource boundary as a string still finds the NPC; it must leave the registry too. +const loose = service.spawn({ resource: "quest", enemyId: "Wolf", position: { x: 1, y: 2, z: 3 } }); +assert.strictEqual(service.destroy(String(loose.id) as unknown as number, "quest"), true); +assert.strictEqual(service.status().managed, 1); +assert.strictEqual(service.destroy(loose.id, "quest"), false); service.stop(); -assert.deepStrictEqual(destroyed, [2, 3]); +assert.deepStrictEqual(destroyed, [2, 4, 3]); assert.throws(() => service.catalog.list().push({ id: "x", label: "x" }) && service.spawn({ resource: "x", enemyId: "Wolf", position: { x: 0, y: 0, z: 0 } }), /stopped/); console.log("hmp-npcs tests passed");