diff --git a/modules/rust/OPERATING.md b/modules/rust/OPERATING.md index ab08ad0..b798c43 100644 --- a/modules/rust/OPERATING.md +++ b/modules/rust/OPERATING.md @@ -5,6 +5,8 @@ configure and permission plugins. This is the surface our own deployment story h of, the way [`installer/INSTALL.md`](../../installer/INSTALL.md) sits on top of ServUO. > **Mirrored verbatim from uMod on 2026-09-15** — 6 pages. uMod is upstream and normative. +> The last section, [Guarded Crate and NpcSpawn](#guarded-crate-and-npcspawn), is Runic Gateway's own and +> is not part of the mirror. --- @@ -16,6 +18,7 @@ of, the way [`installer/INSTALL.md`](../../installer/INSTALL.md) sits on top of - [Plugin configuration](#plugin-configuration-plugins-configuration) — Editing a plugin's config file - [Data Files](#data-files-plugins-data-files) — Where plugin data lives on disk - [Permissions](#permissions-plugins-permissions) — Granting permissions and managing groups in-game +- [Guarded Crate and NpcSpawn](#guarded-crate-and-npcspawn) — Runic Gateway's note on an optional plugin that needs a Codefling extension --- @@ -369,3 +372,41 @@ That's the basics to permissions for Oxide. Permissions give you a fantastic way --- Mirrored from uMod, 2026-09-15. + +--- + + + +## Guarded Crate and NpcSpawn + +Runic Gateway's own section, not mirrored. Design: [PLAN_REDESIGNS.md](PLAN_REDESIGNS.md) §14.3.5 (D361, +D364); wire: [PROTOCOL.md](../../rust-link/PROTOCOL.md) §20.9. + +Bazz3l's **Guarded Crate** (uMod) is one of the optional plugins Runic Gateway reports and drives: a guarded +crate landing and being cleared reach the site's feed, map and *Warden* title, and an event can place one. It +**needs NpcSpawn** for its guards, and NpcSpawn is not on uMod. Runic Gateway never ships or modifies either +(D351, D361); an operator who wants guarded crates installs both. + +**Installing NpcSpawn.** + +1. Download NpcSpawn from Codefling (), a free download. +2. Put `NpcSpawn.cs` in the plugins directory (`oxide/plugins`, or `carbon/plugins` on Carbon). +3. Put the archive's `data/Images/` files in the data directory's `Images` folder (`oxide/data/Images`, or + `carbon/data/Images`). **Without them NpcSpawn unloads itself.** +4. Leave `data/NpcSpawn/` for NpcSpawn to create. On Carbon, a folder made by a panel's file manager may not be + writable by the game, and NpcSpawn then fails until that folder is deleted and left to it. +5. Install Guarded Crate from uMod as any other plugin. + +**What NpcSpawn does at load: it contacts its author's server.** It calls a version server at a bare IP +address (seen as `37.153.157.216:5000` in the §14.2 test) and downloads clothing images. An operator whose +server may not reach the internet, or who does not want it to, should know this before installing it. + +**Without NpcSpawn,** Guarded Crate unloads itself the next time its own schedule starts an event, and Runic +Gateway's *Place a guarded crate* step is refused with *"Guarded Crate is loaded but NpcSpawn is missing: its +events cannot start"*. The bridge refuses that placement itself, because Guarded Crate's `position` command does +not check and would drop a crate with no guards. Admin → Rust servers shows NpcSpawn beside Guarded Crate. + +**Its events** are rows in Guarded Crate's **data** file, `data/GuardedCrate.json` (Easy, Medium, Hard and Elite +as it ships), not its config. The site offers whatever names that file holds. A crate placed by an event is +**kept when the event ends** (D373): Guarded Crate has no way to end one event, and `gcrate stop all` ends every +one on the server. diff --git a/modules/rust/PLAN_REDESIGNS.md b/modules/rust/PLAN_REDESIGNS.md index 15b06c0..e149b87 100644 --- a/modules/rust/PLAN_REDESIGNS.md +++ b/modules/rust/PLAN_REDESIGNS.md @@ -930,7 +930,7 @@ pulls) is research, not a walk, so it still comes first: answered before code: **written on 2026-10-14 as §14.3**, from D361–D369, and answered as D370–D376. Then the build, in §14.3.11's nine stages (two of them core), §13 first. **Stage 1, §13, was built on 2026-10-15 (§14.3.14)**, with D378 and D379. **Stage 2, core's `retryAfterMs` (MODULE_API 1.15.0), was built on - 2026-10-15 (§14.3.15).** **Stage 3, TruePVE in the zone step, was built on 2026-10-15 (§14.3.16)**, and **stage 4, Dynamic PVP, the same day (§14.3.17)**, and **stage 5, Dangerous Treasures, the same day (§14.3.18).** + 2026-10-15 (§14.3.15).** **Stage 3, TruePVE in the zone step, was built on 2026-10-15 (§14.3.16)**, and **stage 4, Dynamic PVP, the same day (§14.3.17)**, and **stage 5, Dangerous Treasures, the same day (§14.3.18)**, and **stage 6, Guarded Crate, the same day (§14.3.19).** **Then one walk, on Oxide and Carbon, of everything still owed (D331):** §2.7; §11's rows; §12.9; §13's and §14's rows; the in-game rows @@ -1094,6 +1094,7 @@ The org lead answered §14.3's Q26–Q32 on 2026-10-14: | **D377** | **The leaderboard changes only for something new and substantial.** A new title may add its column (*Safecracker*, *Scavenger*, *Treasure Hunter*, *Warden*, *Hoarder* do), but a small plugin's own numbers, such as ZLevels' levels, never become columns (org lead, 2026-10-14, confirming D374 beside D375). | Adding a column for every number a plugin reports. | 14.3.1, 14.3.6 | | **D378** | **The Chinook is called with no position.** It drops only at the map's own drop zones (seven on the rigs' map, all at monuments), and the one route that sends it to a chosen zone borrows the Airfield call terminal's shared state, so a player at the real terminal and a step calling at once would race for one Chinook (stage 1's spike, org lead 2026-10-15). The airdrop and the patrol helicopter take a position. | A position snapped to the nearest drop zone through the terminal's state, refused while a real Airfield call is active. | 14.3.1, 14.3.14 | | **D379** | **`chinook.downed` is a 24th world event**, with the credit block and the operator's rule, and counts toward *Rotorbane*: D174 counted Chinook kills there, and the plan's first draft would have dropped them (org lead, 2026-10-15). | Counting Chinook kills through the old tally, with no line; patrol helicopters only. | 14.3.1, 14.3.14 | +| **D380** | **A guarded crate left standing is noted in the run log when it is placed, not when the run ends:** the *Place a guarded crate* step's success line says the crate is kept when the run ends and how it ends (org lead, 2026-10-15). Core gives a module no hook at a run's end for a step that is kept, so D373's note at the end would need new core work. | A run-end hook in core now (a MODULE_API minor, with the Module-uo proof), or filed for later. | 14.3.5, 14.3.19 | ## 11. First-class optional plugins (D199–D202) @@ -2627,6 +2628,10 @@ the bridge already reads Kits' and Economics' (PROTOCOL.md §16.3), and absent w column and the trigger; let one time out; cancel the run and see the crate stay; unload NpcSpawn and see the step refused with the reason. On Carbon: refused (D363). +> **Built on 2026-10-15** (§14.3.19), specified in PROTOCOL.md §20.9. The events are in Guarded Crate's data +> file, not its config; the location is a world call's (monument or point, no grid); the run-log note is written +> at placement (D380); and the Carbon row now expects the step to work. §14.3.19 says how. + #### 14.3.6 ZLevels Remastered (D369, D374) **Hello.** `integrations.zLevels`: `{ loaded, version }`. @@ -3098,6 +3103,81 @@ actions, 11 budgets, 21 option sources; the 89 routes unchanged. hotload the bridge while a chest stands and see it still on the map (the seed); and start one from a run and read its `dt.started` row's `key` from the staff events API (the web client has no staff feed view). +#### 14.3.19 Stage 6, built: Guarded Crate, with NpcSpawn (2026-10-15) + +Built in Rust-Plugins, Rust-Link and Module-Rust, specified in [PROTOCOL.md](../../rust-link/PROTOCOL.md) §20.9 +and §17.3, with the operator's section in [OPERATING.md](OPERATING.md). `PROTOCOL_VERSION` stays 14. Builds: +Rust 25902425 (changeset 167781), Oxide 2.0.7830, Carbon 2.0.262 production (the latest of each that day); the +code read is Guarded Crate 2.0.17, still the newest on uMod, and NpcSpawn 3.4.8. **D380** answers the one point of +§14.3.5 that could not be built as written. No MODULE_API change: `coreApi` stays `^1.15.0`. + +**What §14.3.5 had wrong or unsaid, found in Guarded Crate's source and on the rigs:** + +1. **The events are in its data file, not its config.** `data/GuardedCrate.json`'s `CrateEventEntries`, each + name under the key `event display name)` (the stray bracket is Guarded Crate's). Its console command is a + config value, read from the config. +2. **`gcrate position` does not check NpcSpawn** (only `start` does). Without NpcSpawn a placed crate lands with + no guards and nothing stops a hack, so `npcspawn-missing` is the bridge's own refusal, not a courtesy. +3. **The key's join is exact**, as Dangerous Treasures' is (§14.3.18): the start hook fires inside the call that + creates the event. §14.3.9's "first start within ten seconds" is not needed for either. +4. **Its unload and `stop all` raise no hook**, and the ended hook does not say which event ended. The bridge + reads Guarded Crate's private list of running events by reflection (it has no API that lists them) to seed + the map after a hotload, to drop events `stop all` ended, and to tell which event a clearing ended. +5. **Its cargo plane was an airdrop.** Stage 1 announces every cargo plane's spawn as `airdrop.called`, so each + guarded crate also said *"An airdrop was called"*; found on the rig, fixed here with Guarded Crate's own + `API_IsGuardedCrateCargoPlane`. §14.3.1's walk row *"a Guarded Crate crate raising no Rust frame"* now holds + for the plane as well as the crate. +6. **The run-log note is written at placement (D380):** core has no run-end hook for a kept step. +7. **The location is a world call's**: a monument with offsets, or a point. §14.3.5 said "monument, grid or + point", but no step takes a grid; the zone and call steps take these two. +8. **The Carbon row is out of date**, as §14.3.18 found for DT: the console probe is true on the Carbon rig, and + `gcrate` ran there. The step is expected to work on Carbon. + +**Settled in the code, and overrulable:** + +1. **A clearing counts only for a player.** A last guard killed by another NPC (RunicNPC's, say) or by nothing + is `gc.ended` with no winner: no *Warden* count, no trigger. +2. **`rust.gc.ended` fires only for a crate a player cleared**; an ending with nobody, and a timeout, raise no + trigger. Both are still feed lines (*"A guarded crate ended with nobody clearing it"* public, the timeout + staff's). +3. **Whether anybody cleared it (`won`) is public**; the clearer's name is the presence audience's, the Steam + id, the position and a step's key staff's. +4. **The event param is a name, not `server/name`.** The location already names the server; the list offers + every server's names once each, with the servers that have them, and Guarded Crate's four defaults where no + server has said. Verify refuses a name the chosen server's report does not list. +5. **`gc-too-close` is retried** on core's ordinary delay (the other crate may be cleared); every other plugin + refusal is permanent. +6. **A height is not sent**: the crate falls to the ground and its guards stand on it. +7. **The column is *Crates cleared***, shown where the server reports Guarded Crate or a row counts one, as + Treasures is. +8. **Where Guarded Crate's list cannot be read** (its shape changed), the hooks alone keep the set, and the log + says so once. + +**Tests.** Bridge: `checkPlugin.js` (84 hooks, every one void and listed), 18 tests. Sidecar: 73 tests (one new, +the stamp), `fmt` and `clippy` clean. Module-Rust server: `test/guardedCrate.test.js`, 16 new (the public read +the grid, never the position or the key; the clearer's name for the presence audience and the Steam id for +staff; an ending with nobody is not a win; the feed reads a stored clearing per viewer; the report read +leniently; the step kept, priced and without a revert; the event list from every server and the defaults; the +dry run's refusals for Guarded Crate absent, NpcSpawn missing, the console unavailable and an unknown event, +ignoring case, and a server that has not said left to the plugin; the key, event and location on the wire and +the kept note; retries and permanent refusals; a repeat; the tally's count; the column's sort; the triggers and +their variables; a crate nobody cleared and a timeout firing nothing; an old crate telling nobody), and the +catalogue, title and registration tests extended: 643/643, imports, bundle, swagger and engagement checks. +Client: three new in `feed.test.js`, 95/95, the build and externals. Booted against the pinned core: 47 triggers, +14 actions, 12 budgets, 22 option sources; the 89 routes unchanged. + +**On the rigs**, both: the bridge compiled and loaded; with Guarded Crate 2.0.17 and NpcSpawn 3.4.8 loaded, +`rg.world` read the four events from the data file and Guarded Crate's list as readable; `gcrate start` and +`gcrate position` (in the fully quoted form `ConsoleSystem.Run` sends) each counted a running crate and said +`gc.started`, with no `airdrop.called` after the fix; a hotload of the bridge found the running crate again +(*"1 guarded crate(s) after load"*); and `gcrate stop all` took it off. Both rigs were then cleaned of the two +plugins and their files, and put back on the protocol-13 bridge. The clearing, the timeout, the step through +the sidecar and the site are §14.3.5's walk rows, at the end walk (D331, D359). + +**Walk rows** are §14.3.5's, with the Carbon row expecting the step to work, and three more: a Guarded Crate +placement raising no airdrop line; `gcrate stop all` taking a crate off the map; and a crate's guards killed by +a RunicNPC NPC, which clears it for nobody. + ### 14.4 Questions for the org lead **Answered on 2026-10-14:** Q17 as proposed, out (D358). **Q18 against the proposal: the end walk waits for diff --git a/rust-link/PROTOCOL.md b/rust-link/PROTOCOL.md index 20a763c..b471c58 100644 --- a/rust-link/PROTOCOL.md +++ b/rust-link/PROTOCOL.md @@ -1805,7 +1805,7 @@ drawing a second time. A new map key makes the file stale, and it is ignored. | Field | Rows | |---|---| -| `world` | `kind` (`cargo`, `heli`, `chinook`, `bradley`, `supply`, `crate`, `boss`, `treasure`), `x`, `z`; a locked crate being hacked adds `hackLeftSec`, a hacked one `hacked: true`; a `boss` (protocol 13, RunicNPC D302) is one of RunicNPC's bosses placed outside any event, with its `name`; a `treasure` (protocol 14, §20.8) is a running Dangerous Treasures chest | +| `world` | `kind` (`cargo`, `heli`, `chinook`, `bradley`, `supply`, `crate`, `boss`, `treasure`, `guarded`), `x`, `z`; a locked crate being hacked adds `hackLeftSec`, a hacked one `hacked: true`; a `boss` (protocol 13, RunicNPC D302) is one of RunicNPC's bosses placed outside any event, with its `name`; a `treasure` (protocol 14, §20.8) is a running Dangerous Treasures chest; a `guarded` (protocol 14, §20.9) is a running Guarded Crate event | | `events` | what this site's events placed (§15.2): `kind` (`zone`, `crate`, `npc`), `runId`, `x`, `z`, and a zone's `radius` and `name` or a thing's `prefab` and `key` (the step that placed it). Protocol 13 adds, for one of RunicNPC's NPCs, its `name`, and for a boss `boss: true` and `health` (0–1); and a boss's **adds**, which no step placed, under their boss's run with `add: true` | | `players` | `steamId`, `name`, `x`, `z`, `sleeping`, `online`. Connected players, then offline sleepers | | `bases` | `kind` (`tc`, `vending`), `x`, `z`. **Positions only**: no owner, no authorised list, no shop name | @@ -3249,3 +3249,128 @@ events like any other. shown where the server reports DT or a row counts one (`dangerousTreasures: true` on the answer). - The map draws a running chest on the world layer. Server settings' plugin line says whether DT is loaded, and whether a step can start one; `GET /admin/rust/servers/{id}/integrations` gains `console` for it. + +### 20.9 Guarded Crate — `gc.started`, `gc.ended`, `gc.failed`, `gc.place` (§14.3.5, D361, D363, D364, D373, D375, D380) + +Bazz3l's **Guarded Crate** (2.0.17, uMod), optional like Economics (D199) and not in `overlay.toml`, with its +dependency **NpcSpawn** (3.4.8, Codefling; D361), which spawns its guards. A cargo plane drops a locked crate at +the event's spot, ringed by guards (8 to 20 by event); **whoever kills the last guard has cleared it**, and the +crate unlocks for them. An event nobody clears ends after its time (1,800 s as it ships, counted from the last +guard killed). The bridge reports its events, and places one for a run's step through Guarded Crate's own +console command. The design is [`PLAN_REDESIGNS.md`](../modules/rust/PLAN_REDESIGNS.md) §14.3.5; what the +§14.2 test found is §14.2.1 there, and what the build found in its source is §14.3.19. + +**Hello and status.** `integrations` gains `guardedCrate`, with NpcSpawn beside it (D364): + +```json +"guardedCrate": { "loaded": true, "version": "2.0.17", + "npcSpawn": { "loaded": true, "version": "3.4.8" }, + "events": ["Easy", "Medium", "Hard", "Elite"] } +``` + +`npcSpawn` is always sent, `{ "loaded": false }` when it is absent. **`events` are the event names in Guarded +Crate's DATA file** (`data/GuardedCrate.json`, each `CrateEventEntries` row's `event display name)`, the key's +stray bracket and all), not its config; it is absent while Guarded Crate is not loaded or when the file cannot +be read. Hello is sent again when Guarded Crate or NpcSpawn loads or unloads. Placing one also needs hello's +`console.plugins` (§20.5, D363). `rg.world` prints `guardedCrate=`, `npcSpawn=`, `guarded=` (the running +events the bridge knows), `gcPlaces=` (keys remembered), `gcEvents=`, `gcCommand=` and `gcListReadable=`. + +**Three events**, from Guarded Crate's own hooks: + +```json +{"kind":"gc.started","type":"event","x":-604.0,"z":-342.0,"key":"12:4"} +{"kind":"gc.ended","type":"event","steamId":"76561198000000001","name":"Alice","x":-604.0,"z":-342.0} +{"kind":"gc.failed","type":"event","x":-604.0,"z":-342.0} +``` + +- **`gc.started`** is `OnGuardedCrateEventStart(position)`. **`key` is set when the start answers one of our + `gc.place` commands**, and the join is exact: Guarded Crate raises the hook inside the call that creates the + event, as Dangerous Treasures does (§20.8). +- **`gc.ended`** is `OnGuardedCrateEventEnded(player, crate)`, raised when the last guard dies. **`steamId` and + `name` are the player who killed it, and are absent when nobody did**: the guards went another way (NpcSpawn + reloaded under them, or they died of falls or drowning), or another NPC killed the last one. The hook does not + say which event, so the bridge finds it in Guarded Crate's own list (the one marked complete), else at the + crate's position or the player's, within 100 m, else the only one running; `x`, `z` are absent if none can be + told. +- **`gc.failed`** is `OnGuardedCrateEventFailed(position)`: its time ran out with guards standing. +- **Guarded Crate's unload and `gcrate stop all` end every event and raise no hook.** Nothing is sent for them; + the map's set is corrected instead (below). +- **None is sent while the world is not live** (§20.5's rule). A clearing is still counted. +- **Guarded Crate's cargo plane is not an airdrop.** The plane that delivers its crate raises no + `airdrop.called` (§20.5): the bridge asks Guarded Crate's own `API_IsGuardedCrateCargoPlane`. Its crate is + never Rust's own either, by §20.5's rule for crates. + +**The tally** gains **`guardedWon`**, the events a player cleared (*Warden*, D375). + +**`map.live`'s `world`** (§17.3) gains `kind: "guarded"` for each running event, `x`, `z`. The bridge keeps the +set by the hooks, and **checks it against Guarded Crate's own list** (its private `EventManager.Instances`, read +by reflection, since it has no API that lists them) at its load, before every `map.live` and on `rg.world`: a +hotload of the bridge hears no starts, and `stop all` ends events with no hook. Where the list cannot be read +(said once in the log), the hooks alone keep the set. It is cleared when Guarded Crate unloads. + +**`gc.place` — place one guarded crate for a run (D364, D373).** + +```json +{"cmd":"gc.place","reqId":"r-62","key":"12:4","event":"Elite","monument":"powerplant_1","offsetX":10,"offsetZ":-5} +``` + +`event` is one of `events`, matched ignoring case as Guarded Crate matches it. **The location is a world +call's** (§20.5): a `monument` with `offsetX`/`offsetZ`, or `x` and `z` (and `y`, else the ground). The bridge +runs ` position x,y,z` through `ConsoleSystem.Run`, where `` is Guarded Crate's +console command from its config (`gcrate` unless the operator renamed it). The answer is: + +```json +{"kind":"gc.placed","type":"reply","reqId":"r-62","key":"12:4","event":"Elite","repeat":false,"x":-604.0,"z":-342.0} +``` + +**A key already answered is answered again with its first placement, `repeat: true`, and places nothing.** Keys +are kept seven days in `data/RunicGateway/gc-places.json`. **Nothing removes a placed crate** (D373): Guarded +Crate cannot end one event alone. Refusals are `{"kind":"gc.error","reason":…,"message":…}`: + +| `reason` | When | +|---|---| +| `malformed` | No key, or one over 128 characters; or a location that is both or neither (§20.5) | +| `bad-event` | No event named, a name not in `events`, or Guarded Crate's own "event not found" | +| `gc-missing` | Guarded Crate is not loaded | +| `npcspawn-missing` | NpcSpawn is not loaded. **Guarded Crate's `position` does not check this itself** (only its `start` does), so without the bridge's refusal the crate would land unguarded and free to hack (D364) | +| `console-unavailable` | Hello's `console.plugins` is false (D363, Rust-Plugins#34) | +| `no-monument`, `out-of-range`, `off-map` | The location, as §20.5 | +| `gc-too-close` | Another guarded crate is running within 80 m (Guarded Crate's own rule) | +| `bad-position` | Guarded Crate's own "position invalid" | +| `refused` | Nothing was placed and Guarded Crate answered something else, in its own words; or its console command is switched off in its config | + +`events-disabled` and the world's `not-ready` come first, as `world.error`, as for every world verb (§15). +"Placed" is the start hook firing; Guarded Crate's replies are recognised by reading its own messages from its +lang file, as §20.8 does for DT. + +**Hooks.** `OnGuardedCrateEventStart`, `OnGuardedCrateEventEnded` and `OnGuardedCrateEventFailed` join +`rg.hooks`' list. + +**The sidecar** adds `POST /gc/place`, a thin forward that keeps nothing, and stores and forwards the three +events like any other. + +**The website (Module-Rust).** + +- **`gc.started` and `gc.ended` are public kinds, `gc.failed` a staff kind.** Each row is read per viewer by + `guardedCrate.shapeFor`, as §20.8's are: everyone gets `grid`, and for an ending `won` (whether a player + cleared it); **the clearer's `name` only for the presence audience**; **`x`, `z`, `key` and the clearer's + `steamId` only for staff**. The lines read *"A guarded crate landed · M4"*, *"Alice cleared the guarded crate · + M4"* (*"The guarded crate was cleared"* below the presence audience), *"A guarded crate ended with nobody + clearing it"*, and for staff *"A guarded crate timed out with its guards standing"*. The first two are in the + feed's *World events* filter. +- **Triggers `rust.gc.started` and `rust.gc.ended`** (§14.3.9): public, `everyone` the ceiling, the cooldown + subject the server; `byEvent` on a placement one of the site's steps made; **`rust.gc.ended` fires only for a + crate a player cleared**, with the clearer by in-game name only. A timeout raises nothing. +- **The step `rust.gc.place`**, *Place a guarded crate* (`risk: 'change'`, `reversible: 'none'`, budget + `rust.gc.crates`, one per step): the event (option source `rust.options.gc_events`, every server's own names, + or Guarded Crate's four defaults where none has said) and a world call's location. It is refused at verify + where the server said Guarded Crate is not loaded, that NpcSpawn is missing (*"Guarded Crate is loaded on Main + but NpcSpawn is missing: its events cannot start"*), that `console.plugins` is false, or that it has no event + of that name; a server that has not said is left to the plugin. **Its success line in the run log says the + crate is kept when the run ends** and how it ends (D373, D380). `gc-too-close` and a game that is down retry on + core's ordinary delay; every other refusal is permanent. +- **Warden (D375):** `guardedWon` → `rust_player_wipe_stats.guarded_won`, the title category `guardedwon` + (default *Warden*), and the leaderboard's **Crates cleared** column (`sort=guardedWon`), shown where the + server reports Guarded Crate or a row counts one (`guardedCrate: true` on the answer). +- The map draws a running event on the world layer. Server settings' plugin line names Guarded Crate and + NpcSpawn, the events, and whether a step can place one.