Compare commits

...
Sign in to create a new pull request.

413 commits

Author SHA1 Message Date
Tald0r
38c74a968b
fix(regions): correct end Z coordinate assignment in InitRectangles (#2597)
The `ez` variable was incorrectly assigned `rect.End.X` instead of `rect.End.Z`, causing incorrect rectangle processing in region initialization.
2026-08-27 06:51:42 -07:00
Kamron Batman
e7f85d404d
feat: Adds independent think/move clocks for creature AI to fix speed (#2591)
Splits creature speed into two clocks so movement pace can be tuned without touching reaction time:

- **Think clock** — `ActiveSpeed`/`PassiveSpeed`/`CurrentSpeed`: seconds per AI decision. Unchanged in meaning, storage, and cadence.
- **Move clock** — `ActiveMoveSpeed`/`PassiveMoveSpeed` (+ resolved `CurrentMoveSpeed`): seconds per step. `0` = inherit the matching think value.

### How

- Move speeds come from optional `activeMove`/`passiveMove` in `npc-speeds.json`, are `[props`-tunable per instance (set `0` to re-inherit), and serialize (BaseCreature v22).
- `SetSpeed()` keeps its legacy one-clock semantics — sets the think clock **and clears move overrides** — so existing callers cannot half-configure a creature. `SetMoveSpeed()`/`ClearMoveSpeed()` configure movement explicitly; `ScaleMoveSpeed()` scales overrides for buffs.
- `CurrentMoveSpeed` is derived by classifying `CurrentSpeed`: a verbatim active/passive think value maps to the matching move value; a bespoke pace written directly (mount boosts, follow sprint) stays fused to both clocks. External `CurrentSpeed` writers need no changes.
- `AITimer` schedules the earlier of the two deadlines. Decisions run at the think cadence exactly as before; while a pursuit/investigation is live, the timer also wakes when the movement budget elapses and advances one step with no decisions. Steps no longer snap to the think grid, so any step delay paces smoothly on the 8ms wheel. A blocked creature schedules no move wakes.
- The movement budget is RunUO's `m_NextMove` accumulate-and-clamp at a full step, so long-run pacing averages `CurrentMoveSpeed` exactly.

### Behavior changes

- **`npc-speeds.json` buckets get RunUO `TransformMoveDelay`-parity move values**: creatures step at RunUO pace while thinking/reacting at current speed. The situational +0.1/+0.2 offsets are deliberately omitted.
- **Existing saves migrate on load**: a pre-v22 creature whose think speeds still match its npc-speeds entry (never hand-tuned) adopts the table's move values — worlds and pets pick up the new pacing without a respawn. Tuned creatures keep movement inheriting their think clock.
- **Paragons scale movement by `SpeedBuff` (1.2x)**: RunUO had no deliberate policy here — dividing by 1.2 knocked most speeds off `TransformMoveDelay`'s exact-equality table (raw pass-through, 2x+ faster), while 0.3/0.6 creatures landed back on it for ~1.33x. This applies the uniform 1.2x the buff always claimed. UnConvert snaps speeds back to exact table values within 1e-4 — /1.2 then ×1.2 drifts 0.45 and 0.9 by an ulp, which would read as hand-tuned (and defeat a future skip-table-conformant-values serialization pass); tuned speeds keep.
- **Herding paces the movement clock**: the old `CurrentSpeed` getter hack is gone. A herded creature walks at a fixed 0.3s/step — RunUO's forced pace, without its `TransformMoveDelay` inflation to 0.6 — so herding is never penalized by a slow creature. Thinking is untouched, and `CheckHerding` walks through `MoveToPoint`, so herded creatures path around obstacles.
- **Badly-hurt slowdown now inflates the step delay only** (RunUO parity), computed from the base each step. Previously it wrote `CurrentSpeed = CurrentSpeed + 0.05..0.15` back on every successful step — compounding unboundedly while hurt and slowing decisions too.
- Removes the vestigial `MoveSpeedMod` (never read, written, or serialized).
- With no bucket or per-instance move values, both clocks carry identical values and creatures pace as before.

### Testing

- Full suite passes (1557, including 12 new `MoveSpeedTests`: resolution classes, `SetSpeed` clearing, `0`-re-inherit, v22 round-trip with exact-consumption check, save migration adopt/skip, buff scale/snap, herding).
- In-game verified via local diagnostics build (per-step budget tracing): steady 700ms step cadence on a 0.3s think grid with one-step catch-up after idle, think grid unperturbed by move wakes.
2026-08-23 10:19:59 -07:00
Kamron Batman
8e39da2810
fix: creatures track and chase targets reliably around corners (#2590)
### Summary

Fixes the long-standing reports of monsters losing track of players who run around a corner ("Is monster AI not using pathfinding? It seems to be LOS blocked by statics"). Root-cause investigation compared current behavior against RunUO line-by-line and traced the regressions through the AI overhaul era (#2232, #2246, #2379, #2401, #2461).

### Root causes and fixes

1. **Movement contract** — `MoveTo`/`ApproachTarget` returned false on every healthy mid-chase tick (true only on arrival), so MeleeAI's RunUO-inherited *"move failed and beyond RangePerception+1 → Guard"* clause — which RunUO only evaluated on genuine blockage — fired **every tick of every chase**. A mounted player trivially opens 17 tiles at a corner, the monster guards, Guard nulls the combatant, and re-acquisition is LOS-gated — unrecoverable through a wall. Movement now reports failure only on genuine failure (no step taken with no working path, or approach give-up). ArcherAI's equivalent clause moves to the hard leash.

2. **Last-known-position pursuit** — while a combatant is in LOS its position is recorded each think tick. When the target vanishes (corner, hiding, recall), the creature walks to the last-seen spot, stands guard there ~10s (restoring RunUO's guard grace, which had decayed to a single tick since #2246), and **re-engages instantly** if the same target re-enters view — bypassing the 10s reacquire throttle.

3. **`ChaseLeashRange`** — new virtual on BaseCreature (default `RangePerception * 2` = 32 tiles) replaces the inline `RangePerception * 3` (48) in Melee/Mage/Archer AI. Per-creature tunable via `[props`.

4. **Group movement demoted to a crowding refinement** — previously any uncontrolled creature with one ally within 8 tiles on the same target used greedy ring-stepping for the *entire* chase, with wall-slides counted as success, never invoking the pathfinder — the "aggroed but won't come around the corner" symptom for spawn groups. It now engages only near the target when allies actually contest the ring, and blocked/wall-slid steps escalate to the pathfinding approach primitive.

5. **Mages close distance on broken LOS** — a mage within casting range but LOS-blocked by geometry stood at the wall holding a spell target until the 60s combatant expiry (ProcessTarget short-circuits Think and its RunTo stands off at RangeFight). Geometry-blocked mages now close in until LOS returns, both pre-cast and while holding a target. Hidden targets (CanSee) and poison-cure priority unchanged. The new movement contract also stops the constant spurious `OnFailedMove` teleport rolls mid-chase.

6. **Move budget: one actual step per AI tick** — nothing advanced `NextMove` on a normal step (RunUO's `m_NextMove` budget was lost), so code paths attempting several moves in one think tick could cross multiple tiles at once — visible as "warping" when crowded creatures jockey for position. A successful step now consumes a half-step budget (floor 50ms): blocks intra-tick double moves, stays safely below the timer interval so legitimate next-tick moves are never jitter-throttled, and does not reintroduce `TransformMoveDelay` inflation. Blocked attempts consume nothing, so retry ladders (repath-and-step, the collision fan) are unaffected. `CanMoveNow` is also wraparound-safe now.

### Reference behavior

RunUO requires LOS to *acquire* a target and to *land* a hit or spell — never to *continue* a chase (its MeleeAI LOS bail-out is literally commented out in stock code). Chases drop only on: target hidden, target dead/off-map, beyond `RangePerception * 3`, 60s without combat interaction, or blocked movement while far away. This PR restores those semantics while adding the last-known-position investigation on top. NPC run flags are untouched — pace is AI-timer-driven and most NPC art has no run animation.
2026-08-23 01:13:00 -07:00
Kamron Batman
2935eafe24
feat: convert all delta-time serialization to anchored time (#2589)
## Summary

Phase 3 of the anchored-time work: **every actively-written delta-time value in the engine now stores an anchored timestamp** — absolute on the wire, shifted forward by the downtime at load. Remaining time survives restarts (as delta did), and unlike delta, the bytes do not change on every save, so an idle world serializes identically save after save.

The answer to "is it possible everywhere": **yes** — including the one case that looked impossible.

## The GenericPersistence problem, solved

`GenericPersistence` bins (`Virtues.bin`, `StealableArtifacts.bin`, …) are raw payloads with no idx header, so they have no anchor of their own — anchored reads there would silently apply zero shift. But the anchor is a property of the **save**, not the file: every file in one save shares one `World.SaveStartTime`, and `Persistence.Load` reads **all** entity indexes (phase 1) before **any** persistence payload (phase 2). So the idx v5 header stamps a save-wide `World.LoadTimeShift`, and generic persistence readers inherit it. No file-format change, no per-bin header, old bins unaffected.

## Converted

- **Item v10 → v11**: `LastMoved` — previously whole-minute delta, rewritten every save for every item, the single largest source of idle-save churn — and `DecayResetTime` (retiring the TODO from #2583). **Mobile v37 → v38**: the three stat-gain stamps. **BaseCreature v20 → v21**: `SummonEnd`.
- **17 code-generated classes** (`[DeltaDateTime]` → `[AnchoredDateTime]`, version bump + `MigrateFrom` each): the five field spells, TransientItem, VirtueContext (×7 fields), PuzzleChestSolutionAndTime, BaseCamp, BaseBoat, RentedVendor, PlayerVendor, Ethics Player, Sheep, StarRoomGate, ChampionSpawn (×3), Corpse (`TimeOfDeath`, v19). The `MigrateFrom` bodies were generated from each class's current migration schema and are compiler-verified; VirtueContext's save-flagged nullables fall back to the same defaults the old deserialize left in place. Corpse's six migrations moved to a new `Corpse.Migrations.cs`.
- **Hand-written sites**: StealableArtifacts (v2), VendorInventory (v1), ML quest objectives (persistence v3) — each gated on its own version.

**Not converted, deliberately**: the ~25 read-only `ReadDeltaTime` sites in legacy version fallbacks and migration replays — they decode existing old bytes and must never change. `[DeltaDateTime]`/`WriteDeltaTime` remain available for them.

## Verification

- Build 0 errors / 0 warnings; **837 + 708 tests green**.
- Schema regeneration produced exactly the 17 expected new `vN.json` files (all `AnchoredTime` rule args), nothing else touched.
- **New acceptance tests** pin the point of the whole effort: serializing the same item at two save times **5 hours apart produces byte-identical output**, and `LastMoved`/`DecayResetTime` round-trip **exactly** at sub-minute precision (the old minutes encoding destroyed both properties).

## Notes for review

- `LastMoved` grows from a 1–3 byte encoded minutes value to 8-byte ticks per item — the price of byte-stability; it repays itself in incremental-save behavior since unchanged items now produce unchanged bytes.
- BaseEscortable-style semantics are unchanged: anchored shift preserves *remaining* time exactly, the same contract delta provided, so no gameplay-visible behavior changes — deadlines simply stop being consumed by downtime that delta already protected against, now with stable bytes.

## Enforcement

`WriteDeltaTime` is now `[Obsolete]` (interface + implementation). With the repo's warnings-as-errors, any new delta-time write — hand-written or emitted by a still-unconverted `[DeltaDateTime]` field — fails the build, with the migration instructions in the message. That the full solution still builds with **zero warnings** is itself the proof no active delta writer survived the conversion. `ReadDeltaTime` deliberately stays un-attributed: its remaining callers decode existing old bytes and are correct forever; its XML docs now state the legacy-decode-only contract.
2026-08-22 19:34:43 -07:00
Kamron Batman
b992c7b955
docs: update serialization docs and skills for generator v4 (#2588)
## Summary

Brings every serialization-related doc, skill, and the CLAUDE.md rule in line with generator **v4** (adopted in #2586/#2587). No code changes.

**Updated surface, everywhere it was referenced:**
- `[SerializableFieldSaveFlag(order)]` / `[SerializableFieldDefault(order)]` → `[SaveFlag(nameof(Should), nameof(Default))]` on the field (second method optional).
- `[TimerDrift]` + `[DeserializeTimerField(order)]` → `[DeserializeTimer(nameof(Method), wallClock)]` on the field, with the anchored-time semantics spelled out: drifting by default (downtime preserves the remaining delay, idle saves byte-stable), `wallClock: true` for absolute deadlines, restart method invoked **only when a timer was running** (no sentinel), and the timer `MigrateFrom` pattern (`XxxNext`/`XxxDelay`) for wire-format changes.
- New `[SerializableField]` documentation: the real signature (the documented `saveIf` parameter never existed) plus the setter hooks — `allowFieldChange` (`bool Method(ref T value)`: coerce/veto before assignment) and `fieldChanged` (`void Method(T oldValue, T newValue)` after) — with the generated pipeline and the SG3015/SG3018 guardrails.
- `[SerializableProperty]` guidance narrowed to its remaining purpose: custom getters and setter semantics the hooks cannot express.
- `[AnchoredDateTime]` documented alongside `[DeltaDateTime]` (now marked legacy, with the version-bump warning for converting between them).

**Files:** `dev-docs/serialization.md`, `dev-docs/timers.md`, `dev-docs/claude-skills/modernuo-serialization.md`, `dev-docs/claude-skills/modernuo-timers.md`, `dev-docs/runuo-migration-docs/02-serialization.md`, `dev-docs/runuo-migration-docs/03-timers.md`, and a condensed v4 addition to CLAUDE.md rule 9.

**Example refresh:** the skill's `BagOfSending` "custom properties" example was itself converted in #2587 — it is now quoted in its real post-conversion form as the canonical hooks example; the real-examples list points at `BaseWeapon.cs` for custom getters and `BaseLight.cs` for the drifting-timer + `MigrateFrom` pattern.

Verified by grep: zero references to the removed v3 attribute names remain anywhere in `dev-docs/` or `CLAUDE.md`.
2026-08-22 18:29:27 -07:00
Kamron Batman
b042edcf0b
refactor: fold hand-written serializable property setters into field hooks (#2587)
## Summary

Folds **113** hand-written `[SerializableProperty]` members into plain `[SerializableField]` declarations using the v4 setter hooks — value coercion/vetoes via `allowFieldChange`, post-change side effects via `fieldChanged` (whose `oldValue` parameter covers the old-house/old-sender unsubscribe patterns). Net **-450 lines** of setter boilerplate.

```cs
// before
[SerializableProperty(1)]
[CommandProperty(AccessLevel.GameMaster)]
public int Charges
{
    get => _charges;
    set
    {
        _charges = Math.Clamp(value, 0, MaxCharges);
        InvalidateProperties();
        this.MarkDirty();
    }
}

// after
[SerializableField(1, allowFieldChange: nameof(AllowChargesChange))]
[SerializedCommandProperty(AccessLevel.GameMaster)]
[InvalidateProperties]
private int _charges;

private bool AllowChargesChange(ref int value)
{
    value = Math.Clamp(value, 0, MaxCharges);
    return true;
}
```

## How sites were selected

A classifier parsed all 204 `[SerializableProperty]` sites and converted only those matching strict shapes: getter is exactly `get => _field;`, the assignment comes first (after at most an equality guard), and relocated side effects contain no `return`, no `value` mutation, and no field re-assignment. Everything else was left alone deliberately:

- **~34 custom getters** (fallback defaults like `_x == -1 ? Default : _x`, self-healing refs) — no setter hook can express these.
- **~35 pre-assignment logic** (durability Unscale/Scale sandwiches, old-state captures like PotionKeg's pile weight).
- **virtual/override members, name-mismatched backing fields (`m_`), exotic semantics** (guards' `Focus` does work on *equal* assignment; `ChampionSpawn.Active` never assigns its field).

Five sites the classifier refused were converted by hand where the hooks fit cleanly: `ReceiverCrystal.Sender`, `PlayerVendor.House`, `PlayerBarkeeper.House` (old-value unsubscribe via `oldValue`), `BaseSuit.AccessLevel` (its existing virtual `OnAccessLevelChanged` already had the exact callback shape), and `DyeTub.DyedHue` (a true veto: `AllowDyedHueChange(ref int value) => _redyable`).

## Verification

- Build: **0 errors, 0 warnings**.
- **Schema regeneration produces zero Migrations changes** — the conversion is wire- and schema-neutral by construction (same orders, types, and property names), and CI's schema diff check enforces it.
- **835 + 708 tests green.**

## Behavioral notes (all strict improvements, called out for review)

- Generated setters skip everything when the incoming value equals the current one; a few converted setters previously re-ran side effects on equal assignment (redundant `Update()`-style refreshes).
- Generated setters always `MarkDirty()` on change; several converted setters never did (e.g. `DyeTub.DyedHue`, `MorphItem` ranges) — their changes only persisted if something else dirtied the entity. Those latent persistence bugs are fixed by construction.
2026-08-22 18:20:39 -07:00
Kamron Batman
73f9688083
feat: adopt serialization generator v4 (field-side linkage, anchored timers) (#2586)
## Summary

Adopts ModernUO.Serialization 4.0.0 across the engine. Three commits, reviewable independently:

1. **Package + tool bump to 4.0.0** (`Server.csproj`, `UOContent.csproj`, `dotnet-tools.json`).
2. **Timers → `[DeserializeTimer]`** — the 8 drifting timers (BaseLight, TreasureMapChest, MarkContainer, FillableContainer, DeathRobe, DecayedCorpse, Corpse, BaseEscortable) now store their next tick as **anchored time**: server downtime no longer consumes the remaining delay, and idle-world saves are byte-stable. This changes their wire format, so each class bumps its serialization version with a `MigrateFrom` that replays the old delta-time read through the migration schema (the new `vN.json` files carry `@AnchoredTimer`; the old ones keep `@TimerDrift`, which the generator reads forever). The 2 wall-clock timers (Aquarium, FountainOfLife) keep their exact format via `wallClock: true` — no bump. Restart methods drop their `TimeSpan.MinValue` sentinel checks: v4 invokes them **only when a timer was actually running at save**.
3. **Linkage → field-side declarations** — 175 conversions across 25 files: `[SerializableFieldSaveFlag(order)]`/`[SerializableFieldDefault(order)]` become `[SaveFlag(nameof(...), nameof(...))]` on the field, and `[SerializableFieldChanged(order)]` becomes the `fieldChanged:` argument of `[SerializableField]`. **Wire-neutral: zero migration schemas changed.**

## Verification

- Solution builds with **0 errors, 0 warnings**; all three 4.0.0 packages verified indexed on nuget.org (no local feed needed).
- **835 + 708 tests green.**
- Generated output inspected: old-version content structs replay `ReadDeltaTime` (e.g. `V3Content.DecayTimerNext = reader.ReadDeltaTime()`), current versions write/read anchored time with the gated restart, and the wall-clock classes emit byte-identical `Write`/`ReadDateTime` framing.
- Schema tool run is committed (CI's `git diff --exit-code` schema check passes): exactly the 8 expected new `vN.json` files, nothing else touched.
- The conversion was scripted with a class-scoped resolver (order → same-class `[SerializableField(order)]`/`[SerializableProperty(order)]`); it planned 175/175 with zero ambiguities before applying.

## Notes

- New `MigrateFrom`s use the content structs' provided `XxxDelay` property, matching the pre-existing idiom in Corpse's and TreasureMapChest's older migrations.
- Follow-up candidate (separate PR, wire-neutral, any time): fold the ~150 eligible hand-written `[SerializableProperty]` setters (clamps, post-change side effects) down to `[SerializableField]` with `allowFieldChange`/`fieldChanged` hooks.
2026-08-22 17:54:02 -07:00
Kamron Batman
126a10ce53
feat: anchored-time infrastructure with a save-start anchor in idx v5 (#2585)
## Summary

The save-stability infrastructure consumed by generator v3's `[AnchoredDateTime]`: anchored timestamps are written as **absolute values** and re-based once at load by the elapsed time since the save started — so downtime doesn't age them, and an unchanged entity serializes to identical bytes (the prerequisite for replacing delta-time encodings, which rewrite every entity on every save).

## Design

- **`WriteAnchoredTime` / `ReadAnchoredTime`** on `IGenericWriter`/`IGenericReader`. The read side applies the reader's `AnchoredTimeShift`; `Min/MaxValue` sentinels pass through unshifted, and shifts saturate instead of overflowing.
- **`World.SaveStartTime`** is stamped the moment the world freezes for a snapshot — one anchor for the entire save, no per-persistence skew.
- **idx v5**: the anchor ticks sit in the header right after the version. The anchor travels with the file it re-anchors, so a single idx+bin pair restored from a backup is self-describing, and anchor presence is guaranteed by the same version gate as the record format — there is no separate anchor file to lose.
- **The shift rides the reader instance** (`BufferReader`, `UnmanagedDataReader`, `BinaryFileReader` delegating), not a static — parallel per-persistence loads and ad-hoc restores each see their own file's anchor. idx v4 and older read with a zero shift.

## Scope

Behavior-neutral: nothing serializes anchored values yet (`Item.DecayResetTime` and the `[DeltaDateTime]` field migrations come separately, with their own version bumps). Saves written from this branch are idx v5; loading v4/v3 saves is unchanged and remains pinned by the existing hand-written-header tests.

## Testing

- Unit round-trips: exact with zero shift, shifted read, sentinel passthrough, saturation, Local→UTC normalization.
- End-to-end through the real worker/segment-log pipeline: an anchored timestamp re-bases across a simulated two-hour downtime via the idx v5 header.
- Full suites green: Server.Tests 835/835, UOContent.Tests 708/708 (including the existing v4/v3 idx loading tests).
2026-08-22 15:59:39 -07:00
Kamron Batman
541dbc5ac5
feat: Bumps dependencies. Introduces Serialization Generator v3 (#2584)
### Summary

* Upgrades Serialization Generator to v3. This contains numerous bug fixes and a significant performance improvement.
* Bumps other dependencies.
2026-08-22 15:48:53 -07:00
Kamron Batman
971d7b6a77
fix: stop items from insta-decaying when decay eligibility is restored without a move (#2583)
## Summary

A GM flipping `Movable` back on for a long-frozen item made it vanish within one scheduler tick. The setter registered the item with a deadline computed from its stale `LastMoved`, so `ProcessActiveQueue` deleted it almost immediately. The pre-#2311 save-time sweep had the same semantics, just hidden behind the save cadence. The same failure existed for `Visible` and `Spawner` transitions.

`LastMoved` is deliberately left meaning actual movement — it feeds vendor inventory expiry and house moving-crate checks — so the fix does not rewrite it for state changes.

## Changes

- **`DecayResetTime`** (CompactInfo-backed): the decay countdown runs from the later of `LastMoved` and this stamp. `RestartDecay()` stamps it only when the item can decay and the stamp extends the current deadline, so hot paths with a fresh `LastMoved` allocate nothing.
- **`Movable`/`Visible`/`Spawner` setters** call `RestartDecay()` instead of registering a stale deadline.
- **Region-refusal retry** in `DecayScheduler` uses `RestartDecay()` instead of rewriting `LastMoved`.
- **Persistence**: the stamp survives save/load as a `WriteDeltaTime` delta under `SaveFlag.DecayReset` (to become `WriteAnchoredTime` once the save-time anchor is ported) (Item serialization v10), so a restart mid-window no longer deletes the item.
- **`LastMoved` setter** drops a superseded stamp so the `CompactInfo` can collapse instead of being held (~40 bytes) forever.
- **Raw `Map` setter** now counts as a move for parentless items: it stamps `LastMoved` and updates decay registration, closing the gap where an item moved out of `Map.Internal` via the setter never decayed.
- **`LiftItemDupe`**: the remainder of a partially lifted *ground* stack was placed via raw `Location`/`Map` assignments and never enrolled for decay (lingering-trash leak since #2311) — now enrolled via the Map setter. Parented remainders get their map from `AddItem` (parent first, then map), so container splits never transit the scheduler.
2026-08-22 12:56:00 -07:00
Kamron Batman
fd27b7a3c9
chore: Simplify server requirements section in README (#2582)
Removed unnecessary details about game logic and server requirements.
2026-08-21 19:22:04 -07:00
Kamron Batman
be3a08513f
fix: Fixes PlayerConstructed stacking/BODs (#2579)
### Summary

* Removes player constructed as a requirement for BODs.
* When two items stack and they don't match player constructed flags, the resulting stack loses the flag.
2026-08-14 17:14:38 -07:00
Kamron Batman
2dbaa87377
feat: make the blocklist and manual allowlist opt-in; cut the ban subsystem's on-loop cost (#2577)
Two features ran on every shard out of the box, each polling on its own 60s timer for files most shards never generate, neither ever asked for. Fixing that turned into untangling why they shared a config file — and then into the on-loop cost of the three lists behind them.

## Before / after

Measured on the shipped defaults. On-loop numbers are what freezes the world; the tick budget is 8 ms.

| | before | after |
|---|---:|---:|
| Blocklist poll on a shard with no list | every 60s, forever | **none** (opt-in) |
| Manual allowlist poll on a shard with no carve-outs | every 60s, forever | **none** (opt-in) |
| Promote-guard sweep timer | leaked on `Stop()` | stopped, and only started when hits are reported |
| Login allowlist flush, on-loop | O(n) walk + 2 arrays **every 60s**, LOH past ~5,300 entries | reused buffers, **hourly**, zero steady-state allocation |
| Auto-denylist, accept path | 9.1 ns/call | **6.1 ns/call** |
| Auto-denylist, sustained flood at cap (60k rejected) | 26.7 ms | **9.3 ms** |
| Auto-denylist, flood end — **worst single call** | 9.49 ms | **0.05 ms** |
| Auto-denylist cap | 65,536 (stranding 9,895 slots) | **324,449** (exact `HashSet` capacity, ~19 MB) |

The auto-denylist row that matters is the third: the on-loop stall at flood end drops **190×**, because retiring lapsed holds is now the number expiring rather than the number held.

## Why this design

It is built for the shape of attack these shards actually see: **hundreds to a few thousand connections per second**, occasionally tens of thousands, sustained over minutes rather than delivered instantly. Against that shape the cap now covers the whole observed range (50k–250k distinct sources) in memory, and the work of expiring them spreads across the accept calls that were already happening.

There is one case this design is *worse* at than the old one: if every held entry lapses within the same millisecond, retiring them costs ~10.7 ms against the old ~8.9 ms, because the ring's random-access set removals lose to a sequential dictionary scan. Reaching it requires an entire flood to arrive inside one millisecond. **A shard absorbing 324,449 connections in a millisecond is finished at the accept path no matter what this list does** — that is the point where the answer is upstream security and scrubbing (an L4 proxy, edge filtering, a bouncer at the kernel), not a data structure in the game loop. We chose the design that fits the attacks we see and degrades honestly past them, rather than over-engineering for one we do not.

## Blocklist — now opt-in

`BlocklistFilter.Start` only bailed when `_path == null`, which needs `file` to be empty. The default is `"Configuration/ip-blocklist.txt"`, so on any default install both `Task.Run(PollLoop)` and a recurring `SweepGuard` timer started unconditionally, logging *"Blocklist inert: no list at …; polling every 60s"* and then doing exactly that forever.

Adds `"enabled"`, default `false`, using the `_enabled = s.Enabled && <preconditions>` idiom already in `LoginAllowlist` and `AutoDenylist`. **Upgrade is deliberately loud**: a missing key binds to the default, so `LogWhyDisabled()` splits three cases and a shard with a list on disk but no `enabled` key gets a **Warning**, not silence.

## `FileAllowlist` → `ManualAllowlist`, with its own config

Moves to `Configuration/ip-allowlist.json` (`enabled` default `false`, `files`, `reloadInterval`) and into `Network/ManualAllowlist/`, mirroring `Network/LoginAllowlist/`.

It was never a sub-feature of the blocklist. `ManualAllowlist.Contains` has two callers:

| Caller | Could anything else do it? |
|---|---|
| `BlocklistFilter.Evaluate` | **Yes** — the generator already subtracts these files at generation time |
| `BanExemptions.IsExempt` | **No** — sole mechanism for suppressing behavioural ban contributions |

The second reaches `BanChannel.IsExempt` with no blocklist in the path. A shard running **no blocklist** still needs this so the admin's own IP isn't auto-banned by rate-limit detection, so a shared flag couldn't express it — the implication is asymmetric. They still work together via a startup warning when the blocklist is on and the allowlist is not.

On the name: "File" described the storage. The distinction from `LoginAllowlist` is **provenance** — declared by an operator versus earned by authenticating — and "Manual" matches `BanReasons.Manual`. `allowlistFiles` is removed from `BlocklistSettings` outright; blocklists have not shipped long enough for anyone to have set it.

## Login allowlist flush

`Flush()` allocated two arrays sized to the live entry count and copied the whole dictionary into them **on the game loop**, every 60s. `UInt128` is 16 bytes, so past ~5,300 entries that first array was an LOH allocation once a minute, forever. The file write was already off-loop; the walk was not.

Static buffers grown geometrically; the writer owns them until it posts completion back through `Core.LoopContext`, so `_writing`/`_dirty` stay loop state (rule #10). Interval → 1 hour against a 90-day TTL. Clean shutdown writes synchronously via `EventSink.Shutdown`; `HandleClosed` skips `InvokeShutdown` when crashed, so the crash path subscribes separately and only writes when it is actually on the loop thread. Also fixes a pre-existing hole where `_dirty` was cleared *before* the write, so a failed write dropped entries despite the comment promising a retry.

## Auto-denylist: expiry ring

Reclaiming lapsed holds was O(entries held) — every cap-triggered reclaim during a flood walked the whole dictionary to find the few that expired, and `_warnedFull` suppressed the log, not the work.

A hold is **never refreshed** now: the first detection sets the expiry, later ones leave it. That makes insertion order equal to expiry order, so a ring of the same keys is sorted by construction and retiring stops at the first live record. Nothing is lost — the rate limiter runs *ahead* of the connection filters (`NetState.Network.cs`) and reports to the ban channel, so a flooder whose hold lapses is re-held on its next attempt.

Because the ring carries the expiry, the membership side only answers "present?", so it is a `HashSet` — measured at **36 B/slot against the dictionary's 52**. `HashSet` and `Dictionary` share `HashHelpers`, so the from-empty capacity progression is identical (36,353 → 75,431 → 156,437 → 324,449 → 672,827) and the cap still lands on one exactly. The ring is parallel `UInt128[]`/`long[]` rather than an array of structs — `UInt128` forces 16-byte alignment, so a packed pair costs 32 bytes where these cost 24, and the drain reads only the `long[]`.

Rejected after measuring: splitting the drain into a scan loop plus a removal loop (inside noise — both issue N hash removes, and the pointer math was never the bottleneck), and `Dictionary<UInt128,bool>` with tombstoning instead of removal (10% slower *and* unbounded, which breaks the cap).

## Testing

Build clean, 0 warnings. **1,530 tests pass** — 708 UOContent, 822 Server.

Tests were reworked rather than patched: the refresh test inverts to `Repeat_detection_does_not_extend_the_hold`, the obsolete sweep-throttle test is deleted along with the throttle, and four were added for the ring — set/ring parity, release-then-re-hold not being retired by the stale record, exact fill of a non-power-of-two cap, and the moved allowlist config's casing contract. The throttle test added mid-PR was verified to fail without its fix before being deleted.

One commit is comments only (verified: a diff filtered of `//` lines is empty), removing development narration — a `"(Task 2)"` plan reference, `"matching the per-feature JSON config pattern used by X"` across four loaders, a duplicated threading note — and repointing `Firewall` at `dev-docs/ip-bans-and-allowlists.md` instead of a "ban-channel design doc" that does not exist.

Note `Distribution/Configuration/blocklist.json` is gitignored (`.gitignore:14`) and generated from the record defaults on first boot, so the record default *is* the shipped default.
2026-08-13 23:22:35 -07:00
Kamron Batman
240118340e
fix: stop the idle-sleep backoff tripping on healthy hosts (#2572)
## Problem

The late-wake detector added in #2559 suspends idle sleeping on perfectly healthy hosts. The visible symptom is this Warning firing periodically on stable machines:

> This host returned a 2ms idle wait at least 8ms late 2 time(s) in the last second; idle sleeping suspended for 5000ms

Demoting it to Debug would hide the symptom but not the cost: every one of those lines means the shard dropped idle sleeping for 5s and burned a full core for no reason. The detector is what was mis-tuned.

## Cause 1 — lateness was a count, not a rate

An idle loop performs **~400–500 sleeps per second** (2ms each, bounded by the 8ms wheel tick). The trip condition was `late > 1` across two consecutive one-second samples — a **0.4% tail-outlier rate**. A co-tenant burst, a page fault, or another process changing the system timer resolution clears that bar on a healthy host.

A host that genuinely cannot schedule the process — throttled burstable vCPU — returns *most* of its waits late. Signal and noise were two orders of magnitude apart, and the check sat in the noise.

Now gated on the proportion, with the absolute count kept as a floor:

```csharp
if (late <= _lateWakeThreshold)             { _consecutiveBadSamples = 0; return; }  // floor
if (late * 100 < sleeps * _lateWakePercent) { _consecutiveBadSamples = 0; return; }  // rate
```

New `server.lateWakePercent` (default `10`). The floor is what keeps a window with only a handful of sleeps from tripping on a meaningless percentage; `server.lateWakeThreshold` keeps its existing meaning.

## Cause 2 — GC pauses were charged to the host

`dev-docs/debugging-event-loop.md` already documents that the GC collects preferentially **during idle sleeps** — that is the natural pause point it looks for. So the detector was systematically measuring the GC's chosen pause point and billing it to the host's scheduler. Not an occasional coincidence; a designed-in one.

```csharp
var collections = GC.CollectionCount(1);
NetState.WaitForCompletion(requested);
...
if (elapsed - requested >= Timer.TickRate && GC.CollectionCount(1) == collections)
```

Gen1 (which counts gen2 with it) rather than gen0 — gen0 pauses don't approach the 8ms `TickRate` bar anyway, and gating on them would discard useful samples. The second read short-circuits behind the overshoot test, so the common path costs **one** `GC.CollectionCount` per sleep: an internal counter read, single-digit nanoseconds, ~500/sec.

## Cause 3 — every backoff logged at Warning

Tiered to the escalation that already existed, since a single suspension is recoverable and not something an operator can act on:

| Backoff | Level |
|---|---|
| 1–2 | `Debug` |
| 3–5 | `Warning` (now includes the sleep count and "for the Nth time running") |
| ceiling | `Error`, unchanged |
| recovery | `Information` (new) |

Each backoff doubles the suspension, so every line is already a distinct escalation step — no further rate limiting needed.

## Drive-by

The `BackoffResetAfterCleanMs` reset only ran on the path to a *new* backoff, making it unreachable for a host that recovered for good — such a host never cleared its escalation or re-armed `_loggedBackoffCeiling`. It now runs on every health sample, which is also what makes the new recovery line reachable.

## Testing

Full solution builds clean, 0 warnings. No tests added: the state is private static in `Core` coupled to `_tickCount` with no injection point, and nothing covered it before — adding a seam purely to test it seemed worse than the gap. Happy to add one if reviewers disagree.
2026-08-13 19:58:03 -07:00
Kamron Batman
9b35b39d0d
fix: stop stack merges and splits from laundering PlayerConstructed (#2576)
## Why

`PlayerConstructed` is per-instance provenance, and #2574 put it on every crafted item — including potions, arrows and other stackables. Stack operations were written when no item carried provenance of any kind, so they treated two piles of the same graphic as interchangeable.

**Merging** keeps the receiving stack's value. Dropping bought potions onto a crafted stack made the whole pile count as crafted; the reverse order erased it. Which one happened was decided by drag direction alone.

**Splitting** rebuilds one half in `Mobile.LiftItemDupe`, which copies a fixed list of fields rather than going through `Dupe`/`CopyProperties`. `PlayerConstructed` was not on that list, so dragging part of a pile off stripped the new half. Worth calling out: `[IgnoreDupe]` does **not** govern this path — it only applies to `Dupe()`. Reasoning "the field isn't `[IgnoreDupe]`, so it copies" is wrong here.

## Changes

- `Item.CanStackWith` compares `PlayerConstructed`, so crafted and non-crafted never merge into one indistinguishable pile.
- `Mobile.LiftItemDupe` copies `PlayerConstructed` onto the remainder, so a split cannot produce halves that disagree about what they are.

Refusing to merge is the whole fix. A stack has nowhere to record provenance, so the only coherent behaviour is to keep the two piles apart rather than pick a winner.

## What this deliberately does not do

Paths that genuinely **virtualize** an item — pouring from a `PotionKeg`, for one — rebuild it without the flag, and the result is simply treated as not crafted. That is accepted rather than worked around; the alternative is threading provenance through every count-based container, which buys little. The keg stores a `Held` int rather than a stack, so nothing there depends on merging and nothing breaks.

`CommodityDeed` is unaffected — it holds the real `Commodity` item rather than a count, so the flag rides along.

## Player-visible effect

Crafted potions and arrows will no longer stack with bought or looted ones. That is the intended invariant, and it is the reason the flag can be trusted at all.

## Tests

7 new tests in `Server.Tests`: both merge directions, the matching-provenance case, split copying, and the split/re-merge round trip.

`Server.Tests` **822 passing**, `UOContent.Tests` **701 passing**, build clean with 0 warnings.
2026-08-13 19:18:06 -07:00
Kamron Batman
55ac2c3d98
refactor: Move legacy deserialization into the .Migrations.cs partials (#2575)
Follow-up to #2574, which added a `.Migrations.cs` partial to `BaseWeapon`. Pure relocation — no behaviour change.

## The inconsistency

`BaseArmor` and `BaseClothing` already kept their pre-codegen `Deserialize(reader, version)` in a `.Migrations.cs` partial, but left the `OldSaveFlag` enum and the `GetSaveFlag` helper behind in the main class file — even though every call site is in the partial:

| Class | `Deserialize` | `GetSaveFlag` / `OldSaveFlag` | Call sites outside the partial |
|---|---|---|---|
| `BaseArmor` | already in partial | in main file | 0 of 26 |
| `BaseClothing` | already in partial | in main file | 0 of 12 |
| `BaseWeapon` | in main file | in main file | — |

`BaseWeapon` had all three still inline, with its new `.Migrations.cs` holding only a `MigrateFrom`.

## After

All three follow the same layout: `MigrateFrom` newest to oldest, then the pre-codegen `Deserialize`, then `GetSaveFlag`, then `OldSaveFlag`. That moves ~290 lines of legacy read path out of `BaseWeapon.cs` — the file that needed it most at ~3,900 lines — and leaves the main class files describing only how the type behaves today.

## Reviewing this

The diff is large and almost entirely noise, so it is probably not worth reading line by line. Two checks are stronger:

- **Nothing was lost or altered.** Across each `.cs` / `.Migrations.cs` pair, the multiset of non-blank source lines is identical to `main` except for one added comment (below). The relocation was done mechanically and asserted against that invariant rather than by hand.
- **Nothing about serialization moved with the code.** Running `ModernUOSchemaGenerator` after the move emits no new migration files.

The complete set of intentional additions:

- `using System;` in each of the three partials, for the `[Flags]` attribute (implicit usings are not enabled here).
- `// Version 9 (pre-codegen)` above `BaseWeapon`'s moved `Deserialize`, matching the marker `BaseArmor` and `BaseClothing` already carry. Version 9 is correct because `BaseWeapon.v10.json` is its earliest migration schema, so codegen began at 10.

Everything else is blank-line placement.

## Verification

Full solution builds in Release with 0 errors and 0 warnings; 1516 tests pass (815 `Server.Tests`, 701 `UOContent.Tests`).
2026-08-13 18:43:53 -07:00
Kamron Batman
bd79cb7759
fix: Consolidate PlayerConstructed onto Item, stamped by the craft system (#2574)
Follow-up to #2573. That change made `SmallBOD.EndCombine` require a player-crafted item, but it could only read provenance off `BaseArmor`, `BaseWeapon` and `BaseClothing`, because those are the only three classes that track it — hence the hand-enumerated `armor?.PlayerConstructed ?? clothing?.PlayerConstructed ?? weapon?.PlayerConstructed ?? false`.

The gap is structural rather than cosmetic. `PlayerConstructed` is set inside each base's `OnCraft`, so it can only ever reach types implementing `ICraftable`. Most craftables do not — the tinkering catalogue alone is largely plain `Item` subclasses — so any rule keyed on "was this actually crafted" has nothing to key on for those types.

## What changed

Provenance moves to `Item` and is stamped centrally in `CraftItem`, immediately after the item is constructed and before the `ICraftable` dispatch, covering both the AOS and T2A craft paths. The three `OnCraft` overrides drop their now-redundant assignment and inherit `Item`'s property, so no call site outside them changes — `Resmelt` and `SalvageBag` still read `armor.PlayerConstructed` and still compile unchanged. `SmallBOD`'s three-way null-coalescing chain collapses to `item.PlayerConstructed`.

`OnCraft` is only ever invoked from `CraftItem` (the other three call sites are `base.OnCraft` chaining), so removing those assignments has no other reachable effect.

## Storage cost: none

`Item`'s `SaveFlag` word is written as a fixed-width `int`, not an encoded one, so occupying bit `0x08000000` changes no record lengths. Items that are not player-constructed serialize byte for byte as before, and crafted ones differ by a single bit in a field already being written.

`Item` itself needs no version bump: a bare `SaveFlag` bit is self-describing, so records written before it existed lack it and read `false`.

## Version bumps

The three content classes do need one, since removing a serialized field changes their layout:

| Class | Version | Field removed |
|---|---|---|
| `BaseArmor` | 9 → 10 | 24 (was last, nothing renumbered) |
| `BaseClothing` | 7 → 8 | 7 (fields 8–10 shift down) |
| `BaseWeapon` | 10 → 11 | 26 (fields 27–30 shift down) |

Each gets a `MigrateFrom` for its previous version that assigns the old bool to the inherited property, so existing crafted armour, weapons and clothing keep their provenance across the upgrade. `Item.Deserialize` runs first and reads the absent bit as `false`, then the migration overwrites it — the generated `Deserialize` calls `base.Deserialize` before dispatching, so the ordering holds. `BaseWeapon` had no migrations file and gains one.

The renumbering is not stylistic: the generator requires contiguous field ordering and rejects a hole with `SG3005: Expected field 'Crafter' with order 7 but found 8`.

New schema JSONs (`BaseArmor.v10`, `BaseClothing.v8`, `BaseWeapon.v11`) are generated by `ModernUOSchemaGenerator` and committed alongside.

## One thing worth a second opinion

The new property is a plain auto-property on `Item`, so it does not call `this.MarkDirty()` the way the codegen setters it replaces did. `MarkDirty` is currently a no-op (`// TODO: Add dirty tracking back`) and no property in `Item.cs` calls it, so this matches the file as it stands — but it is worth noting if dirty tracking comes back.

## Verification

Full solution builds in Release with 0 errors and 0 warnings; 1516 tests pass (815 `Server.Tests`, 701 `UOContent.Tests`).
2026-08-13 18:34:59 -07:00
Kamron Batman
5ce0f1e92b
fix: Require BOD combine items to be player-crafted (#2573)
## Problem

`SmallBOD.EndCombine` validates an item's **type**, **material** and **exceptional quality**, but never checks that the item was actually crafted by a player. Any item matching the request is accepted, including one bought straight from an NPC vendor.

https://github.com/modernuo/ModernUO/blob/main/Projects/UOContent/Engines/Bulk%20Orders/SmallBOD.cs#L117-L168

Where a vendor stocks a type a BOD can request, a player can fill the deed by buying the items instead of crafting them, and pocket the reward gold for the difference.

Tailoring is the clearest case. `SmallTailorBOD.CreateRandomFor` guarantees `Material = None` and `RequireExceptional = false` below 70.1 skill, so the rolled deed asks for plain cloth items — and tailor vendors stock several of those directly. A qty-20 Bandana BOD can be filled entirely from vendor stock for a small fraction of the reward gold, with no crafting and no material cost.

The same shape applies anywhere else a vendor-sold type overlaps a requestable BOD type; tailoring is simply where the low-skill deed generator and the vendor inventory overlap most.

## Fix

Add a `PlayerConstructed` check alongside the existing material and quality checks.

```csharp
var playerConstructed = armor?.PlayerConstructed ?? clothing?.PlayerConstructed ??
    weapon?.PlayerConstructed ?? false;

if (!playerConstructed)
{
    from.SendLocalizedMessage(1045169); // The item is not in the request.
}
```

This follows the pattern already used in `Engines/Craft/Core/Resmelt.cs` (L98-L100, L155-L160) to distinguish crafted from store-bought items, and reuses the same null-coalescing chain style as the adjacent `GetMaterial(armor?.Resource ?? clothing?.Resource ?? CraftResource.None)` line directly above it.

`PlayerConstructed` is already set in `OnCraft` and serialized on all three bases (`BaseArmor`, `BaseWeapon`, `BaseClothing`), so the flag survives restarts and no serialization change is needed.

## Open question — the message

There is no dedicated cliloc for "this item must be crafted", so I reused **1045169** (*"The item is not in the request."*). It is arguably accurate — a vendor-bought item genuinely is not what the deed asked for — but it is not precise, and a player who does not know the rule will find it confusing.

I would rather flag this than invent a string. If there is a better cliloc, I am happy to switch it.

## Testing

`dotnet build Projects/UOContent/UOContent.csproj` — **0 errors, 0 warnings**.

Not covered: I have not added an automated test, as I could not find existing coverage for `EndCombine` to extend. Happy to add one if you would like it, with a pointer to the preferred pattern.

## Compatibility note

Any *already-existing* vendor-bought item in a player's possession will now be rejected by a BOD. That is the intended behaviour, but it is a visible change for anyone mid-deed. Worth a line in release notes.
2026-08-12 20:12:54 -07:00
Kamron Batman
1bc83339bb
fix: Fixes guardian lazy check on Treasure Map Chests (#2569)
### Summary

Fixes a crash bug from the lazy check on treasure map chest guardians.
2026-08-10 09:06:43 -07:00
Kamron Batman
c1442aff3e
fix: Stop treasure chest guardian spawn farming via stack splits (#2568)
### Summary

Players reported an exploit: decipher a treasure map, then run a ClassicUO/Razor organizer agent that pulls the gold out of the chest in small amounts. Each pull spawned more monsters, turning one chest into an unbounded farmable spawn generator.

### Root cause

`TreasureMapChest.OnItemLifted` grants a 10% guardian spawn roll per first-time-lifted item, deduplicated by the instance-keyed `_lifted` set. But a partial lift goes through `Mobile.LiftItemDupe`, which re-adds the stack remainder to the chest as a **brand-new item instance** (engine-side `AddItem`, bypassing the `CheckHold` block on refilling). Every subsequent pull lifts an instance the `_lifted` set has never seen, so each one re-rolls the 10% spawn chance:

- A level 4 chest holds 4,000 gold → pulled coin by coin, ~400 spawned creatures (plus more from reagent stacks), hands-free, per chest.
- Spawns use `guardian: false`, so nothing tracks or caps them.
- Legit full-stack looting yields roughly 5–8 bonus spawns per chest for comparison.

The code is inherited from RunUO, so descendant shards likely share the hole.

### Fix

Mark every item that enters the chest **after the initial fill** as already lifted, via an `OnItemAdded` override gated by a non-serialized `_filled` flag (set at the end of the constructor and in `[AfterDeserialization]`). Ordering makes this exact: `LiftItemDupe` re-adds the remainder *before* the chest's `OnItemLifted` runs, so the lifted original still gets its one legitimate roll while the remainder is pre-marked.

This also covers packing items *into* the chest (e.g., merging gold back in to lift it out again) and bounce-backs — anything not part of the original loot can never grant a spawn roll.

### Tests

- `PartialLift_MarksSplitRemainderAsLifted` — drives the real `Mobile.Lift` path with a 1-coin pull and asserts the split remainder is marked (failed before the fix).
- `ItemAddedAfterFill_IsMarkedLifted` — post-fill additions are marked (failed before the fix).
- `OriginalFillLoot_IsNotMarkedLifted` — original loot keeps spawn-roll eligibility.

Full `UOContent.Tests` suite: 701 passed.
2026-08-10 09:01:29 -07:00
Kamron Batman
0628902644
fix: harden idle-sleep scheduling against bad config and misattributed saves (#2567)
Follow-ups to #2559, from a review of the ported idle-sleep/scheduler-health changes.

### Fixes

- **`NetState.IsIdle` omitted `_pendingDisconnects`** — `Slice()` drains five queues; the property checked four. The other deferred work (`_connectingQueue`, alive checks, movement throttle) is time-gated and correctly excluded; the disconnect queue was the only ready-work omission. Impact was bounded (≤ one idle wait of delay), but the property's contract is "sleeping cannot strand pending work".
- **Neither new setting was clamped** (`Main.cs`):
  - `server.lateWakeThreshold: -1` made `late <= threshold` false for every sample even at zero late wakes, so from the second sample on, sleeping was re-suspended every second, forever — a permanent full-core spin whose only trace was a nonsense warning ("… at least 8ms late 0 time(s)").
  - `server.eventLoopIdleWaitMs: -1` disabled sleeping while the admin gump reported **Healthy** (it tested `== 0`).
  - Both now clamp to `>= 0` and log a warning naming the configured value. `-1` is a natural thing to reach for given the sibling key's doc says "set very high to disable".
- **World snapshots were misattributed to `StolenMs`** — `World.Snapshot` ran outside all five profiler phases, so a 3-second save inside a sample read as ~75% stolen, and `debugging-event-loop.md` teaches stolen = "the host ran something else". The diagnostic pointed operators at buying dedicated CPU for their own largest loop-thread stall. Saves now land in a new `WorldSnapshot` phase; `[LoopStats` iterates `PhaseCount` generically, so the report and CSV pick it up with no changes.
- **Admin gump conflated host-forced spin with configured spin** — when the startup probe finds no high-resolution wait support it zeroes the idle wait, after which the gump said "Spinning (configured)" and the operator's config said 2. New `Core.IdleSleepUnsupported` property; the gump now shows "Spinning - host cannot honor short waits" as a distinct fourth verdict. A genuinely configured 0 still reads "configured" (the probe only runs when the configured value was > 0).
- **The backoff-ceiling `Error` logged once per process lifetime** — `_loggedBackoffCeiling` never reset, and at the ceiling the method returns before the `Warning`, so a host that recovered (>60s clean streak) and later degraded back to the ceiling never re-logged the one operator-actionable message. The flag now resets with the clean-streak escalation reset.
- **Removed the unreachable "already suspended, extend" branch** — no sleeps occur while suspended, so `_lateWakes` stays 0 and every suspended sample early-returns before reaching it; with the threshold clamped it can never fire. If sleep gating ever changes, the normal path handles the case by counting a fresh episode.

`dev-docs/debugging-event-loop.md` updated to match (phase list + gump verdict table).

### Verification

- `dotnet build` clean (0 warnings) both normally and with `-p:EventLoopProfiling=true` (the snapshot phase only becomes live IL under the profiling flag).
2026-08-09 22:05:18 -07:00
Kamron Batman
6d846b11e5
perf: Sleep the event loop when idle. Fixes networking micro-stalls. Adds event loop instrumentation. (#2559)
## Problem

`RunEventLoop` span through its body regardless of whether there was anything to do — ~10% of a desktop core for an empty shard, and ~70% of a core on a 3 vCPU VPS. A process that never idles is exactly what burstable vCPU plans throttle, which is how this surfaced: lag spikes that went away when the operator bought more cores. The spin also denied the GC its natural pause points, so memory climbed until a world save forced a collection — alarming in task manager, harmless in practice, and a recurring source of "is my server leaking?" reports.

## Result

Windows desktop, real world of **190,728 items / 33,158 mobiles**, no players, saves and prebake off, three consecutive runs:

| | Legacy spin | Idle sleeping |
|---|---|---|
| **CPU** | 10.42 – 10.50% of one core | **0.78 – 1.00%** |
| **Tick lag** (peak/15s) | 4–10 ms | 5–11 ms |

**~10× less CPU with tick lag unchanged** — the CPU came free rather than being traded for latency. Slower hosts gain proportionally more. Spin mode (`server.eventLoopIdleWaitMs=0`) independently gained **7× the iterations per core** (1.19M → 8.3M cycles/sec) from the ring's AcceptEx rework.

## How

The loop blocks in `NetState.WaitForCompletion` whenever every queue it drains is empty (all the drains are bounded, so leftovers keep it awake). Receive completions, new connections, and cross-thread `LoopContext.Post` (via the ring's sticky `Wake()`) are all in the wait set, so sleeping adds no latency to any of them. Only timer-driven logic sees wheel lag, bounded by the idle wait.

**Health is measured at the only place sleeping can cause harm.** A sleep is bounded by the time to the next wheel turn, so a correctly honoured sleep can never miss a deadline — the only failure mode is the host returning the wait late. That overshoot is measured on every sleep (one extra timestamp read; production's entire accounting cost), and an escalating backoff suspends sleeping when it persists. By construction, server work — saves, heavy staff commands, deep timer callbacks — cannot trip it, so the warning means exactly one thing: *the host is not scheduling the process promptly*, with two known remedies (dedicated CPU, or `=0`). Hosts with no high-resolution wait mechanism at all are detected once at startup and spin instead.

**CPS is removed.** `Core.CyclesPerSecond`/`AverageCPS` measured nothing actionable before and became actively misleading once the loop sleeps (the rate is set by the sleep, not by shard health). The admin gump's Performance page now shows the verdict instead: `Healthy` / `Sleep suspended (host)` / `Spinning (configured)`.

## Configuration

| Setting | Default | Meaning |
|---|---|---|
| `server.eventLoopIdleWaitMs` | `2` | Longest idle block. Measured across 1/2/4/8 ms, 2 is where the trade stops being free. `0` = never sleep: ~98% of a core, zero scheduling overhead — for large shards on dedicated CPU. |
| `server.lateWakeThreshold` | `1` | Idle waits the host may return a full tick late, per second, before sleeping backs off. Raise for jittery hosts; very high disables the backoff. |

## Diagnostics (compiled out by default)

`dotnet build -p:EventLoopProfiling=true` compiles in `EventLoopProfiler` — every hook is `[Conditional("EVENT_LOOP_PROFILING")]`, so normal builds contain zero profiling IL. The profiling build decomposes each second of wall time into **work (per loop phase) / sleep / GC pause / stolen residual**, keeps ~15 minutes of history in a ring buffer, and the `[LoopStats` command prints the last minute and dumps the full history to CSV. `dev-docs/debugging-event-loop.md` is the diagnosis guide (for humans and AI): what production already tells you, when to flip the profiling build, the signature table for host-steal vs deep-processing vs GC vs wake bugs, why dotnet-trace comes last, and the GC/RAM "leak" misconception.

## Verification

- 815 Server.Tests green; both build configurations compile.
- Docker echo harness green on epoll and io_uring (ping-pong mode); kqueue verified manually on an M1 Max.
- A/B measurements and per-change numbers: `measure/event-loop` branch.

## Notes

The full measurement harness and vendored ring sources used to develop this live on the [`measure/event-loop`](https://github.com/modernuo/ModernUO/tree/measure/event-loop) branch, kept for future loop work.
2026-08-09 13:24:59 -07:00
Kamron Batman
a7e65aab01
perf(login): run password hashing on a parked worker thread (#2566)
## Why

An Argon2 verify is **~8.9 ms of frozen world per login attempt** — more than half a 16 ms frame. Failed attempts cost exactly the same as successful ones, by design, so a credential-stuffing flood is a full-cost stall per packet without needing valid credentials. `SetPassword` derives a hash too, so `[password`, the admin gump and account creation each pay the same.

## What the measurement says

Off-loading does not delete the cost, it relocates it. Three things stay on the loop:

| Component | Measured |
|---|---:|
| Inline verify (today) | **8.92 ms** |
| Dispatch to the worker | 210 ns |
| Drain the continuation off `LoopContext` | 13 ns |
| Loop's own work slowed by shared-L3 eviction | **0.05 – 5.44 ms** |

Net gain **3.5 – 8.9 ms** of on-loop time per login. Harness in `ModernUO-Benchmarks` (`Benchmarks/Argon2OffLoop/`): it models the loop as a dependent-load pointer chase swept across working-set sizes, which is an upper bound on cache-latency sensitivity, and copies `EventLoopContext` so the hand-off cost is the real one.

Two results shaped the design:

- **The contention tax peaks in the middle of the working-set range**, not at the top — 5.44 ms at 8 MiB (a quarter of this chip's L3), but 0.76 ms at 30 MiB and 0.10 ms at 256 KiB. A tiny hot set has nothing in L3 to lose; a huge one is already DRAM-bound.
- **Per-login tax falls as concurrency rises** (5.44 → 2.56 → 1.60 ms at 1/2/4 hashers) while *total* loop damage rises. Contention is shared, not additive, so a login rush is not the disaster case — a single login is.

## Why exactly one worker

It is load-bearing three times over, which is also why it must not quietly become a pool:

- **Cost bound.** Off-loop loses to inline only if a hash steals ~82% of the loop's throughput. One hasher contending for one core leaves the loop ~50%. **A single background hasher cannot cost the loop more than the inline verify under any scheduling regime**, which is what lets the measurement hold on hardware we cannot inspect — AMD, VPS, oversubscribed VM. Four hashers drop the loop to ~20% and break it.
- **Memory.** Exactly one hashing arena is live at a time whatever the login volume.
- **Ordering.** Writes apply in dispatch order *only* because a single thread drains FIFO. A second worker would need ordering reintroduced; `WritesApplyInDispatchOrder` fails if that happens.

Throughput is ~110 verifies/sec. Only loop time matters, not login latency, so head-of-line blocking during a rush costs nothing.

## Making every protection safe off-thread

The worker was initially Argon2-only. That was the right call for the wrong reason — it was blamed on Argon2's salt RNG, which is a stateless syscall wrapper and was never a problem. The real blockers were elsewhere, and both are fixed at the source:

| Protection | Was | Now |
|---|---|---|
| MD5/SHA1/SHA2 | shared `HashAlgorithm.ComputeHash`, which carries the running digest across `HashCore`/`HashFinal` through process-wide singletons | static `HashData` into a `stackalloc` span — no state, no allocation, identical bytes |
| PBKDF2 | `Utility.RandomMinMax` → shared `System.Random`, thread-unsafe *and* game state | `RandomNumberGenerator.GetInt32`, matching the salt beside it |
| Argon2 | already safe (`Verify` is static + stackalloc) | unchanged, singleton reused |

Literal digests are pinned in a test **before** the change and still pass after it. These are compared as strings against every account database, so any casing or encoding drift would lock out every SHA and MD5 account at once.

With all three safe, the worker no longer knows which algorithm it runs and the dispatch conditions collapse to "is off-loop available".

## Correctness

- **Phrase derivation** moves to `AccountSecurity.DerivePhrase`, so verification (stored algorithm's rule) and rehash (target algorithm's rule) cannot disagree. Deriving with the wrong one is the shape of the lockout fixed in #2562.
- **Liveness** is checked at dequeue *and* at apply — a connection can drop while queued or while the result sits in the loop queue. A job with no connection attached, such as an admin password change, runs regardless.
- **Queue overflow rejects** a login rather than verifying inline; steering work back onto the loop is what a flood wants. A password change instead falls back to hashing inline, because unlike a login it must not be dropped.
- **Shutdown and crash** both just stop the thread, and pending jobs are dropped. No save is initiated once shutdown begins — saving is the operator's choice up front, via the admin gump's save/no-save variants, and `WaitForWriteCompletion` honours one already in flight — so a write applied during teardown would reach no disk. The crash path needs its own subscription because `HandleClosed` skips `InvokeShutdown` when crashed.

## Bounding

`MaxPending` is 4096 — a backstop, not a flood defense. `SentFirstPacket` holds a connection to one pending verify and the engine caps connections at 4096, so the queue is already bounded by construction and this can only trip if that invariant breaks. A cap low enough to blunt an attack would reject real players first; during a mass reconnect they *are* the queue. Flood defense belongs at the connection layer.

The real DoS improvement is elsewhere: today every attempt stalls the world, and after this a flood occupies one core while the loop keeps ticking.

## Gate

Release builds on 4+ cores. Below that there is no spare core to move work to, so off-loading buys nothing by construction; `DEBUG` is excluded because dev boxes and test shards have few logins. Both modes call the same code — the gate only chooses where it runs.

## Engine change

One property, `AccountLoginEventArgs.Deferred`, so a subscriber can say "no verdict yet". `EventSink.AccountLogin` is `Action<...>` with no continuation, and the packet handler replies in the same call. Approved separately since it touches `Projects/Server/`.

## Docs

`dev-docs/threading-model.md` and the threading skill gain a vetted-workers section. The forbidden-patterns table bans `new Thread`, `ConcurrentQueue<T>`, `Interlocked` and `volatile` in `UOContent`, and its exceptions covered only `Projects/Server/` — the existing Advanced Search fan-out already sat outside it. The new section leads with proving the need (measure on-loop time, not wall-clock; gate on core count; record the measurement), keeps game logic on the loop via chunking, and documents the hand-off protocol in both directions.

## Testing

698 UOContent tests, 810 Server tests, Release build clean.

Covered: verify and rehash outcomes, phrase rules for SHA1/SHA2 vs Argon2, stored-format stability for MD5/SHA1/SHA2, jobs with no connection attached, and dispatch ordering through the real queue. The liveness and ordering guards are mutation-verified.
2026-08-09 00:13:34 -07:00
Kamron Batman
cce035f1c3
fix: Removes unnecessary dictionary removal guards (#2565)
## What

`Dictionary<K,V>.Remove` and `HashSet<T>.Remove` do not bump the collection's version, so removing an entry during a `foreach` does not invalidate the enumerator. A number of loops were still paying for a `PooledRefQueue`/`PooledRefList` to collect keys and drain them in a second pass. This drops those guards.

## Why it's safe

Verified against .NET 10.0.10 rather than taken on trust, since the documented guarantee covers only `Dictionary<TKey,TValue>.Remove` while several of these call sites are `HashSet<T>` or enumerate `.Keys`/`.Values`:

| Case | Result |
|---|---|
| `Dictionary` foreach + `Remove` | safe, all entries visited |
| `Dictionary.Keys` / `.Values` foreach + `Remove` | safe, all entries visited |
| `HashSet` foreach + `Remove` | safe, all entries visited |
| `Dictionary` foreach + `Remove` **then `Add`** | throws `InvalidOperationException` |

Reflection on `_version` confirms the mechanism: neither `Dictionary.Remove` nor `HashSet.Remove` touches it. Because `Remove` never bumps the version, the `Keys` and `Values` enumerators are just as safe as the dictionary's own, even though only `Dictionary.Remove` documents the behaviour. No entries were skipped in any case.

The `HashSet` half is confirmed by [stephentoub on dotnet/dotnet-api-docs#8177](https://github.com/dotnet/dotnet-api-docs/issues/8177#issuecomment-1167251052): *"Both HashSet and Dictionary have been improved to support removal during enumeration. The docs may just benefit from updating."* The gap is in the documentation, not the runtime.

`Remove` followed by `Add` in the same enumeration still throws. That is the line this PR does not cross.

## Guards removed

`VisibilityList`, `ChampionTitleSystem`, `Channel`, `BombingRun`, `Ruleset`, `PuzzleChest`, `RaceChangeGump`, `StepCache`, `PlayerMurderSystem`, `VirtueSystem`, `ProjectedItem`, `StaminaSystem`, `AIGroupMovement`, `PromotedGuard`, `AutoDenylist`, `LoginAllowlist`, `AntiMacroSystem`, `DetectHidden`.

Both collection kinds are covered: `Dictionary` (including loops over `.Keys` and `.Values`) and `HashSet` (`ProjectedItem._active`, `PlayerMurderSystem._contextTerms`, `StaminaSystem._resetHash`). In `StaminaSystem.ResetTimer` the `Count == queue.Count → Clear()` branch goes away with the queue — it only existed to avoid paying for N individual removes.

Where the collection supports it, `Contains` + `Remove` and `TryGetValue` + `Remove` also collapse into a single lookup (`if (list.Remove(x))`, `if (m_Pending.Remove(ns, out var state))`).

`Utility.Tidy<K,V>` keeps its two branches: when `K` is serializable the value is not inspected, otherwise the value is. Only the serializable side may be cast, so `Dictionary<Mobile, int>` and `Dictionary<Mobile, string>` stay valid.

## Deliberately unchanged

**`BaseCreature.LoyaltyTimer.OnTick`** keeps its deferred-delete queue. Removing from `World.Mobiles` while enumerating it is safe, but `Mobile.Delete()` is not a `Remove` — it runs `OnDelete`/`OnAfterDelete`, the `OnParentDeleted` cascade over the creature's pack, `DropHolding()`, and region and guild callbacks. Anything in that surface that constructs a `Mobile` is an `Add` into the dictionary being enumerated, which does invalidate it. `BaseHire.PayTimer.OnTick` has the same shape and is likewise untouched.

**Spatial-query buffers** — `GuardedRegion.CallGuards`, `Thunderstorm`, `Exorcism`, `LeverPuzzleController`, `BaseCreature.TeleportPets` — are a different hazard. They buffer the result of a range query because the drain moves or harms mobiles, which mutates sectors mid-enumeration.

**Re-entrant drains.** The `_users` sets in `Firebomb` and the explosion, conflagration and confusion-blast potions look like this pattern but are not: the loop collects, `Clear()`s, and only then runs `Target.Cancel` on each, which can re-enter. `AnimalTrainer` enumerates `pm.Stabled` and drains through `RemoveStabled`, which nulls the `Stabled` field once it empties — safe for an in-flight enumerator, which holds the set reference rather than the field, but subtle enough not to be worth inlining on a cold path.

## Verification

`dotnet build` clean with 0 warnings; 810 Server and 684 UOContent tests pass.
2026-08-08 11:50:01 -07:00
Kamron Batman
f33bcd6006
fix: Bind the login auth id to its account and drop the redundant verify (#2564)
## What

- Bind the login auth id to the account **and** origin address that earned it, make it a CSPRNG draw, expire it after two minutes, and spend it only once its owner presents it.
- Skip the password verify on `GameLogin` (0x91) when the presented id vouches for the submitted username and address.

## Why

A full client login hashes the password twice — `AccountLogin` (0x80) and then `GameLogin` (0x91). At the current Argon2 parameters that is **most of a 16 ms frame each, on the single-threaded game loop**, for every login attempt.

The second verify is redundant. `GameLogin` already requires an id from `_authIDWindow`, and that window is only populated by `GenerateAuthID`, called from `PlayServer` — reachable only after 0x80 has already authenticated the account **in this same process**. ModernUO Gateway has its own auth-id passing mechanism and is out of scope here.

## Why the id needed hardening first

Skipping the verify promotes the id from a correlation token to a bearer token, and it was not one:

- drawn from `Utility.Random` → `BuiltInRng`, a non-cryptographic PRNG
- bound to nothing — `AuthIDPersistence` carried only `Age` and `Version`
- never expiring; `Age` was only read to pick an eviction victim

A guessed id got you nothing while the password was still checked. Without that check it would have been an account takeover, so the id is now a CSPRNG draw, single-use, two-minute TTL, and bound to both the account and the origin address.

What remains is observing a live id on the client's network or machine — which the server cannot defend against under any design, and which already yields the password itself, since the client transmits it in the same handshake.

Network switching mid-login is deliberately unsupported.

## Behaviour

A full verify was always required before this change, and ids never expired, so every "before" is a password check.

| Case | Before | After |
|---|---|---|
| Id absent | Disconnect | Disconnect |
| Address mismatch | Verify | **Disconnect** |
| Account mismatch | Verify | **Disconnect** |
| Expired | Verify | **Verify** |
| Id vouches | Verify | **Skip** |

No case grants access the previous code would have denied. Expiry deliberately falls back to the verify rather than disconnecting — a player can idle, and turning that into a lockout would be a regression for no gain.

## Look, then take

An id is not consumed until the presenter has shown it is theirs. Removing it first would let anyone who lands on a live id burn it, and its owner would arrive to `"Unable to find auth id."` and have to log in again over a packet they had no part in.

The **address is compared before the account**, so a guesser from anywhere else is rejected before a username is ever looked at. That is what makes it safe to leave the id in place on a mismatch: there is no username-enumeration risk to trade against, and the only presenter who could enumerate is already on the victim's own address.

## The window is not a cap

It was 128 entries with the oldest evicted to make room. That is a cap on *concurrent logins*, not a resource bound: 800 people picking a server at once would have live ids discarded and those clients would arrive to `"Unable to find auth id."` — a failed login caused by nothing except other people logging in.

Issuing now sweeps expired entries and lets the window grow if everything in it is still live. Unbounded is safe here: an entry costs a **successful** password verify to create and dies after two minutes, so its size tracks logins genuinely in flight.

Removing an id when its connection drops is not an option, and this was checked rather than assumed — `NetState.cs:787` disconnects the login connection *deliberately*, immediately after the id is issued, and that disconnect is never cancelled. Surviving it is the whole purpose of the id. Expiry is the only correct reclamation.

## Handshake hardening

Choosing a server queues a disconnect, but the queue drains on the *next* slice, so a client pipelining into the same recv buffer can reach the handshake handlers again. Two had no do-once guard:

- `LoginServerSeed` (0xEF) now rejects when `state.Seeded` is already set.
- `PlayServer` (0xA0) now rejects when `state.AuthId != 0` — otherwise a connection that had already spent its id would be handed the spent one back.

Issuing is also idempotent (`EnsureAuthId`), so a connection holds exactly one id by construction and an orphan is impossible rather than something to clean up. The login state machine itself is untouched.

Also fixes a fall-through: the "Unable to find auth id" branch disconnected without returning, then continued with a default entry and nulled `state.Version`.

## Testing

`ConsumeAuthId` is a seam with no `NetState` dependency, so the auth decision is tested directly: vouching, account mismatch, address mismatch, case-insensitive usernames, IPv4-mapped-IPv6, unknown ids, single-use by the owner, **a rejected attempt leaving the id redeemable**, expiry-into-verify, and an 800-id login rush that must evict nobody. Expiry is driven by moving `Core._now`, not by waiting. Every new clause was verified to discriminate by removing it and confirming only its own tests fail.

## Cost

Halves the per-login game-loop cost. This does not make hashing cheaper or move it off the loop — that is gated on a measurement described in `docs/handoffs/2026-08-07-off-loop-argon2-hashing.md`.
2026-08-08 09:25:42 -07:00
Guflly
64e6fe5da8
fix: Warn when sending empty gumps (#2563)
### Summary

Generates a console warning when users receive an empty gump. This will help prevent client side leaks.
2026-08-08 00:55:12 -07:00
Kamron Batman
b2c59191bd
fix: Fixes Argon2 verify correctness and the password upgrade lockout (#2562)
> ⚠️ **Rollback hazard — one-way door once logins are taken.** Serialization is unchanged, so a save
> written by this build still *loads* on the previous one. Its contents do not survive the trip: on
> its first successful login each account is rehashed to `$argon2id$`, and the previous build ships
> Argon2.Bindings 1.19.0, whose `Verify` is gated by the verifier's own configured type and answers
> `false` for an `$argon2id$` hash. **After a shard running this build has accepted logins, do not
> roll back past this commit** — every account that logged in is locked out on the older binary, and
> the only recovery is rolling forward again or resetting passwords by hand. Roll back only from a
> save taken before the first post-deploy login.

Requires [Argon2.Bindings 1.20.0](https://github.com/modernuo/Argon2.Bindings/pull/14), now published.

## What

- Consume `Argon2.Bindings` 1.20.0, which resolves the Argon2 type from the stored PHC string rather than from the verifier's own configuration.
- Default to **Argon2id, m=16384, t=1, p=1** — 8.51 ms against the old Argon2i 8 MiB t=3 at 10.11 ms. Cheaper *and* stronger.
- Rehash on a successful login whenever the stored parameters are stale, not only when the algorithm changes.
- Fix `SetPassword`, which derived the password phrase from the outgoing algorithm while storing it under the incoming one.

## Why

**Verification was gated by the verifier's configured type.** `Verify` passed the instance's own `ArgonType` to native `argon2_verify`, whose `decode_string` rejects a disagreeing `$argon2i$`/`$argon2id$` prefix and returns `DECODING_FAIL` — folded into `false`, the same answer as a wrong password. Switching the default type would have locked out every existing account, and `VerifyAndUpdate` could not have migrated them either: it delegates to the same type-fixed `Verify` and never compared `ArgonType`. Fixed upstream in 1.20.0. The pinned legacy-`$argon2i$` test here fails on 1.19.0 for exactly that reason, which is what makes the package bump load-bearing rather than incidental.

**Changing the defaults would otherwise have reached nobody.** Argon2's PHC string embeds `m`, `t` and `p`, so verification uses the parameters stored with each account, not the configured ones — and verification is the hot path. `CheckPassword` only rehashed when the *algorithm* changed, never when its cost parameters did, so on an established shard the new defaults would have applied to new accounts only. `IPasswordProtection.NeedsRehash` closes that: it defaults to `false`, so PBKDF2 and the `HashAlgorithm` protections are untouched — only Argon2 carries its cost inside the stored value.

**`SetPassword` picked the phrase rule from the wrong algorithm.** SHA1 and SHA2 salt the phrase with the username; Argon2 and PBKDF2 do not. It chose the rule from the *outgoing* algorithm while storing under the *incoming* one, so any algorithm change wrote a credential its own next verify could not reproduce. It now assigns `PasswordAlgorithm` first and derives the phrase from that. Note this ordering is load-bearing and invisible — `UpgradingAlgorithm_DoesNotLockTheAccountOut` is what pins it.

## Cost

Verification is re-derivation, so these are login numbers. A full login calls `CheckPassword` twice — `AccountLogin` (0x80) then `GameLogin` (0x91): **~20 ms before, ~17 ms after**, plus a one-time ~8.5 ms rehash on each account's migrating login.

That cost is still paid on the game loop. Moving hashing off-loop is deliberately **not** in this PR — it needs a pending-auth state in the login handlers, bounding of in-flight hashes, and login rate limiting.
2026-08-08 00:24:59 -07:00
Kamron Batman
23dc6649a0
fix: Require only runtime packages on Linux, and check ICU and tzdata the way the runtime does (#2561)
## Why

ModernUO mandated `-dev` packages on production servers for exactly one reason: `DllImport` never
asks for a versioned SONAME, so `libdeflate.so.0` and `libargon2.so.1` sitting in `/usr/lib` went
unfound, and the `-dev` package's unversioned symlink was the only thing making resolution work.
The `-dev` packages ship no library of their own — operators were installing headers and a static
lib on machines that compile nothing.

Fixed in the binding packages (modernuo/LibDeflate.Bindings#4, modernuo/Argon2.Bindings#13), so
this picks them up and stops asking.

```
LibDeflate.Bindings 1.0.3  -> 1.0.4
Argon2.Bindings     1.17.0 -> 1.19.0
```

## zstd is dropped too, on every platform

ZstdNet bundles `libzstd` for `linux-x64`, `linux-arm64`, `osx-x64`, `osx-arm64` and win, and
nothing shells out to the CLI. Verified: the 15 `ManagedArchive` round-trip tests pass in a
container with no `zstd` package installed and `which zstd` empty. Removed from the README, the
macOS `brew install`, and CI — so the macOS runners now prove it rather than us assuming it.

## NativeLibraryChecker asks a different question

It asked *"is package X installed"* via `dpkg -l` / `rpm -q`. That is what forced `-dev`, and no
hardcoded name works for ICU anyway — its apt package is release-specific (`libicu70` on Ubuntu
22.04, `libicu76` on Debian 13). It now asks *"will the loader find this"*: `NativeLibrary.TryLoad`
on the unversioned name, then `libfoo.so.N` descending through the range the runtime accepts.

It deliberately does not consult a package database or `ldconfig -p`. Both answer a different
question than "will `dlopen` succeed" — see the ICU section below for how that bit.

## What was wrong with the ICU check

`libicuuc` was **inherited, not derived**. It came from translating the old package-name check into
a library probe, without establishing which library that should be. Reviewing it turned up three
defects, all of which could report ICU present on a host where the runtime then refuses to start:

- **`libicui18n` was never probed.** The only ICU names in `libSystem.Globalization.Native.so` are
  `libicuuc` and `libicui18n`. `libicudata` arrives as a dependency of `libicuuc`, and
  `libicuio`/`libicutu`/`libicutest` are never referenced — so that is the complete list, and both
  are checked now.
- **No version floor.** The runtime's `MinICUVersion` is 60, but the probe accepted down to
  `.so.0`. RHEL/CentOS 7 ships ICU 50, which passed and then aborted at startup.
- **The `ldconfig` fast path bypassed the range.** A cache line for `libicuuc.so.50` still matches a
  `libicuuc.so` prefix test, so the floor was unenforceable through it. It also trusts a stale
  cache — observed reporting a deleted `libdeflate` as present. Removed in favour of asking the
  loader directly, which reads the same cache but answers the real question, and which also deletes
  the musl special-case (`ldconfig -p` exits 0 on musl while producing nothing usable).

Worth knowing when this goes wrong in the field: **missing ICU does not throw, it `FailFast`s** —
SIGABRT, exit 134, uncatchable. The process starts cleanly and dies later at whatever line first
touches a culture, so the stack rarely implicates ICU.

## tzdata is a separate prerequisite, and nothing was checking it

The event scheduler resolves configured zone IDs through `TimeZoneInfo`, which reads
`/usr/share/zoneinfo`. It is data rather than a library, so no loader probe finds it, and slim
container images routinely omit it. Without it every lookup except `UTC` throws
`TimeZoneNotFoundException` and `GetSystemTimeZones()` returns 1 entry instead of ~419.

There is no per-zone packaging to opt into — it is ~2 MB for the whole set. The one split that does
exist is a trap rather than an optimization: Debian 12 and Ubuntu 24.04 move the legacy aliases into
`tzdata-legacy`, so plain `tzdata` has `America/New_York` and `EST5EDT` but is **missing
`US/Eastern` and `Asia/Calcutta`**. A shard configured with a legacy alias throws even though tzdata
is installed. Documented, with both fixes.

## Why `InvariantGlobalization` stays false

Dropping ICU entirely by turning on invariant mode looks tempting and is not safe. Because
`Directory.Build.props` also sets `PredefinedCulturesOnly=false`, invariant mode does **not** throw
`CultureNotFoundException` — it silently hands back invariant data. Measured on .NET 10:

| Behaviour | With ICU | Invariant mode |
|---|---|---|
| `new CultureInfo("de-DE")` | real culture | succeeds, returns invariant data |
| de-DE decimal separator | `,` | `.` |
| `1234.5` as de-DE | `1.234,5` | `1,234.5` |
| `string.Compare("a", "B", InvariantCulture)` | `-1` (linguistic) | `31` (ordinal) |
| sort `[b, A, a, B]` | `a, A, b, B` | `A, B, a, b` |
| `FindSystemTimeZoneById("Eastern Standard Time")` on Linux | resolves | `TimeZoneNotFoundException` |
| UTF-8 round-trip of non-ASCII | unaffected | unaffected |

Number parsing and formatting produce wrong values with no error, and culture-sensitive sort order
silently becomes ordinal. Encoding is not the mechanism — UTF-8 round-trips fine either way.

## Documentation

The rationale now lives in `dev-docs/platform-prerequisites.md` rather than in comments, so it is
discoverable without reading the build tool: what each dependency is for, what breaks without it,
per-distro package names, the ICU floor, the `tzdata-legacy` split, and why the check asks the
loader instead of the package manager.

README drops `libicu-dev`. Matching the runtime package by pattern (`'^libicu[0-9]+$'`) is
version-independent without pulling in headers, so **no `-dev` package is required on any supported
distribution** — which was the point of the whole change.

## CI now proves the claim instead of contradicting it

The dnf job already installed runtime packages only. The apt job installed `libicu-dev`, which ships
the unversioned `libicuuc.so` symlink — so every probe succeeded on the first attempt and the
versioned-SONAME fallback this PR depends on was never exercised. Switched to the pattern match,
verified to resolve exactly one package on jammy (70), bookworm (72), noble (74) and trixie (76).

Added an assertion that the unversioned symlinks are absent. Without it the suite silently stops
testing anything the moment a base image starts shipping one. Verified against all eight matrix
distributions — none ship them — and confirmed the step fails as intended when a symlink is planted.

## Audit of every other native entry point

Checked whether anything else has the same hazard. It does not:

| Import | Verdict |
|---|---|
| `ws2_32.dll` — `SocketHelper` | Always present on Windows |
| `libc` — `SocketHelper` | **Verified safe**, see below |
| ZstdNet → `libzstd` | Bundled for every RID |
| IORingGroup | No native library; raw syscalls |
| ICU | Loaded by the .NET runtime itself, which probes versioned suffixes |

`libc` deserved a hard look, because `libc.so` *is* a `libc6-dev` linker script while the real
library is `libc.so.6` — the same shape as the bug being fixed. It is not affected. Measured in a
container with no `libc6-dev`:

```
/usr/lib/x86_64-linux-gnu/libc.so   ABSENT
/lib/x86_64-linux-gnu/libc.so.6     present
TryLoad("libc")     LOADED      <- resolves where "libdeflate" would not
TryLoad("libc.so")  not found
getpid() -> DllImport("libc") WORKS
```

Confirmed on Alpine/musl as well. No code in this repo registers a `DllImportResolver`, and nothing
else P/Invokes.

## `--check-prereqs`

New flag. `Program.cs` only ran the SDK check in non-interactive mode — `NativeLibraryChecker` was
reachable only through the Spectre-driven guided flow, so there was no way to verify a deployment
target from a script or a container. It is what made the container verification below possible, and
it prints the exact ICU package for the running release via `apt-cache`.

It renders through the same `PrerequisiteChecker` the guided menu uses, rather than a second
hand-rolled table that could drift from it. Spectre drops ANSI styling on its own when stdout is not
a terminal, so redirected output stays clean; the console width is widened in that case so the
install hints, which are shell commands meant to be copied, do not gain a newline mid-command.

```
╭───────────────────────────╮
│ Checking native libraries │
╰───────────────────────────╯

  ✔ libicuuc (Found)
  ✔ libicui18n (Found)
   libdeflate (Not found)
   tzdata (Not found — every zone except UTC will throw)

  ⚠️ Install the missing dependencies. The -dev/-devel packages are not required:
   sudo apt-get install -y libicu74 libdeflate0 tzdata
```

Exit code carries the machine-readable half: 0 when everything resolves, 1 when anything is missing.

## Verification

Against 1.0.4 and 1.19.0: build plus **810 Server.Tests and 642 UOContent.Tests**, on Windows and
on Linux with **only** `libdeflate0` and `libargon2-1` installed — with the absence of the
unversioned symlink asserted first so the run could not pass for the wrong reason.

`--check-prereqs` verified in containers on Debian and Alpine across every state that matters: all
present, each dependency removed individually, tzdata removed, a deliberately stale `ldconfig`
cache, and ICU downgraded to `.so.50` to confirm the floor rejects it. Package resolution and the
absence of unversioned symlinks checked on all eight CI distributions.
2026-08-07 15:03:08 -07:00
Kamron Batman
246f077778
chore: drop the liburing prerequisite, which was never used (#2560)
## Why

`IORingGroup` issues io_uring syscalls directly rather than linking `liburing`, so the package has
never been needed — but we ask operators to install it in the README, install it in CI, and check
for it in `build-tool`.

Verified against the **shipped** `IORingGroup` 1.0.9 assembly, not just the source:

| Symbol | Occurrences in `IORingGroup.dll` |
|---|---|
| `libc`, `libSystem.dylib`, `kernel32.dll`, `kernelbase.dll`, `ws2_32.dll` | present |
| `liburing` | **0** |
| `io_uring_queue_init` — liburing's entry point | **0** |
| `io_uring_setup` — the raw syscall | 1 |

If it linked liburing it would call `io_uring_queue_init` / `io_uring_submit`. It calls neither.

## What changes

Nine lines across three files, removing `liburing-dev` / `liburing-devel` from:

- `README.md` — both the dnf and apt prerequisite blocks
- `.github/workflows/build-test.yml` — both install steps
- `Projects/BuildTool/Prerequisites/NativeLibraryChecker.cs` — the cross-compile target text, the
  apt and dnf package lists, and the `ldconfig` fallback map

Nothing else is touched. `zstd` and the `-dev` packages are a separate discussion and a separate PR.

## Risk

None to the build. `liburing` was only ever installed, never linked or loaded — removing it cannot
change resolution behaviour. `build-tool` builds clean.

This was found while investigating why Linux requires `-dev` packages at all; that fix lives in the
binding packages (modernuo/LibDeflate.Bindings#4, modernuo/Argon2.Bindings#13) and lands separately
once those publish. This piece is independent and unblocked, hence its own PR.
2026-08-06 21:32:25 -07:00
Kamron Batman
6d81077772
perf(network): consume IORingGroup 1.0.9 to drop the per-iteration 6 KiB memset (#2558)
## Problem

`IORingGroup`'s `WindowsManagedRIOGroup.DequeueRioCompletions` stackallocs `RIORESULT[256]` (6144 bytes) and runs **once per game-loop iteration** — `NetState.Slice` → `RingSocketManager.ProcessCompletions` → `PeekCompletions` → `DequeueRioCompletions`.

The 1.0.8 package was compiled with the `.locals init` IL flag set, so every one of those calls memset the full 6 KiB before `RIODequeueCompletion` overwrote the entries it actually filled.

An EventPipe profile of a near-idle shard (3 vCPU VPS, world saves off, one player logging in and moving around) put `System.Buffer.ZeroMemoryInternal` — called directly from `DequeueRioCompletions` — at **~2.8% of main-thread samples**, and it was the dominant frame in several 60–127 ms game-loop stalls.

## Why our existing attribute didn't cover it

`Projects/Server/Module.cs` and `Projects/UOContent/Module.cs` already declare `[module: SkipLocalsInit]`. That attribute is a **compile-time** directive: it clears the flag in the IL of the assembly being compiled, and does not cross assembly boundaries. It never applied to the package.

Verified by reading the shipped IL (`MethodBodyBlock.LocalVariablesInitialized`):

| Assembly | attribute | methods with `.locals init` |
|---|---|---|
| `Server.dll` | present | 0 of 5439 |
| `IORingGroup` 1.0.8 | **absent** | **158** |
| `IORingGroup` 1.0.9 | present | **0 of 389** |

## Testing

Built and tested against the locally-built 1.0.9 package (temporary local feed, not committed):

- `dotnet build -c Release` — **0 warnings, 0 errors**
- `Server.Tests` — **810 passed, 0 failed**
- `UOContent.Tests` — **637 passed, 0 failed**
- Confirmed the `IORingGroup.dll` deployed to `Distribution/` is the fixed build (0 of 389 methods zeroing)

Only the `<PackageReference>` version changes; no source changes on this side.
2026-08-04 20:11:37 -07:00
SynPDX
86df62fd3e
fix(housing): register doors, and stop crashing on client component sheets (#2557)
## Summary

Players could not place **any door** while customizing a house, and placing other pieces could disconnect them outright. Staff saw neither problem: `HouseFoundation.Designer_Build` only enforces `ValidPiece` below `GameMaster`.

Original report and diagnosis by @SynPDX.

## Root cause 1 — no door is ever registered

The retail client's `doors.txt` separates its header rows with lines of **bare tabs** (it is the only sheet that does):

```
int<TAB>int<TAB>...<TAB>string
<TAB><TAB><TAB><TAB><TAB><TAB><TAB><TAB><TAB><TAB>      <-- 10 tabs, not an empty line
Category<TAB>Piece1<TAB>...<TAB>FeatureMask<TAB>Comment
<TAB><TAB><TAB><TAB><TAB><TAB><TAB><TAB><TAB><TAB>
0<TAB>1657<TAB>1659<TAB>...
```

`Spreadsheet.ReadLine` skipped a line only when `line.Length > 0`. A 10-tab line has length 10, so it was returned as the **names row** — every column ended up named `""`, `GetColumnID("Piece1")` and friends returned `-1`, and not one of the 230 door graphics was registered. Unregistered item IDs keep the `-1` sentinel, and `CheckValidity` rejects those, so `ValidPiece` refused every door.

ClassicUO skips these lines (`string.IsNullOrWhiteSpace` in `HouseCustomizationManager.ParseFile`), which is why the client happily offers doors the server then rejects.

Measured against a retail 7.0.x `doors.txt` using the shipped `Spreadsheet`:

| | `FeatureMask` column | door graphics registered |
|---|---|---|
| before | `-1` | **0** |
| after | `9` | **230** |

## Root cause 2 — `IndexOutOfRangeException` out of the packet handler

Every sheet ends in a cosmetic `Comment` column that ModernUO never reads, and client sheets write an empty comment as a plain newline with no trailing tab. `Split('\t')` then returns one field fewer than the header declares, and the parser indexed past the end:

```
System.IndexOutOfRangeException: Index was outside the bounds of the array.
   at Server.Multis.Spreadsheet..ctor(String path)
   at Server.Multis.ComponentVerification.LoadSpreadsheet(...)
   at Server.Multis.ComponentVerification.IsItemValid(Int32 itemID)
   at Server.Multis.HouseFoundation.ValidPiece(Int32 itemID, Boolean roof)
   at Server.Multis.HouseFoundation.Designer_Build(NetState state, ...)
```

The client's own parser only requires the columns up to `FeatureMask` — ClassicUO's `CustomHouseMisc.Parse` guards on `scanf.Length >= 12` for a 13-column `misc.txt` — so such a row is valid data listing real pieces. Missing trailing fields are now treated as empty rather than dropping the row, which would unregister every piece the row lists and reproduce the door symptom.

`EnsureLoaded` also set `_loaded` before loading, so once the throw escaped, an all `-1` table stayed cached and rejected everything for players from then on — the same player-visible symptom as #2500.

## Also made explicit rather than accidental

- **Named the table sentinels.** `NotAComponent` (-1) is the anti-cheat guard and the initial state; `NoFeatureRequired` (0) is a piece with no expansion gate — how `walls.txt` encodes pre-AOS base pieces and what `housing.bin` collapses to under `HousingTierMask` (#2500).
- **A sheet with no `FeatureMask` column is refused and logged.** `GetInt32` on a missing column returns 0 = `NoFeatureRequired`, which would have silently marked every piece in that sheet unconditionally placeable regardless of expansion. This was previously only harmless by accident.
- **A sheet matching none of its expected tile columns is refused and logged** — that is what `doors.txt` was doing silently. Individual missing columns stay tolerated, since older sheets predate columns such as `walls.txt`'s `SecondAltWindowS`/`E`.
- **Catch per sheet**, so one unreadable file no longer costs the other six.
- **Header guards**: an empty file or a types-only file raised a `NullReferenceException`; a names row shorter than the types row indexed past the end.
- **Fall back to the component sheets when `housing.bin` cannot be read**, instead of passing `null` into a `SpanReader`.

`_loaded` is still set before loading, deliberately: this runs from the design packet handler, and retrying would re-read every sheet on each subsequent placement attempt.

Sheet precedence is **unchanged** — the client's copies stay authoritative and `Data/Components` remains the fallback.

## Verification

- Retail 7.0.x client `doors.txt` through the shipped `Spreadsheet`: 0 door graphics before, 230 after.
- 5 new tests in `SpreadsheetTests` covering the tab separators, the omitted trailing field, per-row recovery, and both header guards. All 5 fail against `main` and pass here.
- `dotnet build` clean (0 warnings, 0 errors); `UOContent.Tests` 642/642.
2026-08-04 20:05:28 -07:00
Kamron Batman
aae173a797
feat(network): allowlist false-positive IPs, escalate on behavior (#2556)
## Why

The shard owner, on a Starlink CGNAT address, was blocked by the imported reputation blocklist.

The cause was not CrowdSec. The address was a literal line in `ip-blocklist.txt`, so `BlocklistFilter` denied it at accept and then promoted it — and clearing the CrowdSec decision could not fix it either, because the file entry re-reports within `promoteSuppression` of every reconnect attempt.

This is structural, not a one-off. Reputation feeds list shared consumer address space constantly: on CGNAT one public address fronts many subscribers **at the same time**, so a single abusive customer gets the address listed and everyone else behind it is blocked with them. Where leases rotate, a listing says little about whoever holds the address now. Around 1,000 Starlink addresses sit in the current list.

So exemptions go where they cost nothing, and escalation is driven by what a connection actually does.

## Generator — `tools/Export-IpBlocklist.ps1`

`-AllowlistFile` takes multiple paths, subtracted from the merged set before the output is written. Defaults to every `ip-allowlist*.txt` beside the output, merged into one allow set:

- `ip-allowlist.txt` — operator exemptions, created once and **never rewritten**
- `ip-allowlist-<name>.txt` — a carve-out you built, regenerable and copyable between shards

**Subtraction is range-correct.** An allowlisted address inside a blocked CIDR splits that CIDR around the hole rather than being silently ignored. This also fixes `-ExcludeAnonymizers`, which parsed CIDR entries into `$anonCidr` and then only ever subtracted singles.

**No carve-out ships.** A carve-out names a real network, and which ones a shard should exempt depends on where its players actually are — so publishing one would make that policy call for every shard and put a specific provider's address space in the repo. The script builds them on request instead:

```powershell
.\Export-IpBlocklist.ps1 -AddCarveout starlink -Asn 14593
```

Carve-outs are **discovered, not configured**: every `ip-allowlist*.txt` beside the output is subtracted, by the generator and by the shard, so a file an admin adds needs no config edit and no code change. Each carries an `asn=` marker in its header, which is how `-RefreshCarveouts` rebuilds it without the script keeping a list of anyone's networks; a hand-written allowlist has no marker and is never rewritten.

Prefixes come from **announcements, not ownership records**, because registry data disagrees with what is actually routed and silently caps result sets: ARIN whois returns at most 256 rows and gives per-customer /24s, and `206.83.96.0/19` reads as APNIC in RDAP even though `206.83.96/21` is announced by Starlink.

Editing an allowlist bypasses `-MinInterval`, so a just-added exemption isn't indistinguishable from the allowlist not working. A Starlink carve-out, if you build one, costs **~4,300 IPs + ~144 CIDRs of 4.2M (0.10%)**.

## Allowlists

**`FileAllowlist`** reads the same files the generator subtracts, so an operator entry means "leave this address alone" for real. Subtraction alone only covers being *blocked*; behavioural detections never consult the blocklist, so without this a carve-out was quietly routed around — one scanner behind a shared address was enough to get everyone behind it contributed and firewalled, with nothing in the shard's own config explaining why. Reading the files also means an entry applies on the next reload rather than the next regeneration, which is what matters when someone is complaining now.

**`LoginAllowlist`** is earned by authenticating, with a 90-day TTL because an address that logged in years ago is a stranger. Its own store rather than `Account.LoginIPs`, which has no timestamps and cannot be backfilled. An entry is evidence rather than a licence: 10 suppressed contributions in an hour revokes it, and a fresh login forgives the tally.

Both are consulted **only after the blocklist has already matched**, so a normal accept pays nothing for them and the accept gate stays allowlist-free. `BanExemptions` combines them behind `BanChannel.IsExempt` and suppresses escalation only — every local defence still applies.

Two limits, both deliberate and documented in the class: `LoginAllowlist` **cannot bootstrap** (an entry is only earned by getting in, so it never repairs an existing false positive), and it is weakest on rotating CGNAT. That is why `FileAllowlist` is the fix for those, and why it is manual.

## Behavioural detection

| Reason | Trigger |
|---|---|
| `silent-connect` | Reaped after 5s having sent **zero bytes** |
| `invalid-seed` | Opened with a zero seed |
| `foreign-protocol` | Positively identified as HTTP, TLS or SSH |

**`ForeignProtocol` inverts the test.** Asking "is this a good UO client?" cannot work: `LoginEncryption.ClientDecrypt` is a byte-for-byte stream XOR, so a legitimate client with encryption enabled when the shard expects none sends a structurally perfect connection whose payload is noise. "Speaks HTTP" is safe where "unreadable" is not — however misconfigured a UO client is, it never sends `GET / HTTP/1.1`.

Nothing assumes arrival framing. TCP has no message boundaries, so a rule of the form "these bytes must arrive together" is broken by construction and drops real players on poor links. A prefix match with too few bytes to confirm waits for more. A four-byte seed can legitimately spell `GET ` (the address 71.69.84.32) or `0x16 0x03 0x0?` (22.3.x.x), so confirmation requires the request line to continue in printable ASCII or an actual ClientHello inside a plausible record — a real client's fifth byte is a packet id (`0x80`, `0x91`, `0xEF`), none of them printable, so those collisions fall through.

Everything is keyed on **bytes-received rather than elapsed time**. A connection that sent something and ran out of time is far more likely a slow link than an attack, and banning those produces the worst failure mode available: the player retries, trips the rate limiter, and compounds a bad connection into hours of being firewalled off.

## `AutoDenylist`

A short-lived local hold (15m) on behavioural detections, as `IConnectionFilter` + `IBanReporter` over one store so the engine detection sites never reach into content.

This closes the gap where a flood pays for a socket, buffer and `NetState` slot per connection while waiting for the OS bouncer — the verdicts that matter most are reachable only *after* reading bytes — and it is the entire defence on a shard running no bouncer, which is the default config. Not persisted: a holding pen that survives restarts is a ban without a ban's review.

Cost: one dictionary lookup on a usually-empty dict per accept.

## `BanReasons`

Centralises the reason slugs. `IsBehavioral` is an **opt-in** set, not "everything except manual", so a future reason escalates normally instead of silently inheriting an exemption or entering a local denylist.

This caught a real bug during review: the first cut of the exemption swallowed `manual` admin bans (`Commands.cs`, three sites in `AdminGump`) for any allowlisted address.

## Fixes found in review

- **`BanConfiguration.Settings` was null until `Configure()` ran**, while the reap path dereferences it every `Slice()`. A harness driving `NetState.Slice()` directly hit an NRE that presented as flaky because it depended on whether an earlier test had already called `Configure()` — which is why it failed on some CI platforms and not others. Now starts at the record's defaults, with idempotency tracked by a flag; this also removes the same latent NRE from the pre-existing rate-limit path.
- **`-AllowlistFile` was typed `[string]`** while documented and used as a list, so passing two paths would have collapsed them into one string.

## Layout and docs

Content network code moves out of `Misc/` into `UOContent/Network/`, one concern per folder — `AutoDenylist/`, `Blocklist/`, `CrowdSec/`, `Firewall/`, `LoginAllowlist/`, `Packets/`. **Namespaces are untouched**, so these are pure file moves (git tracks all 16 as renames).

`dev-docs/ip-bans-and-allowlists.md` documents the subsystem, leading with the operator process for unblocking a player — including the three things that look sufficient and are not: deleting the CrowdSec decision alone, editing `ip-blocklist.txt` by hand, and `cscli allowlists` alone. `.gitignore` covers the new config files.

## Testing

Build clean. **Server.Tests 810 passed**, **UOContent.Tests 637 passed**, zero warnings. This branch adds 38 tests; the rest of the delta is main's, since this is rebased on current `main`.

New coverage: TTL boundary and renewal, private-address exclusion, manual-ban-never-exempt, unopted-reason-never-exempt, strike revocation, quiet-window reset, login forgiveness, file-allowlist CIDR coverage, file-allowlist not spending the earned list's strikes, denylist expiry-on-read, cap enforcement, lapsed-entry reclaim, HTTP/TLS/SSH identification, seed-collision fall-through, and encrypted-login-is-not-foreign.

Generator verified end-to-end against live feeds: a clean run ships no carve-out, `-AddCarveout starlink -Asn 14593` fetches and collapses 213 prefixes to 115 ranges in 0.1s over 4.2M entries, `-RefreshCarveouts` rediscovers it by its `asn=` marker, a hand-written allowlist is left untouched, and deleting a carve-out drops it rather than having it rewritten. CIDR splitting verified exhaustively: a single-IP hole in a /24 leaves exactly 255 of 256 addresses blocked.

## Operator note

Existing installs are unaffected until the generator next runs, which creates `ip-allowlist.txt` and nothing else. To unblock someone: add the address to that file and delete any live CrowdSec decision — the existing ban outlives the config change. The shard picks the entry up on its next reload, so re-running the generator is optional.

A shard whose players are on CGNAT (satellite, mobile, or an ISP short on IPv4) will likely also want `-AddCarveout`; see `dev-docs/ip-bans-and-allowlists.md`.

## Also included: a latent CI failure this PR surfaced

`fix(tests): serialize test classes that rent through STArrayPool` touches a property-list test file that has nothing to do with this feature. It is here because it was failing macOS CI, and it is trivially cherry-pickable out if you would rather it went to `main` on its own — **which may be the better call, since it is failing `main` today.**

CI has since gone green with it applied.

`STArrayPool` is single-threaded by design and its bucket cache is a plain `static`, not `[ThreadStatic]`, with a check-then-act initialize in `Return()`:

```csharp
var cacheBuckets = _cacheBuckets ?? InitializeBuckets();
```

Two threads both see null, both initialize, and the loser trips `Debug.Assert(_cacheBuckets is null)`. Anything renting from it has to stay off parallel test threads — which is what the `DisableParallelization` collections are for.

- `ObjectPropertyListReentrancyTests` and `ObjectPropertyListNestedBuildTests` (added in #2555) build property lists, which rent the interpolation buffer, but were not in the sequential collection — unlike `PropertyListInvalidationDuringBuildTests` in the same file. This is a **latent failure already on `main`**; it is timing-dependent, so it shows on some platforms and not others.
- `AutoDenylistTests` (added here) has the same exposure: its cap tests reach `AutoDenylist.Sweep`, which rents a `PooledRefList` without `mt`. The blocklist tests need no marking because `BlocklistSnapshot.Build` asks for the `mt` pool explicitly.

No production change — `STArrayPool` is the right pool on the game loop, where both `Sweep` and the property list actually run.

## Deliberately not included

Waiting for a fragmented four-byte seed at `AwaitingSeed`. It looked like a bug but the disconnect is a deliberate defence: only pre-0xEF clients reach it (0xEF goes through `HandlePacket`, which already waits for its 21 bytes), and waiting converts an instant drop into a full 5s slot hold for a client sending one or two bytes, or a loris dribbling a byte every few seconds. Against a fixed 4096-entry `MaxConnections` table that trades capacity that matters for a fragmentation case a reconnect already fixes.
2026-07-30 23:12:17 -07:00
Kamron Batman
b8d3fec59a
fix(opl): refuse property list invalidation raised from inside GetProperties (#2555)
## The bug

Any property getter reached from `GetProperties` that calls `InvalidateProperties` takes the tooltip build down with it:

```
System.ArgumentNullException: Value cannot be null. (Parameter 'array')
   at Server.ObjectPropertyList.AppendStringDirect(String value)
   at Server.Mobiles.PlayerMobile.GetProperties(IPropertyList list)
```

`InvalidateProperties` rebuilds **in place** — `Reset()`, then `GetProperties()` again on the same instance — and `Reset()` does two destructive things to a build already in flight:

1. **It returns the pooled interpolation buffer.** The compiler rents it in the handler ctor and returns it in the closing `Add`, so *every hole is evaluated while it is live*:

```csharp
var handler = new InterpolatedStringHandler(1, 2, list); // InitializeInterpolation() RENTS
handler.AppendFormatted(pl.Rank.Title);                  // <-- getter runs HERE
handler.AppendLiteral("\t");
handler.AppendFormatted(faction.Definition.PropName);
list.Add(1060776, ref handler);                          // consumes span, RETURNS
```

```
GetProperties(list)
├─ InitializeInterpolation()  -> _arrayToReturnToPool = Rent(256)     buffer LIVE
├─ « hole 1: pl.Rank.Title »
│  └─ PlayerState.Rank.get   (lazy recompute)
│     └─ Invalidate() -> InvalidateProperties() -> m_PropertyList.Reset()
│        └─ Dispose(): Return(buf); _arrayToReturnToPool = null        buffer GONE
└─ handler.AppendFormatted("Knight")
   └─ _arrayToReturnToPool.AsSpan(_pos..)
      └─ ArgumentNullException (Parameter 'array')
```

It surfaces as `ArgumentNullException` rather than `NullReferenceException` because the `Range` overload of `AsSpan` must read `array.Length`, so the BCL null-checks and names the parameter `array`.

2. **It rewinds the packet cursor**, so properties already written are overwritten by the nested pass — a silently corrupted tooltip even where the buffer survives.

## The fix: refuse, don't recover

There is no correct recovery, and retrying the build would only hide the defect. A nested invalidation now logs an error with a stack trace, **throws in `DEBUG`** so it gets found and fixed, and in `RELEASE` returns without touching the list — a possibly stale tooltip, but no crash, no corrupted packet, and nothing leaked back to the pool. Getters that genuinely must invalidate should defer:

```csharp
Timer.DelayCall(InvalidateProperties);
```

The guard flag lives on the `ObjectPropertyList`, not the entity: it is that list's own lifecycle, it costs nothing (both `Item` and `ObjectPropertyList` absorb it in existing padding, and the list is allocated lazily), and it stays correct when builds for different entities nest.

Base instance sizes are unchanged from `main`: Item 128 B, Mobile 792 B, ObjectPropertyList 72 B, PlayerMobile 1216 B.

`PropertyList` also publishes the list into `m_PropertyList` **before** building it rather than assigning through `??=` afterwards, so a nested `InvalidateProperties` sees the build in progress instead of recursing into a second throwaway list whose work is discarded.

`ObjectPropertyList` re-rents its scratch buffer instead of spanning a null array, so a stray `Reset()` from any other caller degrades rather than aborting `GetProperties`.

## Factions `PlayerState`: maintained, not lazily computed

The getter that surfaced this is now a plain field read — the whole `if (m_InvalidateRank)` block and the flag itself are gone:

```csharp
public RankDefinition Rank => m_Rank;
```

`UpdateRank()` recomputes at each point an input actually changes:

| Site | Why |
|---|---|
| `RankIndex` setter | this player's index changed |
| end of `KillPoints` setter | two paths write `m_RankIndex` directly, bypassing the setter; runs once the swap bookkeeping and `ZeroRankOffset` have settled |
| `Faction.AddMember` | *after* the insert — the member count is not settled during the ctor |
| `FactionState` load | once ordering and `ZeroRankOffset` are final |

Supporting fixes this forced out:

- **Both ctors seed the lowest rank.** Nothing recomputes on read any more, so `Rank` has to be usable immediately — including for members that never get a `RankIndex` assigned, which is *every member with no kill points*. Without this, `Rank.Title` NREs.
- **`Rank` always resolves.** Ranks are ordered by `Required` descending ending at `0`, so a *negative* percent (`RankIndex` out of sync with `ZeroRankOffset`) matched nothing and left `m_Rank` null. It no longer divides by a zero `ZeroRankOffset` either.
- **A pre-existing staleness bug.** The `KillPoints` setter writes `m_RankIndex` directly in two places, so the cached rank was never refreshed when a player crossed zero kill points.

All six readers of `Rank` were checked; none relied on the old side effect.

One behaviour change worth flagging: rank refreshes are now **eager** where they used to be lazy, so a `KillPoints` change invalidates each swapped player as it happens. The swap loops break as soon as ordering is satisfied — typically 0–2 swaps — but it is on the path that runs on every faction kill.

## Documentation

The rule is written down so it is enforceable rather than folklore:

- **CLAUDE.md** audit rule 19
- **`dev-docs/property-lists.md`** — new "Never Invalidate From Inside `GetProperties`" section with the failing/passing pattern
- **`dev-docs/claude-skills/modernuo-property-lists.md`** — key rule + anti-pattern
- **`dev-docs/claude-skills/modernuo-code-audit.md`** — rule 19, ERROR severity

## Tests

- `ObjectPropertyListReentrancyTests` — `Reset()` and `Dispose()` re-entered mid-hole (both red against `main` with the exact exception above), nesting behaviour, and the new contract: `DEBUG` throws, `RELEASE` survives, and the build is never retried into a loop.
- `FactionRankTests` — `Rank` is populated before anything reads it, tracks `RankIndex` without a read, is stable across reads, and still resolves when `RankIndex` is out of sync with `ZeroRankOffset`. Red-verified: removing the ctor seed fails the first one.

793/793 `Server.Tests` and 608/608 `UOContent.Tests` pass.

## Noted, not addressed here

`~ObjectPropertyList()` returns the rented array to `STArrayPool<char>.Shared` from the **finalizer thread**, and that pool is single-threaded by design. Left alone as a separate concern.
2026-07-28 21:29:03 -07:00
Kamron Batman
967ddf48fa
fix(crowdsec): send a payload LAPI accepts (500 on alerts, 401 on auth) (#2553)
## Problem

Contributing a ban to CrowdSec failed against a real LAPI — `POST /v1/alerts` answered **500**, and depending on the shard's locale, auth answered **401**. Three independent defects, each sufficient on its own.

## Fixes

**`scenario_hash` / `scenario_version` were never serialized.** LAPI dereferences both unconditionally when persisting an alert, so omitting them is a nil deref and a 500 rather than a validation error. Both are now emitted with the values a watcher without a hub scenario is expected to send (`""` and `"1.0"`).

**`start_at`/`stop_at` were formatted without an `IFormatProvider`.** `:` is the time separator *specifier* in a custom .NET format string, not a literal — a shard running under a culture like `fi-FI` emitted `T15.04.05.123Z`, which Go's `time.RFC3339` rejects, producing another 500. Non-Gregorian cultures (`th-TH`, `ar-SA`) would also shift the year. Formatting is now pinned to `InvariantCulture` in `FormatTimestamp`, which additionally converts non-UTC input — the trailing `Z` is a literal and was previously an unchecked claim.

**The `User-Agent` was a plain product string.** LAPI's default watcher profile matches the `crowdsec/` prefix and answers 401 without it, so the header is a protocol constraint, not cosmetic. It is now an `internal const` carrying that reason.

Also fixed, same root cause as the timestamp bug: the login-expiry parse used a bare `DateTime.TryParse` on LAPI's RFC3339 `expire`. Under a mismatched culture that silently fails and falls back to a fabricated `UtcNow + 1h`, pushing re-auth past the real expiry and costing a 401-relogin round trip on every send.

`capacity` now defaults to `1` instead of `0`, matching the one-decision-per-alert shape actually being sent.

## Note on scope

The two 500 causes are independent. On an `en-US` shard only the missing scenario fields were biting; the date bug was latent and would have surfaced as an unexplained regression the first time someone ran a shard under a European locale.

## Verification

The emitted payload is field-for-field identical to a hand-verified request that a live LAPI accepts:

```json
[
  {
    "scenario": "modernuo/rate-limit",
    "scenario_hash": "",
    "scenario_version": "1.0",
    "message": "ModernUO rate-limit ban for 192.0.2.123",
    "events_count": 1,
    "start_at": "2026-07-27T15:04:05.123Z",
    "stop_at": "2026-07-27T15:04:05.123Z",
    "capacity": 1,
    "leakspeed": "0s",
    "simulated": false,
    "events": [],
    "remediation": true,
    "source": { "scope": "Ip", "value": "192.0.2.123" },
    "decisions": [
      {
        "origin": "modernuo",
        "type": "ban",
        "scope": "Ip",
        "value": "192.0.2.123",
        "duration": "300s",
        "scenario": "modernuo/rate-limit"
      }
    ]
  }
]
```

Regression tests assert the required scenario fields on the **serialized JSON** rather than the DTO — the DTO is not what goes on the wire — and cover the timestamp as a `[Theory]` across `fi-FI`/`th-TH`/`ar-SA`.

`dotnet test --filter "FullyQualifiedName~CrowdSec"` → **21/21 passed**, build clean with 0 warnings.
2026-07-27 23:31:37 -07:00
Kamron Batman
294dcd94a0
fix: Fixes send-path backpressure: consume IORingGroup 1.0.8, stop dropping packets silently (#2551)
## Summary

Two related fixes on the outbound path:

1. Consume **IORingGroup 1.0.8**, which allows more than one send in flight per socket, and expose the two settings that go with it.
2. Stop `NetState.Send` silently discarding packets when the send buffer fills — including an out-of-bounds write reachable in that state.

## 1. Send-path stall (RIO)

RIO reports send completion on **acknowledgement**, not on copy, so a completion cannot arrive sooner than one round trip. With one send in flight, `PostSend` refused to post again until the previous completion arrived — capping a connection at **one send per RTT** whenever it had data queued.

Measured on a 50ms-RTT production shard:

| | before | after |
|---|---|---|
| in-game latency, data flowing | **101–146 ms** | **48–51 ms** |
| p95 | ~135 ms | 52.8 ms |
| samples > 70 ms | 20 | **0** |

The control that confirms the mechanism: server-side post→completion was **unchanged** at median 92ms across both runs. The ACK-binding is inherent to RIO and did not move; only its propagation into application latency did.

Two things worth recording, because they explain why this went unnoticed:

- As little as **6 bytes** of queued data held the gate shut, so it reproduced in empty areas, not just crowded ones.
- The same measurement at loopback RTT is **microseconds**, so local testing could never surface it.

New settings, both restart-time:

- **`network.maxOutstandingSends`** (default 32) — sends in flight per connection. Honoured by RIO only; other backends complete sends on copy and report 1. Costs a request-queue and completion-queue slot per send, **not another buffer**, since every outstanding send addresses a different range of the same registered buffer. Worst-case added latency is roughly `completion RTT / value`.
- **`network.sendBufferSize`** (default 256KB) — per-connection send buffer, coerced to a power of two of at least the platform allocation granularity. This is the lever for the disconnects below, and the per-connection memory ceiling.

## 2. Send buffer full

`NetState.Send` had three failure modes once the buffer filled, none of them visible:

| writable | behaviour |
|---|---|
| `0` | `GetSendBuffer` returned false → **packet dropped**, no log, no disconnect |
| `4 … needed-1` | `Compress` returned 0 → `CommitWrite(0)` → **packet dropped** the same way |
| `1 … 3` | `safeOutputLength = (nuint)output.Length - 4` **underflows** → hot-loop bounds check never trips → **writes past the span** |

The first two leave a client connected while quietly missing game state, which is undiagnosable from either end. The third corrupts the in-flight region of the ring buffer, and is reachable precisely when a connection is congested, since callers only check for non-zero space.

`Compress` now refuses an output too small to bound, and `Send` reports exhaustion instead of dropping — logging and disconnecting with **needed / writable / unacked / capacity**. Those numbers separate a slow client holding the buffer from a buffer genuinely too small for the shard, which is the case that warrants raising `network.sendBufferSize`.

## Testing

`NetworkCompressionBoundsTests` covers the underflow using sentinel bytes around the output window. **Verified to fail without the guard** (4 failures from overwritten sentinels), confirming the out-of-bounds writes were real rather than theoretical.

Full suites green: **788 Server.Tests**, **597 UOContent.Tests**, Release build clean against the published 1.0.8.

## Notes for reviewers

- Upstream change: modernuo/IORingGroup#9.
- The buffer-full path is now *loud* where it used to be silent. If a shard has been quietly dropping packets under load, this will surface as disconnects — that is the intended outcome, and the log line says which setting to raise.
- Follow-up under discussion: promoting a connection to a larger buffer instead of disconnecting, which looks feasible on a live connection since buffers are referenced per-operation rather than bound to the request queue.
2026-07-27 23:06:53 -07:00
Kamron Batman
c909ed1f2f
fix: Streamlines insurance. Insurance only executes when enabled. (#2550)
### Summary

Moves inventory insurance out of `Mobile`/`PlayerMobile` into its own system at `Projects/UOContent/Engines/Insurance/`, wires it into the feature flag system, and makes disabling it actually disable it everywhere.

### Changes

**New `Server.Engines.Insurance.Insurance` system**

* Owns its own `Configure()`, seeding from the existing `insurance.enable` setting (default `Core.AOS`), so no config migration is needed. `Mobile.InsuranceEnabled` is gone, along with its line in `ExpansionConfiguration`.
* `CanInsure`, `GetInsuranceCost`, `ToggleItemInsurance`, `AutoRenewInventoryInsurance`, `CancelRenewInventoryInsurance` and `OpenItemInsuranceMenu` move here from `PlayerMobile`, which keeps four one-line shims for the context-menu callbacks.
* Every entry point is gated on `Insurance.Enabled`, and the death-time state is only allocated when insurance is on — a shard without insurance pays nothing for it.

**Feature flag integration**

Insurance is now a first-class feature flag: `ServerFeatureFlags.InsuranceEnabled`, registered under the `insurance` key in `FeatureFlagManager.SyncStaticFlag`, so it can be inspected and toggled through the normal flag command/gump rather than only at boot. `Insurance.Enabled` reads through to the flag, so there is one source of truth for every consumer.

**Fixes a memory leak from PvP**

`PlayerMobile.m_InsuranceAward` was a `Mobile` field assigned on every death and never cleared, so every player permanently pinned a strong reference to the last player who killed them. Killers were kept alive by their victims indefinitely.

Death-time insurance state now lives in a `Dictionary<Mobile, InsuranceContext>` owned by the insurance system: the entry is created in `OnBeforeDeath` and removed in `OnDeath`, so nothing outlives the death that created it.

**Removes insurance fields from every PlayerMobile**

`m_InsuranceAward`, `m_InsuranceBonus` and `m_NonAutoreinsuredItems` were carried by every `PlayerMobile` whether or not the shard ran insurance. All three are gone; the equivalent state is allocated per-death, only for players who actually die with insured items, only when insurance is enabled.

**Stale `Insured` flags are inert when insurance is off**

`Item.Insured` is a persisted flag, so items stay marked after a shard turns insurance off. Every read path now checks the flag first, so those items behave exactly as if they were never insured:

* `Item.CheckBlessed` / `Item.IsStandardLoot` — they drop again instead of acting blessed
* `Item.AddLootTypeProperty` — no more phantom "insured" tooltip
* `PlayerMobile.FindItems_Callback` — not yanked out of nested bags on death
* `DestroyEquipment` — no longer immune
* `ClothingBlessDeed` — no longer reports "that item is already blessed"

**Gumps promoted out of `PlayerMobile`**

`ItemInsuranceMenuGump`, `ItemInsuranceMenuConfirmGump` and `CancelRenewInventoryInsuranceGump` were private nested classes reaching into `PlayerMobile` privates. They are now public types in `Engines/Insurance/Gumps/`, talking to the insurance system through its public API. `ItemInsuranceMenuGump.ToggleSelected()` replaces the confirm gump's reach-in to the parent's `_items`/`_insure` arrays.

### Behavior changes

* The per-item "You lack the funds to purchase the insurance" message on failed auto-renewal is no longer sent during death; players get the single 1061115 summary instead. Marked with a TODO pending a decision on whether the per-item message should spam.
* The killer's insurance bonus is deposited once at the end of death processing rather than 300 gold at a time per insured item, and the "gold has been deposited" message is now conditional on the deposit succeeding. Same total.

### Drive-by cleanups

`PoisonImpl.IncreaseLevel` -> `Poison.IncreaseLevel`, a redundant `is NetState { } ns` pattern, `new List<Item>(Items)` -> collection expression, alignment of the `SyncStaticFlag` switch arms, and some comment/formatting fixes in `PlayerMobile`.
2026-07-26 09:47:49 -07:00
Kamron Batman
1a9cec1dbb
fix(advancedsearch): clear pause and sample exit before signaling the drain (#2549)
## Summary

`AdvancedSearchThreadWorker.Execute` signals `_stopEvent` **before** clearing `_pause` and **before** reading the exit condition. `Sleep()` unblocks the instant that signal fires, so the owning thread can begin the next cycle while the worker is still finishing the previous one — and the worker's two trailing operations then land on the new cycle's state.

`SerializationThreadWorker` already orders the same handshake correctly and documents why (`Projects/Server/Serialization/SerializationThreadWorker.cs`):

```csharp
// The owning thread may start another pause cycle the moment _stopEvent is set
// (Exit does exactly that). Clear _pause and sample the exit condition before
// signaling, or the new cycle's pause request is clobbered / its Sleep orphaned.
var exiting = Core.Closing || worker._exit;
Volatile.Write(ref worker._pause, false);
worker._stopEvent.Set();
```

This applies the same ordering to the search worker. Three lines; no behavior change on the happy path.

## The two failures

**Reuse hang.** The next cycle's `Wake`/`Push`/`Sleep` writes `_pause = true`, then the worker's stale `_pause = false` lands on top of it. The inner loop never observes `pauseRequested`, its queue is already empty, and it spins on `Thread.Yield()` forever — so the owning thread's next `Sleep()` waits on a `_stopEvent` that is never set again. A single search wakes each worker exactly once, so this only surfaces once `_threadWorkers` is reused by a later search.

**Orphaned `Exit()`.** `Exit()` sets `_exit`, `Wake()`s, then `Sleep()`s — the moment the drain's `Sleep()` returns. Reading `_exit` *after* the signal, the worker can observe that fresh `_exit`, return without ever consuming the `Wake`, and leave `Exit()`'s `Sleep()` waiting on a signal nobody will send. The `_thread.IsAlive` guard doesn't close this: the thread passes the check and returns immediately after.

## Verification

Verified with two throwaway timing tests — 25k reuse cycles and 2k drain-then-`Exit` cycles, each under a bounded wait:

| ordering | result |
|---|---|
| previous | `Failed: 2, Passed: 3` — both reproduce, cleanly at the 20s bound |
| this PR | 3 consecutive runs, 5/5, ~0.6s |

**Those tests are deliberately not included.** Their reproduction threshold is a property of one machine's scheduler — at 2k and 200 cycles the buggy build passed — so as permanent tests they'd cost ~560ms and 2000 thread creations on every suite run for a guarantee that may not hold on a CI runner. The ordering is protected the same way `SerializationThreadWorker`'s is: by the comment at the call site.

`UOContent.Tests`: **597/597**.
2026-07-25 15:32:06 -07:00
Kamron Batman
9c11ccdb80
fix(pathfinding): stop opening every .swb twice at boot (#2548)
## Problem

Every map's `.swb` step cache was opened, indexed and logged **twice** on boot.

`MovementPath.Configure()` explicitly called `PathCacheCommands.Configure()` and `CacheEvictionTimer.Configure()`. Both are types exposing a public static parameterless `Configure()`, which `AssemblyHandler.Invoke("Configure")` already discovers and calls once each (`AssemblyHandler.cs:157`). So `PathCacheCommands.Configure()` ran twice, and `AutoLoadAtStartup()` with it. `PathCacheCommands.Configure()` called `PathfindRecorder.Configure()` the same way.

`TryOpenLazyReader` disposes the prior reader before replacing it, so there was no handle leak — but the header and full chunk index of each `.swb` were read twice (~48 MB of files across six facets). The expensive `.mul` hashing was already memoized, so it was not doubled.

## Fix

Consolidate the cache lifecycle into `Initialize`:

- `Configure()` keeps only settings and command registration.
- `Initialize()` opens the readers once, then prebakes only maps that still lack one.
- The post-bake reopen is per-map instead of a blanket `AutoLoadAtStartup()` — on a partial bake (some valid `.swb`, one stale) that would close and reopen the readers already open, a second double-open on a different path.

`Initialize` is the correct phase. `Configure` runs before `TileMatrixLoader.LoadTileMatrix()` and `World.Load()` (`Main.cs:458/460/463/465`), so opening a `.swb` there forced the lazy `Map.Tiles` property — the fingerprint hashes the map files — and built every `TileMatrix` ahead of the loader that owns it, possibly before `TileMatrix.Configure()` settled `Pre6000ClientSupport`. Both sit at the default call priority and the phase sort is unstable. Moving pathfinding out leaves nothing in `Configure` that touches `Map.Tiles`, closing that hazard; the other 22 `.Tiles` users in UOContent are all runtime paths.

Multis stay out of the bake by design — houses and boats are player data that moves, handled by the multi-aware path at query time.

## Logging

The per-map `StepCache: opened ... chunks indexed` line drops to `Debug`. Opening is the expected case; `BakeMap` already logs a rebuild at `Information`, and `Initialize` still emits `PathBake: pre-bake complete (N map(s) written)`.

## Verification

- `dotnet build Projects/UOContent` — 0 errors, 0 warnings.
- `dotnet test --filter FullyQualifiedName~Pathfinding` — **123 passed, 0 failed**.

Boot logs should now show one `opened` line per map at `Debug`, none at `Information`.
2026-07-25 13:07:20 -07:00
Kamron Batman
c39454137e
feat(network): pluggable connection filters; file blocklist + contribute-first CrowdSec (#2542)
Reshapes IP banning around one idea: **core owns the question, content owns every answer.**

Core gains a single accept-path seam — `IConnectionFilter` — and loses everything that used to implement one. The firewall moves to UOContent, a new file-backed blocklist joins it there, and CrowdSec is repositioned from an in-app enforcer to a contribute-first reporter.

## The seam

```csharp
public interface IConnectionFilter
{
    string Name { get; }
    void Configure();
    void Start(CancellationToken token);
    void Stop();
    bool ShouldDeny(IPAddress address);
}
```

The accept path went from hardcoded branches to one question:

```csharp
else if (ConnectionFilters.ShouldDeny(remoteIP, out var deniedBy))
{
    logger.Debug("{Address} denied by connection filter '{Filter}'", remoteIP, deniedBy);
}
```

Filters register during the Configure sweep. The registry is a plain array walked by an indexed loop — no enumerator, no closure, no allocation — and the first denial short-circuits. An interface dispatch is noise next to the `accept()` syscall, so pluggability costs nothing measurable on the path that has to survive a DDoS.

Whatever a hit implies — persisting, promoting to an OS bouncer, contributing to the ban channel — is the filter's business, not the accept path's.

A filter that throws is **unregistered and the connection fails open**. A filter that faults once faults for every subsequent connection, so leaving it registered means an exception and a log line per accept — exactly the amplification an attacker wants — and a broken filter must not be able to deny everyone either.

This deliberately does **not** reuse `EventSink.InvokeSocketConnect`: that fires later and allocates a `SocketConnectEventArgs` per connection, which is what the accept path avoids for rejected traffic.

## What ships behind it

**`firewall`** (UOContent) — the existing admin-curated set. Collapsed from `Firewall` + `AdminFirewall` + a threaded enforcer into one single-threaded store with **zero concurrency primitives**: the accept path, admin gump, TTL expiry and boot load all run on the game loop. Persists to `Configuration/firewall.json` with automatic migration from the legacy `firewall.cfg`. No behavior change for operators — same namespace, same gump, same commands.

**`blocklist`** (UOContent) — new. Holds a millions-strong list in-app and **demand-pages** hits up to CrowdSec, which promotes them to the OS firewall.

The motivation is concrete: CrowdSec's Windows bouncer cannot load the ~3.9M IPs that 91 community feeds produce, but it handles ~100k fine. So the millions live in-process behind a binary search, and only addresses that *actually connect* get promoted. A `PromotedGuard` suppresses re-reporting an address until the bouncer picks it up.

The list is parsed straight from UTF-8 file bytes with no per-line string allocation, off the game loop, and published as an immutable snapshot swapped through a single `volatile` reference. Reloads yield to world saves.

**`tools/Export-IpBlocklist.ps1`** — the producer. Requires PowerShell 7 and runs on Windows, Linux and macOS; Windows PowerShell 5.1 is refused up front via `#requires`. Merges a thin, non-overlapping feed set into one de-duplicated, bogon-filtered file. Parsing runs in a compiled `Add-Type` hot loop (~1s for ~4M lines instead of minutes). Written to a `.tmp` sibling and swapped with `File.Replace`, so the shard never reads a half-written list, and a total feed outage refuses to overwrite a good list with an empty one. Re-running is idempotent — it exits without downloading anything while the list on disk is younger than `-MinInterval` (default 2h, the anchor feed's own refresh period), so a misconfigured scheduler can't hammer upstream.

## CrowdSec: contribute-first

`IBanReporter` + `BanChannel` fan locally-decided bans out to external systems. `CrowdSecReporter` (UOContent) posts to LAPI `POST /v1/alerts` and retracts via `DELETE /v1/decisions`.

Reporting is **enqueue-only** on the accept path: a bounded, coalescing channel drained off-loop with bounded retry, counted drops on overflow, and a flush on shutdown. Under a DDoS the accept path never does synchronous or lock-contending per-IP work.

### Why not pull decisions from CrowdSec?

The original design streamed decisions into an in-app snapshot and enforced them at the accept gate. That's the wrong layer: by the time the shard sees the connection, the TCP handshake and socket setup are already paid for. `cs-firewall-bouncer` drops the same traffic **at the kernel**, and it's what CrowdSec is built to do. So the shard now contributes what it uniquely knows (rate-limit trips, blocklist hits from real connection attempts) and lets the OS enforce.

The one thing the OS can't do — hold millions of entries on Windows — is exactly what the in-app blocklist covers, and it feeds the same pipeline.

## Threading policy

`CLAUDE.md` rule #3 is rewritten as an explicit three-part policy, with rule #10 restated in tandem:

- Anything touching game state runs **only** on the main loop.
- Heavy work that *needs* game state must be **chunked** across ticks, never threaded.
- Heavy work that does *not* need game state (large-file parse, external I/O) **must** run off-loop **and must yield to world saves**.

Results come back via an immutable snapshot swapped through a single `volatile` reference, or `Core.LoopContext.Post` — never by letting the scheduler decide where heavy work runs. Both new subsystems follow it.

## Shared primitives

`SortedRangeIndex<T> where T : IBinaryInteger<T>` — coalesced disjoint interval arrays plus a binary search. The firewall, the blocklist, and (as of this PR) core's reserved-network tables all use it.

Coalescing is a correctness requirement, not an optimization: multi-feed lists nest CIDRs (`/24` containing a `/32`), and a search that inspects only the rightmost run whose minimum is ≤ the value is sound **only** over disjoint runs. That bug was caught in review and is covered by regression tests.

`IPAddressUtility` collects the allocation-free `IPAddress` ↔ `UInt128` conversions and CIDR parsing that were previously scattered or duplicated.

## Config

| File | Owner | Keys |
|---|---|---|
| `Configuration/bans.json` | core | `reportRateLimitTrips`, `autoBanDuration` |
| `Configuration/blocklist.json` | content | `file`, `reloadInterval`, `reportHits`, `banDuration`, `promoteSuppression` |
| `Configuration/crowdsec.json` | content | `lapiUrl`, `machineId`, `password`, `origin`, `manualBanDuration`, `flushInterval`, `maxQueue` |
| `Configuration/firewall.json` | content | persisted firewall entries (migrated from `firewall.cfg`) |

Everything is inert by default. CrowdSec self-disables without credentials; the blocklist self-disables until its file exists. A shard that changes nothing sees no behavior change.

## Notes for review

- **Core no longer references `Firewall` or `IFirewallEntry` anywhere.** `NetworkUtilities` used to build its reserved-network tables out of `CidrFirewallEntry`, which coupled core to the firewall for something unrelated to banning; those are now a `SortedRangeIndex<UInt128>`, same semantics and public API.
- **`BanChannel.Stop()` no longer persists the firewall** — a contribution coordinator has no business saving an enforcement store. That's the firewall filter's `Stop()`.
- **A dead `whitelisted` parameter was dropped** from the blocklist gate: it was hardcoded `false` at its only call site, and no whitelist concept exists in core.
- **The blocklist filter is an instance, not a static.** The static version forced its tests onto the sequential collection with a reset hook; they now run in parallel.
- `dev-docs/networking-packets.md` documents the seam for content authors, plus a known wart in the `IPAddress` ↔ `UInt128` normalization flagged for a follow-up PR.
- The generator was verified on Linux, macOS and Windows under a temporary CI matrix (since removed). It caught two portability bugs — a Windows-only path separator, and a culture-sensitive duration parse that read `2.5` as `25` on comma-decimal locales and *silently* turned a 2.5h cooldown into 25h — plus a third that made the script unparseable on Windows PowerShell 5.1. The source is ASCII-only for that last reason: `#requires` is only honored once a file parses, so non-ASCII in a BOM-less script produces parse errors instead of the version message.

## Tests

**1344 pass** (782 `Server.Tests`, 562 `UOContent.Tests`). New coverage: filter registry (registration, short-circuit, fault-disable), blocklist parsing/CIDR/coalescing, snapshot reload markers, promote-guard TTL, ban-channel fan-out, CrowdSec alert building/dedup/flush-on-stop, and the generator's output-format contract pinned against the reader.
2026-07-25 11:59:37 -07:00
dependabot[bot]
bec4cfa910
chore(deps): bump actions/setup-dotnet from 5 to 6 (#2545)
Bumps [actions/setup-dotnet](https://github.com/actions/setup-dotnet) from 5 to 6.
- [Release notes](https://github.com/actions/setup-dotnet/releases)
- [Commits](https://github.com/actions/setup-dotnet/compare/v5...v6)

---
updated-dependencies:
- dependency-name: actions/setup-dotnet
  dependency-version: '6'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-07-21 16:30:40 -07:00
dependabot[bot]
e4827fc57b
chore(deps): bump actions/upload-artifact from 4 to 7 (#2544)
Bumps [actions/upload-artifact](https://github.com/actions/upload-artifact) from 4 to 7.
- [Release notes](https://github.com/actions/upload-artifact/releases)
- [Commits](https://github.com/actions/upload-artifact/compare/v4...v7)

---
updated-dependencies:
- dependency-name: actions/upload-artifact
  dependency-version: '7'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-07-21 16:30:08 -07:00
Kamron Batman
1e97ed50f6
fix: Harden Advanced Search: crash-safety, autosave, correct results & worker fixes (#2543)
## Summary

Hardens the **Advanced Search** engine (`Projects/UOContent/Engines/Advanced Search/`) — the GM entity finder that fans searches across background worker threads. A code review surfaced 14 defects (A–N), including a shard-crasher reachable from a single admin typo and a path that silently disables autosave for the rest of the shard's uptime. Each behavioral fix ships with a test.

Full `UOContent.Tests` suite: **530/530 green** (21 new AdvancedSearch tests).

## Fixes

### Crash / data-loss
- **A — Shard crash on a malformed Property Test.** `AdvancedSearchThreadWorker.Execute` had no `try/catch` and the worker `Thread` is foreground, so a parse throw (`Hits>abc`, `Layer=onehanded` — `Enum.Parse` was case-sensitive, `Hits>1@` — empty sub-expression indexing) terminated the process. Now: `ParseValue`/`CompareValues` use `TryParse`/`Enum.TryParse(ignoreCase)` and return no-match instead of throwing; the per-entity filter is wrapped in `try/catch` (logs + skips); empty expressions are guarded.
- **C — Overlapping searches corrupt state + brick autosave.** `_threadWorkers`/`_threadId` were `static` but `DoSearch` is an instance method; a second search (double-click / two admins) stomped shared worker state and could leave a drain waiting forever on the shared `AutoResetEvent`, so `AutoSave.SavesEnabled` was never restored. Now: an `Interlocked` re-entrancy guard rejects concurrent searches.
- **G — Autosave restore not guaranteed.** The restore lived only in the success callback. Now it's in a `finally` (plus an outer `catch` covering the synchronous setup and a `catch` on the drain body), so autosave + the guard are always released.

### Wrong results
- **D — `@`/`|` operator precedence.** `a@b|c` evaluated as `a && (b || c)` instead of `(a && b) || c`. OR now binds looser than AND (`AdvancedSearchUtilities.EvaluateBoolean`, unit-tested).
- **E — Descending sort, partial last page rendered blank** (the index decreased in descending mode and the `break` early-out killed the loop). Now a bounded `VisibleCount`-driven loop renders the last page in both directions.
- **F — Deleted entities** were not skipped (ghost rows). Now `DoEntitySearch` skips `entity.Deleted`.
- **N — Reference-type comparisons** threw (`Comparer<T>.Default.Compare` on non-`IComparable`) and compared references to a string. Now equality is by value and ordering is guarded to `IComparable` (no throw).

### Worker perf / hardening
- **H** busy-spin → `Thread.Yield()` in the drain; **I** `GetProperties()` cached per `Type`; **J** `HandleValidInternal` moved behind the cheap map/range/region filters; **K** worker threads are `IsBackground` + `Exit()` tolerates an already-terminated worker; **L** `_filter == null` guard; **M** consistent `Volatile` access on `_pause`/`_exit`.

### Documented
- **B** — the residual worker/event-loop read race is documented on `AdvancedSearchThreadWorker`: workers read live entity state concurrently with the loop, so value-type reads may be stale-but-safe and getter exceptions are swallowed; fully eliminating it would require snapshotting entity fields on the main thread (deferred).

## Notes
- New test-only seams (`TryBeginSearch`/`EndSearch`/`IsSearchInProgress`/`VisibleCount`/`TryParseValue`/`EvaluateBoolean`) are `internal` via the existing `InternalsVisibleTo("UOContent.Tests")`.
- Dead `public ParseValue<T>` removed.
- `ConcurrentDictionary` for the reflection cache is intentional — these workers are genuinely parallel.
2026-07-21 07:51:06 -07:00
Kamron Batman
858c1d18bc
fix(opl): only apply the ':#' cliloc marker to integer values (#2540)
`ObjectPropertyList.AppendFormatted<T>(value, format)` treated **any** `{value:#}` as the cliloc marker (emitting `#<value>`). But cliloc numbers are integers — a `float`/`double`/`decimal` formatted with `#` is the standard custom-numeric (`#` = digit placeholder) format, not a cliloc reference, so those were being mis-marked.

Gate the marker on an integer value type:
```csharp
if (format == "#" && value is int or uint or long or ulong or short or ushort or byte or sbyte)
```

Now `{someFloat:#}` formats normally (passes `#` through to `TryFormat`); the marker/standard-format ambiguity narrows to the harmless `{0:#}` **integer** case (`#0`). Existing `AddLocalized(int)` / `{value:#}` (all `int`) are unaffected.

Adds `ObjectPropertyListSpanAddTests.HashFormat_OnlyMarksIntegers`: `int {value:#}` → `#<value>`; `double {value:#}` → `42.0.ToString("#")` (`"42"`, no `#`).
2026-07-19 10:49:16 -07:00
Kamron Batman
8d88ef70fd
fix: Eliminates double lookup with Contains->Remove (#2539) 2026-07-19 09:26:27 -07:00
Kamron Batman
e12cc5dd83
perf(saves): eliminate world-save freeze bottlenecks (~9.5x faster freeze) (#2525)
Reduces the world-save freeze window from ~740ms to ~78ms (measured on a synthetic 10M-entity / 1.7GB world, 24 cores, through the real pipeline classes) by removing the per-entity handoff between the game loop and the serialization workers, fixing how large indivisible payloads are scheduled, rewriting the BufferWriter hot path, removing per-entity placement state entirely, and finally replacing the global serialized-types tracking with a per-file type table (idx v4) that also shrinks idx files by ~21% and speeds the background write phase. The pipeline has also been validated end-to-end on live-copy worlds in the multi-million-entity range, where the freeze is drain-bound (real `Serialize()` costs far more CPU per byte than synthetic writes) — the same structural wins hold, and entity/file round-trips are byte-clean across both load paths.

## The problem

The freeze window is `max(main-thread handoff, slowest worker drain)`:

1. **The producer was the bottleneck.** The main thread round-robined every entity through per-worker `ConcurrentQueue`s — two interlocked ops per entity, ~740ms of freeze floor at 10M entities before any serialization happened.
2. **Round-robin distributes count, not cost.** "Deep" systems (50MB generic persistence blobs) and "thick" entities (100K-item storage keys) landed on arbitrary workers, producing lopsided drain times on large worlds.
3. **Worst-case scheduling.** `GenericEntityPersistence.Serialize` pushed its self-payload *after* all entities, and generic persistences sort last in the registry — so the biggest indivisible blobs started serializing at the very end, extending the freeze by their entire duration.

## The fix

**Commit 1 — chunked handoff + LPT scheduling + heap pre-sizing:**
- Pooled 4096-entity chunks published to one shared queue; workers pull chunks and load-balance dynamically (a worker busy with a thick entity simply takes fewer chunks).
- `Persistence.SerializeAll` pushes systems largest-first (LPT) using the previous save's payload size (or loaded file size on first boot); self-payloads get dedicated single-entity chunks so they overlap the entity stream instead of ending it.
- Worker heaps pre-size from the loaded save's `.bin` totals, eliminating copy-on-grow inside the first save's freeze.
- `SpinWait` backoff in the drain loop (never `Sleep(1)`), per-worker balance stats logged in debug builds (the call site is compiled out of Release), 1MB snapshot write buffer.

**Commit 2 — workers iterate the dictionaries directly + main thread joins the drain:**
- `GenericEntityPersistence` publishes 4096-slot ranges over its dictionary's backing entries array; workers serialize occupied slots (`value != null`) directly through a `ShadowEntry<TValue>` struct mirroring the runtime's private `Entry` layout. Safe because the dictionary is frozen during `Saving` (mutations divert to the pending safety queues).
- The layout is **proven at startup before any code reads through it**: validation measures the true `Entry` stride via precise allocation accounting (guaranteeing all shadow reads are in-bounds), then verifies every key/value of a churned, resized, freelist-exercised dictionary — reading value slots as raw pointer bits only, never materializing a managed reference until the layout is proven. If a future runtime changes `Dictionary` internals, validation fails with a logged warning and saves fall back to the (fully maintained) enumerate-and-push path.
- The main thread joins the drain via an inline worker after publishing, instead of idling — worth a full worker share, proportionally more on low-core hosts.

**Commit 3 — 2.2x faster BufferWriter write path, single-pass short strings:**
- PGO already devirtualizes and inlines every `IGenericWriter.Write` callsite (interface vs concrete measured identical) — the real per-write cost was the non-inlinable `Index` setter (range-check throw path + per-write high-water tracking) plus span bounds checks. Writes now reserve capacity once, then do an unaligned store through a ref with a raw index increment; the high-water mark folds at Seek/Resize instead of per write.
- Class-level implementations of the hottest default interface methods keep nested writes inlined (a DIM re-dispatches on `this` even at a devirtualized callsite).
- Strings of 85 chars or fewer encode once into a stack scratch instead of walking the string twice (`GetByteCount` + `GetBytes`). Byte output is identical.
- Measured: 34.4 → 15.7 ns/entity on a generated-style write mix; end-to-end freeze ~99ms → ~74-82ms.

**Commit 4 — branch-free fallback push loop:**
- A bare `foreach { PushToCache(entity); }` runs at 2.3ns/entity; the same loop carrying a per-entity heavy-check runs 2.3x slower — the cost is the fatter loop body defeating tight-loop codegen. Entity-level >1MB payloads are rare enough to ride in shared chunks; system self-payloads (the large ones) are still explicitly scheduled largest-first.

**Commit 5 — drop the 9-byte per-entity placement state; snapshots write from worker segment logs:**
- Every `ISerializable` carried `SerializedThread/SerializedPosition/SerializedLength` so `WriteSnapshot` could gather each entity's bytes from the worker heaps in dictionary order. But the idx records absolute positions — bin order is free — so the snapshot is now written in worker-heap order and the join inverts: workers log segments (owner, slot range, heap start) plus one length per record as they serialize; positions are implicit because a worker's writes are contiguous, and identity comes from re-walking the same snapshot slots in the same order (stable until `PostWorldSave`).
- Chunks are persistence-homogeneous (the partial chunk publishes at each `SerializeAll` boundary) so segments route to files by owner with zero per-entity state. Self-payloads keep placement as three private fields on the handful of persistence instances.
- Net: 9 bytes (plus padding) of resident state removed from every item, mobile, guild, and account on every shard; three interface-property stores per entity leave the drain hot path (stamping dirtied one cache line per entity mid-freeze — the lengths log is one sequential stream); each segment's bytes hit the bin as a single span write instead of one copy per entity, speeding the background write phase; and `IGenericSerializable` shrinks to just `Serialize(IGenericWriter)`. Transient cost: ~4 bytes per entity in pooled per-worker logs, released after each write. The save format was unchanged at this point (idx v3, same loader); the v4 bump comes later in the branch.

**Commit 6 — staged file writes replace memory-mapped snapshot writing:**
- `MemoryMapFileWriter` is removed. `FileBufferWriter` composes through the full `BufferWriter` raw write path into a pooled staging block that drains to the file as large sequential positional writes (`RandomAccess.Write`); seeks flush the block and move the file offset, so backwards patches (the idx entity count) become small positional writes.
- Memory-mapped composition paid a soft page fault on every composed page plus unpredictable dirty-section teardown stalls at dispose — measured ~4x slower end-to-end than staged writes at snapshot sizes.

**Commits 7–11 — idx v4: per-file type table replaces SerializedTypes.db and all runtime type tracking:**
- Previously every `Write(Type)` from every worker enqueued into a shared `ConcurrentQueue<Type>` during the freeze (interlocked writes on a shared cache line, millions of mostly-duplicate entries), the background phase drained and deduped it all into a `HashSet`, and the snapshot recomputed `xxHash64(Type.FullName)` once per entity record (~5.4M redundant hashes per save on a large world) to write 9-byte tag+hash idx records plus a global `SerializedTypes.db`.
- The db's only real job was diagnostics: the string name behind "Type `<X>` was not found. Delete all of those types?" during idx loading. That map now lives in the idx itself: each `GenericEntityPersistence<T>` keeps an insertion-ordered `Type -> ushort` table, hydrated at `AddEntity` and on every deserialize path — one dictionary `TryAdd` per entity add on the game thread, amortized across gameplay, and provably immutable while the background writer reads it (adds divert to the pending queues during saves).
- idx v4 layout: the table (names only) is written before the records; records reference it by 2-byte index, shrinking from 33 to 26 bytes (−21%). The loader resolves each table name **once** (`FindTypeByHash(ComputeHash64(name))` — semantically identical to v3 resolution, `TypeAlias` included) into a constructor array, and each record becomes an array index instead of an 8-byte hash read plus dictionary probe. The unresolved-type prompt now surfaces once per type, with the name.
- Deleted outright: `World.SerializedTypes`, the drain/dedupe pass in `WriteFiles`, `BufferWriter`'s type tracking (its `Write(Type)` is now pure — payload format unchanged: tag byte + xxHash64), `FileBufferWriter`'s typeSet parameter, `Persistence.WriteSerializedTypesSnapshot`, and the adhoc db write. SerializedTypes.db is no longer produced.
- **Backward compatibility:** v0–v3 saves (including their SerializedTypes.db and legacy tdb files) load exactly as before, and every legacy load path hydrates the new table so the first v4 save after an upgrade is complete. Stale db files in existing save folders are simply ignored. Verified live: a v3 save boots, saves as v4 (Items.idx −20.2% on a dev world), and reloads with identical entity counts.

## Measured (synthetic 10M entities / 1.7GB, 64+64+32MB system blobs, 24x 2MB thick entities, dense write profile, 24 cores)

| Metric | Before | After |
|---|---|---|
| Steady-state freeze | ~740 ms | **~78 ms** |
| Main-thread publish cost | ~740 ms | **~0.1 ms** |
| Steady-state allocations | 0 | 0 (by iter 2) |
| Worker byte-load spread | 2x | ~1.15x |

The freeze is now bound by pure serialize throughput (payload / cores).

## Tests

- 779 Server.Tests + 501 UOContent.Tests pass.
- New across the branch: chunk fill/flush/owner-boundary tests, pool reuse/clear tests, an end-to-end multi-worker drain through the real wake/push/flush/pause protocol, a 50K-entry churn equivalence test for the shadow iteration re-walk (the exact pairing the snapshot writer relies on), byte-level BufferWriter output/position pins, `RuntimeLayoutIsSupported` so a silent fallback on a future runtime upgrade fails loudly in CI, and a full snapshot **round-trip test** that serializes 25K entities plus a self-payload through real workers, writes the idx/bin from the segment logs, and reloads them through the standard loader (now in v4 format).
- For idx v4 specifically: `FileBufferWriter` staging/drain/seek-patch and oversized-item tests, type-table registration tests, a hand-written v4 fixture proving an unresolvable type name skips only its own records through the console confirmation flow, and a hand-written **legacy v3 fixture** proving old saves still load and hydrate the type table for their next save.

## Trade-offs

- Chunk scheduling is nondeterministic, so worker heaps ratchet to each worker's max-ever draw rather than a fixed share. With slot ranges the balance is tight (~1.15x), so the effect is small; a shared slab pool remains an option if production shows retention creep.
- Entity-level heavy items inside slot ranges are serialized wherever they're encountered (no LPT for them); worst-case tail is one thick entity's serialize time (~ms). System self-payloads — the large ones — are still explicitly scheduled largest-first.
- Snapshot-write error granularity is per segment rather than per entity (heap-bounds bugs were the only thing the per-entity catch ever caught; idx metadata reads keep per-record granularity).
- idx v4 is a save-format version bump: old saves load unchanged through the preserved legacy paths, but saves written by this branch require this loader. Per-persistence type tables cap at 65,535 distinct entity types per boot (hard throw, orders of magnitude of headroom), and a type's table slot persists until restart even if its last entity is deleted — a few stale name entries per file, by design.
2026-07-16 22:53:28 -07:00
Kamron Batman
3cb077a79e
ci: cap job runtime, dump on test hangs, expand the Linux matrix (#2538)
Two stalls hit CI in one night with nothing bounding them but GitHub's 360-minute default:

1. A silently deadlocked test host (2h41m before manual cancel) — the code fix is on #2525; this PR adds the guardrails that make any recurrence cheap and self-diagnosing.
2. A dnf step stalled mid-download for 12+ then 30 minutes. Probing the EPEL mirror pool directly: the **first** mirror in the current US mirrorlist returns consistent HTTP 500, and sibling mirrors serve mismatched metadata generations, while Fedora's status page reads all-green (it only tracks central services, not the volunteer pool).

## Changes

- **`timeout-minutes: 30` on both jobs.** Verified live: the cap killed a stalled job at exactly 30:02 instead of 6 hours.
- **`dotnet test --blame-hang-timeout 10m --blame-hang-dump-type full`** — a stuck test host is killed after 10 minutes and vstest names the in-flight tests with a full process dump; `TestResults` (including dumps) upload as artifacts on failure. A future hang produces a stack trace instead of a bill.
- **EPEL setup follows the quickstart ordering**: `dnf-plugins-core` → enable CRB → `epel-release`. `epel-next` is no longer installed — none of the prerequisites need it (validated green on Stream 9), EPEL 10 doesn't have it, and it's one more mirrorlist to fetch.
- **Matrix**: adds **Ubuntu 26.04 LTS**, **CentOS Stream 10**, and **AlmaLinux 10** (real-EL10 coverage); bumps **Fedora 42 → 44** (42 went EOL in May). README badges and the Linux prerequisites section updated to match.

Kept deliberately simple per review: no retry wrappers around dnf — mirror hiccups are rare and the job cap bounds the damage.
2026-07-16 22:37:06 -07:00
Kamron Batman
7434ed7ee1
fix(console): stop headless servers from pegging a CPU core (#2535)
## Problem

On headless Linux deployments (systemd service, Docker without a TTY, `nohup`), the ModernUO process pegs a full CPU core even when idle. It does not reproduce on Windows because that runs with an interactive console.

## Root cause

`ConsoleInputHandler` runs a background thread (named "Console Input Handler") that loops on `Console.ReadLine()`. When stdin is **not** an interactive terminal, `Console.ReadLine()` returns `null` at end-of-stream **immediately** on every call, so the loop `continue`s in a tight spin — one core at 100%.

Reproduced in a container running the actual distribution: the "Console Input Handler" thread sat at ~90% CPU on a headless boot; with a blocking stdin it dropped to idle.

## Fix

1. **Detect headless once at startup:** `Core.Headless = Console.IsInputRedirected`.
2. **Extract a testable `ConsoleInputPump`** that owns the input stream: per line read, it *atomically* (under one lock) either delivers the line to a waiting prompt or dispatches a console command, and it **ends on EOF instead of spinning**. Cleanup runs unconditionally in a `finally`, so a pending prompt is always released (never hangs). Replaces the old `async void` loop and the fragile `_expectUserInput` / two-`AutoResetEvent` / `_input` handshake.
3. **`ConsoleInputHandler` becomes a thin headless-aware facade** over the pump. Headless: the reader thread never starts (`Console input disabled (headless: stdin is not a TTY).`), and `ReadLine()` throws a fatal `HeadlessConsoleInputException`.
4. **Data-gating and first-boot prompts** (deserialization "delete bad types? y/n", save-conflict, config/expansion setup) now route through `ConsoleInputHandler.ReadLine()`, so a headless server crashes fatal with a clear message instead of reading `null` (previously an NRE or a silent wrong branch).

Design decision (model b): headless servers are expected to be supplied with configuration/save data (including the owner account); interactive prompts when headless are fatal by design.

## Testing

- New `ConsoleInputPumpTests` (5 tests): EOF ends the loop without spinning; command dispatch; a pending prompt receives the next line; EOF while a prompt is pending completes it with `null` (no hang); a throwing command lookup does not hang a pending prompt. The tests synchronize on real pump state (no `Thread.Sleep`), so they are deterministic on slow CI.
- Full `Server.Tests`: no new failures introduced.

## End-to-end verification (Docker, real distribution)

| | Console Input Handler thread | Container CPU |
|---|---|---|
| Before fix (headless boot) | ~90% | ~199% (2 cores) |
| After fix (headless boot) | **not started** | **~11%** |

After the fix, a headless boot logs `Console input disabled (headless: stdin is not a TTY).`, loads the world normally, and idles instead of spinning.
2026-07-16 18:52:43 -07:00
Kamron Batman
f4a771c19d
fix: Items dropped on the ground never decay (#2536)
## Problem

Items dropped on the ground never decay. Corpses do, which makes the breakage look selective — but corpses are unaffected only because `Corpse.BeginDecay` runs its own `InternalTimer` and never touches `DecayScheduler`. Ordinary items are the only things that depend on the scheduler.

## Root cause

`Item.MoveToWorld` called `SetLastMoved()` — which triggers `UpdateDecayRegistration()` — at the *top* of the method, before detaching the item from its parent and before assigning the new map. `CanDecay()` reads `Decays`, `Parent`, **and** `Map`, so registration was evaluated against the item's *pre-move* state.

Because the `Item` constructor sets `m_Map = Map.Internal`, and `Mobile.Lift` calls `item.Internalize()` to put an item on the cursor, registration was consistently one step behind:

| State | Tracked for decay? | |
|---|---|---|
| Item on the ground | **No** | never decays |
| Item held on the cursor | **Yes** | backwards |

Nothing corrected it afterwards: the later `m_Map = map` assigns the field directly, bypassing the `Map` property setter, and that setter does not refresh registration either. With no parent, `RemoveItem` (which *does* re-register) never runs.

World load masked this — `ItemPersistence.PostDeserialize` re-registers every item against its final state, so decay appears to work for items that survive a restart. Only freshly dropped items are affected.

**Fix:** stamp `LastMoved` up front so decay math stays correct, then call `UpdateDecayRegistration()` once the parent, map, and location are final.

## Audit of the rest of the call sites

All 16 `SetLastMoved()` call sites were reviewed. `SetLastMoved()` must keep refreshing registration — `LastMoved` feeds `ScheduledDecayTime` and therefore which bucket an item belongs in — but it may only run once parent/map are final. The vendor, house, lift and drop sites already satisfy that. The rest of this PR fixes the ones that did not, plus what the audit turned up:

- **`Item.Deserialize`** stamped via `SetLastMoved()` before the version data was read, registering against an unread `Map`/`Parent`. Safe only by accident (the `Item(Serial)` ctor leaves flags at 0, so `Decays` is false), and it cost an unregister per item per world load. Now stamps only; `PostDeserialize` does the registration.
- **`Item` constructor** registered then immediately unregistered every item — the `Movable` setter saw `m_Map` still null, so `CanDecay()` was true. Also removes a `Configure`-order landmine: constructing an `Item` before `DecayScheduler.Configure()` would have thrown in `Shared.Start()`.
- **`Container.Destroy`** stamped `LastMoved` immediately before `MoveToWorld`, which now stamps it itself.
- **`Unregister` was documented O(1)** but scanned twelve buckets and did a linear `PriorityQueue.Remove`, on every construction, deserialize and move. Items now record where they are tracked in `Item.DecaySlot` (1 byte), so untracked items — the common case — leave in O(1).
- **A refused decay silently dropped the item.** `ProcessActiveQueue` deleted on `OnDecay() == true` but did nothing when a region refused, leaving the item dequeued, untracked and on the ground forever. It now restarts the decay clock; re-registering as-is would spin, since `ScheduledDecayTime` is already past.

## Two content bugs of the same class

The decay system replaced a polling sweep. Under polling, a `Decays`/`DecayTime` override could read live state every pass. Under a registration model it cannot — the scheduler drops items that stop being eligible, but nothing enrols one that becomes eligible while untracked.

- **`TreasureChestLevel1-4`** overrode `DecayTime` as `Utility.Random(15, 60)` — a fresh roll on every read. `ScheduledDecayTime` is read repeatedly (to bucket, to re-bucket on rotation, to test whether due), so those reads disagreed: the chest re-bucketed every tick and decayed early instead of after its intended interval. Now rolled once per chest. Distribution unchanged — `Utility.Random(from, count)` is RunUO's `from + Next(count)`, so this is 15–74 minutes, as before.
- **`StrongBox`** overrode `Decays` with a live check on `_house`, `_owner.Deleted` and `IsCoOwner`. Nothing notifies the box when any of those change, so it was never enrolled and the override never decayed anything — and it could not have: `HouseRegion.OnDecay` refuses a secured item inside a standing house, and the box is in `Secures`. Decay was never the mechanism here.
  - A strongbox is only ever its owner's. Without a house, or without an owner still co-owning that house, it would be a free container anyone could loot, so `Validate()` destroys it. The old check missed exactly those two cases — it required a non-null owner and treated a null house as valid. A deleted owner deserializes back as null, which `IsCoOwner` rejects. The now meaningless `Decays`/`DecayTime` overrides and an unhelpful `Console.WriteLine` are gone.

`DecayScheduler` now documents both constraints.

## Tests

`DecayRegistrationTests` (Server) covers world placement, lift/drop, container round-trip, cursor-held (must *not* track), `Container.Destroy` spill, `DecaySlot`/structure agreement, refused decay, and a full decay lifecycle driven through the scheduler. `TreasureChestDecayTests` (UOContent) locks `DecayTime`/`ScheduledDecayTime` stability across all four chest levels.

To make the lifecycle testable deterministically, `DecayScheduler` gains `internal` members (visible only via existing `InternalsVisibleTo`): `IsRegistered()`, `ProcessTick(now)` — extracted from `OnTick()` with no behaviour change — and `ResetForTests()`.

Red/green verified. Without the `MoveToWorld` fix, 5 of 6 of the original tests fail, including `ItemOnGround_ActuallyDecaysAfterDecayTime`, which shows a ground item never decays even after a full simulated hour. Without the chest fix, 8 of 8 chest tests fail. Without the refused-decay fix, that test fails.

Server.Tests 737/737 and UOContent.Tests 509/509 pass.
2026-07-16 18:51:04 -07:00
Kamron Batman
7470cfd43c
fix: Bumps dependencies. (#2531) 2026-07-14 15:17:55 -07:00
Kamron Batman
8b9bab20fd
fix(network): restore huffman code for symbol 0x19 (#2528)
## The bug

#2522 rewrote the outgoing huffman table in `NetworkCompression.cs` and transposed symbol `0x19`'s code from `0x1CE` to `0x12E` (both 9 bits, so the length distribution — and the Kraft sum — stayed valid, which is why nothing obvious tripped).

The real damage is that it broke prefix-freeness. `0x12E` is `100101110`, and symbol `0x0D`'s 8-bit code is `10010111` — a proper prefix of it. The client's decoder walks the tree bit by bit, so it hit a valid leaf at `0x0D` after 8 bits, emitted the wrong byte, and then reframed every subsequent code.

That is exactly what the reporter's capture shows. Server sends `BF 00 0C 00 19 02 00 00 00 01 00 00`; the client's post-decompression stream reads `BF 00 0C 00 0D 55 00 00 01 00 00` — the literal `0D` is the mis-decoded `0x19`, and the packet is now one byte short, so framing desyncs from there on.

## Impact

Any outgoing packet with byte `0x19` anywhere in its body (serials, coordinates, hues, lengths, text) corrupted the stream. Because the desync is in framing rather than a single field, the client silently stops applying server updates while still being able to send — no disconnect, no error.

`StatLockInfo` (`0xBF` subcommand `0x19`) is sent during login, so it reproduces on essentially every connection. This is also #2526: "can only walk a few steps, then the client stops responding" is the same desync, not a VPS sizing problem.

## Fix

One entry, restored to the canonical value:

```diff
-            0x9, 0x191, 0x9, 0x12E, 0x7, 0x03F, ...
+            0x9, 0x191, 0x9, 0x1CE, 0x7, 0x03F, ...
```

## Validation of the whole table

Rather than eyeball 257 entries, I diffed the current table against **every revision of it in this repo's history** — all 34, back through the renames to the original import. All 34 agree with each other, and `0x19` is the sole disagreement with #2522's rewrite. No other entry has ever changed.

I also validated the table structurally: all 257 lengths in `[2,11]`, every value fits its declared bit-length, Kraft–McMillan sum exactly 1, and no code is a prefix of any other. It passes on all counts now, and the prefix check is what located the bug in the first place.

Both checks were one-off validation scripts, not committed — see below.

## Test

A single known-answer test (`~10ms`) that compresses all 256 symbols and asserts the exact output bytes. The expected bytes were generated from the canonical table, *not* from the implementation, so the test isn't circular. Any single wrong table entry changes the output, so it pins all 256 entries plus the terminal code, and it exercises the encoder end to end.

A round-trip test would **not** catch this class of bug — encoder and decoder built from the same table agree with each other even when the table is wrong. The contract being violated is with the client's hard-coded tree, so the expected bytes have to come from outside the implementation.

The structural prefix-free check and a second `StatLockInfo` vector were deliberately dropped after they'd served their purpose: the table is now verified and effectively frozen, so the structural check was guarding a constant, and the `StatLockInfo` vector is a strict subset of the all-symbols one. What remains covers the risk that's still live — `Compress` is a hand-unrolled bit-packing loop that will get optimized again, and this is the guard against that rewrite silently corrupting the wire format, which is precisely what happened here.

Verified the test fails when the bug is reintroduced and passes when fixed. Full `Server.Tests` suite green: 727 passed.
2026-07-14 09:26:39 -07:00
Kamron Batman
b852bca41e
perf(pathing): pool the StepCache strata buffer, then clean up the pathing engine around it (#2523)
Started as an allocation pass over `StepCache` and grew into a cleanup of the surrounding pathing engine. Four commits, each independently reviewable; net **−560 lines**.

Build clean (0 warnings). All 122 `Server.Tests.Pathfinding` tests pass.

---

## 1. `perf`: pool the strata buffer, cut a hot-path dictionary lookup

**The headline is that `TryGetMask` — the actual hot path — was already allocation-free.** `StepMask` is a readonly struct, `StaticTileEnumerable` is a `ref struct`, `ChunkMissState` is a struct in a `Dictionary`. So most of this is a bake-throughput and GC-churn win, with one exception noted below.

`BuildChunk` accumulated packed multi-Z strata into a `List<byte>` that grew by doubling (256 → 512 → 1024 → …) and then paid a final `ToArray()`. A full map bake runs it ~114k times. It now writes into a `byte[]` rented from `STArrayPool<byte>.Shared` through a span writer, and hands the chunk one exact-size copy.

**This required fixing a latent out-of-bounds guard.** The record-fit check reserved headroom for **8** strata (`StratumByteLength * 8`) while `ComputeStandableSurfaceZs` can return up to **16** — so a cell could write 305 bytes starting from a 65,383-byte offset. Against a `List` that was benign (it just grew past 64 KB, and emitted offsets stayed under the `NoStrata` sentinel). Against a fixed-size rented buffer it is an out-of-bounds write, so tightening it was a *prerequisite* for the pooling, not a drive-by. The guard is now exact, which additionally proves no emitted offset can collide with `NoStrata == ushort.MaxValue`.

**One genuine query-path win:** `ShouldPromoteAfterMiss` did *two* dictionary lookups per miss — a `TryGetValue`, then an indexer assignment that re-hashes and re-probes. It now mutates in place via `CollectionsMarshal.GetValueRefOrNullRef`. This runs on every uncached chunk touch during A* expansion. The window-expiry branch keeps its explicit early return, so `MissPromotionThreshold == 1` still resets rather than promoting.

Also dropped `StepProbe.ComputeStrataAt` / `ComputedStratum` (dead code, zero callers) and collapsed six 18-argument `new StepMask(0, 0, …, kind)` blocks into `Fallthrough(kind)`.

**Considered and rejected:** pooling the `Direction[]` that `Find` returns. It *escapes* the call — `MovementPath` holds it across ticks while `PathFollower` walks `m_Index` through it — so it cannot be rented-and-returned, and it cannot be borrowed from the shared `BitmapAStarAlgorithm.Instance` without one creature clobbering another's in-flight path. `CheckPath` rate-limits repaths to one per 2s per creature, putting this at roughly 60 KB/sec at 1,000 pathing creatures. Not worth a public API break plus a use-after-return footgun.

## 2. `docs`: rewrite the comments for publication

The comments had accumulated as development notes: internal phase jargon (`Tier 4`, `the Phase-2 synthesizer`), change narration aimed at a reviewer (`which the old ComputeStandingZ anchor missed`, `legacy behavior`), benchmark anecdotes (`benchmarked as near-optimal`, `a ~20 ns lookup`), and paragraphs restating the code.

Rewritten to keep the rationale you cannot recover by reading the code — why the source-Z guard cannot be widened, why multis fall through with a halo, why the promotion gate counts Finds rather than calls, why `ComputeFingerprint` must hash the *files* and not the live tile tables — and drop the history that got us there.

Three comments were **factually wrong**, not just wordy:

- `CacheEvictionTimer` and `CacheStats` documented a class called `StaticWalkabilityCache`. No such class exists — it is `StepCache`.
- `StepCacheFile` declared `File layout v8` while `FormatVersion` is 9, and called the current record layout "the v6 layout" in four places. The layout descriptions are now unversioned so they cannot drift again.
- `StepProbe.ComputeStandingZ` claimed `StepCache` uses it to bake `SourceZ`. It has not since the baker moved to the clearance-aware `ComputeStandableSurfaceZs`; only a parity test calls it.

## 3. `refactor`: simplify `StepCacheFile.Write`, consolidate the format tests

`SaveToFile` walked `_keysList` **twice** — once to count the map's chunks, then again through a `ChunkEnumerator` closure to emit them — because `Write` needed the count up front to size its index array. Both loops had the same root cause. Passing a **span** collapses them: the count is just `span.Length`.

That deletes the `ChunkEnumerator` delegate, the closure over the list enumerator, and **both `InvalidOperationException` throws**, which existed only to police the delegate's "yield exactly `chunkCount` chunks" contract — a contract a span makes unrepresentable.

`Write` now patches the header's `IndexOffset` by seeking back to it rather than reaching into the writer's live buffer with `BinaryPrimitives`. That also retires `IndexOffsetFieldPosition`, a hand-maintained byte offset that had to track the header layout, and sidesteps the stale-array hazard that motivated the manual patch (`BufferWriter` reallocates on growth).

**Tests:** `StepCacheFileV6/V7/V8Tests` were named for the format version that introduced each transform — and the format is now **v9**, so all three names described formats the loader rejects outright. Beyond triplicated builders and plumbing, two things were actually broken:

- The three near-identical rejection tests each cited a `MinSupportedVersion` that had since moved (`"version 5 < MinSupportedVersion 6"`, `"6 < 7"`, `"7 < 8"`). They passed for the wrong reason.
- `AssertBaseEqual` (used by V7 and V8) **silently skipped the swim and strata trailers**. A regression dropping either would not have failed those tests.

Now one `StepCacheFileFormatTests`, named for behavior — predictive-Z elision, compression, compact index — with a single `AssertIdentical` that does check both trailers, the three rejection tests folded into one theory that also covers a future version, and a zero-chunk case the delegate-based writer never had coverage for.

## 4. `test`: consolidate the parity and lifecycle tests

Three files tested "parity" and none of the names said *which*. They were three different layers, and the seams are the useful part, so they are now one `StepCacheParityTests` that names them:

| Test | Compares | Answers |
|---|---|---|
| `ProbeMatchesSlowPath` | StepProbe vs MovementImpl | Is the bake right? |
| `CacheMatchesProbe` | StepCache vs StepProbe | Is it stored and returned intact? |
| `CacheServesReachableWalkStates` | StepCache vs MovementImpl | End to end, over the states A* visits |

Merging removed a duplicated stub `Mobile`, duplicated region seeds, and a filename/class mismatch (`StepProbeParityTests.cs` declared `StaticWalkabilityParityTests`). `SwimBake_ProducesWetCells` moved with it — it lived in the cache parity file but never touched the cache.

Tests reached into `StepCache._chunks` via `GetField` in **9 places**, each rebuilding the key encoding and cell-index arithmetic by hand. `StepCache` now exposes `GetResidentChunk` and `ResidentIndexInSync` alongside the internal test hooks it already had (`LazyReaderHasChunk`, `CurrentFindGeneration`), and the shared arithmetic moved to `PathingTestSupport`. All 9 reflection blocks are gone.

`StepCacheLifecycleTests` is regrouped by what it covers — promotion gate, fallthrough routes, strata, swim layer, eviction — with the `Tier4*` names dropped. Removed `Singleton_IsAvailable`, which asserted an inline-initialized static property was not null; that is the entire 123 → 122 test-count delta.

---

## Verification

Tests were mutation-checked rather than just run, since round-trip and parity tests can pass while a transform silently no-ops:

- Injecting an off-by-one into the `IndexOffset` patch fails **15 of 123** — the format tests are load-bearing.
- Offsetting the cache's cell index by one fails **7 of 10** parity cases, and the 3 that stay green are exactly the ones that do not touch the cache. The layering localizes a fault rather than just reporting one.
2026-07-12 20:02:29 -07:00
Kamron Batman
e035768ef8
fix: Optimizes outgoing packet encoding. (#2522)
### Summary

* Fixes a regression in ModernUO huffman encoding compared to RunUO & ServUO.
* Optimizes the encoding by 1.6x-2x using.

### Benchmarks
```cs
| Method                 | Categories | Mean     | Error   | StdDev  | Ratio |
|----------------------- |----------- |---------:|--------:|--------:|------:|
| BenchmarkSUOAcctPacket | AcctPacket | 757.1 ns | 3.83 ns | 3.58 ns |  1.00 |
| BenchmarkMUOAcctPacket | AcctPacket | 941.4 ns | 3.27 ns | 2.73 ns |  1.24 |
| BenchmarkOptAcctPacket | AcctPacket | 530.7 ns | 1.24 ns | 1.10 ns |  0.70 |
|                        |            |          |         |         |       |
| BenchmarkSUOGump       | GumpPacket | 526.8 ns | 2.10 ns | 1.86 ns |  1.00 |
| BenchmarkMUOGump       | GumpPacket | 657.7 ns | 1.25 ns | 1.11 ns |  1.25 |
| BenchmarkOptGump       | GumpPacket | 285.0 ns | 0.77 ns | 0.68 ns |  0.54 |
```
2026-07-12 18:37:14 -07:00
Kamron Batman
8bd1b3dc28
fix: AddonGenerator produces broken/incomplete addon output (#2517)
## Summary

`[AddonGen` currently produces addon scripts that **do not compile**, plus a few
gather-logic and UI bugs. This fixes all of them.

## Compile-breaking (verified)

Every generated addon failed to build because of the item-component emission path:

- **Trailing comma + missing semicolon.** Items were emitted as a multi-line
  `AddComponent(\n … ,\n)` — a trailing comma in the argument list and no terminating
  `;`, i.e. `CS1525: Invalid expression term ')'` and `CS1002: ; expected`.
- **`Deed` missing `new`.** The template emitted
  `public override BaseAddonDeed Deed => {name}AddonDeed();` — invoking the type as a
  method (`CS1955: Non-invocable member … cannot be used like a method`).

Both are now fixed; components are emitted on a single line matching the existing
static-tile path:

```csharp
AddComponent(new AddonComponent(3215) { Light = LightType.Circle300, Hue = 5 }, 2, 3, 5);
```

**Verification:** compiled the generator's *output* (a representative two-component addon —
one plain, one hued + light-source) before and after the change against minimal
`BaseAddon`/`AddonComponent` stubs:

- Before: `CS1525` + `CS1002` (item path), and `CS1955` in isolation for the `Deed` line.
- After: **Build succeeded.**

`Projects/UOContent` also builds clean with the source change.

## Gather-logic + UI (reasoned from the code, not runtime-tested)

- **Inverted Z-range guards.** The tile/item scan used `if (range && …)`, so with the range
  filter off (the default) map tiles and items were never captured — inconsistent with the
  Static pass's `if (!range || …)`. Corrected to match.
- **"Export Items" was dead unless "Export Statics" was also checked** — the items scan was
  nested inside `if (statics)`. Items now scan independently. Placed `Static` items are
  skipped in this path because they're already captured (with hue/light) by the
  `GetItemsInBounds<Static>` pass, which also removes a pre-existing double-count.
- **Swapped gump Min/Max labels** — the "Max" label sat over the Min entry and vice versa.

## Notes

The three gather/UI fixes are reasoned from the code rather than exercised through the
in-game gump, so they're worth a close look in review. The compile fixes are the headline
and are output-verified.
2026-07-12 10:15:25 -07:00
Kamron Batman
191d3f3f33
feat(throwing): add remaining SA throwing artifacts (add-only) (#2516)
Adds the four remaining named Stygian Abyss throwing artifacts as item definitions, completing the throwing artifact set (7/7 now defined in ModernUO).

## Added (stat-for-stat from ServUO)
- **Abyss Reaver** (Cyclone) — random Throwing +5..10 skill bonus, +25..35 damage, Exorcism slayer
- **Storm Caller** (Boomerang) — Hit Lightning / Hit Lower Defense, 20/20/20/20/20 elemental split
- **Banshee's Call** (Cyclone) — Hit Harm / Hit Life Leech, 100% cold, Velocity 35
- **Wind of Corruption** (Cyclone) — Hit Stamina Leech / Hit Lower Defense, 100% chaos, Fey slayer

## Acquisition — deferred (documented)
These are **`[add`-only for now**. Their OSI sources aren't in ModernUO yet:
- Abyss Reaver → the Into the Void quest (Agralem), deferred with the Abyss void-creature content.
- Storm Caller / Banshee's Call / Wind of Corruption → renowned/boss creatures (`WyvernRenowned`, `PrimevalLich`, etc.) that need a `BaseRenowned` framework port.

Per direction, the items are worth defining now; wiring their drops follows later.

## Notes
- Storm Caller's ServUO `WeaponAttributes.BattleLust = 1` is left as a `//TODO Implement BattleLust` — the attribute doesn't exist in ModernUO's `AosWeaponAttribute` set yet.
- The human Bow variant `WindOfCorruptionHuman` is archery, not throwing — intentionally out of scope.

Server builds clean.
2026-07-03 08:50:16 -07:00
Kamron Batman
158e7f2e5f
feat(throwing): add Ter Mur reptiles (Raptor + slith family) and their SA claws (#2515)
Follow-up to the gargoyle Throwing work (#2510/#2512/#2514). Ports two Stygian Abyss creatures from ServUO so two of the throwing artifacts finally have a real OSI drop source instead of being `[add`-only.

## Changes
- **`Raptor`** and **`StoneSlith`** — ported stat-for-stat from ServUO. They **spawn automatically**: `Distribution/Data/Spawns/post-uoml/termur/TerMur.json` already contained Raptor/StoneSlith spawner entries referencing these class names, so no spawn-file edits were needed.
- **`RaptorClaw`** (Boomerang-based) and **`StoneSlithClaw`** (Cyclone-based) artifacts, dropped from each creature's `OnDeath` at ServUO's 0.5% rate (uncontrolled only).
- StoneSlith retains its `GraspingClaw` monster ability; both use `BleedAttack`.

## ModernUO idioms
Default range perception (16) / fight range, derived `GetSpeeds` (no `SetSpeed`), `[SerializationGenerator(0)]` codegen, collection-expression arrays.

## Documented omissions vs ServUO
Flagged as TODO in-code — all are content missing from ModernUO, not silent drops: Raptor's friend-spawn timer + its 25% `AncientPotteryFragments` drop; StoneSlith's `TailSwipe`, `DragonBlood`, and `SlithEye`/`TatteredAncientScroll`/`AncientPotteryFragments` drops; plus `HideType.Horned/Spined` and `PackInstinct.Ostard` (no ModernUO equivalents yet).

## Not included
The other three throwing artifacts (Storm Caller, Banshee's Call, Wind of Corruption) drop from renowned/boss creatures via a `BaseRenowned` artifact-list system that doesn't exist in ModernUO yet — a separate, larger effort. AbyssReaver stays with the (deferred) Into-the-Void quest.

Verified: server builds clean; throwing suite 14/14.
2026-07-02 23:58:29 -07:00
Kamron Batman
9c376cb06c
fix(throwing): grant Str/Dex stat gains for the Throwing skill (#2514)
The Throwing skill shipped with `StrScale`/`DexScale`/`StatTotal`/`StrGain`/`DexGain` all `0` in `skills.json`, so training it never raised Str or Dex — unlike every other weapon skill.

Fills those in by mirroring **Archery** (the Dex-primary ranged analog, which matches Throwing's existing `PrimaryStat: Dex` / `SecondaryStat: Str`): `StrScale 0.025` / `DexScale 0.075`, `StatTotal 10`, `StrGain 0.25` / `DexGain 0.75`.

Data-only change.
2026-07-02 23:07:36 -07:00
Kamron Batman
a706ef1449
fix(ci): run test projects on CI; remove brittle OPL attribute tests (#2513)
## Problem

Three coupled issues, each hiding the next:

1. **CI passed despite failing tests, with no test logs.** ([example run](https://github.com/modernuo/ModernUO/actions/runs/28639143286/job/84931544255) — the `Test` step produced zero output and the job went green.)
2. **Two `EmitsLowerStatReqWhenPassed` tests** fail with `KeyNotFoundException: '1060435'`.
3. Once CI actually ran the tests, **~337 UOContent tests failed** with `FileNotFoundException: tiledata.mul was not found` — the test bootstrap force-loaded copyrighted client data that CI doesn't have.

## Root causes & fixes

### 1. CI ran zero tests (`fix(ci)`)
The `Test` step ran `dotnet test --no-restore`, but the `Build` step only restores/builds `Application` — never the test projects. Without a restore, the test projects have no `project.assets.json`, so `Microsoft.NET.Test.Sdk`'s targets aren't imported, they aren't recognized as test projects, and `dotnet test` runs the `VSTest` target against **zero** projects → no output, **exit 0**.

- Both jobs now run `dotnet test --logger trx --results-directory ./TestResults` (test projects restore and run) **plus a guard** that fails the job if no `.trx` is produced — a permanent backstop against silent zero-test passes.

### 2. Impossible OPL tests (`fix(ci)` + `test(opl)`)
#2501 deliberately emits `LowerStatReq` (`1060435`) **inline in each item**, not in `GetProperties`. A follow-up "fix" dropped the `lowerStatReq:` argument to make the tests compile but left the assertions expecting `1060435`.

- Removed the two impossible tests, then removed the **entire `Tests/PropertyList/` OPL attribute set** from #2501: these assert exact cliloc/value/order of OPL emission per item base — a one-time proof of the #2501 rewire, now a permanent tax on modding (any admin reorder/value change/added line reddens the build). The one non-trivial case (LowerStatReq) is what just broke, because the test was wrong. Inline emission stays covered by the `BaseArmor`/`BaseClothing` tests.

### 3. Tile-data-dependent tests crashed CI (`test(uocontent)`)
UOContent.Tests' collection-fixture constructor force-loaded `tiledata.mul` unconditionally. On CI (no client files) it threw, and xUnit failed **every test in the collection** with the same error — mostly collateral (packet/scheduler/spawner tests that don't need tile data).

- Mirror Server.Tests' graceful pattern: `TestServerInitializer` probes for `tiledata.mul` and only loads tile/multi data (and runs the tile-dependent configure steps) when present, exposing `TileDataLoaded` so the fixture no longer throws.
- Add a shared `TileDataRequirement.SkipIfMissing()` guard and apply it to exactly the **31** pathfinding/multi/AI tests that genuinely need real tile data (`[SkippableFact]`/`[SkippableTheory]`).

## Verification (all local)

| Scenario | Server.Tests | UOContent.Tests |
|---|---|---|
| **Client data absent (CI)** | 726 pass, 17 skip, **0 fail** | 469 pass, 32 skip, **0 fail** |
| **Client data present (dev)** | 726 pass, 0 skip, **0 fail** | 501 pass, 0 skip, **0 fail** |

- Full `dotnet test` exits **0**; TRX files produced; the no-test guard trips (exit 1) only when zero `.trx` are produced.
2026-07-02 22:35:37 -07:00
Kamron Batman
8c6eab5fca
feat(throwing): wire SA loot flavor + Valkyrie's Glaive stealable (#2512)
Phase 2 follow-up to #2510 — wires acquisition for the gargoyle Throwing content that Phase 1 deliberately left out.

## Changes
- **SA loot flavor (`IsStygian`)** — completes the dead loot path from the original PR: adds `SAWeaponTypes`/`SARangedWeaponTypes` pools + `isStygian` branches to `Loot.RandomWeapon`/`RandomRangedWeapon`, **and** the missing piece — computes `IsStygian` (`map == Map.TerMur`) in `LootPackEntry.Construct` and threads it through `LootPackItem.Construct`. Ter Mur creatures now roll SA gear + the three throwing weapons (Boomerang/Cyclone/SoulGlaive) from their normal `BaseWeapon`/`BaseRanged` loot entries. Conservative `TerMur`-only (not ServUO's extra `|| RandomBool()`).
- **Valkyrie's Glaive** — re-added as a self-contained Ter Mur stealable artifact at `(843, 665, 27)`, matching ServUO/OSI, with the previously-missing `ArtifactRarity => 5`.

## Deferred (documented)
The 5 host-dependent artifacts (Raptor Claw, Stone Slith Claw, Storm Caller, Banshee's Call, Wind of Corruption) are intentionally NOT included — their OSI drop sources (Raptor, StoneSlith, and the renowned/boss creatures + a `BaseRenowned` artifact-list system) don't exist in ModernUO yet, so re-adding the items now would leave them `[add`-only. They'll follow a dedicated SA-creatures effort.

## Notes
- No new tests: both changes are property/plumbing with RNG-driven output — no deterministic complex logic to assert (consistent with the repo's test-scope conventions).
- Verified: server + test project compile; throwing suite 14/14.
2026-07-02 22:28:55 -07:00
Kamron Batman
e42a62b1d3
fix: Fixes tests for armor/weapons (#2511) 2026-07-02 21:49:33 -07:00
Kamron Batman
0bfbdd0764
feat(throwing): core gargoyle Throwing skill (SA) (#2510)
Supersedes #2376 (@jwvalentine). This is a reviewed, corrected, and scoped-down **Phase 1** of Joe Valentine's throwing implementation — his original commits are cherry-picked here with authorship preserved, plus a fix/scoping pass. Phase 1 lands only the **core gargoyle Throwing skill**; the incomplete content is excised for follow-up PRs (see below).

## What's included (core skill)
- `BaseThrown` combat mechanics on top of the existing skeleton: close-quarters penalty, below-min-range penalty, shield penalty, STR-scaled range, overthrow damage penalty.
- Base weapons: **Boomerang, Cyclone, SoulGlaive** (gargoyle-only), + blacksmith crafting (SA-gated).
- Two symmetric hooks on `BaseWeapon` (`ModifyHitChance`, new `ModifyDamage`) that are inert no-ops for every other weapon.

## Fixes over the original
- **Overthrow damage**: was dead code (the swing gate already guarantees you're within `MaxRange`, so the old `ComputeDamage` check never fired). Reimplemented as `finalDamage × 0.53` applied *after* all offensive bonuses via a new `ModifyDamage` hook, firing at the outer range ring.
- **`DefMaxRange`**: clamped to `[MinThrowRange, MaxThrowRange]` (uncapped before → e.g. range 13 at 200 Str) and guarded against a latent divide-by-zero.
- **Close-quarters mitigation** now uses `RawDex` (matches ServUO/OSI; deterministic under stat mods).
- **Return-throw timer** guarded against a deleted/unmapped thrower/target.
- Reverted the `MovingShot` change (it rebalanced archery — belongs in a separate PR).
- Kept only complex-logic tests (hit-chance/range/damage math); dropped property-value assertions.

## Excised for follow-up PRs (Phase 2/3)
7 named artifacts, the Into-the-Void quest + Agralem, GargishOutcast, the Bladeweaver vendor, and the SA loot tables — these were unwired/non-functional (loot never triggered, quest/creature never spawned) and will return properly wired. `StormCaller` also needs its missing Battle Lust, and the quest its correct void-creature target.

## Verification
- Build: 0 warnings / 0 errors.
- `UOContent.Tests`: **501/501** passing (the `ModifyDamage` hook causes zero regressions across all weapons).
- Full whole-branch review completed: no must-fix defects.
2026-07-02 21:06:11 -07:00
Kamron Batman
d7668df5ee
feat(opl): OplTextBlock multi-line tooltip builder + AddChunked (#2507)
## At a glance

```csharp
public override void GetProperties(IPropertyList list)
{
    base.GetProperties(list);

    // Accumulate any number of free-text lines. On dispose the block flushes via
    // AddChunked, splitting across as many OPL properties as needed so none can
    // overflow the legacy 2D client's per-property buffer (which would crash it).
    using var block = list.TextBlock();

    if (luck > 0)
    {
        block.Add($"Luck Bonus: +{luck}%");       // zero-alloc interpolation
    }

    block.Add("Cannot be repaired".AsSpan());      // plain text, no string allocation

    // Already holding a '\n'-joined string? Skip the builder and chunk directly:
    // list.AddChunked(description);
}
```

## What

Adds a safe path for emitting **variable-length, free-form (non-cliloc) tooltip text**:

- **`ObjectPropertyList.Add(ReadOnlySpan<char>)`** overloads — append raw text with no string allocation. Makes the old single-arg `Add(string)` redundant (a `string` binds to the span overload implicitly), so it's dropped.
- **`AddChunked(ReadOnlySpan<char>)`** on `IPropertyList` — splits newline-joined text at `\n` boundaries across as many passthrough-cliloc properties as needed, so no single property exceeds the cap.
- **`OplTextBlock`** (`ref struct`) + **`IPropertyList.TextBlock()`** — an ergonomic builder that accumulates `\n`-joined lines (with a zero-alloc interpolated `Add($"...")` overload) and flushes via `AddChunked` on dispose. Usage: `using var block = list.TextBlock();`.
- **`MaxArgumentLength` (504)** — per-property cap with a hard backstop that clamps + logs anything that slips through.

## Why

The legacy 2D client copies each OPL property's text into a fixed ~512-char (1024-byte) buffer. A single property longer than that smashes an adjacent world object's vtable on the client heap and crashes the client. `AddChunked`/`OplTextBlock` keep multi-line content safely under the cap instead of risking one oversized `Add`.

## Docs

- `dev-docs/property-lists.md` — new "Multi-Line Free Text" deep-dive section; corrected the stale `IPropertyList` listing.
- `dev-docs/claude-skills/modernuo-property-lists.md` — condensed pattern + anti-pattern.

## Tests

9 tests pass (`OplTextBlockTests`, `ObjectPropertyListSpanAddTests`): line joining, empty-line skipping, no-line no-op, zero-alloc interpolation, and long-content chunking staying under `MaxArgumentLength`. Full `UOContent` build is green, confirming dropping `Add(string)` breaks no call sites.
2026-07-02 19:41:36 -07:00
Kamron Batman
f7c44f7c10
refactor(opl): Consolidate AOS attribute OPL emission into per-family GetProperties (#2501)
## Summary

Consolidates the duplicated inline AOS attribute → `ObjectPropertyList` emission that each item base copy-pastes into per-family `GetProperties(IPropertyList)` methods, mirroring the existing `AosSkillBonuses.GetProperties` precedent.

### Per-family `GetProperties(IPropertyList)`
- **`AosAttributes`** — the 24 common attributes in canonical cliloc-ascending order, with optional `damageBonus` / `hitChanceBonus` / `luckBonus` params so item-computed bonuses (e.g. `GetDamageBonus()`) stay out of the family type.
- **`AosWeaponAttributes`** — `UseBestSkill`, the `Hit*` block (1060416–1060430), `MageWeapon` (`30 - prop`), `SelfRepair`.
- **`AosArmorAttributes`** — `MageArmor`, `SelfRepair` (the always-direct members; `LowerStatReq`/`DurabilityBonus` stay inline since they're item-computed in armor but container-direct in clothing).

### Rewired all 6 `AosAttributes`-emitting item bases
`BaseJewel`, `BaseArmor`, `BaseClothing`, `BaseWeapon`, `BaseTalisman`, `Spellbook` now call the family methods instead of inlining the chain. Net: large dedup in `BaseWeapon`/`BaseArmor`/`BaseClothing`/`BaseTalisman`/`Spellbook`.

## Behavior change: tooltip line **order** (set preserved)

This is **not** a pure no-op refactor, and that's unavoidable. Today the families are emitted **interleaved in cliloc order**, and the relative order differs per item class — e.g. `BonusDex` (1060409) is emitted early in `BaseArmor` but **after** the `Hit*` block in `BaseWeapon`. No single emission order reproduces every class byte-for-byte, so consolidating into contiguous per-family blocks necessarily **de-interleaves**: lines regroup **specific → general** (family-specific, then common `AosAttributes`).

- The **set** of emitted `(cliloc, argument)` lines per item is preserved **exactly** — nothing dropped, added, or value-changed.
- Only the **order** of lines within a tooltip changes for `BaseArmor` / `BaseWeapon` / `BaseClothing`. `BaseJewel` / `BaseTalisman` / `Spellbook` were already canonical, so those are byte-identical.

## Tests
- **Golden set-invariance tests** per item base (`BaseArmor/Clothing/Jewel/Weapon/Talisman/Spellbook PropertiesTests`) — each was written to pass against current `main` **before** the rewire (locking the emitted-line set), then confirmed still passing after, proving no line is lost/added/changed.
- Family-level unit tests for each `GetProperties` (canonical order, computed-bonus folding, the `AosArmorAttributes` exclusions).
- `dotnet build` clean; full `UOContent.Tests` green. (Pre-existing `AccountPacket`/`GumpPacket`/`MobilePacket`/`ClientEnumerator` golden-test failures reproduce on unmodified `main` and are unrelated to this change.)
2026-07-02 19:40:54 -07:00
Chuck Thier
0502f98050
feat: Adds Spectral Spellbinder for Old Haven (#2453) 2026-07-02 19:40:11 -07:00
Kamron Batman
a038655541
fix: Bumps dependencies. Adds Server 2012/2016 support. (#2509)
### Summary

* Bumps dependencies
* Bumps IORingGroup to add epoll support and backward compatibility for Server 2012/2016.

Closes #2508
2026-07-02 19:29:34 -07:00
Kamron Batman
a28a32f46d
fix(json): Rectangle3DConverter loses a z-level on write (#2506)
## Summary

`Rectangle3DConverter.Write` corrupts a rectangle's Z range for certain bounds. The omit-z guard was:

```csharp
var writeZ = value.Start.Z is > sbyte.MinValue and < sbyte.MaxValue
          || value.End.Z   is > sbyte.MinValue and < sbyte.MaxValue;
```

This drops `z1`/`z2` whenever **both** Z bounds sit at/outside the sbyte extremes — but `Read` reconstructs absent Z as exactly `z1 = -128, z2 = 127` (depth 255). So any rectangle that trips the omit condition without *being* that sentinel round-trips to depth 255 and is corrupted.

The concrete case: a **homeRange-style** bound `Start.Z = -128, End.Z = 128` (depth 256, the full vertical range used by spawners) gets `z1`/`z2` omitted on write, then reads back as depth **255** — silently losing the top z-level on every re-serialize.

(Spotted while working on #2505; spawners now prefer the `homeRange` form so most square bounds avoid this path, but any non-square `spawnBounds` or other `Rectangle3D` JSON is affected.)

## Fix

Omit Z only for the exact sentinel `Read` produces (`z1 == -128 && z2 == 127`); write it for anything else:

```csharp
var writeZ = value.Start.Z != sbyte.MinValue || value.End.Z != sbyte.MaxValue;
```

`Read` is unchanged, so existing `regions.json` rectangles that omit Z keep loading identically.

## Tests

New `Rectangle3DConverterTests`: round-trips the homeRange depth-256 case, the depth-255 sentinel, and ordinary/edge z values; asserts the sentinel omits Z while homeRange bounds write it. Server.Tests 717/717, UOContent.Tests 487/487.
2026-06-25 23:42:49 -07:00
Kamron Batman
d8a64f3316
refactor(spawners): replace DynamicJson with typed SpawnerDto records (#2505)
## Summary

Removes the dated `DynamicJson` JSON helper and migrates spawner JSON (de)serialization to a polymorphic `record SpawnerDto` hierarchy. `DynamicJson` was the last remaining consumer (regions moved off it in #1400).

The key correctness improvement: **System.Text.Json deserializes plain DTO records, never a live `Item`.** Previously, deserializing directly into an `Item` meant STJ constructed world-registered objects *before* the data was validated — a malformed/hand-edited spawn file could leave orphaned spawner Items in the world save. Now a parse failure is GC-only; `dto.ToSpawner()` constructs the spawner only from a fully-validated DTO and self-cleans on failure.

## What changed

- **New:** `SpawnerDto` (abstract) + `SpawnerDataDto` / `RegionSpawnerDto` / `ProximitySpawnerDto`, each marked with a reusable `[JsonDiscoverableType]` opt-in attribute. Auto-discovered at the `Configure` phase — no manual registration list (avoids the regions `Register<T>()` footgun), open to custom spawner subtypes.
- **Symmetric mapping:** `BaseSpawner.ToDto()` (export) ⇄ `SpawnerDto.ToSpawner()` (import). `ToSpawner()` deletes-and-rethrows on any failure, so the importer can never orphan an Item.
- **`SpawnerJsonSerializer`** wires `$type` polymorphism on the `SpawnerDto` root with loud collision/constructibility validation.
- **Import/export commands** rewired to the typed DTO path (reflection `FindTypeByName`/`CreateInstance` removed).
- **Data migration:** the 109 `Distribution/Data/Spawns/**` files moved from `"type"`→`"$type"` and legacy `homeRange`→`spawnBounds`. The runtime still *reads* legacy `homeRange` for external files. The `homeRange→spawnBounds` formula is proven equivalent to the runtime conversion (`BoundsEquivalenceTests`, hr=0/1/3/7).
- **Deleted:** `Projects/Server/Json/DynamicJson.cs`.

Sparse export output matches the legacy `ToJson` (nullable DTO properties + `WhenWritingNull`). Binary world-save serialization is untouched.

## Tests

- DTO round-trip per spawner type; sparse-default omission; legacy `homeRange` read; export/import file round-trip.
- `Import_MalformedFile_LeaksNoWorldItems` — proves a mid-array parse failure constructs zero world Items.
- `AllSpawnFilesLoadTests` — every migrated spawn file deserializes and builds.
- Duplicate-discriminator validation.
- UOContent.Tests 485/485, Server.Tests 710/710, build clean.

## Follow-up (not in this PR)

`Rectangle3DConverter.Write` (in `Projects/Server/`) omits `z1/z2` when `Start.Z == -128`, so a future server-side export of a `homeRange`-style spawner round-trips depth 256→255. Pre-existing and out of scope here (Server change); the migrated data reads correctly. Worth a separate small converter PR.
2026-06-25 23:24:45 -07:00
dependabot[bot]
a26219c837
chore(deps): bump actions/checkout from 6 to 7 (#2503)
Bumps [actions/checkout](https://github.com/actions/checkout) from 6 to 7.
- [Release notes](https://github.com/actions/checkout/releases)
- [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md)
- [Commits](https://github.com/actions/checkout/compare/v6...v7)

---
updated-dependencies:
- dependency-name: actions/checkout
  dependency-version: '7'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-06-23 19:26:28 -07:00
Kamron Batman
73d1ee3874
fix(network): don't leak duped-layer equipment via EquipUpdate/OPL (#2502)
## Problem

A creature equipped with two items that resolve to the **same layer** can crash the legacy EA 2D client (use-after-free). Equipment `Layer` comes from tiledata (`Layer = (Layer)ItemData.Quality`), so a **two-handed weapon and a shield both resolve to `Layer.TwoHanded`**. `Mobile.FindItemOnLayer` even documents the invariant: *"We only allow 1 item per layer. Its an implicit contract."*

## Root cause

- `SendMobileIncoming` (0x78) **dedupes by layer** (the `layers` span) and sends only the first item per slot — so a static creature is fine.
- But the per-item **`SendEquipUpdate` (0x2E)** and the item **OPL** sends do **not** dedupe. On any equip/property delta (`Item.ProcessDelta`) they fire per item and leak the second same-layer item on its own.
- The client then holds two items on one equipment slot; when that slot is torn down (e.g. a large group of such creatures and the player runs out of range → mass remove) the legacy 2D client frees one and dereferences it → UAF. ClassicUO bounds-checks and is unaffected, but the server is emitting an invalid, self-contradictory stream either way.

## Fix

Make per-item equip/OPL sends honor the same first-item-per-layer rule `SendMobileIncoming` already uses:

- `Item.IsDupedEquipLayer()` — `m_Parent is Mobile m && m.FindItemOnLayer(m_Layer) != this` (reuses the existing helper; true when an earlier item already holds this items layer).
- `Item.ProcessDelta`: early-return before the per-client loop for a duped equipped item (skips the EquipUpdate and OPL to everyone).
- `Mobile` lift-reject re-show: skip the EquipUpdate and OPL for the dupe.

No behavioral change for valid equipment (distinct layers → never duped). For the invalid duped-layer case the second item was already omitted by the 0x78 packet; this just stops it leaking back via the per-item paths.
2026-06-22 23:20:57 -07:00
Kamron Batman
70276dcf52
fix(housing): allow non-staff to place classic house pieces in customization (#2500)
## Summary

House customization rejected ~40% of components — all classic/base tiles such as **sandstone** — for non-staff players. The pieces appeared briefly in the editor, then vanished before commit; only staff (GM+) could add them.

## Root cause

A data-convention mismatch introduced when housing.bin support was added (#2329).

- The OSI `housing.bin` encodes pre-AOS base pieces with the client **T2A** feature bit (`0x1`).
- `walls.txt` (and RunUO) encode the same pieces as `FeatureMask = 0` (always valid).
- `ComponentVerification.CheckValidity` validates against `ExpansionInfo.HousingFlags`, whose enum has no `0x1` bit, so base pieces failed `(HousingFlags & 0x1) != 0`.
- `HouseFoundation.Designer_Build` only enforces `ValidPiece` for `AccessLevel < GameMaster`, so staff bypassed validation while players had placements rejected — the server re-sends the design state and the client rebuilds the house from it, erasing the just-placed piece.

This only affects servers loading `housing.bin` from a UOP client; the old txt-only path (pre-#2329, like RunUO) was unaffected because base pieces are `0` there.

## Fix

Normalize the `housing.bin` feature mask to the housing-tier bits on load (`& HousingFlags.HousingEJ`). Base pieces collapse to `0` (always valid, exactly as `walls.txt` encodes them); `AOS`/`SE`/`ML`/... pass through unchanged. Both data sources now produce an identical validity table, matching RunUO behavior. `CheckValidity` and the `val != -1` anti-cheat guard are unchanged.

## Verification

- Parsed the real `housing.bin` from a 7.0.x client: sandstone (`0x345`) loads as `0x1` → `& HousingEJ` → `0` → valid; tier pieces (`0x40` SE, etc.) pass through; unregistered tiles stay `-1` → rejected.
- Confirmed OSI's own files disagree for the same pieces: `walls.txt` base `FeatureMask = 0` vs `housing.bin` `0x1`.
- `dotnet build` clean (0 warnings, 0 errors).
2026-06-22 08:47:10 -07:00
Kamron Batman
c67a3cd339
fix: Fixes Addon Generator script (#2499) 2026-06-21 23:24:29 -07:00
Kamron Batman
a7bb8a8222
refactor(archery): centralize SE ammo auto-recovery off PlayerMobile (#2496)
## Summary

Streamlines the SE-era archery ammo auto-recovery (recovering spent arrows/bolts after a miss). The mechanic was previously spread across four unrelated trigger points and a weapon-scoped timer that was divorced from the recovery state living on `PlayerMobile`. It also contained dead code.

### Problems fixed
- **Dead `!Warmode` gate** — `OnMiss` only runs from `OnSwing`, which requires warmode to fire, so the `if (!pm.Warmode)` branch that started the recovery timer could never trigger.
- **Scattered, divorced state** — banked ammo lived on `PlayerMobile.RecoverableAmmo` while the timer lived on the weapon (`_recoveryTimerToken`), and recovery was kicked off from four different places (`OnWarmodeChanged`, `PlayerMobile.OnDamage` kill, `BaseCreature.OnDamage` kill, `OnBeforeDeath`).
- **Per-player footprint** — every `PlayerMobile` carried a `RecoverableAmmo` field even though ~99% never miss with a bow (and most are offline).

### New design — `AmmoRecovery` side table
- All state (banked ammo + one repeating timer) is keyed by player in a static dictionary, so only players who actually miss carry any state. Transient by design — this was never serialized.
- **One feed point:** `OnMiss` banks the spent ammo type and starts the player's timer.
- **One drain point:** the timer self-gates and gathers ammo into the backpack only once the archer has disengaged — **alive, out of warmode, and not running** — otherwise it retries next tick, so banked ammo is never lost while online.
- **"Not running" allows standing still _or_ walking.** The `Direction.Running` bit is stale after a player stops, so it's paired with movement recency (`LastMoveTime`); only an *actively* running archer is blocked.
- Removed the redundant scattered triggers, the dead `!Warmode` branch, `RecoverableAmmo`, `RecoverAmmo()`, and the now-empty `OnWarmodeChanged` override. `PlayerMobile.OnDelete` calls `AmmoRecovery.Forget`.

### Behavior notes
- On death, banked ammo is **no longer flushed to the corpse** — it stays banked and is recovered after resurrection once the archer settles (player keeps it rather than dropping it to looters).
- `OnHit` immediate recovery (the ~40% arrow-to-defender behavior) is **unchanged**.

## Test plan
- [x] `dotnet build Projects/UOContent/UOContent.csproj -c Release` — succeeds, 0 warnings, 0 errors.
- [ ] In-game (SE era): miss bow shots, then disengage (drop warmode + stop) and confirm the "You recover N arrows/bolts" message and backpack contents; confirm recovery does **not** fire while running and **does** while walking/standing.
2026-06-21 23:22:55 -07:00
Kamron Batman
d2df7f7839
feat(build-tool): add application icon and refresh MUO.ico (#2487)
## What

- **BuildTool now has a distinct application icon** — the MUO mark with a three-gear "settings" cluster in the bottom-right, colored by size (azure / steel / teal), wired in via `<ApplicationIcon>` in `BuildTool.csproj`. This differentiates the (now signed) build tool from the server in the taskbar/Explorer.
- **Refreshed `Projects/Application/MUO.ico`** — re-rendered from vector with the full Windows size ladder (16/32/48/64/128/256). The previous icon only carried 128/256 frames, so Windows had nothing proper for small sizes.
- **Added `branding/`** — the source SVGs (`muo.svg`, `gears.svg`, `build-tool.svg`) the `.ico` files are rasterized from.

## How

Both icons are rasterized from the branding SVGs (high-density render → Lanczos downscale per frame → packed into a 6-frame `.ico`). The rasterization script and its node deps are kept local under the gitignored `tools/` dir and intentionally not committed — `branding/` is the source of truth.

## Verification

- `dotnet build Projects/BuildTool/BuildTool.csproj` succeeds (0 warnings/errors).
- The embedded icon resource extracts from the produced `build-tool.exe`.
- Both `.ico`s validate as 6-frame Windows icons.

### Icons

<img width="150" height="150" alt="build-tool" src="https://github.com/user-attachments/assets/9c443947-4c26-46a3-ad9b-5f50630d90ad" />
2026-06-14 11:59:54 -07:00
Kamron Batman
7eb1b71a5b
chore: Update workflow actions to Node 24 runtime + watch github-actions (#2486)
## Summary

Audit of CI/CD workflows (`.github/workflows/**`, `azure-pipelines.yml`) for outdated actions, focused on the Node 20 → Node 24 runner deprecation. Most actions were already migrated; this PR cleans up the remaining stragglers and closes the gap that let them drift.

## Changes

| Action | File(s) | From | To | Reason |
|---|---|---|---|---|
| `softprops/action-gh-release` | `create-release.yml`, `build-tool-release.yml` | `v2` | `v3` | v2 still runs Node 20; v3 is a pure Node 24 runtime move, inputs unchanged (drop-in) |
| `dotnet/nbgv` | `create-release.yml` | `v0.5.1` | `v0.5.2` | Node 24 runtime bump |
| `SethCohen/github-releases-to-discord` | `post-release-discord.yml` | `v1.19.0` | `v1.20.0` | Latest; adds manual-dispatch test support |
| Dependabot | `dependabot.yml` | nuget only | + `github-actions` (weekly) | Auto-PR future action bumps instead of manual audits |

## Already current (no change)

`actions/checkout@v6`, `actions/setup-dotnet@v5`, `actions/upload-artifact@v7`, `actions/download-artifact@v8`, and `signpath/...@v2` are all on current majors running the Node 24 runtime. The Azure tasks (`UseDotNet@2`, `NuGetAuthenticate@1`) are current as well.

## Notes

- All target versions verified against GitHub's release API.
- `action-gh-release@v3` release notes confirm it's a runtime-only change with no input/behavior changes — safe drop-in for both usages.
2026-06-14 09:48:57 -07:00
Kamron Batman
1c2e114b21
feat(pathfinding): multi-aware mask synthesizer + warm interior cache for house/boat cells (#2479)
## Problem

Houses and boats (multis) were pathed correctly only by **delegation to the slow path**: `StepCache.TryGetMask` returns `Fallthrough_Multi` for any multi-covered cell, and `GetSuccessors` ran `CheckMovement` **8× per cell** (each re-resolving the tile stack via `GetStaticAndMultiTiles`) — a sustained per-step cost near every house/boat. There was also no automated test pinning multi pathfinding.

This branch is the full multi-pathfinding effort in phases on one branch.

## Phase 1 — characterization tests (the oracle)

Implementation-agnostic invariants: a cache-on≡cache-off whole-path invariant, a per-cell sweep vs `CheckMovement` over footprint+halo (incl. destination Z), hand-verified routing (around walls, demolish-reopens, foundation-redesign-honored), classic-house / foundation / boat fixtures, non-vacuity guards. These gate every later phase byte-for-byte.

## Phase 2 — live single-pass synthesizer

`StepProbe.ComputeMultiMaskAt` synthesizes a covered cell's full 8-direction `StepMask` in one pass (the existing surface/step logic over `GetStaticAndMultiTiles` instead of 8× `CheckMovement`). `GetSuccessors` routes `Fallthrough_Multi` cells through it. No new cache, no `.swb` change. **~1.5×**, zero added allocations.

## Phase 3 / 3.1 — warm per-`multiID` interior cache (airtight)

`MultiMaskCache` caches each fixed multi's local-frame `StepMask` for **interior** cells (cell + all 8 neighbours covered → terrain-neighbour-free → position-invariant), keyed by `multiID & 0x3FFF`, built lazily from the MCL. Interior cells become ~20 ns lookups.

The cache is gated on a **per-instance footprint-clean flag** (`BaseMulti.PathInteriorCacheState`): an instance whose whole footprint terrain is below its floor (`maxTerrain < minFloor`) serves from the cache; a **dirty** instance (terrain intrudes — a contrived/GM placement) **degrades to live-synth, never a wrong mask**. This closes a cross-instance soundness gap (the cached mask depends on neighbour terrain too) found in a holistic review. The gate resets whenever the footprint's world-terrain relationship can change — **location, map, or ItemID** (a boat's heading swaps the MCL).

**Boats are cached too.** Their per-`multiID` deck masks are movement-invariant (built once per heading), so a sailing boat never rebuilds them; only the cheap clean-flag rescan repeats per move (and only when pathed near). Narrow existing boats have little interior; wide galleons (`multi.mul`) would gain Castle-class. `HouseFoundation` (per-instance runtime `DesignState`) is the one type that stays on the live path.

## Verification

- `UOContent.Tests` **454/454**, `Server.Tests` **708/708**, 0 failures.
- The Phase-1 oracle (`MultiPathInvariantTests`, cache-on ≡ cache-off) stays **byte-identical** with the synthesizer + interior cache active.
- Tests pin: footprint-cleanliness (clean vs sunk), dirty/cluttered placement degrades to live-synth while still pathing, clean placement serves, and the gate resets on move/ItemID change.

## Performance (modernuo/ModernUO-Benchmarks#8, full-fixture)

Houses at **Green Acres** (flat staff region → clean footprints, the legit-placement case):

| Route | Slow path | Phase 3.1 (interior cache) | Speedup |
|-------|----------:|---------------------------:|--------:|
| `around_a` (29 steps) | 238.3 µs | **49.1 µs** | **4.85×** |
| `around_b` (29 steps) | 224.3 µs | **49.5 µs** | **4.53×** |

~130 of ~167 multi cells/route serve from the cache (~20 ns) vs 37 live-synth. Per-cell, the slow path's 8× `CheckMovement` grows with multi complexity (GuildHouse ~857 ns → Castle ~1,194 ns), the synthesizer is a flat ~780 ns, and the cache serve is ~20 ns — so big/tall multis (and wide galleons) gain most. Identical allocations throughout.
2026-06-09 08:02:05 -07:00
Marcelo Paez Sequeira
92a540b2d0
feat(server): add Mobile.SayTo localized overload with explicit hue (#2481)
## Summary

`Mobile.SayTo(Mobile to, int number, string args = "")` always sends the localized message using `SpeechHue`. This adds a parallel overload that accepts an explicit `hue`:

```csharp
public void SayTo(Mobile to, int number, int hue, string args = "") =>
    to.NetState.SendMessageLocalized(Serial, Body, MessageType.Regular, hue, 3, number, Name, args);
```

It mirrors the existing localized overload exactly, only substituting the caller-provided `hue` for `SpeechHue`, so content can send a cliloc message to a single mobile in a chosen color without dropping down to `NetState.SendMessageLocalized` directly. This restores the hued-cliloc `SayTo` that RunUO/ServUO content commonly relied on (e.g. `SayTo(from, 1042205, 0x3B2)`).

## Notes
- Purely additive; no behavior change to existing call sites.
- No overload ambiguity: `SayTo(m, num)` and `SayTo(m, num, "args")` still bind to the existing overload; `SayTo(m, num, hue)` binds to the new one (the third positional arg is `int` vs `string`).
- Null-safe to the same degree as the existing overload (`SendMessageLocalized` guards via `CannotSendPackets()`).

## Test Plan
- [x] `dotnet build Projects/Server` — 0 warnings, 0 errors.
- One-line additive overload mirroring an existing (untested) method; no existing `Mobile.SayTo` unit tests to extend. Happy to add coverage if preferred.
2026-06-09 08:01:03 -07:00
Kamron Batman
10fb74b827
fix(necromancy): correct Blood Oath duration, reflection, and expiry timing (#1690) (#2480)
Fixes #1690. Addresses Blood Oath holistically — three bugs found while researching the spell against RunUO, ServUO, the UODemise/uo.com guides, and the archived UOGuide page.

## Bugs fixed

### 1. Expiry timing (the filed issue)
The `ExpireTimer` polled every 1s, so expiry and death/delete cleanup lagged up to ~1s. Replaced with a **single-shot** timer plus centralized `[OnEvent]` handlers on `PlayerDeathEvent`/`PlayerDeletedEvent`/`CreatureDeathEvent`/`CreatureDeletedEvent` — the oath now breaks immediately on death/delete of either party.

### 2. Duration formula
Used `/80` (the bugged in-game tooltip value) instead of the real OSI formula `((SpiritSpeak - Resist) / 8) + 8`. Confirmed by RunUO, ServUO, the emulator guides, and the code's own fixed-point comment. At GM Spirit Speak this changes duration from ~9.5s to 23s and makes Spirit Speak actually affect duration.

### 3. Damage reflection (`BaseCreature.Damage` vs `PlayerMobile.Damage`)
`BaseCreature.Damage` diverged: it attributed the reflected hit to the attacker itself (`from.Damage(amount, from)`) instead of the caster, reflected the bonused (not original) amount, used `×1.1` vs `×1.2`, lacked the caster-survival guard, and had no Publish 48 resist mitigation.

Unified both paths: reflect the **original** damage attributed to the **caster** at `×1.2`. Publish 48 resist mitigation now applies only to creature casters and is gated behind `Core.SA`.

## Internals
- Collapsed the parallel `_oathTable` into a single `_table` keyed by both participants → shared timer, so `RemoveCurse` resolves from either side (required by the event handlers).
- Extracted `GetDurationSeconds` and `ComputeReflectedDamage` as testable statics.

## Tests
13 new tests (duration formula, reflection mitigation, oath lifecycle, end-to-end event-driven removal). Full suite: **436/436 pass**.
2026-06-08 23:42:28 -07:00
Kamron Batman
9d26a44a28
fix: Fixes pathfinding prebake and pathfinding multi-fallthrough. (#2478)
## Summary

Fixes the pathfinding step-cache (`.swb`) prebake so it bakes **once** and skips when a valid cache already exists, instead of re-baking on every boot. The root cause was the staleness fingerprint hashing mutable in-memory tile data rather than the on-disk files. This PR makes the fingerprint a pure function of the client data files and separates dynamic multis (houses/boats) from the static cache.

> This branch builds on the `ConfigurePrompts` first-boot-prompt unification (commit `5df8d0bd`, also included here) — that commit accounts for the `ServerConfiguration.cs` and `dev-docs/server-lifecycle.md` changes in the diff.

## The bug

With `pathfinding.prebakeMaps` set, the cache re-baked on **every** boot. The `.swb` staleness fingerprint hashed the live `TileData.LandTable`/`ItemTable` flags, which the server patches at runtime (`ItemFixes`, `LOSBlocker`, `PotionKeg`, `CTF`) at nondeterministic lifecycle points (Initialize-phase methods share a priority; static ctors fire lazily). So a fingerprint stamped at runtime (`[PathBake`) never matched the one recomputed during startup `Initialize()`, and the cache rebaked every time.

## Changes

**1. Fingerprint the files, not the in-memory tables** (`fix`)
Hash `tiledata.mul` (cached, computed once) plus the per-map `.mul`/`.uop` files — never the runtime-mutated `TileData` tables. The fingerprint is now lifecycle-stable. Existing `.swb` files rebake once after deploy, then stay stable.

**2. Compute the fingerprint once per boot** (`refactor`)
`Configure()`'s `AutoLoadAtStartup()` already opens and fingerprint-validates a reader for every up-to-date `.swb`. `Initialize()` now skips baking any map that already has an open reader (`StepCache.HasLazyReader`) instead of recomputing the fingerprint a second time.

**3. Bake static-only; route multis to the live path** (`refactor`)
Multis (houses/boats) are dynamic, so they're no longer baked into the static chunk cache — they were tagged with `BuiltMultisVersion`, a non-persisted session counter, which made persisting them unsafe (false matches / wasted re-bakes).
- Chunks bake land + `statics.mul` only.
- At query time, any cell whose sector (or its 1-cell halo) contains a multi routes to `Fallthrough_Multi` → the existing live, multi-aware `CheckMovement` path. The halo prevents a cell proposing a walkable edge into a neighbouring wall; interior (multi-free) cells pay one sector lookup.
- Adds `Sector.HasMultis` (one engine accessor); `.swb` format → v9 (rejects old multi-baked files); new `Fallthrough_Multi` telemetry.
- Behaviour-preserving: multis use the same live path the engine used before the cache existed.

**4. Comment polish** (`style`) — no behaviour change.

## Testing

All green: **92** pathfinding (incl. a new fingerprint-stability test and a multi-halo fallthrough test), **423** UOContent, **708** Server.

## Follow-ups (not in this PR)

- **Background bake worker** — make `[PathBake` and the boot prebake non-blocking (game thread serves tile reads to an off-thread worker).
- **Per-multi MCL cache** — cache walkability in each multi's own frame (keyed by multiID, movement-invariant) so houses/boats get a fast path instead of the live fallback.
- **House-pathfinding equivalence tests** — the one area not yet covered by a dedicated automated test; multi pathing is currently correct by delegation to the live path.
2026-06-08 11:59:24 -07:00
Kamron Batman
eec37edd67
refactor(server): unify first-boot prompts into the ConfigurePrompts phase (#2477)
Stacked on #2475 (the `ConfigurePrompts` phase). Base will switch to `main` once #2475 merges.

## What

Move the engine's own first-boot prompts — data directories, listeners, server name, expansion + map selection — out of `ServerConfiguration.Load` and into **`ServerConfiguration.ConfigurePrompts()`** (`[CallPriority(0)]`), so **all** first-boot prompting (engine and content) runs through the single `AssemblyHandler.Invoke("ConfigurePrompts")` phase. `Load` now only reads/creates the config file.

## Why it's safe

- **Assembly loading uses `AssemblyDirectories` (default `./Assemblies`), not `DataDirectories`** — so assemblies load fine before the now-later data-dir prompt. This is the linchpin that makes the move possible.
- **`UOClient.Load()`** (client-file discovery via `Core.FindDataFile`) needs `DataDirectories`, so it moved *with* the data-dir prompt into `ConfigurePrompts`.
- **`Core.Expansion`** is now assigned in `ConfigurePrompts` (every non-mocked boot). Nothing between `LoadAssemblies` and that phase reads it — type initializers run lazily on first use, not during `LoadAssemblies`.
- **`[CallPriority(0)]`** keeps the engine prompts (including map selection) ahead of content prompts such as the pathfinding pre-bake (priority 50), preserving "after map selection".
- `Main.cs` already invokes the phase — **no startup-ordering edit** here.

## Tests

`Server.Tests` **708/708**, `UOContent.Tests` **418/418**, build clean. Fixtures are unaffected: they call `Load(true)` (now just reads config) and set expansion/data dirs directly; `ConfigurePrompts` is gated on `m_Mocked`.

## ⚠️ Needs first-boot runtime verification

`Main.cs` startup ordering is **not** covered by the fixture-based suite (the fixtures bypass `Main`). Please boot once with a fresh `modernuo.json` to confirm the first-boot prompt sequence (data dirs → … → expansion/maps → pathfinding pre-bake) and that `Core.Expansion` resolves correctly. Docs updated in `dev-docs/server-lifecycle.md`.
2026-06-08 05:18:46 -07:00
Kamron Batman
16bf3016fb
feat: Pre-Publish 14 Crafting (supersedes #2181, #2381) (#2476)
## Summary

Adds authentic **T2A-era (pre-UO:Third-Dawn) packet-based crafting menus**, enabled via the **`t2aCraftMenus` server setting** (read once at startup; default **`!Core.UOTD`**, so a pre-UO:TD shard gets them automatically). When enabled, double-clicking a crafting tool opens the classic `0x7C`/`0x7D` item-list menu — skill- and material-filtered — instead of the modern gump, covering all 8 tool/skill crafts (blacksmithy, tailoring, tinkering, carpentry, alchemy, bowcraft/fletching, inscription, cartography). It is **not** a runtime/admin-flippable feature flag.

This is the **definitive, reconciled** branch and **supersedes**:
- **#2181** (Delphi — `T2A_CraftingMenus`): the original effort.
- **#2381** (Jack/UOLL — `t2a_crafting_menus`): the research-grounded superset (Delphi's base + 12 corrections), rebased onto current `main`.

Original authorship is preserved across the cherry-picked history: foundation commit **@Delphi79**, mechanic fixes **@jackuoll (Jack Ward)**, reconciliation/fixes/docs mine.

## How it was built

1. Cherry-picked Jack's 13 commits onto current `main` (superset of Delphi's; only 2 trivial FeatureFlags conflicts).
2. Applied targeted fixes (below) with tests.
3. Full convention audit, build, and test pass.

Grounded in independent historical research plus Jack's deep dive. Maintainer reference: `dev-docs/t2a-crafting.md`.

## Mechanics (highlights)

- Double-click tool → target resource → skill/material-filtered menu → craft. Resource pre-selection per skill; make-last by targeting the tool.
- **Stacked-gem jewelry:** target a gem stack → the **full stack** is consumed and the piece is named by count ("a 1000 diamond ring"); count persists (`BaseJewel` serialization **v4 → v5**, new `_gemCount`).
- **Tool-less inscription & cartography** (skill-list invoked; no pen/sextant); inscription consumes reagents+scroll on success and failure, mana only on success.
- **Tailoring matching-hue consumption:** targeting hued cloth/leather consumes only that hue. Crafted items take color from their **`CraftResource`** (not the dyed hue), so dyed leather/cloth don't tint the product; in T2A only colored ingots/ore color items (metal armor/shields).
- **Half-resources on failed non-scroll crafts** (pre-UO:TD).
- **Maker's mark** always prompted for exceptional items, via the shared `QueryMakersMarkGump`.
- Server-side menu infra changes are additive (`ItemListEntry.CraftIndex`, `Entries` setter, `HasSent`).

## Notable changes on top of the cherry-pick

- **Toggle is a startup server setting, not a feature flag.** Removed `ContentFeatureFlags.T2ACraftMenus` (and its admin-flippable plumbing); the value is read once via `ServerConfiguration.GetSetting("t2aCraftMenus", !Core.UOTD)` into `T2ACraftSystem.Enabled`. Since the default tracks the era and it can't be flipped at runtime, there's no incoherent "menus-on / UO:TD-era" state.
- **Stacked-gem consumption (B3a/B3):** consume the full `PendingGemCount` (was deliberately consuming 1 while naming by the stack), null-safe gem type, plain-piece fallback + message. New `T2AJewelGemCraftTests`.
- **Convention audit:** `new List<Item>()` → `PooledRefList<Item>` on the hue-aware consume path; removed dead code.

## Decisions & deviations

- `make-last` kept as **QoL** (post-T2A gump-era feature).
- `half-on-failure` (non-scroll) kept as a **reconstruction** (not OSI-confirmed).
- **Stacked-gem** behavior set per shard authority (overrides the "single gem" reconstruction).
- **Cooking** out of scope (no T2A crafting menu existed for it).
- **No colored items from dyed materials:** crafted color comes from the `CraftResource` type. Pre-AOS leather has no colored variant, so leather is always uncolored; weapons retain resource color only in AOS+ (unchanged, intended).

## Test plan

- Automated: `dotnet build ModernUO.slnx -c Debug` clean; `dotnet test Projects/UOContent.Tests` → **421 passed** (incl. 3 new jewelry tests).
- Manual (needs a running T2A shard + client):
  - [ ] Each of the 8 skills opens the correct menu; empty-menu guard fires.
  - [ ] Make-last repeats the last craft (jewelry re-prompts gem).
  - [ ] Jewelry consumes the full targeted gem stack and names by count.
  - [ ] Cartography consumes blank maps only with T2A enabled / maps+scrolls when disabled.
  - [ ] Tailoring consumes only the targeted-hue material; crafted items are not tinted by dyed cloth/leather.
  - [ ] Maker's-mark prompt on exceptional.
  - [ ] Failed non-scroll craft consumes half resources.
  - [ ] Inscription: reagents+scroll on success/failure, mana only on success.
  - [ ] T2A disabled: gump crafting unchanged.

## Credits

Co-authored-by: @Delphi79
Co-authored-by: @jackuoll
2026-06-07 20:27:22 -07:00
Kamron Batman
2e93201e51
feat(pathfinding): first-boot prompt to pre-bake the .swb map cache (#2475)
## What

On **first boot** (right after map selection), offer to pre-bake the pathfinding `.swb` cache for the selected maps. This removes first-pathfind-after-boot latency and is now cheap — ~18 MB/facet after the v8 format work (the old ~565 MB is gone). The answer persists in `modernuo.json` as **`pathfinding.prebakeMaps`** (default **false**): asked exactly once, and skipped on headless/CI boots (redirected input) where operators can set the flag directly.

## How — a generic startup phase, not pathfinding hardcoded in the engine

The clean-console (pre-Serilog) prompt window is inside the engine startup, but UOContent isn't loaded until after `ServerConfiguration.Load`. So rather than coupling the engine to pathfinding, this adds a generic lifecycle phase:

- **`Main.cs`**: new `AssemblyHandler.Invoke("ConfigurePrompts")` — runs **after** `LoadAssemblies` (so content can participate) but **before** the first `logger.Information` (so console prompts aren't interleaved with the async console sink). The first log line moves below it. Any class can hook in with `public static void ConfigurePrompts()` and self-gate on first-boot state. No `ServerConfiguration` or pathfinding coupling added to the engine.
- **`PathCacheCommands.ConfigurePrompts()`**: the first-boot prompt (interactive-only, flag-absent-only); persists the answer.
- **`PathCacheCommands.Initialize()`** (`Invoke("Initialize")` phase, after the tile matrix loads — which the bake walks): when the flag is set, bakes any map whose `.swb` is **missing or stale** (tile-data fingerprint mismatch, via `StepCache.ComputeLiveFingerprint` / `TryReadFingerprintFromFile`). A fresh cache is a no-op, so only the first boot — or a post-client-update boot — pays the several-minute cost.

## Docs

Fixed the now-stale "~565 MB / ~1.5–2 GB / do not bake by default" section in `dev-docs/pathfinding.md` (it's 17.9 MB for Trammel, tens of MB for all six facets after v8), added a "First-boot pre-bake prompt" section, and added the `pathfinding.prebakeMaps` lever row.

## Verified

- `dotnet build UOContent -c Release` → 0 errors (rebased on #2474).
- Pathfinding/StepCache tests: **90/90 pass**.
- Bootstrap streamlining of the startup phases is intentionally left as a follow-up.
2026-06-07 16:30:33 -07:00
Kamron Batman
30fec7da26
fix: Cleans up AI Pathfinding code to make it more portable for custom requirements. (#2474) 2026-06-07 13:25:10 -07:00
Kamron Batman
412a71dfe0
fix(tests): single shared bootstrap; idempotent SerializationThreadWorker.Exit (#2473)
## Problem

Running the full `UOContent.Tests` suite, the test host **hangs ~2.5 minutes at shutdown and then crashes** (`Test host process crashed` / run aborted). The tests themselves are fine — they complete in ~1s — but the process can't exit.

Captured via `--blame-hang` dump. The blocking thread:

```
System.Threading.WaitHandle.WaitOne()
Server.SerializationThreadWorker.Sleep()        SerializationThreadWorker.cs:54  (_stopEvent.WaitOne())
Server.SerializationThreadWorker.Exit()         SerializationThreadWorker.cs:61
Server.World.ExitSerializationThreads()         World.cs:429
Server.Tests.UOContentFixture..ctor()
```

### Root cause

Both collection fixtures (`UOContentFixture` and `PathfindingTestFixture`) each run the **full process-global ModernUO bootstrap**. `World.Load()` is guarded to run once per process, so the **second** fixture's `World.Load()` is a no-op and does **not** respawn the serialization workers — but `World.ExitSerializationThreads()` is **not** guarded, so the second fixture calls `Exit()` on workers whose threads have already terminated. `Exit()` → `Sleep()` → `_stopEvent.WaitOne()` then blocks forever (a dead thread never sets the event). The first collection's tests run; the second collection's fixture deadlocks in its constructor; the host eventually gets killed.

This is why single-collection (filtered) runs were fine — only one fixture ever bootstraps — but the full suite hangs. It's not a parallelization race: even strictly sequential, the second fixture deadlocks.

## Fix

**(a) Engine — idempotent `SerializationThreadWorker.Exit()`**
A second `Exit()` is now a safe no-op instead of a permanent block. Only the owning (main) thread calls `Exit()`, so no synchronization is needed, and the single-call production shutdown path is unchanged.

**(b) Tests — one shared bootstrap, strictly sequential collections**
- New `TestServerBootstrap.EnsureInitialized()` runs the superset global init **exactly once per process** (lock + once-flag).
- `UOContentFixture` / `PathfindingTestFixture` slim down to delegate to it and no longer tear down global state (which the single-bootstrap model owns for the host's lifetime).
- `[assembly: CollectionBehavior(DisableTestParallelization = true)]` so collections never overlap.

## Result

| | Before | After |
|---|---|---|
| Tests run (full suite) | 258 (UOContent collection deadlocked) | **418** |
| Outcome | 2.5-min hang → host crash | **418 passed, clean exit** |
| Wall time | killed | **~7s** |
2026-06-07 12:36:37 -07:00
Kamron Batman
1c1fd4d930
fix: Bumps dependencies (#2472) 2026-06-07 01:25:21 -07:00
Kamron Batman
346228fa69
fix(ai): pet order/home refactor — stop & post-combat behavior (#2459)
## Summary

Fixes two related pet-behavior bugs and the underlying design flaw behind both:

1. **Post-combat erratic** — after a pet killed its `all kill` target it milled around erratically at the kill site (or failed to return to the master) until the player issued `all follow`/`all stop`.
2. **`all stop` returns home** — a pet with a non-zero `Home` walked back toward that location on `all stop` (on ML; on non-ML the old `DoOrderStop` was a no-op, so the sighting there came from a residual `Stay`).

### Root cause

`ControlOrder` and the wild-creature `Home` field were overloaded to express several distinct ideas, mutated/read inconsistently across order transitions:

- `Home` doubled as the controlled-pet "stay anchor" (`HandleStayOrder` set `Home = Location`) but nothing cleared it when the pet left the staying state; `HandleStopOrder` was the only handler that never touched it.
- `DoOrderStop` had dropped RunUO's `Home = Location` re-anchor, so on ML it walked to a stale anchor.
- The post-combat fallback was a fragile `_lastPetOrder` hack in `DoOrderNone` that re-anchored a resumed `Stay` at the corpse.
- Controlled idle-wander bypassed the `CheckIdle()` rest gate that every non-controlled creature uses, so idling pets jittered every AI tick.

## Approach

Separate three concepts that were tangled together:

- **`ControlOrder`** — the active order (may be transient: Come/Attack/Drop).
- **Persistent command** (`PersistentOrder` ∈ `{None, Stay, Follow, Guard}`) — the standing directive a pet falls back to when a transient order completes. Runtime-only (not serialized; reset to `None` on load) and **derived from master proximity on login** (near → Follow, far → Stay).
- **Anchor** (`Home`) — a pure function of the persistent command, set only when that command changes (never on transient transitions or fallback-resume), so it can't go stale.

### Behavior

- **Stop** is resolved immediately from what the pet was doing: Attack/Come → resume the persistent command; Follow/Guard → cancel to idle where it stands; Stay → stay put.
- **Stay** holds its post (returns only if displaced, e.g. after a fight) — no shuffle.
- **Idle** (`None`) is a gentle wander routed through `CheckMove/CanMoveNow/CheckIdle`, so idling pets take the same 15–25s rest periods as other creatures, on both ML and non-ML.
- **Post-combat** the pet resumes its persistent command (a staying pet returns to its original post, not the corpse).
- **Release** without a spawner anchors where the pet stands instead of pathing to a stale anchor.

This restores the RunUO-intended behavior (verified against the RunUO reference) while fixing the ModernUO regressions.

## Tests

New `PetOrderTests` (13 deterministic xUnit tests) cover: anchor lifecycle, the full Stop truth table, report 1 (post-combat return to post), report 2 (no stale-anchor walk-home), frozen-Stay/gated-idle wiring, release fix, derive-on-login, and a non-ML spot-check. The subjective wander *feel* is covered by a manual-QA checklist in the implementation plan.

## Notes

- Engine project (`Projects/Server`) untouched; the one `BaseCreature.cs` change is the `ControlOrder` setter passing the previous order to `OnCurrentOrderChanged`.
- `DoOrderCome` keeps auto-converting to `Stay` on arrival, which under the new model cleanly means "come and hold near me."
- Commits in this PR are temporarily **unsigned** (the signing agent's passphrase cache expired mid-session); happy to re-sign / amend on request.
2026-06-07 01:22:43 -07:00
Kamron Batman
c7aaf33de9
feat(pathfinding): .swb format v8 compact index (#3b) (#2471)
## Summary
Phase #3b (final roadmap item), stacked on #2470. Compacts the index trailer from 20 to 8 bytes/chunk.

Trammel: 19.2 MB → 17.9 MB. Roadmap total: 565 MB → 17.9 MB (−96.8%).

## Details
- Trailer stores `{ u32 packedKey = (ChunkX << 16) | ChunkY, u32 recordLength }` per chunk, in record write order; the file offset is dropped and reconstructed by cumulative recordLength from HeaderSize.
- No record reordering, no varint; fixed-stride, TryReadChunk unchanged.
- Also simplifies the accumulated `.swb` code comments across the stack.
- Format v8; v7 files rejected and re-baked once.

## Tests
v8 multi-chunk round-trip (cumulative offset reconstruction) + the v6/v7 suite; full pathfinding suite green; Release build clean.
2026-06-07 01:22:20 -07:00
Kamron Batman
c265bcbb5e
feat(pathfinding): .swb format v7 per-chunk compression (#3a) (#2470)
## Summary
Phase #3a, stacked on #2469. Compresses each chunk record independently with libdeflate (random access preserved).

Trammel: 124.7 MB → 19.2 MB (−85%).

## Details
- Whole-record framing `[u32 UncompressedLen][payload]`; records that don't shrink (tiny Uniform) are stored raw, detected as payload length == UncompressedLen.
- Codec chosen by full-Trammel spike: libdeflate VeryHigh (16.5 MB, 1.83 µs/chunk decompress) over zstd L19/22 (17.4 MB) and managed Brotli q11 (16.4 MB) — best native ratio, fastest decompress, already the repo's packet codec (no new dependency).
- Reuses cached thread-static bindings: `Deflate.Maximum` for bake, `Deflate.Standard` for reads.
- Compression is bake-time only; decompression is one-time per chunk (LRU-cached).
- Format v7; v6 files rejected and re-baked once.

## Tests
v7 unit tests + the v6 suite run through the compression path; full pathfinding suite green; Release build clean.
2026-06-07 00:22:11 -07:00
Kamron Batman
94537f83f8
feat(pathfinding): .swb format v6 predictive-Z residuals (#2469)
## Summary
Phase #2 of the `.swb` step-cache size-reduction roadmap (after #2465, v5 uniform elision). Stores the 16 base directional Z arrays as masked residuals against each cell's own SourceZ and omits any array that matches its prediction. Lossless, byte-identical reconstruction.

Trammel: 231.9 MB → 124.7 MB (−46%).

## Details
- Predictor: `predict = mask bit ? SourceZ : 0` (matches the baker's 0 on unwalkable directions); residual `Z − predict` via unchecked two's-complement (byte-exact for all inputs); reconstruct `Z = predict + residual`.
- A `u16 ZArrayMask` flags which of the 16 base arrays differ from prediction; matching arrays are omitted and synthesized from mask + SourceZ at read.
- Serializer-layer only: StepChunk, the cache, the algorithm, and the baker are unchanged.
- Format v6; v5 files rejected and re-baked once.

## Tests
21 v6 unit tests; full pathfinding suite green; Release build clean.
2026-06-07 00:12:08 -07:00
Kamron Batman
8e5e4f72c8
fix(ninjitsu): gate Animal Form gump by skill and stop mana drain (#2452) (#2468)
## Issue

Fixes #2452. A player with 30 Ninjitsu reported that the Animal Form menu showed **every** form; selecting one above their skill (e.g. Dog, req 40) **consumed mana** and returned "you need at least 40 skill", and afterwards the **gump never reopened** — every recast silently re-attempted the unusable form and drained more mana.

## Root cause

Three linked bugs, all reproduced from the code:

1. **Gump not gated by skill.** `AnimalFormGump.BuildLayout` compared `Skill.Fixed` (which is `Value * 10`, so 30 skill → `300`) against the raw 0–100 `ReqSkill` (Dog = `40`). `300 >= 40` is always true, so all forms were shown. `Morph` itself correctly uses `.Value`.
2. **Mana charged on a no-skill cast.** `Morph` returns `MorphResult.NoSkill` for an under-skilled form, but both call sites (`OnCast`, `OnResponse`) only special-cased `MorphResult.Fail`; `NoSkill` fell through to the branch that deducts mana.
3. **Menu never reopened.** Per OSI ([uo.com](https://uo.com/wiki/ultima-online-wiki/skills/ninjitsu/), [uoguide](https://www.uoguide.com/Animal_Form)), casting while **standing still always opens the selection menu**, and casting while **moving** quick-transforms into the last selected form. ModernUO only opened the menu when `lastAnimalForm == -1`, so once any form was selected a stationary recast skipped the menu.

## Fix

- Add `AnimalForm.CanSelectEntry` (compares `Skill.Value` to `ReqSkill`, plus the talisman check) and use it for the gump's per-entry enable check.
- `OnCast`: standing still always opens the menu; moving quick-transforms into the last form. `NoSkill` no longer costs mana.
- `OnResponse`: handle `Success` / `Fail` / `NoSkill` explicitly so `NoSkill` costs no mana.

## Tests

Adds `AnimalFormTests`:
- `CanSelectEntry` rejects forms above skill, accepts forms at/below skill, and requires a talisman for talisman-gated forms.
- `Morph` returns `NoSkill` (without transforming) when under-skilled, and `Success` when sufficiently skilled.

Verified the gating test catches the regression (reintroducing `.Fixed` fails it). Full solution build is clean; the 5 new tests plus 284 other UOContent tests pass (the pathfinding/AI sequential tests were excluded only because they deadlock under concurrent local runs — they are unrelated to this change).
2026-06-06 15:56:31 -07:00
Kamron Batman
122e20c954
chore: Update README with Code Signing Policy (#2467)
Added Code Signing Policy section to README.
2026-06-06 15:32:01 -07:00
Kamron Batman
c7697e1dc5
feat(pathfinding): .swb uniform-chunk elision (format v5) — Trammel 592→232 MB (#2465)
## Summary

Sub-project #1 of the `.swb` step-cache size-reduction roadmap (`dev-docs/pathfinding.md` § Future work). Adds **uniform-chunk elision** to the `StepCacheFile` format, bumping it **v4 → v5**.

A fully-uniform 16×16 chunk — no strata, **no swim layer**, all 19 base arrays constant (open ocean, Green Acres, void) — serializes to a **~28-byte record** (`KindUniform`) instead of ~5,393, and reconstructs **byte-identically** via `Array.Fill`. Non-uniform chunks use the existing v4 body (`KindFull`) with the swim-layer and strata trailers **fully preserved** — the Kind byte is just prepended.

## Calibrated result (measured, not projected)

Baked Trammel via `SaveToFile`:

| | |
|---|---:|
| Chunks | 114,688 |
| Uniform (swim-aware) → elided | 62.7% |
| Swim-layer chunks (stay Full) | 8.9% |
| Strata chunks (stay Full) | 1.8% |
| Baseline (full records) | 592.2 MB |
| **Actual v5 `.swb`** | **231.9 MB (−61%)** |

The residual is ~150 MB of non-uniform land Z-blocks (targeted by #2 predictive-Z) + ~81 MB of swim-layer trailers (#2/#3). #2 and #3 are separate follow-up PRs.

## Implementation

- `StepChunk.IsUniform()` — false if it has strata **or a swim layer**, else true only when all 19 base arrays are constant (the "all-same" check uses the SIMD-accelerated `ContainsAnyExcept`).
- `StepCacheFile` v5 — `Kind` byte (`KindFull=0`/`KindUniform=2`, 1 reserved); uniform write/read; `FormatVersion`/`MinSupportedVersion` → 5 (v4 files rejected on open and re-baked). No `StepCache`/algorithm/index changes; fingerprint logic untouched.

## Tests

7 `StepCacheFileV5Tests` (uniform round-trip + `<200 B` compactness, varied-full, swim-layer-full, strata-full, swim+strata combined, v4 version-gate rejection) + the existing StepCache/pathfinding suite — **70 pass**, including the prior `SwimLayer_RoundTrips`. An independent review verified write/read symmetry, cast round-tripping, swim/strata preservation, and the version gate (READY TO MERGE).
2026-06-06 15:20:27 -07:00
Kamron Batman
47bf2d1f13
chore: Change signing policy slug to 'release-signing' (#2466) 2026-06-06 15:00:06 -07:00
Kamron Batman
a8acfa31f8
refactor: decompose mobile/corpse hair, delete VirtualHairInfo, fix removal serial (#2462) (#2463)
Fixes #2462

## Summary
Removes the per-object `VirtualHairInfo` heap wrapper for mobile/corpse hair. Hair is now stored **inline** on `Mobile` and `Corpse` as `int _hairItemId` / `int _hairHue` plus a lazily-allocated, **non-serialized** ephemeral `Serial _hairSerial` (in the high virtual-serial range) — and likewise for facial hair. The `VirtualHairInfo` class is deleted, with a **lossless** save migration.

This delivers three things:
1. **Fixes a hair-removal bug.** `Delta(MobileDelta.Hair)` is deferred (it enqueues; `ProcessDeltaQueue` runs later in the tick). The old `HairItemID = 0` setter nulled `_hair` *immediately*, so by the time `ProcessDelta` built the remove packet the equipped virtual serial was already gone — the old `??=` code then re-materialized a **fresh** serial (≠ the equipped one), so clients never removed the right entity, and it left a phantom ItemId-0 object behind. The serial now lives on the entity and **persists across removal**, so remove packets carry the correct serial.
2. **Lightens the entity.** No heap hair object; bald mobiles allocate nothing (the serial is minted lazily only when hair is present). This was the original reason `HairItemID`/`HairHue` exist.
3. **Removes `VirtualHairInfo` entirely**, keeping the high-range virtual serial behavior.

## How
- **Mobile** (manual serialization): inline `_hairItemId/_hairHue/_hairSerial` (+facial); lazy `HairSerial`/`FacialHairSerial`; `ProcessDelta` reads those. Serialization **v36 → v37** — the v30-v37 deserialize is unified, reading the legacy per-hair `VirtualHairInfo` version int only when `version < 37`. Setting item id to 0 clears the hue (matching the old object-nulling) while retaining the serial.
- **Corpse** (codegen serialization): decomposed to `[SerializableField] int _hairItemId/_hairHue` (+facial) + ephemeral serial; **v16 → v17** with `MigrateFrom(V16Content)`.
- **Lossless migration:** the loader validates exact byte length, and the old corpse hair is a presence-bool-gated block, so a tiny **migration-only** `LegacyHairInfo` reader (no runtime role) consumes the legacy `[bool][int ver][int itemId][int hue]` bytes. Frozen `Corpse.v14/v15/v16.json` are retyped to it; `v17.json` describes the new int fields.
- All consumers updated to discrete accessors: `OutgoingMobilePackets`, `CorpsePackets`, corpse subclasses (`MilitiaFighterCorpse`, `SchmendrickApprenticeCorpse`), and the packet test mirrors.
- `VirtualHair.cs` renamed to `OutgoingVirtualHairPackets.cs` (the only type left in it after `VirtualHairInfo` was removed).

## Test Plan
- [x] Full solution build: **0 warnings, 0 errors** (`TreatWarningsAsErrors`).
- [x] `Server.Tests`: **708 passed** (incl. new `RemoveHairUsesEquippedSerial` / `RemoveFacialHairUsesEquippedSerial` proving the serial survives removal + hue clears).
- [x] `UOContent.Tests` corpse/hair: **6 passed** (incl. `CorpseHairMigrationTests` asserting the legacy hair bytes are consumed exactly — the loader's length invariant).
- [x] Generated migration code inspected: V14/V15/V16 readers consume the legacy block byte-for-byte; serial never written to disk.

## Upgrade notes
- Old Mobile (v30–v36) and Corpse (v13–v16) saves load losslessly.
- Minor cosmetic-only change: `SchmendrickApprenticeCorpse` hair/facial-hair RNG draws shift order within each pair (same draw count); irrelevant for a quest NPC corpse.
2026-06-06 14:33:28 -07:00
Kamron Batman
978f314f0e
docs(pathfinding): architecture, configuration, and tuning reference (#2464)
## Summary

Adds `dev-docs/pathfinding.md` — a reference for how creature pathfinding works in ModernUO, written so a future contributor (human or AI) can reason about it without re-deriving it from the code.

Covers:
- **The stack** end to end: `ApproachTarget` → `PathFollower` → `MovementPath` → `BitmapAStarAlgorithm` → `StepCache` → `MovementImpl` slow path (and that the removed FastAStar survives as the slow path).
- **Windowed-A* limits** (`AreaSize=38`, `MaxSearchNodes`, Z planes) and what they mean (2D-adjacent-but-obstacle-separated / a-floor-up goals are unsolvable by design).
- **StepCache**: second-touch warming, LRU-bounded memory, lazy `.swb` backing stores — with **measured** disk sizes (~565 MB/Trammel; ~1.5–2 GB all facets).
- **Four config levers** in a table: `pathfinding.enable`, `bitmap_pathfinding_cache`, `pathfinding.maxResidentChunks`, `pathfinding.maxSearchNodes`.
- **Small/crappy-hardware shard spectrum** (cache off ≈ FastAStar at ~1× with zero warming memory, up through baked `.swb`).
- **Diagnostics & tooling** (`[PathCacheStats`/`[PathRecord`/`[PathBake`, the MapDump tool, the benchmark suite) and the Debug/Release test note.
- **Future work**: background-thread bake, long-traverse BDN scenario, swim `SourceZ` bake, and the `.swb` size-reduction roadmap.

## Note on scope

This is docs-only. It documents the *complete* pathfinding system, so it references a couple of pieces that ride in separate PRs (the `ApproachTarget` AI fix and the `pathfinding.maxSearchNodes` setting). If those havent merged yet, sequence this after them so the doc doesnt describe unshipped code. The StepCache/`.swb`/provider material it documents is already on `main`.
2026-06-06 13:29:26 -07:00
Kamron Batman
9a3d88988c
feat(pathfinding): non-eager TryGetMask + second-touch promotion (#2451)
## Summary

Closes the Cold-cache regression flagged in PR #2450. `StepCache.TryGetMask` no longer eagerly runs `BuildChunk` on the first miss for a chunk that isn't in a `.swb` lazy reader. Instead it returns `Fallthrough_NotBuilt` and the caller (`BitmapAStarAlgorithm`) takes the per-cell slow path. The chunk is only promoted to the bitmap fast path after the **second** miss within a 30-second window, filtering single-touch pass-throughs.

This makes BitmapAStar's worst-case (cold cache + short hops) collapse from **12–47× slower** than FastAStar to **roughly the same**, which is the floor the slow path can deliver. Steady-state warm performance (the actual deliverable) is unchanged from PR-5 — it was always the cache fast path.

## The pet-follow scenario this fixes

A mounted player at ~4 tiles/sec with a pet/hireable following will trigger an NPC pathfind every 100–300 ms. Each pathfind is 1–6 tiles. As the player crosses chunk boundaries (~4 sec/chunk), the pet's first pathfind in the new chunk under the previous behavior triggered a full ~700 µs `BuildChunk` for a chunk the player would leave shortly after. At 50–100 mobiles per shard, this exceeded the 8 ms tick budget. PR-5 BDN data showed scenarios 6–9 (2–8 tile NPC perception) at 2,300–3,700 µs Cold vs FastAStar's 80–200 µs.

Under the new gate:

- First miss → `Fallthrough_NotBuilt` → caller uses slow path (~30–50 µs short path). No `BuildChunk`. No allocation.
- Player keeps moving → chunk never gets a second touch within window → never promoted, no rot.
- NPC patrolling a fixed territory → repeatedly hits the same chunks → second touch within window → promote → cache fast path on subsequent calls.

## What changed

- **`CacheHitKind.Fallthrough_NotBuilt = 6`** + **`CacheStats.FallthroughNotBuilt`** counter. `IsHit=false`, so the caller routes to slow path.
- **`StepCache._chunkMissTracker`** — `Dictionary<long, ChunkMissState>` capped at 4096 entries. State is `(byte missCount, uint lastMissTickStamp)` keyed by chunk key. Window-expired entries reset count to 1; capacity overflow prunes window-old entries first.
- **`StepCache.MissPromotionThreshold`** (default `2`) and **`StepCache.MissPromotionWindowMs`** (default `30_000`) — tunable, can be wired through `ServerConfiguration` if shards want different policy. Setting threshold to `1` restores legacy eager-build behavior (used by tests that prime chunks via single `TryGetMask` call).
- **`StepCache.TryGetMask` miss branch** — try lazy reader first (file-loaded chunks bypass the tracker entirely; an `.swb` represents an explicit prior decision to keep the chunk warm). Otherwise consult the tracker.
- **`BitmapAStarAlgorithm.GetSuccessorsSlowPath`** now layers `IsBlockedByDynamic` on top of `CalcMoves.CheckMovement`. Previously the slow path only ran for `CanFly` creatures and rare cache fallthroughs — `CheckMovement` doesn't iterate same-cell mobiles, so the bitmap fast path's `IsBlockedByDynamic` was the only mobile-blocking check. Now first-touch pathfinds run through the slow path, so the gap had to close.

## Tests

50 pathfinding tests pass (was 47). New / updated:

- **`TryGetMask_FirstTouchOnUnbuiltChunk_DefersBuildAndReturnsFallthrough`** — single TryGetMask call returns `Fallthrough_NotBuilt`, no chunk built, no allocation.
- **`TryGetMask_SecondTouchWithinWindow_PromotesAndBuilds`** — second call inside the 30s window builds + serves.
- **`TryGetMask_SecondTouchAfterWindow_RestartsCounterAndDefers`** — second call outside the window restarts the count, returns Fallthrough again.
- **`TryGetMask_DistinctChunks_TrackedIndependently`** — counters are per-chunk; one touch on each of two adjacent chunks both stay in fallthrough.
- **`LazyReaderHit_BypassesMissTrackerOnFirstTouch`** — open `.swb` + first touch hits without consulting the tracker. Production with `.swb` loaded skips the gate entirely.
- **`MultisVersion_Bump_TriggersDirtyRebuild`** — updated to reflect the new 3-step flow (Fallthrough → Miss_NotBuilt → Miss_DirtyRebuild).
- Tests that prime chunks via a single `TryGetMask` call (multi-Z, Tier4, lifecycle, parity, BitmapAStar uses-cache) set `MissPromotionThreshold = 1` to opt into eager behavior.

## Expected BDN impact

The Cold column from PR-5's BDN should change as follows once the bench's submodule pointer is updated to this branch:

| # | Scenario        | Cold (PR-5)  | Cold (PR-6 expected) | FastAStar Cold |
|--:|-----------------|-------------:|---------------------:|---------------:|
| 2 | sewer corridor  | 1,627 µs     | ~36 µs               | 36 µs          |
| 4 | causeway        | 1,533 µs     | ~39 µs               | 39 µs          |
| 6 | pet 2-tile      | 2,364 µs     | ~80 µs               | 81 µs          |
| 8 | npc 5-tile      | 3,708 µs     | ~140 µs              | 141 µs         |
| 9 | npc 8-tile      | 2,386 µs     | ~200 µs              | 197 µs         |

WarmNoFile and LazyWarm rows should be unchanged — they were always cache-warm. The miss tracker only fires when neither resident chunks nor the lazy reader can satisfy the request.

## Future work (not in this PR)

- **Background-thread bake**: builds outside the game thread so even promoted chunks don't pay the 700 µs build cost on the main thread. Rule 10 (no Task.Run) applies, so this needs careful design — the bake is a pure data transform but main-thread synchronization on chunk-state transitions has to be threaded through. Defer to a follow-up.
- **Long-traverse BDN scenario**: a multi-Find benchmark simulating 50 pet repaths across chunk transitions. Requires restructuring the bench harness; the existing 10-scenario corpus + Cold provider already exercises the gate.
- **Swim sourceZ bake**: scenario 5 (sea serpent) shows 56 B alloc on warm paths because the cache's SourceZ is computed under default-walker rules. Swim creatures fall through to slow path. Independent of this PR.
2026-06-06 13:11:53 -07:00
Kamron Batman
cff9fbda29
fix(ai): creatures pathfind around concave obstacles instead of oscillating (#2461)
## Summary

Creatures (pets following, monsters chasing, NPCs approaching) would **oscillate — "pace back and forth really fast"** at concave obstacles (reported at the Britain Inn L-desk: a pet at `(1493,1614,20)` never reaching its master at `(1494,1605,21)`) instead of routing around them.

**Root cause** (the A* pathfinder itself was correct): all goal-seeking funnels through `MoveTo` and `WalkMobileRange` → `MoveTowardsOrAwayFrom`, which step greedily via `DoMove(dir, badStateOk:true)`. `DoMove` returns `true` even when the direct step was blocked and the creature merely **auto-turned and sidestepped** (`MoveResult.SuccessAutoTurn`), and the caller then set `Path = null`, discarding the `PathFollower`. So at a concave obstacle a non-progressing sidestep was mistaken for progress and the creature never committed to a route. (AOS pet-follow runs at `CurrentSpeed = 0.1`, hence the "really fast" shuffle.)

## What changed

- **New centralized `BaseAI.ApproachTarget(target, run, range)` primitive.** A greedy step is committed only when it **fully succeeds (`MoveResult.Success`) and actually gets closer**; otherwise the creature commits to a **persistent `PathFollower`** that routes around the obstacle and is never discarded by a greedy step. The open-terrain fast path (one greedy step, no pathfinding) is preserved. `MoveTo`, `MoveTowardsOrAwayFrom`, and `MoveToWithCollisionAvoidance` all delegate to it — public signatures unchanged, so no AI-class call site changes.
- **Best-distance give-up + idle.** A creature that cannot reach a **stationary** in-range goal stops shuffling and idles after `ApproachGiveUpTicks` (40) ticks without lowering its closest-ever distance; a **moving** goal (active chase) never gives up. It resumes the moment the goal moves.
- **Pathfinder fix (required):** `BitmapAStarAlgorithm.IsBlockedByDynamic` now skips the dynamic mobile-block check **at the goal cell only** (`MoveImpl.Goal`). Previously A* returned `null` whenever the target mobile stood on the goal cell, so creatures could never pathfind *toward* another mobile — only toward empty ground. The follower stops within `range` short of it. Static/item blocking and all non-goal mobile blocking are unchanged.

## Tests

New AI-loop integration tests in `ApproachTargetTests.cs` drive the real `BaseAI` primitives against live Britain Inn map statics: exact-repro pet follow, open-terrain (asserts zero pathfinding), `MoveTo` chase (static + walking-away target), route-around-a-dynamic-wall, and walled-off give-up-and-idle.

- Pathfinding + AI subset: **52/52** pass.
- Full `UOContent.Tests`: **301/301** pass. (Note: the test host lingers on shutdown — a pre-existing infra quirk unrelated to this change; all tests complete and pass.)
- Full solution build: clean (0 warnings / 0 errors).

## Notes

- Branched off `main`; independent of the in-flight step-cache work.
- Out of scope (future work): proactive "SmartAI" look-ahead pathfinding so clever creatures plan a route before walking into the obstacle, rather than reacting after they hit it.

## Test Plan

- [X] In-game: order a pet to `follow`/`come` across the Britain Inn L-desk; confirm it routes around and reaches you instead of pacing.
- [X] Aggro a monster and kite it around a building/treeline; confirm it chases around obstacles.
- [X] Confirm open-terrain following/chasing feels unchanged (no extra latency).
- [X] Confirm a creature with a genuinely unreachable target idles rather than shuffling forever.
2026-06-06 10:20:52 -07:00
Alcsaar
b589da3efb
fix: HouseRaffleStone timer cleanup (#2456) 2026-05-23 10:39:58 -07:00
Kamron Batman
7bd2cb6a2a
feat(pathfinding): Tier 4 multi-Z strata (file format v2) (#2450)
## Summary

Multi-Z cells (bridges, stairs, paver-over-ground, multi-floor structures) now carry **per-stratum walkability data** in the cache instead of falling through to the slow path. The data is computed at chunk-build time, persisted in the `.swb` file, and selected at query time by matching the request's `sourceZ` against each stratum's `zCenter` (within `StepHeight` tolerance).

This is the Tier 4 strata feature, deferred from PR #2447 / PR #2448 / PR #2449. Builds on PR #2449's lazy backing store and public bake helpers.

## Wire format change (v1 → v2)

`StepCacheFile.FormatVersion = 2`. `MinSupportedVersion = 2`. v1 `.swb` files are silently rejected at open time (treated as missing) and overwritten on the next `SaveToFile` / `BakeMap`. **No migration** — older files just get re-baked.

The `MinSupportedVersion` sentinel is the model going forward: bump the constant when an incompatible change lands; admins re-bake on the next deploy. No matrix of v1↔v2↔v3 migration logic to maintain.

## What changed

- **`StepProbe.ComputeStrataAt(map, x, y)`** — enumerates walkable standing-Zs at the cell (one per land surface plus one per walkable static), collapses Zs within `2*StepHeight`, runs `ComputeMaskAt` at each surviving Z. Returns `null` for single-Z cells (caller uses the chunk's main mask).
- **`StepChunk`** — replaces the old `MultiZCells` bitmap with a **strata storage pair**:
  - `ushort[256] StrataOffsetByCell` (sentinel `NoStrata = 0xFFFF` = "no strata for that cell")
  - `byte[] StrataData` packed: `u8 stratumCount`, then `count × 19-byte stratum`
    - `sbyte zCenter, byte walkMask, byte wetMask, sbyte walkZ_N..NW (8), sbyte swimZ_N..NW (8)`
  - `IsCellMultiZ` derives from `StrataOffsetByCell[cell] != NoStrata` — same semantics, single source of truth.
- **`StepCache.BuildChunk`** — populates strata for cells flagged multi-Z via `SetStrata`. Chunks with zero multi-Z cells pay zero strata overhead (offset array + data array stay null).
- **`StepCache.TryGetMask`** — for multi-Z cells, scans strata with `TryStratumHit`; returns the matching one with `HitKind=Hit`. Falls through to slow path only when no stratum matches the query `sourceZ`.
- **`StepCacheFile`** — v2 serialization with strata trailer per chunk + `recordLength` in index entry. Lazy reader sizes scratch per-chunk-record using the recorded length, growing on demand for multi-Z-heavy chunks. Patches `IndexOffset` on `w.Buffer` (BufferWriter's current backing array) since it grows during variable-size chunk writes.

## File layout v2

```
Header (48 bytes):
  u32  Magic           = 0x42575300 ('SWB\0')
  u32  Version         = 2
  u32  MapId
  u64  Fingerprint     XxHash3 over LandTable + ItemTable flags + map files (mapX.mul/.uop, staidxX.mul, staticsX.mul)
  u64  BakeTimestamp   informational
  u32  ChunkCount
  u64  IndexOffset     position where chunk index begins

Per chunk (variable size):
  u16  ChunkX, ChunkY
  u32  BuiltMultisVersion
  u8   HasStrata       0 = no strata trailer; 1 = strata trailer follows
  byte WalkMask[256], WetMask[256]
  sbyte SourceZ[256], WalkZN..WalkZNW[256], SwimZN..SwimZNW[256]
  // Strata trailer (only when HasStrata == 1):
  u16  StrataOffsetByCell[256]    // NoStrata sentinel = 0xFFFF
  u32  StrataDataLength
  byte StrataData[StrataDataLength]
       Per multi-Z cell: u8 count, then count × Stratum (19 bytes)

Index trailer (20 × ChunkCount bytes):
  per chunk: { u64 chunkKey, u64 fileOffset, u32 recordLength }
```
2026-05-06 22:55:42 -07:00
Kamron Batman
a8ca82738d
feat(pathfinding): JSONL recorder + public bake helpers (#2449)
## Summary

Adds two pieces of pathfinding tooling on top of PR #2448's lazy `.swb` infrastructure:

- **`PathfindRecorder`** — admin-toggled JSONL telemetry capture; one record per `BitmapAStarAlgorithm.Find` call. Output format matches the BDN harness corpus, so production traffic can be captured and replayed in benchmarks without an adapter.
- **Public bake helpers on `StepCache`** — `ComputeLiveTileDataHash`, `TryReadTileDataHashFromFile`, `BakeMap`, `ClearResidentChunks`. Lets the benchmark project (and any future bake utility) drive cache fill + persist without exposing internal types.

The companion BDN harness update lives in [ModernUO-Benchmarks#kb/pathfinding-pr4-bench](https://github.com/modernuo/ModernUO-Benchmarks/tree/kb/pathfinding-pr4-bench): porting `Benchmarks/PathfindInGame/` from the `kb/ai_pathfinding` branch to the API shipped in #2446–#2448.

## What's in this PR

### `PathfindRecorder` (`PathfindRecorder.cs`)
- Holds a single `StreamWriter` open while recording; its internal buffer absorbs per-record writes without per-call `File.AppendAllText`.
- Single `bool` check on the hot path; cheap when disabled.
- Disabling flushes + disposes; an IO failure during write also disables the recorder.
- Server config:
  - `pathfinding.recorder.enable` — bool, default `false`. Read on boot via `GetOrUpdateSetting`.
  - `pathfinding.recorder.path` — default `<basedir>/Data/Pathfinding/recordings/pathfinds.jsonl`.
- Hooked into `BitmapAStarAlgorithm.Find` — runs once per call, does nothing when disabled.
- Admin command: `[PathRecord [on|off|flush|status]` (default `status`).

### Public cache helpers
- `static ulong StepCache.ComputeLiveTileDataHash()` — wraps the file module's hash function for staleness checks.
- `static bool StepCache.TryReadTileDataHashFromFile(string, out ulong)` — peeks at a `.swb` file's hash field (20 bytes).
- `int StepCache.BakeMap(int, string)` — walks every chunk in the map, populates resident set, saves. Offline / fixture use; blocks for many seconds on a full-map walk.
- `void StepCache.ClearResidentChunks()` — drops chunks + zeros counters but keeps lazy readers open. Lets benchmark loops measure "first query after boot" cost across iterations without the lazy-reader reopen overhead.
2026-05-06 13:06:28 -07:00
Kamron Batman
7c9215d97c
feat(pathfinding): lazy .swb backing store for the step cache (#2448)
## Summary

Adds a binary disk format + lazy reader so the step cache can warm-start from a precomputed file without paying chunk-build cost on the first pathfind through a region. **Resident memory stays bounded by `MaxResidentChunks` regardless of file size** — opening a `.swb` reads only the header + chunk-offset index (~16 bytes per indexed chunk), and individual chunks are seeked + deserialized only when `ResolveMissingChunk` asks for them.

The lazy design (vs. an eager bulk load): a 250 MB bake on a RAM-constrained shard never materializes more than the LRU cap (~40 MB at the default 8192-chunk cap), and unwanted regions never enter memory at all.

Builds on PR #2447.

## What changed

- **`StepCacheFile`** — binary reader/writer module. Writer emits header → chunks (offsets recorded) → index trailer, then patches the header's `IndexOffset` field. Reader is `OpenForLazy(path)` returning a `LazyReader` that holds an open `FileStream` + offset dictionary.
- **`StepCacheFile.LazyReader`** — `TryReadChunk(chunkX, chunkY)` does a single seek + bulk read for one record. `Dispose` releases the underlying stream. Files are opened with `FileShare.Read | FileShare.Delete` so admin tooling can replace them.
- **TileData fingerprint via XxHash3.** The `.swb` header carries a hash of `LandTable + ItemTable` flags. Load rejects any file whose hash doesn't match the running server. Computed via `HashUtility.ComputeHash64` (engine-blessed hasher) — adds a `ReadOnlySpan<byte>` overload alongside the existing `ReadOnlySpan<char>` one for parity.
- **`StepCache.SaveToFile(path, mapId)`** — writes resident chunks for the given map.
- **`StepCache.TryOpenLazyReader(path, mapId)`** — opens the file, validates header, holds the reader for the map's lifetime.
- **`StepCache.ResolveMissingChunk`** — now consults the lazy reader before invoking the runtime baker. A loaded chunk whose `BuiltMultisVersion` doesn't match the live sector falls through to the baker (snapshot was made before a multi was added/removed in that sector).
- **`StepCache.Clear` closes lazy readers.** Test cleanup can delete `.swb` files cleanly.
- **Auto-load at startup.** `PathCacheCommands.Configure()` opens `Data/Pathfinding/<mapId>.swb` as a lazy reader for every map.
- **`[PathCacheSave`** / **`[PathCacheLoad`** — admin commands for the same workflow.
- **`pathfinding.maxResidentChunks` shard-tunable.** Read from `server.cfg` at boot via `ServerConfiguration.GetOrUpdateSetting` (default 8192 ≈ 40 MB). Small shards can tune down; large shards with substantial bakes can tune up to reduce eviction churn. Default is written back to `server.cfg` on first boot, matching the engine pattern used by other settings.

## File layout (v1)

```
Header (48 bytes):
  u32  Magic           = 0x42575300 ('SWB\0')
  u32  Version         = 1
  u32  MapId
  u64  TileDataHash    XxHash3 over LandTable + ItemTable flags (HashUtility)
  u64  BakeTimestamp   informational
  u32  ChunkCount
  u64  IndexOffset     file position where the chunk index begins

Chunk records (fixed size, ~5,393 bytes each, +32 if multi-Z):
  u16  ChunkX
  u16  ChunkY
  u32  BuiltMultisVersion
  u8   HasMultiZ
  byte WalkMask[256], WetMask[256]
  sbyte SourceZ[256], WalkZN..WalkZNW[256], SwimZN..SwimZNW[256]
  [byte MultiZCells[32] when HasMultiZ == 1]

Index trailer (16 × ChunkCount bytes):
  (u64 chunkKey, u64 fileOffset)
```

## Memory math

| Scenario | Disk file | RAM at boot | Notes |
|---|---|---|---|
| Empty / no `.swb` files | — | 0 | Silent; cache builds on demand. |
| Admin-curated towns (5K chunks) | 25 MB | 0 + per-query | Index ≈ 80 KB. Resident grows to the configured cap under steady-state queries. |
| Full-map bake (50K chunks) | 250 MB | 0 + per-query | Index ≈ 800 KB. Same configured cap. Cold areas never load. |
| All 5 maps fully baked | 1.25 GB | 0 + per-query | Index ≈ 4 MB total. Same configured cap. |

## Hash choice (FNV-1a → XxHash3)

The original draft used inlined FNV-1a-64. Switched to XxHash3 via `HashUtility`:

- ~30× faster on this workload (~30 GB/s SIMD vs ~2 GB/s byte-by-byte). Boot-time only, so absolute saving is microseconds — the real wins are elsewhere.
- Stronger collision resistance and distribution.
- Drops ~25 lines of inlined hash code; matches the rest of the codebase's hashing pattern.
- Hash is stable as long as `HashUtility`'s `xxHash3Seed` constant doesn't change (already marked `// DO NOT CHANGE THIS NUMBER`).
2026-05-06 01:32:44 -07:00
Kamron Batman
9066e8fd00
feat: expand cache to nearly all mobiles + dynamic-obstacle pass (#2447)
## Summary

Builds on PR #2446's cache-direct A*. The previous PR conservatively routed players + creatures with capability flags entirely through the slow path. This PR pushes that line: most mobile classes now use the cache, with the right rule set layered on top per-mobile, and the cache fast-path now does the dynamic items / mobiles check that PR #2446 had silently skipped.

## What changed

- **Non-GM players** now use the cache. Diagonal corner-cut applies the strict AND-rule (BOTH cardinal partners walkable) by reading the same source-cell mask byte the creature OR-rule reads — both rules are evaluable from one byte.
- **Creatures with `CanOpenDoors` / `CanMoveOverObstacles`** now use the cache. Reading `MovementImpl` confirmed those flags only affect dynamic items, never static tiles, so they were over-conservatively excluded before.
- **Swim creatures** now use the cache via a capability overlay. `StepProbe` bakes a second rule set (`canSwim=true, cantWalk=true`) producing `WetMask` + `SwimZ_*`. The algorithm composes `effectiveMask = (walkMask & !cantWalk) | (wetMask & canSwim)` per direction; walk Z preferred when both apply.
- **Dynamic-obstacle pass.** Cache fast-path now mirrors `MovementImpl`'s per-cell items + mobiles collision check (`GetItemsAt` / `GetMobilesAt` at the target cell, with `CanOpenDoors` / `CanMoveOverObstacles` / spell-field overrides). This closes a correctness gap from PR #2446 — the cache fast-path was silently skipping dynamic obstacles entirely.
- **`StepCache.TryGetMask` returns `StepMask` struct** instead of 11 out parameters. `HitKind` rolls into the struct with an `IsHit` accessor. Sets up wet/swim without ballooning the call site.
- **`StepChunk.MultiZCells` is lazy-init.** Most chunks are entirely single-Z; allocating the 32-byte bitmap up-front wasted ~256KB at full cap.
- **Admin commands.** `[PathCacheStats` (resident chunks + hit/miss/eviction counters) and `[PathCacheClear` (drop everything, zero counters).
- **Feature flag.** `bitmap_pathfinding_cache` (default true) gates the cache fast-path. Flipped off, every cell expansion routes to `MovementImpl` — equivalent to PR #2446's slow-path-only behavior. Safety net for shipping the new behavior.

`RequiresSlowPath` shrinks to just `CanFly` — flying creatures Z-jump arbitrarily, which the cache's static-Z model can't accommodate.
2026-05-06 00:14:08 -07:00
Kamron Batman
6a3804addc
feat: Replace FastAStarAlgorithm with BitmapAStarAlgorithm (#2446)
## Summary

Replaces `FastAStarAlgorithm` with `BitmapAStarAlgorithm`: one cache lookup per cell expansion (8-direction mask + per-direction destination Z) instead of 8 separate `MovementImpl.CheckMovement` calls. Adds the supporting cache infrastructure to back it.

Public API unchanged — `MovementPath` / `Mobile.Move` / `CalcMoves.Find` return the same shapes; the algorithm swap is internal.

## What's in this PR

- **`BitmapAStarAlgorithm`** — A* that issues one `StepCache.TryGetMask` call per cell expansion. Inline fallthrough to the per-cell slow path for multi-Z, off-map, source-Z mismatch, and non-default walkers.
- **`StepCache`** — singleton chunk store keyed by `(mapId, chunkX, chunkY)`. Lazily built on first query, invalidated by `Sector.MultisVersion` mismatch, memory-bounded by sampled probabilistic LRU.
- **`StepProbe`** — computes static-only walkability for a single cell, mirroring `MovementImpl.Check` minus the item / mobile collision phases.
- **`StepMask` / `StepChunk`** — value / storage types for the per-cell results.
- **`CacheEvictionTimer`** — periodic cap backstop (60s interval; early-returns when not over cap).
- **`Map.Sector.MultisVersion`** promoted to `public` so the cache can detect dynamic-static invalidations cheaply.

## Eviction strategy

Sampled probabilistic LRU (Redis-style). Per eviction, sample 5 random keys from a parallel `List<long>` kept in lockstep with the chunk dictionary; evict the oldest of the sample via swap-and-pop. O(1) per eviction regardless of resident count, so sustained cap pressure has no perpetual perf hit.

## Capability handling (interim)

Non-default walkers (non-GM players, creatures with `CanSwim` / `CanFly` / `CanOpenDoors` / `CanMoveOverObstacles`) route entirely through the per-cell slow path via `BitmapAStarAlgorithm.GetSuccessorsSlowPath`. The 2-pass design (cache + capability overlay + dynamic-obstacle pass) lands in the follow-up PR.
2026-05-05 21:53:43 -07:00
Wyatt88
ee1bf23b72
feat: add young_player_system feature flag to disable Young player system (#2445)
Adds an opt-in 'youngPlayerSystem.enabled' server setting (default true) that, when set to false, disables the Young player system server-wide:

- Account.Young and PlayerMobile.Young getters short-circuit to false.

- New characters no longer receive Young status or a NewPlayerTicket.

- All downstream Young checks (notoriety beneficial-action restriction, CheckYoungProtection, stealing penalties, YoungDeathTeleport, death-item movement, poison immunity, '(Young)' name suffix, CanLogout, renounce-young keyword/gump, BaseCreature.OnDeath fame penalty, OnLogin time-remaining message) become inert.

Setters are intentionally left untouched so serialized account/player flags round-trip cleanly when the setting is later re-enabled.
2026-05-05 20:09:49 -07:00
Kamron Batman
e9c7aac510
perf(conpvp): zero-alloc trophy text via TrophyRank.LowerName (#2438)
## Summary

Phase 3.3 of the message-interpolation cleanup. Eliminates the `rank.ToString().ToLower()` two-allocation pattern in ConPVP trophy-award messages.

- Adds `TrophyRank.LowerName()` extension returning a static lowercase string per enum value via switch expression.
- Updates 10 call sites across Tournament, KingOfTheHill, DoubleDom, CTF, BombingRun (2 each - cash and no-cash branches).

The handler now appends a static interned string directly into the packet buffer; no `ToString()` formatter and no `ToLower()` allocation per call. Source comment in BombingRun.cs ("There is no formatting flag for Lowercase, we may need a custom interface to get rid of it") is now resolved at the call-site level.
2026-05-03 18:31:40 -07:00
Kamron Batman
ca6064b775
perf(messages): restructure format-string call sites (#2437)
## Summary

Phase 3 PR B of the message-interpolation cleanup. Handles the multi-line restructure sites flagged in `dev-docs/string-handling-message-interp-audit.md` (Phase 2). Phase 3.1 (PR #2436) handled trivial sweeps; this PR handles sites that needed an `if/else` hoist or switch restructure to eliminate `string.Format` while preserving exact message text.

Each site previously allocated an intermediate `string.Format(...)` result before passing to the message handler, despite Phase 1 making the handler accept interpolated string handlers natively.

## Sites fixed

- **`Projects/Server/Mobiles/Mobile.cs:7911`** - Title/guild header was using `string.Format` with a conditional template (`"[{1}]{2}"` vs `"[{0}, {1}]{2}"`). Split into `if (title.Length <= 0)` / `else` with direct `$"..."` interpolation.
- **`Projects/UOContent/Engines/ConPVP/DuelContext.cs:1337`** - View-ladder rank text used `string.Format(text, from == pm ? "You" : "They")`. Split into `if (from == pm)` / `else` with direct `$"..."` interpolation in each branch.
- **`Projects/UOContent/Engines/ConPVP/DuelContext.cs:1463`** - Showladder text reused a single format string for both `LocalOverheadMessage` ("You ... are ...") and `NonlocalOverheadMessage` ("`{pm.Name}` ... is ..."). Each call now uses an inline `$"..."` directly; no shared template.
- **`Projects/UOContent/Engines/ConPVP/Gumps/ConfirmSignupGump.cs:518`** - The signup confirmation message used a `switch` expression assigning a literal format string to `fmt`, then `string.Format(fmt, from.Female ? "Lady" : "Lord", timeUntil)`. Converted to a `switch` statement where each case calls `_registrar.PrivateOverheadMessage(...)` directly with an inline `$"..."`. Lady/Lord branching is hoisted to a `title` local.

## Exempted

- **`Projects/UOContent/Engines/ConPVP/Participant.cs:138`** - The `nonLocalOverhead` format string is a parameter passed in by callers of `Participant.Broadcast`. Investigation found 5 call sites in `DuelContext.cs` (lines 782, 802, 1187, 1196, and three at 1535/1564/1608) that pass distinct literal format strings. Refactoring would require changing all 5 callers and the method signature - out of scope for this PR. Marked with a `// Phase 3 audit:` comment per the audit's exemption convention.
2026-05-03 18:29:11 -07:00
Kamron Batman
b2ccc7e4f3
perf(messages): mechanical interpolation cleanups (#2436)
## Summary

Phase 3.1 of the message-interpolation optimization series. Fixes 9 of the 28 sites flagged in the Phase 2 audit (PR #2435):

| File | Fix |
|---|---|
| `Commands/StaffAccess.cs:88,99` | Drop redundant `.ToString()` on enum holes |
| `Commands/Handlers.cs:102` | `builder.ToString()` -> `builder.AsSpan()` |
| `World Saves/SaveCommands.cs:71-75` | Merge 3 concatenated `$"..."` into one literal |
| `Server/Items/Item.cs:4213` | Hoist nested ternary `$"..."` to if/else |
| `Engines/Factions/Mobiles/Guards/BaseFactionGuard.cs:140-150` | Convert switch expression to switch statement |
| `Mobiles/Monsters/LBR/Jukas/JukaLord.cs:85` | Restructure `string.Format(toSay.RandomElement(), ...)` into switch |
| `Misc/AttackMessage.cs:30-41` | Inline `AggressorFormat`/`AggressedFormat` constants |

No functional changes. Each site emits identical text; the only difference is that the message string is now built into a pooled char buffer instead of being allocated as a `string` first.
2026-05-03 18:26:49 -07:00
Kamron Batman
679e66b99d
feat(buffers): add :L lowercase format spec to RawInterpolatedStringHandler (#2440)
## Summary

Adds a custom `:L` format specifier to `RawInterpolatedStringHandler`. When the format string is `"L"`, the handler lowercases the formatted value's chars in-place after the underlying `ISpanFormattable.TryFormat` / `IFormattable.ToString` path completes. Zero allocation, single-pass.

## Usage

```csharp
mob.SendMessage($"You earned a {rank:L} trophy!");           // "gold"
mob.SendMessage($"Welcome, {playerName:L}");                  // lowercased
mob.SendMessage($"{count:L} kills");                          // ints unchanged ("42")
```

## Motivation

Eliminates the `value.ToString().ToLowerInvariant()` two-allocation idiom that appears across the codebase for any type that goes through an interpolation handler. After this lands, content code can use the `:L` specifier directly instead of helper extensions or per-enum lookup tables.

## Coverage

- `AppendFormatted<T>(T value, string? format)` — generic path (covers IFormattable, ISpanFormattable, .ToString fallback)
- `AppendFormatted(ReadOnlySpan<char> value, int alignment, string? format)` — span path with alignment-aware lowercase range (only the value range is lowercased, not padding)
- `AppendFormatted<T>(T value, int alignment, string? format)` and `AppendFormatted(string? value, int alignment, string? format)` and `AppendFormatted(object? value, int alignment, string? format)` — inherit via delegation

The `format == "L"` comparison is case-sensitive — `:l` (lowercase L) is NOT recognized. `:L` matches the convention of e.g. `:N0` / `:F2` (numeric format specifiers traditionally use uppercase). `char.ToLowerInvariant` is used (not locale-dependent) for predictable game text.

## Future cleanup

Phase 3.3 (#2438) introduced a per-enum `TrophyRank.LowerName()` extension to eliminate `rank.ToString().ToLower()` allocations at 10 ConPVP sites. Once this PR lands, those sites can be simplified to `{rank:L}` and the `TrophyRankExtensions` helper can be removed. Tracked as a follow-up.
2026-05-03 18:24:57 -07:00
Kamron Batman
9ea1b54758
docs(messages): document interpolation anti-patterns and :L format spec (#2441)
## Summary

Captures the durable learnings from the message-interpolation work (PRs #2434, #2436, #2437, #2438, #2440) as reference documentation. **Doc-only PR — no code changes.**

The original Phase 2 audit (PR #2435) was development scaffolding and was closed unmerged once Phase 3 consumed it. This PR replaces it with proper reference docs that future authors can consult.

## What's added

### `dev-docs/string-handling.md`
- Promote `RawInterpolatedStringHandler` from a one-line note to a proper section listing all APIs that accept it (messages, OPL, gumps, packets).
- Document the `:L` lowercase format specifier.
- New comprehensive **"Interpolation Anti-Patterns"** section covering 8 patterns with before/after examples — applies to any handler-aware API:
  1. Ternary with interpolated branches
  2. Switch expression with interpolated arms
  3. Pre-built local typed as `string`
  4. `.ToString()` (or any string-returning method) inside a hole
  5. String concatenation inside a hole
  6. `string.Format` feeding a handler-aware API
  7. LINQ-built strings inside a hole
  8. Pre-built concat var

### `dev-docs/networking-packets.md`
- Add **"Player-Facing Message APIs"** section listing `Mobile` / `Item` / `NetState` message methods with their handler overloads.
- Note the `IBroadcastFilter` pattern for new spatial-broadcast helpers.

### `dev-docs/property-lists.md`, `dev-docs/gump-system.md`
- Cross-reference the new anti-patterns section.
- Add explicit `.ToString()` inside holes warning to property-lists (it had no such guidance before).

### `dev-docs/claude-skills/`
- Mirror the same content (condensed) in `modernuo-string-handling.md`, `modernuo-networking.md`, `modernuo-property-lists.md`, `modernuo-gump-system.md`.
- Add audit rule #17 to `modernuo-code-audit.md` covering all 8 anti-patterns with severity WARNING, plus the `:L` format spec.

### `CLAUDE.md`
- Add audit rule #18 summarizing the interpolation anti-patterns + `:L`, pointing to `dev-docs/string-handling.md` for details.

## Why this matters

Before this PR there was no documentation explaining when an interpolated string call site silently allocates a string despite the receiving API providing a handler overload. The Phase 3 cleanup (PRs #2436/#2437/#2438) discovered ~28 such sites in the codebase; without these docs the same patterns would re-emerge. The new audit rule + CLAUDE.md entry will catch them at write time.
2026-05-03 18:23:50 -07:00
Kamron Batman
5f9fa88220
perf: Zero-alloc interpolation for SendMessage/Overhead APIs (#2434)
## Summary

Phase 1 of a multi-phase optimization to eliminate intermediate string allocations between `$"..."` interpolation and the packet text region for ModernUO's player-facing message APIs.

- Adds `[InterpolatedStringHandler]` overloads to every `Send*`/`Public/Local/Private/NonlocalOverheadMessage`/`Say`/`Emote`/`Whisper`/`Yell`/`SendLocalizedMessageTo` API in `OutgoingMessagePackets`, `Mobile`, and `Item`. Each overload is a 3-line shim that forwards `handler.Text` to the existing span-based path then calls `handler.Clear()` to return the rented `STArrayPool<char>` buffer (matches the established `SpanWriter.WriteAscii(ref RawInterpolatedStringHandler)` precedent).
- Converts `string text/args/affix/name` parameters to `ReadOnlySpan<char>` for consistency with the handler path. `lang` intentionally stays `string` (it's never interpolated and the `??= "ENU"` fallback stays cleaner).
- Adds `int charCount` overloads of the three `GetMaxMessage*Length` helpers so stackalloc sizing can avoid the redundant `ROS<char>` round-trip.
- Moves `Mobile` (17 methods) and `Item` (4 methods) message methods into new partial-class files (`Mobile.Messages.cs`, `Item.Messages.cs`) for organization.

No UOContent call sites change in this PR — existing `string`/`ROS<char>` calls compile unchanged via implicit conversion. Phase 2 (intermediate-string audit) and Phase 3 (cleanup PRs) follow.

## Files

- `Projects/Server/Network/Packets/OutgoingMessagePackets.cs` — `string` → `ROS<char>` for text params, `int charCount` length helpers added, class made `partial`
- `Projects/Server/Network/Packets/OutgoingMessagePackets.Interpolated.cs` (new) — 3 `ref RawInterpolatedStringHandler` extension overloads
- `Projects/Server/Mobiles/Mobile.cs` — message methods extracted (-262 lines)
- `Projects/Server/Mobiles/Mobile.Messages.cs` (new, 463 lines) — moved + ROS-converted methods + 25 handler overloads
- `Projects/Server/Items/Item.cs` — message methods extracted (-93 lines)
- `Projects/Server/Items/Item.Messages.cs` (new, 142 lines) — moved + ROS-converted methods + 4 handler overloads
- `Projects/Server.Tests/Tests/Network/Packets/Outgoing/MessagePacketTests.cs` — 3 new regression tests verifying byte-equivalence for the handler overloads
2026-05-03 18:04:02 -07:00
Chuck Thier
b150c48328
feat: Adds rope teleporter for New Haven Mines (#2439)
### Summary

- Add InteractiveTeleporter that teleports on double-click
- Add support to decorate command
- Add rope teleporters to the New Haven mines in the decoration file
2026-05-03 17:23:31 -07:00
Kamron Batman
22dc0937a8
perf: Migrate HouseRaffleManagementGump to DynamicGump (#2433)
## Summary
Converts `HouseRaffleManagementGump` from legacy `Gump` to `DynamicGump` with the static `DisplayTo` entry-point pattern.

The gump has paginated entries (up to 10 rows per page), conditional prev/next navigation buttons (vs. inactive image when at page boundary), and per-entry conditional layout (account-bearing vs. raw name). DynamicGump is the right choice.

Constructor is private; `DisplayTo` validates `from` / `NetState` / `stone.Deleted` before constructing. The list+sort runs eagerly in `DisplayTo` so paging math stays consistent across rebuilds.

Builder labels use `$"{value}"` interpolated-string-handler form for zero-allocation text.

Updates the caller in `HouseRaffleStone.ManagementEntry.OnClick`.
2026-05-03 10:50:54 -07:00
Kamron Batman
0d510c027c
perf: Migrate RewardGump to DynamicGump (#2432)
## Summary
Converts `RewardGump` and the inner `RewardConfirmGump` from legacy `Gump` to `DynamicGump` with the static `DisplayTo` entry-point pattern.

Both gumps have variable per-instance layout — the reward grid loops over `_rewards` and adds `AddItem(itemID, hue)` / `AddTooltip(tooltipID)` calls whose values are baked into the layout buffer per entry, so a cached static layout would be incorrect. DynamicGump is the right choice and avoids the placeholder dance for non-text values.

Constructors are private; `DisplayTo` validates `NetState`, null `rewards`/`onPicked`, and empty arrays before constructing.

Builder labels use `$"{value}"` interpolated-string-handler form for zero-allocation text.
2026-05-03 10:22:52 -07:00
Kamron Batman
402f3bc934
perf: Migrate vendor management gumps to DynamicGump/StaticGump (#2431)
## Summary
Converts `ReclaimVendorGump`, `VendorInventoryGump`, and the five gumps in `VendorRentalGumps.cs` from legacy `Gump` to `DynamicGump` / `StaticGump<T>` with the static `DisplayTo` entry-point pattern.

| Gump | Target | Reason |
|---|---|---|
| `ReclaimVendorGump` | DynamicGump | Variable vendor inventory list count. |
| `VendorInventoryGump` | DynamicGump | Variable inventory list; keeps `from` (per-row button depends on owner check). |
| `BaseVendorRentalGump` (abstract) → `VendorRentalContractGump`, `VendorRentalOfferGump`, `RenterVendorRentalGump`, `LandlordVendorRentalGump` | DynamicGump | Layout has many conditional sections driven by `GumpType` enum. |
| `VendorRentalRefundGump` | StaticGump | Fixed layout; vendor name, shop name, refund amount in `BuildStrings` placeholders. |

All builder labels and string slots use `$"{value}"` interpolated-string-handler form for zero-allocation text.

Updates callers in `PlayerMobile`, `HouseSign`, `RentedVendor`, and `VendorRentalContract`.
2026-05-03 10:19:12 -07:00
Kamron Batman
14f9d682e8
perf: Migrate PlayerVendor gumps to DynamicGump/StaticGump (#2430)
## Summary
Converts the five PlayerVendor-related gumps from legacy `Gump` to `DynamicGump` or `StaticGump<T>` using the static `DisplayTo` entry-point pattern.

| Gump | Target | Reason |
|---|---|---|
| `PlayerVendorBuyGump` | StaticGump | Fixed layout; per-instance `Description` and `Price` filled via `BuildStrings` placeholders. |
| `PlayerVendorOwnerGump` | StaticGump | Fixed layout; `HoldGold`, `BankAccount`, `perDay`, `days`, `earthDays` via placeholders. |
| `NewPlayerVendorOwnerGump` | DynamicGump | Layout varies by `goldHeld < perRealWorldDay` and `RentedVendor` branch. |
| `PlayerVendorCustomizeGump` | StaticGump | Categories-driven fixed layout. **Dropped unused `Mobile from` constructor arg** since it's only used in `OnResponse` (where `state.Mobile` is available). |
| `NewPlayerVendorCustomizeGump` | DynamicGump | BEARD section depends on `vendor.Female`. |

All builder slots and labels use `$"{value}"` interpolated-string-handler form for zero-allocation text. Inner `PVHuePicker` / `PVHairHuePicker` (HuePicker derivatives) are unchanged.

Updates callers in `PlayerVendor` and `PlayerBarkeeper`.
2026-05-03 10:04:12 -07:00
Kamron Batman
acae1c6ead
perf: Migrate HouseGumpAOS to DynamicGump (#2429)
## Summary
- Converts `HouseGumpAOS` from legacy `Gump` to `DynamicGump` with a private constructor and the static `DisplayTo` entry-point pattern.
- `AddPageButton`, `AddButtonLabeled`, and `AddList` helpers now write directly to the `DynamicGumpBuilder` via `ref` parameters.
- All internal re-display calls and the `HouseSign` / `ConfirmResizeHouseGump` callers route through `HouseGumpAOS.DisplayTo`.
- Field naming updated to underscore-prefix convention (`_house`, `_page`, `_from`, `_list`, `_hangerNumbers`, `_foundationNumbers`, `_postNumbers`, `_houseSigns`).
2026-05-03 09:52:17 -07:00
Kamron Batman
2e67e60703
perf: Migrate HouseGump to DynamicGump (#2428)
## Summary
- Converts the legacy pre-AOS `HouseGump` to `DynamicGump` with a private constructor and the static `DisplayTo` entry-point pattern.
- `HouseListGump` and `HouseRemoveGump` now route back through `HouseGump.DisplayTo` instead of constructing the gump directly.
- Updates the `HouseSign` caller accordingly.
2026-05-03 09:33:20 -07:00
Kamron Batman
1ea69d3d40
perf: Migrate Barkeeper customization gump to StaticGump (#2427)
Splits BarkeeperGump (DynamicGump) into two StaticGump<T> variants selected by body type — Human (modifiable appearance) and NonHuman (no appearance/gender controls). Each variant gets its own cached static layout via a CRTP base; dynamic per-instance text (rumor messages, keywords, tip message) is filled via slot placeholders in BuildStrings.

Moves PlayerBarkeeper, BarkeeperGump, and BarkeeperTitleGump into a dedicated Mobiles/Vendors/Barkeeper/ folder.

Pulls the Back button on the appearance-categories page out of the ModifyAppearance branch so non-human barkeepers no longer hit a dead end on that page.
2026-05-03 02:57:21 -07:00
Kamron Batman
29e4ecdb1b
fix: Preserve corpse notoriety across server restart (#2426)
BaseCreatures are deleted on death (Mobile.OnDeath calls Delete for non-players), so after save/restart the corpse's _owner reference resolves to null. CorpseNotoriety gated its entire creature branch on `target.Owner is BaseCreature`, falling through to player-corpse logic once the reference vanished. That made monster corpses turn red (body.IsMonster -> Murderer) and innocent NPC corpses turn grey (null is not PlayerMobile -> CanBeAttacked) on the next restart.

Snapshots the relevant owner state into CorpseFlag at corpse creation: OwnerWasBaseCreature, OwnerWasSummoned, OwnerWasAnimatedDead. Folds the standalone _murderer bool into CorpseFlag.Murderer for consistency with Criminal. CorpseNotoriety now consults the flags so the creature branch stays correct without a live mobile reference.

Bumps Corpse serialization to v16 with a MigrateFrom(V15Content) that maps the old Murderer bool onto the new flag. Pre-fix corpses already on disk decay within 7 minutes; their first post-restart color may be wrong, which is acceptable.

Also documents that the schema generator must be run after every version bump (`dotnet tool run ModernUOSchemaGenerator -- ModernUO.slnx`) since `dotnet build` does not emit migration JSON files.
2026-05-03 02:15:01 -07:00
Kamron Batman
a74c7f9d4e
perf: Migrates ConPVP lobby gumps from legacy Gump. (#2423)
## Summary

Migrates 14 ConPVP lobby/tournament gumps from legacy `Gump` to modern `DynamicGump`/`StaticGump<T>`. Layouts move into `BuildLayout(ref DynamicGumpBuilder)`, constructors become private, and validation moves into static `DisplayTo` entry points (empty-gump rule).

**Per-gump base type decisions:**

- `BeginGump` → `StaticGump<BeginGump>`: layout is fully fixed (no dynamic content). All other gumps below are `DynamicGump` because they bake dynamic player names, guild abbreviations, ruleset titles, arena names, tournament participant names, ladder rankings, or per-instance rule modifications. Per the cliloc/dynamic-text rule, dynamic content forces `DynamicGump`.
- `ReadyGump`, `ReadyUpGump` → `DynamicGump` (per-instance participant rosters).
- `AcceptDuelGump`, `AcceptTeamGump`, `ConfirmSignupGump` → `DynamicGump` (challenger/registrar/team names, dynamic rule modifications).
- `PickRulesetGump`, `RulesetGump` → `DynamicGump` (ruleset titles and option labels per instance).
- `ParticipantGump`, `DuelContextGump` → `DynamicGump` (player rosters/team labels).
- `LadderGump` → `DynamicGump` (ladder entries: ranks, levels, guild abbrs, names, wins/losses).
- `ArenaGump` → `DynamicGump` (arena names with active player names).
- `PreferencesGump` → `DynamicGump` (arena name list).
- `TournamentBracketGump` (~1k LOC) → `DynamicGump`. The whole gump is one type-switched view that re-renders on every button press across `Index`, `Rules_Info`, `Participant_List`, `Participant_Info`, `Round_List`, `Round_Info`, `Match_Info`, `Player_Info`. All branches bake per-instance content.

**Refresh-via-this conversions (the big perf wins):**

- `LadderGump`: page +/- now mutates `_page` and calls `from.SendGump(this)` instead of allocating a new `LadderGump`.
- `PickRulesetGump`: ruleset apply / flavor toggle now refreshes via `this`.
- `ParticipantGump`: increase/decrease team size, remove player, target failure all refresh via `this`.
- `DuelContextGump`: failed-start and add-participant refresh via `this`.
- `ConfirmSignupGump`: every signup-validation rejection branch in `OnResponse` and every `AddPlayer_OnTarget` rejection branch refreshes via `this` (was allocating a new gump per branch).
- `TournamentBracketGump`: every navigation button (back/forward, type change, page change, drill-down) mutates `_type`/`_object`/`_list`/`_page` and refreshes via `this`. Previously each click allocated a new 1k LOC gump.

All gumps are `Singleton`, use private constructors with static `DisplayTo` entry points that null-check `NetState` before allocation. External callers in `DuelContext`, `TournamentBracketItem`, `TournamentController`, `TournamentSignupItem`, and the cross-references between `AcceptDuelGump`/`ParticipantGump`/`AcceptTeamGump`/`ConfirmSignupGump` are all updated to use `DisplayTo`. Legacy `m_X` fields renamed to `_x` per coding standards.
2026-05-03 01:06:04 -07:00
Kamron Batman
9c2ac2b8ea
perf: Migrates New Guild System gumps from legacy Gump. (#2422)
## Summary

Migrates the eight concrete New Guild System gumps (CreateGuild, GuildInfo,
GuildMemberInfo, GuildRoster, GuildDiplomacy, WarDeclaration,
GuildAdvancedSearch, GuildInvitationRequest) and three abstract bases
(BaseGuildGump, BaseGuildListGump, OtherGuildInfo) from the legacy `Gump`
class to `DynamicGump`. Layout work moves from constructor-side `AddX(...)`
calls into `BuildLayout(ref DynamicGumpBuilder builder)` — the abstract
`BaseGuildGump` now provides a `BuildContent` callout for shared
tab-strip chrome, and `BaseGuildListGump<T>` adds another
`BuildListExtras` hook so subclasses can paint highlighted titles after
the filter/sort/pagination chrome.

The headline win is the **self-refresh pattern** on the list gumps and
diplomacy advanced search. Previously each filter/sort/back/forward
click allocated a brand new gump via `GetResentGump`. After migration,
those handlers mutate `_filter`, `_startNumber`, `_comparer`, `_ascending`,
or `_display` on the existing gump and call `from.SendGump(this)`,
letting the singleton path in `NetStateGumps.Send` swap in the same
instance with the new layout. The original list is preserved separately
from the per-render filtered/sorted `_displayList`, so refreshes pick up
the latest state without losing the source list.

All guild gumps deal with per-instance dynamic strings (guild names,
member names, war declarations, alliance names), which would defeat
`StaticGump<T>` caching per the cliloc rule, so every concrete subclass
migrates to `DynamicGump`. `AllianceRosterGump` (in `Misc/Guild.cs`)
is a `GuildDiplomacyGump` subclass and inherits the new behavior; its
unused override and stored alliance reference were dropped along with
the now-obsolete `GetResentGump` abstract.
2026-05-03 00:09:22 -07:00
Kamron Batman
598c3c125c
perf: Migrate Item Interaction gumps to DynamicGump/StaticGump (#2421)
## Summary

Migrates 11 player-facing Item Interaction gumps from legacy `Gump` to `StaticGump<T>` or `DynamicGump`.

| File | Gump | Base | Reason |
|---|---|---|---|
| `Items/Deeds/NameChangeDeed.cs` | `NameChangeDeedGump` | `StaticGump<T>` | Fully fixed layout |
| `Items/Deeds/HairRestylingDeed.cs` | `HairRestylingDeedGump` (renamed from `InternalGump`) | `DynamicGump` | Per-race/per-gender cliloc IDs and gump images vary every instance |
| `Items/Deeds/HolidayTreeDeed.cs` | `HolidayTreeChoiceGump` | `StaticGump<T>` | Fully fixed (Classic/Modern radio) |
| `Items/Deeds/NewPlayerTicket.cs` | `NewPlayerTicketGump` (renamed from `InternalGump`) | `StaticGump<T>` | Fully fixed gift menu |
| `Items/Misc/PromotionalToken.cs` | `PromotionalTokenGump` | `DynamicGump` | `TextDefinition` (`ItemGumpName`) varies per token subclass — cliloc/string differs |
| `Items/Misc/SpecialHairDye.cs` | `SpecialHairDyeGump` | `StaticGump<T>` | Static entry array drives a fixed layout |
| `Items/Misc/InteriorDecorator.cs` | `InteriorDecoratorGump` (renamed from `InternalGump`) | `DynamicGump` | Button art flips per `decorator.Command` (Turn/Up/Down) |
| `Items/Special/8th Anniversary Items/Dawn's Music Box/DawnsMusicBoxGump.cs` | `DawnsMusicBoxGump` | `DynamicGump` | Page count derived from variable `Tracks.Count` |
| `Items/Misc/PowerGenerator.cs` | `PowerGeneratorGump` (renamed from `GameGump`) | `DynamicGump` | Variable side length 3-6 and per-step node hues |
| `Gumps/HeritageTokenGump.cs` | `HeritageTokenGump` | `StaticGump<T>` | All 7 pages fully baked — only static cliloc IDs |
| `Gumps/ConfirmHeritageGump.cs` | `ConfirmHeritageGump` | `DynamicGump` | Confirmation cliloc chosen at runtime per item selected |

All gumps use `Singleton => true`, private constructors, and a static `DisplayTo` entry point that validates prerequisites before constructing.

External callers updated (`HeritageToken`, `DawnsMusicBox`) to the new `DisplayTo` entry points. `Console.WriteLine` in `ConfirmHeritageGump` exception handler replaced with `LogFactory`-backed logger.
2026-04-26 11:31:49 -07:00
Kamron Batman
15e506ffc2
perf: Migrate Old Guild System gumps to DynamicGump (#2420)
## Summary

Migrates the Old Guild System (pre-AOS guild stones) gumps from the legacy `Gump` class to `DynamicGump`, following the same pattern used for the Quest gump migration in #2416. All concrete gumps now have private constructors gated by static `DisplayTo` entry points (empty-gump rule), and `Singleton => true` is set across the board so reopening a sibling dialog automatically closes the previous one.

**Migrated gumps:**
- `GuildGump` - main guild dialog
- `GuildmasterGump` - guildmaster functions
- `GuildCharterGump` - charter and website display
- `GuildWarGump` - warfare status (kept as player-facing)
- `GuildWarAdminGump` - war menu (retained as player-facing - reachable from `GuildmasterGump`'s WAR button by guildmasters)
- `GuildChangeTypeGump` - Standard/Order/Chaos selection

**Abstract bases:** `GuildListGump` and `GuildMobileListGump` keep their shared list-rendering chrome inside a single concrete `BuildLayout` on the abstract class and expose a `protected abstract void BuildHeader(ref DynamicGumpBuilder builder)` hook for subclasses (replacing the old `Design()` override). This mirrors the abstract-base treatment used for the ML quest base in the quest-gump migration PR.

**Concrete subclasses migrated alongside the abstract bases:**
- `GuildListGump` subclasses: `GuildAcceptWarGump`, `GuildDeclarePeaceGump`, `GuildDeclareWarGump`, `GuildRejectWarGump`, `GuildRescindDeclarationGump`
- `GuildMobileListGump` subclasses: `DeclareFealtyGump`, `GrantGuildTitleGump`, `GuildAdminCandidatesGump`, `GuildCandidatesGump`, `GuildDismissGump`, `GuildRosterGump`

**Cliloc rule:** Every gump bakes per-instance dynamic content (guild names, member names, war declarations, candidate lists), which would defeat `StaticGump<T>` caching. Per the cliloc rule, all are `DynamicGump`.

**External callers updated:** the prompt files (`GuildAbbrvPrompt`, `GuildCharterPrompt`, `GuildDeclareWarPrompt`, `GuildNamePrompt`, `GuildTitlePrompt`, `GuildWebsitePrompt`), `RecruitTarget`, the `Guildstone` item, and the New Guild System `GuildInfoGump`'s Order/Chaos handler all now go through static `DisplayTo` entry points instead of `new XGump(...)`.
2026-04-26 10:44:11 -07:00
Kamron Batman
70a69d3efe
perf: Migrate ConPVP game board gumps to DynamicGump (#2419)
## Summary

Migrates the four ConPVP game board (scoreboard) gumps from legacy `Gump` to `DynamicGump`:

- **`BRBoardGump`** (Bombing Run) — variable layout: row-per-team based on `Participants.Count`. Migrated to `DynamicGump`, `Singleton`, private constructor + `DisplayTo`, `SetNoClose()`.
- **`CTFBoardGump`** (Capture the Flag) — variable layout: row-per-team filtered to only teams with a flag. Same migration shape.
- **`DDBoardGump`** (Double Domination) — variable layout: row-per-team. Same migration shape.
- **`KHBoardGump`** (King of the Hill) — variable layout: row-per-team. `sealed`. Same migration shape.

### Refresh-via-this decision

For all four boards, **score data lives on the `*Game` / `*TeamInfo` objects, not the gump**. The gump just renders a snapshot of those values at the moment it is sent. The three call sites per board are:

1. `OnDoubleClick` on the in-world scoreboard item — one-shot manual open.
2. After death/kill score events (in `OnDeath`) — game logic pushes a fresh board to the dying player so they see updated scores.
3. End-of-game broadcast loop — sends final results to every participant.

None of these are button-driven refreshes from inside the gump, and the gump owns no mutable state. Therefore each event allocates a fresh gump (now via `DisplayTo(...)`) rather than calling `SendGump(this)` on a long-lived instance — that pattern doesn't fit when the data source is external. The win comes from `DynamicGump`'s ref-struct builder writing directly to buffers, eliminating the legacy `GumpEntry` list allocations on every send.

### Mechanics

- `: Gump` → `: DynamicGump`; layout moved from constructor to `BuildLayout(ref DynamicGumpBuilder)`.
- `Closable = false` → `builder.SetNoClose()`.
- Constructors are `private`; static `DisplayTo(Mobile, *Game, ...)` validates `mob?.NetState != null && game != null` before constructing.
- Team-section-mode parameter (`section`) preserved on BR / CTF / DD as an optional `DisplayTo` param even though no current caller uses it.
- The four `m_Game` / similar fields are renamed to `_game`; new fields use `_camelCase` per CLAUDE.md §12.
- `AddBorderedText` / `AddColoredText` helpers became `static` and take `ref DynamicGumpBuilder`.
- Updated all 12 internal call sites (3 per file) to go through `DisplayTo`. No external callers.
- No `OnResponse` was defined on any of these gumps (the only button is a close button), so no `RelayInfo` signature changes were needed.

Touches 4 game files, but only the gump classes — game logic (BR death/scoring, CTF flag handling, DD domination, KH king timer) is untouched.
2026-04-26 09:55:46 -07:00
Kamron Batman
802849bebc
perf: Migrate Bulletin/Poll/SOS gumps to DynamicGump (#2418)
## Summary

Migrates three legacy `Gump`-based dialogs to the modern builder API:

- **PlayerBBGump** (`Projects/UOContent/Items/Misc/PlayerBulletinBoards.cs`) — `DynamicGump`. The bulletin board renders a different post per page (variable per-instance state), so the layout cannot be cached. Now `Singleton`, with a private constructor and a static `DisplayTo` entry point. Scroll/banish/delete/post-props buttons mutate `_page` and self-refresh via `SendGump(this)` instead of allocating a fresh gump on every click. Prompt-driven flows (post message / set title / post greeting) re-enter through `DisplayTo` after the prompt completes.
- **MessageGump** + **OldMessageGump** (`Projects/UOContent/Items/Skill Items/Fishing/Misc/SOS.cs`) — `StaticGump<T>`. Both render a fixed structure (background + body + button) where only the formatted sextant coordinate string varies per SOS bottle. The cliloc IDs that *appear in the gump packet* are constant (`MessageGump` uses 1018326; `OldMessageGump` uses no `AddHtmlLocalized` at all — its message is pre-formatted into a string before construction), so the layout cache is safe per the cliloc rule. The varying coordinate text is fed through an HTML placeholder via `BuildStrings`. Both gumps are now `Singleton` with private constructors and static `DisplayTo` entry points.
- **ShardPollGump** (`Projects/UOContent/Misc/ShardPoller.cs`) — `DynamicGump`. The gump's structure changes both with the number of poll options (variable loop) and with the `editing` flag (admin sees radios + add-option row + result percentages; players see only radios). The dual-purpose view is preserved as a single `DynamicGump` with the `editing` flag still controlling layout shape — staff path verified to still render the editor with the totals header, vote percentages, and "Create new option" radio. `Closable = false` becomes `builder.SetNoClose()`. Now `Singleton`, with private constructor and a `DisplayTo` that returns the gump instance so `EventSink_Login_Callback` can still call `QueuePoll` on it. The cancel/edit re-issue paths use `SendGump(this)` for self-refresh; the queued login flow uses `DisplayTo` so each queued poll gets its own gump.

External callers (`OnDoubleClick`, `PostPrompt`, `SetTitlePrompt`, `ShardPollPrompt`, `EventSink_Login_Callback`, the Timer-delayed queued poll send) all updated to use the new `DisplayTo` entry points. Legacy `m_X` field naming was already absent in two of the three files; the bulletin board fields kept their `_camelCase` names. `dotnet build Projects/UOContent/UOContent.csproj` reports 0 warnings, 0 errors.
2026-04-25 20:49:12 -07:00
Kamron Batman
4e565ca6da
perf: Migrate SoulStone and TMap chest gumps to DynamicGump (#2417)
## Summary

Migrates the five-step SoulStone wizard and the TreasureMapChest remove-confirmation dialog from legacy `Gump` to the modern builder API.

Per-gump base type:

- **`SelectSkillGump` -> `DynamicGump`** -- the skill picker iterates the player's skill list and emits one button per non-zero skill, so the layout shape varies per instance.
- **`ConfirmSkillGump` -> `DynamicGump`** -- skill name uses `AosSkillBonuses.GetLabel(...)` which returns dynamic clilocs in the `1044060 + (int)skill` range, plus current/cap skill values rendered as text labels.
- **`ConfirmTransferGump` -> `DynamicGump`** -- same dynamic skill cliloc plus per-instance Base/Cap/Stored values.
- **`ConfirmRemovalGump` -> `StaticGump<ConfirmRemovalGump>`** -- only fixed clilocs (warning text, Continue, Cancel), so the layout caches.
- **`ErrorGump` -> `DynamicGump`** -- title and message clilocs are constructor parameters that vary per call site.
- **`TreasureMapChest.RemoveGump` -> `StaticGump<RemoveGump>`** -- fixed-cliloc confirmation prompt (no item list, despite the name); `Closable=false`/`Disposable=false` are now `builder.SetNoClose()`/`builder.SetNoDispose()`.

All six gumps are now `Singleton => true`, have private constructors, and expose a static `DisplayTo` entry point that validates `from`, `NetState`, and the underlying entity before constructing -- prevents the empty-gump leak. Wizard navigation between steps now goes through `DisplayTo` (e.g. `ConfirmSkillGump.DisplayTo(from, _stone, skill)` from the skill picker, `ErrorGump.DisplayTo(...)` from absorption pre-checks, `SelectSkillGump.DisplayTo(...)` from the "make another selection" button on `ConfirmSkillGump` and from `ErrorGump` bounce-back). Because each gump is Singleton, sending the same type again automatically closes any prior instance instead of stacking; the explicit `gumps.Close<T>()` chain on `OnDoubleClick` is preserved so opening the soulstone still resets any orphaned step from another wizard.

`OnResponse` now uses `in RelayInfo info`. All inline `AddX(...)` calls move to `builder.AddX(...)` inside `BuildLayout`. `Skill.Base.ToString("F1")` etc. are converted to `$"{value:F1}"` interpolation passed to `AddLabel(ReadOnlySpan<char>)`. Skill picker pagination still uses client-side `AddPage` / `GumpButtonType.Page` -- no server-state pagination to migrate.
2026-04-25 20:38:24 -07:00
Kamron Batman
b90ac0d481
perf: Migrate Quest gumps to DynamicGump (#2416)
## Summary

Migrates the Quest system gumps from legacy `Gump` to `DynamicGump` / `StaticGump<T>`.

**Renames** `Engines/ML Quests/Gumps/BaseQuestGump` to **`BaseMLQuestGump`** to
disambiguate from the Core quest abstract (`Engines/Quests/Core/QuestSystem.cs`).
Both abstracts now extend `DynamicGump`.

**Abstract bases**:
- `Server.Engines.Quests.BaseQuestGump` (Core) - now `abstract DynamicGump`. Holds constants and a static `AddHtmlObject(ref DynamicGumpBuilder, ...)` helper. Concrete subclasses each provide their own `BuildLayout`.
- `Server.Engines.MLQuests.Gumps.BaseMLQuestGump` (ML, renamed) - now `abstract DynamicGump` with a `protected abstract BuildContent(ref DynamicGumpBuilder)` hook. The base's `BuildLayout` draws shared chrome (background art, frame, label header) and then invokes `BuildContent`; subclasses use `BuildPage`, `SetTitle`, `RegisterButton`, `SetPageCount`, and content helpers (`AddDescription`, `AddObjectives`, `AddObjectivesProgress`, `AddRewardsPage`, `AddRewards`, `AddConversation`).

**Concrete Core gumps** migrated to `DynamicGump`:
- `QuestCancelGump`, `QuestOfferGump` (Core), `QuestObjectivesGump`, `QuestConversationsGump`, `QuestLogUpdatedGump`, `QuestItemInfoGump`, `SheetMusicOfferGump` (Impresario), `PaintedImageGump` (renamed from `PaintedImage.InternalGump`).

**Concrete ML gumps** migrated to `DynamicGump` (extending `BaseMLQuestGump` or directly):
- `InfoNPCGump`, `QuestConversationGump`, `QuestLogDetailedGump`, `QuestLogGump`, `QuestOfferGump` (ML), `QuestReportBackGump`, `QuestRewardGump`, `QuestCancelConfirmGump`, `RaceChangeConfirmGump`.

**`StaticGump<T>` migrations**:
- `ScrollOfAbraxusGump` (Dark Tides). Its layout is a single hard-coded cliloc (1060116) with no per-instance dynamic content - safe to cache.

**Cliloc rule**: Every other quest dialog bakes per-instance cliloc IDs into its layout (quest titles, NPC names, race-specific prompts, era-conditional progress messages, escort destinations). Per the cliloc rule, baking different cliloc numbers into a cached layout would defeat `StaticGump<T>` caching - so all of these are `DynamicGump`.

**Signature changes** to support the migration:
- `QuestObjective.RenderMessage`/`RenderProgress` now take `ref DynamicGumpBuilder builder` (15 overrides updated across Collector, Solen Matriarch, Ambitious Solen Queen, Study of the Solen Hive, Terrible Hatchlings, The Summoning, Uzeraan Turmoil, Witch Apprentice, Emino's Undertaking).
- ML `BaseObjective.WriteToGump` / `BaseObjectiveInstance.WriteToGump` / `BaseReward.WriteToGump` now take `ref DynamicGumpBuilder` (KillObjective, GainSkillObjective, EscortObjective, CollectObjective, DeliverObjective, BaseReward).

**Empty-gump and `Singleton` rules**: All concrete gumps now have private constructors with static `DisplayTo` entry points that null-check the player NetState before allocation. All gumps that shouldn't stack are `Singleton => true`.

**External callers updated**: `MLQuest.SendOffer`/`OnRefuse`, `MLQuestEntry.SendProgressGump`/`SendRewardGump`/`SendReportBackGump`, `MLQuestSystem.QuestGumpRequest` and `ViewQuestsCommand`, `BoonCollector` (Darius/Nedrick), `SirHelper.OnDoubleClick` (no longer caches a single shared `InfoNPCGump` instance - `DisplayTo` constructs one per click and the gump's `Singleton => true` handles deduplication), `RaceChangeDeed.OnDoubleClick`, `PaintedImage.OnDoubleClick`, `ScrollOfAbraxus.OnDoubleClick`, `Impresario.OnTalk`, and `PlayerMobile`'s `BaseQuestGump` alias is now `BaseMLQuestGump`.

**Concrete subclasses found beyond the listed entry points**: `SheetMusicOfferGump` (in Impresario.cs), `PaintedImageGump` (was `PaintedImage.InternalGump`), `QuestObjectivesGump`, `QuestConversationsGump`, `QuestLogUpdatedGump`, `QuestItemInfoGump` (the Core base has these embedded across `QuestSystem.cs`/`QuestObjective.cs`/`QuestConversation.cs`/`QuestItemInfo.cs`).

**Code-standards cleanup**: Renamed legacy `m_X` private fields to `_x` in rewritten files; braces on all control flow.
2026-04-25 20:23:13 -07:00
Kamron Batman
ab5f615840
perf: Migrate Veteran Reward gumps to DynamicGump/StaticGump (#2415)
## Summary

Migrates 16 player-facing legacy `Gump` subclasses in the Veteran Rewards system to `DynamicGump` or `StaticGump<T>`, renames every nested `InternalGump`/`ConfirmGump` to system-prefixed names, and updates external callers to use new static `DisplayTo` entry points.

### Reward chain (Engines/Veteran Rewards)

| Gump | Base type | Reasoning |
|---|---|---|
| `RewardNoticeGump` | `StaticGump<T>` | Fixed layout |
| `RewardChoiceGump` | `DynamicGump` | Layout shape varies (per-player categories, entries, pages) |
| `RewardConfirmGump` | `DynamicGump` | `entry.Name` is a dynamic cliloc per reward (cliloc rule) |
| `RewardOptionGump` | `DynamicGump` | Title cliloc and per-option clilocs are dynamic (cliloc rule) |
| `RewardDemolitionGump` | `DynamicGump` | `question` cliloc is dynamic per caller (cliloc rule) |

### Statue (Engines/Veteran Rewards/Character Statue Maker)

| Gump | Base type | Reasoning |
|---|---|---|
| `CharacterStatueGump` | `DynamicGump` | Pose/Direction/Material labels are dynamic clilocs (cliloc rule) |
| `CharacterPlinthGump` | `DynamicGump` | `statue.Name`, sculpted-on date, and statue-type cliloc are dynamic (cliloc rule) |

### Decorations (Items/Special/Veteran Rewards) — every `InternalGump` renamed

| Original | New name | Base type | Reasoning |
|---|---|---|---|
| `Banner.InternalGump` | `BannerGump` | `StaticGump<T>` | Fully fixed item-id picker over `Start..End` range |
| `Brazier.InternalGump` | `BrazierGump` | `StaticGump<T>` | Fixed two-item picker |
| `Cannon.InternalGump` | `CannonGump` | `DynamicGump` | `keg.Validate()` value passed as `~1_CHARGES~` arg (cliloc-arg rule) |
| `DecorativeShield.InternalGump` | `DecorativeShieldGump` | `StaticGump<T>` | Fixed item picker over a constant range |
| `DecorativeShield.FacingGump` | `DecorativeShieldFacingGump` | `DynamicGump` | Item ids depend on instance state |
| `HangingSkeleton.InternalGump` | `HangingSkeletonGump` | `StaticGump<T>` | Fixed item picker (5 hard-coded ids) |
| `PottedCactus.InternalGump` | `PottedCactusGump` | `StaticGump<T>` | Fixed 6-item picker |
| `StoneAnkh.InternalGump` | `StoneAnkhGump` | `StaticGump<T>` | Fixed two-direction picker |
| `WallBanner.InternalGump` | `WallBannerGump` | `StaticGump<T>` | Fixed 30-banner picker across 5 pages |
| `WeaponEngravingTool.InternalGump` | `WeaponEngravingToolGump` | `StaticGump<T>` | Fixed text-entry dialog |
| `WeaponEngravingTool.ConfirmGump` | `WeaponEngravingToolConfirmGump` | `DynamicGump` | Layout branches on `guildmaster != null` |

### External callers updated

`RewardSystem`, `CharacterStatue`, `FireFliesDeed`, `FlamingHead`, `Banner`, `DecorativeShield`, `HangingSkeleton`, `StoneAnkh` (RewardDemolitionGump callers); `AnkhOfSacrifice`, `Cannon`, `MiningCart`, `MinotaurStatue`, `TreeStump` (RewardOptionGump callers); `TinkerGuildmaster` (engraver confirm gump).

The pre-existing `FacingGump` inside `Banner.cs` (already `DynamicGump`) was left as-is — it is private, nested inside `InternalTarget`, and never referenced outside.
2026-04-25 20:09:27 -07:00
Kamron Batman
50599e0453
perf: Migrate NPC and Skill UI gumps to DynamicGump/StaticGump (#2414)
## Summary

Migrates five legacy `Gump`-derived UI dialogs in the NPC and Skill domains to the modern `DynamicGump` builder pipeline. All five gumps were chosen as `DynamicGump` rather than `StaticGump<T>` because their layout shape varies per instance, and several of them carry per-instance localization numbers (cliloc IDs) that the static cache cannot bake (see CLAUDE.md gump-system rule and `dev-docs/gump-system.md`).

Per-gump rationale:

- **`TownCrierGump` (`Mobiles/Townfolk/TownCrier.cs`)** - DynamicGump. Announcement count varies, expiration text is rebuilt per render via `ValueStringBuilder`, and one button per entry is emitted in a loop.
- **`ClaimListGump` (`Mobiles/Vendors/NPC/AnimalTrainer.cs`)** - DynamicGump. The pet list and resulting background/alpha-region heights vary per stabling player.
- **`AnimalLoreGump` (`Skills/AnimalLore.cs`)** - DynamicGump. Page count itself varies (3 pages pre-AOS, 5 pages on AOS) and several `AddHtmlLocalized` calls use cliloc IDs computed from per-creature data (loyalty rating `1049595 + c.Loyalty / 10`, food preference, pack instinct), which violates the StaticGump cliloc-bake rule.
- **`DisguiseGump` (`Items/Skill Items/Thief/DisguiseKit.cs`)** - DynamicGump. Page count and entry order shift on `from.Female`, `Body.IsFemale`, and `startAtHair`.
- **`CommentsGump` (`Gumps/CommentsGump.cs`)** - DynamicGump. Comment list and pagination depend on `Account.Comments` size; the title label encodes the variable account username string.

All five are now `Singleton => true`, have `private` constructors, and expose static `DisplayTo(...)` entry points that validate prerequisites before constructing - guaranteeing no empty gumps (CLAUDE.md Sec.13). All internal refresh paths (prompts, `OnDoubleClick`, command handlers, target callbacks) were updated to call `DisplayTo` rather than `new XGump(...)`. Legacy `m_`-prefixed fields renamed to `_camelCase` (CLAUDE.md Sec.12), and `DisguiseEntry`'s `m_`-prefixed public readonly fields converted to PascalCase auto-properties. `OnResponse` signatures updated to `in RelayInfo info`. No external callers needed updating - all `new XGump(...)` sites lived inside the same files.
2026-04-25 19:51:42 -07:00
Kamron Batman
ec85d97482
perf: Migrate Holiday and Decorative gumps to DynamicGump/StaticGump (#2413)
## Summary

Migrates 10 player-facing legacy `Gump` subclasses for holiday and decorative items to the modern `DynamicGump` / `StaticGump<T>` system. All migrated gumps are `Singleton`, use private constructors gated by static `DisplayTo(...)` entry points (empty-gump rule, CLAUDE.md §13), and replace legacy `m_X` fields with `_x` per coding standards.

Per-gump base type and rationale:

- **Mistletoe.cs** — `MistletoeAddonGump` -> `StaticGump<MistletoeAddonGump>`. Fixed re-deed confirmation layout.
- **StValentinesBears.cs** — Renamed `InternalGump` -> `StValentinesBearsGump`, base `StaticGump<T>`. Fixed sign-bear layout with three text entries; legacy `m_Bear` -> `_bear`. Switched legacy `AddTextEntry(..., size)` -> `AddTextEntryLimited(...)` (modern API split).
- **Wreath.cs** — `WreathAddonGump` -> `StaticGump<WreathAddonGump>`. Fixed re-deed confirmation layout.
- **HolidayPottedPlant.cs** — Renamed `InternalGump` -> `HolidayPottedPlantGump`, base `StaticGump<T>`. Fixed plant-picker layout.
- **SnowStatue.cs** — Renamed `InternalGump` -> `SnowStatueGump`, base `StaticGump<T>`. Fixed statue-picker layout. Dropped unused `Mobile from` ctor arg.
- **TapestryOfSosaria.cs** — Renamed `InternalGump` -> `TapestryOfSosariaGump`, base `StaticGump<T>`. Single image, fixed.
- **HouseRaffleDeed.cs** — `WritOfLeaseGump` -> `DynamicGump`. Description HTML is computed per-instance from deed expiration / days-left, so layout text varies per instance. Added `Singleton => true` (was missing implicitly via legacy default).
- **SpecialScroll.cs** — Renamed `InternalGump` -> `SpecialScrollGump`, base `DynamicGump`. **Cliloc rule**: `_scroll.Message`, `_scroll.Title`, `_scroll.SkillLabel` are dynamic cliloc numbers per scroll type — `AddHtmlLocalized` bakes the cliloc number into the cached layout, so `StaticGump<T>` cannot cache it.
- **BallotBox.cs** — Renamed `InternalGump` -> `BallotBoxGump`, base `DynamicGump`. Layout shape varies: variable topic-line count, owner-vs-voter buttons, and vote-tally bars all add/remove elements. Legacy `m_Box` -> `_box`. Updated the `TopicPrompt` callbacks to call `BallotBoxGump.DisplayTo(...)` instead of allocating a new gump directly.
- **AquariumGump.cs** — `AquariumGump` -> `DynamicGump`. Per-page layout depends on each item's `LabelNumber` and (for `BaseFish`) `GetDescription()` cliloc — those vary per fish/decoration. Two `DisplayTo` overloads (auto-detect access vs explicit edit flag) to mirror the original two call sites in `Aquarium.cs`. Legacy `m_Aquarium` -> `_aquarium`.

### Skipped

- **HouseRaffleManagementGump.cs** — Skipped as **staff-only**. Only invoked via `ManagementEntry` context entry inside `HouseRaffleStone.cs`, gated by `from.AccessLevel >= AccessLevel.Seer`. Per task instructions, staff-only gumps are out of scope for this PR.

### External callers updated

- `Aquarium.cs` — Two `new AquariumGump(...)` sites swapped to `AquariumGump.DisplayTo(...)`.

All other migrated inner classes were nested in the same file and only had local references, which were updated to call the new `DisplayTo(...)` static entry point.
2026-04-25 17:22:41 -07:00
Kamron Batman
83f771c99a
perf: Migrate Travel/Moongate gumps to DynamicGump (#2412)
## Summary

Migrates the three player-facing travel/moongate gumps from legacy `Gump` to the modern `DynamicGump` system.

- `GoGump` (Gumps/Go/GoGump.cs) → `DynamicGump`. The category-tree layout's row count varies with the current `GoCategory`'s child count and pagination. Refreshes via `SendGump(this)` after mutating `_node`/`_page` instead of allocating a new instance per nav/page click.
- `MoongateGump` (Items/Misc/PublicMoongate.cs) → `DynamicGump`. The destination tab strip and per-map pages are gated by ruleset (sigil bearer, murderer, faction facet) and expansion/young flag, plus the configured map selection. The set of pages and the active-map swap make the layout shape per-instance.
- `MoongateConfirmGump` (Items/Skill Items/Magical/Misc/Moongate.cs) → `DynamicGump`. Per the **dynamic-cliloc rule**, the gump bakes one of two different cliloc numbers (1062050 Felucca-warning vs 1062049 generic confirm) and selects between an AOS and pre-AOS layout shape — both characteristics force `DynamicGump` rather than `StaticGump<T>` because cached layout bytes would otherwise lock in the wrong cliloc/shape.

All three now use a `private` constructor with a `public static DisplayTo(...)` entry point that validates prerequisites before any gump is allocated (empty-gump rule, CLAUDE.md §13). All are `Singleton` and use `SendGump(this)` self-refresh on internal navigation. Updated callers: `PublicMoongate.UseGate` and `Moongate.BeginConfirmation` now call `DisplayTo(...)`.
2026-04-25 17:03:24 -07:00
Kamron Batman
2423950cca
perf: Migrate Plant gumps to DynamicGump/StaticGump (#2411)
## Summary

Second PR in the player-facing legacy gump migration. Converts the four Plants system gumps:

- `MainPlantGump`, `ReproductionGump`, `EmptyTheBowlGump` → `DynamicGump`. Layout varies by plant status, growth stage, health, and pollination/resource availability — cannot use cached `StaticGump<T>`.
- `SetToDecorativeGump` → `StaticGump<SetToDecorativeGump>`. Pure confirmation dialog with no per-instance variation.

All four:

- `Singleton => true` (auto-replace previous plant gump on re-open instead of stacking)
- Constructor `private`; entry is static `DisplayTo(Mobile, PlantItem)` per the empty-gump rule (CLAUDE.md §13)
- `OnResponse` self-refresh paths converted from `from.SendGump(new XGump(_plant))` to `from.SendGump(this)` — saves an allocation on every help/info button click and on every "gather resources/seeds/pollen" action
- Helper draw methods take `ref DynamicGumpBuilder builder` instead of mutating instance state
- Renamed legacy `m_Plant` to `_plant` per CLAUDE.md §12

Updated external callers to use the new entry points:

- `PlantItem.OnDoubleClick` → `MainPlantGump.DisplayTo(from, this)`
- `PlantPourTarget.OnTargetFinish` → `MainPlantGump.DisplayTo(from, m_Plant)` (also drops the now-redundant legacy `singleton: true` flag — `Singleton` property handles it)
- `PollinateTarget.OnTargetFinish` → `ReproductionGump.DisplayTo(from, m_Plant)`
2026-04-25 16:46:37 -07:00
Kamron Batman
839e56da1a
perf: Migrates Virtue gumps from legacy Gump to DynamicGump/StaticGump. (#2410)
## Summary

First PR in a multi-PR migration of player-facing legacy `Gump` subclasses to the modern `DynamicGump` / `StaticGump<T>` system. Plan covers ~95 files across ~14 PRs by system; this PR is the foundation (smallest, isolated, no cross-refs).

- `VirtueGump` → `DynamicGump`: per-instance virtue hues from `GetHueFor()` and the conditional self/other button block prevent layout caching.
- `VirtueStatusGump` → `StaticGump<VirtueStatusGump>`: layout is identical for every player; only used as a navigation hub.
- `VirtueInfoGump` → `DynamicGump`: dynamic cliloc IDs (`1051000 + (int)virtue`, the description cliloc, and the conditional `1052055`/`1052052` footer) cannot be cached by `StaticGump<T>` — cliloc *numbers* are baked into layout bytes, only HTML/label *text* can be deferred to placeholders.

All three:

- `Singleton => true` (replaces previous instance instead of stacking)
- Constructors made `private`; entry points are static (`RequestVirtueGump`, `DisplayTo`) per the empty-gump rule (CLAUDE.md §13)
- `OnResponse` updated to `in RelayInfo info` modern signature
- `VirtueInfoGump` self-refresh button now uses `_beholder.SendGump(this)` instead of allocating a new instance
- Removed the unused `VirtueGumpItem : GumpImage` nested class — replaced with direct `builder.AddImage(...)` calls; preserves the legacy `class=VirtueGumpItem` attribute for packet parity

The special-cased TypeID for VirtueGump (`BaseGump.cs:86`) is preserved because the type's full name (`Server.Engines.Virtues.VirtueGump`) is unchanged.
2026-04-25 16:40:07 -07:00
Kamron Batman
c552f65673
perf: Eliminates allocations in Container searching. (#2409)
## Summary

Removes per-call heap allocations from `Container`'s consume / find / group hot paths and from `BaseCreature.OnDeath`'s fame/karma tracking. The headline wins: kill the `List<List<Item>>` + `Item[][]` + `int[]` grouping bridges in `ConsumeTotal*` / `ConsumeTotalGrouped*` / `GetBestGroupAmount*`, and kill the per-call `Predicate<Item>` allocations in `FindItemsByType(Type)` / `FindItemsByType(Type[])`.

### `Container.cs`

- `ConsumeTotal`, `ConsumeTotalGrouped`, `GetBestGroupAmount` now share four streaming helpers (`HasAmount`, `TryFindGroupMeetingAmount`, `BestGroupTotal`, `ConsumeSlice`) backed by `PooledRefList` instead of allocating per-group lists and jagged arrays. Two-phase validate-then-consume pattern preserved — all-or-nothing semantics for spell reagents, vendor pay, and crafting still hold.
- `(Type)` / `(Type[])` / `(Type[][])` overload trios collapsed to single `ReadOnlySpan<Type>` + `ReadOnlySpan<int>` implementations. Implicit `T[] → ReadOnlySpan<T>` conversion means UOContent callers compile unchanged.
- Unused overloads deleted: `ConsumeTotalGrouped(Type)`, `ConsumeTotalGrouped(Type[][])`, `GetBestGroupAmount(Type)`, `GetBestGroupAmount(Type[][])`, plus the never-called `TryDropItems` hook and its private `ItemStackEntry` struct.
- Fixes a `PooledRefList` leak in `GetBestGroupAmount(Type[], …)` (missing `using`).
- `m_ContainerData` / `m_Items` / `m_TotalGold` / `m_TotalItems` / `m_TotalWeight` / `ContainerData.m_Table` / `ContainerData.logger` renamed to the underscored convention. `m_Items` cross-file rename for the Container-side references in `Item.cs`; `Item.CompactInfo.m_Items` deliberately left alone (separate effort).
- `CheckHold` parent walk simplified; trivial dispatch methods (`CheckHold` overloads, `OnItemAdded`, `OnItemRemoved`, `OnStackAttempt`) get `[MethodImpl(AggressiveInlining)]`; `Destroy` and `DisplayTo` cache `Items` outside the loop; dead comments removed.

### `Item.Enumerable.cs`

- `FindItemsByType(Type)` previously allocated a `Predicate<Item>` per call (method-group conversion). `FindItemsByType(Type[])` allocated a closure capturing `types`. Both now construct the enumerator with a `Type` / `ReadOnlySpan<Type>` field directly, no delegate.
- `FindItemsByTypeEnumerator<T>` gains two constructors plus a `Matches(T)` helper that picks the right filter inline. Constructor chaining via a private 2-arg seed constructor incidentally fixes a pre-existing bug where `PooledRefQueue` was always rented at capacity 0 because `_recurse` hadn't been assigned yet.
- `(Type[])` overload of `FindItemsByType` becomes `(ReadOnlySpan<Type>)`.
- `EnumerateItemsByType(Type)` / `EnumerateItemsByType(ReadOnlySpan<Type>)` / `ListItemsByType(Type)` / `ListItemsByType(ReadOnlySpan<Type>)` simplified to delegate to the new alloc-free overloads instead of filtering manually.

### `Utility.cs`

- `InTypeList<T>(this T, Type[])` and `InTypeList(this Type, Type[])` switched to `ReadOnlySpan<Type>`.

### `BaseCreature.cs`

- `OnDeath` per-death `List<Mobile>` / `List<int>` / `List<int>` for fame/karma tracking switched to `PooledRefList`.
2026-04-25 13:40:21 -07:00
Jack
597c81345e
feat: Adjusts fame and karma system with era gates for OSI accuracy (#2389)
## Summary

Overhauls the fame and karma system to be more era-accurate, based on original design documents and publish notes.

### Karma on player kill → karma on murder report
- Removes karma gain/loss on player kill (was immediate on death)
- Karma is now set to `Kills * -1000` on murder **report** instead
- Fame on player kill now uses the same formula as monster kills (`Fame / 100`)

This karma loss on murder report behaviour was tested on both the demo and live servers, behaviour was matching in terms of karma loss on report. Official UO servers karma loss AMOUNT match with my memory of T2A/UOR with one caveat - it's doubled on live servers (-2000 * kill count). I'm not sure when this changed and this behaviour has always had very poor and incorrect documentation, even 10-20 years ago.  I was obsessed with the dread lord title on OSI and the only way I knew how to get it was reach 10 kills then macro them off. Even in publish 16 (when "The Murderer" title was removed) it still required 10 kills. Maybe it changed to -2000*Kills in AOS - that's where I've put the era gating diff.

### Era gates
- **Karma lock** (ankh toggle + auto-lock on negative karma) gated to `Core.UOTD && !Core.AOS` — [didn't exist before Jan 28, 2001](https://web.archive.org/web/20010128092700/http://update.uo.com/design_300.html)
- **Felucca fame/karma +30% bonus** gated to `Core.LBR` — [added in Publish 16, July 2002](https://uo.com/wiki/ultima-online-wiki/technical/previous-publishes/2002-2/publish-16-part-2-5-23rd-july/)
- **Fame/karma splitting** among damage dealers gated to `Core.UOR` — pre-UOR awards go to last hit only

### Skill karma penalties
- **Provocation** on innocent NPC: NPC says cliloc 501591, karma loss (floor -7500)
- **Stealing** attempt: karma loss on every attempt (floor -5000). Stealing did not cause karma loss at all before!
- **Summon Daemon**: karma loss on successful cast (floor -7000)
- **Corpse carving** (human): -70 for innocent corpses (floor -7000), -20 for freely-aggressable (floor -2000)
- **Bounty head turn-in**: karma gain capped at 2000 (was awarding flat +2000)

### Beneficial action karma
- **Beneficial spells** (heal, cure, etc.): `AwardKarma(caster, target.Karma / 5)` — healing good targets raises karma, healing evil targets lowers it - source is UO98 demo scripts
- **Bandages**: same formula but gain only (skipped if target karma ≤ 0) - see stratics link ("only ever gain karma, not lose it")

Sources: UO98 Demo scripts and playing, [Fame and Karma wiki](https://uo.com/wiki/ultima-online-wiki/player/fame-and-karma/), [UO design doc (Jan 2001)](https://web.archive.org/web/20010128092700/http://update.uo.com/design_300.html), [Publish 16](https://uo.com/wiki/ultima-online-wiki/technical/previous-publishes/2002-2/publish-16-part-2-5-23rd-july/), [Stratics healing reference](https://web.archive.org/web/20001209014200fw_/http://uo.stratics.com/heal.shtml)

### Refactoring
- Extracted `Titles.ComputeKillAwards(killed, map)` shared by player kill and creature kill paths
- Extracted `Titles.SetKarma(m, value, message)` for direct karma assignment (used by murder report)
- Extracted `SendKarmaMessage` and `CheckKarmaLock` helpers from `AwardKarma`
2026-04-25 11:19:57 -07:00
Kamron Batman
36e09bae13
fix: Pets stop permanently, simplifies pet movement speed (#2401)
## Summary
- Fixes pets falling behind mounted masters in AOS+ by setting `CurrentSpeed = 0.1` when following master
- Fixes AI timer permanently stopping when `Obey()`/`Think()` returns `false` for transient conditions
- Fixes controlled pets losing AI in inactive sectors (pet follows owner across sector boundary, sector deactivates, AI dies)
- Adds defense-in-depth: AI timer restarts on pet resurrection and order changes

## AI Timer Permanent Stop (Bug Fix)

`AITimer.OnTick()` called `Stop()` when `Obey()` or `Think()` returned `false`. By that point, `ShouldStop()` had already validated the creature is alive, on a valid map, and in an active sector — so any `false` return was a **transient** condition, not terminal. The timer stopped permanently with no mechanism to restart it.

**Scenarios that triggered permanent AI death:**
- Dead bonded pet with attack order (`DoOrderAttack` returned `false` for `IsDeadPet`)
- Failed pet transfer — loyalty refusal, combat, disconnected player, or pending trade (`DoOrderTransfer` returned `false` for 5 different transient conditions)
- Unknown `OrderType` or `ActionType` (defensive defaults)

**Fixes:**
- Removed `Stop()` from the `Obey()`/`Think()` failure path — timer skips the tick and fires again next interval
- Changed `DoOrderAttack()` and all five `DoOrderTransfer()` failure paths to return `true` (correct semantics: these are recoverable states, not "stop AI forever" signals)
- Added `Activate()` call in `ResurrectPet()` — ensures dead bonded pets have AI running after resurrection
- Added `Activate()` call in `OnCurrentOrderChanged()` — self-heals timer if any voice command is issued to a pet with a stopped timer

## Controlled Pet Sector Deactivation (Bug Fix)

`ShouldStop()` stopped the AI timer for **all** `PlayerRangeSensitive` creatures in inactive sectors, including controlled pets. But `Deactivate()` intentionally exempted controlled pets. The exemption was dead code — `ShouldStop()` bypassed it.

This matters when a pet follows its owner across a sector boundary: the owner enters the next sector (active), the pet's old sector deactivates (no more players), and the pet's AI dies. The pet stops following and stands there until the player backtracks far enough to reactivate the sector.

**Fix:** Added `Controlled` check to `ShouldStop()` to match `Deactivate()`. Controlled pets now keep their AI running in inactive sectors. The overhead is negligible — controlled pets are bounded by follower slots.

## Movement Speed Simplification

- Simplifies `AITimer` to use `CurrentSpeed` directly as the tick interval (in seconds), removing the complex multiplier/floor logic in `GetBaseInterval`
- Refactors `DoMoveImpl` speed assignment into explicit if/else for clarity
- AOS+ pets following master use `CurrentSpeed = 0.1` (100ms), matching `RunMountDelay`

## Files Changed
- `AITimer.cs` — removed `Stop()` on Obey/Think failure, added `Controlled` exemption to `ShouldStop()`, simplified interval logic
- `BaseAI.cs` — renamed `_timer` to `AITimer` (public), simplified `Deactivate()`, fixed `ReturnToHome` to use `Activate()`
- `PetOrders.cs` — `DoOrderAttack` and `DoOrderTransfer` return `true` for transient failures
- `PetOrderHandlers.cs` — `OnCurrentOrderChanged()` calls `Activate()` to self-heal stopped timers
- `BaseCreature.cs` — `ResurrectPet()` calls `Activate()`, fixed `GoHome_Callback` PlayerRangeSensitive check
- `AIMovement.cs` — refactored speed assignment, AOS+ follow-master speed fix
2026-04-25 11:10:30 -07:00
Kamron Batman
6a132560eb
fix: Bumps dependencies to address vulns. (#2408) 2026-04-19 13:41:27 -07:00
Kamron Batman
c207c2c19f
chore: Signs build tool (#2406) 2026-04-09 08:55:19 -06:00
Kamron Batman
1c6d36c12d
fix: Removes vendor buy/sell context when FF is off (#2405) 2026-04-08 13:46:43 -06:00
Kamron Batman
8a34903326
feat: Consolidates staff gump layouts (#2404)
## Summary
- Creates `PropsLayoutExtensions.cs` with reusable extension methods for both legacy `Gump` and `DynamicGumpBuilder` that encapsulate the repeating PropsConfig-style layout patterns (frame, header navigation, entry rows)
- Converts all 12 standardized staff gumps to use the new extensions, reducing ~570 lines of duplicated layout code
- Adds Type and Serial display to PropsGump and SkillsGump headers (e.g. `PlayerMobile (0x1)`)

### Extension methods provided
| Method | Pattern |
|--------|---------|
| `AddPropsFrame` | Background + offset region + origin coordinates |
| `AddPropsHeader` | 3-column: [Prev] [Title] [Next] |
| `AddPropsHeaderWithBack` | 4-column: [Back] [Title] [Prev] [Next] |
| `AddPropsEntryButton` | Label + action button |
| `AddPropsEntryNameValue` | Name + Value + action button |
| `AddPropsEntryTextInput` | Text input + action button |
| `AddPropsEntryLabel` | Label only (no button) |
| `AddPropsEntryType` | Full-width type label |
| `AddPropsEntryBlank` | Separator row |

### Gumps converted
PropsGump, GoGump, WhoGump, SkillsGump (frame+header), EditSkillGump, SetGump, SetObjectGump, SetPoint2DGump, SetPoint3DGump, SetTimeSpanGump (frame), SetListOptionGump (frame+header), CategorizedAddGump (frame+header)

## Test plan
- [x] Verify `[props` gump displays correctly with type + serial in header
- [x] Verify `[skills` gump displays correctly with type + serial in header
- [x] Verify `[go` navigation gump works (prev/next/back)
- [x] Verify property editing gumps (Set, SetObject, SetPoint2D, SetPoint3D, SetTimeSpan, SetListOption)
- [x] Verify `[categorizedadd` gump works
- [x] Verify `[who` gump works with pagination
2026-04-08 11:42:46 -06:00
Kamron Batman
c0d75562be
chore: Removes dead gump code (#2403) 2026-04-08 08:39:23 -06:00
Kamron Batman
5b6ea0c3b5
fix: Fixes disabling BODs (#2402) 2026-04-08 08:04:18 -06:00
Kamron Batman
401e38bd97
fix: Adds stamp check for build tool versioning (#2400) 2026-04-06 16:18:02 -06:00
Kamron Batman
5cf782acc2
fix: Fixes buildtool detecting icu4c on OSX (#2399) 2026-04-06 16:01:21 -06:00
Kamron Batman
4c62bccb73
fix: Fixes publishing with build tool (#2398) 2026-04-04 00:35:39 -07:00
Kamron Batman
7c8d02e63a
chore: Removes docs (#2397) 2026-03-29 20:45:51 -07:00
Bohica
4698299ea7
fix: Fixes various target/movement bugs in BaseAI (#2379) 2026-03-29 09:39:02 -07:00
Kamron Batman
aa6932739f
chore: Fixes Build Tool Release Tag (#2396) 2026-03-28 22:39:11 -07:00
Kamron Batman
6f80a91fbf
chore: Fixes Build Tool .NET SDK Download (#2395) 2026-03-28 22:33:02 -07:00
Kamron Batman
26c1e399ba
chore: Cleans workflows for NodeJS 24 (#2394) 2026-03-28 21:55:29 -07:00
Kamron Batman
12b81c7375
chore: Fixes build tool workflow (#2393) 2026-03-28 21:30:50 -07:00
Kamron Batman
ec4d6a7a85
feat: Adds Build Tool for Publishing/Setup (#2392)
## Summary

Replaces the basic `publish.cmd`/`publish.sh` scripts with an interactive **BuildTool** — a C# console app using [Spectre.Console](https://spectreconsole.net/) that guides users through publishing, prerequisite checking, and cross-compilation.

### Why
The community found the existing publish scripts unhelpful for newcomers. They worked but didn't walk users through the process, didn't check prerequisites, and provided no feedback when things went wrong.

### What's New

**Interactive BuildTool** (`Projects/BuildTool/`)
- NativeAOT-compiled C# console app with true-color ASCII logo and ModernUO brand gold/silver palette
- Guided publish wizard with step-by-step back navigation (Ctrl+C or menu "Back" to go to previous step)
- Prerequisite checking: .NET SDK version, VC++ Redistributable (Windows), native libraries (Linux/macOS)
- .NET SDK auto-install offer via Microsoft's official install scripts
- Platform detection: Windows 10 vs 11 (build number), macOS codenames, Linux distro + kernel version
- Cross-compilation support: skips native library checks, shows target prerequisites after build
- Non-interactive mode for CI: `--config Release --skip-prereqs`
- Backward-compatible positional args: `publish.cmd release win x64` still works

**Shell Wrappers** (`publish.cmd`, `publish.ps1`, `publish.sh`)
- Try native BuildTool binary first (downloaded from GitHub Releases)
- Fall back to `dotnet run --project Projects/BuildTool` if unavailable
- SDK bootstrapping: offer to install .NET if not found

**CI/CD Updates**
- Build/test workflows target `Projects/Application/Application.csproj` instead of the solution (excludes BuildTool and test projects from publish)
- New `build-tool-release.yml` workflow builds NativeAOT binaries for win-x64, win-arm64, osx-arm64, linux-x64, linux-arm64
- Minimum SDK bumped to 10.0.201 (required for Serialization Generator 2.14.3 / Roslyn 5.3.0)

**Other Changes**
- Solution converted from `.sln` to `.slnx`
- Updated README with interactive mode instructions and deployment guidance

## Screenshots

<img width="320" height="378" alt="image" src="https://github.com/user-attachments/assets/83c057c5-3992-4dbd-99fa-0e3c24ef6428" />

<img width="749" height="554" alt="image" src="https://github.com/user-attachments/assets/e4ee4d6f-71d4-47f9-86b6-8fd1ca3c3e7a" />
2026-03-28 21:21:50 -07:00
Kamron Batman
3b4e1137f6
feat: Implements new robust/pluggable backup/archive system. (#2388)
## Summary

Overhauls the backup/archive system to address stability, robustness, fault tolerance, performance, and pluggability.

### Problems solved
- **Critical bug**: `Interlocked.CompareExchange` arguments were reversed — the concurrent archive guard never actually prevented concurrent archives
- **Brittle compression**: Relied on spawning external `zstd.exe`/`tar.exe`/`bsdtar.exe` processes with platform-specific workarounds (Windows two-step hack, auto-downloading bsdtar from libarchive.org)
- **No fault tolerance**: Partial archives possible if process killed mid-write; no recovery mechanism; originals deleted before verification
- **No extensibility**: No way to add remote backup destinations (S3, rsync) without modifying core code
- **Silent failures**: Bare `catch` blocks swallowed exceptions with no diagnostics

### What changed

**Cross-platform streaming compression** — Replaced external process spawning with `System.Formats.Tar` (built-in .NET) + `ZstdNet` (native libzstd P/Invoke). Single-pass streaming: `TarWriter → CompressionStream → FileStream`. No intermediate `.tar` file, no platform-specific code paths.

**Archive journal** — JSON-based operation journal (`Archives/.archive-journal.json`) tracks state machine: `Started → Archived → Distributed → Completed` (or `Failed`). On startup, recovers interrupted operations (cleans temp files, completes pruning).

**Verify before delete** — Archives are verified (entry count check) before deleting source backups. Temp-file-then-atomic-rename pattern prevents partial archives.

**Retry logic** — File operations (moves, deletes) retry with linear backoff for transient I/O failures (antivirus locks, file copy operations).

**Plugin architecture** — `IArchiveDestination` interface in `Projects/Server/` enables external plugins (loaded via `assemblies.json`) to receive completed archives. `ArchiveDestinationRegistry` tracks destinations. Conservative pruning: source backups preserved if any destination with retention fails.

**Configurable** — Retention counts (hourly/daily/monthly), compression level, retry settings, backup max age all configurable via `ServerConfiguration`.

### New admin commands
- `[ArchiveStatus` — Shows journal state, destinations, next scheduled archive times
- `[ArchiveNow` — Forces immediate rollup regardless of schedule

### Files

| Change | File |
|--------|------|
| New | `Server/Saves/ArchiveEnums.cs`, `IArchiveDestination.cs`, `ArchiveDestinationRegistry.cs`, `ArchiveEventArgs.cs` |
| New | `Server/Events/ArchiveEvents.cs` (partial EventSink) |
| New | `UOContent/Compression/ManagedArchive.cs` |
| New | `UOContent/World Saves/ArchiveJournal.cs`, `LocalArchiveDestination.cs` |
| Rewrite | `UOContent/World Saves/AutoArchive.cs` |
| Modified | `UOContent/World Saves/SaveCommands.cs`, `Server/Utilities/PathUtility.cs`, `UOContent.csproj` |
| Deleted | `UOContent/Compression/TarArchive.cs`, `ZstdArchive.cs` |
| Tests | 29 new tests (ManagedArchive, ArchiveJournal, AutoArchiveHelpers) |

### Backward compatibility
- Existing `.tar.zst` archives are fully readable by the new managed code
- Config keys preserved; new keys use sensible defaults
- `autoArchive.compressionFormat` setting removed (Zstd only)
- Legacy `bsdtar/` directory logged as removable on startup

### Plugin example (external repo)
```csharp
public static class S3Plugin
{
    public static void Configure()
    {
        ArchiveDestinationRegistry.Register(new S3Destination("my-bucket", "us-east-1"));
    }
}
```

## Test plan
- [x] 29 new unit tests covering archive create/extract/count, journal state machine, recovery, destinations
- [x] All 969 tests pass (671 Server + 298 UOContent)
- [x] Manual: run server, trigger save, verify backup created and archive rolled up
- [x] Manual: kill server mid-archive, restart, verify journal recovery
- [x] Manual: test with large save files (~500MB+) to benchmark streaming vs old approach
2026-03-22 19:51:20 -07:00
Kamron Batman
61e41df00c
feat: Add zero-alloc interpolation handler to ValueStringBuilder, replace all StringBuilder usage (#2387)
## Summary

- **Add a self-referencing `InterpolationHandler` to `ValueStringBuilder`** that writes directly into the builder's buffer — zero intermediate allocation, works with `stackalloc`-backed builders
- **Replace all `System.Text.StringBuilder` usage** across the codebase with `ValueStringBuilder`
- **Convert `ValueStringBuilder.Create()` to `stackalloc`** at 10 sites where output length is provably bounded
- **Convert manual `Dispose()` to `using var`** where possible, and hoist loop-scoped builders outside loops with `Reset()`
- **Convert verbose `Append()` chains to `Append($"...")`** interpolation for readability
- **Add comprehensive documentation** for string handling patterns

## InterpolationHandler Design

`ValueStringBuilder` is a `ref struct`, which creates challenges for C#'s interpolated string handler pattern:

- **`ref` fields to ref structs are not allowed** (CS9050)
- **`[InterpolatedStringHandlerArgument("")]` passes struct receivers by value**, not by ref
- **`ISelfInterpolatedStringHandler` requires boxing** ref structs into interface fields

**Solution: Copy-and-reconcile pattern.** The handler receives a value copy of the builder. The copy shares the same underlying `char` buffer (`Span` points to the same `stackalloc`/pooled memory), so writes go to the original buffer. `Append()` reconciles by `this = handler._builder`, updating `_length` and any buffer references changed by `Grow()`.

This is safe because:
- The game loop is single-threaded — no concurrent access between handler construction and reconciliation
- If `Grow()` occurs in the copy, the original's stale buffer isn't accessed until `Append()` replaces it
- `Dispose()` correctly returns the reconciled buffer to the pool

## Changes by Category

### ValueStringBuilder (`Projects/Server/Buffers/ValueStringBuilder.cs`)
- Added nested `InterpolationHandler` ref struct with copy-and-reconcile pattern
- Added `Append([InterpolatedStringHandlerArgument("")] scoped ref InterpolationHandler)` method
- Removed `RawInterpolatedStringHandler` overloads (new handler replaces them)
- All `AppendFormatted` overloads delegate to existing `Append` methods (no code duplication)
- Alignment support via direct private field access (nested type privilege)

### StringBuilder → ValueStringBuilder (15 files)
Replaced all `new StringBuilder()` with `ValueStringBuilder.Create()` or `stackalloc`:
- ConPVP games: KingOfTheHill, DoubleDom, CTF, BombingRun, TourneyMatch
- ConPVP infrastructure: Tournament, Participant, TourneyParticipant
- ConPVP gumps: ArenaGump, TournamentBracketGump, AcceptTeamGump, ConfirmSignupGump
- Commands: Handlers, Logging, Add
- Other: TownCrier, SpeechLogGump, TestCenter

Key patterns:
- `sb = new StringBuilder()` reassignment → `sb.Reset()`
- `sb.AppendFormat("{0:N0}", value)` → `sb.Append($"{value:N0}")`
- `sb.Append(x).Append(y)` chains → separate statements (VSB returns void)

### Create() → stackalloc (10 files)
Converted heap-allocated builders to stackalloc where output is bounded:
- ClientVersion (32), MapSelection (160), HouseRaffleStone (48)
- HolySense (96), UnholySense (96), ClientVerification (192)
- AcceptTeamGump (64), ConfirmSignupGump (64)
- BaseWeapon (160), BaseArmor (128)

### Loop optimizations (2 files)
Hoisted `ValueStringBuilder` creation outside loops with `Reset()` per iteration:
- TourneyMatch.cs: `using var` inside for loop → stackalloc before loop
- ArenaGump.cs: `Create()` + `Dispose()` per iteration → stackalloc before loop

### Append chain → interpolation (5 files)
Converted multi-line `Append()` chains to `Append($"...")`:
- BountyMessage.cs: title switch (6 cases), paragraph (15→1 Append), description lines, closing
- AcceptTeamGump, ConfirmSignupGump, TournamentBracketGump: tournament type strings
- AdminGump: comment/tag formatting in loops

### Documentation
- `dev-docs/string-handling.md`: Full reference — construction, interpolation, disposal, decision guide
- `dev-docs/claude-skills/modernuo-string-handling.md`: Claude skill with quick reference
- `CLAUDE.md`: Added rule 17 (no StringBuilder), dev-docs table entry, skills table entry
- `dev-docs/code-standards.md`: Updated memory management section

## Test Plan

- [x] `dotnet build` — 0 errors, 0 warnings
- [x] `dotnet test` — 940/940 tests pass
- [x] 28 ValueStringBuilder tests covering all reconciliation scenarios:
  - Stackalloc no-grow, stackalloc with grow (→pool transition)
  - Heap no-grow, heap with grow, heap double grow
  - Pre-existing content with and without grow
  - Sequential multiple `Append($"...")` calls
  - Mixed plain + interpolated Append
  - Empty interpolation, literal-only, format specifiers
  - Null string holes, ISpanFormattable types
  - Dispose after stackalloc→pool grow
2026-03-22 14:23:44 -07:00
Jack
9f39198fab
feat: Adds pre-T2A-pub15 bounty system (#2377)
# Bounty Boards

<img width="717" height="382" alt="image" src="https://github.com/user-attachments/assets/455e5206-47d8-4449-805c-19b143d059e5" />
<img width="918" height="637" alt="image" src="https://github.com/user-attachments/assets/e78e1ca2-62b8-4abf-8f69-21438bb1b759" />
<img width="340" height="296" alt="image" src="https://github.com/user-attachments/assets/fd17d7e6-8c53-47df-ac58-a89ea3ef665e" />

## Setup
* Setup as part of decorate when bounty system is enabled
  * Several bounty board locations with a WarriorGuard spawner in front of the board. Guard spawns and idles around 5 range.

## Tests

### ReportBountyMurdererGump
* Follows same behaviour as ReportMurdererGump
* Extracted common logic
* Didn't use staticgump due to several dynamic parts including input
* Optional bounty with validation >0 and <bankbox.total
* Murder report is honored either way

### Bounty boards
* Open bounty board with many bounties, no bounties
* Keep a bounty board open, invalidate a bounty by turning in the head, then try to click on the post of the now invalid post. As expected: does nothing
* Bounty messages use the last murder time as their post date and expire in 14 days
* Bounty boards/messages are not reliant on serialization; they use serialized fields from MurderContext to build messages when clicked.
* Bounty messages use synthetic serials so they do not have to persist bounty messages as items. They are constructed and sent as raw packets when needed.
* Players only appear on the bounty board if they are a murderer (though a non-murderer can technically still have a bounty if they decayed kills)
* Tested skin/hair color descriptions vs a dozen spot checks

### Head turn in behaviour
* Guard accepts head
  * Bounty -> gives bounty
  * No bounty -> generic response
  * Expired head (24h) -> generic response

NOTE: CUO "latest" has a bug with bounty/bulletinmessages that causes overflow outside of the container. It has nothing to do with this PR. It is fixed here in CUO https://github.com/ClassicUO/ClassicUO/pull/1871

# Murderer title

Bounty system and "murderer" title eliminated in [pub16](https://uo.com/wiki/ultima-online-wiki/technical/previous-publishes/2002-2/publish-16-part-2-5-23rd-july/). So UO:LBR and before had bounties and murderer title.

# Pre-T2A caveat

There were differences in pre-T2A, but this cannot be currently implemented because we do not have any pre-T2A systems in general, so at least for now, behavior is consistent with later eras. Pre-T2A was a whole other ballgame, but the bounty system still worked similarly.
2026-03-22 12:35:16 -07:00
Jack
7be8b5f7f6
feat: T2A defensive spells (#2378)
## Summary

Implements T2A-accurate mechanics for the three defensive spells based on UO98 demo scripts. Pre-UOR (`!Core.UOR`) triggers the new behavior; UOR and AOS paths are unchanged.

- **Reactive Armor**: percentage-based melee damage reflection (10-35% based on target Magery), targeted, timed (25-75s). Guards exempt, ranged attacks beyond 1 tile unaffected. Reflected damage is sourceless.
- **Protection**: temporary AC bonus (casterMagery/10, 1-10 AR) via VirtualArmorMod, targeted, timed (12-120s). Shared table with Arch Protection — only one protection buff per mobile.
- **Magic Reflect**: single-use spell reflection using MagicDamageAbsorb as a flag. No timer, no DefensiveSpell lock. Consumed on first reflected spell.
- **Arch Protection**: T2A path applies Protection via shared table with area sound (0x1F7 vs 0x1ED).
- No DefensiveSpell mutual exclusion for T2A — all three spells operate independently.

## Test plan

- [x] Set expansion to T2A
- [x] Cast Reactive Armor on self → verify particles (0x376A) and sound (0x1F2)
- [x] Get hit in melee → verify attacker takes reflected damage with spark effect and sound
- [x] Get hit by ranged weapon from > 1 tile → verify no reflection
- [x] Wait for RA to expire → verify effect ends silently
- [x] Cast RA on target that already has it → verify "This spell is already in effect"
- [x] Cast Protection on another player → verify particles (0x375A) and sound (0x1ED)
- [x] Check target's AR → verify it increased by casterMagery/10
- [x] Wait for Protection to expire → verify AR returns to normal
- [x] Cast Protection on self, then cast Protection on another player → verify both succeed
- [x] Cast Arch Protection on ground near allies → verify each gets AR bonus with sound 0x1F7
- [x] Cast Arch Protection near a target already protected → verify that target is skipped
- [x] Cast Magic Reflect on self → verify particles (0x375A) and sound (0x1E9)
- [x] Have an enemy cast a harmful spell at you → verify spell reflects back, effect consumed
- [x] Cast Magic Reflect again → verify it can be recast after consumption
- [x] Cast Magic Reflect when already active → verify "This spell is already in effect"
- [x] Verify all three spells can be active simultaneously on the same mobile
- [x] Switch expansion to UOR → verify existing UOR behavior unchanged
- [x] Switch expansion to AOS → verify existing AOS behavior unchanged
2026-03-22 11:18:34 -07:00
Jack
6cdec42146
fix: Special moves and various UOR+ bonuses (#2384)
## Special moves
Special moves were added [27 Apr, 2000.](https://uo.com/wiki/ultima-online-wiki/technical/previous-publishes/2000-2/2000-publish-05-27th-april/). This is well past the discussed cutoff date for T2A, which is **[Nov 23, 1999 - Publish 1](https://uo.com/wiki/ultima-online-wiki/technical/previous-publishes/1999-2/1999-publish-01-23rd-november/)** as per the UOSecondAge shard which is the golden standard for T2A accuracy and represents a logical cutoff for this era. 

Regarding the specific date: **Nov 23, 1999 (Publish 1)** - the game significantly changed as a "pre-patch" to UOR on this date, and UOSecondAge picks and chooses from this date based on QoL features, while keeping the gameplay accurate to what is widely known as the "T2A era". However this date is ~4 months before the actual UOR release date.

## Defensive wrestling
Defensively wrestling as (eval+anat)/2 was added in [publish 16](https://uo.com/wiki/ultima-online-wiki/technical/previous-publishes/2002-2/publish-16-changes-and-bug-fixes-24th-july/) and should be gated behind !Core.LBR
* "Unarmed “to-be-hit” changes (chance to get hit while unarmed is now the better of either Wrestling skill, or an average of Anatomy & Evaluate Intelligence)."

## Lumberjacking damage bonus

Lumberjacking damage bonus was added in [UOR](https://uo.com/wiki/ultima-online-wiki/technical/previous-publishes/2000-2/2000-publish-05-27th-april/)

## Anatomy and LJ +10% GM bonus
Anatomy/LJ +10% GM bonus was added in [pub 13](https://uo.com/wiki/ultima-online-wiki/technical/previous-publishes/2001-2/2001-publish-13-19th-august/), which was during UOTD.
2026-03-22 10:55:01 -07:00
Kamron Batman
2b66817937
chore: Fixes flaky tests (#2386) 2026-03-22 10:54:12 -07:00
Kamron Batman
992bc95164
feat: Refactor Poison system, implement Darkglow & Parasitic effects (#2385)
## Summary

- Refactors the poison system to separate `Index` (globally unique ID) from `Level` (tier within a family), enabling multiple poison families (Standard, Darkglow, Parasitic) to coexist without collisions
- Implements Darkglow and Parasitic poison special effects from Mondain's Legacy: Darkglow boosts damage by 10% when attacker is ranged, Parasitic heals the attacker for damage dealt in melee range
- Fixes several bugs: `Register()` crashing on duplicate `Level` values across families, `IncreaseLevel()` crossing family boundaries, `InfectiousStrike` and `NinjaWeapons` stripping poison family via level-based lookups, and `ArchCure`/`CleansingWinds` using raw `Level + 1` instead of `IncreaseLevel()`

## Changes

**`Projects/Server/Poison.cs`** — Adds `PoisonFamily` enum and abstract `Family` property. Adds `Index` as unique identifier. Fixes `Register()` to check `Index` uniqueness (not `Level`) and validate the new poison's name (not the existing one's). Fixes `IncreaseLevel()` to use `Index + 1`, naturally respecting family boundaries via Index gaps. Replaces linear name lookup with `Dictionary`-based `PoisonsByName`.

**`Projects/UOContent/Misc/Poison.cs`** — Adds `family` parameter to `PoisonImpl`. Implements Darkglow effect (10% damage boost when `From` >1 tile, cliloc 1072850) and Parasitic effect (heals `From` for damage dealt within 1 tile, cliloc 1060203) in `PoisonTimer.OnTick()`. Renames `m_` fields to `_` convention.

**`Projects/UOContent/Misc/PoisonKinds.cs`** — New file. Moves poison registration out of `PoisonImpl` into `PoisonKinds.Configure()`. Adds `PoisonFamily` to Darkglow/Parasitic registrations. Provides extension properties (`Lesser`, `Deadly`, `LesserDarkglow`, etc.), `GetPoison(int level)` (standard-only), `GetPoisonByFamilyAndLevel()`, and `IsDarkglow`/`IsParasitic` instance helpers.

**`Projects/UOContent/Items/Weapons/Abilities/InfectiousStrike.cs`** — Family-aware poison scaling: Darkglow caps at Deadly (Poisoning/33.3), Parasitic caps at Lethal (Poisoning/25), Standard unchanged. Level bump uses `IncreaseLevel()` with family boundary check.

**`Projects/UOContent/Items/Skill Items/Ninjitsu/NinjaWeapons.cs`** — EvilOmen level bump uses `Poison.IncreaseLevel()` instead of `Poison.GetPoison(Level + 1)`.

**`Projects/UOContent/Spells/Fourth/ArchCure.cs`** and **`CleansingWindsSpell.cs`** — Replace `poison.Level + 1` with `Poison.IncreaseLevel(poison).Level` for family-safe cure chance calculation.

**`Projects/Server/Serialization/SerializationExtensions.cs`** — Serializes/deserializes `Index` instead of `Level`.

**`DarkglowPotion.cs`** / **`ParasiticPotion.cs`** — Point to actual Darkglow/Parasitic poisons instead of placeholder `Greater`.

**`PotionKeg.cs`** / **`BasePotion.cs`** — Adds Darkglow, Parasitic, Invisibility, and FlintsPungentBrew to `PotionEffect` enum and keg label support.

## Test plan

- [ ] `dotnet build` compiles cleanly (verified, 0 warnings 0 errors)
- [ ] Verify `PoisonKinds.Configure()` registers all poisons without throwing (Register bug fix)
- [ ] Standard poison behavior unchanged — PoisonField, PoisonSpell, SerpentArrow, SavageShaman, TrappableContainer all use `GetPoison(int level)` which now correctly filters to Standard family
- [ ] Darkglow: poison tick deals +10% damage when attacker is >1 tile away, sends "Darkglow poison increases your damage!" message
- [ ] Parasitic: poison tick heals attacker for damage dealt when within 1 tile, sends heal message
- [ ] InfectiousStrike preserves poison family and respects family-specific skill scaling
- [ ] EvilOmen + NinjaWeapons level bump stays within poison family
- [ ] ArchCure/CleansingWinds cure chance calculations work correctly across all poison families
- [ ] Serialization round-trips correctly using Index
2026-03-21 21:27:22 -07:00
Jack
3bb38bcb5b
fix: Remove distance based harm before uotd (#23 2026-03-21 11:31:38 -07:00
Joe
e1ae970636
fix: Fixes multiple factions bugs with sigil generation, lighting, and theft (#2375) 2026-03-15 11:02:56 -07:00
Kamron Batman
b0189ccf4a
fix: Fixes displaying ping pongs (#2374) 2026-03-15 02:05:33 -07:00
Kamron Batman
6f8e9b9aec
fix: Fixes considering sins for all eras (#2373) 2026-03-15 01:55:41 -07:00
Kamron Batman
af35c25ca2
docs: Updates CLAUDE dev-docs/skills for serialization (#2372) 2026-03-15 01:05:03 -07:00
Joe
ff10811d3d
feat: Adds Endless Decanter of Water and Water Elemental acquisition (#2371)
## Summary

- Adds the Endless Decanter of Water (introduced in Publish 66.2 / SA era)
- Players throw a full Pitcher of Water at a Water Elemental for a 10% chance to receive the decanter; the pitcher is always destroyed on impact
- Each Water Elemental can only yield one decanter; the state is persisted and previously saved elementals are migrated to the new serialization version
- The decanter auto-refills from a linked water trough when the owner empties it within 10 tiles of the stored trough location
- Linking stores a Point3D + Map snapshot, supporting both static tile troughs and addon troughs
- The decanter is blessed and displays Linked/Unlinked status in its tooltip
2026-03-14 16:45:06 -07:00
Kamron Batman
e3cba66284
feat: Adds dynamic thread idle to address CPU usage (#2370)
## Summary
- **Timer-aware idle sleep**: Exposes `Timer.MillisecondsUntilNextTick()` to calculate remaining ms until the next timer wheel tick (0–8ms). The game loop sleeps for that duration minus a 1ms safety margin, instead of spinning at 100% CPU.
- **I/O completion wakeup**: Replaces `Thread.Sleep` with `NetState.WaitForCompletion()`, which uses platform-native completion notification (RIO `RIONotify` on Windows, `eventfd` on Linux, `kevent` timeout on macOS) to wake immediately when network data arrives during sleep.
- **Always-on**: Removes the debug-only `core.enableIdleCPU` config gate. The sleep is self-regulating — under load, `MillisecondsUntilNextTick` returns 0 so no sleep occurs (zero overhead). On idle, CPU drops from ~100% to ~1%.
- **CPS calculation cleanup**: Replaces the 128-element ring buffer with an EMA (exponential moving average) for `CyclesPerSecond`/`AverageCPS` — fewer allocations, no LINQ `.Average()` call each sample.
- **IORingGroup 1.0.6**: Adds `WaitForCompletion(int timeoutMs)` to the `IIORingGroup` interface with platform implementations:
  - **Windows**: `RIONotify` arms the CQ event, `WaitForSingleObject` with timeout
  - **Linux**: `eventfd` registered with io_uring, `poll()` with timeout
  - **macOS**: `kevent()` with timeout

## Test plan
- [ ] Build succeeds on all platforms (`dotnet build`)
- [ ] Empty server: verify CPU usage drops from ~100% to ~1% idle
- [ ] Loaded server: verify no added latency — `MillisecondsUntilNextTick` returns 0 when timers are firing, sleep is skipped
- [ ] Connect a client during idle — verify connection accepted within one timer tick (~8ms)
- [ ] Verify `[admin` gump shows reasonable CPS values (EMA convergence)
2026-03-13 23:53:49 -07:00
Kamron Batman
1e4cdc4ab6
fix: Fixes criminals having guards called on them (#2348)
## Summary

Fixes three bugs in `GuardedRegion.CallGuards`:

- **Operator precedence bug**: The condition `!m.Region.IsPartOf(this) && !m_GuardCandidates.ContainsKey(m)`
  was inverted from intent. Replaced with explicit split: dictionary members are targeted regardless of
  region; permanent candidates (reds/AlwaysMurderer) must be inside the region.
- **Premature `break`**: Only the first guard candidate was ever processed per "guards" call.
  Removed the `break` so all valid candidates in range get a guard spawned.
- **Misleading message for permanent reds**: "Guards can no longer be called on you." (502276)
  was sent to permanent reds, but guards can *always* be called on them. Now only sent to
  temporary criminals whose guard window is actually consumed.

Also extracts `IsAlwaysGuardCandidate()` helper and simplifies `IsGuardCandidate()`.

## Edge cases verified (by analysis)

- **Multiple players call guards on same red**: `BaseGuard.Spawn` dedup (scans 15 tiles for
  existing guard with same `Focus`) prevents duplicate guards. Message suppression eliminates spam.
- **Red fights spawned guard → criminal → guards called again**: Same dedup prevents infinite
  guard spawns. Existing guard already has `Focus == red`.

## Test plan

- [ ] Temporary criminal in guarded region → guard spawns, receives "Guards can no longer be called on you."
- [ ] Permanent red (5+ kills) in guarded region → guard spawns, does NOT receive the message
- [ ] Multiple players call "guards" on same red → only 1 guard spawns
- [ ] Criminal outside region but in dictionary → still targeted by guards call from inside region
- [ ] Red outside guarded region → NOT targeted by guards call (must be inside region)
2026-03-13 17:35:47 -07:00
Kamron Batman
4f9bc1d9f6
feat: Adds AI skills to migrate from RunUO (#2366)
## Summary

Adds comprehensive RunUO → ModernUO migration documentation and Claude AI skills to help shard owners and script authors convert RunUO 2.7 code to ModernUO.

- **10 migration skills** (`dev-docs/claude-skills/migrate-from-runuo/`) — system-by-system conversion guides (foundation, serialization, timers, gumps, packets, property lists, commands/events, persistence, items/mobiles, systems/engines)
- **12 reference docs** (`dev-docs/runuo-migration-docs/`) — deep-reference with before/after examples, API mapping tables, edge cases, and gotchas
- **Updated existing skills** — `modernuo-timers`, `modernuo-serialization`, and `modernuo-threading` now document that `Serialize()` runs on background threads and timers are not thread-safe
- **Updated `CLAUDE.md`** — added migration skill lookup table

### Key migration patterns covered
- Manual `Serialize()`/`Deserialize()` → source-generated `[SerializableField]`
- `Packet` class hierarchy → static `SpanWriter`/`SpanReader` methods
- `Timer` subclasses → `TimerExecutionToken` fire-and-forget
- `Gump` → `StaticGump<T>`/`DynamicGump` with builders
- `EventSink.WorldSave` → `GenericPersistence`
- `ObjectPropertyList` → `IPropertyList` with string hole rules
- Universal changes: naming (`m_` → `_`), `[Constructable]` → `[Constructible]`, logging, spatial queries
2026-03-13 00:33:45 -07:00
Jack
31b22d4773
fix: Fixes bug with non blessed starter spellbooks (#2368) 2026-03-13 00:20:23 -07:00
Kamron Batman
bb62434f46
docs: Adds RunUO-Encryption notice attribution to DarkStorm (#2369) 2026-03-12 23:35:47 -07:00
Kamron Batman
f150458578
fix: Fixes encryption support for pre-6.0.5 clients (#2365)
### Summary

Fixes encryption detection for clients pre-6.0.5.0. To limit the amount of brute-force key checking we are only checking 4.0.11 to 6.0.4.
2026-03-12 21:59:01 -07:00
Bohica
76395b77ec
feat: Adds Discord integration for GM Pages (#2361) 2026-03-12 20:42:23 -07:00
Kamron Batman
217d641861
fix: Makes courtyard/2nd floor doors locked. (#2364) 2026-03-12 20:07:41 -07:00
Kamron Batman
55fe688eaf
fix: Adds guard for monster ability recursion (#2363) 2026-03-12 19:26:49 -07:00
Jack
6b6cc10771
feat: T2A ping-pong mechanic and consider sins behaviour (#2356)
## Summary

- Adds `Mobile.Murderer` virtual property and consolidates kill-threshold checks across the codebase
- Tracks ping-pong count: how many times a player crosses the 5-kill murderer threshold (T2A/UOR/UOTD only, disabled on LBR+)
- Adds `[CommandProperty]` to view a player's ping-pong count via the admin panel
- After enough ping-pongs, player is permanently flagged as a murderer regardless of kill count
- Accounts for perma-red players with low kills in murderer status transition notifications
- Implements era-appropriate "I must consider my sins" speech responses:
  - **T2A**: contextual cliloc flavor text (502122–502126)
  - **UOR–AOS**: raw short/long-term murder counts + ping-pong count if applicable
  - **SE+**: localized stats message (1114370)
- Refactors kill-report logic out of `Keywords.cs` into `PlayerMurderSystem.ReportKillsToSelf`

## Testing

- [x] Thoroughly tested and self reviewed
- [x] Test T2A "I must consider my sins" behaviour over all scenarios.
- [x] Test UOR "I must consider my sins" behaviour over all scenarios.
- [x] Test that LBR does not have ping pongs enabled (I must consider my sins)
- [x] Test serialization cross over from v0 -> v1 increments 1 ping pong if player is already red.

## Notes
* Manually setting kills to 5 does not trigger a ping pong, it must go through the actual murder system. This includes if the kills were manually set to 5 and then migrated (as manually setting kills to 5 never adds the player into the murder system - it only happens via ReportMurderer). This is arguably a bug in the existing system, but one that currently only ever happens via staff interaction.
* Thieves guild SuspendOnMurder specifically checks for kills > 0. This means a person with 0 shorts but 5 ping pongs (flagged as murderer) can steal. This may be accurate, as according to a forum post this is how it works on UOSA which is the T2A gold standard.
* This doesn't implement Pre-T2A behaviour which should be that "I must consider my sins" does nothing at all. The reason I didn't implement it for Pre-T2A is then it 100% have to sit behind a feature flag. I don't mind adding it as a feature flag, just let me know.
2026-03-10 23:10:27 -07:00
Jack
0d7b27fe7a
fix: Removes Core.SE guard from recently reported murder checks (#2359) 2026-03-10 23:05:48 -07:00
Bohica
e37ad17d7d
fix: Adds handling empty spawner files (#2360) 2026-03-10 23:04:39 -07:00
Kamron Batman
04d438239d
feat: Adds robust speed hack detection and movement throttling (#2266)
## Summary

   Server-side movement throttle that prevents speed hacking while accurately identifying cheaters with detection of lagging connections.

   **Key features:**
   - Credit buffer (200ms) absorbs timing jitter from legitimate players
   - Movement queue handles larger bursts, draining at proper game-tick intervals
   - RTT measurement distinguishes network lag from speed hacks
   - Queue depth detection catches ACK-throttled speed hacks (going straight)

   ## How It Works

   **Throttle** (prevention): Movements arriving too early either consume credit or get queued. The queue drains at
   correct intervals, so speed hackers can't move faster regardless of what they send.

   **Detection** (identification): Combines multiple signals to identify cheaters:
   | Signal | What it catches |
   |--------|-----------------|
   | Queue depth ≥4 sustained | ACK-throttled speed hacks (client limits unacked moves to 5) |
   | Movement rate >1.05x | Direction-change speed hacks where timing is visible |
   | Stable RTT + high queue | Eliminates false positives from laggy players |

   **RTT-Aware Logic:**
   - Probes only sent to players actively moving (event-driven, not global loop)
   - Stable low-latency + problems = suspicious
   - Unstable/high-latency + problems = probably just lag, throttle handles it

   ## Configuration

   ```json
   {
     "movementThrottle.maxCredit": 200,
     "movementThrottle.softQueueLimit": 6,
     "movementThrottle.hardQueueLimit": 10,
     "movementThrottle.debugLogging": false
   }
   ```
2026-03-07 11:44:37 -08:00
Jack
6745cf2075
feat: Adds Mobile.Murderer virtual property, consolidates kill-threshold checks (#2355)
## Summary

- Adds `public virtual bool Murderer => Kills >= 5` property to `Mobile`, replacing ~30 scattered `Kills >= 5` / `Kills < 5` magic-number checks across the codebase
- `BaseCreature` overrides `Murderer` to also return `true` when `AlwaysMurderer` is set
- Updates `Corpse` serialization to v15, storing `_murderer` as a `bool` field (migrated from `int Kills >= 5` in earlier versions)
2026-03-06 08:36:41 -08:00
Bohica
192f092ee7
fix: Fixes various spell animations (#2346)
### Summary

This is a partial fix for servers that enable 0xC7 and clients that support 0xC7. A proper fix should be made on ClassicUO.
2026-03-06 00:30:40 -08:00
Bohica
2b80286d2e
fix: Fixes arrow animations by using 0xC7 when possible. (#2343)
### Summary

This is a partial fix for servers that enable 0xC7 and clients that support 0xC7. A proper fix should be made on ClassicUO.
2026-03-05 23:23:20 -08:00
Bohica
0ac16ddcb3
fix: Fixes timer leak in creature healing (#2349) 2026-03-05 19:37:48 -08:00
Kamron Batman
e1e1a7c640
fix: Bumps deps. Updates copyrights (#2353) 2026-03-05 19:36:54 -08:00
Kamron Batman
1391c563fe
chore: Adds AI instructions and SKILLs for ModernUO codebase (#2347)
Summary

  - Adds CLAUDE.md at repo root with 14 terse code audit rules (always loaded, low token cost)
  - Adds pointer files for other AI tools: AGENTS.md (Codex), GEMINI.md, .github/COPILOT-INSTRUCTIONS.md (Copilot), .cursorrules (Cursor) — all redirect to CLAUDE.md as single source of truth
  - Gitignores /.claude so personal AI config isn't distributed
  - Moves Claude skills to dev-docs/claude-skills/ (opt-in, not auto-loaded)
  - Adds 14 dev-docs covering codebase conventions

  Code Audit Rules (in CLAUDE.md)

  1. LINQ tiered rules (Tier 1 free, Tier 2 warm, Tier 3 forbidden)
  2. No Console.WriteLine — use LogFactory.GetLogger()
  3. No concurrency primitives in game code
  4. No World.Mobiles/World.Items iteration
  5. Clean up refs in OnDelete()/OnAfterDelete()
  6. Cancel timers in OnDelete()/OnAfterDelete()
  7. STArrayPool<T>.Shared not ArrayPool<T>.Shared
  8. PooledRefList<T> not new List<T>() on hot paths
  9. Serialization: partial class, [Constructible], no serialized TimerExecutionToken
  10. No Task.Run/new Thread() in game code
  11. Never assume era — ask which expansion
  12. _camelCase fields, PascalCase properties/methods
  13. No empty gumps — use DisplayTo() pattern
  14. PropertyList string literals must be {} holes, cliloc-as-argument uses :#
2026-03-01 11:42:19 -08:00
Joe
e77a566f32
feat: Implements passive Detect Hidden mechanics (#2342) 2026-02-28 11:24:49 -08:00
Bohica
36e91b2228
feat: Adds walk/run restriction configs (#2345) 2026-02-28 09:41:40 -08:00
Bohica
5ea926bf1c
fix: Fixes slayer assignment (#2344) 2026-02-26 19:14:19 -08:00
Kamron Batman
c494fb4cc3
fix: Fixes container enumeration not recycling pooled arrays (#2341)
### Summary

Updates all calls to container.EnumerateItems() to properly dispose of the underlying PooledRefQueue so that we are properly recycling pooled arrays.
2026-02-17 09:54:32 -08:00
Kamron Batman
1780edf0be
fix: Bumps IORingGroup to fix excessive syscalls (#2340) 2026-02-15 11:31:04 -08:00
Kamron Batman
b01a40a3de
fix: Converts ConPVP to serialization generator (#2338) 2026-02-14 14:50:10 -08:00
Kamron Batman
6ffb63ec82
fix: Fix IORing disconnect issues. (#2335)
### Summary

- Bump IORingGroup 1.0.0 → 1.0.1 — fixes a disconnect handling bug in the native ring layer
- Fix ghost NetStates — Dispose() set _running = false before checking it, so the "force immediate disconnect" path
was dead code. Capture wasRunning before clearing it, add [Obsolete] guard, and route internal callers through
DisposeInternal()
- Fix unauthenticated socket cleanup — graceful disconnect on unauthed connections could get stuck with pending sends;
 now force-immediate after Disconnect() if DisconnectPending is already set
- Replace ConcurrentQueue<NetState> _disposed with Queue<NetState> — server is single-threaded; moved the field into
the Network partial class where it's consumed
- Move ConnectingSocketIdleLimit into the Network partial class alongside DisconnectUnattachedSockets
- Reset activity timer on receive, not just send — receiving data directly proves liveness instead of relying on the
ping→pong→send round-trip to reset the timer
- Move CheckAllAlive from Timer into Slice — the timer fired before I/O completions were processed, so after server
stalls (world saves), buffered client pings hadn't reset timestamps yet, causing false disconnects. Now runs at the
end of Slice() after all recv completions are handled
- Lower inactivity timeout 90s → 30s, check interval 90s → 5s — clients ping every ~1s, so 30s of silence is ~30
missed pings; worst-case detection drops from ~180s to ~35s
- Simplify CheckAlive — early-return when socket is null or alive; force-kill stuck DisconnectPending sockets
immediately instead of calling Disconnect() again
- Remove unused imports from GameEncryption.cs
2026-02-13 23:05:20 -08:00
Kamron Batman
4b392079e9
fix: Fixes exploits with bank box deposits. Makes stacking more efficient (#2337)
### Summary

- Fix AccountGold gold duplication exploit: When AccountGold.Enabled was true, double-clicking a BankCheck deposited
the full value to the account but then continued creating physical gold piles for the same amount, duplicating the
value
- Fix Deposit/DepositUpTo partial deposit exploit: Both methods created new max-size gold piles and checks without
first filling existing partial stacks, wasting container slots and causing premature "bank full" failures that could
be leveraged to manipulate gold distribution
- Fix BagOfSending bank stacking: Gold and BankCheck items sent via BagOfSending now use Banker.Deposit for efficient
stacking instead of naive TryDropItem, which could fail on a full bank even when existing piles had room
- Improve gold/check deposit efficiency: Banker.Deposit and Banker.DepositUpTo now top off existing gold piles (up to
60k) and bank checks (up to 1M) before creating new items, maximizing use of available container slots
2026-02-13 20:34:35 -08:00
Kamron Batman
f7eab79adb
fix: Fixes arrows in the internal map (#2336) 2026-02-12 21:31:42 -08:00
Kamron Batman
7fe9712593
fix: Fixes NPC acquire on being attacked (#2334) 2026-02-11 00:49:29 -08:00
Kamron Batman
0b8dcdfeaf
fix: Fixes parsing Rect3D and fixes AS contains name (#2333) 2026-02-09 22:49:18 -08:00
Kamron Batman
d625ea5ba4
fix: Fixes punching while pacified (#2332) 2026-02-09 14:35:07 -08:00
Kamron Batman
10f26c387e
fix: Fixes bug with naming item back to default (#2331) 2026-02-08 21:12:57 -08:00
Kamron Batman
f7f1265216
fix: Fixes PotionKeg causing weight issues (#2330) 2026-02-08 20:55:15 -08:00
Kamron Batman
a9d2b0c01f
feat: Adds housing.bin support (#2329)
### Summary

Changes component verification so the order is now housing.bin, then txt files in client, then txt files in Data/Components folder on the server.
2026-02-08 19:10:23 -08:00
Kamron Batman
b191498569
feat: Adds Feature Flag System (#2328)
## Feature Flag System

A runtime feature flag system for dynamically enabling/disabling game systems and blocking specific game elements
without server restarts.

### Overview

Two types of controls:

1. Feature Flags - Named boolean flags that gate entire systems (trading, PvP, vendors, housing, etc.)
2. Dynamic Blocks - Block specific gumps, items, skills, or spells by type

All changes are persisted to JSON, broadcast to online staff, and bypass-able by administrators.

### Predefined Flags
```
┌─────────────────┬──────────┬─────────────────────────────────────┐
│      Flag       │ Category │             Description             │
├─────────────────┼──────────┼─────────────────────────────────────┤
│ player_trading  │ Economy  │ Allow secure trades between players │
├─────────────────┼──────────┼─────────────────────────────────────┤
│ vendor_purchase │ Economy  │ Allow purchasing from NPC vendors   │
├─────────────────┼──────────┼─────────────────────────────────────┤
│ vendor_sell     │ Economy  │ Allow selling to NPC vendors        │
├─────────────────┼──────────┼─────────────────────────────────────┤
│ player_vendors  │ Economy  │ Allow player vendor interactions    │
├─────────────────┼──────────┼─────────────────────────────────────┤
│ bank_access     │ Economy  │ Allow bank box access               │
├─────────────────┼──────────┼─────────────────────────────────────┤
│ house_placement │ Housing  │ Allow new house placements          │
├─────────────────┼──────────┼─────────────────────────────────────┤
│ boat_placement  │ Housing  │ Allow new boat placements           │
├─────────────────┼──────────┼─────────────────────────────────────┤
│ bulk_orders     │ Crafting │ Allow bulk order deeds              │
├─────────────────┼──────────┼─────────────────────────────────────┤
│ pvp_combat      │ Combat   │ Allow player vs player combat       │
└─────────────────┴──────────┴─────────────────────────────────────┘
```

### Commands

#### Feature Flags
```
┌────────────────────────────────────────────────────┬───────────────────────┐
│                      Command                       │      Description      │
├────────────────────────────────────────────────────┼───────────────────────┤
│ [FeatureFlag <key> on|off|toggle|info              │ Manage a feature flag │
├────────────────────────────────────────────────────┼───────────────────────┤
│ [FF <key> on|off                                   │ Shorthand alias       │
├────────────────────────────────────────────────────┼───────────────────────┤
│ [FeatureFlag <key> create <category> <desc>        │ Create a custom flag  │
├────────────────────────────────────────────────────┼───────────────────────┤
│ [FeatureFlag <key> delete                          │ Delete a custom flag  │
├────────────────────────────────────────────────────┼───────────────────────┤
│ [FeatureList [flags|gumps|items|skills|spells|all] │ List all flags/blocks │
├────────────────────────────────────────────────────┼───────────────────────┤
│ [FeatureAdmin                                      │ Open the admin gump   │
└────────────────────────────────────────────────────┴───────────────────────┘
```

#### Gump Blocks
```
┌────────────────────────────────┬──────────────────────────────────────────────────────┐
│            Command             │                     Description                      │
├────────────────────────────────┼──────────────────────────────────────────────────────┤
│ [BlockGump <typeName> [reason] │ Block a gump type from displaying                    │
├────────────────────────────────┼──────────────────────────────────────────────────────┤
│ [UnblockGump <typeName>        │ Remove a gump block                                  │
├────────────────────────────────┼──────────────────────────────────────────────────────┤
│ [ListGumps [self]              │ List open gumps on a player (for finding type names) │
└────────────────────────────────┴──────────────────────────────────────────────────────┘
```

#### Item Blocks
```
┌────────────────────────────────────────────────┬───────────────────────────────┐
│                    Command                     │          Description          │
├────────────────────────────────────────────────┼───────────────────────────────┤
│ [BlockItemUse <typeName|target> [reason]       │ Block item use                │
├────────────────────────────────────────────────┼───────────────────────────────┤
│ [BlockItemEquip <typeName|target> [reason]     │ Block item equipping          │
├────────────────────────────────────────────────┼───────────────────────────────┤
│ [BlockItemContainer <typeName|target> [reason] │ Block container access        │
├────────────────────────────────────────────────┼───────────────────────────────┤
│ [UnblockItemUse <typeName>                     │ Remove use block              │
├────────────────────────────────────────────────┼───────────────────────────────┤
│ [UnblockItemEquip <typeName>                   │ Remove equip block            │
├────────────────────────────────────────────────┼───────────────────────────────┤
│ [UnblockItemContainer <typeName>               │ Remove container access block │
└────────────────────────────────────────────────┴───────────────────────────────┘
```
Item block commands create an entry if one doesn't exist, or flip the relevant flag on an existing entry. Unblock
commands only clear their specific flag -- the entry is removed when all flags (use/equip/container) are off.

#### Skill & Spell Blocks
```
┌──────────────────────────────────────┬──────────────────────┐
│               Command                │     Description      │
├──────────────────────────────────────┼──────────────────────┤
│ [BlockSkill <SkillName> [reason]     │ Block a skill        │
├──────────────────────────────────────┼──────────────────────┤
│ [UnblockSkill <SkillName>            │ Remove a skill block │
├──────────────────────────────────────┼──────────────────────┤
│ [BlockSpell <SpellTypeName> [reason] │ Block a spell        │
├──────────────────────────────────────┼──────────────────────┤
│ [UnblockSpell <SpellTypeName>        │ Remove a spell block │
└──────────────────────────────────────┴──────────────────────┘
```

### Architecture

- Static bool classes for hot-path checks (no dictionary lookups): ServerFeatureFlags (Server project) and
ContentFeatureFlags (UOContent)
- Synced automatically by FeatureFlagManager.SyncStaticFlag() when flags change
- Predefined flags loaded from Distribution/Configuration/FeatureFlags/default-flags.json
- Runtime state persisted to Configuration/FeatureFlags/*.json (flags, gump-blocks, item-blocks, skill-blocks,
spell-blocks)
- Admin gump with tabbed UI for managing all block types, per-entry PropertiesGump editing, and pagination
- All staff at AccessLevel.GameMaster+ bypass dynamic blocks; AccessLevel.Administrator+ bypass feature flags

### Hook Points

Hook: Player trading
Location: Mobile.OpenTrade
Mechanism: ServerFeatureFlags.PlayerTrading
────────────────────────────────────────
Hook: PvP combat
Location: Mobile.CanBeHarmful
Mechanism: ServerFeatureFlags.PvPCombat
────────────────────────────────────────
Hook: Bank access
Location: BankBox.Open()
Mechanism: ServerFeatureFlags.BankAccess
────────────────────────────────────────
Hook: Vendor buy/sell
Location: BaseVendor
Mechanism: ContentFeatureFlags.VendorPurchase/Sell
────────────────────────────────────────
Hook: Player vendors
Location: PlayerVendor
Mechanism: ContentFeatureFlags.PlayerVendors
────────────────────────────────────────
Hook: House placement
Location: HousePlacement.Check()
Mechanism: ContentFeatureFlags.HousePlacement (returns BadRegionTemp)
────────────────────────────────────────
Hook: Boat placement
Location: BaseBoatDeed.OnDoubleClick/OnPlacement
Mechanism: ContentFeatureFlags.BoatPlacement
────────────────────────────────────────
Hook: Bulk orders
Location: SmallBOD/LargeBOD
Mechanism: ContentFeatureFlags.BulkOrders
────────────────────────────────────────
Hook: Gump display
Location: GumpSystem.SendGump
Mechanism: FeatureFlagManager.IsGumpBlocked
────────────────────────────────────────
Hook: Item use
Location: PlayerMobile.AllowItemUse
Mechanism: FeatureFlagManager.IsItemUseBlocked
────────────────────────────────────────
Hook: Item equip
Location: PlayerMobile.CheckEquip
Mechanism: FeatureFlagManager.IsItemEquipBlocked
────────────────────────────────────────
Hook: Container access
Location: BaseContainer.DisplayTo
Mechanism: FeatureFlagManager.IsContainerAccessBlocked
────────────────────────────────────────
Hook: Skill use
Location: PlayerMobile.AllowSkillUse
Mechanism: FeatureFlagManager.IsSkillBlocked
────────────────────────────────────────
Hook: Spell casting
Location: Spell.Cast / Spellbook.CastSpellRequest
Mechanism: FeatureFlagManager.IsSpellBlocked
2026-02-07 12:02:57 -08:00
Bohica
e4178bb495
fix: pets will now fallback to follow/stay/guard after combat (#2325) 2026-02-05 15:38:39 -08:00
Kamron Batman
15557aa216
fix: Fixes SmallBOD Exception validation (#2327) 2026-02-05 12:02:54 -08:00
Kamron Batman
ac9d7d83ff
fix: Fixes logout in new networking (#2326) 2026-02-04 22:42:18 -08:00
Kamron Batman
fd2f90cba9
feat: Adds SerializableFieldChanged option (#2324) 2026-02-01 21:28:26 -08:00
Kamron Batman
06bb2f552e
fix: Moves PlayerBarkeeper to Serialization Generator (#2323) 2026-02-01 17:01:39 -08:00
Kamron Batman
3c0d6cb9d6
feat: Upgrades networking to use io_uring. (#2315)
> [!IMPORTANT]
> **Breaking Changes**
> - DecodePacket and EncodePacket delegates replaced with IClientEncryption interface
> - NetState.Connection (Socket) replaced with internal RingSocket management
> - NetState.RecvPipe and NetState.SendPipe removed (buffers managed internally)

## Summary

Upgrades the networking stack from PollGroup-based I/O to io_uring, significantly improving I/O performance on Linux.
This also adds native client encryption support for encrypted UO clients.

## Major Changes

io_uring Networking Architecture
- Replaced PollGroup with IORingGroup for async socket I/O operations
- Removed Pipe.cs (mirrored ring buffer) and TcpServer.cs in favor of RingSocketManager
- Added NetState.Network.cs - centralized network infrastructure handling accept, recv, send, and disconnect
completions
- Added SocketHelper.cs - platform-specific socket utilities for raw socket handle operations (getpeername,
getsockname)
- Buffer management now handled by RingSocketManager with configurable slab allocation

### Client Encryption Support
- Added full encryption stack in Network/Encryption/:
  - EncryptionConfig.cs - configurable encryption modes (None, Unencrypted, Encrypted, Both)
  - EncryptionManager.cs - encryption detection and initialization for login/game packets
  - LoginEncryption.cs - handles login packet encryption with version-derived keys
  - GameEncryption.cs - handles game server encryption using Twofish
  - TwofishEngine.cs - optimized Twofish block cipher implementation
  - LoginKeys.cs - encryption key table for client versions
  - IClientEncryption.cs - interface for client encryption implementations

### NetState Improvements
- Replaced Socket Connection with RingSocket _socket for managed socket lifecycle
- Changed from GCHandle polling to event-based completion processing
- Disconnect handling now properly waits for pending sends to flush
- Simplified connecting socket management using lazy queue removal

### Configuration
- New settings: network.encryptionMode and network.encryptionDebug
- Encryption mode flags: Unencrypted, Encrypted, or Both

### Dependencies
- Replaced PollGroup NuGet package with IORingGroup
- Linux requires liburing-dev / liburing-devel package

### Test plan

- Verify server starts and accepts connections on Linux with io_uring
- Verify server starts and accepts connections on Windows (fallback to IOCP)
- Test unencrypted client connections (ClassicUO with encryption disabled)
- Test encrypted client connections if available
- Verify graceful disconnect flushes pending data
- Confirm CI builds pass on all target platforms
2026-02-01 16:02:32 -08:00
Kamron Batman
91a553b0bc
chore: Fixes 0x78 and min client version for packets.html (#2322) 2026-01-31 10:58:13 -08:00
Kamron Batman
3e8f14a0ac
chore: Adds missing packets and cleans up packets.html (#2321) 2026-01-31 10:20:48 -08:00
Kamron Batman
0b92eec791
fix: Fixes puzzle chest migration (#2319) 2026-01-27 17:18:21 -08:00
Kamron Batman
6b64fb3c3f
feat: Converts PuzzleChest to generator, optimizes (#2318) 2026-01-26 21:20:01 -06:00
Kamron Batman
0404251638
feat: Adds Latin1 text support (#2317)
## Summary

- Adds proper Latin1 encoding support, replacing CP1252 usage throughout the codebase
- Adds specialized, optimized string decoding methods with safe string filtering for each encoding type
- Filters invalid Unicode characters (C0/C1 control codes, non-characters) by removal rather than replacement since
the UO client renders nothing for these characters
- Fixes UTF-16 null terminator position handling to correctly advance by 2 bytes

## Changes

TextEncoding.cs

- Added SearchValues-based invalid byte/char detection for efficient filtering
- Added encoding-specific GetString methods: GetStringAscii, GetStringLatin1, GetStringUtf8, GetStringBigUni,
GetStringLittleUni
- Each method supports a safeString parameter for filtering invalid characters
- Little-endian UTF-16 uses direct memory cast for zero-copy decoding on LE systems
- Invalid characters are removed (not replaced with U+FFFD) since the client renders nothing for them

SpanReader.cs

- Added ReadLatin1() and ReadLatin1Safe() methods
- Rewrote encoding-specific read methods to use optimized TextEncoding.GetString* methods
- Fixed UTF-16 null terminator handling: position now correctly advances by byteLength (2) instead of 1

SpanWriter.cs

- Added WriteLatin1 and WriteLatin1Null methods

## Packet Updates

- Updated all packet code to use Latin1 encoding instead of CP1252
- Affected: account packets, equipment packets, menu packets, message packets, mobile packets, player packets, secure
trade packets, vendor packets, gump packets, book packets, mahjong packets

## Filtering Behavior

Invalid characters filtered in safe mode:
```
┌───────────────┬────────────────────────┐
│     Range     │      Description       │
├───────────────┼────────────────────────┤
│ 0x00-0x1F     │ C0 control codes       │
├───────────────┼────────────────────────┤
│ 0x7F          │ DEL                    │
├───────────────┼────────────────────────┤
│ 0x80-0x9F     │ C1 control codes       │
├───────────────┼────────────────────────┤
│ 0xFFFE-0xFFFF │ Unicode non-characters │
└───────────────┴────────────────────────┘
```

Note: Surrogate pairs (0xD800-0xDFFF) are not filtered because proper validation requires context checking for paired
vs unpaired surrogates. The UO client renders nothing for these anyway.

## Test Plan

- All 631 Server.Tests pass
- Verified client rendering behavior using TestUnicodeGump command (pages 1-5)
- Confirmed U+FFFD, unpaired surrogates, and non-characters all render as blank in client
- Verified Latin1 characters (0xA0-0xFF) display correctly
- Verified C1 control codes (0x80-0x9F) are filtered and don't display
2026-01-22 15:51:00 -08:00
Kamron Batman
1e4c32c809
fix: Exempts localhost from IPLimiter. Preps testing for io_uring (#2316)
### Summary
* Exempts localhost from IPLimiter
* Updates tests to have proper sequential testing with packets
* Updates tests to clean up NetState
2026-01-20 08:54:15 -08:00
Arutosio
8133983b64
feat: Adds OnThirstChanged (#2313) 2026-01-13 17:04:41 -08:00
Kamron Batman
e8c129b39d
fix: Bumps PollGroup to fix socket migrations and optimize for Windows 10+ (#2312)
### Summary

* Fixes migration a socket from one poll group to another
* Fixes epoll/wepoll incompatibility
* Updates minimum support to Windows 10+ (Server 2019+)
* Enhances performance by supporting synchronous IOCP completions when available (up to 30% increase in performance).
2026-01-08 21:42:50 -08:00
Kamron Batman
ee2cd1d18d
feat: Adds new decay system and SkipSerialization (#2311)
## Summary

- Replaces scan-based decay checking during world saves with an event-driven timer wheel scheduler
- Removes virtual property checks from serialization hot path, achieving 6-8x faster world saves

## Changes

### DecayScheduler (new):
- Timer wheel with 12 HashSet buckets (5-min intervals) + PriorityQueue for active processing
- O(1) register/unregister vs O(n) scan of all items
- Auto start/stop when items exist/empty
- Configurable tick interval with jitter to prevent system synchronization

### Item.cs:
- Added ScheduledDecayTime computed property
- Added UpdateDecayRegistration() called from SetLastMoved(), property setters, AddItem()/RemoveItem()
- Hooks in Visible, Movable, Spawner setters and Delete()

### World.cs:
- Removed _decayQueue, EnqueueForDecay(), ProcessDecay()
- ItemPersistence.Serialize() now tight loop without virtual calls

## Performance

| Metric        | Before     | After     | Improvement |
|---------------|------------|-----------|-------------|
| Items (230K)  | 245K ticks | 34K ticks | 8x faster   |
| Mobiles (43K) | 56K ticks  | 6K ticks  | 9x faster   |
| Total         | 302K ticks | 40K ticks | 7.5x faster |

## Configuration

decay.maxItemsPerTick = 250       # Items processed per tick
decay.tickInterval = 256ms        # Base processing interval
decay.bucketInterval = 5min       # Timer wheel bucket size
decay.jitterMaxMs = 25            # ±25ms tick jitter
2026-01-08 11:43:51 -08:00
Kamron Batman
c14f361de1
fix: Moves containers/bods/traps/etc to serialization generator (#2310) 2026-01-07 21:55:55 -08:00
Kamron Batman
f4a87a8629
fix: Fixes adding items with ambiguous type lookup (#2307)
Fix [add command failing with ambiguous type names + refactor for performance

### Problem

[add blight would fail with "No type with that name was found" because multiple types contain "blight" (e.g., Server.Items.Blight, Server.Ethics.Evil.Blight, Server.Items.BlightGrippedLongbow, Server.Items.QuiverOfBlight). The old code only succeeded when exactly one type matched the search regardless of constructability and inheriting Mobi les/Items.

### Solution

Exact match takes priority: If a type's name exactly equals the search string (case-insensitive), use it directly. Otherwise, show the AddGump with all partial matches.

- [add blight → Creates Blight (exact name match)
- [add bligh → Shows gump with Blight, BlightGrippedLongbow, etc.

### Refactoring

- CommandEventArgs context: Added GetContext<T>/SetContext<T> to pass resolved type through the command chain without method signature changes
- Removed TrySetupTarget duplication: Validation now happens only in ValidateArgs, eliminating redundant code paths
- Split type matching:
  - ExactMatch(string) → Returns Type for exact name match (used by [add)
  - MatchEmptyCtor(string) → Returns ConstructorInfo[] for gump display (empty-callable constructors only)

### Memory & Performance Improvements

| Optimization                  | Benefit                                                                                      |
|-------------------------------|----------------------------------------------------------------------------------------------|
| _mobileItemTypes cache        | Filters Mobile/Item types once per assembly, reused on all subsequent searches               |
| ReadOnlySpan<string> for args | Avoids string[] heap allocations when slicing arguments                                      |
| ValueStringBuilder            | Stack-allocated string building, avoids StringBuilder heap allocation                        |
| Single type resolution        | Type resolved once in ValidateArgs, passed via context to Execute (was resolved 2-3x before) |
2026-01-06 21:21:29 -08:00
Kamron Batman
c4c8b45b79
fix: Fixes BaseVendor IsInvuln configuration (#2309) 2026-01-05 09:19:48 -08:00
Kamron Batman
89f620218d
fix: Fixes idleCPU override (#2308) 2026-01-04 17:21:08 -08:00
Kamron Batman
bc6735bd23
feat: Adds grid support for the DynamicGump system (#2306)
### Summary 

- Adds a new grid layout system for DynamicGump with zero heap allocations
 - Migrates SpawnerControllerGump from legacy GumpGrid to DynamicGump
 - Migrates CommandListGump from BaseGridGump to DynamicGump
 - Removes legacy GumpGrid (no longer needed)

 ### New Grid Layout Components

 | Component | Purpose |
 |-----------|---------|
 | `GridCell` | Value type for cell bounds (x, y, width, height) |
 | `GridSizeSpec` | Parses CSS-like sizing specs ("10*", "*", "100") |
 | `GridCalculator` | Computes track positions using stackalloc |
 | `ListViewLayout` | Pagination + column layout for list views |
 | `GridBuilderExtensions` | Extension methods accepting `GridCell` |
 | `GridEntryStyle` | Styling properties for BaseGridGump migration |
 | `GridEntryExtensions` | Entry methods matching BaseGridGump patterns |

 ### Memory Impact

 | Component | Legacy | New |
 |-----------|--------|-----|
 | Grid storage | ~200 bytes heap | 0 bytes (stackalloc) |
 | Column/Row lists | ~400 bytes heap | ~128 bytes stack |
 | ListView | ~300 bytes heap | ~64 bytes stack |
 | **Total per render** | **~900 bytes heap** | **0 bytes heap** |

 ### Test plan

 - [x] All 21 gump tests pass
 - [x] Build succeeds with no warnings
 - [ ] In-game verification of SpawnerControllerGump
 - [ ] In-game verification of CommandListGump (HelpInfo command)
2026-01-03 10:12:22 -08:00
Kamron Batman
a354f79f54
fix: Fixes Packet Documentation scrolling on mobile. (#2305) 2026-01-02 22:17:01 -08:00
Kamron Batman
efe385a176
chore: Adds responsiveness to Packet Documentation (#2304) 2026-01-01 21:28:48 -08:00
Kamron Batman
a9e611d46a
chore: Fixes github pages (#2303) 2026-01-01 19:35:35 -08:00
Kamron Batman
1a7c94a442
feat: Adds Network Packet Documentation (#2302) 2026-01-01 17:52:56 -08:00
Kamron Batman
ec287d7691
fix: Fixes zero height spawners and normalizes spawn bounds before use. (#2301) 2025-12-31 10:25:42 -08:00
Kamron Batman
723e1a836d
feat: Deduplicates spawns. Adds spawnbounds where needed. (#2299)
### Summary

- Add z-restricted spawnBounds to 35 spawners across 15 files to prevent creatures from spawning on incorrect floors
- Building spawners (Pyramid, Fire dungeon, Shame, Vendors) use minZ = spawner_z to prevent spawning below
- Ground level spawners (Graveyards, Outdoors, Tokuno) use minZ = -128 for natural terrain variation
- Multi-floor structures in Ilshenar use floor-specific Z restrictions

### Test plan

- Verify spawners in Fire dungeon building don't spawn creatures on floors below
- Verify Pyramid spawners keep creatures on their respective levels
- Verify ground level spawners (graveyards, outdoors) still spawn correctly on terrain
- Check Ilshenar multi-floor structure spawns creatures on correct floors
2025-12-30 00:21:03 -08:00
Kamron Batman
598223703f
refactor: Add BitMask256 utility for 256-bit bitmask operations (#2300)
### Summary

- Create BitMask256 struct with scalar operations (benchmarks showed AVX2 vectorization provides no benefit for this size)
- Refactor Map.cs to use BitMask256 for full Z range support (-128 to 127)
- Remove SectorSpawnCache struct, use BitMask256 directly in manager
- Update tests to use BitMask256 directly
- Remove unused test assertions for old 64-bit behavior
2025-12-29 12:54:49 -08:00
Kamron Batman
d3b43f6f0e
feat: Fixes spawner bugs with position finding, Adds [ShowSpawnerBorders (#2297)
### Summary

- Fixes BaseSpawner and Spawner bugs.
- Adds [ShowSpawnerBorders to see the spawn bounds.
2025-12-28 18:26:03 -08:00
Kamron Batman
9887818e7e
fix: Prevent cascading deletes during world deserialization (#2298)
### Summary

Fixes an issue where deleting entities during world load can cause cascading delete failures when the world is not fully loaded.
2025-12-28 17:54:53 -08:00
Kamron Batman
cc4a679fb0
fix: Fixes serializing EngravedText (#2296) 2025-12-28 02:44:49 -08:00
Kamron Batman
3e8d548f38
feat: Add spawn position caching and spiral scan optimization (#2295)
### Summary

Adds spawn position caching and optimization for constrained spawners (e.g., those near houses, water, or blocked terrain).

### Key features:
- Sector-based bitmap cache (32 bytes per 16x16 sector) stores valid spawn positions
- Spiral scan progressively discovers positions from spawner center outward
- Automatic mode detects constrained spawners after 5+ non-transient failures
- Prevents mob spawning inside private houses (allows public AoS buildings)
- Deduplicates sector lookups for multi-bounds spawners (RegionSpawner)
- Cache invalidation on house placement/demolition
- Moves SpawnBounds to Spawner

### New spawner properties:
- SpawnPositionMode: Automatic (default), Enabled, Disabled, Abandoned
- MaxSpawnAttempts: Configurable attempts before optimization engages (default: 5)
2025-12-28 02:40:21 -08:00
Kamron Batman
6d51b33cf8
feat: Add CanSpawnMobile overload with props Z-range support. (#2293)
### Summary

- Adds CanSpawnMobile(x, y, minZ, maxZ, canSwim, cantWalk, out spawnZ) overload for finding spawn surfaces within a Z range
- Adds CanSpawnItem(x, y, minZ, maxZ, out spawnZ) for item spawning with Surface+Impassable support (tables, furniture)
- Uses bitmask optimization inspired by Item.DropToWorld's m_OpenSlots pattern for O(1) surface/blocker checks
- HomeRange spawners now use surface detection to set proper Z bounds

### Key Changes

Map.cs:
- CanSpawnMobile with Z-range finds lowest valid surface for mobiles
- CanSpawnItem with Z-range finds lowest valid surface for items (including tables)
- CanFitItem for point-check item placement on Surface+Impassable tiles
- Bitmask approach eliminates nested loops and stackalloc arrays

Spawners:
- Simplified GetSpawnPosition using new Z-range methods
- HomeRange setter detects surface below spawner for proper Z bounds
- Consistent handling for mobiles and items

### Bug Fixes

- Water tiles (Impassable | Wet) no longer block swimming mobs
- Items can now spawn on tables/furniture (Surface+Impassable)

### Test Plan

- Run dotnet test - 631 tests pass
- Manual testing: multi-story spawning, water mobs, item spawning on tables
- Verify HomeRange spawner movement shifts bounds correctly
2025-12-27 17:01:15 -08:00
Kamron Batman
ebaf104935
chore: Use var everywhere (#2294) 2025-12-27 16:47:28 -08:00
Quick
0b34cc4417
feat: Changes Spawner HomeRange to SpawnBounds (#2290)
### Summary

This PR transitions the spawner system from a simple radius-based model to a flexible 3D boundary system.

### Core Changes

* **Replaced `HomeRange` with `SpawnBounds`**: Spawners now use a `Rectangle3D` to define spawn areas instead of a circular integer range.
* **Backward Compatibility**:
* The `HomeRange` property remains as a helper that generates square `SpawnBounds` centered on the spawner.
* Included a migration path (v10 to v11) that automatically converts old range data into new bounds during deserialization.


* **Dynamic Bounds Shifting**: If a spawner is moved, its `SpawnBounds` will automatically shift with it, provided the bounds are currently configured as a centered square.
* **New Spawn Logic**: Added `SpawnLocationIsHome` toggle. If enabled, spawned mobiles treat their exact spawn coordinates as their "Home" rather than the spawner's location.

### Implementation Details

* **Interface Updates**: Updated `ISpawner` to include `WalkingRange`, `SpawnBounds`, and `IsInSpawnBounds()`.
* **UI Enhancements**: The Spawner Controller Gump now displays "Custom" for complex bounds and allows copying of the new boundary properties between spawners.
* **Refactored Constructors**: Streamlined `BaseSpawner`, `ProximitySpawner`, and `RegionSpawner` constructors to support the new data types.
2025-12-26 18:07:26 -08:00
Kamron Batman
bde072f81c
feat: Bumps Serialization Generator for better support (#2292)
### Summary

* Adds better `SaveFlag` support.
* Fixes `SortedSet` support.
* Adds custom comparer support for `SortedSet`.
* Fixes various bugs.
* Adds support for `struct`, `record`, and `generic` classes.
* Adds support for `readonly` fields by skipping serializaton entirely.
* Adds support for interface fields that inherit ISerializable
* Adds tests.

See: 59a92cc49a
2025-12-26 14:27:24 -08:00
Bohica
99d8a3db41
fix: Fixes transfer pet spam (#2291) 2025-12-20 09:52:34 -08:00
Bohica
0d25f83de5
fix: Fixes pet guard order (#2287) 2025-12-06 16:18:26 -08:00
Kamron Batman
771cb50919
fix: Converts houses/deeds to serialization generator (#2288)
### Summary
* Converts HouseDeeds to serialization generator
* Converts Houses to serialization generator
* Converts ContestHouses to serialization generator
2025-12-04 21:26:39 -08:00
Philip Gheno
d984961b38
fix: Updates Visual Studio version in solution file to Visual Studio 2026 (#2285) 2025-12-01 14:46:36 -08:00
Kamron Batman
8048711483
fix: Fixes ConditionTeleporter.DenyFollowers (#2284) 2025-11-29 16:28:14 -08:00
Kamron Batman
1f0f272915
feat: Asks the name of the shard (#2283) 2025-11-29 12:37:09 -08:00
Kamron Batman
6a7d49b679
fix: Fixes allocating CompactInfo when changing the weight of an item (#2282)
### Summary

* Fixes extra allocations and computing weight values when changing the weight of an item.
2025-11-29 12:28:27 -08:00
Kamron Batman
5fb0e806c5
feat: Adds proper OSI tracking up to 120 tiles. (#2281)
### Summary

Fixes tracking skill to 10 tiles per 10% up to 120 tiles.
2025-11-29 09:55:52 -08:00
Kamron Batman
ad26ab6260
fix: Fixes returning min distance from GetXInRangeByDistance (#2280) 2025-11-28 11:56:06 -08:00
Kamron Batman
06443ba0ca
fix: Cleans up the GetXDistance methods. (#2279) 2025-11-28 11:01:20 -08:00
Kamron Batman
f2ce860c18
feat: Adds Map.GetXByDistance (for tracking skill). Fixes negative range checks. (#2252)
### Summary

Adds `XInRangeByDistance` and `XInBoundsByDistance` methods to `Map.cs`:

**Item Distance Enumeration:**
```cs
ItemDistanceEnumerable<Item> GetItemsInRangeByDistance(Point3D p);
ItemDistanceEnumerable<Item> GetItemsInRangeByDistance(Point3D p, int range);
ItemDistanceEnumerable<T> GetItemsInRangeByDistance<T>(Point3D p) where T : Item;
ItemDistanceEnumerable<T> GetItemsInRangeByDistance<T>(Point3D p, int range) where T : Item;
ItemDistanceEnumerable<Item> GetItemsInRangeByDistance(Point2D p);
ItemDistanceEnumerable<Item> GetItemsInRangeByDistance(Point2D p, int range);
ItemDistanceEnumerable<T> GetItemsInRangeByDistance<T>(Point2D p) where T : Item;
ItemDistanceEnumerable<T> GetItemsInRangeByDistance<T>(Point2D p, int range) where T : Item;
ItemDistanceEnumerable<Item> GetItemsInRangeByDistance(int x, int y, int range);
ItemDistanceEnumerable<T> GetItemsInRangeByDistance<T>(int x, int y, int range) where T : Item;
ItemDistanceEnumerable<Item> GetItemsInBoundsByDistance(Rectangle2D bounds, , bool makeBoundsInclusive = false);
ItemDistanceEnumerable<T> GetItemsInBoundsByDistance<T>(Rectangle2D bounds, bool makeBoundsInclusive = false) where T : Item;
```

**Mobile Distance Enumeration:**
```cs
MobileDistanceEnumerable<Mobile> GetMobilesInRangeByDistance(Point3D p);
MobileDistanceEnumerable<Mobile> GetMobilesInRangeByDistance(Point3D p, int range);
MobileDistanceEnumerable<T> GetMobilesInRangeByDistance<T>(Point3D p) where T : Mobile;
MobileDistanceEnumerable<T> GetMobilesInRangeByDistance<T>(Point3D p, int range) where T : Mobile;
MobileDistanceEnumerable<Mobile> GetMobilesInRangeByDistance(Point2D p);
MobileDistanceEnumerable<Mobile> GetMobilesInRangeByDistance(Point2D p, int range);
MobileDistanceEnumerable<T> GetMobilesInRangeByDistance<T>(Point2D p) where T : Mobile;
MobileDistanceEnumerable<T> GetMobilesInRangeByDistance<T>(Point2D p, int range) where T : Mobile;
MobileDistanceEnumerable<Mobile> GetMobilesInRangeByDistance(int x, int y, int range);
MobileDistanceEnumerable<T> GetMobilesInRangeByDistance<T>(int x, int y, int range) where T : Mobile;
MobileDistanceEnumerable<Mobile> GetMobilesInBoundsByDistance(Rectangle2D bounds, bool makeBoundsInclusive = false);
MobileDistanceEnumerable<T> GetMobilesInBoundsByDistance<T>(Rectangle2D bounds, bool makeBoundsInclusive = false) where T : Mobile;
```

**Client Distance Enumeration:**
```cs
ClientDistanceEnumerable GetClientsInRangeByDistance(Point3D p);
ClientDistanceEnumerable GetClientsInRangeByDistance(Point3D p, int range);
ClientDistanceEnumerable GetClientsInRangeByDistance(Point2D p);
ClientDistanceEnumerable GetClientsInRangeByDistance(Point2D p, int range);
ClientDistanceEnumerable GetClientsInRangeByDistance(int x, int y, int range);
ClientDistanceEnumerable GetClientsInBoundsByDistance(Rectangle2D bounds, bool makeBoundsInclusive = false);
```

**Example Usage:**

How to use `minDistance` to terminate early when all subsequent mobiles in the iteration will be at an increasing min distance.

```csharp
var playerLocation = player.Location;
const int maxRange = 100;
const int maxMobiles = 12;

var closestMobiles = new SortedSet<Mobile>(Comparer<Mobile>.Create((x, y) =>
{
    var distX = x.GetDistanceToSqrt(playerLocation);
    var distY = y.GetDistanceToSqrt(playerLocation);

    int result = distX.CompareTo(distY);
    if (result == 0)
    {
        result = (x?.Serial ?? Serial.MinusOne).CompareTo(y?.Serial ?? Serial.MinusOne);
    }
    return result;
}));

int lastMinDistance = 0;

foreach (var (mobile, minDistance) in map.GetMobilesInRangeByDistance(playerLocation, maxRange))
{
    // Stop if we have enough and distance starts increasing
    if (closestMobiles.Count >= maxMobiles && minDistance > lastMinDistance)
    {
        break;
    }

    closestMobiles.Add(mobile);
    lastMinDistance = minDistance;
}

// Results are already ordered by proximity
foreach (var mobile in closestMobiles)
{
    var actualDistance = mobile.GetDistanceToSqrt(playerLocation);
    Console.WriteLine($"{mobile.Name}: ActualDist={actualDistance:F2}");
}
```
2025-11-28 10:57:54 -08:00
Kamron Batman
d3fdb180b3
fix: Fixes searching multis/clients. Adds missing map enumeration tests (#2278)
> [!IMPORTANT]
> **Dev Note:** This is an **important** patch as the bug could lead to major issues like:
> * Multis/Players disappearing from view or not being counted during game logic.
> * World processes (e.g., area checks, targeting) failing to detect entities correctly.
> * General stability and correctness concerns for core map functionality.
>
> **Important Breaking Change**: Multis now properly use the map link list. This means modifying a multi while iterating will cause the server to crash. The crash _is expected_. Please modify/fix code accordingly to create a list using `PooledRefQueue` or `PooledRefList` instead of moving/deleting multis while inside the foreach.

### Summary

* Fixes a bug where deleting/moving a multi (boat/house) in some circumstances can use undefined behavior due to unsafe changes to List<BaseMulti>
* Fixes a bug where Multis may not be considered while searching due to a bug causing the sector search to end early.
2025-11-27 11:59:47 -08:00
Kamron Batman
5aed018232
chore: Update README with logo link and image sources 2025-11-25 20:05:05 -08:00
Kamron Batman
c5bbe0bc90
chore: Updates README.md to use stable CDN links for images. 2025-11-25 17:35:22 -08:00
Kamron Batman
7bd5a853a3
feat: Adds size/style support to gump builders, optimizes EscapeHtml (#2257)
> [!IMPORTANT]
> **Developer Note**
> THIS IS A BREAKING CHANGE TO THE NEW API GUMP.
> Please give us feedback in [discord ](https://muo.gg/discord) if you have issues, need help, or have ideas for a better API change!

### Summary

* Adds support for size/style to dynamic/static builder.
* Drastically simplifies the dynamic/static builder api for AddHtml.
* Cleans up some legacy gump files.
2025-11-23 11:35:31 -08:00
Kamron Batman
ea86d5b2a6
chore: Adds Debian 13 to CI/CD (#2277) 2025-11-23 10:09:42 -08:00
Bohica
dfb83cda13
fix: Fixes priced healer resurrection when not choosing the correct option (#2276) 2025-11-23 09:58:33 -08:00
Kamron Batman
4836bff5eb
fix: Eliminates List allocations in various places. (#2158) 2025-11-16 18:33:53 -08:00
Kamron Batman
6f64cddd0b
feat: Optimizes HTML Escape (#2273)
### Summary

Optimizes HTML escaping by using a vectorized search.
2025-11-16 10:32:29 -08:00
Bohica
ee1a40bb51
fix: Fixes movement pathing while in combat (#2271) 2025-11-16 10:14:06 -08:00
Bohica
051f22ff93
fix: Fixes vendor look spam (#2269) 2025-11-16 10:12:12 -08:00
Bohica
5e65f9f0b2
fix: Fixes descending comparer in admin gump (#2272) 2025-11-16 10:09:44 -08:00
Bohica
6cabac5a4f
fix: Fixes pets from attacking themselves (Again) (#2267) 2025-11-15 14:21:18 -08:00
Bohica
5667017874
fix: Fixes pets from attacking themselves (#2265)
### Summary

Stops pets from attacking themselves.
2025-11-15 10:15:10 -08:00
Kamron Batman
6f5b7f7a6b
fix: Fixes SpanWriter/SpanReader tests (#2263) 2025-11-13 00:00:19 -08:00
Kamron Batman
68caaab92c
fix: Makes SpanWriter/SpanReader exceptions clearer (#2262) 2025-11-12 23:34:13 -08:00
Kamron Batman
5d920a25b1
chore: Updates .NET reference material to 10 (#2261) 2025-11-12 23:03:14 -08:00
Bohica
c308a3baad
fix: Fixes timer leak in BaseCamp (#2259) 2025-11-12 09:49:35 -08:00
Kamron Batman
3a3f5ee518
feat: Adds .NET 10 / C# 14 support. (#2258)
### Summary
* Adds .NET 10 support
* Bumps to C# 14
2025-11-11 11:26:34 -08:00
Copilot
fbf8497c59
fix: Fix PlantSystem serialization of LeftSeeds and LeftResources at zero (#2255)
* Initial plan

* Add SerializableFieldDefault attributes for LeftSeeds and LeftResources

This fixes the serialization bug where _leftSeeds and _leftResources were initialized to 8 in the constructor but didn't serialize when their value was 0. Upon deserialization, the constructor would run again and reset these values back to 8.

The SerializableFieldDefault attributes tell the serialization system that the default value is 8, so it will properly serialize 0 values and maintain the correct state across server restarts.

Co-authored-by: kamronbatman <3953314+kamronbatman@users.noreply.github.com>

* Update ShouldSerialize methods to check against default value

Changed ShouldSerializeLeftSeeds() and ShouldSerializeLeftResources() to check if the value is != 8 (the default) instead of != 0. This ensures that only non-default values are serialized, following the same pattern used in BaseWeapon.cs and other classes with SerializableFieldDefault attributes.

Co-authored-by: kamronbatman <3953314+kamronbatman@users.noreply.github.com>

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: kamronbatman <3953314+kamronbatman@users.noreply.github.com>
2025-10-31 00:53:47 -07:00
Kamron Batman
df171926bb
fix: Fixes potential notoriety caching issue in Mobile.cs (#2251)
Replaces the 2D packet cache with a simpler one outlining the packet flag changes
2025-09-11 06:32:48 -07:00
Kamron Batman
641a3eb1f8
fix: Updates BOB/BOD gumps to the new API (#2250)
### Summary

* Moves BOBGump to DynamicGump.
* BOBGump no longer creates a whole new gump context object on every send.
* Moves BODBuyGump to StaticGump.
2025-09-07 12:50:20 -07:00
Kamron Batman
340acafcb3
fix: Fixes mining/lumberjacking multi-harvest (#2249) 2025-08-20 15:30:04 -07:00
Kamron Batman
bbb7dc2294
fix: Fixes dropping a stack of scrolls on a spellbook (#2248) 2025-08-09 20:55:36 -07:00
Kamron Batman
52e309ef95
chore: Updates readme files (#2247) 2025-07-27 11:04:32 -07:00
Bohica
d6bb8316d0
feat: Refactors A* Movement to use PriorityQueue (#2244)
### Summary

* Replaces the node chain with a PriorityQueue.
* Performance difference is up to 3x faster.

### Benchmarks
```cs
| Method              | Mean     | Error     | StdDev    | Gen0   | Allocated |
|-------------------- |---------:|----------:|----------:|-------:|----------:|
| FastAStar_PQueue    | 2.112 us | 0.0186 us | 0.0165 us | 0.0038 |      64 B |
| FastAStar_NodeChain | 6.430 us | 0.0800 us | 0.1807 us |      - |      64 B |
```

### Video

https://github.com/user-attachments/assets/3edd2524-5146-4775-be45-3516f0b01c27
2025-07-26 15:53:06 -07:00
Bohica
6938735113
fix: fixes movement jitter for non-AOS (#2245) 2025-07-26 10:21:36 -07:00
Bohica
f59605190c
fix: fixes mobiles pathing home after combat (#2246) 2025-07-26 10:11:31 -07:00
Kamron Batman
fa0b67ec29
fix: Optimizes items to use default weights. (Part 4) (#2243) 2025-07-24 23:29:29 -07:00
Kamron Batman
40ba85d7b8
fix: Optimizes items to use default weights. (Part 3) (#2242) 2025-07-24 16:21:27 -07:00
Kamron Batman
2c5441731e
fix: Optimizes items to use default weights. (Part 2) (#2241) 2025-07-24 15:41:44 -07:00
Kamron Batman
2eba2868a6
fix: Optimizes items to use default weights. (Part 1) (#2240) 2025-07-24 14:44:39 -07:00
Kamron Batman
c2e44a58f6
fix: Moves container enumerables to Item. (#2238) 2025-07-24 14:26:35 -07:00
Kamron Batman
1e2f4b1171
fix: Fixes tentacles not getting removed from harrower's list (#2239) 2025-07-24 14:18:16 -07:00
Kamron Batman
2df62fba5a
chore: Bumps various dependencies (#2237) 2025-07-22 16:04:04 -07:00
Bohica
173037ae77
fix: Prevents recursion in BaseCreature.DoHarmful() (#2202) 2025-07-22 16:01:56 -07:00
Kamron Batman
8a118170e7
fix: Fixes treasure map chest crash from null guardians (#2236) 2025-07-22 16:01:27 -07:00
Kamron Batman
aab731ed96
feat: Overhauls AI (Speech/Movement) (#2232)
### Summary

* Refactors AI so it is easier to read and maintain
* Fixes NPC speed issues
* Fixes pet sector AI issue that was causing stuttering
* Fixes direction snapping for Melee/Mage AI
* Refactors pet orders
* Refactors speech commands
* Removes scale speed by dex for HS+ (it was a stupid feature anyways)
2025-07-18 22:39:57 -07:00
Kamron Batman
f89150735e
fix: Consolidates debug check messages for AI and adds debounce (#2235) 2025-07-18 18:08:03 -07:00
Kamron Batman
8f88de68a7
fix: Standardizes the variables for AI (#2234) 2025-07-18 17:50:32 -07:00
Kamron Batman
416e1be735
fix: Adds GetDistanceToSqrt for Point3D and centralizes all the calls (#2233) 2025-07-18 09:24:41 -07:00
Bohica
2086e9b292
fix: Fixes direction snapping when pathing to combatant for MeleeAI (#2187)
* fix: Fixes direction snapping when pathing to combatant

* Fixes speed when exiting out of flee action
2025-07-16 21:55:47 -07:00
Bohica
c1af2ade83
fix: Fixes pet validation for resurrection spell (#2197) 2025-07-16 21:34:43 -07:00
Bohica
5f0561b7fc
feat: New Jail System (#2215)
New Jail System for MUO
----------------------------

Commands:
[jail [player] [reason] - Jail with time escalation per offense (5 to 120 minutes, GM only)
[unjail [player] - Manual release from jail regardless of time (GM only)
[jailinfo [player] - Check jail status and history (GM only)
[jailrecord - Checks their own jail record stats (player access)

Jail/Unjail can be found in the client view of a player in Admin Gump
![Screenshot 2025-06-12 030226](https://github.com/user-attachments/assets/a9f1a736-0316-464c-8958-c589ea0a1dcb)

Jail record gump, invoked with [jailrecord (30 second cooldown)
![Screenshot 2025-06-12 030320](https://github.com/user-attachments/assets/c19b3e76-3042-48ae-a00d-c70ef564bc84)
2025-07-16 20:41:21 -07:00
Bohica
c2c486c06e
fix: Adds body parts to headless one loot (#2225) 2025-07-07 18:54:18 -07:00
Felipe Maya Muniz
4209f7ea16
fix: Add properties for crafting ranged weapons with colored logs (#2222) 2025-07-07 18:51:08 -07:00
Kamron Batman
55d653d752
fix: Fixes skill cooldown for some target skills (#2231)
### Summary

Stealing, Detect Hidden, and Begging will now give credit for the time it took the player to use the targeter against the total skill cooldown. So if the player takes longer than 10s to target, then they can use a skill again immediately.

> [!NOTE]
> This does not change the requirement that players can no longer stack target these skills.
2025-07-06 22:50:40 -07:00
Felipe Maya Muniz
c0948fd0e2
feat: Adds ClearXY command (#2229) 2025-07-06 20:15:50 -07:00
Kamron Batman
edf16effa0
fix: Fixes container not properly consuming in some cases (#2230) 2025-07-06 10:23:37 -07:00
Bohica
8997130e57
fix: fixes death explosion trigger on death (#2228) 2025-07-03 09:47:52 -07:00
Bohica
35faba5087
fix: fixes the id for farmable cabbage (#2227) 2025-07-02 15:04:48 -07:00
Felipe Maya Muniz
c6b5dba83b
fix: Adds colored logs to fletching (#2223) 2025-06-28 17:32:13 -07:00
mdodkins
f83fc94b05
fix: Do not add an extraneous Open Paperdoll context menu entry (#2224) 2025-06-28 17:30:18 -07:00
Felipe Maya Muniz
aadcd6db70
fix: Withdraw should have positive amount to work (#2221) 2025-06-28 11:42:43 -07:00
Kamron Batman
a37481ea93
fix: Fixes control target not reset when giving pet order from context menu (#2220) 2025-06-25 23:31:51 -07:00
Kamron Batman
7d748491d0
fix: Fixes empty/missing bank box prevent withdraw from account gold (#2219) 2025-06-24 10:37:59 -07:00
Bohica
d3e4606050
fix: Fixes SpanReader.Read to fix bounds checks and now returns bytes written (#2211) 2025-06-12 17:25:32 -07:00
Kamron Batman
a2ea4bda37
chore: Removes Qodana. Sad face. (#2217) 2025-06-12 16:48:02 -07:00
Kamron Batman
006c5ed037
fix: Fixes boat decay calculation (#2216) 2025-06-12 16:32:00 -07:00
Kamron Batman
a286e282a9
fix: Fixes fist disarm (pre-aos) (#2214) 2025-06-07 18:22:54 -07:00
Kamron Batman
c8d489b72b
fix: Fixes detect hidden skill cooldown (#2213) 2025-06-07 14:05:49 -07:00
Kamron Batman
14bc38e375
fix: Removes the absurb 6 hour skill edge case and fixes target cancellations (#2212) 2025-06-07 13:58:01 -07:00
Kamron Batman
ea4080ce0e
fix: Eliminates allocations in canned evil timer (#2209) 2025-06-02 22:48:14 -10:00
Kamron Batman
0d2ed60fed
fix: Fixes null components in BaseAddon (#2208) 2025-06-02 22:11:40 -10:00
Kamron Batman
78e2e24fcd
fix: Fixes gold trade exploit (clearing checks properly) (#2207) 2025-06-01 10:38:28 -10:00
Bohica
7695174bcc
fix: Adds null-conditional checks when stopping timers for TownCrier (#2206) 2025-06-01 10:33:09 -10:00
Kamron Batman
35ff4023c3
fix: Fixes gold trade exploit (#2205) 2025-06-01 10:28:56 -10:00
Kamron Batman
7fc98498e4
feat: Adds option for houses to face east with proper calculations (#2204) 2025-05-28 00:31:58 -07:00
Kamron Batman
4e23a8e205
chore: Updates JB logo per Jetbrain's request. (#2203) 2025-05-28 00:09:36 -07:00
Kamron Batman
fff6e19a18
chore: Fixes name verification test (#2201) 2025-05-26 22:36:23 -07:00
Kamron Batman
a07e225911
fix: Changes stealing to use proper defaults per era (#2200) 2025-05-26 22:27:54 -07:00
Kamron Batman
fec5f8ee50
fix: Fixes decaying kill on disconnect (#2199) 2025-05-25 22:06:26 -07:00
Kamron Batman
902797bee0
fix: Fixes murder context getting lost (#2198) 2025-05-25 20:43:59 -07:00
Kamron Batman
c5c647bf77
fix: Fixes curse spell fizzling (#2193) 2025-05-25 14:58:02 -07:00
Kamron Batman
c47ecadace
fix: Fixes item child properties in the wrong order when overridden (#2196) 2025-05-24 12:15:49 -07:00
Kamron Batman
31dc9ffac4
fix: Fixes player vendors not reattaching to their house on deserialization (#2195) 2025-05-24 12:00:53 -07:00
Kamron Batman
f3bcf89015
fix: Fixes weapons not serializing lesser poison (#2194)
### Summary

Fixes weapons not serializing lesser poison.

> [!NOTE]
> Dev Note: After applying this fix, fix weapons already in-game using the following command
> `[global set poison lesser where baseweapon poison = null poisoncharges > 0`
2025-05-23 15:54:17 -07:00
Kamron Batman
5de42d94fc
fix: Fixes detecting consecutive exceptions in name validation (#2192) 2025-05-18 21:02:05 -07:00
Kamron Batman
f630ec2a2d
fix: Fixes command conditional comparisons (#2191) 2025-05-18 20:16:04 -07:00
Kamron Batman
972e7723ae
fix: Reverts change for trade window that causes an exploit (#2189) 2025-05-17 21:25:48 -07:00
Kamron Batman
c1d8606167
fix: Refactors Custom Hairstylist (#2188) 2025-05-17 03:30:07 -07:00
Kamron Batman
31fb68015f
chore: Updates SPONSORS.md 2025-05-15 23:38:36 -07:00
Kamron Batman
e468f79e18
feat: Adds SpecialAttack monster ability trigger. Fixes fanning fire. (#2186) 2025-05-15 22:14:52 -07:00
Kamron Batman
91b2912ee4
fix: Cleans up potential memory leak in monster abilities. (#2185) 2025-05-15 19:42:54 -07:00
Kamron Batman
f2526c82f3
fix: Fixes Freeshard Protocol (UOGateway) support. (#2183) 2025-05-12 12:24:24 -07:00
mdodkins
0f3d984efc
fix: Fixes craft gump non-cliloc label offsets. (#2182) 2025-05-11 18:13:00 -07:00
Kamron Batman
18903ee17b
fix: Fixes props gump for interfaces again (#2180) 2025-05-09 15:44:32 -07:00
Kamron Batman
05825a11e2
fix: Fixes props for interfaces and adds account manipulation (#2179) 2025-05-09 15:39:47 -07:00
Kamron Batman
f95b3a8b3b
fix: Streamlines event scheduler API. Adds months to weekly recurrence (#2178) 2025-05-07 22:42:44 -07:00
Kamron Batman
990e86b983
fix: Fixes guard infinite loop, and streamlines call methods (#2175) 2025-05-06 23:54:13 -07:00
Kamron Batman
f02403a374
feat: Adds yearly calendared events with recurrences (#2177) 2025-05-06 23:52:49 -07:00
Kamron Batman
4beda6a29d
fix: Eliminate intermediate string in typecache (#2176) 2025-05-06 21:04:54 -07:00
Kamron Batman
f0f7d44d4d
fix: Fixes polymorph spell (#2173) 2025-05-04 09:31:32 -07:00
Kamron Batman
da517f56c4
chore: Fixes variable types for possible performance issues (#2172) 2025-05-01 21:54:41 -07:00
Kamron Batman
6d43f2549c
fix: Adjusts spell ranges to match OSI (#2171)
### Summary

Adjusts spell ranges according to OSI. Specifically, the [April 14, 1999 patch](https://uo.com/wiki/ultima-online-wiki/technical/previous-publishes/1999-2/1999-06-14th-april/) changed spell ranges to 10 tiles, and 15 tiles for fields.
2025-05-01 20:03:56 -07:00
Kamron Batman
5cb97cc6de
fix: Fixes BitArray serialization (#2170)
Fixes serialization/deserialization edge cases with BitArray. If you use BitArray, you will need to migrate.

1. Change the type in the migration JSON file (if there is one) from `BitArray` to `byte[]` for all the versions you need to migrate.
2. Then in the `MigrateFrom`, use the following function to convert the field from a byte[] back to the BitArray.

Example Migration JSON:
```json
    {
      "name": "RestrictedSpells",
      "type": "byte[]",
      "rule": "ArrayMigrationRule",
      "ruleArguments": [
        "byte",
        "PrimitiveTypeMigrationRule",
        ""
      ]
    },
```

Migration function to use in MigrateFrom:
```cs
public static BitArray MigrateBitArray(byte[] data, int bitLength) => new(data) { Length = bitLength };
```

Example use:
```cs
    private void MigrateFrom(V0Content content)
    {
        // ... deserialize
        _restrictedSpells = content.RestrictedSpells.MigrateBitArray(SpellRegistry.Types.Length);
        _restrictedSkills = content.RestrictedSkills.MigrateBitArray(SkillInfo.Table.Length);
        // ... rest of deserialize
    }

```
2025-04-30 19:45:55 -07:00
Kamron Batman
1eeeabb729
fix: Fixes weekly interval schedules for multiple weeks (biweekly) (#2169) 2025-04-30 17:12:29 -07:00
Kamron Batman
7dbfc9d161
fix: Fixes flaky tests, formatting, and sequential testing. (#2168) 2025-04-30 16:42:38 -07:00
Kamron Batman
415c7a6bfd
fix: Optimizes EventScheduler by using PriorityQueue (#2167) 2025-04-30 16:33:59 -07:00
Kamron Batman
21a4092dd8
fix: Changes EventScheduler API so it is more explicit (#2164)
### Summary

Refactors ScheduledEvent and EventScheduler API to use TimeOnly so recurrence offset is explicit.

API:

```cs
public ScheduledEvent(
    DateTime startAfter,
    DateTime endOn,
    TimeOnly time,
    IRecurrencePattern recurrence,
    TimeZoneInfo timeZone = null
)
```

Example:
```cs
// Schedule a daily event at 8:00 AM UTC
EventScheduler.DailyAt(
    new DateTime(2024, 6, 1, 8, 0, 0, DateTimeKind.Utc),
    () => Console.WriteLine("Daily event triggered!")
);

// Schedule a custom recurring event at 3:30 PM UTC every Monday
var recurrence = new WeeklyRecurrencePattern(1, DaysOfWeek.Monday);
EventScheduler.Shared.ScheduleEvent(
    DateTime.UtcNow,
    new TimeOnly(15, 30),
    () => Console.WriteLine("Weekly Monday event!"),
    recurrence
);
```
2025-04-28 16:16:33 -07:00
Kamron Batman
f89b2be653
fix: Fixes major bug with BoBEntries removal (#2165) 2025-04-27 22:05:07 -07:00
Kamron Batman
78feea86b4
feat: Adds scheduler with wallclock timer (#2163)
### Summary

* Adds an event scheduler.
* Adds conveniences for hourly, daily, weekly, biweekly, monthly, ordinal monthly, and yearly recurrences

Example:

```cs
var tz = TimeZoneInfo.FindSystemTimeZoneById("America/New_York");

// Specify the time of the day, and the day of the week you want it to occur. Make sure it is translated into Utc.
// The next occurrence will be _after_ the specified date/time.
var scheduledEvent = EventScheduler.WeeklyAt(new DateTime(2025, 04, 26, 17, 00, 00), StartEvent, tz);

void StartEvent()
{
    World.Broadcast(0x30, false, "The event has started!");
}

Console.WriteLine("Event starts on {0}", scheduledEvent.NextOccurrence);
```

In this example, on _Saturday, May 3rd, 2025 @ 5pm ET_, the message "The event has started!" will be broadcasted.
2025-04-26 22:27:08 -07:00
Kamron Batman
4e25c74cdf
fix: Fixes magical barrier NPE and an edge case (#2162) 2025-04-22 17:52:09 -07:00
Kamron Batman
47b03f5d62
fix: Fixes NPCs not able to equip armor/clothing/weapons (#2161) 2025-04-21 14:01:08 -07:00
Kamron Batman
e54782441e
fix: Fixes TcpServer edge cases where packets are split on login (#2160) 2025-04-15 19:29:17 -07:00
Kamron Batman
4aa272d429
feat: Adds an Item/Mobile memory leak detector. Fixes minor leak in doors. (#2159)
### Summary

Adds the command [TrackLeaks to enable tracking item/mobiles that have been deleted but still have dangling references. Requires adding the _TRACK_LEAKS_ define constant during build.
2025-04-15 19:14:45 -07:00
Kamron Batman
1e349f4369
fix: Fixes mutate speech character limit (#2157)
### Summary

* Fixes the accidental limitation of dead character speech (OoOo) to 256 characters.
* Optimizes it by 1.5x

```cs
| Method                  | Mean      | Error    | StdDev   | Allocated |
|------------------------ |----------:|---------:|---------:|----------:|
| ManuaLoopMutation       | 147.01 ns | 1.298 ns | 1.084 ns |         - |
| SpanLoopMutation        |  92.03 ns | 0.583 ns | 0.487 ns |         - |
```
2025-04-13 22:55:44 -07:00
Kamron Batman
61d074213b
chore(deps): Updates MailKit to 4.11.0 and Microsoft deps to 9.0.4 (#2156) 2025-04-12 12:54:40 -07:00
Kamron Batman
5a23d9f6f8
fix: Fixes an edge case in GetString that can cause a crash while parsing (#2155) 2025-04-12 12:52:10 -07:00
Kamron Batman
872de8d095
fix: Optimizes GetString to eliminate allocations (#2154)
### Summary

* Optimized GetString by eliminating the intermediate string allocation.

** Note **: Encoding.GetChars() is still really inefficient, especially when strings are not aligned or have regular ascii/unicode characters. Thankfully we generally don't have to worry about these odd edge cases, but if they happen then GetChars can allocate hundreds of bytes.


This is a benchmark for just the related changes. NonSpecial are just ascii characters, while the other tests include control codes.
```cs
| Method                         | Mean     | Error   | StdDev  | Gen0   | Allocated |
|------------------------------- |---------:|--------:|--------:|-------:|----------:|
| GetString                      | 306.7 ns | 5.96 ns | 6.86 ns | 0.0124 |     200 B |
| GetStringNotSpecial            | 257.0 ns | 4.83 ns | 4.52 ns | 0.0114 |     184 B |
| GetStringSpanHelpers           | 166.4 ns | 3.26 ns | 3.48 ns |      - |         - |
| GetStringSpanHelpersNotSpecial | 137.3 ns | 1.57 ns | 1.22 ns |      - |         - |
```
2025-04-12 12:42:15 -07:00
Kamron Batman
a94814a7ef
fix: Fixes and optimizes NameVerification and ProfanityProtection (#2153)
### Summary

Significantly improves the performance of NameVerification & ProfanityProtection:

```cs
| Method         | Mean      | Error     | StdDev    |
|--------------- |----------:|----------:|----------:|
| ValidateName   | 728.34 ns | 13.244 ns | 11.059 ns |
| ValidateNameSV |  26.62 ns |  0.233 ns |  0.207 ns |
```
2025-04-12 02:26:10 -07:00
Kamron Batman
0ce2a62a76
fix: Use vectorized search for username-password validation. (#2152)
### Summary

Optimizes account validation:

```cs
| Method              | Mean       | Error     | StdDev    |
|-------------------- |-----------:|----------:|----------:|
| ForLoopUsernameSafe | 158.536 ns | 1.6247 ns | 1.5198 ns |
| SVUsernameSafe      |   2.859 ns | 0.0581 ns | 0.0796 ns |
```
2025-04-11 22:40:06 -07:00
Kamron Batman
fdf8c5cf23
fix: Fixes critical bug in TickCount calculation. (#2151)
### Summary

On some operating systems (like hosted Linux VMs), the `TimeStamp.GetTimeStamp()` CPU tick count will wrap around. This is generally not an issue, except for legacy reasons the TickCount is returned in milliseconds instead of ticks. This means when the values wrap around, they are already divided by the CPU Frequency (usually 1million) and then converted to milliseconds. That means the delta between the tick count before and after wrapping is off by a magnitude of (Frequency / 1000).

Example:

TimeStamp A = 9223372036654775807
Some time has passed:
TimeStamp B = -9223372036654775809

The raw delta is 400_000_000 (400ms) when you do `unchecked(A - B)`.
If we do the calculation AFTER converting it to milliseconds, then:

TickCount A = 9223372036654
TickCount B = -9223372036654

The raw delta is -18446744073308 instead of 400_000_000.

To fix this the calculation was changed so `long` -> `ulong`, then divided, then converted back to `long`, effectively bypassing wrap-around issue.

The new TickCount values in our example become:

TickCount A = 9223372037054
TickCount B = 9223372036654

The delta is 400 (in milliseconds). 🎉
2025-04-09 16:46:53 -07:00
Kamron Batman
0a4579f298
fix: Fixes duping bags (#2148)
### Summary

Fixes duping items in a container not working properly.
2025-04-07 22:27:41 -07:00
Kamron Batman
2c6f1854fa
fix: Fixes guards attacking mobs (#2145)
### Summary

- Fixes infinite teleport from guard refactor for spawners
- Fixes guards not attacking Always Murderer creatures
- Fixes crash when calling guards due to map modification while enumeration
2025-03-12 16:45:39 -07:00
Kamron Batman
47da961a37
fix: Fixes spawning guards. Adds back non-lethal with config (#2140) 2025-03-12 13:08:14 -07:00
mark1145
0024d53739
fix: Fixes edge cases where a leader resigns and becomes promoted in their new guild. (#2136) 2025-03-05 17:59:07 -08:00
Kamron Batman
56e3086ed1
fix: Makes hairdye gump static (#2138) 2025-03-04 15:55:05 -08:00
Kamron Batman
ad361001c5
fix: Fixes CUO connecting with new TCP Peek Filtering (#2137) 2025-03-04 11:00:20 -08:00
Kamron Batman
49fd5f210d
fix: Fixes removing virtue when player is deleted (#2135) 2025-03-02 17:50:18 -08:00
Kamron Batman
2a8c62e8be
fix: Fixes TCPServer accept async, makes Firewall/IP Limiter multithreaded (#2134) 2025-02-27 22:19:38 -08:00
Kamron Batman
bcab91255a
fix: Fixes EVs/BSs attacking players in Pre-AOS (#2132) 2025-02-26 18:43:49 -08:00
Voxpire
b8a8bfd1a9
fix: Fixes console clobbering when prompted to add an owner account (#2133)
### Summary
- Updates AccountPrompt to use `logger` to avoid console clobbering

### Screenshot
![image](https://github.com/user-attachments/assets/8bfbaccc-3c23-4d05-ab49-a656e6a8803f)
2025-02-26 18:43:20 -08:00
Kamron Batman
0265e0673a
fix: Fixes empty spellbooks with EA client on Pre-AOS (#2131) 2025-02-25 20:46:20 -08:00
Kamron Batman
9b361822f3
fix: Fixes issue with delete all of missing type on deserialize (#2129) 2025-02-19 15:36:29 -08:00
Kamron Batman
d10b32aed3
fix: Fixes dead plant serialization (#2128) 2025-02-19 15:23:49 -08:00
Kamron Batman
525112943c
fix: Changes Chyloth gump to static (#2126) 2025-02-14 00:18:38 -08:00
Kamron Batman
2e08eda8cb
feat: Standardizes South/East selection gumps (#1839) 2025-02-14 00:15:14 -08:00
Kamron Batman
279b10dd0f
feat: Replaces params array with params ReadOnlySpan (#2125) 2025-02-13 21:19:02 -08:00
Kamron Batman
90059c5e74
feat: Adds convenience methods to GumpStringsBuilder (#2124) 2025-02-13 19:58:46 -08:00
Kamron Batman
6d7b1b8bee
fix: Adds missing string interpolation handler for SetStringSlot (#2123) 2025-02-13 19:30:47 -08:00
Kamron Batman
899146dcb7
fix: Fixes static gumps sending too much data. (#2122) 2025-02-13 19:12:28 -08:00
Kamron Batman
3cf1bd0f7d
fix: Fixes AddLabelPlaceholder on StaticGump (#2121) 2025-02-13 18:53:24 -08:00
Kamron Batman
41e909cd22
fix: Fixes serialization generator issue with arrays (#2120) 2025-02-12 22:30:46 -08:00
Kamron Batman
af027cdac6
Bumps dependencies. Fixes bugs with serialization generator (#2119) 2025-02-12 20:50:28 -08:00
Kamron Batman
2d0957119e
feat: Adds BaseGump.GetGumpOffsetForItemGraphic (#2116) 2025-02-10 19:53:47 -08:00
Kamron Batman
40479c946a
feat: Adds item graphic size to Bounds.bin and makes item graphic offset available to gumps (#2115) 2025-02-10 19:31:39 -08:00
Bohica
772511d1e8
fix: Reverts GetProtOffset, Adds PreT2A Weapon Damage & PreUOR Armor Durability (#2114) 2025-02-06 01:27:40 -08:00
Bohica
51367df0d2
fix: Fixes AR/Durability/Tool Uses for Pre-UOR and LBR (#2113) 2025-02-05 19:54:56 -08:00
Kamron Batman
d407847fc4
fix: Fixes axes not working when equipped or in backpack (#2112) 2025-02-04 22:11:29 -08:00
uogem
b83c52a7b1
fix: Spawned items should only decay after unlinked from spawner (#2109) 2025-02-04 16:14:56 -08:00
Bohica
1c5fa824d6
fix: Fixes all summon spells so they use summoned variation of monster (#2106) 2025-02-03 23:44:51 -08:00
Bohica
de12f8cb02
fix: Fixes turning to target on spellcast (#2107) 2025-02-03 17:38:07 -08:00
Kamron Batman
2c806cb072
fix: Converts house placement gumps to dynamic (#2100) 2025-02-02 13:25:05 -08:00
Kamron Batman
e64a632998
fix: Moves snapshot request synchronously (#2105) 2025-02-02 13:07:28 -08:00
Kamron Batman
717a1a062e
fix: Fixes race condition with world save snapshot request (#2104)
### Summary

* Fixes a race condition where the snapshot path isn't between the request snapshot being set on a background thread, and the main loop consuming that flag.


Closes #2102
2025-02-02 12:05:40 -08:00
Kamron Batman
019672b026
fix: Fixes cannot see that issue with Map LOS Refactor. (#2103) 2025-02-02 11:38:10 -08:00
Kamron Batman
1264302ee0
fix: Converts barkeep and house foundation commit gumps to dynamic (#2099) 2025-01-27 21:36:56 -08:00
Kamron Batman
cbae44ef20
fix: Updates SummonFamiliar & Tracking gumps to static (#2098) 2025-01-27 17:57:25 -08:00
Kamron Batman
ed87df1a61
fix: Converts bedroll/insurance gumps to static. (#2096) 2025-01-27 14:00:32 -08:00
Kamron Batman
b5e3f49d06
fix: Converts MakersMark gump to static & CraftGumpItem to dynamic (#2095) 2025-01-27 11:29:23 -08:00
Kamron Batman
6d879e1d22
feat: Simplifies honorable execution (#2089) 2025-01-26 23:05:54 -08:00
mark1145
c0eb6c81fe
fix: Fixes door monster LOS exploit & AOS House Gump NPE (#2091) 2025-01-26 23:04:01 -08:00
Kamron Batman
f241a9dec0
fix: Fixes the wrong unit used for BuffIcon Timers (#2093) 2025-01-26 22:17:03 -08:00
Kamron Batman
ea364237f0
fix: Fix crash from not enough fish to kill in Aquarium (#2092) 2025-01-25 18:45:20 -08:00
Kamron Batman
a8b625b338
fix: Aquarium no longer uses list to kill fish (#2090) 2025-01-25 17:41:27 -08:00
mark1145
d5160bd6db
fix: Fixes buffs don't start/stop properly. Streamlines constructor and Add/Remove buffs. (#2082) 2025-01-24 18:20:23 -08:00
Kamron Batman
c8f13b2933
fix: Fixes runebook OPL not updating with charges (#2088) 2025-01-24 18:17:55 -08:00
Kamron Batman
28460114cb
fix: Converts metal/strong puzzle chest to serialiation generator and puzzle gump (#2087) 2025-01-23 22:42:50 -08:00
Kamron Batman
2a97db34e8
fix: Adds quest items to serialization generator (#2086) 2025-01-23 21:59:15 -08:00
Bohica
8e48177cd4
fix: Fixes rangePerception to 16 to match OSI (#2084) 2025-01-23 21:27:18 -08:00
Kamron Batman
ccc9618c91
chore: Bumps macos to 15 (#2085) 2025-01-23 19:36:30 -08:00
Nathan Oines
e2a41ed77c
chore: Removes commented Thank You for vendor buyback. (#2083) 2025-01-21 17:53:21 -08:00
Kamron Batman
aa6fbb66bc
fix: Fixes strangle buff icon appearing on fizzle and adds comment about bug on OSI (#2081) 2025-01-21 17:52:32 -08:00
Kamron Batman
84d9383294
feat: Fixes beneficial notoriety checks and adds better young restrictions/messaging (#2000) 2025-01-20 11:27:19 -08:00
Bohica
64e32a817d
fix: Fixes fillable containers start respawn on unlock. (#2069) 2025-01-20 09:20:26 -08:00
mark1145
f5e944335b
fix: Bufficon timer is not cancelled when refreshed with another one. (#2080) 2025-01-20 09:18:52 -08:00
Kamron Batman
b4b7182ce6
fix: Fixes looking up accounts that were renamed. (#2078) 2025-01-18 17:11:43 -08:00
Kamron Batman
d92b735c18
chore(deps): Bumps Nerdbank, Hashing, FileSystemGlobbing, xunit, and C# version (#2077) 2025-01-18 17:02:18 -08:00
Bohica
8f8d88b5ff
fix: Creature is given follow orders upon taming. (#2076) 2025-01-18 16:58:13 -08:00
2511 changed files with 230045 additions and 242908 deletions

View file

@ -3,7 +3,7 @@
"isRoot": true,
"tools": {
"modernuoschemagenerator": {
"version": "2.12.18",
"version": "4.0.0",
"commands": [
"ModernUOSchemaGenerator"
]

1
.cursorrules Normal file
View file

@ -0,0 +1 @@
Read and follow all instructions in CLAUDE.md in this repository's root.

1
.github/COPILOT-INSTRUCTIONS.md vendored Normal file
View file

@ -0,0 +1 @@
Read and follow all instructions in CLAUDE.md in this repository's root.

View file

@ -9,3 +9,7 @@ updates:
directory: "/" # Location of package manifests
schedule:
interval: "daily"
- package-ecosystem: "github-actions"
directory: "/" # Watches .github/workflows/**
schedule:
interval: "weekly"

View file

@ -3,94 +3,179 @@ name: Build
on:
push:
branches: [main]
paths:
- '.config/dotnet-tools.json'
- 'Projects/**'
- 'Directory.Build.props'
- 'global.json'
- 'version.json'
- '*.slnx'
pull_request:
branches: [main]
paths:
- '.config/dotnet-tools.json'
- 'Projects/**'
- 'Directory.Build.props'
- 'global.json'
- 'version.json'
- '*.slnx'
jobs:
build-macos:
runs-on: ${{ matrix.os }}
# A hung run otherwise bills the full 360-minute default before GitHub kills it.
timeout-minutes: 30
name: Build (${{ matrix.name }})
strategy:
fail-fast: false
matrix:
include:
- os: macos-13
name: MacOS 13
- os: macos-14
name: MacOS 14
- os: macos-15
name: MacOS 15
- os: macos-26
name: MacOS 26
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v7
with:
fetch-depth: 0 # avoid shallow clone so nbgv can do its work.
- name: Install .NET 9
uses: actions/setup-dotnet@v4
- name: Install .NET
uses: actions/setup-dotnet@v6
with:
global-json-file: global.json
- name: Install Prerequisites
run: |
brew update
brew install icu4c libdeflate zstd argon2
brew install icu4c libdeflate argon2
- name: Set Library Path
run: echo "DYLD_LIBRARY_PATH=/opt/homebrew/lib:\$DYLD_LIBRARY_PATH" >> $GITHUB_ENV
run: echo "DYLD_LIBRARY_PATH=/opt/homebrew/lib:$DYLD_LIBRARY_PATH" >> $GITHUB_ENV
- name: Build
run: ./publish.cmd Release
run: dotnet run --project Projects/BuildTool -- --config Release --skip-prereqs
- name: Migration Changes
run: git diff --exit-code ./**/Migrations/*.v*.json
- name: Test
run: dotnet test --no-restore
# blame-hang kills a stuck test host after 10 minutes and reports the in-flight
# tests plus a process dump instead of hanging until the job timeout.
run: |
dotnet test --logger trx --results-directory ./TestResults --blame-hang --blame-hang-timeout 10m --blame-hang-dump-type full
if [ -z "$(find ./TestResults -name '*.trx' 2>/dev/null)" ]; then
echo "::error::No test result files were produced - no test projects ran. Failing to avoid masking failures."
exit 1
fi
- name: Upload test results on failure
if: failure()
uses: actions/upload-artifact@v7
with:
name: TestResults-${{ matrix.name }}
path: ./TestResults
if-no-files-found: ignore
build-linux:
runs-on: ubuntu-latest
container: ${{ matrix.container }}
# A hung run otherwise bills the full 360-minute default before GitHub kills it.
timeout-minutes: 30
container:
image: ${{ matrix.container }}
options: --security-opt seccomp=unconfined
name: Build (${{ matrix.name }})
strategy:
fail-fast: false
matrix:
include:
- container: ubuntu:26.04
name: Ubuntu 26
packageManager: apt
- container: ubuntu:noble
name: Ubuntu 24
packageManager: apt
- container: ubuntu:jammy
name: Ubuntu 22
packageManager: apt
- container: ubuntu:focal
name: Ubuntu 20
- container: debian:trixie
name: Debian 13
packageManager: apt
- container: debian:bookworm
name: Debian 12
packageManager: apt
- container: debian:bullseye
name: Debian 11
packageManager: apt
- container: fedora:39
name: Fedora 39
packageManager: dnf
- container: fedora:40
name: Fedora 40
- container: fedora:44
name: Fedora 44
packageManager: dnf
- container: quay.io/centos/centos:stream9
name: CentOS 9 Stream
packageManager: dnf
epel: true
- container: quay.io/centos/centos:stream10
name: CentOS 10 Stream
packageManager: dnf
epel: true
- container: almalinux:10
name: AlmaLinux 10
packageManager: dnf
epel: true
steps:
- name: Enable EPEL
run: dnf upgrade --refresh -y && dnf install -y epel-release epel-next-release
if: ${{ startsWith(matrix.name, 'CentOS') }}
# Enable CRB before EPEL, per the EPEL quickstart. epel-next is not installed:
# none of the prerequisites need it, EPEL 10 does not have it, and it is one more
# mirrorlist to fetch.
- name: Enable EPEL and CRB
run: |
dnf upgrade --refresh -y
dnf install -y dnf-plugins-core
dnf config-manager --set-enabled crb
dnf install -y epel-release
if: ${{ matrix.epel }}
# Runtime packages only, deliberately. Installing the -dev packages here would add the
# unversioned .so symlink and mask the very thing the binding packages now probe for, so a
# regression in versioned-SONAME resolution would sail through CI.
- name: Install Prerequisites using dnf
run: dnf makecache --refresh && dnf install -y findutils libicu libdeflate-devel zstd libargon2-devel
run: dnf makecache --refresh && dnf install -y findutils libicu libdeflate libargon2 tzdata
if: ${{ matrix.packageManager == 'dnf' }}
# ICU's runtime package carries the ABI version in its name (libicu70 on jammy, libicu76 on
# trixie) and has no stable alias, so match it by pattern. libicu-dev was the old way to stay
# version-independent, but it drags in the unversioned symlink and defeats the check below.
- name: Install Prerequisites using apt
run: apt-get update -y && apt-get install -y curl libicu-dev libdeflate-dev zstd libargon2-dev
run: apt-get update -y && apt-get install -y curl '^libicu[0-9]+$' libdeflate0 libargon2-1 tzdata
if: ${{ matrix.packageManager == 'apt' }}
- uses: actions/checkout@v4
# Versioned-SONAME resolution is only under test while the unversioned symlink is absent. If a
# base image or a package ever starts shipping it, every probe would succeed on the first try
# and a regression in the fallback would sail through CI, so fail loudly instead of silently
# testing nothing.
- name: Assert the unversioned .so symlinks are absent
run: |
found=""
for lib in libicuuc libicui18n libdeflate libargon2; do
hit=$(ls /usr/lib/*/"$lib".so /usr/lib64/"$lib".so 2>/dev/null || true)
if [ -n "$hit" ]; then
found="$found $hit"
fi
done
if [ -n "$found" ]; then
echo "::error::Unversioned symlinks present, so CI is no longer exercising versioned SONAME resolution:$found"
exit 1
fi
echo "No unversioned symlinks present; versioned SONAME resolution is under test."
- uses: actions/checkout@v7
with:
fetch-depth: 0 # avoid shallow clone so nbgv can do its work.
- name: Install .NET 9
uses: actions/setup-dotnet@v4
- name: Install .NET
uses: actions/setup-dotnet@v6
with:
global-json-file: global.json
- name: Build
run: ./publish.cmd Release
run: dotnet run --project Projects/BuildTool -- --config Release --skip-prereqs
- name: Test
run: dotnet test --no-restore
# blame-hang kills a stuck test host after 10 minutes and reports the in-flight
# tests plus a process dump instead of hanging until the job timeout.
run: |
dotnet test --logger trx --results-directory ./TestResults --blame-hang --blame-hang-timeout 10m --blame-hang-dump-type full
if [ -z "$(find ./TestResults -name '*.trx' 2>/dev/null)" ]; then
echo "::error::No test result files were produced - no test projects ran. Failing to avoid masking failures."
exit 1
fi
- name: Upload test results on failure
if: failure()
uses: actions/upload-artifact@v7
with:
name: TestResults-${{ matrix.name }}
path: ./TestResults
if-no-files-found: ignore

163
.github/workflows/build-tool-release.yml vendored Normal file
View file

@ -0,0 +1,163 @@
name: Build Tool Release
on:
push:
branches: [main]
paths:
- 'Projects/BuildTool/**'
workflow_dispatch:
jobs:
build:
runs-on: ${{ matrix.os }}
name: Build (${{ matrix.rid }})
strategy:
fail-fast: false
matrix:
include:
- os: windows-latest
rid: win-x64
artifact: build-tool-win-x64.exe
- os: windows-latest
rid: win-arm64
artifact: build-tool-win-arm64.exe
- os: macos-15
rid: osx-arm64
artifact: build-tool-osx-arm64
- os: ubuntu-latest
rid: linux-x64
artifact: build-tool-linux-x64
- os: ubuntu-24.04-arm
rid: linux-arm64
artifact: build-tool-linux-arm64
steps:
- uses: actions/checkout@v7
with:
fetch-depth: 0 # Full clone required for Nerdbank.GitVersioning
- name: Install .NET
uses: actions/setup-dotnet@v6
with:
global-json-file: global.json
- name: Publish NativeAOT
run: dotnet publish Projects/BuildTool/BuildTool.csproj -c Release -r ${{ matrix.rid }} -o publish/
- name: Rename artifact (Unix)
if: runner.os != 'Windows'
run: mv publish/build-tool publish/${{ matrix.artifact }}
- name: Rename artifact (Windows)
if: runner.os == 'Windows'
run: mv publish/build-tool.exe publish/${{ matrix.artifact }}
shell: bash
- name: Upload artifact
uses: actions/upload-artifact@v7
with:
name: ${{ matrix.artifact }}
path: publish/${{ matrix.artifact }}
sign:
needs: build
if: github.ref == 'refs/heads/main'
runs-on: ubuntu-latest
name: Sign (${{ matrix.artifact }})
permissions:
actions: read
contents: read
id-token: write
strategy:
matrix:
include:
- artifact: build-tool-win-x64.exe
- artifact: build-tool-win-arm64.exe
steps:
- name: Download unsigned artifact
uses: actions/download-artifact@v8
with:
name: ${{ matrix.artifact }}
path: unsigned/
- name: Upload for signing
id: upload-for-signing
uses: actions/upload-artifact@v7
with:
name: ${{ matrix.artifact }}-unsigned
path: unsigned/${{ matrix.artifact }}
- name: Submit signing request
id: submit
uses: signpath/github-action-submit-signing-request@v2
with:
api-token: '${{ secrets.SIGNPATH_API_TOKEN }}'
organization-id: '${{ secrets.SIGNPATH_ORGANIZATION_ID }}'
project-slug: 'modernuo'
signing-policy-slug: 'release-signing'
artifact-configuration-slug: 'build-tool'
github-artifact-id: '${{ steps.upload-for-signing.outputs.artifact-id }}'
wait-for-completion: true
output-artifact-directory: signed/
- name: Upload signed artifact
uses: actions/upload-artifact@v7
with:
name: ${{ matrix.artifact }}-signed
path: signed/${{ matrix.artifact }}
release:
needs: [build, sign]
runs-on: ubuntu-latest
if: github.ref == 'refs/heads/main'
permissions:
contents: write
steps:
- uses: actions/checkout@v7
- name: Move tag to current commit
run: |
git tag -f build-tool-latest
git push origin build-tool-latest --force
- name: Download signed Windows artifacts
uses: actions/download-artifact@v8
with:
pattern: '*-signed'
path: artifacts/
merge-multiple: true
- name: Download macOS artifacts
uses: actions/download-artifact@v8
with:
pattern: 'build-tool-osx-*'
path: artifacts/
merge-multiple: true
- name: Download Linux artifacts
uses: actions/download-artifact@v8
with:
pattern: 'build-tool-linux-*'
path: artifacts/
merge-multiple: true
- name: Generate checksums
run: |
cd artifacts
sha256sum build-tool-* > checksums-sha256.txt
- name: Create or update release
uses: softprops/action-gh-release@v3
with:
tag_name: build-tool-latest
name: Build Tool (Latest)
body: |
Latest NativeAOT-compiled build tool binaries.
These are automatically downloaded by `publish.cmd` / `publish.sh`.
prerelease: true
files: |
artifacts/build-tool-*
artifacts/checksums-sha256.txt
make_latest: false

View file

@ -1,24 +0,0 @@
name: Qodana
on:
workflow_dispatch:
pull_request:
push:
branches: # Specify your branches here
- main # The 'main' branch
jobs:
qodana:
runs-on: ubuntu-latest
permissions:
contents: write
pull-requests: write
checks: write
steps:
- uses: actions/checkout@v3
with:
ref: ${{ github.event.pull_request.head.sha }} # to check out the actual pull request commit, not the merge commit
fetch-depth: 0 # a full history is required for pull request analysis
- name: 'Qodana Scan'
uses: JetBrains/qodana-action@v2024.1
env:
QODANA_TOKEN: ${{ secrets.QODANA_TOKEN }}

View file

@ -10,27 +10,26 @@ jobs:
name: Create Release
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v7
with:
fetch-depth: 0 # avoid shallow clone so nbgv can do its work.
token: ${{ secrets.PERSONAL_ACCESS_TOKEN }}
- name: Install .NET 9
uses: actions/setup-dotnet@v4
- name: Install .NET
uses: actions/setup-dotnet@v6
with:
global-json-file: global.json
- name: Install NGBV
uses: dotnet/nbgv@master
- name: Compute version
uses: dotnet/nbgv@v0.5.2
id: nbgv
- name: Push git changes
uses: ad-m/github-push-action@master
with:
github_token: ${{ secrets.PERSONAL_ACCESS_TOKEN }}
tags: true
- name: Push version tag
run: |
git tag ${{ steps.nbgv.outputs.Version }}
git push origin ${{ steps.nbgv.outputs.Version }}
env:
GITHUB_TOKEN: ${{ secrets.PERSONAL_ACCESS_TOKEN }}
- name: Create Release
id: create_release
uses: softprops/action-gh-release@v2
uses: softprops/action-gh-release@v3
with:
body: ${{ steps.changelog.outputs.clean_changelog }}
tag_name: ${{ steps.nbgv.outputs.Version }}
name: ${{ steps.nbgv.outputs.Version }}
draft: false

View file

@ -9,12 +9,8 @@ jobs:
post-release-discord:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0 # avoid shallow clone so nbgv can do its work.
token: ${{ secrets.PERSONAL_ACCESS_TOKEN }}
- name: Post Release on Discord
uses: SethCohen/github-releases-to-discord@v1.15.0
uses: SethCohen/github-releases-to-discord@v1.20.0
with:
webhook_url: ${{ secrets.WEBHOOK_URL }}
username: "Release Changelog"

View file

@ -1,27 +0,0 @@
name: Updates Docs
on:
repository_dispatch:
types: [docs]
workflow_dispatch:
jobs:
update-docs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: 3.x
- run: pip install mkdocs-material
- run: mkdocs gh-deploy --force
- name: Extract commands web file
run: |
7z x ./docs/commands/commands.7z -o./docs/commands
rm ./docs/commands/commands.7z
- name: Deploy commands file
uses: JamesIves/github-pages-deploy-action@v4
with:
folder: ./docs/commands
clean: false
force: false

20
.gitignore vendored
View file

@ -9,16 +9,30 @@
/Distribution/bsdtar
/Distribution/Configuration/antimacro.json
/Distribution/Configuration/assistants.json
/Distribution/Configuration/auto-denylist.json
/Distribution/Configuration/bans.json
/Distribution/Configuration/blocklist.json
/Distribution/Configuration/crowdsec.json
/Distribution/Configuration/expansion.json
/Distribution/Configuration/firewall.json
/Distribution/Configuration/ip-allowlist*.txt
/Distribution/Configuration/ip-allowlist*.txt.tmp
/Distribution/Configuration/ip-blocklist.txt
/Distribution/Configuration/ip-blocklist.txt.tmp
/Distribution/Configuration/login-allowlist.json
/Distribution/Configuration/login-allowlist.txt
/Distribution/Configuration/login-allowlist.txt.tmp
/Distribution/Configuration/modernuo.json
/Distribution/Configuration/email-settings.json
/Distribution/Configuration/throttles.json
/Distribution/Configuration/tot.json
/Distribution/Data/Pathfinding
/Distribution/Logs
/Distribution/Archives
/Distribution/Backups
/Distribution/Saves
/Distribution/docs
/docs/
/Distribution/temp
/Distribution/*.dylib
/Distribution/*.so
@ -39,8 +53,14 @@
/.idea
/.vs
/.vscode
/.claude
.DS_Store
/packages/*
/Distribution/Configuration/server-access.json
# BuildTool native binaries (downloaded from GitHub Releases).
# Ignore everything under tools/ except the operator scripts checked in below.
/tools/*
!/tools/Export-IpBlocklist.ps1

1
AGENTS.md Normal file
View file

@ -0,0 +1 @@
Read and follow all instructions in CLAUDE.md in this repository's root.

117
CLAUDE.md Normal file
View file

@ -0,0 +1,117 @@
# ModernUO
.NET 10 Ultima Online server emulator. Single-threaded game loop. All game logic runs on one thread.
- **Server engine**: `Projects/Server/` — do NOT modify without explicit request
- **Game content**: `Projects/UOContent/` — primary editing target
- **Build**: `dotnet build` from repo root
## Code Audit Rules
Apply these when writing or reviewing `.cs` files under `Projects/`.
1. **LINQ** — Tier 1 (zero-cost patterns) free on hot paths; Tier 2 (low overhead) OK on warm paths; Tier 3 (allocating) forbidden on hot paths → `dev-docs/code-standards.md`
2. **No `Console.WriteLine`** — use `LogFactory.GetLogger(typeof(MyClass))``logger.Information(...)` (requires `using Server.Logging;`)
3. **Threading policy** — game logic runs only on the main loop; **never** touch game state (`World`, mobiles, items, maps, timers) from a background thread. Heavy work that *needs* game state must be **chunked** across ticks, not threaded. Heavy work that does *not* need game state (large-file parse, external I/O) **must** run on a background thread **and must yield to world saves** (defer while `World.Saving`/`WorldState.PendingSave`). Publish results back to the loop as an immutable snapshot swapped via a single `volatile` reference — the only sanctioned `volatile`. No `lock`/`Mutex`/`ConcurrentDictionary` in game logic. Rule #10 covers how background work hands results back to the loop → `dev-docs/threading-model.md`
4. **No `World.Mobiles`/`World.Items` iteration** — use spatial queries: `map.GetMobilesInRange<T>()`, `map.GetItemsInRange<T>()`
5. **Clean up refs in `OnDelete()`/`OnAfterDelete()`** — null out `Item`/`Mobile` references
6. **Cancel timers in `OnDelete()`/`OnAfterDelete()`** — call `_token.Cancel()` or `_timer?.Stop()`
7. **`STArrayPool<T>.Shared`** not `ArrayPool<T>.Shared` — single-threaded optimized, no locks
8. **`PooledRefList<T>`** not `new List<T>()` on hot paths — zero GC pressure, stack-allocated ref struct
9. **Serialization** — class must be `partial`, constructor needs `[Constructible]`, `TimerExecutionToken` must NOT have `[SerializableField]`. New classes: use `[SerializationGenerator(version)]` (omit `encoded`). Setters that coerce/veto/run side effects: use `[SerializableField]` args `allowFieldChange: nameof(BoolRefMethod)` / `fieldChanged: nameof(OldNewMethod)` — reserve `[SerializableProperty]` for custom getters. Serializable `Timer` members declare `[DeserializeTimer(nameof(Method))]` on the field (anchored by default — downtime preserves remaining delay; `wallClock: true` = absolute; method runs only when a timer was running at save). Conditional writes: `[SaveFlag(nameof(Should), nameof(Default))]` on the field. When bumping versions, add `MigrateFrom(VXContent)` (X = previous version). Never modify `Deserialize(reader, version)` for version bumps — that method is only for pre-codegen legacy saves. When migrating from pre-codegen Serialize/Deserialize: pass `false` if old code used `reader.ReadInt()`, bump version +1, and keep old logic as `private void Deserialize(IGenericReader reader, int version)``dev-docs/serialization.md`, `dev-docs/runuo-migration-docs/02-serialization.md`
10. **No `Task.Run`/`new Thread()` for game logic** (tandem with rule #3) — game logic is the single-threaded event loop. Backgrounding is allowed only for work that does not itself touch game state (external service calls, large-file parse). **Prove the need before adding a thread**: measure **on-loop** time, not wall-clock (frozen world is the cost, player latency is not), and gate on `Environment.ProcessorCount` — off-loading creates no CPU and buys nothing on 12 cores. New workers go in the vetted table in `dev-docs/threading-model.md` with their measurement. When such work must *feed* game logic: run the heavy/I/O part off-loop and `ConfigureAwait(false)` its awaits so a continuation never resumes on the loop and silently foregrounds heavy work; then hand the result back **explicitly** — publish an immutable snapshot swapped via a `volatile` reference (the loop reads it lock-free), or marshal the apply step with `Core.LoopContext.Post(() => …)`, re-validating in the continuation whatever may have changed while it ran. Never touch game state off-thread; never let the scheduler decide where the heavy work runs → `dev-docs/threading-model.md`
11. **Never assume era** — if code uses `Core.AOS`/`Core.SE`/etc., ask which expansion to target
12. **Naming**`_camelCase` private fields, `PascalCase` properties/methods/classes; don't flag legacy `m_` but use `_` for new code
13. **No empty gumps** — every gump must produce visual elements. An empty gump leaks on client+server (no way to close it). Use static `DisplayTo()` to validate before constructing → `dev-docs/gump-system.md`
14. **PropertyList string literals must be holes**`$"{"Map"}\t{value}"` not `$"Map\t{value}"`. The handler treats bare text as delimiters, `{}` holes as arguments. Only `\t` should be a bare literal → `dev-docs/property-lists.md`
15. **Braces required on all control flow**`if`, `else`, `for`, `foreach`, `while`, `do`, `switch` must always have braces, even for single-line bodies → `dev-docs/code-standards.md`
16. **Prefer switch expressions and switch-when** — use switch expressions for value mapping and switch-when for pattern matching where they improve readability. Exception: skip if unreadable or cold path → `dev-docs/code-standards.md`
17. **No `System.Text.StringBuilder`** — use `ValueStringBuilder` with `stackalloc` (bounded output) or `ValueStringBuilder.Create()` (unbounded). Supports `$"..."` interpolation directly. Always use `using var` for disposal. Use `Reset()` instead of reassigning → `dev-docs/string-handling.md`
18. **Interpolation anti-patterns on handler-aware APIs**`Send*`/`Say`/`Emote`/`PublicOverhead*`/`IPropertyList.Add`/gump `AddLabel`/`AddHtml`/`Html.Center`/`SpanWriter.Write*` all have `ref RawInterpolatedStringHandler` overloads that allocate zero strings, but only when the call-site argument is a `$"..."` literal directly. Avoid: ternaries with interpolated branches (`Send(c ? $"a" : $"b")`), switch expressions with interpolated arms, pre-built `var s = $"..."` locals (single-use), `.ToString()` / `.String()` / `string.Format` inside holes, string concat (`{a + b}`), LINQ string ops in holes. Use `:L` format spec for lowercase (`{rank:L}` not `rank.ToString().ToLowerInvariant()`) → `dev-docs/string-handling.md` § Interpolation Anti-Patterns
19. **No `InvalidateProperties()` from inside `GetProperties`** — every property a `GetProperties` override reads must be a pure read. `InvalidateProperties()` rebuilds the list in place (`Reset()` + rebuild), and `Reset()` returns the pooled interpolation buffer — which the compiler rents for the whole `$"..."` expression, so every hole is evaluated while it is live — and rewinds the packet cursor. A getter that invalidates therefore throws `ArgumentNullException` (parameter `"array"`) out of `GetProperties` from an unrelated-looking line, or silently corrupts the tooltip. The engine refuses and logs an error; `DEBUG` throws. Lazy recomputation in a getter is fine — the *notification* is not. Invalidate in the setter that changes the value, or defer with `Timer.DelayCall(InvalidateProperties)``dev-docs/property-lists.md` § Never Invalidate From Inside `GetProperties`
20. **Tick-count math must be wraparound-safe** — compare `Core.TickCount`/`GetTimestamp()` values only by subtraction (`a - b < 0`, never `a < b`), no zero/sign sentinels on tick fields, seed deadline fields from a real tick (never rely on the 0 default). Cloud hypervisors (GCP) pass through the host's never-resetting counter: ticks start enormous and can wrap negative. Linux affected in production; Windows not so far → `dev-docs/tick-counts.md`
## Dev-Docs Reference
| Topic | File |
|---|---|
| Code standards & LINQ tiers | `dev-docs/code-standards.md` |
| Serialization system | `dev-docs/serialization.md` |
| Content patterns (Items, Mobiles, Creatures) | `dev-docs/content-patterns.md` |
| Era & expansion handling | `dev-docs/era-expansion.md` |
| Timer system | `dev-docs/timers.md` |
| Event scheduler (wall-clock/calendar) | `dev-docs/event-scheduler.md` |
| Object property lists (tooltips) | `dev-docs/property-lists.md` |
| Gump (UI dialog) system | `dev-docs/gump-system.md` |
| Commands & targeting | `dev-docs/commands-targeting.md` |
| Event system | `dev-docs/events.md` |
| Threading model | `dev-docs/threading-model.md` |
| Server hardware requirements | `dev-docs/server-requirements.md` |
| Debugging event-loop performance (profiling build, decomposition, GC/RAM) | `dev-docs/debugging-event-loop.md` |
| Tick-count overflow rules (subtraction comparisons; GCP pass-through counters) | `dev-docs/tick-counts.md` |
| Server lifecycle & bootstrap phases (Configure/ConfigurePrompts/Initialize) | `dev-docs/server-lifecycle.md` |
| Platform prerequisites (ICU, tzdata, native libs per distro) | `dev-docs/platform-prerequisites.md` |
| Configuration system | `dev-docs/configuration.md` |
| Networking & packets | `dev-docs/networking-packets.md` |
| IP bans, blocklists & allowlists (incl. unblocking a player) | `dev-docs/ip-bans-and-allowlists.md` |
| Region system | `dev-docs/regions.md` |
| String handling & ValueStringBuilder | `dev-docs/string-handling.md` |
| RunUO migration (overview) | `dev-docs/runuo-migration-docs/00-overview.md` |
| RunUO migration (all docs) | `dev-docs/runuo-migration-docs/` |
## Claude Skills (Opt-In)
Detailed Claude Code skills live in `dev-docs/claude-skills/`. They are **not auto-loaded** — they must be copied to `.claude/skills/` to activate.
**When to offer**: If the user is building complex content (new items, creatures, spells, gumps, quests, packets, serialization work, etc.), ask:
> I have detailed Claude Code skills for this kind of work. Want me to enable them?
> I'll copy the relevant files from `dev-docs/claude-skills/` to `.claude/skills/`.
Then copy only the relevant skill files based on the task:
| Task | Skills to enable |
|---|---|
| New Item or Mobile | `modernuo-content-patterns`, `modernuo-serialization`, `modernuo-property-lists` |
| Creature / spawn | `modernuo-content-patterns`, `modernuo-serialization`, `modernuo-timers` |
| Spell or ability | `modernuo-content-patterns`, `modernuo-serialization`, `modernuo-timers`, `modernuo-era-expansion` |
| Gump / UI dialog | `modernuo-gump-system`, `modernuo-commands-targeting` |
| Quest or event system | `modernuo-events`, `modernuo-content-patterns`, `modernuo-configuration` |
| Scheduled / seasonal / holiday events | `modernuo-event-scheduler`, `modernuo-timers` |
| Custom regions / dynamic areas | `modernuo-regions`, `modernuo-content-patterns` |
| Packet / networking | `modernuo-networking`, `modernuo-threading` |
| Commands | `modernuo-commands-targeting` |
| Timer work | `modernuo-timers`, `modernuo-serialization` |
| Config system | `modernuo-configuration` |
| Era-conditional code | `modernuo-era-expansion` |
| String building / formatting | `modernuo-string-handling` |
| Code review / audit | `modernuo-code-audit` |
| Any `.cs` file edit | `modernuo-code-audit` (always offer for code changes) |
| **RunUO Migration** | |
| Migrate any RunUO script | `migrate-from-runuo/migrate-foundation` (always), plus system-specific skills below |
| Migrate Item/Mobile/Creature | `migrate-from-runuo/migrate-foundation`, `migrate-from-runuo/migrate-serialization`, `migrate-from-runuo/migrate-items-mobiles` |
| Migrate serialization | `migrate-from-runuo/migrate-serialization` |
| Migrate timers | `migrate-from-runuo/migrate-timers` |
| Migrate gumps | `migrate-from-runuo/migrate-gumps` |
| Migrate packets | `migrate-from-runuo/migrate-packets` |
| Migrate property lists | `migrate-from-runuo/migrate-property-lists` |
| Migrate events/commands | `migrate-from-runuo/migrate-commands-events` |
| Migrate persistence (WorldSave) | `migrate-from-runuo/migrate-persistence` |
| Migrate multi-file system | `migrate-from-runuo/migrate-systems` |
To enable a skill — Claude Code loads `.claude/skills/<name>/SKILL.md`; a bare `.md` dropped
directly into `.claude/skills/` is **not** picked up, and newly installed skills appear in the
*next* session:
```sh
# Standard skills (modernuo-*)
mkdir -p .claude/skills/<name> && cp dev-docs/claude-skills/<name>.md .claude/skills/<name>/SKILL.md
# Migration skills — sources live in the migrate-from-runuo/ subfolder, but install under the
# bare skill name (the table's "migrate-from-runuo/<name>" is the source path, not the name):
mkdir -p .claude/skills/<name> && cp dev-docs/claude-skills/migrate-from-runuo/<name>.md .claude/skills/<name>/SKILL.md
```
Migration skills reference the deep docs in `dev-docs/runuo-migration-docs/` and point to existing ModernUO skills for best practices.
The `modernuo-code-audit` skill auto-triggers on `.cs` file edits and flags convention violations (warnings only, asks before fixing).

View file

@ -3,9 +3,9 @@
<PropertyGroup>
<Authors>Kamron Batman</Authors>
<Company>ModernUO</Company>
<Copyright>2019-2023</Copyright>
<TargetFramework>net9.0</TargetFramework>
<LangVersion>12</LangVersion>
<Copyright>2019-2026</Copyright>
<TargetFramework>net10.0</TargetFramework>
<LangVersion>14</LangVersion>
<PublicRelease>true</PublicRelease>
<AutoGenerateBindingRedirects>true</AutoGenerateBindingRedirects>
<NoWarn>NU1603</NoWarn>
@ -31,10 +31,12 @@
<GitVersionBaseDirectory>$(SolutionDir)</GitVersionBaseDirectory>
<PredefinedCulturesOnly>false</PredefinedCulturesOnly>
</PropertyGroup>
<PropertyGroup>
<CopyLocalLockFileAssemblies>true</CopyLocalLockFileAssemblies>
</PropertyGroup>
<PropertyGroup Condition="'$(RuntimeIdentifier)'==''">
<DisableFastUpToDateCheck>true</DisableFastUpToDateCheck>
<SelfContained>false</SelfContained>
<CopyLocalLockFileAssemblies>true</CopyLocalLockFileAssemblies>
<InvariantGlobalization>false</InvariantGlobalization>
</PropertyGroup>
<PropertyGroup Condition="'$(IsWindows)'=='true' AND '$(IsX64)'=='true' AND '$(RuntimeIdentifier)'==''">
@ -61,12 +63,19 @@
<CodeAnalysisRuleSet>..\..\Rules.ruleset</CodeAnalysisRuleSet>
<AnalysisLevel>latest</AnalysisLevel>
</PropertyGroup>
<!-- Event-loop time accounting, compiled out unless requested:
dotnet build -p:EventLoopProfiling=true
See dev-docs/debugging-event-loop.md. Placed last so it appends to whatever the
configuration groups above set DefineConstants to. -->
<PropertyGroup Condition="'$(EventLoopProfiling)'=='true'">
<DefineConstants>$(DefineConstants);EVENT_LOOP_PROFILING</DefineConstants>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Serilog" Version="4.2.0" />
<PackageReference Include="Serilog" Version="4.4.0" />
<PackageReference Include="Serilog.Sinks.Async" Version="2.1.0" />
<PackageReference Include="Serilog.Sinks.Console" Version="6.0.0" />
<PackageReference Include="Serilog.Sinks.Console" Version="6.1.1" />
<PackageReference Include="Nerdbank.GitVersioning" Condition="!Exists('packages.config')">
<Version>3.7.112</Version>
<Version>3.10.91</Version>
<PrivateAssets>all</PrivateAssets>
</PackageReference>
<AdditionalFiles Include="..\..\Rules.ruleset" />

Binary file not shown.

View file

@ -0,0 +1,25 @@
# Bounty Boards — placed when murderSystem.bountiesEnabled is true
# 0x1E5E = east-facing, 0x1E5F = south-facing
BountyBoard 0x1E5E
2887 3482 17
2734 2184 0
2891 680 0
5668 3123 13
BountyBoard 0x1E5F
5278 3981 37
1442 1687 0
2512 551 0
600 2147 0
# Guards — one per board, standing in front
Spawner 0x1F13 (Spawn=WarriorGuard; Count=1; HomeRange=0; WalkingRange=5; Name=Bounty Board Guard Spawner)
2887 3487 17
2734 2189 0
2891 685 0
5668 3128 13
5283 3981 37
1447 1687 0
2517 551 0
605 2147 0

View file

@ -331,4 +331,13 @@ FlourMillSouthAddon 0x192C
# bellows
LargeForgeEastAddon 0x1986
3644 2619 0
3596 2601 0
3596 2601 0
# New Haven Mine Teleporters
# Rope(0x14FA) teleporter
InteractionTeleporter 0x14FA (Name=a rope leading down;PointDest=(5953, 319, 0))
5994 319 0
# Rope(0x14FA) teleporter
InteractionTeleporter 0x14FA (Name=a rope leading up;PointDest=(5994, 319, 0))
5953 319 0

Binary file not shown.

View file

@ -1,290 +0,0 @@
[
{
"type": "RegionSpawner",
"location": [375, 1191, 0],
"map": "Felucca",
"region": "A Wheatfield in Yew 2",
"count": 6,
"entries": [{ "maxCount": 6, "name": "FarmableWheat", "probability": 100 }],
"minDelay": "00:00:10",
"maxDelay": "00:00:30",
"guid": "bba64d97-11eb-4a87-b326-e41e3c9b355d",
"name": "Spawner"
},
{
"type": "RegionSpawner",
"location": [567, 1239, 0],
"map": "Felucca",
"region": "A Wheatfield in Yew 1",
"count": 5,
"entries": [{ "maxCount": 5, "name": "FarmableWheat", "probability": 100 }],
"minDelay": "00:00:10",
"maxDelay": "00:00:30",
"guid": "e37b4d74-c87e-4102-8c87-8f80c728fe0c",
"name": "Spawner"
},
{
"type": "RegionSpawner",
"location": [571, 1099, 0],
"map": "Felucca",
"region": "A Farm in Yew",
"count": 10,
"entries": [{ "maxCount": 10, "name": "Sheep", "probability": 100 }],
"minDelay": "00:00:10",
"maxDelay": "00:00:30",
"guid": "aed692cc-2dac-4c80-931b-35c73ca8d092",
"name": "Spawner"
},
{
"type": "RegionSpawner",
"location": [675, 939, 0],
"map": "Felucca",
"region": "A Field of Sheep in Yew 1",
"count": 10,
"entries": [{ "maxCount": 10, "name": "Sheep", "probability": 100 }],
"minDelay": "00:00:10",
"maxDelay": "00:00:30",
"guid": "425ac125-1597-41b4-9e6d-72b91801230b",
"name": "Spawner"
},
{
"type": "RegionSpawner",
"location": [675, 1179, 0],
"map": "Felucca",
"region": "A Field of Sheep in Yew 2",
"count": 10,
"entries": [{ "maxCount": 10, "name": "Sheep", "probability": 100 }],
"minDelay": "00:00:10",
"maxDelay": "00:00:30",
"guid": "3d7925bf-2642-4185-bc03-fa314fb75a7f",
"name": "Spawner"
},
{
"type": "RegionSpawner",
"location": [813, 2163, 0],
"map": "Felucca",
"region": "A Wheatfield in Skara Brae 1",
"count": 6,
"entries": [{ "maxCount": 6, "name": "FarmableWheat", "probability": 100 }],
"minDelay": "00:00:10",
"maxDelay": "00:00:30",
"guid": "57da6436-6b16-4697-add5-f4b45776ccbc",
"name": "Spawner"
},
{
"type": "RegionSpawner",
"location": [823, 2254, 0],
"map": "Felucca",
"region": "A Carrot Field in Skara Brae",
"count": 6,
"entries": [{ "maxCount": 6, "name": "FarmableCarrot", "probability": 100 }],
"minDelay": "00:00:10",
"maxDelay": "00:00:30",
"guid": "427451dd-93fa-43c9-85c6-01536066c8cc",
"name": "Spawner"
},
{
"type": "RegionSpawner",
"location": [823, 2264, 0],
"map": "Felucca",
"region": "An Onion Field in Skara Brae",
"count": 6,
"entries": [{ "maxCount": 6, "name": "FarmableOnion", "probability": 100 }],
"minDelay": "00:00:10",
"maxDelay": "00:00:30",
"guid": "640fe3f8-9917-4dfa-986d-038923163add",
"name": "Spawner"
},
{
"type": "RegionSpawner",
"location": [823, 2264, 0],
"map": "Felucca",
"region": "A Cabbage Field in Skara Brae 1",
"count": 6,
"entries": [{ "maxCount": 6, "name": "FarmableCabbage", "probability": 100 }],
"minDelay": "00:00:10",
"maxDelay": "00:00:30",
"guid": "f4bccdc2-c663-4ccd-b7df-31656fa2ae09",
"name": "Spawner"
},
{
"type": "RegionSpawner",
"location": [823, 2274, 0],
"map": "Felucca",
"region": "A Cabbage Field in Skara Brae 2",
"count": 6,
"entries": [{ "maxCount": 6, "name": "FarmableCabbage", "probability": 100 }],
"minDelay": "00:00:10",
"maxDelay": "00:00:30",
"guid": "0c48edf9-a88e-45f1-9ba3-cd8c919aefbc",
"name": "Spawner"
},
{
"type": "RegionSpawner",
"location": [823, 2355, 0],
"map": "Felucca",
"region": "A Cotton Field in Skara Brae",
"count": 6,
"entries": [{ "maxCount": 6, "name": "FarmableCotton", "probability": 100 }],
"minDelay": "00:00:10",
"maxDelay": "00:00:30",
"guid": "e7b34039-ec1f-43c8-b64f-d7462ce7ae9d",
"name": "Spawner"
},
{
"type": "RegionSpawner",
"location": [843, 2352, 0],
"map": "Felucca",
"region": "A Wheatfield in Skara Brae 2",
"count": 6,
"entries": [{ "maxCount": 6, "name": "FarmableWheat", "probability": 100 }],
"minDelay": "00:00:10",
"maxDelay": "00:00:30",
"guid": "2bf378f0-77ed-44cd-8dfb-181d068f401e",
"name": "Spawner"
},
{
"type": "RegionSpawner",
"location": [1120, 1624, 0],
"map": "Felucca",
"region": "A Wheatfield in Britain 4",
"count": 8,
"entries": [{ "maxCount": 8, "name": "FarmableWheat", "probability": 100 }],
"minDelay": "00:00:10",
"maxDelay": "00:00:30",
"guid": "e6a4aede-5a76-473d-8331-4bb73701ea1a",
"name": "Spawner"
},
{
"type": "RegionSpawner",
"location": [1135, 1791, 0],
"map": "Felucca",
"region": "A Wheatfield in Britain 1",
"count": 8,
"entries": [{ "maxCount": 8, "name": "FarmableWheat", "probability": 100 }],
"minDelay": "00:00:10",
"maxDelay": "00:00:30",
"guid": "b29b54ab-5ae3-4ce3-93ac-326f324a34f6",
"name": "Spawner"
},
{
"type": "RegionSpawner",
"location": [1152, 1576, 0],
"map": "Felucca",
"region": "A Wheatfield in Britain 5",
"count": 8,
"entries": [{ "maxCount": 8, "name": "FarmableWheat", "probability": 100 }],
"minDelay": "00:00:10",
"maxDelay": "00:00:30",
"guid": "e416e9fa-5a9a-426b-962e-50cdce64ca7f",
"name": "Spawner"
},
{
"type": "RegionSpawner",
"location": [1183, 1683, 0],
"map": "Felucca",
"region": "A Cabbage Field in Britain 1",
"count": 8,
"entries": [{ "maxCount": 8, "name": "FarmableCabbage", "probability": 100 }],
"minDelay": "00:00:10",
"maxDelay": "00:00:30",
"guid": "8536c19c-2036-495a-a3c5-e82c9f32f5ad",
"name": "Spawner"
},
{
"type": "RegionSpawner",
"location": [1199, 1683, 0],
"map": "Felucca",
"region": "A Turnip Field in Britain 1",
"count": 8,
"entries": [{ "maxCount": 8, "name": "FarmableTurnip", "probability": 100 }],
"minDelay": "00:00:10",
"maxDelay": "00:00:30",
"guid": "d8a2e54e-4951-468f-93c5-376460070cba",
"name": "Spawner"
},
{
"type": "RegionSpawner",
"location": [1199, 1823, 0],
"map": "Felucca",
"region": "A Wheatfield in Britain 2",
"count": 8,
"entries": [{ "maxCount": 8, "name": "FarmableWheat", "probability": 100 }],
"minDelay": "00:00:10",
"maxDelay": "00:00:30",
"guid": "58b14343-2032-4507-a913-6b536507c2e5",
"name": "Spawner"
},
{
"type": "RegionSpawner",
"location": [1215, 1723, 0],
"map": "Felucca",
"region": "A Carrot Field in Britain 1",
"count": 8,
"entries": [{ "maxCount": 8, "name": "FarmableCarrot", "probability": 100 }],
"minDelay": "00:00:10",
"maxDelay": "00:00:30",
"guid": "9805ef58-355f-4352-bfc4-43c6710ff409",
"name": "Spawner"
},
{
"type": "RegionSpawner",
"location": [1216, 1604, 0],
"map": "Felucca",
"region": "A Turnip Field in Britain 2",
"count": 6,
"entries": [{ "maxCount": 6, "name": "FarmableTurnip", "probability": 100 }],
"minDelay": "00:00:10",
"maxDelay": "00:00:30",
"guid": "0c6481ab-3341-4113-ac1a-6523c19c881d",
"name": "Spawner"
},
{
"type": "RegionSpawner",
"location": [1231, 1723, 0],
"map": "Felucca",
"region": "An Onion Field in Britain 1",
"count": 8,
"entries": [{ "maxCount": 8, "name": "FarmableOnion", "probability": 100 }],
"minDelay": "00:00:10",
"maxDelay": "00:00:30",
"guid": "57f955bb-49e1-4a66-a214-4fbddf8d572b",
"name": "Spawner"
},
{
"type": "RegionSpawner",
"location": [1231, 1887, 0],
"map": "Felucca",
"region": "A Wheatfield in Britain 3",
"count": 8,
"entries": [{ "maxCount": 8, "name": "FarmableWheat", "probability": 100 }],
"minDelay": "00:00:10",
"maxDelay": "00:00:30",
"guid": "f68b6bc8-f822-4c7a-9d01-37fad25bc95b",
"name": "Spawner"
},
{
"type": "RegionSpawner",
"location": [1232, 1604, 0],
"map": "Felucca",
"region": "A Carrot Field in Britain 2",
"count": 6,
"entries": [{ "maxCount": 6, "name": "FarmableCarrot", "probability": 100 }],
"minDelay": "00:00:10",
"maxDelay": "00:00:30",
"guid": "7b09d4b5-fe63-4f95-8525-3c970c513adc",
"name": "Spawner"
},
{
"type": "RegionSpawner",
"location": [4567, 1475, 0],
"map": "Felucca",
"region": "A Cotton Field in Moonglow",
"count": 8,
"entries": [{ "maxCount": 8, "name": "FarmableCotton", "probability": 100 }],
"minDelay": "00:00:10",
"maxDelay": "00:00:30",
"guid": "4b4e3973-d836-4f7c-b9f7-5fb7d468da28",
"name": "Spawner"
}
]

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

View file

@ -1,6 +1,6 @@
[
{
"type": "Spawner",
"$type": "Spawner",
"guid": "b37c3d38-2d53-4d2d-a8aa-a2e4926a29c6",
"name": "Spawner (402)",
"location": [1119, 530, -90],
@ -19,7 +19,7 @@
]
},
{
"type": "Spawner",
"$type": "Spawner",
"guid": "e1ba3ada-d1fd-44fb-83b8-82e0e1fcf94f",
"name": "Spawner (402)",
"location": [1674, 659, -84],
@ -40,7 +40,7 @@
]
},
{
"type": "Spawner",
"$type": "Spawner",
"guid": "099efd1a-520c-4e6b-bb39-552def69e2df",
"name": "Spawner (402)",
"location": [1434, 607, -89],
@ -61,7 +61,7 @@
]
},
{
"type": "Spawner",
"$type": "Spawner",
"guid": "c9c80a39-40db-4cb9-a3c7-243957ecdd39",
"name": "Spawner (402)",
"location": [1194, 638, -84],
@ -82,7 +82,7 @@
]
},
{
"type": "Spawner",
"$type": "Spawner",
"guid": "0335054c-d38b-4c5a-9fc6-c04c42ba9de1",
"name": "Spawner (402)",
"location": [920, 356, -85],
@ -101,348 +101,5 @@
{ "name": "GreatHart", "maxCount": 1, "probability": 100 },
{ "name": "JackRabbit", "maxCount": 4, "probability": 100 }
]
},
{
"type": "Spawner",
"guid": "6ef4d890-5038-4d39-977c-5eb15448543a",
"name": "Spawner (402)",
"location": [1659, 112, -80],
"map": "Malas",
"count": 7,
"minDelay": "00:05:00",
"maxDelay": "00:10:00",
"team": 0,
"homeRange": 20,
"walkingRange": 20,
"entries": [
{ "name": "Bird", "maxCount": 2, "probability": 100 },
{ "name": "Walrus", "maxCount": 1, "probability": 100 },
{ "name": "SnowLeopard", "maxCount": 1, "probability": 100 },
{ "name": "WhiteWolf", "maxCount": 1, "probability": 100 },
{ "name": "IceSnake", "maxCount": 1, "probability": 100 },
{ "name": "PolarBear", "maxCount": 1, "probability": 100 }
]
},
{
"type": "Spawner",
"guid": "a7be1139-619a-4d84-a98a-8ea04c550827",
"name": "Spawner (402)",
"location": [1869, 480, -50],
"map": "Malas",
"count": 7,
"minDelay": "00:05:00",
"maxDelay": "00:10:00",
"team": 0,
"homeRange": 40,
"walkingRange": 40,
"entries": [
{ "name": "Bird", "maxCount": 2, "probability": 100 },
{ "name": "Walrus", "maxCount": 1, "probability": 100 },
{ "name": "SnowLeopard", "maxCount": 1, "probability": 100 },
{ "name": "WhiteWolf", "maxCount": 1, "probability": 100 },
{ "name": "IceSnake", "maxCount": 1, "probability": 100 },
{ "name": "PolarBear", "maxCount": 1, "probability": 100 }
]
},
{
"type": "Spawner",
"guid": "41a269ac-0492-4a16-bbc3-449c7efa9ef9",
"name": "Spawner (402)",
"location": [2121, 597, -50],
"map": "Malas",
"count": 7,
"minDelay": "00:05:00",
"maxDelay": "00:10:00",
"team": 0,
"homeRange": 20,
"walkingRange": 20,
"entries": [
{ "name": "Bird", "maxCount": 2, "probability": 100 },
{ "name": "Walrus", "maxCount": 1, "probability": 100 },
{ "name": "SnowLeopard", "maxCount": 1, "probability": 100 },
{ "name": "WhiteWolf", "maxCount": 1, "probability": 100 },
{ "name": "IceSnake", "maxCount": 1, "probability": 100 },
{ "name": "PolarBear", "maxCount": 1, "probability": 100 }
]
},
{
"type": "Spawner",
"guid": "a1e3857a-952a-47c2-9311-d6bcc3806c45",
"name": "Spawner (402)",
"location": [1980, 660, -50],
"map": "Malas",
"count": 7,
"minDelay": "00:05:00",
"maxDelay": "00:10:00",
"team": 0,
"homeRange": 40,
"walkingRange": 40,
"entries": [
{ "name": "Bird", "maxCount": 2, "probability": 100 },
{ "name": "Walrus", "maxCount": 1, "probability": 100 },
{ "name": "SnowLeopard", "maxCount": 1, "probability": 100 },
{ "name": "WhiteWolf", "maxCount": 1, "probability": 100 },
{ "name": "IceSnake", "maxCount": 1, "probability": 100 },
{ "name": "PolarBear", "maxCount": 1, "probability": 100 }
]
},
{
"type": "Spawner",
"guid": "e2af6c55-d320-4255-a85f-c694f8d9886b",
"name": "Spawner (402)",
"location": [2233, 555, -90],
"map": "Malas",
"count": 7,
"minDelay": "00:05:00",
"maxDelay": "00:10:00",
"team": 0,
"homeRange": 60,
"walkingRange": 60,
"entries": [
{ "name": "Bird", "maxCount": 2, "probability": 100 },
{ "name": "Walrus", "maxCount": 1, "probability": 100 },
{ "name": "SnowLeopard", "maxCount": 1, "probability": 100 },
{ "name": "WhiteWolf", "maxCount": 1, "probability": 100 },
{ "name": "IceSnake", "maxCount": 1, "probability": 100 },
{ "name": "PolarBear", "maxCount": 1, "probability": 100 }
]
},
{
"type": "Spawner",
"guid": "b85a0fee-612e-4bd9-8489-28fbcdc60ef5",
"name": "Spawner (402)",
"location": [1137, 223, -50],
"map": "Malas",
"count": 7,
"minDelay": "00:05:00",
"maxDelay": "00:10:00",
"team": 0,
"homeRange": 20,
"walkingRange": 40,
"entries": [
{ "name": "Bird", "maxCount": 2, "probability": 100 },
{ "name": "Walrus", "maxCount": 1, "probability": 100 },
{ "name": "SnowLeopard", "maxCount": 1, "probability": 100 },
{ "name": "WhiteWolf", "maxCount": 1, "probability": 100 },
{ "name": "IceSnake", "maxCount": 1, "probability": 100 },
{ "name": "PolarBear", "maxCount": 1, "probability": 100 }
]
},
{
"type": "Spawner",
"guid": "b48ebb7a-8bfc-4c48-a0ab-f82ca41e38ac",
"name": "Spawner (402)",
"location": [1381, 340, -50],
"map": "Malas",
"count": 7,
"minDelay": "00:05:00",
"maxDelay": "00:10:00",
"team": 0,
"homeRange": 40,
"walkingRange": 40,
"entries": [
{ "name": "Bird", "maxCount": 2, "probability": 100 },
{ "name": "Walrus", "maxCount": 1, "probability": 100 },
{ "name": "SnowLeopard", "maxCount": 1, "probability": 100 },
{ "name": "WhiteWolf", "maxCount": 1, "probability": 100 },
{ "name": "IceSnake", "maxCount": 1, "probability": 100 },
{ "name": "PolarBear", "maxCount": 1, "probability": 100 }
]
},
{
"type": "Spawner",
"guid": "67b52f00-d808-4db4-91ca-9313330d0b40",
"name": "Spawner (402)",
"location": [1710, 403, -50],
"map": "Malas",
"count": 7,
"minDelay": "00:05:00",
"maxDelay": "00:10:00",
"team": 0,
"homeRange": 20,
"walkingRange": 40,
"entries": [
{ "name": "Bird", "maxCount": 2, "probability": 100 },
{ "name": "Walrus", "maxCount": 1, "probability": 100 },
{ "name": "SnowLeopard", "maxCount": 1, "probability": 100 },
{ "name": "WhiteWolf", "maxCount": 1, "probability": 100 },
{ "name": "IceSnake", "maxCount": 1, "probability": 100 },
{ "name": "PolarBear", "maxCount": 1, "probability": 100 }
]
},
{
"type": "Spawner",
"guid": "63d80004-20f7-48d8-bcc8-639a8d83c099",
"name": "Spawner (402)",
"location": [766, 114, -90],
"map": "Malas",
"count": 7,
"minDelay": "00:05:00",
"maxDelay": "00:10:00",
"team": 0,
"homeRange": 20,
"walkingRange": 20,
"entries": [
{ "name": "Bird", "maxCount": 2, "probability": 100 },
{ "name": "Walrus", "maxCount": 1, "probability": 100 },
{ "name": "SnowLeopard", "maxCount": 1, "probability": 100 },
{ "name": "WhiteWolf", "maxCount": 1, "probability": 100 },
{ "name": "IceSnake", "maxCount": 1, "probability": 100 },
{ "name": "PolarBear", "maxCount": 1, "probability": 100 }
]
},
{
"type": "Spawner",
"guid": "be2f76e8-a03b-42b9-b39c-c641f03ee091",
"name": "Spawner (402)",
"location": [1206, 191, -50],
"map": "Malas",
"count": 7,
"minDelay": "00:05:00",
"maxDelay": "00:10:00",
"team": 0,
"homeRange": 20,
"walkingRange": 40,
"entries": [
{ "name": "Bird", "maxCount": 2, "probability": 100 },
{ "name": "Walrus", "maxCount": 1, "probability": 100 },
{ "name": "SnowLeopard", "maxCount": 1, "probability": 100 },
{ "name": "WhiteWolf", "maxCount": 1, "probability": 100 },
{ "name": "IceSnake", "maxCount": 1, "probability": 100 },
{ "name": "PolarBear", "maxCount": 1, "probability": 100 }
]
},
{
"type": "Spawner",
"guid": "abaa8a2d-9997-4bff-b033-a9dfa78d46f9",
"name": "Spawner (402)",
"location": [2030, 500, -50],
"map": "Malas",
"count": 7,
"minDelay": "00:05:00",
"maxDelay": "00:10:00",
"team": 0,
"homeRange": 40,
"walkingRange": 70,
"entries": [
{ "name": "Bird", "maxCount": 2, "probability": 100 },
{ "name": "Walrus", "maxCount": 1, "probability": 100 },
{ "name": "SnowLeopard", "maxCount": 1, "probability": 100 },
{ "name": "WhiteWolf", "maxCount": 1, "probability": 100 },
{ "name": "IceSnake", "maxCount": 1, "probability": 100 },
{ "name": "PolarBear", "maxCount": 1, "probability": 100 }
]
},
{
"type": "Spawner",
"guid": "135a20a6-0885-4a61-9a55-a01593f3e874",
"name": "Spawner (402)",
"location": [1482, 347, -50],
"map": "Malas",
"count": 7,
"minDelay": "00:05:00",
"maxDelay": "00:10:00",
"team": 0,
"homeRange": 40,
"walkingRange": 60,
"entries": [
{ "name": "Bird", "maxCount": 2, "probability": 100 },
{ "name": "Walrus", "maxCount": 1, "probability": 100 },
{ "name": "SnowLeopard", "maxCount": 1, "probability": 100 },
{ "name": "WhiteWolf", "maxCount": 1, "probability": 100 },
{ "name": "IceSnake", "maxCount": 1, "probability": 100 },
{ "name": "PolarBear", "maxCount": 1, "probability": 100 }
]
},
{
"type": "Spawner",
"guid": "e3f940e1-3225-416f-a9ba-717b28fb944a",
"name": "Spawner (402)",
"location": [1023, 192, -50],
"map": "Malas",
"count": 7,
"minDelay": "00:05:00",
"maxDelay": "00:10:00",
"team": 0,
"homeRange": 20,
"walkingRange": 20,
"entries": [
{ "name": "Bird", "maxCount": 2, "probability": 100 },
{ "name": "Walrus", "maxCount": 1, "probability": 100 },
{ "name": "SnowLeopard", "maxCount": 1, "probability": 100 },
{ "name": "WhiteWolf", "maxCount": 1, "probability": 100 },
{ "name": "IceSnake", "maxCount": 1, "probability": 100 },
{ "name": "PolarBear", "maxCount": 1, "probability": 100 }
]
},
{
"type": "Spawner",
"guid": "2db6869c-b590-4f82-a5eb-edb7afdd18e4",
"name": "Spawner (402)",
"location": [1274, 300, -50],
"map": "Malas",
"count": 7,
"minDelay": "00:05:00",
"maxDelay": "00:10:00",
"team": 0,
"homeRange": 40,
"walkingRange": 40,
"entries": [
{ "name": "Bird", "maxCount": 2, "probability": 100 },
{ "name": "Walrus", "maxCount": 1, "probability": 100 },
{ "name": "SnowLeopard", "maxCount": 1, "probability": 100 },
{ "name": "WhiteWolf", "maxCount": 1, "probability": 100 },
{ "name": "IceSnake", "maxCount": 1, "probability": 100 },
{ "name": "PolarBear", "maxCount": 1, "probability": 100 }
]
},
{
"type": "Spawner",
"guid": "dec6f03f-a79d-414a-8b8b-c46af723371a",
"name": "Spawner (402)",
"location": [1587, 386, -50],
"map": "Malas",
"count": 7,
"minDelay": "00:05:00",
"maxDelay": "00:10:00",
"team": 0,
"homeRange": 40,
"walkingRange": 40,
"entries": [
{ "name": "Bird", "maxCount": 2, "probability": 100 },
{ "name": "Walrus", "maxCount": 1, "probability": 100 },
{ "name": "SnowLeopard", "maxCount": 1, "probability": 100 },
{ "name": "WhiteWolf", "maxCount": 1, "probability": 100 },
{ "name": "IceSnake", "maxCount": 1, "probability": 100 },
{ "name": "PolarBear", "maxCount": 1, "probability": 100 }
]
},
{
"type": "Spawner",
"guid": "0c628980-8a6f-45e1-bdc8-ed7ea16765fd",
"name": "Spawner (402)",
"location": [2205, 145, -90],
"map": "Malas",
"count": 3,
"minDelay": "00:05:00",
"maxDelay": "00:10:00",
"team": 0,
"homeRange": 50,
"walkingRange": 50,
"entries": [{ "name": "Unicorn", "maxCount": 3, "probability": 100 }]
},
{
"type": "Spawner",
"guid": "2efda65d-ebc3-4251-b6a3-356d42cca581",
"name": "Spawner (402)",
"location": [2291, 295, -90],
"map": "Malas",
"count": 6,
"minDelay": "00:05:00",
"maxDelay": "00:10:00",
"team": 0,
"homeRange": 60,
"walkingRange": 60,
"entries": [{ "name": "GoreFiend", "maxCount": 6, "probability": 100 }]
}
]

View file

@ -1,6 +1,6 @@
[
{
"type": "Spawner",
"$type": "Spawner",
"guid": "27bdb175-d7c0-49c9-b389-b26fe028e58a",
"name": "Spawner (404)",
"location": [1920, 1124, -90],
@ -23,7 +23,7 @@
]
},
{
"type": "Spawner",
"$type": "Spawner",
"guid": "56c3146a-f634-4ee8-9dd8-65e7fb98f8e2",
"name": "Spawner (404)",
"location": [1920, 1124, -90],
@ -52,7 +52,7 @@
]
},
{
"type": "Spawner",
"$type": "Spawner",
"guid": "5286c49e-21e5-407a-a363-8ecf2ff5eafe",
"name": "Spawner (404)",
"location": [2266, 1218, -85],
@ -74,7 +74,7 @@
]
},
{
"type": "Spawner",
"$type": "Spawner",
"guid": "695dcc16-4e4d-4b93-be25-05cf6ddf199c",
"name": "Spawner (404)",
"location": [2267, 1218, -85],
@ -103,7 +103,7 @@
]
},
{
"type": "Spawner",
"$type": "Spawner",
"guid": "bb4e40ab-96e4-4061-85e9-8011b42ed020",
"name": "Spawner (404)",
"location": [2303, 1258, -86],
@ -125,7 +125,7 @@
]
},
{
"type": "Spawner",
"$type": "Spawner",
"guid": "3e41feda-8300-4fa4-ba72-6752d37486cd",
"name": "Spawner (404)",
"location": [2304, 1258, -86],
@ -146,7 +146,7 @@
]
},
{
"type": "Spawner",
"$type": "Spawner",
"guid": "4b59baa2-7ce0-40a6-9c68-c6f2210f0869",
"name": "Spawner (404)",
"location": [1641, 1797, -110],
@ -165,7 +165,7 @@
]
},
{
"type": "Spawner",
"$type": "Spawner",
"guid": "9aa39353-19ab-480a-9e96-6a7ba1485251",
"name": "Spawner (404)",
"location": [1899, 1609, -110],
@ -184,7 +184,7 @@
]
},
{
"type": "Spawner",
"$type": "Spawner",
"guid": "6216b5d7-8c6f-420b-b615-54edf74cb173",
"name": "Spawner (404)",
"location": [1741, 1487, -110],
@ -203,7 +203,7 @@
]
},
{
"type": "Spawner",
"$type": "Spawner",
"guid": "3f641570-9b47-44a3-859a-e9ab15dd4da8",
"name": "Spawner (404)",
"location": [1837, 1803, -110],
@ -222,7 +222,7 @@
]
},
{
"type": "Spawner",
"$type": "Spawner",
"guid": "18a04411-aa44-46c4-9476-a77365090afa",
"name": "Spawner (404)",
"location": [1823, 1799, -90],
@ -231,7 +231,10 @@
"minDelay": "00:05:00",
"maxDelay": "00:10:00",
"team": 0,
"homeRange": 4,
"spawnBounds": {
"start": { "x": 1819, "y": 1795, "z": -90 },
"end": { "x": 1827, "y": 1803, "z": -75 }
},
"walkingRange": 15,
"entries": [
{ "name": "SkeletalKnight", "maxCount": 2, "probability": 100 },
@ -243,7 +246,7 @@
]
},
{
"type": "Spawner",
"$type": "Spawner",
"guid": "4a2b88ce-b146-439d-917c-3f056775012e",
"name": "Spawner (404)",
"location": [1821, 1798, -110],
@ -252,7 +255,10 @@
"minDelay": "00:05:00",
"maxDelay": "00:10:00",
"team": 0,
"homeRange": 4,
"spawnBounds": {
"start": { "x": 1817, "y": 1794, "z": -110 },
"end": { "x": 1825, "y": 1802, "z": -95 }
},
"walkingRange": 15,
"entries": [
{ "name": "GiantRat", "maxCount": 4, "probability": 100 },
@ -262,27 +268,7 @@
]
},
{
"type": "Spawner",
"guid": "6bbb82e4-b0d1-4a23-a9da-5d35911ce762",
"name": "Spawner (404)",
"location": [2231, 1605, -95],
"map": "Malas",
"count": 33,
"minDelay": "00:05:00",
"maxDelay": "00:10:00",
"team": 0,
"homeRange": 160,
"walkingRange": 160,
"entries": [
{ "name": "Mongbat", "maxCount": 7, "probability": 100 },
{ "name": "SandVortex", "maxCount": 6, "probability": 100 },
{ "name": "Scorpion", "maxCount": 7, "probability": 100 },
{ "name": "Snake", "maxCount": 6, "probability": 100 },
{ "name": "Spectre", "maxCount": 7, "probability": 100 }
]
},
{
"type": "Spawner",
"$type": "Spawner",
"guid": "d91c089c-551a-4333-adbb-4ff82e82908f",
"name": "Spawner (404)",
"location": [1466, 1418, -90],
@ -302,7 +288,7 @@
]
},
{
"type": "Spawner",
"$type": "Spawner",
"guid": "01e68ebc-8521-44bd-ada3-a96cf6dc7b8e",
"name": "Spawner (404)",
"location": [1477, 1241, -85],
@ -322,7 +308,7 @@
]
},
{
"type": "Spawner",
"$type": "Spawner",
"guid": "7a14417b-5b9c-4c46-978d-4f9aaf4ab2d8",
"name": "Spawner (404)",
"location": [1347, 1739, -110],
@ -342,7 +328,7 @@
]
},
{
"type": "Spawner",
"$type": "Spawner",
"guid": "c5036af7-93ad-41b3-a29b-48b1e60393fe",
"name": "Spawner (404)",
"location": [1327, 1146, -90],
@ -361,7 +347,7 @@
]
},
{
"type": "Spawner",
"$type": "Spawner",
"guid": "6d49d6f6-4054-4b02-a319-8be526aaefcf",
"name": "Spawner (404)",
"location": [1256, 1221, -90],
@ -380,7 +366,7 @@
]
},
{
"type": "Spawner",
"$type": "Spawner",
"guid": "aa9e1655-d9ed-4fdc-99c9-ef04a9234b9b",
"name": "Spawner (404)",
"location": [1345, 1314, -85],
@ -399,7 +385,7 @@
]
},
{
"type": "Spawner",
"$type": "Spawner",
"guid": "fad094bd-915f-49d1-a0c6-486029d25506",
"name": "Spawner (404)",
"location": [943, 1150, -90],
@ -410,10 +396,12 @@
"team": 0,
"homeRange": 10,
"walkingRange": 20,
"entries": [{ "name": "Brigand", "maxCount": 6, "probability": 100 }]
"entries": [
{ "name": "Brigand", "maxCount": 6, "probability": 100 }
]
},
{
"type": "Spawner",
"$type": "Spawner",
"guid": "c2cdce00-aaf5-40cc-b1f5-28c0d1364eff",
"name": "Spawner (404)",
"location": [900, 1494, -90],
@ -432,7 +420,7 @@
]
},
{
"type": "Spawner",
"$type": "Spawner",
"guid": "c5657dd4-3bbc-4557-a5bb-9a111c7e6763",
"name": "Spawner (404)",
"location": [829, 1474, -85],
@ -451,7 +439,7 @@
]
},
{
"type": "Spawner",
"$type": "Spawner",
"guid": "eb36fa8a-c49d-42b2-ac5a-246391bdb716",
"name": "Spawner (404)",
"location": [989, 1419, -82],
@ -470,7 +458,7 @@
]
},
{
"type": "Spawner",
"$type": "Spawner",
"guid": "e9f2158f-8b22-46cd-94ce-924a22b6dd65",
"name": "Spawner (404)",
"location": [913, 1335, -90],
@ -489,7 +477,7 @@
]
},
{
"type": "Spawner",
"$type": "Spawner",
"guid": "20e95aca-18d5-4862-a0cd-72e65f188dc4",
"name": "Spawner (404)",
"location": [881, 927, -90],
@ -500,10 +488,12 @@
"team": 0,
"homeRange": 15,
"walkingRange": 30,
"entries": [{ "name": "Pixie", "maxCount": 6, "probability": 100 }]
"entries": [
{ "name": "Pixie", "maxCount": 6, "probability": 100 }
]
},
{
"type": "Spawner",
"$type": "Spawner",
"guid": "51a28e73-74fd-454b-8c02-5aeacd7f041b",
"name": "Spawner (404)",
"location": [882, 1000, -90],
@ -514,10 +504,12 @@
"team": 0,
"homeRange": 15,
"walkingRange": 40,
"entries": [{ "name": "Pixie", "maxCount": 6, "probability": 100 }]
"entries": [
{ "name": "Pixie", "maxCount": 6, "probability": 100 }
]
},
{
"type": "Spawner",
"$type": "Spawner",
"guid": "83ded31b-46a4-4d33-b340-abb593f01ea9",
"name": "Spawner (404)",
"location": [989, 1420, -82],
@ -528,10 +520,12 @@
"team": 0,
"homeRange": 15,
"walkingRange": 40,
"entries": [{ "name": "Pixie", "maxCount": 6, "probability": 100 }]
"entries": [
{ "name": "Pixie", "maxCount": 6, "probability": 100 }
]
},
{
"type": "Spawner",
"$type": "Spawner",
"guid": "0109ed70-dc93-4c76-9647-ef3565315a1b",
"name": "Spawner (404)",
"location": [1455, 1445, -90],
@ -542,10 +536,12 @@
"team": 0,
"homeRange": 10,
"walkingRange": 30,
"entries": [{ "name": "Pixie", "maxCount": 6, "probability": 100 }]
"entries": [
{ "name": "Pixie", "maxCount": 6, "probability": 100 }
]
},
{
"type": "Spawner",
"$type": "Spawner",
"guid": "c3fa83a1-72e9-440b-9ddf-d8733e54e59f",
"name": "Spawner (404)",
"location": [1429, 1549, -110],
@ -556,10 +552,12 @@
"team": 0,
"homeRange": 15,
"walkingRange": 25,
"entries": [{ "name": "Pixie", "maxCount": 6, "probability": 100 }]
"entries": [
{ "name": "Pixie", "maxCount": 6, "probability": 100 }
]
},
{
"type": "Spawner",
"$type": "Spawner",
"guid": "f18bf6f0-7dde-43f7-81f5-ac26393e48c0",
"name": "Spawner (404)",
"location": [1305, 1095, -90],
@ -570,10 +568,12 @@
"team": 0,
"homeRange": 15,
"walkingRange": 30,
"entries": [{ "name": "Pixie", "maxCount": 6, "probability": 100 }]
"entries": [
{ "name": "Pixie", "maxCount": 6, "probability": 100 }
]
},
{
"type": "Spawner",
"$type": "Spawner",
"guid": "b49f301b-9378-489a-b999-51785a3307e9",
"name": "Spawner (404)",
"location": [790, 948, -90],
@ -592,7 +592,7 @@
]
},
{
"type": "Spawner",
"$type": "Spawner",
"guid": "74e793f9-ab88-4c51-b713-203fe3ec3828",
"name": "Spawner (404)",
"location": [766, 1364, -90],
@ -611,7 +611,7 @@
]
},
{
"type": "Spawner",
"$type": "Spawner",
"guid": "d5aa4b2a-02bd-4fab-ad78-6e0a2cd7c575",
"name": "Spawner (404)",
"location": [656, 1431, -90],
@ -630,7 +630,7 @@
]
},
{
"type": "Spawner",
"$type": "Spawner",
"guid": "ff590783-b876-429c-aaad-e6096a86ea7e",
"name": "Spawner (404)",
"location": [805, 1232, -90],
@ -649,7 +649,7 @@
]
},
{
"type": "Spawner",
"$type": "Spawner",
"guid": "eeaacf1f-a83f-4ba0-86f3-1f39aff6a3ec",
"name": "Spawner (404)",
"location": [672, 1237, -90],
@ -668,7 +668,7 @@
]
},
{
"type": "Spawner",
"$type": "Spawner",
"guid": "d0fe5086-d802-43d1-b62f-a81dbc603ca1",
"name": "Spawner (404)",
"location": [684, 1109, -90],
@ -687,7 +687,7 @@
]
},
{
"type": "Spawner",
"$type": "Spawner",
"guid": "c30d468f-b35f-4aa5-a13f-773518810151",
"name": "Spawner (404)",
"location": [1079, 1440, -90],
@ -706,7 +706,7 @@
]
},
{
"type": "Spawner",
"$type": "Spawner",
"guid": "cf2df3ea-792b-4d9e-9723-b094b5654dcf",
"name": "Spawner (404)",
"location": [966, 1213, -90],
@ -725,7 +725,7 @@
]
},
{
"type": "Spawner",
"$type": "Spawner",
"guid": "6651b803-1285-427b-93a5-c127957915bc",
"name": "Spawner (404)",
"location": [925, 1105, -90],
@ -744,7 +744,7 @@
]
},
{
"type": "Spawner",
"$type": "Spawner",
"guid": "bfcc0baf-f539-440f-96b6-911679afc732",
"name": "Spawner (404)",
"location": [1049, 1332, -90],
@ -763,7 +763,7 @@
]
},
{
"type": "Spawner",
"$type": "Spawner",
"guid": "445e307b-72d4-4d2e-9284-14938e4dab15",
"name": "Spawner (404)",
"location": [1086, 1067, -90],
@ -782,7 +782,7 @@
]
},
{
"type": "Spawner",
"$type": "Spawner",
"guid": "375e8bd1-6823-442a-a125-2e2acf53edd9",
"name": "Spawner (404)",
"location": [1062, 988, -90],

View file

@ -1,470 +1,6 @@
[
{
"type": "Spawner",
"guid": "99cde945-5b9c-4cff-924f-a59142bcbdb9",
"name": "Spawner (405)",
"location": [989, 520, -50],
"map": "Malas",
"count": 2,
"minDelay": "00:05:00",
"maxDelay": "00:10:00",
"team": 0,
"homeRange": 0,
"walkingRange": 5,
"entries": [
{ "name": "Banker", "maxCount": 1, "probability": 100 },
{ "name": "Minter", "maxCount": 1, "probability": 100 }
]
},
{
"type": "Spawner",
"guid": "7bd799ab-c7fb-446e-b6cb-580d1fe2f7fb",
"name": "Spawner (405)",
"location": [976, 512, -50],
"map": "Malas",
"count": 2,
"minDelay": "00:05:00",
"maxDelay": "00:10:00",
"team": 0,
"homeRange": 0,
"walkingRange": 5,
"entries": [
{ "name": "Blacksmith", "maxCount": 1, "probability": 100 },
{ "name": "BlacksmithGuildmaster", "maxCount": 1, "probability": 100 }
]
},
{
"type": "Spawner",
"guid": "f62df843-74a7-4bb0-a101-53d1dc4cb806",
"name": "Spawner (405)",
"location": [976, 527, -50],
"map": "Malas",
"count": 5,
"minDelay": "00:05:00",
"maxDelay": "00:10:00",
"team": 0,
"homeRange": 0,
"walkingRange": 5,
"entries": [
{ "name": "Tailor", "maxCount": 1, "probability": 100 },
{ "name": "Weaver", "maxCount": 1, "probability": 100 },
{ "name": "TailorGuildmaster", "maxCount": 1, "probability": 100 },
{ "name": "Tanner", "maxCount": 1, "probability": 100 },
{ "name": "Furtrader", "maxCount": 1, "probability": 100 }
]
},
{
"type": "Spawner",
"guid": "79aa68b8-d2bd-4a4d-ad84-422588aeaaff",
"name": "Spawner (405)",
"location": [1016, 514, -70],
"map": "Malas",
"count": 1,
"minDelay": "00:05:00",
"maxDelay": "00:10:00",
"team": 0,
"homeRange": 0,
"walkingRange": 5,
"entries": [{ "name": "KeeperOfChivalry", "maxCount": 1, "probability": 100 }]
},
{
"type": "Spawner",
"guid": "e08eb38c-0b25-4387-8999-f9d8a0a877aa",
"name": "Spawner (405)",
"location": [961, 517, -70],
"map": "Malas",
"count": 1,
"minDelay": "00:05:00",
"maxDelay": "00:10:00",
"team": 0,
"homeRange": 0,
"walkingRange": 5,
"entries": [{ "name": "KeeperOfChivalry", "maxCount": 1, "probability": 100 }]
},
{
"type": "Spawner",
"guid": "a4200c02-8035-48bc-b79d-8b45225ffd4f",
"name": "Spawner (405)",
"location": [1004, 527, -50],
"map": "Malas",
"count": 4,
"minDelay": "00:05:00",
"maxDelay": "00:10:00",
"team": 0,
"homeRange": 0,
"walkingRange": 5,
"entries": [
{ "name": "HolyMage", "maxCount": 1, "probability": 100 },
{ "name": "Herbalist", "maxCount": 1, "probability": 100 },
{ "name": "Alchemist", "maxCount": 1, "probability": 100 },
{ "name": "CustomHairstylist", "maxCount": 1, "probability": 100 }
]
},
{
"type": "Spawner",
"guid": "c1bda852-fee4-45ee-aa79-630980bc44e9",
"name": "Spawner (405)",
"location": [1003, 512, -50],
"map": "Malas",
"count": 5,
"minDelay": "00:05:00",
"maxDelay": "00:10:00",
"team": 0,
"homeRange": 0,
"walkingRange": 5,
"entries": [
{ "name": "Tinker", "maxCount": 1, "probability": 100 },
{ "name": "TinkerGuildmaster", "maxCount": 1, "probability": 100 },
{ "name": "Carpenter", "maxCount": 1, "probability": 100 },
{ "name": "Architect", "maxCount": 1, "probability": 100 },
{ "name": "RealEstateBroker", "maxCount": 1, "probability": 100 }
]
},
{
"type": "Spawner",
"guid": "df531ac9-d2b0-48ae-bc19-dc043d6af3de",
"name": "Spawner (405)",
"location": [993, 511, -50],
"map": "Malas",
"count": 3,
"minDelay": "00:05:00",
"maxDelay": "00:10:00",
"team": 0,
"homeRange": 0,
"walkingRange": 5,
"entries": [
{ "name": "Provisioner", "maxCount": 1, "probability": 100 },
{ "name": "Cobbler", "maxCount": 1, "probability": 100 },
{ "name": "Jeweler", "maxCount": 1, "probability": 100 }
]
},
{
"type": "Spawner",
"guid": "54deea77-bc1e-446b-8274-ea23bddb369e",
"name": "Spawner (405)",
"location": [989, 527, -50],
"map": "Malas",
"count": 1,
"minDelay": "00:05:00",
"maxDelay": "00:10:00",
"team": 0,
"homeRange": 0,
"walkingRange": 5,
"entries": [{ "name": "InnKeeper", "maxCount": 1, "probability": 100 }]
},
{
"type": "Spawner",
"guid": "7a10d380-53e3-48c9-9c5b-07a3c215032a",
"name": "Spawner (405)",
"location": [1027, 494, -70],
"map": "Malas",
"count": 1,
"minDelay": "00:05:00",
"maxDelay": "00:10:00",
"team": 0,
"homeRange": 0,
"walkingRange": 5,
"entries": [{ "name": "AnimalTrainer", "maxCount": 1, "probability": 100 }]
},
{
"type": "Spawner",
"guid": "3d301c77-1ade-4823-b2ff-f1d4e0ed2bf0",
"name": "Spawner (405)",
"location": [1029, 520, -55],
"map": "Malas",
"count": 2,
"minDelay": "00:05:00",
"maxDelay": "00:10:00",
"team": 0,
"homeRange": 0,
"walkingRange": 5,
"entries": [
{ "name": "Healer", "maxCount": 1, "probability": 100 },
{ "name": "HealerGuildmaster", "maxCount": 1, "probability": 100 }
]
},
{
"type": "Spawner",
"guid": "0d9c6323-7da9-4e12-861a-76fd30b3e3dc",
"name": "Spawner (405)",
"location": [950, 520, -55],
"map": "Malas",
"count": 2,
"minDelay": "00:05:00",
"maxDelay": "00:10:00",
"team": 0,
"homeRange": 0,
"walkingRange": 5,
"entries": [
{ "name": "Healer", "maxCount": 1, "probability": 100 },
{ "name": "HealerGuildmaster", "maxCount": 1, "probability": 100 }
]
},
{
"type": "Spawner",
"guid": "55fab330-6d2a-4823-a666-3dfa33dceeb6",
"name": "Spawner (405)",
"location": [1977, 1365, -80],
"map": "Malas",
"count": 2,
"minDelay": "00:05:00",
"maxDelay": "00:10:00",
"team": 0,
"homeRange": 0,
"walkingRange": 5,
"entries": [
{ "name": "Blacksmith", "maxCount": 1, "probability": 100 },
{ "name": "BlacksmithGuildmaster", "maxCount": 1, "probability": 100 }
]
},
{
"type": "Spawner",
"guid": "81e4af4e-3310-4864-8517-a032d487716f",
"name": "Spawner (405)",
"location": [1992, 1315, -90],
"map": "Malas",
"count": 1,
"minDelay": "00:05:00",
"maxDelay": "00:10:00",
"team": 0,
"homeRange": 0,
"walkingRange": 5,
"entries": [{ "name": "AnimalTrainer", "maxCount": 1, "probability": 100 }]
},
{
"type": "Spawner",
"guid": "9ad9f47f-17bc-4790-9a84-1e9aee66478f",
"name": "Spawner (405)",
"location": [2011, 1326, -80],
"map": "Malas",
"count": 2,
"minDelay": "00:05:00",
"maxDelay": "00:10:00",
"team": 0,
"homeRange": 0,
"walkingRange": 5,
"entries": [
{ "name": "Provisioner", "maxCount": 1, "probability": 100 },
{ "name": "Cobbler", "maxCount": 1, "probability": 100 }
]
},
{
"type": "Spawner",
"guid": "fa9dbd50-e0ca-4206-a197-0fe85a349900",
"name": "Spawner (405)",
"location": [2017, 1356, -90],
"map": "Malas",
"count": 1,
"minDelay": "00:05:00",
"maxDelay": "00:10:00",
"team": 0,
"homeRange": 0,
"walkingRange": 5,
"entries": [{ "name": "Baker", "maxCount": 1, "probability": 100 }]
},
{
"type": "Spawner",
"guid": "b4f71d7e-8c76-47db-a1c1-b81c4484741b",
"name": "Spawner (405)",
"location": [2027, 1353, -90],
"map": "Malas",
"count": 4,
"minDelay": "00:05:00",
"maxDelay": "00:10:00",
"team": 0,
"homeRange": 0,
"walkingRange": 5,
"entries": [
{ "name": "TavernKeeper", "maxCount": 1, "probability": 100 },
{ "name": "Waiter", "maxCount": 1, "probability": 100 },
{ "name": "Cook", "maxCount": 1, "probability": 100 },
{ "name": "Barkeeper", "maxCount": 1, "probability": 100 }
]
},
{
"type": "Spawner",
"guid": "3dcbbc92-14ac-4798-8753-41aa3b6b2450",
"name": "Spawner (405)",
"location": [2023, 1379, -80],
"map": "Malas",
"count": 3,
"minDelay": "00:05:00",
"maxDelay": "00:10:00",
"team": 0,
"homeRange": 0,
"walkingRange": 5,
"entries": [
{ "name": "Mage", "maxCount": 1, "probability": 100 },
{ "name": "Alchemist", "maxCount": 1, "probability": 100 },
{ "name": "MageGuildmaster", "maxCount": 1, "probability": 100 }
]
},
{
"type": "Spawner",
"guid": "a0c83d13-2354-494f-9535-d4e587cc7a6b",
"name": "Spawner (405)",
"location": [2025, 1387, -80],
"map": "Malas",
"count": 3,
"minDelay": "00:05:00",
"maxDelay": "00:10:00",
"team": 0,
"homeRange": 0,
"walkingRange": 5,
"entries": [
{ "name": "Herbalist", "maxCount": 1, "probability": 100 },
{ "name": "Alchemist", "maxCount": 1, "probability": 100 },
{ "name": "CustomHairstylist", "maxCount": 1, "probability": 100 }
]
},
{
"type": "Spawner",
"guid": "3d69c950-70d4-48c5-8729-be8848330c1a",
"name": "Spawner (405)",
"location": [2045, 1397, -90],
"map": "Malas",
"count": 1,
"minDelay": "00:05:00",
"maxDelay": "00:10:00",
"team": 0,
"homeRange": 0,
"walkingRange": 5,
"entries": [{ "name": "Jeweler", "maxCount": 1, "probability": 100 }]
},
{
"type": "Spawner",
"guid": "a263ccfc-65e1-4181-93b6-6e1c7914f5b8",
"name": "Spawner (405)",
"location": [2048, 1343, -85],
"map": "Malas",
"count": 2,
"minDelay": "00:05:00",
"maxDelay": "00:10:00",
"team": 0,
"homeRange": 0,
"walkingRange": 5,
"entries": [
{ "name": "Banker", "maxCount": 1, "probability": 100 },
{ "name": "Minter", "maxCount": 1, "probability": 100 }
]
},
{
"type": "Spawner",
"guid": "9347985d-23a4-4044-9a5c-79939c9f7df0",
"name": "Spawner (405)",
"location": [2037, 1311, -85],
"map": "Malas",
"count": 1,
"minDelay": "00:05:00",
"maxDelay": "00:10:00",
"team": 0,
"homeRange": 0,
"walkingRange": 5,
"entries": [{ "name": "InnKeeper", "maxCount": 1, "probability": 100 }]
},
{
"type": "Spawner",
"guid": "eb50b13f-dd98-42c1-bfa0-2599bdc85de3",
"name": "Spawner (405)",
"location": [2066, 1282, -80],
"map": "Malas",
"count": 2,
"minDelay": "00:05:00",
"maxDelay": "00:10:00",
"team": 0,
"homeRange": 0,
"walkingRange": 5,
"entries": [
{ "name": "Tinker", "maxCount": 1, "probability": 100 },
{ "name": "TinkerGuildmaster", "maxCount": 1, "probability": 100 }
]
},
{
"type": "Spawner",
"guid": "8ed338dc-51c1-40b3-a91a-7ebd8d91131b",
"name": "Spawner (405)",
"location": [2060, 1283, -80],
"map": "Malas",
"count": 3,
"minDelay": "00:05:00",
"maxDelay": "00:10:00",
"team": 0,
"homeRange": 0,
"walkingRange": 5,
"entries": [
{ "name": "Carpenter", "maxCount": 1, "probability": 100 },
{ "name": "Architect", "maxCount": 1, "probability": 100 },
{ "name": "RealEstateBroker", "maxCount": 1, "probability": 100 }
]
},
{
"type": "Spawner",
"guid": "d6de9f30-1ee9-4914-853c-be075bd119ce",
"name": "Spawner (405)",
"location": [2083, 1322, -80],
"map": "Malas",
"count": 3,
"minDelay": "00:05:00",
"maxDelay": "00:10:00",
"team": 0,
"homeRange": 0,
"walkingRange": 5,
"entries": [
{ "name": "Tailor", "maxCount": 1, "probability": 100 },
{ "name": "Weaver", "maxCount": 1, "probability": 100 },
{ "name": "TailorGuildmaster", "maxCount": 1, "probability": 100 }
]
},
{
"type": "Spawner",
"guid": "72a7f47d-d4cb-4530-9ba6-3878482279b2",
"name": "Spawner (405)",
"location": [2078, 1327, -80],
"map": "Malas",
"count": 2,
"minDelay": "00:05:00",
"maxDelay": "00:10:00",
"team": 0,
"homeRange": 0,
"walkingRange": 5,
"entries": [
{ "name": "Tanner", "maxCount": 1, "probability": 100 },
{ "name": "Furtrader", "maxCount": 1, "probability": 100 }
]
},
{
"type": "Spawner",
"guid": "0db7d5e9-beaa-43a2-ac46-2802ac962fcb",
"name": "Spawner (405)",
"location": [2068, 1372, -75],
"map": "Malas",
"count": 2,
"minDelay": "00:05:00",
"maxDelay": "00:10:00",
"team": 0,
"homeRange": 0,
"walkingRange": 5,
"entries": [
{ "name": "Healer", "maxCount": 1, "probability": 100 },
{ "name": "HealerGuildmaster", "maxCount": 1, "probability": 100 }
]
},
{
"type": "Spawner",
"guid": "b7226aea-9fd0-4079-8e21-70a7a443d0b2",
"name": "Spawner (405)",
"location": [1056, 1434, -85],
"map": "Malas",
"count": 1,
"minDelay": "00:05:00",
"maxDelay": "00:10:00",
"team": 0,
"homeRange": 0,
"walkingRange": 5,
"entries": [{ "name": "InnKeeper", "maxCount": 1, "probability": 100 }]
},
{
"type": "Spawner",
"$type": "Spawner",
"guid": "befc0201-8bec-432e-8ced-3ee5ceb57d09",
"name": "Spawner (405)",
"location": [1050, 1434, -85],
@ -482,7 +18,7 @@
]
},
{
"type": "Spawner",
"$type": "Spawner",
"guid": "d8e03ce8-cd76-433e-af76-0c0767290705",
"name": "Spawner (405)",
"location": [1042, 1444, -90],

View file

@ -1,6 +1,6 @@
[
{
"type": "Spawner",
"$type": "Spawner",
"guid": "35fb2ebe-b2e6-4a64-af3a-ce1eaacd9a54",
"name": "Spawner (601)",
"location": [795, 753, 53],
@ -11,10 +11,12 @@
"team": 0,
"homeRange": 15,
"walkingRange": 15,
"entries": [{ "name": "Hydra", "maxCount": 8, "probability": 100 }]
"entries": [
{ "name": "Hydra", "maxCount": 8, "probability": 100 }
]
},
{
"type": "Spawner",
"$type": "Spawner",
"guid": "8d0bc27a-c900-4d7f-8811-a52f386b6d0c",
"name": "Spawner (601)",
"location": [745, 739, 16],
@ -25,10 +27,12 @@
"team": 0,
"homeRange": 5,
"walkingRange": 5,
"entries": [{ "name": "MaddeningHorror", "maxCount": 1, "probability": 100 }]
"entries": [
{ "name": "MaddeningHorror", "maxCount": 1, "probability": 100 }
]
},
{
"type": "Spawner",
"$type": "Spawner",
"guid": "f7e9ab99-b970-4be1-881b-9e7ef1af0d01",
"name": "Spawner (601)",
"location": [749, 475, -17],
@ -39,10 +43,12 @@
"team": 0,
"homeRange": 20,
"walkingRange": 20,
"entries": [{ "name": "SlasherOfVeils", "maxCount": 1, "probability": 100 }]
"entries": [
{ "name": "SlasherOfVeils", "maxCount": 1, "probability": 100 }
]
},
{
"type": "Spawner",
"$type": "Spawner",
"guid": "dd5aea6e-c13b-40f4-949f-9f41eac96589",
"name": "Spawner (601)",
"location": [326, 159, 20],
@ -53,10 +59,12 @@
"team": 0,
"homeRange": 10,
"walkingRange": 10,
"entries": [{ "name": "StygianDragon", "maxCount": 1, "probability": 100 }]
"entries": [
{ "name": "StygianDragon", "maxCount": 1, "probability": 100 }
]
},
{
"type": "Spawner",
"$type": "Spawner",
"guid": "5c81b759-1add-4e8a-b151-420aa778e236",
"name": "Spawner (601)",
"location": [716, 720, -11],
@ -76,7 +84,7 @@
]
},
{
"type": "Spawner",
"$type": "Spawner",
"guid": "a02bf178-9eb0-48ff-ae18-d06958f798a8",
"name": "Spawner (601)",
"location": [992, 337, 9],
@ -96,7 +104,7 @@
]
},
{
"type": "Spawner",
"$type": "Spawner",
"guid": "3929df23-08d5-4650-89a1-6cb025fddbac",
"name": "Spawner (601)",
"location": [982, 491, -12],
@ -114,7 +122,7 @@
]
},
{
"type": "Spawner",
"$type": "Spawner",
"guid": "941cbed7-bf9a-448d-ae6a-f741459f50d1",
"name": "Spawner (601)",
"location": [981, 491, -12],
@ -125,10 +133,12 @@
"team": 0,
"homeRange": 18,
"walkingRange": 18,
"entries": [{ "name": "ClockworkScorpion", "maxCount": 16, "probability": 100 }]
"entries": [
{ "name": "ClockworkScorpion", "maxCount": 16, "probability": 100 }
]
},
{
"type": "Spawner",
"$type": "Spawner",
"guid": "517bfdd3-f625-4e1c-af4b-8467810e6e46",
"name": "Spawner (601)",
"location": [919, 502, -12],
@ -146,7 +156,7 @@
]
},
{
"type": "Spawner",
"$type": "Spawner",
"guid": "592811d2-8196-4006-a774-5658b4df3ecb",
"name": "Spawner (601)",
"location": [918, 502, -12],
@ -157,10 +167,12 @@
"team": 0,
"homeRange": 20,
"walkingRange": 20,
"entries": [{ "name": "ClanRibbonPlagueRat", "maxCount": 15, "probability": 100 }]
"entries": [
{ "name": "ClanRibbonPlagueRat", "maxCount": 15, "probability": 100 }
]
},
{
"type": "Spawner",
"$type": "Spawner",
"guid": "8ff19011-9cc1-4aaf-a795-1867c6667123",
"name": "Spawner (601)",
"location": [949, 555, -14],
@ -178,7 +190,7 @@
]
},
{
"type": "Spawner",
"$type": "Spawner",
"guid": "1b0b5739-68fc-4048-b2be-10da4dee13b8",
"name": "Spawner (601)",
"location": [950, 555, -14],
@ -189,10 +201,12 @@
"team": 0,
"homeRange": 20,
"walkingRange": 20,
"entries": [{ "name": "ClanScratchSavageWolf", "maxCount": 18, "probability": 100 }]
"entries": [
{ "name": "ClanScratchSavageWolf", "maxCount": 18, "probability": 100 }
]
},
{
"type": "Spawner",
"$type": "Spawner",
"guid": "c33dadb8-e340-4fc6-9944-627054cf96f6",
"name": "Spawner (601)",
"location": [974, 161, -11],
@ -215,7 +229,7 @@
]
},
{
"type": "Spawner",
"$type": "Spawner",
"guid": "dcc3ba35-0e14-48fe-8a1b-8a7e5b3e4c03",
"name": "Spawner (601)",
"location": [581, 815, -45],
@ -238,7 +252,7 @@
]
},
{
"type": "Spawner",
"$type": "Spawner",
"guid": "6dfe8446-6623-4133-8990-6d11813b2b0d",
"name": "Spawner (601)",
"location": [526, 766, -92],
@ -262,7 +276,7 @@
]
},
{
"type": "Spawner",
"$type": "Spawner",
"guid": "6d71c735-e0cb-414d-b687-10c1a4192856",
"name": "Spawner (601)",
"location": [536, 657, 8],
@ -284,7 +298,7 @@
]
},
{
"type": "Spawner",
"$type": "Spawner",
"guid": "29d4421f-924d-47c3-abe5-45fe5e1be9a5",
"name": "Spawner (601)",
"location": [601, 905, -60],
@ -307,7 +321,7 @@
]
},
{
"type": "Spawner",
"$type": "Spawner",
"guid": "7dac51b3-7c0a-4f22-8652-7e29ba875fee",
"name": "Spawner (601)",
"location": [818, 927, -15],
@ -318,10 +332,12 @@
"team": 0,
"homeRange": 15,
"walkingRange": 15,
"entries": [{ "name": "Medusa", "maxCount": 1, "probability": 100 }]
"entries": [
{ "name": "Medusa", "maxCount": 1, "probability": 100 }
]
},
{
"type": "Spawner",
"$type": "Spawner",
"guid": "c64053fc-272a-4fdd-9c6c-4f05a1ec02be",
"name": "Spawner (601)",
"location": [684, 579, -15],
@ -341,7 +357,7 @@
]
},
{
"type": "Spawner",
"$type": "Spawner",
"guid": "a72aa352-96b2-4163-83c0-de180a8fb7e8",
"name": "Spawner (601)",
"location": [440, 708, 24],
@ -360,7 +376,7 @@
]
},
{
"type": "Spawner",
"$type": "Spawner",
"guid": "875d5be5-9156-4e42-a41a-973aef764aee",
"name": "Spawner (601)",
"location": [674, 828, -109],
@ -383,7 +399,7 @@
]
},
{
"type": "Spawner",
"$type": "Spawner",
"guid": "11551b29-0ee5-4fae-8d9b-2fd60465b032",
"name": "Spawner (601)",
"location": [887, 277, 3],

View file

@ -1,6 +1,6 @@
[
{
"type": "Spawner",
"$type": "Spawner",
"guid": "910e07be-fd79-4608-9990-5b860c14f95f",
"name": "Spawner (602)",
"location": [1109, 3744, -34],
@ -17,7 +17,7 @@
]
},
{
"type": "Spawner",
"$type": "Spawner",
"guid": "e93d2519-a992-406d-84db-c246d4c9e68c",
"name": "Spawner (602)",
"location": [1088, 3732, -43],
@ -28,10 +28,12 @@
"team": 0,
"homeRange": 5,
"walkingRange": 5,
"entries": [{ "name": "TrapdoorSpider", "maxCount": 2, "probability": 100 }]
"entries": [
{ "name": "TrapdoorSpider", "maxCount": 2, "probability": 100 }
]
},
{
"type": "Spawner",
"$type": "Spawner",
"guid": "5de9c1d3-274e-418f-8d1f-01357fa82fa4",
"name": "Spawner (602)",
"location": [1126, 3714, -43],
@ -42,10 +44,12 @@
"team": 0,
"homeRange": 5,
"walkingRange": 5,
"entries": [{ "name": "TrapdoorSpider", "maxCount": 2, "probability": 100 }]
"entries": [
{ "name": "TrapdoorSpider", "maxCount": 2, "probability": 100 }
]
},
{
"type": "Spawner",
"$type": "Spawner",
"guid": "88ee3368-50a1-458c-9c99-8fb7ceddf079",
"name": "Spawner (602)",
"location": [1134, 3483, -42],
@ -67,7 +71,7 @@
]
},
{
"type": "Spawner",
"$type": "Spawner",
"guid": "dd1b50db-06cc-4ab8-b1ae-7eb6d7ebeb03",
"name": "Spawner (602)",
"location": [1118, 3416, -42],
@ -88,7 +92,7 @@
]
},
{
"type": "Spawner",
"$type": "Spawner",
"guid": "17ea295b-36bd-43e1-aa63-81d61bb7db98",
"name": "Spawner (602)",
"location": [1112, 3624, -45],
@ -105,7 +109,7 @@
]
},
{
"type": "Spawner",
"$type": "Spawner",
"guid": "38fa4d5b-ab23-455a-9b32-3cfae24dacf1",
"name": "Spawner (602)",
"location": [1036, 3670, 0],
@ -122,7 +126,7 @@
]
},
{
"type": "Spawner",
"$type": "Spawner",
"guid": "08c6d785-3e69-4343-a48e-54e876e09863",
"name": "Spawner (602)",
"location": [1012, 3652, -30],
@ -139,7 +143,7 @@
]
},
{
"type": "Spawner",
"$type": "Spawner",
"guid": "ae84b446-e1a6-4df6-84b7-487218c2ffc0",
"name": "Spawner (602)",
"location": [996, 3406, -43],
@ -164,7 +168,7 @@
]
},
{
"type": "Spawner",
"$type": "Spawner",
"guid": "f6d1d4f5-6a84-430b-bbef-8b248d6b7c34",
"name": "Spawner (602)",
"location": [851, 3642, -43],
@ -189,7 +193,7 @@
]
},
{
"type": "Spawner",
"$type": "Spawner",
"guid": "1faffedd-40e4-4dcf-b944-418dbd7f1300",
"name": "Spawner (602)",
"location": [934, 3564, -43],
@ -214,7 +218,7 @@
]
},
{
"type": "Spawner",
"$type": "Spawner",
"guid": "f76c2cff-e0c9-4b37-a7f8-9463975f34ac",
"name": "Spawner (602)",
"location": [965, 3489, -43],
@ -239,7 +243,7 @@
]
},
{
"type": "Spawner",
"$type": "Spawner",
"guid": "56b271ca-0fcc-4ea4-9c2d-7b5aeec0d532",
"name": "Spawner (602)",
"location": [858, 3532, -43],
@ -256,7 +260,7 @@
]
},
{
"type": "Spawner",
"$type": "Spawner",
"guid": "3b6298d3-c9f3-40df-af03-20af1ed36ad5",
"name": "Spawner (602)",
"location": [981, 3318, -42],
@ -273,7 +277,7 @@
]
},
{
"type": "Spawner",
"$type": "Spawner",
"guid": "869aeec5-52eb-4041-ab43-5d943d05055f",
"name": "Spawner (602)",
"location": [881, 3344, -43],
@ -290,7 +294,7 @@
]
},
{
"type": "Spawner",
"$type": "Spawner",
"guid": "46785472-bc5c-4563-8a5b-2ea1c0f9f465",
"name": "Spawner (602)",
"location": [1073, 3322, -42],
@ -313,7 +317,7 @@
]
},
{
"type": "Spawner",
"$type": "Spawner",
"guid": "5f076796-65bf-4d41-88f3-78dacfab04b8",
"name": "Spawner (602)",
"location": [981, 3574, -43],
@ -330,7 +334,7 @@
]
},
{
"type": "Spawner",
"$type": "Spawner",
"guid": "55507c03-3ae8-43ba-a2c5-7e8ecd7a3457",
"name": "Spawner (602)",
"location": [909, 3235, 38],
@ -353,7 +357,7 @@
]
},
{
"type": "Spawner",
"$type": "Spawner",
"guid": "6d62246c-b5a5-4a2a-8f8a-3a9d54387e8a",
"name": "Spawner (602)",
"location": [802, 3231, 40],
@ -376,7 +380,7 @@
]
},
{
"type": "Spawner",
"$type": "Spawner",
"guid": "572149ff-9542-42c1-92b9-99b71d8f2579",
"name": "Spawner (602)",
"location": [679, 3248, 38],
@ -396,7 +400,7 @@
]
},
{
"type": "Spawner",
"$type": "Spawner",
"guid": "c05a8e56-f166-414e-babc-dcc1d14221c4",
"name": "Spawner (602)",
"location": [588, 3267, 37],
@ -419,7 +423,7 @@
]
},
{
"type": "Spawner",
"$type": "Spawner",
"guid": "1a1cad72-ebdf-4491-a623-66d87bc45538",
"name": "Spawner (602)",
"location": [521, 3355, 38],
@ -440,7 +444,7 @@
]
},
{
"type": "Spawner",
"$type": "Spawner",
"guid": "e23908dc-858c-4ec1-a41a-474fd7e9730d",
"name": "Spawner (602)",
"location": [511, 3273, 36],
@ -458,7 +462,7 @@
]
},
{
"type": "Spawner",
"$type": "Spawner",
"guid": "955417f2-d918-4c43-93a6-346ca6fb4bbc",
"name": "Spawner (602)",
"location": [547, 3421, 37],
@ -479,7 +483,7 @@
]
},
{
"type": "Spawner",
"$type": "Spawner",
"guid": "ebbbc910-1d0f-4180-8e7a-544d819906b8",
"name": "Spawner (602)",
"location": [571, 3506, 38],
@ -497,7 +501,7 @@
]
},
{
"type": "Spawner",
"$type": "Spawner",
"guid": "153e658a-818b-4ed3-80d0-8b5f99193b6e",
"name": "Spawner (602)",
"location": [1169, 3338, -42],
@ -519,7 +523,7 @@
]
},
{
"type": "Spawner",
"$type": "Spawner",
"guid": "18cef512-871f-4ff0-b21d-54e22e6197bc",
"name": "Spawner (602)",
"location": [689, 3711, -43],
@ -539,7 +543,7 @@
]
},
{
"type": "Spawner",
"$type": "Spawner",
"guid": "8745bf7c-2b8b-45ae-b992-7de9c58d14fc",
"name": "Spawner (602)",
"location": [761, 3650, -43],
@ -559,7 +563,7 @@
]
},
{
"type": "Spawner",
"$type": "Spawner",
"guid": "97c2ed7b-69c7-43db-8ccb-8591ecd90316",
"name": "Spawner (602)",
"location": [616, 3684, -43],
@ -577,7 +581,7 @@
]
},
{
"type": "Spawner",
"$type": "Spawner",
"guid": "990f3875-b9e4-4060-8e14-7f2c4b9a69a2",
"name": "Spawner (602)",
"location": [668, 3831, -31],
@ -596,7 +600,7 @@
]
},
{
"type": "Spawner",
"$type": "Spawner",
"guid": "0b5d810e-182e-4d7c-9a79-2dfb3ba8e5f3",
"name": "Spawner (602)",
"location": [680, 3788, -43],
@ -607,10 +611,12 @@
"team": 0,
"homeRange": 5,
"walkingRange": 5,
"entries": [{ "name": "TrapdoorSpider", "maxCount": 2, "probability": 100 }]
"entries": [
{ "name": "TrapdoorSpider", "maxCount": 2, "probability": 100 }
]
},
{
"type": "Spawner",
"$type": "Spawner",
"guid": "8151255d-cede-4c52-bacc-fc3a1a434c45",
"name": "Spawner (602)",
"location": [760, 3810, -42],
@ -631,7 +637,7 @@
]
},
{
"type": "Spawner",
"$type": "Spawner",
"guid": "1017634b-fad7-41ee-a13c-ec15dc0e5ae9",
"name": "Spawner (602)",
"location": [826, 3762, -43],
@ -653,7 +659,7 @@
]
},
{
"type": "Spawner",
"$type": "Spawner",
"guid": "e2817ac3-8c77-4901-af5f-d51f0ceb7f7d",
"name": "Spawner (602)",
"location": [795, 3827, -19],
@ -664,10 +670,12 @@
"team": 0,
"homeRange": 5,
"walkingRange": 5,
"entries": [{ "name": "TrapdoorSpider", "maxCount": 2, "probability": 100 }]
"entries": [
{ "name": "TrapdoorSpider", "maxCount": 2, "probability": 100 }
]
},
{
"type": "Spawner",
"$type": "Spawner",
"guid": "54228f45-e935-4df1-8d50-e15ec557d110",
"name": "Spawner (602)",
"location": [799, 3863, 48],
@ -689,7 +697,7 @@
]
},
{
"type": "Spawner",
"$type": "Spawner",
"guid": "91a9c0fa-51f3-4726-aeb1-25a03911fffa",
"name": "Spawner (602)",
"location": [763, 3991, -42],
@ -710,7 +718,7 @@
]
},
{
"type": "Spawner",
"$type": "Spawner",
"guid": "866bde45-dd02-4eef-b6c4-143bf3b6c31a",
"name": "Spawner (602)",
"location": [1129, 3163, -42],
@ -730,7 +738,7 @@
]
},
{
"type": "Spawner",
"$type": "Spawner",
"guid": "08ce21d5-67cc-46c7-a827-68048f38e3ef",
"name": "Spawner (602)",
"location": [687, 3917, -43],
@ -741,10 +749,12 @@
"team": 0,
"homeRange": 5,
"walkingRange": 5,
"entries": [{ "name": "TrapdoorSpider", "maxCount": 2, "probability": 100 }]
"entries": [
{ "name": "TrapdoorSpider", "maxCount": 2, "probability": 100 }
]
},
{
"type": "Spawner",
"$type": "Spawner",
"guid": "63140eed-b7f0-4ca9-87f5-5caeff3b6607",
"name": "Spawner (602)",
"location": [641, 3922, -43],
@ -765,7 +775,7 @@
]
},
{
"type": "Spawner",
"$type": "Spawner",
"guid": "5216666c-6c47-4d72-9818-510a88ea6edc",
"name": "Spawner (602)",
"location": [1065, 2996, 74],
@ -785,7 +795,7 @@
]
},
{
"type": "Spawner",
"$type": "Spawner",
"guid": "089d3262-1c31-4f80-aa13-d2d5383f0535",
"name": "Spawner (602)",
"location": [1099, 3010, 50],
@ -796,10 +806,12 @@
"team": 0,
"homeRange": 3,
"walkingRange": 3,
"entries": [{ "name": "ToxicSlith", "maxCount": 1, "probability": 100 }]
"entries": [
{ "name": "ToxicSlith", "maxCount": 1, "probability": 100 }
]
},
{
"type": "Spawner",
"$type": "Spawner",
"guid": "c1db00c8-9759-4f55-8ac4-f7557a6b9242",
"name": "Spawner (602)",
"location": [1053, 2971, 50],
@ -810,10 +822,12 @@
"team": 0,
"homeRange": 3,
"walkingRange": 3,
"entries": [{ "name": "ToxicSlith", "maxCount": 2, "probability": 100 }]
"entries": [
{ "name": "ToxicSlith", "maxCount": 2, "probability": 100 }
]
},
{
"type": "Spawner",
"$type": "Spawner",
"guid": "a7437a00-ed2e-4acc-9375-b234ceffd92a",
"name": "Spawner (602)",
"location": [1062, 3026, 50],
@ -824,10 +838,12 @@
"team": 0,
"homeRange": 3,
"walkingRange": 3,
"entries": [{ "name": "ToxicSlith", "maxCount": 1, "probability": 100 }]
"entries": [
{ "name": "ToxicSlith", "maxCount": 1, "probability": 100 }
]
},
{
"type": "Spawner",
"$type": "Spawner",
"guid": "d0e61272-a938-4b64-8713-3bac2356f1c7",
"name": "Spawner (602)",
"location": [762, 3073, 105],
@ -850,7 +866,7 @@
]
},
{
"type": "Spawner",
"$type": "Spawner",
"guid": "27b77304-78cf-43bd-9ea3-076c491369a4",
"name": "Spawner (602)",
"location": [917, 3088, 37],
@ -873,7 +889,7 @@
]
},
{
"type": "Spawner",
"$type": "Spawner",
"guid": "150f4f85-b244-48a4-a46f-d5b3c56951c6",
"name": "Spawner (602)",
"location": [839, 3086, 83],
@ -896,7 +912,7 @@
]
},
{
"type": "Spawner",
"$type": "Spawner",
"guid": "9f18de9c-4862-4940-a2b2-ea02d78c2a69",
"name": "Spawner (602)",
"location": [817, 2960, 76],
@ -916,7 +932,7 @@
]
},
{
"type": "Spawner",
"$type": "Spawner",
"guid": "4b7fcf89-dc47-4a59-8da2-bda0f810beeb",
"name": "Spawner (602)",
"location": [1106, 3138, -43],
@ -933,7 +949,7 @@
]
},
{
"type": "Spawner",
"$type": "Spawner",
"guid": "743c46bb-a95f-4a7b-8a7f-4c4c0473fe38",
"name": "Spawner (602)",
"location": [861, 2923, 38],
@ -953,7 +969,7 @@
]
},
{
"type": "Spawner",
"$type": "Spawner",
"guid": "de473023-c4ad-43f6-b9bd-b84d281234e8",
"name": "Spawner (602)",
"location": [648, 3131, 38],
@ -975,7 +991,7 @@
]
},
{
"type": "Spawner",
"$type": "Spawner",
"guid": "7ad064f2-fb54-47c6-ba74-7d36d9425ea6",
"name": "Spawner (602)",
"location": [622, 3045, 35],
@ -992,7 +1008,7 @@
]
},
{
"type": "Spawner",
"$type": "Spawner",
"guid": "10d3d73f-7f91-4eed-bd56-fc5805653892",
"name": "Spawner (602)",
"location": [685, 2901, 36],
@ -1011,7 +1027,7 @@
]
},
{
"type": "Spawner",
"$type": "Spawner",
"guid": "f1c797af-4863-4b66-8169-795330712f08",
"name": "Spawner (602)",
"location": [562, 2942, 36],
@ -1030,7 +1046,7 @@
]
},
{
"type": "Spawner",
"$type": "Spawner",
"guid": "fc314f2e-f697-4a0b-8bb0-f7dd51c1ed2a",
"name": "Spawner (602)",
"location": [486, 3211, 38],
@ -1048,7 +1064,7 @@
]
},
{
"type": "Spawner",
"$type": "Spawner",
"guid": "4182343a-a4c0-4bc5-b07a-2e71cb1d3d97",
"name": "Spawner (602)",
"location": [426, 3190, 38],
@ -1068,7 +1084,7 @@
]
},
{
"type": "Spawner",
"$type": "Spawner",
"guid": "a340eb44-bdd5-4ae8-a316-570d801cc0d3",
"name": "Spawner (602)",
"location": [354, 3283, 57],
@ -1085,7 +1101,7 @@
]
},
{
"type": "Spawner",
"$type": "Spawner",
"guid": "f5ea0039-e107-461c-8091-21561b83f1d8",
"name": "Spawner (602)",
"location": [453, 3632, 37],
@ -1102,7 +1118,7 @@
]
},
{
"type": "Spawner",
"$type": "Spawner",
"guid": "80ecee75-df9e-462a-ac4c-695f0941b8fb",
"name": "Spawner (602)",
"location": [387, 3524, 38],
@ -1119,7 +1135,7 @@
]
},
{
"type": "Spawner",
"$type": "Spawner",
"guid": "0b95e3c9-d94c-478c-9179-0ee10faad038",
"name": "Spawner (602)",
"location": [1130, 3240, -42],
@ -1140,7 +1156,7 @@
]
},
{
"type": "Spawner",
"$type": "Spawner",
"guid": "6eff3e1c-0886-4c54-b7d6-0b01477deca7",
"name": "Spawner (602)",
"location": [1054, 3215, 38],
@ -1159,7 +1175,7 @@
]
},
{
"type": "Spawner",
"$type": "Spawner",
"guid": "b928e0bb-64e8-442d-bd4f-c058e9bde1c2",
"name": "Spawner (602)",
"location": [1057, 3167, 38],
@ -1179,7 +1195,7 @@
]
},
{
"type": "Spawner",
"$type": "Spawner",
"guid": "979cdd74-3b3d-419b-9cf2-d7721332883d",
"name": "Spawner (602)",
"location": [1186, 3558, -42],
@ -1198,7 +1214,7 @@
]
},
{
"type": "Spawner",
"$type": "Spawner",
"guid": "4383a8fc-ec81-4ae6-b57e-f28ef51d26a1",
"name": "Spawner (602)",
"location": [35, 99, -5],
@ -1215,7 +1231,7 @@
]
},
{
"type": "Spawner",
"$type": "Spawner",
"guid": "2841721d-dcb8-41ce-84ed-ec8fca926424",
"name": "Spawner (602)",
"location": [35, 159, 0],
@ -1233,7 +1249,7 @@
]
},
{
"type": "Spawner",
"$type": "Spawner",
"guid": "600fd870-3e98-44cf-9650-3e5e80977bc5",
"name": "Spawner (602)",
"location": [143, 158, -20],
@ -1244,6 +1260,8 @@
"team": 0,
"homeRange": 5,
"walkingRange": 5,
"entries": [{ "name": "Niporailem", "maxCount": 1, "probability": 100 }]
"entries": [
{ "name": "Niporailem", "maxCount": 1, "probability": 100 }
]
}
]

View file

@ -1,6 +1,6 @@
[
{
"type": "Spawner",
"$type": "Spawner",
"guid": "1cd4979b-aac9-4287-b8af-9a31bb5526bc",
"name": "Spawner (603)",
"location": [1127, 1202, -2],
@ -11,10 +11,12 @@
"team": 0,
"homeRange": 0,
"walkingRange": -1,
"entries": [{ "name": "genericguard", "maxCount": 1, "probability": 100 }]
"entries": [
{ "name": "genericguard", "maxCount": 1, "probability": 100 }
]
},
{
"type": "Spawner",
"$type": "Spawner",
"guid": "568aaef8-af43-44f6-8212-a50e96dc51d8",
"name": "Spawner (603)",
"location": [1130, 1202, -2],
@ -25,10 +27,12 @@
"team": 0,
"homeRange": 0,
"walkingRange": -1,
"entries": [{ "name": "genericguard", "maxCount": 1, "probability": 100 }]
"entries": [
{ "name": "genericguard", "maxCount": 1, "probability": 100 }
]
},
{
"type": "Spawner",
"$type": "Spawner",
"guid": "c7124c0d-db07-4d72-92e4-ff8ebb54ce89",
"name": "Spawner (603)",
"location": [1128, 1165, -12],
@ -39,10 +43,12 @@
"team": 0,
"homeRange": 4,
"walkingRange": -1,
"entries": [{ "name": "Garamon", "maxCount": 1, "probability": 100 }]
"entries": [
{ "name": "Garamon", "maxCount": 1, "probability": 100 }
]
},
{
"type": "Spawner",
"$type": "Spawner",
"guid": "29ebc79d-9f58-4ab8-9017-44a71fe17440",
"name": "Spawner (603)",
"location": [1108, 1153, -22],
@ -53,10 +59,12 @@
"team": 0,
"homeRange": 4,
"walkingRange": -1,
"entries": [{ "name": "gremlin", "maxCount": 1, "probability": 100 }]
"entries": [
{ "name": "gremlin", "maxCount": 1, "probability": 100 }
]
},
{
"type": "Spawner",
"$type": "Spawner",
"guid": "81c4ea7e-4927-4bba-a31b-62ec12092f66",
"name": "Spawner (603)",
"location": [1110, 1169, -23],
@ -67,10 +75,12 @@
"team": 0,
"homeRange": 7,
"walkingRange": 10,
"entries": [{ "name": "Slime", "maxCount": 10, "probability": 100 }]
"entries": [
{ "name": "Slime", "maxCount": 10, "probability": 100 }
]
},
{
"type": "Spawner",
"$type": "Spawner",
"guid": "4eddff06-5db8-4a97-ad02-9284c3437410",
"name": "Spawner (603)",
"location": [1097, 1189, -38],
@ -81,10 +91,12 @@
"team": 0,
"homeRange": 7,
"walkingRange": 10,
"entries": [{ "name": "acidslug", "maxCount": 25, "probability": 100 }]
"entries": [
{ "name": "acidslug", "maxCount": 25, "probability": 100 }
]
},
{
"type": "Spawner",
"$type": "Spawner",
"guid": "bf730c77-6395-4ba6-80a4-8b004a7a78a1",
"name": "Spawner (603)",
"location": [1039, 1185, -56],
@ -101,7 +113,7 @@
]
},
{
"type": "Spawner",
"$type": "Spawner",
"guid": "f6324bc0-73ac-4936-805a-875b46164c54",
"name": "Spawner (603)",
"location": [1058, 1135, -37],
@ -112,10 +124,12 @@
"team": 0,
"homeRange": 10,
"walkingRange": 15,
"entries": [{ "name": "rotworm", "maxCount": 1, "probability": 100 }]
"entries": [
{ "name": "rotworm", "maxCount": 1, "probability": 100 }
]
},
{
"type": "Spawner",
"$type": "Spawner",
"guid": "dc88d319-4808-474a-8897-8bf076d951f9",
"name": "Spawner (603)",
"location": [1064, 1106, -65],
@ -126,10 +140,12 @@
"team": 0,
"homeRange": 10,
"walkingRange": 25,
"entries": [{ "name": "Kraken", "maxCount": 2, "probability": 100 }]
"entries": [
{ "name": "Kraken", "maxCount": 2, "probability": 100 }
]
},
{
"type": "Spawner",
"$type": "Spawner",
"guid": "bcc44632-3114-4d86-86c5-a9b1e05a9c98",
"name": "Spawner (603)",
"location": [1153, 1187, -32],
@ -140,10 +156,12 @@
"team": 0,
"homeRange": 14,
"walkingRange": 20,
"entries": [{ "name": "rotworm", "maxCount": 3, "probability": 100 }]
"entries": [
{ "name": "rotworm", "maxCount": 3, "probability": 100 }
]
},
{
"type": "Spawner",
"$type": "Spawner",
"guid": "84dd7320-c5a2-4b62-a959-83b749d88285",
"name": "Spawner (603)",
"location": [1187, 1186, -40],
@ -154,10 +172,12 @@
"team": 0,
"homeRange": 7,
"walkingRange": 20,
"entries": [{ "name": "WaterElemental", "maxCount": 1, "probability": 100 }]
"entries": [
{ "name": "WaterElemental", "maxCount": 1, "probability": 100 }
]
},
{
"type": "Spawner",
"$type": "Spawner",
"guid": "bd06c2cc-090f-47c9-98af-119591c20f30",
"name": "Spawner (603)",
"location": [1063, 1159, -42],
@ -175,7 +195,7 @@
]
},
{
"type": "Spawner",
"$type": "Spawner",
"guid": "0784ac30-eae0-4d6e-8953-f2217416f2a8",
"name": "Spawner (603)",
"location": [1152, 1130, -42],
@ -186,10 +206,12 @@
"team": 0,
"homeRange": 12,
"walkingRange": 20,
"entries": [{ "name": "rotworm", "maxCount": 1, "probability": 100 }]
"entries": [
{ "name": "rotworm", "maxCount": 1, "probability": 100 }
]
},
{
"type": "Spawner",
"$type": "Spawner",
"guid": "1485dfcd-1dea-412f-a8fe-06a3f6a076cb",
"name": "Spawner (603)",
"location": [1121, 1132, -42],
@ -200,10 +222,12 @@
"team": 0,
"homeRange": 4,
"walkingRange": -1,
"entries": [{ "name": "FiddlingTobin", "maxCount": 1, "probability": 100 }]
"entries": [
{ "name": "FiddlingTobin", "maxCount": 1, "probability": 100 }
]
},
{
"type": "Spawner",
"$type": "Spawner",
"guid": "fd834a4b-778b-4f9c-9a86-c92de20ff2fe",
"name": "Spawner (603)",
"location": [1150, 963, -43],
@ -214,10 +238,12 @@
"team": 0,
"homeRange": 4,
"walkingRange": -1,
"entries": [{ "name": "NevilleBrightwhistle", "maxCount": 1, "probability": 100 }]
"entries": [
{ "name": "NevilleBrightwhistle", "maxCount": 1, "probability": 100 }
]
},
{
"type": "Spawner",
"$type": "Spawner",
"guid": "f94a912f-46d6-45b9-81c0-910a92f2aa87",
"name": "Spawner (603)",
"location": [1108, 1086, -60],
@ -228,10 +254,12 @@
"team": 0,
"homeRange": 4,
"walkingRange": -1,
"entries": [{ "name": "tanglingroots", "maxCount": 2, "probability": 100 }]
"entries": [
{ "name": "tanglingroots", "maxCount": 2, "probability": 100 }
]
},
{
"type": "Spawner",
"$type": "Spawner",
"guid": "0829e9d8-84e7-41d2-9587-2d61b117f473",
"name": "Spawner (603)",
"location": [1095, 1076, -71],
@ -242,10 +270,12 @@
"team": 0,
"homeRange": 10,
"walkingRange": -1,
"entries": [{ "name": "tanglingroots", "maxCount": 3, "probability": 100 }]
"entries": [
{ "name": "tanglingroots", "maxCount": 3, "probability": 100 }
]
},
{
"type": "Spawner",
"$type": "Spawner",
"guid": "587467d9-8b59-4257-a242-9890ec058b00",
"name": "Spawner (603)",
"location": [1089, 1128, -42],
@ -256,10 +286,12 @@
"team": 0,
"homeRange": 4,
"walkingRange": -1,
"entries": [{ "name": "Gretchen", "maxCount": 1, "probability": 100 }]
"entries": [
{ "name": "Gretchen", "maxCount": 1, "probability": 100 }
]
},
{
"type": "Spawner",
"$type": "Spawner",
"guid": "9e60f208-6e14-4d77-83f0-fb067d02e804",
"name": "Spawner (603)",
"location": [1104, 1128, -52],
@ -270,10 +302,12 @@
"team": 0,
"homeRange": 4,
"walkingRange": -1,
"entries": [{ "name": "ElderDugan", "maxCount": 1, "probability": 100 }]
"entries": [
{ "name": "ElderDugan", "maxCount": 1, "probability": 100 }
]
},
{
"type": "Spawner",
"$type": "Spawner",
"guid": "66d7c6f5-eeb7-4b34-af75-c82d38330ee1",
"name": "Spawner (603)",
"location": [1135, 1132, -42],
@ -284,10 +318,12 @@
"team": 0,
"homeRange": 4,
"walkingRange": -1,
"entries": [{ "name": "QuartermasterFlint", "maxCount": 1, "probability": 100 }]
"entries": [
{ "name": "QuartermasterFlint", "maxCount": 1, "probability": 100 }
]
},
{
"type": "Spawner",
"$type": "Spawner",
"guid": "2affc9d3-8b16-4c2e-947f-45d362a4a0f4",
"name": "Spawner (603)",
"location": [1015, 1118, -42],
@ -312,7 +348,7 @@
]
},
{
"type": "Spawner",
"$type": "Spawner",
"guid": "382e2dee-6150-4d26-960b-c07b96638749",
"name": "Spawner (603)",
"location": [1022, 1087, -42],
@ -337,7 +373,7 @@
]
},
{
"type": "Spawner",
"$type": "Spawner",
"guid": "d072d262-08f3-4da3-9dcc-3983fa81d186",
"name": "Spawner (603)",
"location": [1039, 1072, -32],
@ -348,10 +384,12 @@
"team": 0,
"homeRange": 5,
"walkingRange": 10,
"entries": [{ "name": "GreaterPoisonElemental", "maxCount": 1, "probability": 100 }]
"entries": [
{ "name": "GreaterPoisonElemental", "maxCount": 1, "probability": 100 }
]
},
{
"type": "Spawner",
"$type": "Spawner",
"guid": "9beef510-d580-44fc-95a6-d5ba9100cc34",
"name": "Spawner (603)",
"location": [1191, 1022, -42],
@ -368,7 +406,7 @@
]
},
{
"type": "Spawner",
"$type": "Spawner",
"guid": "afed6e34-b8ee-4c64-b7d1-1b66629ccb97",
"name": "Spawner (603)",
"location": [1177, 995, -27],
@ -385,7 +423,7 @@
]
},
{
"type": "Spawner",
"$type": "Spawner",
"guid": "5caafbc5-1310-408c-8a26-ec00335494c2",
"name": "Spawner (603)",
"location": [1218, 1049, -42],
@ -402,7 +440,7 @@
]
},
{
"type": "Spawner",
"$type": "Spawner",
"guid": "a7f77781-8fcb-4aa0-b398-8f9b87b59eb2",
"name": "Spawner (603)",
"location": [1178, 971, -27],
@ -413,10 +451,12 @@
"team": 0,
"homeRange": 10,
"walkingRange": 15,
"entries": [{ "name": "GrayGoblin", "maxCount": 2, "probability": 100 }]
"entries": [
{ "name": "GrayGoblin", "maxCount": 2, "probability": 100 }
]
},
{
"type": "Spawner",
"$type": "Spawner",
"guid": "6b2a7732-7ee9-480e-abd4-db501214cb05",
"name": "Spawner (603)",
"location": [1214, 1026, -22],
@ -427,10 +467,12 @@
"team": 0,
"homeRange": 5,
"walkingRange": 15,
"entries": [{ "name": "GrayGoblin", "maxCount": 2, "probability": 100 }]
"entries": [
{ "name": "GrayGoblin", "maxCount": 2, "probability": 100 }
]
},
{
"type": "Spawner",
"$type": "Spawner",
"guid": "b00555a0-97e8-4eb3-b609-2bb9bc5d21bd",
"name": "Spawner (603)",
"location": [1199, 1053, -42],
@ -447,7 +489,7 @@
]
},
{
"type": "Spawner",
"$type": "Spawner",
"guid": "23b17a4f-d7e9-421c-8581-ba8eb8aff487",
"name": "Spawner (603)",
"location": [1204, 1041, -42],
@ -458,10 +500,12 @@
"team": 0,
"homeRange": 4,
"walkingRange": -1,
"entries": [{ "name": "Jaacar", "maxCount": 1, "probability": 100 }]
"entries": [
{ "name": "Jaacar", "maxCount": 1, "probability": 100 }
]
},
{
"type": "Spawner",
"$type": "Spawner",
"guid": "25d8cb74-d701-4e5a-9074-e7c20f7cff39",
"name": "Spawner (603)",
"location": [1189, 989, -27],
@ -472,10 +516,12 @@
"team": 0,
"homeRange": 4,
"walkingRange": -1,
"entries": [{ "name": "Barreraak", "maxCount": 1, "probability": 100 }]
"entries": [
{ "name": "Barreraak", "maxCount": 1, "probability": 100 }
]
},
{
"type": "Spawner",
"$type": "Spawner",
"guid": "95edace5-c3dc-4582-b559-1c3f9905f6f5",
"name": "Spawner (603)",
"location": [1208, 984, -1],
@ -492,7 +538,7 @@
]
},
{
"type": "Spawner",
"$type": "Spawner",
"guid": "975d89b2-f693-48c0-b88b-9796d8e39ee8",
"name": "Spawner (603)",
"location": [1217, 1074, -52],
@ -514,7 +560,7 @@
]
},
{
"type": "Spawner",
"$type": "Spawner",
"guid": "1b91d22a-24a2-4aee-88fa-36304f40dfdc",
"name": "Spawner (603)",
"location": [1053, 861, -32],
@ -525,10 +571,12 @@
"team": 0,
"homeRange": 15,
"walkingRange": 20,
"entries": [{ "name": "NavreyNightEyes", "maxCount": 1, "probability": 100 }]
"entries": [
{ "name": "NavreyNightEyes", "maxCount": 1, "probability": 100 }
]
},
{
"type": "Spawner",
"$type": "Spawner",
"guid": "a37dcfad-6094-4074-8fc4-ead7d8dd889c",
"name": "Spawner (603)",
"location": [1094, 1002, -16],
@ -545,7 +593,7 @@
]
},
{
"type": "Spawner",
"$type": "Spawner",
"guid": "5a3f2d8b-8ed7-43fe-977b-a082ad4fd243",
"name": "Spawner (603)",
"location": [1124, 1008, -43],
@ -562,7 +610,7 @@
]
},
{
"type": "Spawner",
"$type": "Spawner",
"guid": "31bf6eb1-fe34-4d25-b7cc-d0f395b2370a",
"name": "Spawner (603)",
"location": [1147, 993, -41],
@ -573,10 +621,12 @@
"team": 0,
"homeRange": 7,
"walkingRange": 7,
"entries": [{ "name": "IronBeetle", "maxCount": 1, "probability": 100 }]
"entries": [
{ "name": "IronBeetle", "maxCount": 1, "probability": 100 }
]
},
{
"type": "Spawner",
"$type": "Spawner",
"guid": "399bf1fe-0c47-4945-8c14-5ee8a1d3b6b3",
"name": "Spawner (603)",
"location": [1034, 1028, -43],
@ -594,7 +644,7 @@
]
},
{
"type": "Spawner",
"$type": "Spawner",
"guid": "df3d7a25-190a-4f00-968c-a6def4cfa15e",
"name": "Spawner (603)",
"location": [1015, 1022, -43],
@ -605,10 +655,12 @@
"team": 0,
"homeRange": 3,
"walkingRange": 10,
"entries": [{ "name": "GreenGoblinAlchemist", "maxCount": 2, "probability": 100 }]
"entries": [
{ "name": "GreenGoblinAlchemist", "maxCount": 2, "probability": 100 }
]
},
{
"type": "Spawner",
"$type": "Spawner",
"guid": "4bdd2d24-47f0-4678-b220-921df1e1c213",
"name": "Spawner (603)",
"location": [1014, 1000, -43],
@ -625,7 +677,7 @@
]
},
{
"type": "Spawner",
"$type": "Spawner",
"guid": "b1225d9f-1777-4ceb-9e03-2566fb2dc314",
"name": "Spawner (603)",
"location": [1034, 1004, -43],
@ -642,7 +694,7 @@
]
},
{
"type": "Spawner",
"$type": "Spawner",
"guid": "af6a0287-46a1-490c-88ae-15172ec414f8",
"name": "Spawner (603)",
"location": [1024, 1018, -42],
@ -653,10 +705,12 @@
"team": 0,
"homeRange": 4,
"walkingRange": 10,
"entries": [{ "name": "rotworm", "maxCount": 4, "probability": 100 }]
"entries": [
{ "name": "rotworm", "maxCount": 4, "probability": 100 }
]
},
{
"type": "Spawner",
"$type": "Spawner",
"guid": "332360f9-69c2-4c45-b36b-0325b7c6650e",
"name": "Spawner (603)",
"location": [1015, 976, -27],
@ -674,7 +728,7 @@
]
},
{
"type": "Spawner",
"$type": "Spawner",
"guid": "4bb2f7ea-9131-47be-83a6-0f13831e76b1",
"name": "Spawner (603)",
"location": [1044, 976, -30],

View file

@ -1,6 +1,6 @@
[
{
"type": "Spawner",
"$type": "Spawner",
"guid": "7bd2b3c7-3c39-40c5-9c3b-98b73d060c65",
"name": "Spawner (604)",
"location": [976, 3881, -42],
@ -11,10 +11,12 @@
"team": 0,
"homeRange": 0,
"walkingRange": 5,
"entries": [{ "name": "Baker", "maxCount": 1, "probability": 100 }]
"entries": [
{ "name": "Baker", "maxCount": 1, "probability": 100 }
]
},
{
"type": "Spawner",
"$type": "Spawner",
"guid": "a80c5b72-e181-4266-8109-9c7a5eaf6a10",
"name": "Spawner (604)",
"location": [1010, 3883, -42],
@ -31,7 +33,7 @@
]
},
{
"type": "Spawner",
"$type": "Spawner",
"guid": "99c47ee2-bbb0-4d0b-9294-18104ebb0f6b",
"name": "Spawner (604)",
"location": [1013, 3913, -42],
@ -48,7 +50,7 @@
]
},
{
"type": "Spawner",
"$type": "Spawner",
"guid": "6b95fe1b-1c63-479d-b481-1b4d4c929a79",
"name": "Spawner (604)",
"location": [977, 3909, -42],
@ -65,7 +67,7 @@
]
},
{
"type": "Spawner",
"$type": "Spawner",
"guid": "9c632527-c639-469c-9ded-ca859e4a8859",
"name": "Spawner (604)",
"location": [943, 3924, -42],
@ -76,10 +78,12 @@
"team": 0,
"homeRange": 0,
"walkingRange": 5,
"entries": [{ "name": "InnKeeper", "maxCount": 1, "probability": 100 }]
"entries": [
{ "name": "InnKeeper", "maxCount": 1, "probability": 100 }
]
},
{
"type": "Spawner",
"$type": "Spawner",
"guid": "b317c56d-c032-4e81-aa3a-78988500a740",
"name": "Spawner (604)",
"location": [1020, 3884, -42],
@ -96,7 +100,7 @@
]
},
{
"type": "Spawner",
"$type": "Spawner",
"guid": "556fdb4d-f7f2-4ba0-9d01-d94a76ee7a28",
"name": "Spawner (604)",
"location": [740, 3466, -19],
@ -107,10 +111,12 @@
"team": 0,
"homeRange": 0,
"walkingRange": 5,
"entries": [{ "name": "InnKeeper", "maxCount": 1, "probability": 100 }]
"entries": [
{ "name": "InnKeeper", "maxCount": 1, "probability": 100 }
]
},
{
"type": "Spawner",
"$type": "Spawner",
"guid": "65b19c79-0f40-4772-9037-1cad0769456d",
"name": "Spawner (604)",
"location": [772, 3467, -20],
@ -121,10 +127,12 @@
"team": 0,
"homeRange": 0,
"walkingRange": 5,
"entries": [{ "name": "Scribe", "maxCount": 1, "probability": 100 }]
"entries": [
{ "name": "Scribe", "maxCount": 1, "probability": 100 }
]
},
{
"type": "Spawner",
"$type": "Spawner",
"guid": "fd3342ae-de6c-436e-964a-e410ba1f983b",
"name": "Spawner (604)",
"location": [773, 3478, -20],
@ -142,7 +150,7 @@
]
},
{
"type": "Spawner",
"$type": "Spawner",
"guid": "fcffbb1c-fb73-4c59-9257-52e1d6ebadb4",
"name": "Spawner (604)",
"location": [772, 3490, -20],
@ -160,7 +168,7 @@
]
},
{
"type": "Spawner",
"$type": "Spawner",
"guid": "bb731ab1-950e-4e4b-8e55-6378eeb2c3b0",
"name": "Spawner (604)",
"location": [783, 3491, -20],
@ -177,7 +185,7 @@
]
},
{
"type": "Spawner",
"$type": "Spawner",
"guid": "36f1b1bc-0754-4605-929a-d7c8e030a80b",
"name": "Spawner (604)",
"location": [797, 3494, 0],
@ -194,7 +202,7 @@
]
},
{
"type": "Spawner",
"$type": "Spawner",
"guid": "28cda9f4-bbd1-451d-acb0-f568361a7d6f",
"name": "Spawner (604)",
"location": [805, 3491, -20],
@ -205,10 +213,12 @@
"team": 0,
"homeRange": 0,
"walkingRange": 5,
"entries": [{ "name": "Baker", "maxCount": 1, "probability": 100 }]
"entries": [
{ "name": "Baker", "maxCount": 1, "probability": 100 }
]
},
{
"type": "Spawner",
"$type": "Spawner",
"guid": "0bd1f793-51e9-4dbe-ab5c-dbd452e85d8c",
"name": "Spawner (604)",
"location": [817, 3456, -10],
@ -219,10 +229,12 @@
"team": 0,
"homeRange": 0,
"walkingRange": 5,
"entries": [{ "name": "Butcher", "maxCount": 1, "probability": 100 }]
"entries": [
{ "name": "Butcher", "maxCount": 1, "probability": 100 }
]
},
{
"type": "Spawner",
"$type": "Spawner",
"guid": "5f573458-bb63-4617-b2c4-c73762718dae",
"name": "Spawner (604)",
"location": [817, 3438, 0],
@ -240,7 +252,7 @@
]
},
{
"type": "Spawner",
"$type": "Spawner",
"guid": "37378f9d-5e71-46a8-8d15-0b98d48a3d04",
"name": "Spawner (604)",
"location": [817, 3426, 0],
@ -257,7 +269,7 @@
]
},
{
"type": "Spawner",
"$type": "Spawner",
"guid": "2088913c-0ec7-4225-8f9d-1112e817542b",
"name": "Spawner (604)",
"location": [816, 3419, 0],
@ -274,7 +286,7 @@
]
},
{
"type": "Spawner",
"$type": "Spawner",
"guid": "094d18b0-4b1a-491d-ae33-2463d400904b",
"name": "Spawner (604)",
"location": [805, 3397, 0],
@ -292,7 +304,7 @@
]
},
{
"type": "Spawner",
"$type": "Spawner",
"guid": "b9c41d27-d185-41dc-aa13-7258c018abc1",
"name": "Spawner (604)",
"location": [804, 3387, 0],
@ -309,7 +321,7 @@
]
},
{
"type": "Spawner",
"$type": "Spawner",
"guid": "720f9c6d-c0e6-4f66-aca9-cca448a5d02d",
"name": "Spawner (604)",
"location": [851, 3404, -20],
@ -320,10 +332,12 @@
"team": 0,
"homeRange": 0,
"walkingRange": 5,
"entries": [{ "name": "AnimalTrainer", "maxCount": 1, "probability": 100 }]
"entries": [
{ "name": "AnimalTrainer", "maxCount": 1, "probability": 100 }
]
},
{
"type": "Spawner",
"$type": "Spawner",
"guid": "0197b002-1c6d-45ac-8d7d-21706a854586",
"name": "Spawner (604)",
"location": [833, 3439, -20],
@ -340,7 +354,7 @@
]
},
{
"type": "Spawner",
"$type": "Spawner",
"guid": "3e2450a8-7aa3-434a-a144-dfc36f784d9e",
"name": "Spawner (604)",
"location": [843, 3439, -20],
@ -357,7 +371,7 @@
]
},
{
"type": "Spawner",
"$type": "Spawner",
"guid": "f2b8fe59-6e8b-4db1-a20c-9663efe3b725",
"name": "Spawner (604)",
"location": [702, 3435, -20],
@ -374,7 +388,7 @@
]
},
{
"type": "Spawner",
"$type": "Spawner",
"guid": "59459615-255f-449a-b4ee-c6adf848c09a",
"name": "Spawner (604)",
"location": [727, 3425, -20],
@ -391,7 +405,7 @@
]
},
{
"type": "Spawner",
"$type": "Spawner",
"guid": "fa9d60df-aa99-4934-bfab-35066f170481",
"name": "Spawner (604)",
"location": [715, 3446, -20],
@ -408,7 +422,7 @@
]
},
{
"type": "Spawner",
"$type": "Spawner",
"guid": "27644d88-d3d5-458f-902d-78b7b59eaf95",
"name": "Spawner (604)",
"location": [713, 3438, -20],
@ -426,7 +440,7 @@
]
},
{
"type": "Spawner",
"$type": "Spawner",
"guid": "2a0e5133-2658-44f4-95ca-bcc5d68ddaeb",
"name": "Spawner (604)",
"location": [794, 3446, -10],
@ -443,7 +457,7 @@
]
},
{
"type": "Spawner",
"$type": "Spawner",
"guid": "6d91f1ff-12e5-4d75-aa5a-033ff00b9a24",
"name": "Spawner (604)",
"location": [779, 3431, -10],
@ -454,10 +468,12 @@
"team": 0,
"homeRange": 0,
"walkingRange": 5,
"entries": [{ "name": "Jeweler", "maxCount": 1, "probability": 100 }]
"entries": [
{ "name": "Jeweler", "maxCount": 1, "probability": 100 }
]
},
{
"type": "Spawner",
"$type": "Spawner",
"guid": "a1ba61c1-0fc5-46a4-b7de-1350606fb0fc",
"name": "Spawner (604)",
"location": [842, 3453, -18],
@ -468,6 +484,8 @@
"team": 0,
"homeRange": 0,
"walkingRange": 4,
"entries": [{ "name": "TownCrier", "maxCount": 1, "probability": 100 }]
"entries": [
{ "name": "TownCrier", "maxCount": 1, "probability": 100 }
]
}
]

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

View file

@ -1,290 +0,0 @@
[
{
"type": "RegionSpawner",
"location": [375, 1191, 0],
"map": "Trammel",
"region": "A Wheatfield in Yew 2",
"count": 6,
"entries": [{ "maxCount": 6, "name": "FarmableWheat", "probability": 100 }],
"minDelay": "00:00:10",
"maxDelay": "00:00:30",
"guid": "fb826650-9fad-43ff-8ba1-9d848f1f26c5",
"name": "Spawner"
},
{
"type": "RegionSpawner",
"location": [567, 1239, 0],
"map": "Trammel",
"region": "A Wheatfield in Yew 1",
"count": 5,
"entries": [{ "maxCount": 5, "name": "FarmableWheat", "probability": 100 }],
"minDelay": "00:00:10",
"maxDelay": "00:00:30",
"guid": "79af53a5-33e2-4fdb-95dd-278a050e54fc",
"name": "Spawner"
},
{
"type": "RegionSpawner",
"location": [571, 1099, 0],
"map": "Trammel",
"region": "A Farm in Yew",
"count": 10,
"entries": [{ "maxCount": 10, "name": "Sheep", "probability": 100 }],
"minDelay": "00:00:10",
"maxDelay": "00:00:30",
"guid": "8408baaa-0de1-4cb0-ad89-973782e3a565",
"name": "Spawner"
},
{
"type": "RegionSpawner",
"location": [675, 939, 0],
"map": "Trammel",
"region": "A Field of Sheep in Yew 1",
"count": 10,
"entries": [{ "maxCount": 10, "name": "Sheep", "probability": 100 }],
"minDelay": "00:00:10",
"maxDelay": "00:00:30",
"guid": "b17f2f9c-9d36-4bea-87db-69f8effc4926",
"name": "Spawner"
},
{
"type": "RegionSpawner",
"location": [675, 1179, 0],
"map": "Trammel",
"region": "A Field of Sheep in Yew 2",
"count": 10,
"entries": [{ "maxCount": 10, "name": "Sheep", "probability": 100 }],
"minDelay": "00:00:10",
"maxDelay": "00:00:30",
"guid": "690ccc1c-2ac8-4f05-8066-695aa53c98aa",
"name": "Spawner"
},
{
"type": "RegionSpawner",
"location": [813, 2163, 0],
"map": "Trammel",
"region": "A Wheatfield in Skara Brae 1",
"count": 6,
"entries": [{ "maxCount": 6, "name": "FarmableWheat", "probability": 100 }],
"minDelay": "00:00:10",
"maxDelay": "00:00:30",
"guid": "ee6692af-82cf-477e-96ec-375be7a23db9",
"name": "Spawner"
},
{
"type": "RegionSpawner",
"location": [823, 2254, 0],
"map": "Trammel",
"region": "A Carrot Field in Skara Brae",
"count": 6,
"entries": [{ "maxCount": 6, "name": "FarmableCarrot", "probability": 100 }],
"minDelay": "00:00:10",
"maxDelay": "00:00:30",
"guid": "c052a5e0-493d-483e-a98a-665cf05c5c14",
"name": "Spawner"
},
{
"type": "RegionSpawner",
"location": [823, 2264, 0],
"map": "Trammel",
"region": "An Onion Field in Skara Brae",
"count": 6,
"entries": [{ "maxCount": 6, "name": "FarmableOnion", "probability": 100 }],
"minDelay": "00:00:10",
"maxDelay": "00:00:30",
"guid": "41a583ac-749c-40a7-8658-fb3deb1b222a",
"name": "Spawner"
},
{
"type": "RegionSpawner",
"location": [823, 2264, 0],
"map": "Trammel",
"region": "A Cabbage Field in Skara Brae 1",
"count": 6,
"entries": [{ "maxCount": 6, "name": "FarmableCabbage", "probability": 100 }],
"minDelay": "00:00:10",
"maxDelay": "00:00:30",
"guid": "df4c5918-b490-4257-9aeb-541b44980b27",
"name": "Spawner"
},
{
"type": "RegionSpawner",
"location": [823, 2274, 0],
"map": "Trammel",
"region": "A Cabbage Field in Skara Brae 2",
"count": 6,
"entries": [{ "maxCount": 6, "name": "FarmableCabbage", "probability": 100 }],
"minDelay": "00:00:10",
"maxDelay": "00:00:30",
"guid": "1025906e-047c-460b-9bb8-581f57ff1357",
"name": "Spawner"
},
{
"type": "RegionSpawner",
"location": [823, 2355, 0],
"map": "Trammel",
"region": "A Cotton Field in Skara Brae",
"count": 6,
"entries": [{ "maxCount": 6, "name": "FarmableCotton", "probability": 100 }],
"minDelay": "00:00:10",
"maxDelay": "00:00:30",
"guid": "185e475b-36cf-4dad-8776-537e84b15e42",
"name": "Spawner"
},
{
"type": "RegionSpawner",
"location": [843, 2352, 0],
"map": "Trammel",
"region": "A Wheatfield in Skara Brae 2",
"count": 6,
"entries": [{ "maxCount": 6, "name": "FarmableWheat", "probability": 100 }],
"minDelay": "00:00:10",
"maxDelay": "00:00:30",
"guid": "59e99d3b-b727-491b-b0f3-03a441b10c04",
"name": "Spawner"
},
{
"type": "RegionSpawner",
"location": [1120, 1624, 0],
"map": "Trammel",
"region": "A Wheatfield in Britain 4",
"count": 8,
"entries": [{ "maxCount": 8, "name": "FarmableWheat", "probability": 100 }],
"minDelay": "00:00:10",
"maxDelay": "00:00:30",
"guid": "85cc6fca-4f66-4428-aa3a-347a88523a4b",
"name": "Spawner"
},
{
"type": "RegionSpawner",
"location": [1135, 1791, 0],
"map": "Trammel",
"region": "A Wheatfield in Britain 1",
"count": 8,
"entries": [{ "maxCount": 8, "name": "FarmableWheat", "probability": 100 }],
"minDelay": "00:00:10",
"maxDelay": "00:00:30",
"guid": "afdfec82-dac3-43df-bd7b-cd2cfad5f265",
"name": "Spawner"
},
{
"type": "RegionSpawner",
"location": [1152, 1576, 0],
"map": "Trammel",
"region": "A Wheatfield in Britain 5",
"count": 8,
"entries": [{ "maxCount": 8, "name": "FarmableWheat", "probability": 100 }],
"minDelay": "00:00:10",
"maxDelay": "00:00:30",
"guid": "34b0579c-1fc6-4868-82c5-09138c5e5878",
"name": "Spawner"
},
{
"type": "RegionSpawner",
"location": [1183, 1683, 0],
"map": "Trammel",
"region": "A Cabbage Field in Britain 1",
"count": 8,
"entries": [{ "maxCount": 8, "name": "FarmableCabbage", "probability": 100 }],
"minDelay": "00:00:10",
"maxDelay": "00:00:30",
"guid": "997d9ff5-ad2b-421b-ae76-6794e7e88150",
"name": "Spawner"
},
{
"type": "RegionSpawner",
"location": [1199, 1683, 0],
"map": "Trammel",
"region": "A Turnip Field in Britain 1",
"count": 8,
"entries": [{ "maxCount": 8, "name": "FarmableTurnip", "probability": 100 }],
"minDelay": "00:00:10",
"maxDelay": "00:00:30",
"guid": "bfb677f7-e824-46f6-95ef-ee5602f4f9c3",
"name": "Spawner"
},
{
"type": "RegionSpawner",
"location": [1199, 1823, 0],
"map": "Trammel",
"region": "A Wheatfield in Britain 2",
"count": 8,
"entries": [{ "maxCount": 8, "name": "FarmableWheat", "probability": 100 }],
"minDelay": "00:00:10",
"maxDelay": "00:00:30",
"guid": "c42ef172-78c8-4c5e-b26c-fcc3fc3da1cc",
"name": "Spawner"
},
{
"type": "RegionSpawner",
"location": [1215, 1723, 0],
"map": "Trammel",
"region": "A Carrot Field in Britain 1",
"count": 8,
"entries": [{ "maxCount": 8, "name": "FarmableCarrot", "probability": 100 }],
"minDelay": "00:00:10",
"maxDelay": "00:00:30",
"guid": "57551e80-0018-4fcf-8b83-9d39ef19b0a6",
"name": "Spawner"
},
{
"type": "RegionSpawner",
"location": [1216, 1604, 0],
"map": "Trammel",
"region": "A Turnip Field in Britain 2",
"count": 6,
"entries": [{ "maxCount": 6, "name": "FarmableTurnip", "probability": 100 }],
"minDelay": "00:00:10",
"maxDelay": "00:00:30",
"guid": "0da29e36-7bff-4fcf-803d-79dd16212653",
"name": "Spawner"
},
{
"type": "RegionSpawner",
"location": [1231, 1723, 0],
"map": "Trammel",
"region": "An Onion Field in Britain 1",
"count": 8,
"entries": [{ "maxCount": 8, "name": "FarmableOnion", "probability": 100 }],
"minDelay": "00:00:10",
"maxDelay": "00:00:30",
"guid": "4656ad9d-a455-4217-9316-ec10d10cf68c",
"name": "Spawner"
},
{
"type": "RegionSpawner",
"location": [1231, 1887, 0],
"map": "Trammel",
"region": "A Wheatfield in Britain 3",
"count": 8,
"entries": [{ "maxCount": 8, "name": "FarmableWheat", "probability": 100 }],
"minDelay": "00:00:10",
"maxDelay": "00:00:30",
"guid": "d5b08634-5c5d-4bc2-b6cd-74cb06132423",
"name": "Spawner"
},
{
"type": "RegionSpawner",
"location": [1232, 1604, 0],
"map": "Trammel",
"region": "A Carrot Field in Britain 2",
"count": 6,
"entries": [{ "maxCount": 6, "name": "FarmableCarrot", "probability": 100 }],
"minDelay": "00:00:10",
"maxDelay": "00:00:30",
"guid": "ab0cb920-b25f-4811-8ef6-938549dbfa27",
"name": "Spawner"
},
{
"type": "RegionSpawner",
"location": [4567, 1475, 0],
"map": "Trammel",
"region": "A Cotton Field in Moonglow",
"count": 8,
"entries": [{ "maxCount": 8, "name": "FarmableCotton", "probability": 100 }],
"minDelay": "00:00:10",
"maxDelay": "00:00:30",
"guid": "69dfbe70-b46c-42b0-9ff1-351fd9651884",
"name": "Spawner"
}
]

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

View file

@ -1,27 +1,38 @@
[
{
"type": "Spawner",
"guid": "c6dedce5-b3a9-4510-b8a4-4e1be285305c",
"$type": "Spawner",
"guid": "163c83bd-e63d-4654-8c0e-231486150f45",
"name": "Spawner (201)",
"location": [6513, 871, 0],
"location": [6540, 872, 0],
"map": "Felucca",
"count": 17,
"count": 1,
"minDelay": "00:05:00",
"maxDelay": "00:10:00",
"team": 0,
"homeRange": 35,
"walkingRange": 35,
"homeRange": 5,
"walkingRange": 5,
"entries": [
{ "name": "GiantSerpent", "maxCount": 17, "probability": 100 },
{ "name": "GiantToad", "maxCount": 17, "probability": 100 },
{ "name": "Harpy", "maxCount": 17, "probability": 100 },
{ "name": "SilverSerpent", "maxCount": 17, "probability": 100 },
{ "name": "Snake", "maxCount": 17, "probability": 100 },
{ "name": "Bogling", "maxCount": 17, "probability": 100 }
{ "name": "InsaneDryad", "maxCount": 1, "probability": 100 }
]
},
{
"type": "Spawner",
"$type": "Spawner",
"guid": "196961bc-de35-4e5c-9fbf-079ab5092d5f",
"name": "Spawner (201)",
"location": [6518, 879, 0],
"map": "Felucca",
"count": 1,
"minDelay": "00:05:00",
"maxDelay": "00:10:00",
"team": 0,
"homeRange": 5,
"walkingRange": 5,
"entries": [
{ "name": "InsaneDryad", "maxCount": 1, "probability": 100 }
]
},
{
"$type": "Spawner",
"guid": "3bd66ec7-4d6b-46cb-8913-5c62344510c0",
"name": "Spawner (201)",
"location": [6507, 843, 0],
@ -43,7 +54,7 @@
]
},
{
"type": "Spawner",
"$type": "Spawner",
"guid": "687f8c07-43c4-4724-8ede-078862373736",
"name": "Spawner (201)",
"location": [6535, 848, 0],
@ -61,7 +72,44 @@
]
},
{
"type": "Spawner",
"$type": "Spawner",
"guid": "9b045049-0b03-4f18-9148-b0bd1fa3600b",
"name": "Spawner (201)",
"location": [6489, 897, 0],
"map": "Felucca",
"count": 1,
"minDelay": "00:05:00",
"maxDelay": "00:10:00",
"team": 0,
"homeRange": 5,
"walkingRange": 5,
"entries": [
{ "name": "InsaneDryad", "maxCount": 1, "probability": 100 }
]
},
{
"$type": "Spawner",
"guid": "c6dedce5-b3a9-4510-b8a4-4e1be285305c",
"name": "Spawner (201)",
"location": [6513, 871, 0],
"map": "Felucca",
"count": 17,
"minDelay": "00:05:00",
"maxDelay": "00:10:00",
"team": 0,
"homeRange": 35,
"walkingRange": 35,
"entries": [
{ "name": "GiantSerpent", "maxCount": 17, "probability": 100 },
{ "name": "GiantToad", "maxCount": 17, "probability": 100 },
{ "name": "Harpy", "maxCount": 17, "probability": 100 },
{ "name": "SilverSerpent", "maxCount": 17, "probability": 100 },
{ "name": "Snake", "maxCount": 17, "probability": 100 },
{ "name": "Bogling", "maxCount": 17, "probability": 100 }
]
},
{
"$type": "Spawner",
"guid": "d4583e64-cac2-49f5-bb09-86c9481e0147",
"name": "Spawner (201)",
"location": [6518, 870, 0],
@ -83,21 +131,7 @@
]
},
{
"type": "Spawner",
"guid": "163c83bd-e63d-4654-8c0e-231486150f45",
"name": "Spawner (201)",
"location": [6540, 872, 0],
"map": "Felucca",
"count": 1,
"minDelay": "00:05:00",
"maxDelay": "00:10:00",
"team": 0,
"homeRange": 5,
"walkingRange": 5,
"entries": [{ "name": "InsaneDryad", "maxCount": 1, "probability": 100 }]
},
{
"type": "Spawner",
"$type": "Spawner",
"guid": "f708c05a-1da4-4452-8d8b-1965849d6e39",
"name": "Spawner (201)",
"location": [6585, 878, 0],
@ -113,33 +147,5 @@
{ "name": "Saliva", "maxCount": 7, "probability": 100 },
{ "name": "Changeling", "maxCount": 7, "probability": 100 }
]
},
{
"type": "Spawner",
"guid": "9b045049-0b03-4f18-9148-b0bd1fa3600b",
"name": "Spawner (201)",
"location": [6489, 897, 0],
"map": "Felucca",
"count": 1,
"minDelay": "00:05:00",
"maxDelay": "00:10:00",
"team": 0,
"homeRange": 5,
"walkingRange": 5,
"entries": [{ "name": "InsaneDryad", "maxCount": 1, "probability": 100 }]
},
{
"type": "Spawner",
"guid": "196961bc-de35-4e5c-9fbf-079ab5092d5f",
"name": "Spawner (201)",
"location": [6518, 879, 0],
"map": "Felucca",
"count": 1,
"minDelay": "00:05:00",
"maxDelay": "00:10:00",
"team": 0,
"homeRange": 5,
"walkingRange": 5,
"entries": [{ "name": "InsaneDryad", "maxCount": 1, "probability": 100 }]
}
]

View file

@ -1,60 +1,6 @@
[
{
"type": "Spawner",
"guid": "aca1fcd4-cc2b-4472-ab6e-89e0322f9fec",
"name": "Spawner (202)",
"location": [6049, 1447, 4],
"map": "Felucca",
"count": 3,
"minDelay": "00:00:00",
"maxDelay": "00:01:00",
"team": 0,
"homeRange": 2,
"walkingRange": 15,
"entries": [
{ "name": "SewerRat", "maxCount": 2, "probability": 100 },
{ "name": "Rat", "maxCount": 2, "probability": 100 },
{ "name": "BullFrog", "maxCount": 1, "probability": 100 }
]
},
{
"type": "Spawner",
"guid": "8674d509-c36a-4ab8-9399-6329f1379984",
"name": "Spawner (202)",
"location": [6091, 1491, 10],
"map": "Felucca",
"count": 3,
"minDelay": "00:00:00",
"maxDelay": "00:01:00",
"team": 0,
"homeRange": 2,
"walkingRange": 15,
"entries": [
{ "name": "SewerRat", "maxCount": 2, "probability": 100 },
{ "name": "Rat", "maxCount": 2, "probability": 100 },
{ "name": "BullFrog", "maxCount": 1, "probability": 100 }
]
},
{
"type": "Spawner",
"guid": "5da0bc6b-dfdb-47b8-a0df-0ff3eaf5c212",
"name": "Spawner (202)",
"location": [6048, 1487, 5],
"map": "Felucca",
"count": 3,
"minDelay": "00:00:00",
"maxDelay": "00:01:00",
"team": 0,
"homeRange": 2,
"walkingRange": 15,
"entries": [
{ "name": "SewerRat", "maxCount": 2, "probability": 100 },
{ "name": "Rat", "maxCount": 2, "probability": 100 },
{ "name": "BullFrog", "maxCount": 1, "probability": 100 }
]
},
{
"type": "Spawner",
"$type": "Spawner",
"guid": "07b03a23-af40-4539-be8b-6834573838df",
"name": "Spawner (202)",
"location": [6065, 1459, 5],
@ -72,43 +18,55 @@
]
},
{
"type": "Spawner",
"guid": "b8273eff-be93-4ab9-8642-c1e9a92ddbbc",
"$type": "Spawner",
"guid": "099f6832-75ef-4f46-91b2-0d64509897c2",
"name": "Spawner (202)",
"location": [6092, 1448, 5],
"location": [6052, 1463, 0],
"map": "Felucca",
"count": 3,
"count": 8,
"minDelay": "00:00:00",
"maxDelay": "00:01:00",
"team": 0,
"homeRange": 2,
"walkingRange": 15,
"homeRange": 5,
"walkingRange": 25,
"entries": [
{ "name": "SewerRat", "maxCount": 2, "probability": 100 },
{ "name": "Rat", "maxCount": 2, "probability": 100 },
{ "name": "BullFrog", "maxCount": 1, "probability": 100 }
{ "name": "Alligator", "maxCount": 8, "probability": 100 }
]
},
{
"type": "Spawner",
"guid": "ebace9c4-5941-4346-9ebf-6ca7ff7e33c5",
"$type": "Spawner",
"guid": "2fd77ead-e282-4216-aa6c-0273391bdd4e",
"name": "Spawner (202)",
"location": [6107, 1452, 25],
"location": [6090, 1483, 0],
"map": "Felucca",
"count": 3,
"count": 8,
"minDelay": "00:00:00",
"maxDelay": "00:01:00",
"team": 0,
"homeRange": 2,
"walkingRange": 15,
"homeRange": 5,
"walkingRange": 25,
"entries": [
{ "name": "SewerRat", "maxCount": 2, "probability": 100 },
{ "name": "Rat", "maxCount": 2, "probability": 100 },
{ "name": "BullFrog", "maxCount": 1, "probability": 100 }
{ "name": "Alligator", "maxCount": 8, "probability": 100 }
]
},
{
"type": "Spawner",
"$type": "Spawner",
"guid": "3237bbc2-29a2-488c-a918-d8db1f5e7f60",
"name": "Spawner (202)",
"location": [6087, 1443, 0],
"map": "Felucca",
"count": 2,
"minDelay": "00:00:00",
"maxDelay": "00:01:00",
"team": 0,
"homeRange": 4,
"walkingRange": 10,
"entries": [
{ "name": "Alligator", "maxCount": 2, "probability": 100 }
]
},
{
"$type": "Spawner",
"guid": "371c3545-e908-4d18-8dbb-3b86c422566a",
"name": "Spawner (202)",
"location": [6112, 1462, 5],
@ -126,10 +84,10 @@
]
},
{
"type": "Spawner",
"guid": "a9a6ccfe-d9b7-40f3-bfa8-d00994214056",
"$type": "Spawner",
"guid": "5da0bc6b-dfdb-47b8-a0df-0ff3eaf5c212",
"name": "Spawner (202)",
"location": [6039, 1438, 4],
"location": [6048, 1487, 5],
"map": "Felucca",
"count": 3,
"minDelay": "00:00:00",
@ -144,97 +102,7 @@
]
},
{
"type": "Spawner",
"guid": "a8680e6d-12db-4ef9-8107-9df6850893db",
"name": "Spawner (202)",
"location": [6115, 1491, -15],
"map": "Felucca",
"count": 3,
"minDelay": "00:00:00",
"maxDelay": "00:01:00",
"team": 0,
"homeRange": 2,
"walkingRange": 15,
"entries": [
{ "name": "SewerRat", "maxCount": 2, "probability": 100 },
{ "name": "Rat", "maxCount": 2, "probability": 100 },
{ "name": "BullFrog", "maxCount": 1, "probability": 100 }
]
},
{
"type": "Spawner",
"guid": "da9f7dcc-c64d-4a72-ba8a-b5846258a093",
"name": "Spawner (202)",
"location": [6084, 1470, 5],
"map": "Felucca",
"count": 3,
"minDelay": "00:00:00",
"maxDelay": "00:01:00",
"team": 0,
"homeRange": 1,
"walkingRange": 15,
"entries": [
{ "name": "SewerRat", "maxCount": 2, "probability": 100 },
{ "name": "Rat", "maxCount": 2, "probability": 100 },
{ "name": "BullFrog", "maxCount": 1, "probability": 100 }
]
},
{
"type": "Spawner",
"guid": "c93d730a-8c5a-4580-8952-14008d80b395",
"name": "Spawner (202)",
"location": [6049, 1470, 5],
"map": "Felucca",
"count": 3,
"minDelay": "00:00:00",
"maxDelay": "00:01:00",
"team": 0,
"homeRange": 2,
"walkingRange": 15,
"entries": [
{ "name": "SewerRat", "maxCount": 2, "probability": 100 },
{ "name": "Rat", "maxCount": 2, "probability": 100 },
{ "name": "BullFrog", "maxCount": 1, "probability": 100 }
]
},
{
"type": "Spawner",
"guid": "ba6cecc1-f3c1-4e95-a624-ac4d5d668265",
"name": "Spawner (202)",
"location": [6034, 1467, 5],
"map": "Felucca",
"count": 3,
"minDelay": "00:00:00",
"maxDelay": "00:01:00",
"team": 0,
"homeRange": 2,
"walkingRange": 15,
"entries": [
{ "name": "SewerRat", "maxCount": 2, "probability": 100 },
{ "name": "Rat", "maxCount": 2, "probability": 100 },
{ "name": "BullFrog", "maxCount": 1, "probability": 100 }
]
},
{
"type": "Spawner",
"guid": "75adb4cb-b0a7-44db-a5fe-d4e9b139ce7b",
"name": "Spawner (202)",
"location": [6059, 1438, 4],
"map": "Felucca",
"count": 3,
"minDelay": "00:00:00",
"maxDelay": "00:01:00",
"team": 0,
"homeRange": 2,
"walkingRange": 15,
"entries": [
{ "name": "SewerRat", "maxCount": 2, "probability": 100 },
{ "name": "Rat", "maxCount": 2, "probability": 100 },
{ "name": "BullFrog", "maxCount": 1, "probability": 100 }
]
},
{
"type": "Spawner",
"$type": "Spawner",
"guid": "62ba5343-d337-495e-aaf4-9dfb4d46c8b3",
"name": "Spawner (202)",
"location": [6061, 1490, 5],
@ -252,7 +120,183 @@
]
},
{
"type": "Spawner",
"$type": "Spawner",
"guid": "75adb4cb-b0a7-44db-a5fe-d4e9b139ce7b",
"name": "Spawner (202)",
"location": [6059, 1438, 4],
"map": "Felucca",
"count": 3,
"minDelay": "00:00:00",
"maxDelay": "00:01:00",
"team": 0,
"homeRange": 2,
"walkingRange": 15,
"entries": [
{ "name": "SewerRat", "maxCount": 2, "probability": 100 },
{ "name": "Rat", "maxCount": 2, "probability": 100 },
{ "name": "BullFrog", "maxCount": 1, "probability": 100 }
]
},
{
"$type": "Spawner",
"guid": "75e04bc3-078d-40c2-9ca8-d3a11a97965f",
"name": "Spawner (202)",
"location": [6108, 1471, 0],
"map": "Felucca",
"count": 2,
"minDelay": "00:00:00",
"maxDelay": "00:01:00",
"team": 0,
"homeRange": 4,
"walkingRange": 10,
"entries": [
{ "name": "Alligator", "maxCount": 2, "probability": 100 }
]
},
{
"$type": "Spawner",
"guid": "77bf6da3-ca32-446e-8d0b-239bb4520afd",
"name": "Spawner (202)",
"location": [6037, 1494, 0],
"map": "Felucca",
"count": 2,
"minDelay": "00:00:00",
"maxDelay": "00:01:00",
"team": 0,
"homeRange": 4,
"walkingRange": 10,
"entries": [
{ "name": "Alligator", "maxCount": 2, "probability": 100 }
]
},
{
"$type": "Spawner",
"guid": "8674d509-c36a-4ab8-9399-6329f1379984",
"name": "Spawner (202)",
"location": [6091, 1491, 10],
"map": "Felucca",
"count": 3,
"minDelay": "00:00:00",
"maxDelay": "00:01:00",
"team": 0,
"homeRange": 2,
"walkingRange": 15,
"entries": [
{ "name": "SewerRat", "maxCount": 2, "probability": 100 },
{ "name": "Rat", "maxCount": 2, "probability": 100 },
{ "name": "BullFrog", "maxCount": 1, "probability": 100 }
]
},
{
"$type": "Spawner",
"guid": "a8680e6d-12db-4ef9-8107-9df6850893db",
"name": "Spawner (202)",
"location": [6115, 1491, -15],
"map": "Felucca",
"count": 3,
"minDelay": "00:00:00",
"maxDelay": "00:01:00",
"team": 0,
"homeRange": 2,
"walkingRange": 15,
"entries": [
{ "name": "SewerRat", "maxCount": 2, "probability": 100 },
{ "name": "Rat", "maxCount": 2, "probability": 100 },
{ "name": "BullFrog", "maxCount": 1, "probability": 100 }
]
},
{
"$type": "Spawner",
"guid": "a9a6ccfe-d9b7-40f3-bfa8-d00994214056",
"name": "Spawner (202)",
"location": [6039, 1438, 4],
"map": "Felucca",
"count": 3,
"minDelay": "00:00:00",
"maxDelay": "00:01:00",
"team": 0,
"homeRange": 2,
"walkingRange": 15,
"entries": [
{ "name": "SewerRat", "maxCount": 2, "probability": 100 },
{ "name": "Rat", "maxCount": 2, "probability": 100 },
{ "name": "BullFrog", "maxCount": 1, "probability": 100 }
]
},
{
"$type": "Spawner",
"guid": "aca1fcd4-cc2b-4472-ab6e-89e0322f9fec",
"name": "Spawner (202)",
"location": [6049, 1447, 4],
"map": "Felucca",
"count": 3,
"minDelay": "00:00:00",
"maxDelay": "00:01:00",
"team": 0,
"homeRange": 2,
"walkingRange": 15,
"entries": [
{ "name": "SewerRat", "maxCount": 2, "probability": 100 },
{ "name": "Rat", "maxCount": 2, "probability": 100 },
{ "name": "BullFrog", "maxCount": 1, "probability": 100 }
]
},
{
"$type": "Spawner",
"guid": "b8273eff-be93-4ab9-8642-c1e9a92ddbbc",
"name": "Spawner (202)",
"location": [6092, 1448, 5],
"map": "Felucca",
"count": 3,
"minDelay": "00:00:00",
"maxDelay": "00:01:00",
"team": 0,
"homeRange": 2,
"walkingRange": 15,
"entries": [
{ "name": "SewerRat", "maxCount": 2, "probability": 100 },
{ "name": "Rat", "maxCount": 2, "probability": 100 },
{ "name": "BullFrog", "maxCount": 1, "probability": 100 }
]
},
{
"$type": "Spawner",
"guid": "ba6cecc1-f3c1-4e95-a624-ac4d5d668265",
"name": "Spawner (202)",
"location": [6034, 1467, 5],
"map": "Felucca",
"count": 3,
"minDelay": "00:00:00",
"maxDelay": "00:01:00",
"team": 0,
"homeRange": 2,
"walkingRange": 15,
"entries": [
{ "name": "SewerRat", "maxCount": 2, "probability": 100 },
{ "name": "Rat", "maxCount": 2, "probability": 100 },
{ "name": "BullFrog", "maxCount": 1, "probability": 100 }
]
},
{
"$type": "Spawner",
"guid": "c93d730a-8c5a-4580-8952-14008d80b395",
"name": "Spawner (202)",
"location": [6049, 1470, 5],
"map": "Felucca",
"count": 3,
"minDelay": "00:00:00",
"maxDelay": "00:01:00",
"team": 0,
"homeRange": 2,
"walkingRange": 15,
"entries": [
{ "name": "SewerRat", "maxCount": 2, "probability": 100 },
{ "name": "Rat", "maxCount": 2, "probability": 100 },
{ "name": "BullFrog", "maxCount": 1, "probability": 100 }
]
},
{
"$type": "Spawner",
"guid": "d0d1dbda-9b50-4646-9509-d5d32eb9f081",
"name": "Spawner (202)",
"location": [6041, 1486, 5],
@ -270,21 +314,25 @@
]
},
{
"type": "Spawner",
"guid": "2fd77ead-e282-4216-aa6c-0273391bdd4e",
"$type": "Spawner",
"guid": "da9f7dcc-c64d-4a72-ba8a-b5846258a093",
"name": "Spawner (202)",
"location": [6090, 1483, 0],
"location": [6084, 1470, 5],
"map": "Felucca",
"count": 8,
"count": 3,
"minDelay": "00:00:00",
"maxDelay": "00:01:00",
"team": 0,
"homeRange": 5,
"walkingRange": 25,
"entries": [{ "name": "Alligator", "maxCount": 8, "probability": 100 }]
"homeRange": 1,
"walkingRange": 15,
"entries": [
{ "name": "SewerRat", "maxCount": 2, "probability": 100 },
{ "name": "Rat", "maxCount": 2, "probability": 100 },
{ "name": "BullFrog", "maxCount": 1, "probability": 100 }
]
},
{
"type": "Spawner",
"$type": "Spawner",
"guid": "e8b1dc3e-cc13-4253-b228-12abe499061b",
"name": "Spawner (202)",
"location": [6077, 1492, 0],
@ -295,62 +343,26 @@
"team": 0,
"homeRange": 4,
"walkingRange": 10,
"entries": [{ "name": "Alligator", "maxCount": 2, "probability": 100 }]
"entries": [
{ "name": "Alligator", "maxCount": 2, "probability": 100 }
]
},
{
"type": "Spawner",
"guid": "75e04bc3-078d-40c2-9ca8-d3a11a97965f",
"$type": "Spawner",
"guid": "ebace9c4-5941-4346-9ebf-6ca7ff7e33c5",
"name": "Spawner (202)",
"location": [6108, 1471, 0],
"location": [6107, 1452, 25],
"map": "Felucca",
"count": 2,
"count": 3,
"minDelay": "00:00:00",
"maxDelay": "00:01:00",
"team": 0,
"homeRange": 4,
"walkingRange": 10,
"entries": [{ "name": "Alligator", "maxCount": 2, "probability": 100 }]
},
{
"type": "Spawner",
"guid": "3237bbc2-29a2-488c-a918-d8db1f5e7f60",
"name": "Spawner (202)",
"location": [6087, 1443, 0],
"map": "Felucca",
"count": 2,
"minDelay": "00:00:00",
"maxDelay": "00:01:00",
"team": 0,
"homeRange": 4,
"walkingRange": 10,
"entries": [{ "name": "Alligator", "maxCount": 2, "probability": 100 }]
},
{
"type": "Spawner",
"guid": "099f6832-75ef-4f46-91b2-0d64509897c2",
"name": "Spawner (202)",
"location": [6052, 1463, 0],
"map": "Felucca",
"count": 8,
"minDelay": "00:00:00",
"maxDelay": "00:01:00",
"team": 0,
"homeRange": 5,
"walkingRange": 25,
"entries": [{ "name": "Alligator", "maxCount": 8, "probability": 100 }]
},
{
"type": "Spawner",
"guid": "77bf6da3-ca32-446e-8d0b-239bb4520afd",
"name": "Spawner (202)",
"location": [6037, 1494, 0],
"map": "Felucca",
"count": 2,
"minDelay": "00:00:00",
"maxDelay": "00:01:00",
"team": 0,
"homeRange": 4,
"walkingRange": 10,
"entries": [{ "name": "Alligator", "maxCount": 2, "probability": 100 }]
"homeRange": 2,
"walkingRange": 15,
"entries": [
{ "name": "SewerRat", "maxCount": 2, "probability": 100 },
{ "name": "Rat", "maxCount": 2, "probability": 100 },
{ "name": "BullFrog", "maxCount": 1, "probability": 100 }
]
}
]

View file

@ -1,24 +1,6 @@
[
{
"type": "Spawner",
"guid": "e8017d92-b455-4ab2-b484-f95b2957b3be",
"name": "Spawner (206)",
"location": [5209, 965, -40],
"map": "Felucca",
"count": 4,
"minDelay": "00:05:00",
"maxDelay": "00:10:00",
"team": 0,
"homeRange": 30,
"walkingRange": 50,
"entries": [
{ "name": "Dragon", "maxCount": 1, "probability": 100 },
{ "name": "Drake", "maxCount": 2, "probability": 100 },
{ "name": "Wyvern", "maxCount": 1, "probability": 100 }
]
},
{
"type": "Spawner",
"$type": "Spawner",
"guid": "0fbce341-ca59-4ec4-a718-cc8f7b3ed34e",
"name": "Spawner (206)",
"location": [5247, 956, -40],
@ -35,92 +17,23 @@
]
},
{
"type": "Spawner",
"guid": "79707431-23e6-4347-a89e-7bc970162938",
"$type": "Spawner",
"guid": "171e7677-5e0b-4da4-8599-9c3c16200e7a",
"name": "Spawner (206)",
"location": [5280, 955, -40],
"location": [5141, 968, 0],
"map": "Felucca",
"count": 4,
"minDelay": "00:05:00",
"maxDelay": "00:10:00",
"team": 0,
"homeRange": 30,
"walkingRange": 50,
"entries": [
{ "name": "Dragon", "maxCount": 1, "probability": 100 },
{ "name": "Drake", "maxCount": 2, "probability": 100 },
{ "name": "Wyvern", "maxCount": 1, "probability": 100 }
]
},
{
"type": "Spawner",
"guid": "7dfd256b-c903-4a4f-abf1-bdce7ff2217b",
"name": "Spawner (206)",
"location": [5284, 926, -40],
"map": "Felucca",
"count": 4,
"minDelay": "00:05:00",
"maxDelay": "00:10:00",
"team": 0,
"homeRange": 30,
"walkingRange": 50,
"entries": [
{ "name": "Dragon", "maxCount": 1, "probability": 100 },
{ "name": "Drake", "maxCount": 2, "probability": 100 },
{ "name": "Wyvern", "maxCount": 1, "probability": 100 }
]
},
{
"type": "Spawner",
"guid": "889ccd57-8eda-4910-9790-2dc3bc80ca99",
"name": "Spawner (206)",
"location": [5214, 917, -40],
"map": "Felucca",
"count": 4,
"minDelay": "00:05:00",
"maxDelay": "00:10:00",
"team": 0,
"homeRange": 30,
"walkingRange": 50,
"entries": [
{ "name": "Dragon", "maxCount": 1, "probability": 100 },
{ "name": "Drake", "maxCount": 2, "probability": 100 },
{ "name": "Wyvern", "maxCount": 1, "probability": 100 }
]
},
{
"type": "Spawner",
"guid": "3e318f8e-aec0-4b16-b435-43dc4fde4372",
"name": "Spawner (206)",
"location": [5241, 945, -40],
"map": "Felucca",
"count": 3,
"count": 1,
"minDelay": "00:05:00",
"maxDelay": "00:10:00",
"team": 0,
"homeRange": 10,
"walkingRange": 10,
"entries": [
{ "name": "TreasureChestLevel3", "maxCount": 1, "probability": 100 },
{ "name": "TreasureChestLevel4", "maxCount": 2, "probability": 100 }
{ "name": "FireElemental", "maxCount": 1, "probability": 100 }
]
},
{
"type": "Spawner",
"guid": "f5973fd2-89b7-405a-bdea-4b1f2fafa92d",
"name": "Spawner (206)",
"location": [5161, 940, 0],
"map": "Felucca",
"count": 6,
"minDelay": "00:05:00",
"maxDelay": "00:10:00",
"team": 0,
"homeRange": 20,
"walkingRange": 50,
"entries": [{ "name": "GiantSerpent", "maxCount": 6, "probability": 100 }]
},
{
"type": "Spawner",
"$type": "Spawner",
"guid": "326c64f4-34d3-414d-a79c-c7bb9d441e96",
"name": "Spawner (206)",
"location": [5149, 907, 0],
@ -138,87 +51,24 @@
]
},
{
"type": "Spawner",
"guid": "ef6d6675-0683-47e4-b236-bfc890a12227",
"$type": "Spawner",
"guid": "3e318f8e-aec0-4b16-b435-43dc4fde4372",
"name": "Spawner (206)",
"location": [5141, 915, 0],
"location": [5241, 945, -40],
"map": "Felucca",
"count": 3,
"minDelay": "00:05:00",
"maxDelay": "00:10:00",
"team": 0,
"homeRange": 25,
"walkingRange": 25,
"entries": [
{ "name": "TreasureChestLevel2", "maxCount": 1, "probability": 100 },
{ "name": "TreasureChestLevel3", "maxCount": 2, "probability": 100 }
]
},
{
"type": "Spawner",
"guid": "4f8eb77e-64cb-4846-8c0f-0816f16c94b3",
"name": "Spawner (206)",
"location": [5352, 934, -5],
"map": "Felucca",
"count": 4,
"minDelay": "00:05:00",
"maxDelay": "00:10:00",
"team": 0,
"homeRange": 10,
"walkingRange": 10,
"entries": [{ "name": "WaterElemental", "maxCount": 4, "probability": 100 }]
},
{
"type": "Spawner",
"guid": "d045c909-34f1-46a6-8d61-7275dc6e45c9",
"name": "Spawner (206)",
"location": [5317, 980, 0],
"map": "Felucca",
"count": 3,
"minDelay": "00:05:00",
"maxDelay": "00:10:00",
"team": 0,
"homeRange": 7,
"walkingRange": 7,
"entries": [
{ "name": "TreasureChestLevel3", "maxCount": 1, "probability": 100 },
{ "name": "TreasureChestLevel4", "maxCount": 2, "probability": 100 }
]
},
{
"type": "Spawner",
"guid": "e2bea874-3646-4dec-8fc9-cbc0c67f2a0a",
"name": "Spawner (206)",
"location": [5250, 869, 0],
"map": "Felucca",
"count": 6,
"minDelay": "00:05:00",
"maxDelay": "00:10:00",
"team": 0,
"homeRange": 30,
"walkingRange": 50,
"entries": [{ "name": "GiantSerpent", "maxCount": 6, "probability": 100 }]
},
{
"type": "Spawner",
"guid": "87114b5e-ac9d-4d7f-90c1-c6db7a570c2f",
"name": "Spawner (206)",
"location": [5226, 826, 0],
"map": "Felucca",
"count": 4,
"minDelay": "00:05:00",
"maxDelay": "00:10:00",
"team": 0,
"homeRange": 30,
"walkingRange": 50,
"entries": [
{ "name": "Dragon", "maxCount": 1, "probability": 100 },
{ "name": "Drake", "maxCount": 2, "probability": 100 },
{ "name": "Wyvern", "maxCount": 1, "probability": 100 }
]
},
{
"type": "Spawner",
"$type": "Spawner",
"guid": "48b37663-ddee-4f04-978d-35f4c085f785",
"name": "Spawner (206)",
"location": [5269, 811, 7],
@ -236,7 +86,161 @@
]
},
{
"type": "Spawner",
"$type": "Spawner",
"guid": "4f8eb77e-64cb-4846-8c0f-0816f16c94b3",
"name": "Spawner (206)",
"location": [5352, 934, -5],
"map": "Felucca",
"count": 4,
"minDelay": "00:05:00",
"maxDelay": "00:10:00",
"team": 0,
"homeRange": 10,
"walkingRange": 10,
"entries": [
{ "name": "WaterElemental", "maxCount": 4, "probability": 100 }
]
},
{
"$type": "Spawner",
"guid": "506ce404-853e-4168-9b6b-1296d40c90e3",
"name": "Spawner (206)",
"location": [5183, 1007, 0],
"map": "Felucca",
"count": 3,
"minDelay": "00:05:00",
"maxDelay": "00:10:00",
"team": 0,
"homeRange": 10,
"walkingRange": 10,
"entries": [
{ "name": "TreasureChestLevel3", "maxCount": 1, "probability": 100 },
{ "name": "TreasureChestLevel4", "maxCount": 2, "probability": 100 }
]
},
{
"$type": "Spawner",
"guid": "59c245e9-e59f-4f6c-b3cb-2add65aba939",
"name": "Spawner (206)",
"location": [5203, 787, 5],
"map": "Felucca",
"count": 4,
"minDelay": "00:05:00",
"maxDelay": "00:10:00",
"team": 0,
"homeRange": 12,
"walkingRange": 25,
"entries": [
{ "name": "WaterElemental", "maxCount": 4, "probability": 100 }
]
},
{
"$type": "Spawner",
"guid": "79707431-23e6-4347-a89e-7bc970162938",
"name": "Spawner (206)",
"location": [5280, 955, -40],
"map": "Felucca",
"count": 4,
"minDelay": "00:05:00",
"maxDelay": "00:10:00",
"team": 0,
"homeRange": 30,
"walkingRange": 50,
"entries": [
{ "name": "Dragon", "maxCount": 1, "probability": 100 },
{ "name": "Drake", "maxCount": 2, "probability": 100 },
{ "name": "Wyvern", "maxCount": 1, "probability": 100 }
]
},
{
"$type": "Spawner",
"guid": "7dfd256b-c903-4a4f-abf1-bdce7ff2217b",
"name": "Spawner (206)",
"location": [5284, 926, -40],
"map": "Felucca",
"count": 4,
"minDelay": "00:05:00",
"maxDelay": "00:10:00",
"team": 0,
"homeRange": 30,
"walkingRange": 50,
"entries": [
{ "name": "Dragon", "maxCount": 1, "probability": 100 },
{ "name": "Drake", "maxCount": 2, "probability": 100 },
{ "name": "Wyvern", "maxCount": 1, "probability": 100 }
]
},
{
"$type": "Spawner",
"guid": "831f9d08-398c-42b0-bc53-72e1c0685e34",
"name": "Spawner (206)",
"location": [5294, 842, 0],
"map": "Felucca",
"count": 4,
"minDelay": "00:05:00",
"maxDelay": "00:10:00",
"team": 0,
"homeRange": 30,
"walkingRange": 50,
"entries": [
{ "name": "WaterElemental", "maxCount": 4, "probability": 100 }
]
},
{
"$type": "Spawner",
"guid": "87114b5e-ac9d-4d7f-90c1-c6db7a570c2f",
"name": "Spawner (206)",
"location": [5226, 826, 0],
"map": "Felucca",
"count": 4,
"minDelay": "00:05:00",
"maxDelay": "00:10:00",
"team": 0,
"homeRange": 30,
"walkingRange": 50,
"entries": [
{ "name": "Dragon", "maxCount": 1, "probability": 100 },
{ "name": "Drake", "maxCount": 2, "probability": 100 },
{ "name": "Wyvern", "maxCount": 1, "probability": 100 }
]
},
{
"$type": "Spawner",
"guid": "889ccd57-8eda-4910-9790-2dc3bc80ca99",
"name": "Spawner (206)",
"location": [5214, 917, -40],
"map": "Felucca",
"count": 4,
"minDelay": "00:05:00",
"maxDelay": "00:10:00",
"team": 0,
"homeRange": 30,
"walkingRange": 50,
"entries": [
{ "name": "Dragon", "maxCount": 1, "probability": 100 },
{ "name": "Drake", "maxCount": 2, "probability": 100 },
{ "name": "Wyvern", "maxCount": 1, "probability": 100 }
]
},
{
"$type": "Spawner",
"guid": "8b681dd7-a815-4513-85e6-901967f4e6ed",
"name": "Spawner (206)",
"location": [5143, 986, 0],
"map": "Felucca",
"count": 3,
"minDelay": "00:05:00",
"maxDelay": "00:10:00",
"team": 0,
"homeRange": 10,
"walkingRange": 10,
"entries": [
{ "name": "TreasureChestLevel3", "maxCount": 1, "probability": 100 },
{ "name": "TreasureChestLevel4", "maxCount": 2, "probability": 100 }
]
},
{
"$type": "Spawner",
"guid": "936bde4a-dce6-44bb-859c-81a3e1f0a87b",
"name": "Spawner (206)",
"location": [5307, 815, 0],
@ -253,52 +257,7 @@
]
},
{
"type": "Spawner",
"guid": "831f9d08-398c-42b0-bc53-72e1c0685e34",
"name": "Spawner (206)",
"location": [5294, 842, 0],
"map": "Felucca",
"count": 4,
"minDelay": "00:05:00",
"maxDelay": "00:10:00",
"team": 0,
"homeRange": 30,
"walkingRange": 50,
"entries": [{ "name": "WaterElemental", "maxCount": 4, "probability": 100 }]
},
{
"type": "Spawner",
"guid": "59c245e9-e59f-4f6c-b3cb-2add65aba939",
"name": "Spawner (206)",
"location": [5203, 787, 5],
"map": "Felucca",
"count": 4,
"minDelay": "00:05:00",
"maxDelay": "00:10:00",
"team": 0,
"homeRange": 12,
"walkingRange": 25,
"entries": [{ "name": "WaterElemental", "maxCount": 4, "probability": 100 }]
},
{
"type": "Spawner",
"guid": "b6125064-8514-4c32-953d-c28eca82fa4e",
"name": "Spawner (206)",
"location": [5229, 841, 1],
"map": "Felucca",
"count": 3,
"minDelay": "00:05:00",
"maxDelay": "00:10:00",
"team": 0,
"homeRange": 10,
"walkingRange": 10,
"entries": [
{ "name": "TreasureChestLevel2", "maxCount": 1, "probability": 100 },
{ "name": "TreasureChestLevel3", "maxCount": 2, "probability": 100 }
]
},
{
"type": "Spawner",
"$type": "Spawner",
"guid": "9641ac5e-6eec-4f35-8536-226265c67ace",
"name": "Spawner (206)",
"location": [5205, 778, 0],
@ -309,95 +268,12 @@
"team": 0,
"homeRange": 7,
"walkingRange": 7,
"entries": [{ "name": "TreasureChestLevel3", "maxCount": 1, "probability": 100 }]
},
{
"type": "Spawner",
"guid": "be7b487d-c201-413a-94c8-8201dc3a1c4d",
"name": "Spawner (206)",
"location": [5141, 834, 0],
"map": "Felucca",
"count": 3,
"minDelay": "00:05:00",
"maxDelay": "00:10:00",
"team": 0,
"homeRange": 30,
"walkingRange": 50,
"entries": [
{ "name": "Drake", "maxCount": 1, "probability": 100 },
{ "name": "ShadowWyrm", "maxCount": 1, "probability": 100 },
{ "name": "Wyvern", "maxCount": 1, "probability": 100 }
{ "name": "TreasureChestLevel3", "maxCount": 1, "probability": 100 }
]
},
{
"type": "Spawner",
"guid": "edd4aa65-92a9-491c-97e8-d9906e31fbea",
"name": "Spawner (206)",
"location": [5169, 836, 0],
"map": "Felucca",
"count": 3,
"minDelay": "00:05:00",
"maxDelay": "00:10:00",
"team": 0,
"homeRange": 30,
"walkingRange": 50,
"entries": [
{ "name": "Daemon", "maxCount": 2, "probability": 100 },
{ "name": "EvilMage", "maxCount": 1, "probability": 100 }
]
},
{
"type": "Spawner",
"guid": "f2aa7cb3-ff27-4ea9-a2a4-fe62a0e78b76",
"name": "Spawner (206)",
"location": [5152, 869, 0],
"map": "Felucca",
"count": 3,
"minDelay": "00:05:00",
"maxDelay": "00:10:00",
"team": 0,
"homeRange": 25,
"walkingRange": 50,
"entries": [
{ "name": "Drake", "maxCount": 1, "probability": 100 },
{ "name": "ShadowWyrm", "maxCount": 1, "probability": 100 },
{ "name": "Wyvern", "maxCount": 1, "probability": 100 }
]
},
{
"type": "Spawner",
"guid": "d8aceeb2-7644-4b3d-96de-4e172f41abd6",
"name": "Spawner (206)",
"location": [5153, 842, 0],
"map": "Felucca",
"count": 4,
"minDelay": "00:05:00",
"maxDelay": "00:10:00",
"team": 0,
"homeRange": 40,
"walkingRange": 40,
"entries": [
{ "name": "TreasureChestLevel4", "maxCount": 4, "probability": 100 },
{ "name": "TreasureChestLevel3", "maxCount": 4, "probability": 100 },
{ "name": "TreasureChestLevel2", "maxCount": 4, "probability": 100 }
]
},
{
"type": "Spawner",
"guid": "171e7677-5e0b-4da4-8599-9c3c16200e7a",
"name": "Spawner (206)",
"location": [5141, 968, 0],
"map": "Felucca",
"count": 1,
"minDelay": "00:05:00",
"maxDelay": "00:10:00",
"team": 0,
"homeRange": 10,
"walkingRange": 10,
"entries": [{ "name": "FireElemental", "maxCount": 1, "probability": 100 }]
},
{
"type": "Spawner",
"$type": "Spawner",
"guid": "ad446354-3f49-4c9d-ab1d-dea29378794e",
"name": "Spawner (206)",
"location": [5157, 997, 0],
@ -415,7 +291,93 @@
]
},
{
"type": "Spawner",
"$type": "Spawner",
"guid": "b6125064-8514-4c32-953d-c28eca82fa4e",
"name": "Spawner (206)",
"location": [5229, 841, 1],
"map": "Felucca",
"count": 3,
"minDelay": "00:05:00",
"maxDelay": "00:10:00",
"team": 0,
"homeRange": 10,
"walkingRange": 10,
"entries": [
{ "name": "TreasureChestLevel2", "maxCount": 1, "probability": 100 },
{ "name": "TreasureChestLevel3", "maxCount": 2, "probability": 100 }
]
},
{
"$type": "Spawner",
"guid": "be7b487d-c201-413a-94c8-8201dc3a1c4d",
"name": "Spawner (206)",
"location": [5141, 834, 0],
"map": "Felucca",
"count": 3,
"minDelay": "00:05:00",
"maxDelay": "00:10:00",
"team": 0,
"homeRange": 30,
"walkingRange": 50,
"entries": [
{ "name": "Drake", "maxCount": 1, "probability": 100 },
{ "name": "ShadowWyrm", "maxCount": 1, "probability": 100 },
{ "name": "Wyvern", "maxCount": 1, "probability": 100 }
]
},
{
"$type": "Spawner",
"guid": "d045c909-34f1-46a6-8d61-7275dc6e45c9",
"name": "Spawner (206)",
"location": [5317, 980, 0],
"map": "Felucca",
"count": 3,
"minDelay": "00:05:00",
"maxDelay": "00:10:00",
"team": 0,
"homeRange": 7,
"walkingRange": 7,
"entries": [
{ "name": "TreasureChestLevel3", "maxCount": 1, "probability": 100 },
{ "name": "TreasureChestLevel4", "maxCount": 2, "probability": 100 }
]
},
{
"$type": "Spawner",
"guid": "d8aceeb2-7644-4b3d-96de-4e172f41abd6",
"name": "Spawner (206)",
"location": [5153, 842, 0],
"map": "Felucca",
"count": 4,
"minDelay": "00:05:00",
"maxDelay": "00:10:00",
"team": 0,
"homeRange": 40,
"walkingRange": 40,
"entries": [
{ "name": "TreasureChestLevel4", "maxCount": 4, "probability": 100 },
{ "name": "TreasureChestLevel3", "maxCount": 4, "probability": 100 },
{ "name": "TreasureChestLevel2", "maxCount": 4, "probability": 100 }
]
},
{
"$type": "Spawner",
"guid": "e2bea874-3646-4dec-8fc9-cbc0c67f2a0a",
"name": "Spawner (206)",
"location": [5250, 869, 0],
"map": "Felucca",
"count": 6,
"minDelay": "00:05:00",
"maxDelay": "00:10:00",
"team": 0,
"homeRange": 30,
"walkingRange": 50,
"entries": [
{ "name": "GiantSerpent", "maxCount": 6, "probability": 100 }
]
},
{
"$type": "Spawner",
"guid": "e6703676-d851-4394-b09f-51286d5d85f2",
"name": "Spawner (206)",
"location": [5185, 1006, 0],
@ -426,40 +388,94 @@
"team": 0,
"homeRange": 20,
"walkingRange": 50,
"entries": [{ "name": "AncientWyrm", "maxCount": 1, "probability": 100 }]
},
{
"type": "Spawner",
"guid": "8b681dd7-a815-4513-85e6-901967f4e6ed",
"name": "Spawner (206)",
"location": [5143, 986, 0],
"map": "Felucca",
"count": 3,
"minDelay": "00:05:00",
"maxDelay": "00:10:00",
"team": 0,
"homeRange": 10,
"walkingRange": 10,
"entries": [
{ "name": "TreasureChestLevel3", "maxCount": 1, "probability": 100 },
{ "name": "TreasureChestLevel4", "maxCount": 2, "probability": 100 }
{ "name": "AncientWyrm", "maxCount": 1, "probability": 100 }
]
},
{
"type": "Spawner",
"guid": "506ce404-853e-4168-9b6b-1296d40c90e3",
"$type": "Spawner",
"guid": "e8017d92-b455-4ab2-b484-f95b2957b3be",
"name": "Spawner (206)",
"location": [5183, 1007, 0],
"location": [5209, 965, -40],
"map": "Felucca",
"count": 4,
"minDelay": "00:05:00",
"maxDelay": "00:10:00",
"team": 0,
"homeRange": 30,
"walkingRange": 50,
"entries": [
{ "name": "Dragon", "maxCount": 1, "probability": 100 },
{ "name": "Drake", "maxCount": 2, "probability": 100 },
{ "name": "Wyvern", "maxCount": 1, "probability": 100 }
]
},
{
"$type": "Spawner",
"guid": "edd4aa65-92a9-491c-97e8-d9906e31fbea",
"name": "Spawner (206)",
"location": [5169, 836, 0],
"map": "Felucca",
"count": 3,
"minDelay": "00:05:00",
"maxDelay": "00:10:00",
"team": 0,
"homeRange": 10,
"walkingRange": 10,
"homeRange": 30,
"walkingRange": 50,
"entries": [
{ "name": "TreasureChestLevel3", "maxCount": 1, "probability": 100 },
{ "name": "TreasureChestLevel4", "maxCount": 2, "probability": 100 }
{ "name": "Daemon", "maxCount": 2, "probability": 100 },
{ "name": "EvilMage", "maxCount": 1, "probability": 100 }
]
},
{
"$type": "Spawner",
"guid": "ef6d6675-0683-47e4-b236-bfc890a12227",
"name": "Spawner (206)",
"location": [5141, 915, 0],
"map": "Felucca",
"count": 3,
"minDelay": "00:05:00",
"maxDelay": "00:10:00",
"team": 0,
"homeRange": 25,
"walkingRange": 25,
"entries": [
{ "name": "TreasureChestLevel2", "maxCount": 1, "probability": 100 },
{ "name": "TreasureChestLevel3", "maxCount": 2, "probability": 100 }
]
},
{
"$type": "Spawner",
"guid": "f2aa7cb3-ff27-4ea9-a2a4-fe62a0e78b76",
"name": "Spawner (206)",
"location": [5152, 869, 0],
"map": "Felucca",
"count": 3,
"minDelay": "00:05:00",
"maxDelay": "00:10:00",
"team": 0,
"homeRange": 25,
"walkingRange": 50,
"entries": [
{ "name": "Drake", "maxCount": 1, "probability": 100 },
{ "name": "ShadowWyrm", "maxCount": 1, "probability": 100 },
{ "name": "Wyvern", "maxCount": 1, "probability": 100 }
]
},
{
"$type": "Spawner",
"guid": "f5973fd2-89b7-405a-bdea-4b1f2fafa92d",
"name": "Spawner (206)",
"location": [5161, 940, 0],
"map": "Felucca",
"count": 6,
"minDelay": "00:05:00",
"maxDelay": "00:10:00",
"team": 0,
"homeRange": 20,
"walkingRange": 50,
"entries": [
{ "name": "GiantSerpent", "maxCount": 6, "probability": 100 }
]
}
]

View file

@ -0,0 +1,338 @@
[
{
"$type": "RegionSpawner",
"guid": "0c48edf9-a88e-45f1-9ba3-cd8c919aefbc",
"name": "Spawner",
"location": [823, 2274, 0],
"map": "Felucca",
"count": 6,
"minDelay": "00:00:10",
"maxDelay": "00:00:30",
"entries": [
{ "name": "FarmableCabbage", "maxCount": 6, "probability": 100 }
],
"region": "A Cabbage Field in Skara Brae 2"
},
{
"$type": "RegionSpawner",
"guid": "0c6481ab-3341-4113-ac1a-6523c19c881d",
"name": "Spawner",
"location": [1216, 1604, 0],
"map": "Felucca",
"count": 6,
"minDelay": "00:00:10",
"maxDelay": "00:00:30",
"entries": [
{ "name": "FarmableTurnip", "maxCount": 6, "probability": 100 }
],
"region": "A Turnip Field in Britain 2"
},
{
"$type": "RegionSpawner",
"guid": "2bf378f0-77ed-44cd-8dfb-181d068f401e",
"name": "Spawner",
"location": [843, 2352, 0],
"map": "Felucca",
"count": 6,
"minDelay": "00:00:10",
"maxDelay": "00:00:30",
"entries": [
{ "name": "FarmableWheat", "maxCount": 6, "probability": 100 }
],
"region": "A Wheatfield in Skara Brae 2"
},
{
"$type": "RegionSpawner",
"guid": "3d7925bf-2642-4185-bc03-fa314fb75a7f",
"name": "Spawner",
"location": [675, 1179, 0],
"map": "Felucca",
"count": 10,
"minDelay": "00:00:10",
"maxDelay": "00:00:30",
"entries": [
{ "name": "Sheep", "maxCount": 10, "probability": 100 }
],
"region": "A Field of Sheep in Yew 2"
},
{
"$type": "RegionSpawner",
"guid": "425ac125-1597-41b4-9e6d-72b91801230b",
"name": "Spawner",
"location": [675, 939, 0],
"map": "Felucca",
"count": 10,
"minDelay": "00:00:10",
"maxDelay": "00:00:30",
"entries": [
{ "name": "Sheep", "maxCount": 10, "probability": 100 }
],
"region": "A Field of Sheep in Yew 1"
},
{
"$type": "RegionSpawner",
"guid": "427451dd-93fa-43c9-85c6-01536066c8cc",
"name": "Spawner",
"location": [823, 2254, 0],
"map": "Felucca",
"count": 6,
"minDelay": "00:00:10",
"maxDelay": "00:00:30",
"entries": [
{ "name": "FarmableCarrot", "maxCount": 6, "probability": 100 }
],
"region": "A Carrot Field in Skara Brae"
},
{
"$type": "RegionSpawner",
"guid": "4b4e3973-d836-4f7c-b9f7-5fb7d468da28",
"name": "Spawner",
"location": [4567, 1475, 0],
"map": "Felucca",
"count": 8,
"minDelay": "00:00:10",
"maxDelay": "00:00:30",
"entries": [
{ "name": "FarmableCotton", "maxCount": 8, "probability": 100 }
],
"region": "A Cotton Field in Moonglow"
},
{
"$type": "RegionSpawner",
"guid": "57da6436-6b16-4697-add5-f4b45776ccbc",
"name": "Spawner",
"location": [813, 2163, 0],
"map": "Felucca",
"count": 6,
"minDelay": "00:00:10",
"maxDelay": "00:00:30",
"entries": [
{ "name": "FarmableWheat", "maxCount": 6, "probability": 100 }
],
"region": "A Wheatfield in Skara Brae 1"
},
{
"$type": "RegionSpawner",
"guid": "57f955bb-49e1-4a66-a214-4fbddf8d572b",
"name": "Spawner",
"location": [1231, 1723, 0],
"map": "Felucca",
"count": 8,
"minDelay": "00:00:10",
"maxDelay": "00:00:30",
"entries": [
{ "name": "FarmableOnion", "maxCount": 8, "probability": 100 }
],
"region": "An Onion Field in Britain 1"
},
{
"$type": "RegionSpawner",
"guid": "58b14343-2032-4507-a913-6b536507c2e5",
"name": "Spawner",
"location": [1199, 1823, 0],
"map": "Felucca",
"count": 8,
"minDelay": "00:00:10",
"maxDelay": "00:00:30",
"entries": [
{ "name": "FarmableWheat", "maxCount": 8, "probability": 100 }
],
"region": "A Wheatfield in Britain 2"
},
{
"$type": "RegionSpawner",
"guid": "640fe3f8-9917-4dfa-986d-038923163add",
"name": "Spawner",
"location": [823, 2264, 0],
"map": "Felucca",
"count": 6,
"minDelay": "00:00:10",
"maxDelay": "00:00:30",
"entries": [
{ "name": "FarmableOnion", "maxCount": 6, "probability": 100 }
],
"region": "An Onion Field in Skara Brae"
},
{
"$type": "RegionSpawner",
"guid": "7b09d4b5-fe63-4f95-8525-3c970c513adc",
"name": "Spawner",
"location": [1232, 1604, 0],
"map": "Felucca",
"count": 6,
"minDelay": "00:00:10",
"maxDelay": "00:00:30",
"entries": [
{ "name": "FarmableCarrot", "maxCount": 6, "probability": 100 }
],
"region": "A Carrot Field in Britain 2"
},
{
"$type": "RegionSpawner",
"guid": "8536c19c-2036-495a-a3c5-e82c9f32f5ad",
"name": "Spawner",
"location": [1183, 1683, 0],
"map": "Felucca",
"count": 8,
"minDelay": "00:00:10",
"maxDelay": "00:00:30",
"entries": [
{ "name": "FarmableCabbage", "maxCount": 8, "probability": 100 }
],
"region": "A Cabbage Field in Britain 1"
},
{
"$type": "RegionSpawner",
"guid": "9805ef58-355f-4352-bfc4-43c6710ff409",
"name": "Spawner",
"location": [1215, 1723, 0],
"map": "Felucca",
"count": 8,
"minDelay": "00:00:10",
"maxDelay": "00:00:30",
"entries": [
{ "name": "FarmableCarrot", "maxCount": 8, "probability": 100 }
],
"region": "A Carrot Field in Britain 1"
},
{
"$type": "RegionSpawner",
"guid": "aed692cc-2dac-4c80-931b-35c73ca8d092",
"name": "Spawner",
"location": [571, 1099, 0],
"map": "Felucca",
"count": 10,
"minDelay": "00:00:10",
"maxDelay": "00:00:30",
"entries": [
{ "name": "Sheep", "maxCount": 10, "probability": 100 }
],
"region": "A Farm in Yew"
},
{
"$type": "RegionSpawner",
"guid": "b29b54ab-5ae3-4ce3-93ac-326f324a34f6",
"name": "Spawner",
"location": [1135, 1791, 0],
"map": "Felucca",
"count": 8,
"minDelay": "00:00:10",
"maxDelay": "00:00:30",
"entries": [
{ "name": "FarmableWheat", "maxCount": 8, "probability": 100 }
],
"region": "A Wheatfield in Britain 1"
},
{
"$type": "RegionSpawner",
"guid": "bba64d97-11eb-4a87-b326-e41e3c9b355d",
"name": "Spawner",
"location": [375, 1191, 0],
"map": "Felucca",
"count": 6,
"minDelay": "00:00:10",
"maxDelay": "00:00:30",
"entries": [
{ "name": "FarmableWheat", "maxCount": 6, "probability": 100 }
],
"region": "A Wheatfield in Yew 2"
},
{
"$type": "RegionSpawner",
"guid": "d8a2e54e-4951-468f-93c5-376460070cba",
"name": "Spawner",
"location": [1199, 1683, 0],
"map": "Felucca",
"count": 8,
"minDelay": "00:00:10",
"maxDelay": "00:00:30",
"entries": [
{ "name": "FarmableTurnip", "maxCount": 8, "probability": 100 }
],
"region": "A Turnip Field in Britain 1"
},
{
"$type": "RegionSpawner",
"guid": "e37b4d74-c87e-4102-8c87-8f80c728fe0c",
"name": "Spawner",
"location": [567, 1239, 0],
"map": "Felucca",
"count": 5,
"minDelay": "00:00:10",
"maxDelay": "00:00:30",
"entries": [
{ "name": "FarmableWheat", "maxCount": 5, "probability": 100 }
],
"region": "A Wheatfield in Yew 1"
},
{
"$type": "RegionSpawner",
"guid": "e416e9fa-5a9a-426b-962e-50cdce64ca7f",
"name": "Spawner",
"location": [1152, 1576, 0],
"map": "Felucca",
"count": 8,
"minDelay": "00:00:10",
"maxDelay": "00:00:30",
"entries": [
{ "name": "FarmableWheat", "maxCount": 8, "probability": 100 }
],
"region": "A Wheatfield in Britain 5"
},
{
"$type": "RegionSpawner",
"guid": "e6a4aede-5a76-473d-8331-4bb73701ea1a",
"name": "Spawner",
"location": [1120, 1624, 0],
"map": "Felucca",
"count": 8,
"minDelay": "00:00:10",
"maxDelay": "00:00:30",
"entries": [
{ "name": "FarmableWheat", "maxCount": 8, "probability": 100 }
],
"region": "A Wheatfield in Britain 4"
},
{
"$type": "RegionSpawner",
"guid": "e7b34039-ec1f-43c8-b64f-d7462ce7ae9d",
"name": "Spawner",
"location": [823, 2355, 0],
"map": "Felucca",
"count": 6,
"minDelay": "00:00:10",
"maxDelay": "00:00:30",
"entries": [
{ "name": "FarmableCotton", "maxCount": 6, "probability": 100 }
],
"region": "A Cotton Field in Skara Brae"
},
{
"$type": "RegionSpawner",
"guid": "f4bccdc2-c663-4ccd-b7df-31656fa2ae09",
"name": "Spawner",
"location": [823, 2264, 0],
"map": "Felucca",
"count": 6,
"minDelay": "00:00:10",
"maxDelay": "00:00:30",
"entries": [
{ "name": "FarmableCabbage", "maxCount": 6, "probability": 100 }
],
"region": "A Cabbage Field in Skara Brae 1"
},
{
"$type": "RegionSpawner",
"guid": "f68b6bc8-f822-4c7a-9d01-37fad25bc95b",
"name": "Spawner",
"location": [1231, 1887, 0],
"map": "Felucca",
"count": 8,
"minDelay": "00:00:10",
"maxDelay": "00:00:30",
"entries": [
{ "name": "FarmableWheat", "maxCount": 8, "probability": 100 }
],
"region": "A Wheatfield in Britain 3"
}
]

View file

@ -1,6 +1,110 @@
[
{
"type": "Spawner",
"$type": "Spawner",
"guid": "04856160-7e89-4672-a7d9-468b8634c3d6",
"name": "Spawner (208)",
"location": [4545, 1317, 8],
"map": "Felucca",
"count": 4,
"minDelay": "00:05:00",
"maxDelay": "00:10:00",
"team": 0,
"spawnBounds": {
"start": { "x": 4535, "y": 1307, "z": -128 },
"end": { "x": 4555, "y": 1327, "z": 25 }
},
"walkingRange": 20,
"entries": [
{ "name": "Spectre", "maxCount": 1, "probability": 100 },
{ "name": "Shade", "maxCount": 1, "probability": 100 },
{ "name": "Wraith", "maxCount": 1, "probability": 100 },
{ "name": "Skeleton", "maxCount": 1, "probability": 100 },
{ "name": "Zombie", "maxCount": 2, "probability": 100 }
]
},
{
"$type": "Spawner",
"guid": "14b45cdf-987e-4dff-9e9f-10e90e3662e6",
"name": "Spawner (208)",
"location": [2758, 867, 0],
"map": "Felucca",
"count": 9,
"minDelay": "00:05:00",
"maxDelay": "00:10:00",
"team": 0,
"spawnBounds": {
"start": { "x": 2738, "y": 847, "z": -128 },
"end": { "x": 2778, "y": 887, "z": 15 }
},
"walkingRange": 20,
"entries": [
{ "name": "Spectre", "maxCount": 2, "probability": 100 },
{ "name": "Wraith", "maxCount": 2, "probability": 100 },
{ "name": "Skeleton", "maxCount": 3, "probability": 100 },
{ "name": "Zombie", "maxCount": 4, "probability": 100 }
]
},
{
"$type": "Spawner",
"guid": "43d53b7d-95f9-4332-8d22-33e5f5a1fcf6",
"name": "Spawner (208)",
"location": [722, 1119, 0],
"map": "Felucca",
"count": 10,
"minDelay": "00:05:00",
"maxDelay": "00:10:00",
"team": 0,
"homeRange": 20,
"walkingRange": 20,
"entries": [
{ "name": "Skeleton", "maxCount": 10, "probability": 100 }
]
},
{
"$type": "Spawner",
"guid": "47aa8449-5b66-401f-ac38-c5b6538627b6",
"name": "Spawner (208)",
"location": [4543, 1314, 8],
"map": "Felucca",
"count": 1,
"minDelay": "00:05:00",
"maxDelay": "00:10:00",
"team": 0,
"spawnBounds": {
"start": { "x": 4533, "y": 1304, "z": -128 },
"end": { "x": 4553, "y": 1324, "z": 25 }
},
"walkingRange": 20,
"entries": [
{ "name": "Lich", "maxCount": 1, "probability": 100 }
]
},
{
"$type": "Spawner",
"guid": "522530f4-50ea-4fbf-9119-b783bd955678",
"name": "Spawner (208)",
"location": [2438, 1100, 8],
"map": "Felucca",
"count": 4,
"minDelay": "00:05:00",
"maxDelay": "00:10:00",
"team": 0,
"spawnBounds": {
"start": { "x": 2428, "y": 1090, "z": -128 },
"end": { "x": 2448, "y": 1110, "z": 30 }
},
"walkingRange": 20,
"entries": [
{ "name": "Spectre", "maxCount": 1, "probability": 100 },
{ "name": "Shade", "maxCount": 1, "probability": 100 },
{ "name": "Wraith", "maxCount": 1, "probability": 100 },
{ "name": "Lich", "maxCount": 1, "probability": 100 },
{ "name": "Skeleton", "maxCount": 1, "probability": 100 },
{ "name": "Zombie", "maxCount": 1, "probability": 100 }
]
},
{
"$type": "Spawner",
"guid": "7dfee8fd-49f0-457d-a88c-f38c65bf2474",
"name": "Spawner (208)",
"location": [1369, 1475, 10],
@ -19,115 +123,7 @@
]
},
{
"type": "Spawner",
"guid": "b3ba766a-4852-4c5e-adf1-0276e193317f",
"name": "Spawner (208)",
"location": [1285, 3731, 0],
"map": "Felucca",
"count": 9,
"minDelay": "00:05:00",
"maxDelay": "00:10:00",
"team": 0,
"homeRange": 10,
"walkingRange": 15,
"entries": [
{ "name": "Spectre", "maxCount": 2, "probability": 100 },
{ "name": "Wraith", "maxCount": 2, "probability": 100 },
{ "name": "Shade", "maxCount": 2, "probability": 100 },
{ "name": "Skeleton", "maxCount": 3, "probability": 100 },
{ "name": "Zombie", "maxCount": 4, "probability": 100 }
]
},
{
"type": "Spawner",
"guid": "522530f4-50ea-4fbf-9119-b783bd955678",
"name": "Spawner (208)",
"location": [2438, 1100, 8],
"map": "Felucca",
"count": 4,
"minDelay": "00:05:00",
"maxDelay": "00:10:00",
"team": 0,
"homeRange": 10,
"walkingRange": 20,
"entries": [
{ "name": "Spectre", "maxCount": 1, "probability": 100 },
{ "name": "Shade", "maxCount": 1, "probability": 100 },
{ "name": "Wraith", "maxCount": 1, "probability": 100 },
{ "name": "Lich", "maxCount": 1, "probability": 100 },
{ "name": "Skeleton", "maxCount": 1, "probability": 100 },
{ "name": "Zombie", "maxCount": 1, "probability": 100 }
]
},
{
"type": "Spawner",
"guid": "04856160-7e89-4672-a7d9-468b8634c3d6",
"name": "Spawner (208)",
"location": [4545, 1317, 8],
"map": "Felucca",
"count": 4,
"minDelay": "00:05:00",
"maxDelay": "00:10:00",
"team": 0,
"homeRange": 10,
"walkingRange": 20,
"entries": [
{ "name": "Spectre", "maxCount": 1, "probability": 100 },
{ "name": "Shade", "maxCount": 1, "probability": 100 },
{ "name": "Wraith", "maxCount": 1, "probability": 100 },
{ "name": "Skeleton", "maxCount": 1, "probability": 100 },
{ "name": "Zombie", "maxCount": 2, "probability": 100 }
]
},
{
"type": "Spawner",
"guid": "47aa8449-5b66-401f-ac38-c5b6538627b6",
"name": "Spawner (208)",
"location": [4543, 1314, 8],
"map": "Felucca",
"count": 1,
"minDelay": "00:05:00",
"maxDelay": "00:10:00",
"team": 0,
"homeRange": 10,
"walkingRange": 20,
"entries": [{ "name": "Lich", "maxCount": 1, "probability": 100 }]
},
{
"type": "Spawner",
"guid": "14b45cdf-987e-4dff-9e9f-10e90e3662e6",
"name": "Spawner (208)",
"location": [2758, 867, 0],
"map": "Felucca",
"count": 9,
"minDelay": "00:05:00",
"maxDelay": "00:10:00",
"team": 0,
"homeRange": 20,
"walkingRange": 20,
"entries": [
{ "name": "Spectre", "maxCount": 2, "probability": 100 },
{ "name": "Wraith", "maxCount": 2, "probability": 100 },
{ "name": "Skeleton", "maxCount": 3, "probability": 100 },
{ "name": "Zombie", "maxCount": 4, "probability": 100 }
]
},
{
"type": "Spawner",
"guid": "43d53b7d-95f9-4332-8d22-33e5f5a1fcf6",
"name": "Spawner (208)",
"location": [722, 1119, 0],
"map": "Felucca",
"count": 10,
"minDelay": "00:05:00",
"maxDelay": "00:10:00",
"team": 0,
"homeRange": 20,
"walkingRange": 20,
"entries": [{ "name": "Skeleton", "maxCount": 10, "probability": 100 }]
},
{
"type": "Spawner",
"$type": "Spawner",
"guid": "8bdfe6b3-df80-48b6-8d1e-8e052efde18b",
"name": "Spawner (208)",
"location": [3407, 2652, 48],
@ -142,5 +138,28 @@
{ "name": "Zombie", "maxCount": 4, "probability": 100 },
{ "name": "Skeleton", "maxCount": 4, "probability": 100 }
]
},
{
"$type": "Spawner",
"guid": "b3ba766a-4852-4c5e-adf1-0276e193317f",
"name": "Spawner (208)",
"location": [1285, 3731, 0],
"map": "Felucca",
"count": 9,
"minDelay": "00:05:00",
"maxDelay": "00:10:00",
"team": 0,
"spawnBounds": {
"start": { "x": 1275, "y": 3721, "z": -128 },
"end": { "x": 1295, "y": 3741, "z": 15 }
},
"walkingRange": 15,
"entries": [
{ "name": "Spectre", "maxCount": 2, "probability": 100 },
{ "name": "Wraith", "maxCount": 2, "probability": 100 },
{ "name": "Shade", "maxCount": 2, "probability": 100 },
{ "name": "Skeleton", "maxCount": 3, "probability": 100 },
{ "name": "Zombie", "maxCount": 4, "probability": 100 }
]
}
]

View file

@ -1,100 +1,6 @@
[
{
"type": "Spawner",
"guid": "5a62f325-4e00-49c3-8576-fdd9fcf330e5",
"name": "Spawner (210)",
"location": [5771, 224, -2],
"map": "Felucca",
"count": 1,
"minDelay": "00:05:00",
"maxDelay": "00:10:00",
"team": 0,
"homeRange": 10,
"walkingRange": 20,
"entries": [{ "name": "ArcticOgreLord", "maxCount": 1, "probability": 100 }]
},
{
"type": "Spawner",
"guid": "3d3f7414-5249-4ee4-9602-c842af0da38c",
"name": "Spawner (210)",
"location": [5799, 188, -6],
"map": "Felucca",
"count": 6,
"minDelay": "00:05:00",
"maxDelay": "00:10:00",
"team": 0,
"homeRange": 10,
"walkingRange": 25,
"entries": [
{ "name": "IceSerpent", "maxCount": 2, "probability": 100 },
{ "name": "SnowElemental", "maxCount": 1, "probability": 100 },
{ "name": "Ratman", "maxCount": 1, "probability": 100 },
{ "name": "FrostTroll", "maxCount": 1, "probability": 100 },
{ "name": "FrostSpider", "maxCount": 1, "probability": 100 }
]
},
{
"type": "Spawner",
"guid": "b8a2691a-996a-4c38-834f-897b972bfe2f",
"name": "Spawner (210)",
"location": [5867, 218, -4],
"map": "Felucca",
"count": 4,
"minDelay": "00:05:00",
"maxDelay": "00:10:00",
"team": 0,
"homeRange": 15,
"walkingRange": 25,
"entries": [
{ "name": "FrostSpider", "maxCount": 1, "probability": 100 },
{ "name": "FrostOoze", "maxCount": 2, "probability": 100 },
{ "name": "IceSnake", "maxCount": 1, "probability": 100 }
]
},
{
"type": "Spawner",
"guid": "d31d21b1-8768-4d45-a962-182e67f5a5c0",
"name": "Spawner (210)",
"location": [5850, 219, -3],
"map": "Felucca",
"count": 1,
"minDelay": "00:05:00",
"maxDelay": "00:10:00",
"team": 0,
"homeRange": 10,
"walkingRange": 20,
"entries": [{ "name": "RatmanMage", "maxCount": 1, "probability": 100 }]
},
{
"type": "Spawner",
"guid": "e111cb63-9804-4eac-96ad-290a0b963c8f",
"name": "Spawner (210)",
"location": [5814, 237, 0],
"map": "Felucca",
"count": 1,
"minDelay": "00:05:00",
"maxDelay": "00:10:00",
"team": 0,
"homeRange": 15,
"walkingRange": 15,
"entries": [{ "name": "ArcticOgreLord", "maxCount": 1, "probability": 100 }]
},
{
"type": "Spawner",
"guid": "d7d771c1-605a-4253-965e-dec048f72502",
"name": "Spawner (210)",
"location": [5752, 144, 9],
"map": "Felucca",
"count": 1,
"minDelay": "00:05:00",
"maxDelay": "00:10:00",
"team": 0,
"homeRange": 10,
"walkingRange": 10,
"entries": [{ "name": "WhiteWyrm", "maxCount": 1, "probability": 100 }]
},
{
"type": "Spawner",
"$type": "Spawner",
"guid": "03498749-3218-4779-8864-b854b36c79f7",
"name": "Spawner (210)",
"location": [5839, 159, 0],
@ -115,7 +21,7 @@
]
},
{
"type": "Spawner",
"$type": "Spawner",
"guid": "0614ccac-5ffc-4223-8348-cbb5a0f0cc2d",
"name": "Spawner (210)",
"location": [5785, 161, -6],
@ -136,7 +42,173 @@
]
},
{
"type": "Spawner",
"$type": "Spawner",
"guid": "1cf83838-f5d3-42f9-8390-f445a01ac889",
"name": "Spawner (210)",
"location": [5754, 211, -6],
"map": "Felucca",
"count": 1,
"minDelay": "00:05:00",
"maxDelay": "00:10:00",
"team": 0,
"homeRange": 1,
"walkingRange": 1,
"entries": [
{ "name": "TreasureChestLevel3", "maxCount": 1, "probability": 100 }
]
},
{
"$type": "Spawner",
"guid": "33003603-a1fc-43a4-b9d7-2408eb2e5e50",
"name": "Spawner (210)",
"location": [5848, 226, 0],
"map": "Felucca",
"count": 1,
"minDelay": "00:05:00",
"maxDelay": "00:10:00",
"team": 0,
"homeRange": 1,
"walkingRange": 1,
"entries": [
{ "name": "TreasureChestLevel2", "maxCount": 1, "probability": 100 }
]
},
{
"$type": "Spawner",
"guid": "3bf7b937-9eb6-45fc-81e6-d907910ca62a",
"name": "Spawner (210)",
"location": [5758, 139, 5],
"map": "Felucca",
"count": 1,
"minDelay": "00:05:00",
"maxDelay": "00:10:00",
"team": 0,
"homeRange": 1,
"walkingRange": 1,
"entries": [
{ "name": "TreasureChestLevel3", "maxCount": 1, "probability": 100 }
]
},
{
"$type": "Spawner",
"guid": "3d3f7414-5249-4ee4-9602-c842af0da38c",
"name": "Spawner (210)",
"location": [5799, 188, -6],
"map": "Felucca",
"count": 6,
"minDelay": "00:05:00",
"maxDelay": "00:10:00",
"team": 0,
"homeRange": 10,
"walkingRange": 25,
"entries": [
{ "name": "IceSerpent", "maxCount": 2, "probability": 100 },
{ "name": "SnowElemental", "maxCount": 1, "probability": 100 },
{ "name": "Ratman", "maxCount": 1, "probability": 100 },
{ "name": "FrostTroll", "maxCount": 1, "probability": 100 },
{ "name": "FrostSpider", "maxCount": 1, "probability": 100 }
]
},
{
"$type": "Spawner",
"guid": "479095c0-02ab-4651-9edc-362d77a65a65",
"name": "Spawner (210)",
"location": [5763, 187, 0],
"map": "Felucca",
"count": 1,
"minDelay": "00:05:00",
"maxDelay": "00:10:00",
"team": 0,
"homeRange": 1,
"walkingRange": 1,
"entries": [
{ "name": "TreasureChestLevel4", "maxCount": 1, "probability": 100 }
]
},
{
"$type": "Spawner",
"guid": "4ab984eb-913e-4b21-a9b2-5242726ae03f",
"name": "Spawner (210)",
"location": [5731, 176, -5],
"map": "Felucca",
"count": 18,
"minDelay": "00:05:00",
"maxDelay": "00:10:00",
"team": 0,
"homeRange": 25,
"walkingRange": 35,
"entries": [
{ "name": "IceSerpent", "maxCount": 9, "probability": 100 },
{ "name": "SnowElemental", "maxCount": 9, "probability": 100 },
{ "name": "IceElemental", "maxCount": 9, "probability": 100 }
]
},
{
"$type": "Spawner",
"guid": "4b8dd28c-f72f-45c5-afc4-84dc637c1e0a",
"name": "Spawner (210)",
"location": [5684, 181, -5],
"map": "Felucca",
"count": 1,
"minDelay": "00:05:00",
"maxDelay": "00:10:00",
"team": 0,
"homeRange": 1,
"walkingRange": 1,
"entries": [
{ "name": "TreasureChestLevel4", "maxCount": 1, "probability": 100 }
]
},
{
"$type": "Spawner",
"guid": "51445c30-3ef8-4c0e-95bc-b12e643e39ba",
"name": "Spawner (210)",
"location": [5669, 330, 0],
"map": "Felucca",
"count": 1,
"minDelay": "00:05:00",
"maxDelay": "00:10:00",
"team": 0,
"homeRange": 10,
"walkingRange": 20,
"entries": [
{ "name": "IceFiend", "maxCount": 1, "probability": 100 }
]
},
{
"$type": "Spawner",
"guid": "57c38552-9c17-4451-9e53-3a8d2048d2f6",
"name": "Spawner (210)",
"location": [5666, 331, 0],
"map": "Felucca",
"count": 1,
"minDelay": "00:05:00",
"maxDelay": "00:10:00",
"team": 0,
"homeRange": 1,
"walkingRange": 1,
"entries": [
{ "name": "TreasureChestLevel4", "maxCount": 1, "probability": 100 }
]
},
{
"$type": "Spawner",
"guid": "5a62f325-4e00-49c3-8576-fdd9fcf330e5",
"name": "Spawner (210)",
"location": [5771, 224, -2],
"map": "Felucca",
"count": 1,
"minDelay": "00:05:00",
"maxDelay": "00:10:00",
"team": 0,
"homeRange": 10,
"walkingRange": 20,
"entries": [
{ "name": "ArcticOgreLord", "maxCount": 1, "probability": 100 }
]
},
{
"$type": "Spawner",
"guid": "61e8233d-f991-4429-9e4e-c7e0d24201d0",
"name": "Spawner (210)",
"location": [5757, 213, -7],
@ -156,7 +228,235 @@
]
},
{
"type": "Spawner",
"$type": "Spawner",
"guid": "72473a5f-3d9b-4f69-b3f9-0d266291468a",
"name": "Spawner (210)",
"location": [5824, 363, 0],
"map": "Felucca",
"count": 1,
"minDelay": "00:05:00",
"maxDelay": "00:10:00",
"team": 0,
"homeRange": 1,
"walkingRange": 1,
"entries": [
{ "name": "TreasureChestLevel3", "maxCount": 1, "probability": 100 }
]
},
{
"$type": "Spawner",
"guid": "7b080104-77b8-491b-b8f9-44cec8ecbb65",
"name": "Spawner (210)",
"location": [5680, 306, 0],
"map": "Felucca",
"count": 1,
"minDelay": "00:05:00",
"maxDelay": "00:10:00",
"team": 0,
"homeRange": 10,
"walkingRange": 20,
"entries": [
{ "name": "IceFiend", "maxCount": 1, "probability": 100 }
]
},
{
"$type": "Spawner",
"guid": "89792e6b-89bb-4c0a-8f45-259ab52f6844",
"name": "Spawner (210)",
"location": [5676, 331, 0],
"map": "Felucca",
"count": 1,
"minDelay": "00:05:00",
"maxDelay": "00:10:00",
"team": 0,
"homeRange": 1,
"walkingRange": 1,
"entries": [
{ "name": "TreasureChestLevel4", "maxCount": 1, "probability": 100 }
]
},
{
"$type": "Spawner",
"guid": "8aba961b-8ce4-49ba-9ec8-7861204815c4",
"name": "Spawner (210)",
"location": [5831, 358, -1],
"map": "Felucca",
"count": 24,
"minDelay": "00:05:00",
"maxDelay": "00:10:00",
"team": 0,
"homeRange": 15,
"walkingRange": 25,
"entries": [
{ "name": "Ratman", "maxCount": 12, "probability": 100 },
{ "name": "RatmanArcher", "maxCount": 7, "probability": 100 },
{ "name": "RatmanMage", "maxCount": 5, "probability": 100 }
]
},
{
"$type": "Spawner",
"guid": "9b93a6e4-67e5-4d22-aa51-ff84ea98bb72",
"name": "Spawner (210)",
"location": [5751, 140, 10],
"map": "Felucca",
"count": 1,
"minDelay": "00:05:00",
"maxDelay": "00:10:00",
"team": 0,
"homeRange": 1,
"walkingRange": 1,
"entries": [
{ "name": "TreasureChestLevel4", "maxCount": 1, "probability": 100 }
]
},
{
"$type": "Spawner",
"guid": "a3c447a5-c7bb-4826-a5ae-81d8b3863130",
"name": "Spawner (210)",
"location": [5854, 227, 0],
"map": "Felucca",
"count": 1,
"minDelay": "00:05:00",
"maxDelay": "00:10:00",
"team": 0,
"homeRange": 1,
"walkingRange": 1,
"entries": [
{ "name": "TreasureChestLevel3", "maxCount": 1, "probability": 100 }
]
},
{
"$type": "Spawner",
"guid": "b8a2691a-996a-4c38-834f-897b972bfe2f",
"name": "Spawner (210)",
"location": [5867, 218, -4],
"map": "Felucca",
"count": 4,
"minDelay": "00:05:00",
"maxDelay": "00:10:00",
"team": 0,
"homeRange": 15,
"walkingRange": 25,
"entries": [
{ "name": "FrostSpider", "maxCount": 1, "probability": 100 },
{ "name": "FrostOoze", "maxCount": 2, "probability": 100 },
{ "name": "IceSnake", "maxCount": 1, "probability": 100 }
]
},
{
"$type": "Spawner",
"guid": "b96eb341-ef4c-4b7e-ba01-6fb487be56a7",
"name": "Spawner (210)",
"location": [5721, 147, -1],
"map": "Felucca",
"count": 1,
"minDelay": "00:05:00",
"maxDelay": "00:10:00",
"team": 0,
"homeRange": 1,
"walkingRange": 1,
"entries": [
{ "name": "TreasureChestLevel3", "maxCount": 1, "probability": 100 }
]
},
{
"$type": "Spawner",
"guid": "bca4ab21-15b1-4b04-b698-05d4f8df1f6c",
"name": "Spawner (210)",
"location": [5838, 355, 0],
"map": "Felucca",
"count": 1,
"minDelay": "00:05:00",
"maxDelay": "00:10:00",
"team": 0,
"homeRange": 1,
"walkingRange": 1,
"entries": [
{ "name": "TreasureChestLevel2", "maxCount": 1, "probability": 100 }
]
},
{
"$type": "Spawner",
"guid": "cb37f510-fff9-4c76-8c51-2d59ddd8f715",
"name": "Spawner (210)",
"location": [5683, 197, -4],
"map": "Felucca",
"count": 1,
"minDelay": "00:05:00",
"maxDelay": "00:10:00",
"team": 0,
"homeRange": 1,
"walkingRange": 1,
"entries": [
{ "name": "TreasureChestLevel3", "maxCount": 1, "probability": 100 }
]
},
{
"$type": "Spawner",
"guid": "cdb2ea29-4ee5-48c3-9444-018c5003afbd",
"name": "Spawner (210)",
"location": [5837, 245, -5],
"map": "Felucca",
"count": 1,
"minDelay": "00:05:00",
"maxDelay": "00:10:00",
"team": 0,
"homeRange": 1,
"walkingRange": 1,
"entries": [
{ "name": "TreasureChestLevel4", "maxCount": 1, "probability": 100 }
]
},
{
"$type": "Spawner",
"guid": "d31d21b1-8768-4d45-a962-182e67f5a5c0",
"name": "Spawner (210)",
"location": [5850, 219, -3],
"map": "Felucca",
"count": 1,
"minDelay": "00:05:00",
"maxDelay": "00:10:00",
"team": 0,
"homeRange": 10,
"walkingRange": 20,
"entries": [
{ "name": "RatmanMage", "maxCount": 1, "probability": 100 }
]
},
{
"$type": "Spawner",
"guid": "d5ca5e49-cbb2-4079-81f1-c8679fcecb46",
"name": "Spawner (210)",
"location": [5696, 303, 0],
"map": "Felucca",
"count": 1,
"minDelay": "00:05:00",
"maxDelay": "00:10:00",
"team": 0,
"homeRange": 1,
"walkingRange": 1,
"entries": [
{ "name": "TreasureChestLevel4", "maxCount": 1, "probability": 100 }
]
},
{
"$type": "Spawner",
"guid": "d7d771c1-605a-4253-965e-dec048f72502",
"name": "Spawner (210)",
"location": [5752, 144, 9],
"map": "Felucca",
"count": 1,
"minDelay": "00:05:00",
"maxDelay": "00:10:00",
"team": 0,
"homeRange": 10,
"walkingRange": 10,
"entries": [
{ "name": "WhiteWyrm", "maxCount": 1, "probability": 100 }
]
},
{
"$type": "Spawner",
"guid": "d8d3fc0f-231c-4d04-b0a3-7191cc4512b2",
"name": "Spawner (210)",
"location": [5681, 191, -6],
@ -176,193 +476,23 @@
]
},
{
"type": "Spawner",
"guid": "4ab984eb-913e-4b21-a9b2-5242726ae03f",
"$type": "Spawner",
"guid": "e111cb63-9804-4eac-96ad-290a0b963c8f",
"name": "Spawner (210)",
"location": [5731, 176, -5],
"location": [5814, 237, 0],
"map": "Felucca",
"count": 18,
"count": 1,
"minDelay": "00:05:00",
"maxDelay": "00:10:00",
"team": 0,
"homeRange": 25,
"walkingRange": 35,
"homeRange": 15,
"walkingRange": 15,
"entries": [
{ "name": "IceSerpent", "maxCount": 9, "probability": 100 },
{ "name": "SnowElemental", "maxCount": 9, "probability": 100 },
{ "name": "IceElemental", "maxCount": 9, "probability": 100 }
{ "name": "ArcticOgreLord", "maxCount": 1, "probability": 100 }
]
},
{
"type": "Spawner",
"guid": "33003603-a1fc-43a4-b9d7-2408eb2e5e50",
"name": "Spawner (210)",
"location": [5848, 226, 0],
"map": "Felucca",
"count": 1,
"minDelay": "00:05:00",
"maxDelay": "00:10:00",
"team": 0,
"homeRange": 1,
"walkingRange": 1,
"entries": [{ "name": "TreasureChestLevel2", "maxCount": 1, "probability": 100 }]
},
{
"type": "Spawner",
"guid": "cb37f510-fff9-4c76-8c51-2d59ddd8f715",
"name": "Spawner (210)",
"location": [5683, 197, -4],
"map": "Felucca",
"count": 1,
"minDelay": "00:05:00",
"maxDelay": "00:10:00",
"team": 0,
"homeRange": 1,
"walkingRange": 1,
"entries": [{ "name": "TreasureChestLevel3", "maxCount": 1, "probability": 100 }]
},
{
"type": "Spawner",
"guid": "b96eb341-ef4c-4b7e-ba01-6fb487be56a7",
"name": "Spawner (210)",
"location": [5721, 147, -1],
"map": "Felucca",
"count": 1,
"minDelay": "00:05:00",
"maxDelay": "00:10:00",
"team": 0,
"homeRange": 1,
"walkingRange": 1,
"entries": [{ "name": "TreasureChestLevel3", "maxCount": 1, "probability": 100 }]
},
{
"type": "Spawner",
"guid": "3bf7b937-9eb6-45fc-81e6-d907910ca62a",
"name": "Spawner (210)",
"location": [5758, 139, 5],
"map": "Felucca",
"count": 1,
"minDelay": "00:05:00",
"maxDelay": "00:10:00",
"team": 0,
"homeRange": 1,
"walkingRange": 1,
"entries": [{ "name": "TreasureChestLevel3", "maxCount": 1, "probability": 100 }]
},
{
"type": "Spawner",
"guid": "1cf83838-f5d3-42f9-8390-f445a01ac889",
"name": "Spawner (210)",
"location": [5754, 211, -6],
"map": "Felucca",
"count": 1,
"minDelay": "00:05:00",
"maxDelay": "00:10:00",
"team": 0,
"homeRange": 1,
"walkingRange": 1,
"entries": [{ "name": "TreasureChestLevel3", "maxCount": 1, "probability": 100 }]
},
{
"type": "Spawner",
"guid": "fe1657a5-a11c-4fe6-9544-a7ed0d2059b2",
"name": "Spawner (210)",
"location": [5833, 242, -2],
"map": "Felucca",
"count": 1,
"minDelay": "00:05:00",
"maxDelay": "00:10:00",
"team": 0,
"homeRange": 1,
"walkingRange": 1,
"entries": [{ "name": "TreasureChestLevel3", "maxCount": 1, "probability": 100 }]
},
{
"type": "Spawner",
"guid": "a3c447a5-c7bb-4826-a5ae-81d8b3863130",
"name": "Spawner (210)",
"location": [5854, 227, 0],
"map": "Felucca",
"count": 1,
"minDelay": "00:05:00",
"maxDelay": "00:10:00",
"team": 0,
"homeRange": 1,
"walkingRange": 1,
"entries": [{ "name": "TreasureChestLevel3", "maxCount": 1, "probability": 100 }]
},
{
"type": "Spawner",
"guid": "f2eb3f17-d5f4-4c58-a248-825e46a8a0f8",
"name": "Spawner (210)",
"location": [5772, 188, -3],
"map": "Felucca",
"count": 1,
"minDelay": "00:05:00",
"maxDelay": "00:10:00",
"team": 0,
"homeRange": 1,
"walkingRange": 1,
"entries": [{ "name": "TreasureChestLevel3", "maxCount": 1, "probability": 100 }]
},
{
"type": "Spawner",
"guid": "cdb2ea29-4ee5-48c3-9444-018c5003afbd",
"name": "Spawner (210)",
"location": [5837, 245, -5],
"map": "Felucca",
"count": 1,
"minDelay": "00:05:00",
"maxDelay": "00:10:00",
"team": 0,
"homeRange": 1,
"walkingRange": 1,
"entries": [{ "name": "TreasureChestLevel4", "maxCount": 1, "probability": 100 }]
},
{
"type": "Spawner",
"guid": "9b93a6e4-67e5-4d22-aa51-ff84ea98bb72",
"name": "Spawner (210)",
"location": [5751, 140, 10],
"map": "Felucca",
"count": 1,
"minDelay": "00:05:00",
"maxDelay": "00:10:00",
"team": 0,
"homeRange": 1,
"walkingRange": 1,
"entries": [{ "name": "TreasureChestLevel4", "maxCount": 1, "probability": 100 }]
},
{
"type": "Spawner",
"guid": "479095c0-02ab-4651-9edc-362d77a65a65",
"name": "Spawner (210)",
"location": [5763, 187, 0],
"map": "Felucca",
"count": 1,
"minDelay": "00:05:00",
"maxDelay": "00:10:00",
"team": 0,
"homeRange": 1,
"walkingRange": 1,
"entries": [{ "name": "TreasureChestLevel4", "maxCount": 1, "probability": 100 }]
},
{
"type": "Spawner",
"guid": "ed18e76c-c568-42fe-9c98-8482c467ff61",
"name": "Spawner (210)",
"location": [5756, 203, -2],
"map": "Felucca",
"count": 1,
"minDelay": "00:05:00",
"maxDelay": "00:10:00",
"team": 0,
"homeRange": 1,
"walkingRange": 1,
"entries": [{ "name": "TreasureChestLevel4", "maxCount": 1, "probability": 100 }]
},
{
"type": "Spawner",
"$type": "Spawner",
"guid": "e89bcb09-65c3-4474-bded-50e62ce3c153",
"name": "Spawner (210)",
"location": [5712, 145, -34],
@ -373,45 +503,15 @@
"team": 0,
"homeRange": 1,
"walkingRange": 1,
"entries": [{ "name": "TreasureChestLevel4", "maxCount": 1, "probability": 100 }]
},
{
"type": "Spawner",
"guid": "4b8dd28c-f72f-45c5-afc4-84dc637c1e0a",
"name": "Spawner (210)",
"location": [5684, 181, -5],
"map": "Felucca",
"count": 1,
"minDelay": "00:05:00",
"maxDelay": "00:10:00",
"team": 0,
"homeRange": 1,
"walkingRange": 1,
"entries": [{ "name": "TreasureChestLevel4", "maxCount": 1, "probability": 100 }]
},
{
"type": "Spawner",
"guid": "8aba961b-8ce4-49ba-9ec8-7861204815c4",
"name": "Spawner (210)",
"location": [5831, 358, -1],
"map": "Felucca",
"count": 24,
"minDelay": "00:05:00",
"maxDelay": "00:10:00",
"team": 0,
"homeRange": 15,
"walkingRange": 25,
"entries": [
{ "name": "Ratman", "maxCount": 12, "probability": 100 },
{ "name": "RatmanArcher", "maxCount": 7, "probability": 100 },
{ "name": "RatmanMage", "maxCount": 5, "probability": 100 }
{ "name": "TreasureChestLevel4", "maxCount": 1, "probability": 100 }
]
},
{
"type": "Spawner",
"guid": "bca4ab21-15b1-4b04-b698-05d4f8df1f6c",
"$type": "Spawner",
"guid": "ed18e76c-c568-42fe-9c98-8482c467ff61",
"name": "Spawner (210)",
"location": [5838, 355, 0],
"location": [5756, 203, -2],
"map": "Felucca",
"count": 1,
"minDelay": "00:05:00",
@ -419,13 +519,15 @@
"team": 0,
"homeRange": 1,
"walkingRange": 1,
"entries": [{ "name": "TreasureChestLevel2", "maxCount": 1, "probability": 100 }]
"entries": [
{ "name": "TreasureChestLevel4", "maxCount": 1, "probability": 100 }
]
},
{
"type": "Spawner",
"guid": "72473a5f-3d9b-4f69-b3f9-0d266291468a",
"$type": "Spawner",
"guid": "f2eb3f17-d5f4-4c58-a248-825e46a8a0f8",
"name": "Spawner (210)",
"location": [5824, 363, 0],
"location": [5772, 188, -3],
"map": "Felucca",
"count": 1,
"minDelay": "00:05:00",
@ -433,52 +535,12 @@
"team": 0,
"homeRange": 1,
"walkingRange": 1,
"entries": [{ "name": "TreasureChestLevel3", "maxCount": 1, "probability": 100 }]
"entries": [
{ "name": "TreasureChestLevel3", "maxCount": 1, "probability": 100 }
]
},
{
"type": "Spawner",
"guid": "7b080104-77b8-491b-b8f9-44cec8ecbb65",
"name": "Spawner (210)",
"location": [5680, 306, 0],
"map": "Felucca",
"count": 1,
"minDelay": "00:05:00",
"maxDelay": "00:10:00",
"team": 0,
"homeRange": 10,
"walkingRange": 20,
"entries": [{ "name": "IceFiend", "maxCount": 1, "probability": 100 }]
},
{
"type": "Spawner",
"guid": "51445c30-3ef8-4c0e-95bc-b12e643e39ba",
"name": "Spawner (210)",
"location": [5669, 330, 0],
"map": "Felucca",
"count": 1,
"minDelay": "00:05:00",
"maxDelay": "00:10:00",
"team": 0,
"homeRange": 10,
"walkingRange": 20,
"entries": [{ "name": "IceFiend", "maxCount": 1, "probability": 100 }]
},
{
"type": "Spawner",
"guid": "d5ca5e49-cbb2-4079-81f1-c8679fcecb46",
"name": "Spawner (210)",
"location": [5696, 303, 0],
"map": "Felucca",
"count": 1,
"minDelay": "00:05:00",
"maxDelay": "00:10:00",
"team": 0,
"homeRange": 1,
"walkingRange": 1,
"entries": [{ "name": "TreasureChestLevel4", "maxCount": 1, "probability": 100 }]
},
{
"type": "Spawner",
"$type": "Spawner",
"guid": "f522aee5-3ff7-427c-a3f1-31b162a65fbe",
"name": "Spawner (210)",
"location": [5682, 314, 0],
@ -489,13 +551,15 @@
"team": 0,
"homeRange": 1,
"walkingRange": 1,
"entries": [{ "name": "TreasureChestLevel4", "maxCount": 1, "probability": 100 }]
"entries": [
{ "name": "TreasureChestLevel4", "maxCount": 1, "probability": 100 }
]
},
{
"type": "Spawner",
"guid": "57c38552-9c17-4451-9e53-3a8d2048d2f6",
"$type": "Spawner",
"guid": "fe1657a5-a11c-4fe6-9544-a7ed0d2059b2",
"name": "Spawner (210)",
"location": [5666, 331, 0],
"location": [5833, 242, -2],
"map": "Felucca",
"count": 1,
"minDelay": "00:05:00",
@ -503,20 +567,8 @@
"team": 0,
"homeRange": 1,
"walkingRange": 1,
"entries": [{ "name": "TreasureChestLevel4", "maxCount": 1, "probability": 100 }]
},
{
"type": "Spawner",
"guid": "89792e6b-89bb-4c0a-8f45-259ab52f6844",
"name": "Spawner (210)",
"location": [5676, 331, 0],
"map": "Felucca",
"count": 1,
"minDelay": "00:05:00",
"maxDelay": "00:10:00",
"team": 0,
"homeRange": 1,
"walkingRange": 1,
"entries": [{ "name": "TreasureChestLevel4", "maxCount": 1, "probability": 100 }]
"entries": [
{ "name": "TreasureChestLevel3", "maxCount": 1, "probability": 100 }
]
}
]

View file

@ -1,24 +1,24 @@
[
{
"type": "Spawner",
"guid": "572a2fac-11c7-4583-837a-1b03eae15f85",
"$type": "Spawner",
"guid": "01ae830b-c7f9-4857-8a6b-4bacca6758ad",
"name": "Spawner (212)",
"location": [5146, 1995, 0],
"location": [5356, 1304, 0],
"map": "Felucca",
"count": 3,
"count": 4,
"minDelay": "00:05:00",
"maxDelay": "00:10:00",
"team": 0,
"homeRange": 8,
"walkingRange": 8,
"walkingRange": 10,
"entries": [
{ "name": "Orc", "maxCount": 1, "probability": 100 },
{ "name": "OrcBomber", "maxCount": 1, "probability": 100 },
{ "name": "OrcCaptain", "maxCount": 1, "probability": 100 },
{ "name": "OrcishLord", "maxCount": 1, "probability": 100 }
{ "name": "Orc", "maxCount": 2, "probability": 100 }
]
},
{
"type": "Spawner",
"$type": "Spawner",
"guid": "03b59250-db54-481c-85f4-1d0b8b433252",
"name": "Spawner (212)",
"location": [5153, 1971, 0],
@ -29,66 +29,12 @@
"team": 0,
"homeRange": 8,
"walkingRange": 8,
"entries": [{ "name": "Orc", "maxCount": 5, "probability": 100 }]
"entries": [
{ "name": "Orc", "maxCount": 5, "probability": 100 }
]
},
{
"type": "Spawner",
"guid": "a1dbbaf6-03e3-42f2-ba42-35d88349fd0c",
"name": "Spawner (212)",
"location": [5153, 1961, 0],
"map": "Felucca",
"count": 2,
"minDelay": "00:05:00",
"maxDelay": "00:10:00",
"team": 0,
"homeRange": 8,
"walkingRange": 8,
"entries": [{ "name": "DireWolf", "maxCount": 2, "probability": 100 }]
},
{
"type": "Spawner",
"guid": "5145b262-72b3-4bf2-8a73-91efe509ce18",
"name": "Spawner (212)",
"location": [5144, 1960, 0],
"map": "Felucca",
"count": 3,
"minDelay": "00:05:00",
"maxDelay": "00:10:00",
"team": 0,
"homeRange": 8,
"walkingRange": 8,
"entries": [{ "name": "OrcishLord", "maxCount": 3, "probability": 100 }]
},
{
"type": "Spawner",
"guid": "fe0be93b-5f45-40b0-9169-16df378c0e6e",
"name": "Spawner (212)",
"location": [5141, 1968, 0],
"map": "Felucca",
"count": 3,
"minDelay": "00:05:00",
"maxDelay": "00:10:00",
"team": 0,
"homeRange": 8,
"walkingRange": 8,
"entries": [{ "name": "OrcCaptain", "maxCount": 3, "probability": 100 }]
},
{
"type": "Spawner",
"guid": "ee800978-f2bf-47e0-8e19-c7f5fe0b33bc",
"name": "Spawner (212)",
"location": [5332, 1365, 0],
"map": "Felucca",
"count": 3,
"minDelay": "00:05:00",
"maxDelay": "00:10:00",
"team": 0,
"homeRange": 8,
"walkingRange": 8,
"entries": [{ "name": "Orc", "maxCount": 3, "probability": 100 }]
},
{
"type": "Spawner",
"$type": "Spawner",
"guid": "0e858157-adc4-4963-a34b-101fefb109d1",
"name": "Spawner (212)",
"location": [5308, 1372, 0],
@ -99,27 +45,12 @@
"team": 0,
"homeRange": 8,
"walkingRange": 8,
"entries": [{ "name": "Noble", "maxCount": 1, "probability": 100 }]
},
{
"type": "Spawner",
"guid": "593ee3b4-df42-426d-8976-faced7cd2435",
"name": "Spawner (212)",
"location": [5302, 1355, 0],
"map": "Felucca",
"count": 4,
"minDelay": "00:05:00",
"maxDelay": "00:10:00",
"team": 0,
"homeRange": 8,
"walkingRange": 12,
"entries": [
{ "name": "OrcishLord", "maxCount": 1, "probability": 100 },
{ "name": "Orc", "maxCount": 3, "probability": 100 }
{ "name": "Noble", "maxCount": 1, "probability": 100 }
]
},
{
"type": "Spawner",
"$type": "Spawner",
"guid": "1a6833ee-9e32-4900-ad6e-87926e8fb427",
"name": "Spawner (212)",
"location": [5316, 1332, 0],
@ -130,24 +61,46 @@
"team": 0,
"homeRange": 8,
"walkingRange": 8,
"entries": [{ "name": "GiantRat", "maxCount": 3, "probability": 100 }]
"entries": [
{ "name": "GiantRat", "maxCount": 3, "probability": 100 }
]
},
{
"type": "Spawner",
"guid": "70960aad-63ab-4bc2-9784-0ae5db883fe1",
"$type": "Spawner",
"guid": "1bff583d-3643-4e60-9a44-e3dea6882a02",
"name": "Spawner (212)",
"location": [5331, 1346, 0],
"location": [5308, 2005, 0],
"map": "Felucca",
"count": 3,
"count": 5,
"minDelay": "00:05:00",
"maxDelay": "00:10:00",
"team": 0,
"homeRange": 8,
"walkingRange": 8,
"entries": [{ "name": "DireWolf", "maxCount": 3, "probability": 100 }]
"homeRange": 12,
"walkingRange": 12,
"entries": [
{ "name": "OrcBrute", "maxCount": 1, "probability": 100 },
{ "name": "Orc", "maxCount": 4, "probability": 100 }
]
},
{
"type": "Spawner",
"$type": "Spawner",
"guid": "2d24f1f9-20ff-4ee1-8f31-8304e2e4822d",
"name": "Spawner (212)",
"location": [5310, 1969, 0],
"map": "Felucca",
"count": 4,
"minDelay": "00:05:00",
"maxDelay": "00:10:00",
"team": 0,
"homeRange": 12,
"walkingRange": 12,
"entries": [
{ "name": "OrcBomber", "maxCount": 2, "probability": 100 },
{ "name": "Orc", "maxCount": 2, "probability": 100 }
]
},
{
"$type": "Spawner",
"guid": "40c5eacc-9b5d-49f4-b13d-c605a32f6816",
"name": "Spawner (212)",
"location": [5354, 1332, 0],
@ -166,25 +119,7 @@
]
},
{
"type": "Spawner",
"guid": "01ae830b-c7f9-4857-8a6b-4bacca6758ad",
"name": "Spawner (212)",
"location": [5356, 1304, 0],
"map": "Felucca",
"count": 4,
"minDelay": "00:05:00",
"maxDelay": "00:10:00",
"team": 0,
"homeRange": 8,
"walkingRange": 10,
"entries": [
{ "name": "OrcBomber", "maxCount": 1, "probability": 100 },
{ "name": "OrcCaptain", "maxCount": 1, "probability": 100 },
{ "name": "Orc", "maxCount": 2, "probability": 100 }
]
},
{
"type": "Spawner",
"$type": "Spawner",
"guid": "471bb8b3-f768-4654-a3ae-9517ecc8dd45",
"name": "Spawner (212)",
"location": [5317, 1309, 0],
@ -203,7 +138,139 @@
]
},
{
"type": "Spawner",
"$type": "Spawner",
"guid": "5145b262-72b3-4bf2-8a73-91efe509ce18",
"name": "Spawner (212)",
"location": [5144, 1960, 0],
"map": "Felucca",
"count": 3,
"minDelay": "00:05:00",
"maxDelay": "00:10:00",
"team": 0,
"homeRange": 8,
"walkingRange": 8,
"entries": [
{ "name": "OrcishLord", "maxCount": 3, "probability": 100 }
]
},
{
"$type": "Spawner",
"guid": "572a2fac-11c7-4583-837a-1b03eae15f85",
"name": "Spawner (212)",
"location": [5146, 1995, 0],
"map": "Felucca",
"count": 3,
"minDelay": "00:05:00",
"maxDelay": "00:10:00",
"team": 0,
"homeRange": 8,
"walkingRange": 8,
"entries": [
{ "name": "Orc", "maxCount": 1, "probability": 100 },
{ "name": "OrcCaptain", "maxCount": 1, "probability": 100 },
{ "name": "OrcishLord", "maxCount": 1, "probability": 100 }
]
},
{
"$type": "Spawner",
"guid": "593ee3b4-df42-426d-8976-faced7cd2435",
"name": "Spawner (212)",
"location": [5302, 1355, 0],
"map": "Felucca",
"count": 4,
"minDelay": "00:05:00",
"maxDelay": "00:10:00",
"team": 0,
"homeRange": 8,
"walkingRange": 12,
"entries": [
{ "name": "OrcishLord", "maxCount": 1, "probability": 100 },
{ "name": "Orc", "maxCount": 3, "probability": 100 }
]
},
{
"$type": "Spawner",
"guid": "70960aad-63ab-4bc2-9784-0ae5db883fe1",
"name": "Spawner (212)",
"location": [5331, 1346, 0],
"map": "Felucca",
"count": 3,
"minDelay": "00:05:00",
"maxDelay": "00:10:00",
"team": 0,
"homeRange": 8,
"walkingRange": 8,
"entries": [
{ "name": "DireWolf", "maxCount": 3, "probability": 100 }
]
},
{
"$type": "Spawner",
"guid": "81a40400-c93d-42f3-8f19-0e9492198a82",
"name": "Spawner (212)",
"location": [5292, 1316, 0],
"map": "Felucca",
"count": 2,
"minDelay": "00:05:00",
"maxDelay": "00:10:00",
"team": 0,
"homeRange": 8,
"walkingRange": 8,
"entries": [
{ "name": "Corpser", "maxCount": 2, "probability": 100 }
]
},
{
"$type": "Spawner",
"guid": "a1dbbaf6-03e3-42f2-ba42-35d88349fd0c",
"name": "Spawner (212)",
"location": [5153, 1961, 0],
"map": "Felucca",
"count": 2,
"minDelay": "00:05:00",
"maxDelay": "00:10:00",
"team": 0,
"homeRange": 8,
"walkingRange": 8,
"entries": [
{ "name": "DireWolf", "maxCount": 2, "probability": 100 }
]
},
{
"$type": "Spawner",
"guid": "a70f186a-94a1-4f54-963f-5c039c7d4006",
"name": "Spawner (212)",
"location": [5348, 2011, 0],
"map": "Felucca",
"count": 5,
"minDelay": "00:05:00",
"maxDelay": "00:10:00",
"team": 0,
"homeRange": 10,
"walkingRange": 15,
"entries": [
{ "name": "OrcBomber", "maxCount": 1, "probability": 100 },
{ "name": "EarthElemental", "maxCount": 4, "probability": 100 }
]
},
{
"$type": "Spawner",
"guid": "d01dca90-9fac-438e-9660-a93e9df7fc58",
"name": "Spawner (212)",
"location": [5281, 2029, 0],
"map": "Felucca",
"count": 3,
"minDelay": "00:05:00",
"maxDelay": "00:10:00",
"team": 0,
"homeRange": 8,
"walkingRange": 8,
"entries": [
{ "name": "Orc", "maxCount": 3, "probability": 100 }
]
},
{
"$type": "Spawner",
"guid": "d313d866-1199-45fc-a72c-018d146bbe8c",
"name": "Spawner (212)",
"location": [5300, 1314, 0],
@ -221,24 +288,10 @@
]
},
{
"type": "Spawner",
"guid": "81a40400-c93d-42f3-8f19-0e9492198a82",
"$type": "Spawner",
"guid": "ee800978-f2bf-47e0-8e19-c7f5fe0b33bc",
"name": "Spawner (212)",
"location": [5292, 1316, 0],
"map": "Felucca",
"count": 2,
"minDelay": "00:05:00",
"maxDelay": "00:10:00",
"team": 0,
"homeRange": 8,
"walkingRange": 8,
"entries": [{ "name": "Corpser", "maxCount": 2, "probability": 100 }]
},
{
"type": "Spawner",
"guid": "d01dca90-9fac-438e-9660-a93e9df7fc58",
"name": "Spawner (212)",
"location": [5281, 2029, 0],
"location": [5332, 1365, 0],
"map": "Felucca",
"count": 3,
"minDelay": "00:05:00",
@ -246,57 +299,24 @@
"team": 0,
"homeRange": 8,
"walkingRange": 8,
"entries": [{ "name": "Orc", "maxCount": 3, "probability": 100 }]
},
{
"type": "Spawner",
"guid": "1bff583d-3643-4e60-9a44-e3dea6882a02",
"name": "Spawner (212)",
"location": [5308, 2005, 0],
"map": "Felucca",
"count": 5,
"minDelay": "00:05:00",
"maxDelay": "00:10:00",
"team": 0,
"homeRange": 12,
"walkingRange": 12,
"entries": [
{ "name": "OrcBrute", "maxCount": 1, "probability": 100 },
{ "name": "Orc", "maxCount": 4, "probability": 100 }
{ "name": "Orc", "maxCount": 3, "probability": 100 }
]
},
{
"type": "Spawner",
"guid": "2d24f1f9-20ff-4ee1-8f31-8304e2e4822d",
"$type": "Spawner",
"guid": "fe0be93b-5f45-40b0-9169-16df378c0e6e",
"name": "Spawner (212)",
"location": [5310, 1969, 0],
"location": [5141, 1968, 0],
"map": "Felucca",
"count": 4,
"count": 3,
"minDelay": "00:05:00",
"maxDelay": "00:10:00",
"team": 0,
"homeRange": 12,
"walkingRange": 12,
"homeRange": 8,
"walkingRange": 8,
"entries": [
{ "name": "OrcBomber", "maxCount": 2, "probability": 100 },
{ "name": "Orc", "maxCount": 2, "probability": 100 }
]
},
{
"type": "Spawner",
"guid": "a70f186a-94a1-4f54-963f-5c039c7d4006",
"name": "Spawner (212)",
"location": [5348, 2011, 0],
"map": "Felucca",
"count": 5,
"minDelay": "00:05:00",
"maxDelay": "00:10:00",
"team": 0,
"homeRange": 10,
"walkingRange": 15,
"entries": [
{ "name": "OrcBomber", "maxCount": 1, "probability": 100 },
{ "name": "EarthElemental", "maxCount": 4, "probability": 100 }
{ "name": "OrcCaptain", "maxCount": 3, "probability": 100 }
]
}
]

View file

@ -1,6 +1,6 @@
[
{
"type": "Spawner",
"$type": "Spawner",
"guid": "2733b112-93bf-47cc-b071-02e87c3150ab",
"name": "Spawner (214)",
"location": [6279, 879, -1],
@ -21,7 +21,7 @@
]
},
{
"type": "Spawner",
"$type": "Spawner",
"guid": "bc6475e8-885e-4e5e-bf83-49c3e408ac06",
"name": "Spawner (214)",
"location": [6275, 879, -2],

View file

@ -1,30 +1,61 @@
[
{
"type": "Spawner",
"guid": "604b87ed-6718-4802-b1b6-f572ed7d9935",
"$type": "Spawner",
"guid": "04a75e0b-4499-4fe6-8bd6-49e3dbf0d528",
"name": "Spawner (215)",
"location": [6295, 616, -50],
"location": [6416, 447, -40],
"map": "Felucca",
"count": 27,
"count": 15,
"minDelay": "00:05:00",
"maxDelay": "00:10:00",
"team": 0,
"homeRange": 50,
"walkingRange": 50,
"homeRange": 20,
"walkingRange": 20,
"entries": [
{ "name": "Daemon", "maxCount": 6, "probability": 100 },
{ "name": "Succubus", "maxCount": 6, "probability": 100 },
{ "name": "InterredGrizzle", "maxCount": 6, "probability": 100 },
{ "name": "PoisonElemental", "maxCount": 6, "probability": 100 },
{ "name": "PlagueBeast", "maxCount": 6, "probability": 100 },
{ "name": "PlagueSpawn", "maxCount": 6, "probability": 100 },
{ "name": "CorrosiveSlime", "maxCount": 3, "probability": 100 },
{ "name": "Putrefier", "maxCount": 3, "probability": 100 },
{ "name": "PlagueBeastLord", "maxCount": 3, "probability": 100 }
{ "name": "PlagueBeast", "maxCount": 8, "probability": 100 },
{ "name": "PlagueSpawn", "maxCount": 8, "probability": 100 },
{ "name": "AcidElemental", "maxCount": 7, "probability": 100 },
{ "name": "CorrosiveSlime", "maxCount": 7, "probability": 100 }
]
},
{
"type": "Spawner",
"$type": "Spawner",
"guid": "04cc01ab-f7da-4992-8913-4db4ea63149e",
"name": "Spawner (215)",
"location": [6298, 395, 60],
"map": "Felucca",
"count": 2,
"minDelay": "00:05:00",
"maxDelay": "00:10:00",
"team": 0,
"homeRange": 8,
"walkingRange": 8,
"entries": [
{ "name": "EarthElemental", "maxCount": 2, "probability": 100 },
{ "name": "CorrosiveSlime", "maxCount": 2, "probability": 100 }
]
},
{
"$type": "Spawner",
"guid": "3a0d9e92-6661-4ad8-a38e-dcf7516e05de",
"name": "Spawner (215)",
"location": [6354, 455, -40],
"map": "Felucca",
"count": 10,
"minDelay": "00:05:00",
"maxDelay": "00:10:00",
"team": 0,
"homeRange": 20,
"walkingRange": 20,
"entries": [
{ "name": "PlagueBeast", "maxCount": 5, "probability": 100 },
{ "name": "CorrosiveSlime", "maxCount": 5, "probability": 100 },
{ "name": "PoisonElemental", "maxCount": 5, "probability": 100 },
{ "name": "PlagueSpawn", "maxCount": 5, "probability": 100 }
]
},
{
"$type": "Spawner",
"guid": "5a586bde-b6a0-4f4d-b147-1e1886aae735",
"name": "Spawner (215)",
"location": [6384, 580, -50],
@ -48,139 +79,7 @@
]
},
{
"type": "Spawner",
"guid": "e695b242-221c-49b5-853a-9e943afe8355",
"name": "Spawner (215)",
"location": [6328, 517, -50],
"map": "Felucca",
"count": 4,
"minDelay": "00:05:00",
"maxDelay": "00:10:00",
"team": 0,
"homeRange": 10,
"walkingRange": 10,
"entries": [{ "name": "PoisonElemental", "maxCount": 4, "probability": 100 }]
},
{
"type": "Spawner",
"guid": "3a0d9e92-6661-4ad8-a38e-dcf7516e05de",
"name": "Spawner (215)",
"location": [6354, 455, -40],
"map": "Felucca",
"count": 10,
"minDelay": "00:05:00",
"maxDelay": "00:10:00",
"team": 0,
"homeRange": 20,
"walkingRange": 20,
"entries": [
{ "name": "PlagueBeast", "maxCount": 5, "probability": 100 },
{ "name": "CorrosiveSlime", "maxCount": 5, "probability": 100 },
{ "name": "PoisonElemental", "maxCount": 5, "probability": 100 },
{ "name": "PlagueSpawn", "maxCount": 5, "probability": 100 }
]
},
{
"type": "Spawner",
"guid": "04a75e0b-4499-4fe6-8bd6-49e3dbf0d528",
"name": "Spawner (215)",
"location": [6416, 447, -40],
"map": "Felucca",
"count": 15,
"minDelay": "00:05:00",
"maxDelay": "00:10:00",
"team": 0,
"homeRange": 20,
"walkingRange": 20,
"entries": [
{ "name": "PlagueBeast", "maxCount": 8, "probability": 100 },
{ "name": "PlagueSpawn", "maxCount": 8, "probability": 100 },
{ "name": "AcidElemental", "maxCount": 7, "probability": 100 },
{ "name": "CorrosiveSlime", "maxCount": 7, "probability": 100 }
]
},
{
"type": "Spawner",
"guid": "ad3fbc20-7914-4651-a7c1-994d9b88ed22",
"name": "Spawner (215)",
"location": [6466, 543, -50],
"map": "Felucca",
"count": 22,
"minDelay": "00:05:00",
"maxDelay": "00:10:00",
"team": 0,
"homeRange": 30,
"walkingRange": 30,
"entries": [
{ "name": "Balron", "maxCount": 6, "probability": 100 },
{ "name": "Succubus", "maxCount": 6, "probability": 100 },
{ "name": "Daemon", "maxCount": 6, "probability": 100 },
{ "name": "CorrosiveSlime", "maxCount": 3, "probability": 100 },
{ "name": "PlagueBeastLord", "maxCount": 8, "probability": 100 },
{ "name": "PlagueBeast", "maxCount": 8, "probability": 100 },
{ "name": "PlagueSpawn", "maxCount": 8, "probability": 100 },
{ "name": "InterredGrizzle", "maxCount": 5, "probability": 100 },
{ "name": "PoisonElemental", "maxCount": 5, "probability": 100 }
]
},
{
"type": "Spawner",
"guid": "b7da2a27-b13e-4113-b8c8-e5f2e7ac2e3a",
"name": "Spawner (215)",
"location": [6292, 462, -50],
"map": "Felucca",
"count": 18,
"minDelay": "00:05:00",
"maxDelay": "00:10:00",
"team": 0,
"homeRange": 40,
"walkingRange": 40,
"entries": [
{ "name": "ChaosDaemon", "maxCount": 7, "probability": 100 },
{ "name": "Moloch", "maxCount": 7, "probability": 100 },
{ "name": "Succubus", "maxCount": 7, "probability": 100 },
{ "name": "Daemon", "maxCount": 7, "probability": 100 },
{ "name": "Balron", "maxCount": 4, "probability": 100 }
]
},
{
"type": "Spawner",
"guid": "04cc01ab-f7da-4992-8913-4db4ea63149e",
"name": "Spawner (215)",
"location": [6298, 395, 60],
"map": "Felucca",
"count": 2,
"minDelay": "00:05:00",
"maxDelay": "00:10:00",
"team": 0,
"homeRange": 8,
"walkingRange": 8,
"entries": [
{ "name": "EarthElemental", "maxCount": 2, "probability": 100 },
{ "name": "CorrosiveSlime", "maxCount": 2, "probability": 100 }
]
},
{
"type": "Spawner",
"guid": "74269ae8-5e94-4496-ad04-611e81b14168",
"name": "Spawner (215)",
"location": [6404, 378, -40],
"map": "Felucca",
"count": 4,
"minDelay": "00:05:00",
"maxDelay": "00:10:00",
"team": 0,
"homeRange": 18,
"walkingRange": 18,
"entries": [
{ "name": "AcidElemental", "maxCount": 2, "probability": 100 },
{ "name": "PlagueSpawn", "maxCount": 2, "probability": 100 },
{ "name": "AcidElemental", "maxCount": 1, "probability": 100 },
{ "name": "PlagueSpawn", "maxCount": 1, "probability": 100 }
]
},
{
"type": "Spawner",
"$type": "Spawner",
"guid": "5ef37566-7b9c-47fb-a88c-d6c0177d92f3",
"name": "Spawner (215)",
"location": [6418, 358, -40],
@ -205,7 +104,148 @@
]
},
{
"type": "Spawner",
"$type": "Spawner",
"guid": "604b87ed-6718-4802-b1b6-f572ed7d9935",
"name": "Spawner (215)",
"location": [6295, 616, -50],
"map": "Felucca",
"count": 27,
"minDelay": "00:05:00",
"maxDelay": "00:10:00",
"team": 0,
"homeRange": 50,
"walkingRange": 50,
"entries": [
{ "name": "Daemon", "maxCount": 6, "probability": 100 },
{ "name": "Succubus", "maxCount": 6, "probability": 100 },
{ "name": "InterredGrizzle", "maxCount": 6, "probability": 100 },
{ "name": "PoisonElemental", "maxCount": 6, "probability": 100 },
{ "name": "PlagueBeast", "maxCount": 6, "probability": 100 },
{ "name": "PlagueSpawn", "maxCount": 6, "probability": 100 },
{ "name": "CorrosiveSlime", "maxCount": 3, "probability": 100 },
{ "name": "Putrefier", "maxCount": 3, "probability": 100 },
{ "name": "PlagueBeastLord", "maxCount": 3, "probability": 100 }
]
},
{
"$type": "Spawner",
"guid": "74269ae8-5e94-4496-ad04-611e81b14168",
"name": "Spawner (215)",
"location": [6404, 378, -40],
"map": "Felucca",
"count": 4,
"minDelay": "00:05:00",
"maxDelay": "00:10:00",
"team": 0,
"homeRange": 18,
"walkingRange": 18,
"entries": [
{ "name": "AcidElemental", "maxCount": 2, "probability": 100 },
{ "name": "PlagueSpawn", "maxCount": 2, "probability": 100 },
{ "name": "AcidElemental", "maxCount": 1, "probability": 100 },
{ "name": "PlagueSpawn", "maxCount": 1, "probability": 100 }
]
},
{
"$type": "Spawner",
"guid": "9c1149a5-ac2a-42e6-b6e2-296e6ca05602",
"name": "Spawner (215)",
"location": [6249, 352, 60],
"map": "Felucca",
"count": 9,
"minDelay": "00:05:00",
"maxDelay": "00:10:00",
"team": 0,
"homeRange": 15,
"walkingRange": 15,
"entries": [
{ "name": "AcidElemental", "maxCount": 6, "probability": 100 },
{ "name": "EarthElemental", "maxCount": 6, "probability": 100 },
{ "name": "CorrosiveSlime", "maxCount": 6, "probability": 100 },
{ "name": "AcidElemental", "maxCount": 1, "probability": 100 },
{ "name": "EarthElemental", "maxCount": 1, "probability": 100 },
{ "name": "CorrosiveSlime", "maxCount": 1, "probability": 100 }
]
},
{
"$type": "Spawner",
"guid": "ad3fbc20-7914-4651-a7c1-994d9b88ed22",
"name": "Spawner (215)",
"location": [6466, 543, -50],
"map": "Felucca",
"count": 22,
"minDelay": "00:05:00",
"maxDelay": "00:10:00",
"team": 0,
"homeRange": 30,
"walkingRange": 30,
"entries": [
{ "name": "Balron", "maxCount": 6, "probability": 100 },
{ "name": "Succubus", "maxCount": 6, "probability": 100 },
{ "name": "Daemon", "maxCount": 6, "probability": 100 },
{ "name": "CorrosiveSlime", "maxCount": 3, "probability": 100 },
{ "name": "PlagueBeastLord", "maxCount": 8, "probability": 100 },
{ "name": "PlagueBeast", "maxCount": 8, "probability": 100 },
{ "name": "PlagueSpawn", "maxCount": 8, "probability": 100 },
{ "name": "InterredGrizzle", "maxCount": 5, "probability": 100 },
{ "name": "PoisonElemental", "maxCount": 5, "probability": 100 }
]
},
{
"$type": "Spawner",
"guid": "af6ef316-bb00-417f-852c-14e9c98ca74c",
"name": "Spawner (215)",
"location": [6285, 353, 60],
"map": "Felucca",
"count": 1,
"minDelay": "00:05:00",
"maxDelay": "00:10:00",
"team": 0,
"homeRange": 5,
"walkingRange": 5,
"entries": [
{ "name": "PoisonElemental", "maxCount": 1, "probability": 100 }
]
},
{
"$type": "Spawner",
"guid": "b7da2a27-b13e-4113-b8c8-e5f2e7ac2e3a",
"name": "Spawner (215)",
"location": [6292, 462, -50],
"map": "Felucca",
"count": 18,
"minDelay": "00:05:00",
"maxDelay": "00:10:00",
"team": 0,
"homeRange": 40,
"walkingRange": 40,
"entries": [
{ "name": "ChaosDaemon", "maxCount": 7, "probability": 100 },
{ "name": "Moloch", "maxCount": 7, "probability": 100 },
{ "name": "Succubus", "maxCount": 7, "probability": 100 },
{ "name": "Daemon", "maxCount": 7, "probability": 100 },
{ "name": "Balron", "maxCount": 4, "probability": 100 }
]
},
{
"$type": "Spawner",
"guid": "bbed003d-43d6-4690-b93e-763f42a4187f",
"name": "Spawner (215)",
"location": [6353, 400, 60],
"map": "Felucca",
"count": 4,
"minDelay": "00:05:00",
"maxDelay": "00:10:00",
"team": 0,
"homeRange": 10,
"walkingRange": 10,
"entries": [
{ "name": "EarthElemental", "maxCount": 4, "probability": 100 },
{ "name": "CorrosiveSlime", "maxCount": 4, "probability": 100 }
]
},
{
"$type": "Spawner",
"guid": "d3ba2375-fc73-4996-9f54-3a09dc39c653",
"name": "Spawner (215)",
"location": [6319, 348, 60],
@ -228,45 +268,10 @@
]
},
{
"type": "Spawner",
"guid": "9c1149a5-ac2a-42e6-b6e2-296e6ca05602",
"$type": "Spawner",
"guid": "e695b242-221c-49b5-853a-9e943afe8355",
"name": "Spawner (215)",
"location": [6249, 352, 60],
"map": "Felucca",
"count": 9,
"minDelay": "00:05:00",
"maxDelay": "00:10:00",
"team": 0,
"homeRange": 15,
"walkingRange": 15,
"entries": [
{ "name": "AcidElemental", "maxCount": 6, "probability": 100 },
{ "name": "EarthElemental", "maxCount": 6, "probability": 100 },
{ "name": "CorrosiveSlime", "maxCount": 6, "probability": 100 },
{ "name": "AcidElemental", "maxCount": 1, "probability": 100 },
{ "name": "EarthElemental", "maxCount": 1, "probability": 100 },
{ "name": "CorrosiveSlime", "maxCount": 1, "probability": 100 }
]
},
{
"type": "Spawner",
"guid": "af6ef316-bb00-417f-852c-14e9c98ca74c",
"name": "Spawner (215)",
"location": [6285, 353, 60],
"map": "Felucca",
"count": 1,
"minDelay": "00:05:00",
"maxDelay": "00:10:00",
"team": 0,
"homeRange": 5,
"walkingRange": 5,
"entries": [{ "name": "PoisonElemental", "maxCount": 1, "probability": 100 }]
},
{
"type": "Spawner",
"guid": "bbed003d-43d6-4690-b93e-763f42a4187f",
"name": "Spawner (215)",
"location": [6353, 400, 60],
"location": [6328, 517, -50],
"map": "Felucca",
"count": 4,
"minDelay": "00:05:00",
@ -275,8 +280,7 @@
"homeRange": 10,
"walkingRange": 10,
"entries": [
{ "name": "EarthElemental", "maxCount": 4, "probability": 100 },
{ "name": "CorrosiveSlime", "maxCount": 4, "probability": 100 }
{ "name": "PoisonElemental", "maxCount": 4, "probability": 100 }
]
}
]

View file

@ -1,56 +1,6 @@
[
{
"type": "Spawner",
"guid": "b87b2fe5-b1dc-46b1-9c35-f1627c33ad04",
"name": "Spawner (216)",
"location": [6553, 157, 0],
"map": "Felucca",
"count": 3,
"minDelay": "00:05:00",
"maxDelay": "00:10:00",
"team": 0,
"homeRange": 8,
"walkingRange": 8,
"entries": [{ "name": "CrystalLatticeSeeker", "maxCount": 3, "probability": 100 }]
},
{
"type": "Spawner",
"guid": "c04125c7-bad1-4747-8e55-7664e9797a6d",
"name": "Spawner (216)",
"location": [6474, 74, -34],
"map": "Felucca",
"count": 9,
"minDelay": "00:05:00",
"maxDelay": "00:10:00",
"team": 0,
"homeRange": 4,
"walkingRange": 4,
"entries": [
{ "name": "Rat", "maxCount": 9, "probability": 100 },
{ "name": "IceSnake", "maxCount": 9, "probability": 100 }
]
},
{
"type": "Spawner",
"guid": "f083b3bb-c6c8-4774-a9d7-65d6b2cfc831",
"name": "Spawner (216)",
"location": [6552, 155, 0],
"map": "Felucca",
"count": 6,
"minDelay": "00:05:00",
"maxDelay": "00:10:00",
"team": 0,
"homeRange": 15,
"walkingRange": 15,
"entries": [
{ "name": "Wisp", "maxCount": 5, "probability": 100 },
{ "name": "CrystalWisp", "maxCount": 5, "probability": 100 },
{ "name": "TreasureChestLevel2", "maxCount": 5, "probability": 100 },
{ "name": "TreasureChestLevel2", "maxCount": 1, "probability": 100 }
]
},
{
"type": "Spawner",
"$type": "Spawner",
"guid": "3ccfb405-2b18-4397-9d18-5b37302d62e4",
"name": "Spawner (216)",
"location": [6521, 139, -20],
@ -61,10 +11,12 @@
"team": 0,
"homeRange": 4,
"walkingRange": 4,
"entries": [{ "name": "Wisp", "maxCount": 1, "probability": 100 }]
"entries": [
{ "name": "Wisp", "maxCount": 1, "probability": 100 }
]
},
{
"type": "Spawner",
"$type": "Spawner",
"guid": "49f09e2e-fb04-42a9-96e2-d930c04f729f",
"name": "Spawner (216)",
"location": [6530, 139, -20],
@ -75,125 +27,12 @@
"team": 0,
"homeRange": 4,
"walkingRange": 4,
"entries": [{ "name": "Wisp", "maxCount": 1, "probability": 100 }]
},
{
"type": "Spawner",
"guid": "f620b501-c242-4301-a813-b4d7a44e141f",
"name": "Spawner (216)",
"location": [6576, 91, 0],
"map": "Felucca",
"count": 2,
"minDelay": "00:05:00",
"maxDelay": "00:10:00",
"team": 0,
"homeRange": 4,
"walkingRange": 4,
"entries": [{ "name": "CrystalSeaSerpent", "maxCount": 2, "probability": 100 }]
},
{
"type": "Spawner",
"guid": "539ddfd6-d1ad-4e3f-b9cf-414a6dc0553a",
"name": "Spawner (216)",
"location": [6509, 176, 0],
"map": "Felucca",
"count": 2,
"minDelay": "00:05:00",
"maxDelay": "00:10:00",
"team": 0,
"homeRange": 4,
"walkingRange": 4,
"entries": [{ "name": "UnfrozenMummy", "maxCount": 2, "probability": 100 }]
},
{
"type": "Spawner",
"guid": "84ae0146-70ef-44e4-9159-db9a045c14c9",
"name": "Spawner (216)",
"location": [6536, 79, -10],
"map": "Felucca",
"count": 1,
"minDelay": "00:05:00",
"maxDelay": "00:10:00",
"team": 0,
"homeRange": 4,
"walkingRange": 4,
"entries": [{ "name": "CrystalDaemon", "maxCount": 1, "probability": 100 }]
},
{
"type": "Spawner",
"guid": "9c7afc30-19c5-4034-a344-8b75e52b6b40",
"name": "Spawner (216)",
"location": [6547, 91, -10],
"map": "Felucca",
"count": 1,
"minDelay": "00:05:00",
"maxDelay": "00:10:00",
"team": 0,
"homeRange": 4,
"walkingRange": 4,
"entries": [{ "name": "CrystalDaemon", "maxCount": 1, "probability": 100 }]
},
{
"type": "Spawner",
"guid": "50809274-807a-49ee-b9f9-6c13693a56d0",
"name": "Spawner (216)",
"location": [6572, 89, 0],
"map": "Felucca",
"count": 2,
"minDelay": "00:05:00",
"maxDelay": "00:10:00",
"team": 0,
"homeRange": 4,
"walkingRange": 4,
"entries": [{ "name": "CrystalHydra", "maxCount": 2, "probability": 100 }]
},
{
"type": "Spawner",
"guid": "8f88efe1-68b8-478e-8166-b03984e956e1",
"name": "Spawner (216)",
"location": [6504, 181, 0],
"map": "Felucca",
"count": 1,
"minDelay": "00:05:00",
"maxDelay": "00:10:00",
"team": 0,
"homeRange": 1,
"walkingRange": 1,
"entries": [{ "name": "TreasureChestLevel4", "maxCount": 1, "probability": 100 }]
},
{
"type": "Spawner",
"guid": "93922052-811a-473e-b3b2-eddf5380829a",
"name": "Spawner (216)",
"location": [6475, 101, -44],
"map": "Felucca",
"count": 5,
"minDelay": "00:05:00",
"maxDelay": "00:10:00",
"team": 0,
"homeRange": 5,
"walkingRange": 5,
"entries": [{ "name": "ShadowWisp", "maxCount": 5, "probability": 100 }]
},
{
"type": "Spawner",
"guid": "c66d4bde-6308-44bb-80fe-f6f0070beda7",
"name": "Spawner (216)",
"location": [6478, 174, 4],
"map": "Felucca",
"count": 6,
"minDelay": "00:05:00",
"maxDelay": "00:10:00",
"team": 0,
"homeRange": 10,
"walkingRange": 10,
"entries": [
{ "name": "Wisp", "maxCount": 6, "probability": 100 },
{ "name": "CrystalWisp", "maxCount": 6, "probability": 100 }
{ "name": "Wisp", "maxCount": 1, "probability": 100 }
]
},
{
"type": "Spawner",
"$type": "Spawner",
"guid": "4e01097c-b1f4-4d22-ad9b-dcd117eeaeec",
"name": "Spawner (216)",
"location": [6566, 120, 0],
@ -210,10 +49,10 @@
]
},
{
"type": "Spawner",
"guid": "9dc74009-4df1-4872-874f-fcc908ec105b",
"$type": "Spawner",
"guid": "50809274-807a-49ee-b9f9-6c13693a56d0",
"name": "Spawner (216)",
"location": [6534, 178, 0],
"location": [6572, 89, 0],
"map": "Felucca",
"count": 2,
"minDelay": "00:05:00",
@ -221,10 +60,124 @@
"team": 0,
"homeRange": 4,
"walkingRange": 4,
"entries": [{ "name": "CrystalWisp", "maxCount": 2, "probability": 100 }]
"entries": [
{ "name": "CrystalHydra", "maxCount": 2, "probability": 100 }
]
},
{
"type": "Spawner",
"$type": "Spawner",
"guid": "539ddfd6-d1ad-4e3f-b9cf-414a6dc0553a",
"name": "Spawner (216)",
"location": [6509, 176, 0],
"map": "Felucca",
"count": 2,
"minDelay": "00:05:00",
"maxDelay": "00:10:00",
"team": 0,
"homeRange": 4,
"walkingRange": 4,
"entries": [
{ "name": "UnfrozenMummy", "maxCount": 2, "probability": 100 }
]
},
{
"$type": "Spawner",
"guid": "6912561a-5610-4441-8c64-16f4bd9f73f8",
"name": "Spawner (216)",
"location": [6510, 171, 0],
"map": "Felucca",
"count": 3,
"minDelay": "00:05:00",
"maxDelay": "00:10:00",
"team": 0,
"homeRange": 8,
"walkingRange": 8,
"entries": [
{ "name": "Protector", "maxCount": 3, "probability": 100 }
]
},
{
"$type": "Spawner",
"guid": "84ae0146-70ef-44e4-9159-db9a045c14c9",
"name": "Spawner (216)",
"location": [6536, 79, -10],
"map": "Felucca",
"count": 1,
"minDelay": "00:05:00",
"maxDelay": "00:10:00",
"team": 0,
"homeRange": 4,
"walkingRange": 4,
"entries": [
{ "name": "CrystalDaemon", "maxCount": 1, "probability": 100 }
]
},
{
"$type": "Spawner",
"guid": "8f88efe1-68b8-478e-8166-b03984e956e1",
"name": "Spawner (216)",
"location": [6504, 181, 0],
"map": "Felucca",
"count": 1,
"minDelay": "00:05:00",
"maxDelay": "00:10:00",
"team": 0,
"homeRange": 1,
"walkingRange": 1,
"entries": [
{ "name": "TreasureChestLevel4", "maxCount": 1, "probability": 100 }
]
},
{
"$type": "Spawner",
"guid": "8fe6c0c3-9fcb-4725-b6b2-ee89df036d80",
"name": "Spawner (216)",
"location": [6532, 79, -10],
"map": "Felucca",
"count": 1,
"minDelay": "00:05:00",
"maxDelay": "00:10:00",
"team": 0,
"homeRange": 4,
"walkingRange": 4,
"entries": [
{ "name": "CrystalDaemon", "maxCount": 1, "probability": 100 }
]
},
{
"$type": "Spawner",
"guid": "93922052-811a-473e-b3b2-eddf5380829a",
"name": "Spawner (216)",
"location": [6475, 101, -44],
"map": "Felucca",
"count": 5,
"minDelay": "00:05:00",
"maxDelay": "00:10:00",
"team": 0,
"homeRange": 5,
"walkingRange": 5,
"entries": [
{ "name": "ShadowWisp", "maxCount": 5, "probability": 100 }
]
},
{
"$type": "Spawner",
"guid": "9c7afc30-19c5-4034-a344-8b75e52b6b40",
"name": "Spawner (216)",
"location": [6547, 91, -10],
"map": "Felucca",
"count": 1,
"minDelay": "00:05:00",
"maxDelay": "00:10:00",
"team": 0,
"homeRange": 4,
"walkingRange": 4,
"entries": [
{ "name": "CrystalDaemon", "maxCount": 1, "probability": 100 }
]
},
{
"$type": "Spawner",
"guid": "9ce6b636-4bca-44d7-adf2-5ef841d08a61",
"name": "Spawner (216)",
"location": [6535, 179, 0],
@ -243,7 +196,23 @@
]
},
{
"type": "Spawner",
"$type": "Spawner",
"guid": "9dc74009-4df1-4872-874f-fcc908ec105b",
"name": "Spawner (216)",
"location": [6534, 178, 0],
"map": "Felucca",
"count": 2,
"minDelay": "00:05:00",
"maxDelay": "00:10:00",
"team": 0,
"homeRange": 4,
"walkingRange": 4,
"entries": [
{ "name": "CrystalWisp", "maxCount": 2, "probability": 100 }
]
},
{
"$type": "Spawner",
"guid": "adf86a17-213e-408b-be55-3cb37b9a1a36",
"name": "Spawner (216)",
"location": [6502, 120, -10],
@ -254,10 +223,62 @@
"team": 0,
"homeRange": 0,
"walkingRange": 0,
"entries": [{ "name": "TreasureChestLevel3", "maxCount": 1, "probability": 100 }]
"entries": [
{ "name": "TreasureChestLevel3", "maxCount": 1, "probability": 100 }
]
},
{
"type": "Spawner",
"$type": "Spawner",
"guid": "b87b2fe5-b1dc-46b1-9c35-f1627c33ad04",
"name": "Spawner (216)",
"location": [6553, 157, 0],
"map": "Felucca",
"count": 3,
"minDelay": "00:05:00",
"maxDelay": "00:10:00",
"team": 0,
"homeRange": 8,
"walkingRange": 8,
"entries": [
{ "name": "CrystalLatticeSeeker", "maxCount": 3, "probability": 100 }
]
},
{
"$type": "Spawner",
"guid": "c04125c7-bad1-4747-8e55-7664e9797a6d",
"name": "Spawner (216)",
"location": [6474, 74, -34],
"map": "Felucca",
"count": 9,
"minDelay": "00:05:00",
"maxDelay": "00:10:00",
"team": 0,
"homeRange": 4,
"walkingRange": 4,
"entries": [
{ "name": "Rat", "maxCount": 9, "probability": 100 },
{ "name": "IceSnake", "maxCount": 9, "probability": 100 }
]
},
{
"$type": "Spawner",
"guid": "c66d4bde-6308-44bb-80fe-f6f0070beda7",
"name": "Spawner (216)",
"location": [6478, 174, 4],
"map": "Felucca",
"count": 6,
"minDelay": "00:05:00",
"maxDelay": "00:10:00",
"team": 0,
"homeRange": 10,
"walkingRange": 10,
"entries": [
{ "name": "Wisp", "maxCount": 6, "probability": 100 },
{ "name": "CrystalWisp", "maxCount": 6, "probability": 100 }
]
},
{
"$type": "Spawner",
"guid": "e82c7115-8675-4d30-b321-c5e6a3b43ea0",
"name": "Spawner (216)",
"location": [6507, 151, 0],
@ -268,24 +289,12 @@
"team": 0,
"homeRange": 5,
"walkingRange": 5,
"entries": [{ "name": "Protector", "maxCount": 3, "probability": 100 }]
"entries": [
{ "name": "Protector", "maxCount": 3, "probability": 100 }
]
},
{
"type": "Spawner",
"guid": "8fe6c0c3-9fcb-4725-b6b2-ee89df036d80",
"name": "Spawner (216)",
"location": [6532, 79, -10],
"map": "Felucca",
"count": 1,
"minDelay": "00:05:00",
"maxDelay": "00:10:00",
"team": 0,
"homeRange": 4,
"walkingRange": 4,
"entries": [{ "name": "CrystalDaemon", "maxCount": 1, "probability": 100 }]
},
{
"type": "Spawner",
"$type": "Spawner",
"guid": "ed613a4e-b19f-422e-943c-c94a29411ff8",
"name": "Spawner (216)",
"location": [6578, 183, 31],
@ -296,20 +305,43 @@
"team": 0,
"homeRange": 5,
"walkingRange": 5,
"entries": [{ "name": "Ferret", "maxCount": 2, "probability": 100 }]
"entries": [
{ "name": "Ferret", "maxCount": 2, "probability": 100 }
]
},
{
"type": "Spawner",
"guid": "6912561a-5610-4441-8c64-16f4bd9f73f8",
"$type": "Spawner",
"guid": "f083b3bb-c6c8-4774-a9d7-65d6b2cfc831",
"name": "Spawner (216)",
"location": [6510, 171, 0],
"location": [6552, 155, 0],
"map": "Felucca",
"count": 3,
"count": 6,
"minDelay": "00:05:00",
"maxDelay": "00:10:00",
"team": 0,
"homeRange": 8,
"walkingRange": 8,
"entries": [{ "name": "Protector", "maxCount": 3, "probability": 100 }]
"homeRange": 15,
"walkingRange": 15,
"entries": [
{ "name": "Wisp", "maxCount": 5, "probability": 100 },
{ "name": "CrystalWisp", "maxCount": 5, "probability": 100 },
{ "name": "TreasureChestLevel2", "maxCount": 5, "probability": 100 },
{ "name": "TreasureChestLevel2", "maxCount": 1, "probability": 100 }
]
},
{
"$type": "Spawner",
"guid": "f620b501-c242-4301-a813-b4d7a44e141f",
"name": "Spawner (216)",
"location": [6576, 91, 0],
"map": "Felucca",
"count": 2,
"minDelay": "00:05:00",
"maxDelay": "00:10:00",
"team": 0,
"homeRange": 4,
"walkingRange": 4,
"entries": [
{ "name": "CrystalSeaSerpent", "maxCount": 2, "probability": 100 }
]
}
]

View file

@ -1,29 +1,6 @@
[
{
"type": "Spawner",
"guid": "1511b1c9-952f-4b34-a527-40c8458fd839",
"name": "Spawner (217)",
"location": [6263, 118, -10],
"map": "Felucca",
"count": 24,
"minDelay": "00:05:00",
"maxDelay": "00:10:00",
"team": 0,
"homeRange": 20,
"walkingRange": 20,
"entries": [
{ "name": "Chicken", "maxCount": 6, "probability": 100 },
{ "name": "Bird", "maxCount": 6, "probability": 100 },
{ "name": "Dog", "maxCount": 3, "probability": 100 },
{ "name": "Cat", "maxCount": 3, "probability": 100 },
{ "name": "Cow", "maxCount": 6, "probability": 100 },
{ "name": "Bull", "maxCount": 6, "probability": 100 },
{ "name": "Pig", "maxCount": 3, "probability": 100 },
{ "name": "Boar", "maxCount": 3, "probability": 100 }
]
},
{
"type": "Spawner",
"$type": "Spawner",
"guid": "072beafe-a629-4c48-b5ff-b984254a5d46",
"name": "Spawner (217)",
"location": [6349, 89, -20],
@ -50,110 +27,30 @@
]
},
{
"type": "Spawner",
"guid": "f96a5381-773a-4b5a-af6a-4cc8d2d08811",
"$type": "Spawner",
"guid": "1511b1c9-952f-4b34-a527-40c8458fd839",
"name": "Spawner (217)",
"location": [6168, 117, 0],
"location": [6263, 118, -10],
"map": "Felucca",
"count": 34,
"count": 24,
"minDelay": "00:05:00",
"maxDelay": "00:10:00",
"team": 0,
"homeRange": 30,
"walkingRange": 30,
"homeRange": 20,
"walkingRange": 20,
"entries": [
{ "name": "Ratman", "maxCount": 11, "probability": 100 },
{ "name": "RatmanArcher", "maxCount": 11, "probability": 100 },
{ "name": "RatmanMage", "maxCount": 11, "probability": 100 },
{ "name": "Changeling", "maxCount": 11, "probability": 100 },
{ "name": "Doppleganger", "maxCount": 11, "probability": 100 },
{ "name": "Dog", "maxCount": 9, "probability": 100 },
{ "name": "Chicken", "maxCount": 9, "probability": 100 },
{ "name": "Cat", "maxCount": 9, "probability": 100 },
{ "name": "Boar", "maxCount": 9, "probability": 100 },
{ "name": "Bird", "maxCount": 9, "probability": 100 },
{ "name": "Gargoyle", "maxCount": 3, "probability": 100 },
{ "name": "Titan", "maxCount": 3, "probability": 100 },
{ "name": "Cyclops", "maxCount": 3, "probability": 100 },
{ "name": "OrcishMage", "maxCount": 5, "probability": 100 },
{ "name": "orcscout", "maxCount": 5, "probability": 100 }
]
},
{
"type": "Spawner",
"guid": "a03d9ae7-cc7d-41c7-83ad-fd30bdb1b5be",
"name": "Spawner (217)",
"location": [6189, 71, 0],
"map": "Felucca",
"count": 12,
"minDelay": "00:05:00",
"maxDelay": "00:10:00",
"team": 0,
"homeRange": 25,
"walkingRange": 25,
"entries": [
{ "name": "OgreLord", "maxCount": 8, "probability": 100 },
{ "name": "OrcishLord", "maxCount": 8, "probability": 100 },
{ "name": "Ratman", "maxCount": 8, "probability": 100 },
{ "name": "Bird", "maxCount": 4, "probability": 100 },
{ "name": "Cow", "maxCount": 4, "probability": 100 },
{ "name": "Bull", "maxCount": 4, "probability": 100 },
{ "name": "Pig", "maxCount": 4, "probability": 100 },
{ "name": "Chicken", "maxCount": 4, "probability": 100 }
]
},
{
"type": "Spawner",
"guid": "584c49ef-3604-4c2a-a385-7fca422c604e",
"name": "Spawner (217)",
"location": [6187, 101, 0],
"map": "Felucca",
"count": 34,
"minDelay": "00:05:00",
"maxDelay": "00:10:00",
"team": 0,
"homeRange": 25,
"walkingRange": 25,
"entries": [
{ "name": "Titan", "maxCount": 10, "probability": 100 },
{ "name": "Ettin", "maxCount": 10, "probability": 100 },
{ "name": "Doppleganger", "maxCount": 10, "probability": 100 },
{ "name": "MougGuur", "maxCount": 10, "probability": 100 },
{ "name": "Dog", "maxCount": 10, "probability": 100 },
{ "name": "Chicken", "maxCount": 10, "probability": 100 },
{ "name": "Rat", "maxCount": 10, "probability": 100 },
{ "name": "Cow", "maxCount": 10, "probability": 100 },
{ "name": "Bull", "maxCount": 10, "probability": 100 },
{ "name": "OrcBomber", "maxCount": 7, "probability": 100 },
{ "name": "OrcBrute", "maxCount": 7, "probability": 100 },
{ "name": "Orc", "maxCount": 7, "probability": 100 },
{ "name": "Snake", "maxCount": 7, "probability": 100 },
{ "name": "Gargoyle", "maxCount": 7, "probability": 100 },
{ "name": "Ogre", "maxCount": 7, "probability": 100 }
]
},
{
"type": "Spawner",
"guid": "36e62443-3909-4f76-b267-0ffe2138c841",
"name": "Spawner (217)",
"location": [6184, 29, 0],
"map": "Felucca",
"count": 9,
"minDelay": "00:05:00",
"maxDelay": "00:10:00",
"team": 0,
"homeRange": 10,
"walkingRange": 10,
"entries": [
{ "name": "Bird", "maxCount": 6, "probability": 100 },
{ "name": "Chicken", "maxCount": 6, "probability": 100 },
{ "name": "Rat", "maxCount": 6, "probability": 100 },
{ "name": "Dog", "maxCount": 6, "probability": 100 },
{ "name": "Doppleganger", "maxCount": 3, "probability": 100 }
{ "name": "Bird", "maxCount": 6, "probability": 100 },
{ "name": "Dog", "maxCount": 3, "probability": 100 },
{ "name": "Cat", "maxCount": 3, "probability": 100 },
{ "name": "Cow", "maxCount": 6, "probability": 100 },
{ "name": "Bull", "maxCount": 6, "probability": 100 },
{ "name": "Pig", "maxCount": 3, "probability": 100 },
{ "name": "Boar", "maxCount": 3, "probability": 100 }
]
},
{
"type": "Spawner",
"$type": "Spawner",
"guid": "1be8d7aa-7b8d-48b2-a99e-7e8aadda47ae",
"name": "Spawner (217)",
"location": [6265, 48, -10],
@ -190,7 +87,80 @@
]
},
{
"type": "Spawner",
"$type": "Spawner",
"guid": "36e62443-3909-4f76-b267-0ffe2138c841",
"name": "Spawner (217)",
"location": [6184, 29, 0],
"map": "Felucca",
"count": 9,
"minDelay": "00:05:00",
"maxDelay": "00:10:00",
"team": 0,
"homeRange": 10,
"walkingRange": 10,
"entries": [
{ "name": "Bird", "maxCount": 6, "probability": 100 },
{ "name": "Chicken", "maxCount": 6, "probability": 100 },
{ "name": "Rat", "maxCount": 6, "probability": 100 },
{ "name": "Dog", "maxCount": 6, "probability": 100 },
{ "name": "Doppleganger", "maxCount": 3, "probability": 100 }
]
},
{
"$type": "Spawner",
"guid": "584c49ef-3604-4c2a-a385-7fca422c604e",
"name": "Spawner (217)",
"location": [6187, 101, 0],
"map": "Felucca",
"count": 34,
"minDelay": "00:05:00",
"maxDelay": "00:10:00",
"team": 0,
"homeRange": 25,
"walkingRange": 25,
"entries": [
{ "name": "Titan", "maxCount": 10, "probability": 100 },
{ "name": "Ettin", "maxCount": 10, "probability": 100 },
{ "name": "Doppleganger", "maxCount": 10, "probability": 100 },
{ "name": "MougGuur", "maxCount": 10, "probability": 100 },
{ "name": "Dog", "maxCount": 10, "probability": 100 },
{ "name": "Chicken", "maxCount": 10, "probability": 100 },
{ "name": "Rat", "maxCount": 10, "probability": 100 },
{ "name": "Cow", "maxCount": 10, "probability": 100 },
{ "name": "Bull", "maxCount": 10, "probability": 100 },
{ "name": "OrcBomber", "maxCount": 7, "probability": 100 },
{ "name": "OrcBrute", "maxCount": 7, "probability": 100 },
{ "name": "Orc", "maxCount": 7, "probability": 100 },
{ "name": "Snake", "maxCount": 7, "probability": 100 },
{ "name": "Gargoyle", "maxCount": 7, "probability": 100 },
{ "name": "Ogre", "maxCount": 7, "probability": 100 }
]
},
{
"$type": "Spawner",
"guid": "a03d9ae7-cc7d-41c7-83ad-fd30bdb1b5be",
"name": "Spawner (217)",
"location": [6189, 71, 0],
"map": "Felucca",
"count": 12,
"minDelay": "00:05:00",
"maxDelay": "00:10:00",
"team": 0,
"homeRange": 25,
"walkingRange": 25,
"entries": [
{ "name": "OgreLord", "maxCount": 8, "probability": 100 },
{ "name": "OrcishLord", "maxCount": 8, "probability": 100 },
{ "name": "Ratman", "maxCount": 8, "probability": 100 },
{ "name": "Bird", "maxCount": 4, "probability": 100 },
{ "name": "Cow", "maxCount": 4, "probability": 100 },
{ "name": "Bull", "maxCount": 4, "probability": 100 },
{ "name": "Pig", "maxCount": 4, "probability": 100 },
{ "name": "Chicken", "maxCount": 4, "probability": 100 }
]
},
{
"$type": "Spawner",
"guid": "a976bf7f-b394-4429-92e9-b3dd47eae839",
"name": "Spawner (217)",
"location": [6191, 155, 0],
@ -218,5 +188,35 @@
{ "name": "Gargoyle", "maxCount": 9, "probability": 100 },
{ "name": "Snake", "maxCount": 3, "probability": 100 }
]
},
{
"$type": "Spawner",
"guid": "f96a5381-773a-4b5a-af6a-4cc8d2d08811",
"name": "Spawner (217)",
"location": [6168, 117, 0],
"map": "Felucca",
"count": 34,
"minDelay": "00:05:00",
"maxDelay": "00:10:00",
"team": 0,
"homeRange": 30,
"walkingRange": 30,
"entries": [
{ "name": "Ratman", "maxCount": 11, "probability": 100 },
{ "name": "RatmanArcher", "maxCount": 11, "probability": 100 },
{ "name": "RatmanMage", "maxCount": 11, "probability": 100 },
{ "name": "Changeling", "maxCount": 11, "probability": 100 },
{ "name": "Doppleganger", "maxCount": 11, "probability": 100 },
{ "name": "Dog", "maxCount": 9, "probability": 100 },
{ "name": "Chicken", "maxCount": 9, "probability": 100 },
{ "name": "Cat", "maxCount": 9, "probability": 100 },
{ "name": "Boar", "maxCount": 9, "probability": 100 },
{ "name": "Bird", "maxCount": 9, "probability": 100 },
{ "name": "Gargoyle", "maxCount": 3, "probability": 100 },
{ "name": "Titan", "maxCount": 3, "probability": 100 },
{ "name": "Cyclops", "maxCount": 3, "probability": 100 },
{ "name": "OrcishMage", "maxCount": 5, "probability": 100 },
{ "name": "orcscout", "maxCount": 5, "probability": 100 }
]
}
]

View file

@ -1,46 +1,6 @@
[
{
"type": "Spawner",
"guid": "c08391fb-b609-4c8d-be85-2d3986f1639a",
"name": "Spawner (224)",
"location": [5976, 1387, -22],
"map": "Felucca",
"count": 6,
"minDelay": "00:05:00",
"maxDelay": "00:10:00",
"team": 0,
"homeRange": 10,
"walkingRange": 20,
"entries": [
{ "name": "DreadSpider", "maxCount": 2, "probability": 100 },
{ "name": "FireElemental", "maxCount": 1, "probability": 100 },
{ "name": "HellCat", "maxCount": 1, "probability": 100 },
{ "name": "LavaLizard", "maxCount": 1, "probability": 100 },
{ "name": "LavaSnake", "maxCount": 1, "probability": 100 }
]
},
{
"type": "Spawner",
"guid": "c0481877-f573-4c00-af1f-c1f5873f4bbb",
"name": "Spawner (224)",
"location": [5940, 1356, -2],
"map": "Felucca",
"count": 6,
"minDelay": "00:05:00",
"maxDelay": "00:10:00",
"team": 0,
"homeRange": 10,
"walkingRange": 20,
"entries": [
{ "name": "DreadSpider", "maxCount": 2, "probability": 100 },
{ "name": "FireElemental", "maxCount": 1, "probability": 100 },
{ "name": "HellCat", "maxCount": 1, "probability": 100 },
{ "name": "LavaLizard", "maxCount": 1, "probability": 100 },
{ "name": "LavaSnake", "maxCount": 1, "probability": 100 }
]
},
{
"type": "Spawner",
"$type": "Spawner",
"guid": "047a7695-7bd9-46ad-852d-f84b2895b843",
"name": "Spawner (224)",
"location": [5912, 1326, 0],
@ -60,7 +20,24 @@
]
},
{
"type": "Spawner",
"$type": "Spawner",
"guid": "37ea1620-9487-40df-8035-e76b2443a7b2",
"name": "Spawner (224)",
"location": [5915, 1302, 2],
"map": "Felucca",
"count": 3,
"minDelay": "00:05:00",
"maxDelay": "00:10:00",
"team": 0,
"homeRange": 1,
"walkingRange": 1,
"entries": [
{ "name": "TreasureChestLevel2", "maxCount": 1, "probability": 100 },
{ "name": "TreasureChestLevel3", "maxCount": 2, "probability": 100 }
]
},
{
"$type": "Spawner",
"guid": "8190243a-f39d-48b2-9df3-9b00b6e418d4",
"name": "Spawner (224)",
"location": [5956, 1310, -2],
@ -80,7 +57,47 @@
]
},
{
"type": "Spawner",
"$type": "Spawner",
"guid": "c0481877-f573-4c00-af1f-c1f5873f4bbb",
"name": "Spawner (224)",
"location": [5940, 1356, -2],
"map": "Felucca",
"count": 6,
"minDelay": "00:05:00",
"maxDelay": "00:10:00",
"team": 0,
"homeRange": 10,
"walkingRange": 20,
"entries": [
{ "name": "DreadSpider", "maxCount": 2, "probability": 100 },
{ "name": "FireElemental", "maxCount": 1, "probability": 100 },
{ "name": "HellCat", "maxCount": 1, "probability": 100 },
{ "name": "LavaLizard", "maxCount": 1, "probability": 100 },
{ "name": "LavaSnake", "maxCount": 1, "probability": 100 }
]
},
{
"$type": "Spawner",
"guid": "c08391fb-b609-4c8d-be85-2d3986f1639a",
"name": "Spawner (224)",
"location": [5976, 1387, -22],
"map": "Felucca",
"count": 6,
"minDelay": "00:05:00",
"maxDelay": "00:10:00",
"team": 0,
"homeRange": 10,
"walkingRange": 20,
"entries": [
{ "name": "DreadSpider", "maxCount": 2, "probability": 100 },
{ "name": "FireElemental", "maxCount": 1, "probability": 100 },
{ "name": "HellCat", "maxCount": 1, "probability": 100 },
{ "name": "LavaLizard", "maxCount": 1, "probability": 100 },
{ "name": "LavaSnake", "maxCount": 1, "probability": 100 }
]
},
{
"$type": "Spawner",
"guid": "d661a6de-3520-464f-994f-77ff738ff5bd",
"name": "Spawner (224)",
"location": [5999, 1327, 10],
@ -98,22 +115,5 @@
{ "name": "LavaLizard", "maxCount": 1, "probability": 100 },
{ "name": "LavaSnake", "maxCount": 1, "probability": 100 }
]
},
{
"type": "Spawner",
"guid": "37ea1620-9487-40df-8035-e76b2443a7b2",
"name": "Spawner (224)",
"location": [5915, 1302, 2],
"map": "Felucca",
"count": 3,
"minDelay": "00:05:00",
"maxDelay": "00:10:00",
"team": 0,
"homeRange": 1,
"walkingRange": 1,
"entries": [
{ "name": "TreasureChestLevel2", "maxCount": 1, "probability": 100 },
{ "name": "TreasureChestLevel3", "maxCount": 2, "probability": 100 }
]
}
]

View file

@ -1,6 +1,6 @@
[
{
"type": "Spawner",
"$type": "Spawner",
"guid": "29168b55-1571-4743-9178-01314586cbe5",
"name": "Spawner (227)",
"location": [5819, 591, 0],
@ -11,99 +11,12 @@
"team": 0,
"homeRange": 5,
"walkingRange": 25,
"entries": [{ "name": "JukaWarrior", "maxCount": 2, "probability": 100 }]
},
{
"type": "Spawner",
"guid": "419487ab-4c4b-4726-8f93-0cd1b49aed0d",
"name": "Spawner (227)",
"location": [5808, 588, 12],
"map": "Felucca",
"count": 5,
"minDelay": "00:05:00",
"maxDelay": "00:10:00",
"team": 0,
"homeRange": 15,
"walkingRange": 30,
"entries": [
{ "name": "JukaLord", "maxCount": 1, "probability": 100 },
{ "name": "JukaMage", "maxCount": 1, "probability": 100 },
{ "name": "JukaWarrior", "maxCount": 3, "probability": 100 }
{ "name": "JukaWarrior", "maxCount": 2, "probability": 100 }
]
},
{
"type": "Spawner",
"guid": "84d09a21-c001-44ed-9c8a-2e7791fa4b1e",
"name": "Spawner (227)",
"location": [5792, 544, 10],
"map": "Felucca",
"count": 8,
"minDelay": "00:05:00",
"maxDelay": "00:10:00",
"team": 0,
"homeRange": 6,
"walkingRange": 30,
"entries": [
{ "name": "GolemController", "maxCount": 4, "probability": 100 },
{ "name": "Golem", "maxCount": 4, "probability": 100 }
]
},
{
"type": "Spawner",
"guid": "63285a8f-9f0d-4966-9f0e-a7fa33fe3a45",
"name": "Spawner (227)",
"location": [5857, 586, 15],
"map": "Felucca",
"count": 5,
"minDelay": "00:05:00",
"maxDelay": "00:10:00",
"team": 0,
"homeRange": 7,
"walkingRange": 30,
"entries": [
{ "name": "JukaLord", "maxCount": 1, "probability": 100 },
{ "name": "JukaMage", "maxCount": 1, "probability": 100 },
{ "name": "JukaWarrior", "maxCount": 3, "probability": 100 }
]
},
{
"type": "Spawner",
"guid": "dfb2dd7e-c751-49e6-9059-25b892efce67",
"name": "Spawner (227)",
"location": [5857, 562, 15],
"map": "Felucca",
"count": 5,
"minDelay": "00:05:00",
"maxDelay": "00:10:00",
"team": 0,
"homeRange": 7,
"walkingRange": 30,
"entries": [
{ "name": "JukaLord", "maxCount": 1, "probability": 100 },
{ "name": "JukaMage", "maxCount": 1, "probability": 100 },
{ "name": "JukaWarrior", "maxCount": 3, "probability": 100 }
]
},
{
"type": "Spawner",
"guid": "41bd23d3-0a22-41fd-a2d2-5d6d809b2018",
"name": "Spawner (227)",
"location": [5828, 530, 0],
"map": "Felucca",
"count": 5,
"minDelay": "00:05:00",
"maxDelay": "00:10:00",
"team": 0,
"homeRange": 8,
"walkingRange": 30,
"entries": [
{ "name": "JukaLord", "maxCount": 1, "probability": 100 },
{ "name": "JukaMage", "maxCount": 1, "probability": 100 },
{ "name": "JukaWarrior", "maxCount": 3, "probability": 100 }
]
},
{
"type": "Spawner",
"$type": "Spawner",
"guid": "346ff700-516b-4788-85a2-9ee7146726f9",
"name": "Spawner (227)",
"location": [5659, 563, 20],
@ -122,7 +35,96 @@
]
},
{
"type": "Spawner",
"$type": "Spawner",
"guid": "3ce378dd-3952-4fac-a5fe-ad12a7b09f20",
"name": "Spawner (227)",
"location": [5690, 534, 0],
"map": "Felucca",
"count": 5,
"minDelay": "00:05:00",
"maxDelay": "00:10:00",
"team": 0,
"homeRange": 8,
"walkingRange": 30,
"entries": [
{ "name": "JukaLord", "maxCount": 1, "probability": 100 },
{ "name": "JukaMage", "maxCount": 1, "probability": 100 },
{ "name": "JukaWarrior", "maxCount": 3, "probability": 100 }
]
},
{
"$type": "Spawner",
"guid": "419487ab-4c4b-4726-8f93-0cd1b49aed0d",
"name": "Spawner (227)",
"location": [5808, 588, 12],
"map": "Felucca",
"count": 5,
"minDelay": "00:05:00",
"maxDelay": "00:10:00",
"team": 0,
"homeRange": 15,
"walkingRange": 30,
"entries": [
{ "name": "JukaLord", "maxCount": 1, "probability": 100 },
{ "name": "JukaMage", "maxCount": 1, "probability": 100 },
{ "name": "JukaWarrior", "maxCount": 3, "probability": 100 }
]
},
{
"$type": "Spawner",
"guid": "41bd23d3-0a22-41fd-a2d2-5d6d809b2018",
"name": "Spawner (227)",
"location": [5828, 530, 0],
"map": "Felucca",
"count": 5,
"minDelay": "00:05:00",
"maxDelay": "00:10:00",
"team": 0,
"homeRange": 8,
"walkingRange": 30,
"entries": [
{ "name": "JukaLord", "maxCount": 1, "probability": 100 },
{ "name": "JukaMage", "maxCount": 1, "probability": 100 },
{ "name": "JukaWarrior", "maxCount": 3, "probability": 100 }
]
},
{
"$type": "Spawner",
"guid": "63285a8f-9f0d-4966-9f0e-a7fa33fe3a45",
"name": "Spawner (227)",
"location": [5857, 586, 15],
"map": "Felucca",
"count": 5,
"minDelay": "00:05:00",
"maxDelay": "00:10:00",
"team": 0,
"homeRange": 7,
"walkingRange": 30,
"entries": [
{ "name": "JukaLord", "maxCount": 1, "probability": 100 },
{ "name": "JukaMage", "maxCount": 1, "probability": 100 },
{ "name": "JukaWarrior", "maxCount": 3, "probability": 100 }
]
},
{
"$type": "Spawner",
"guid": "84d09a21-c001-44ed-9c8a-2e7791fa4b1e",
"name": "Spawner (227)",
"location": [5792, 544, 10],
"map": "Felucca",
"count": 8,
"minDelay": "00:05:00",
"maxDelay": "00:10:00",
"team": 0,
"homeRange": 6,
"walkingRange": 30,
"entries": [
{ "name": "GolemController", "maxCount": 4, "probability": 100 },
{ "name": "Golem", "maxCount": 4, "probability": 100 }
]
},
{
"$type": "Spawner",
"guid": "87ed7838-1bc6-4987-bd1e-0cc16bd54f32",
"name": "Spawner (227)",
"location": [5724, 561, 20],
@ -141,16 +143,16 @@
]
},
{
"type": "Spawner",
"guid": "3ce378dd-3952-4fac-a5fe-ad12a7b09f20",
"$type": "Spawner",
"guid": "dfb2dd7e-c751-49e6-9059-25b892efce67",
"name": "Spawner (227)",
"location": [5690, 534, 0],
"location": [5857, 562, 15],
"map": "Felucca",
"count": 5,
"minDelay": "00:05:00",
"maxDelay": "00:10:00",
"team": 0,
"homeRange": 8,
"homeRange": 7,
"walkingRange": 30,
"entries": [
{ "name": "JukaLord", "maxCount": 1, "probability": 100 },

View file

@ -1,40 +1,9 @@
[
{
"type": "Spawner",
"guid": "cbb2e5a6-ad0e-4bf3-b53d-528756994f1b",
"$type": "Spawner",
"guid": "113fa6c7-b58a-48eb-8cd8-18332e92e22e",
"name": "Spawner (301)",
"location": [50, 689, -28],
"map": "Ilshenar",
"count": 1,
"minDelay": "00:10:00",
"maxDelay": "00:20:00",
"team": 0,
"homeRange": 30,
"walkingRange": 15,
"entries": [{ "name": "AncientWyrm", "maxCount": 1, "probability": 100 }]
},
{
"type": "Spawner",
"guid": "6c0fa0c8-c4fb-4c89-a310-528a89dc22e9",
"name": "Spawner (301)",
"location": [115, 716, -28],
"map": "Ilshenar",
"count": 3,
"minDelay": "00:05:00",
"maxDelay": "00:10:00",
"team": 0,
"homeRange": 15,
"walkingRange": 15,
"entries": [
{ "name": "FireElemental", "maxCount": 3, "probability": 100 },
{ "name": "LavaLizard", "maxCount": 3, "probability": 100 }
]
},
{
"type": "Spawner",
"guid": "9f1df533-f0b6-4d42-8c80-ebf852a0830b",
"name": "Spawner (301)",
"location": [131, 78, 0],
"location": [79, 687, -28],
"map": "Ilshenar",
"count": 1,
"minDelay": "00:05:00",
@ -48,7 +17,7 @@
]
},
{
"type": "Spawner",
"$type": "Spawner",
"guid": "60859b12-15e4-4d3f-bbc6-b48c940c4080",
"name": "Spawner (301)",
"location": [54, 718, -28],
@ -65,7 +34,57 @@
]
},
{
"type": "Spawner",
"$type": "Spawner",
"guid": "6c0fa0c8-c4fb-4c89-a310-528a89dc22e9",
"name": "Spawner (301)",
"location": [115, 716, -28],
"map": "Ilshenar",
"count": 3,
"minDelay": "00:05:00",
"maxDelay": "00:10:00",
"team": 0,
"homeRange": 15,
"walkingRange": 15,
"entries": [
{ "name": "FireElemental", "maxCount": 3, "probability": 100 },
{ "name": "LavaLizard", "maxCount": 3, "probability": 100 }
]
},
{
"$type": "Spawner",
"guid": "9f1df533-f0b6-4d42-8c80-ebf852a0830b",
"name": "Spawner (301)",
"location": [131, 78, 0],
"map": "Ilshenar",
"count": 1,
"minDelay": "00:05:00",
"maxDelay": "00:10:00",
"team": 0,
"homeRange": 10,
"walkingRange": 10,
"entries": [
{ "name": "FireElemental", "maxCount": 1, "probability": 100 },
{ "name": "LavaLizard", "maxCount": 1, "probability": 100 }
]
},
{
"$type": "Spawner",
"guid": "cbb2e5a6-ad0e-4bf3-b53d-528756994f1b",
"name": "Spawner (301)",
"location": [50, 689, -28],
"map": "Ilshenar",
"count": 1,
"minDelay": "00:10:00",
"maxDelay": "00:20:00",
"team": 0,
"homeRange": 30,
"walkingRange": 15,
"entries": [
{ "name": "AncientWyrm", "maxCount": 1, "probability": 100 }
]
},
{
"$type": "Spawner",
"guid": "fdfe8209-e586-454c-aa1e-fdabf9b43125",
"name": "Spawner (301)",
"location": [82, 718, -28],
@ -80,22 +99,5 @@
{ "name": "FireElemental", "maxCount": 2, "probability": 100 },
{ "name": "LavaLizard", "maxCount": 2, "probability": 100 }
]
},
{
"type": "Spawner",
"guid": "113fa6c7-b58a-48eb-8cd8-18332e92e22e",
"name": "Spawner (301)",
"location": [79, 687, -28],
"map": "Ilshenar",
"count": 1,
"minDelay": "00:05:00",
"maxDelay": "00:10:00",
"team": 0,
"homeRange": 10,
"walkingRange": 10,
"entries": [
{ "name": "FireElemental", "maxCount": 1, "probability": 100 },
{ "name": "LavaLizard", "maxCount": 1, "probability": 100 }
]
}
]

View file

@ -1,97 +1,70 @@
[
{
"type": "Spawner",
"guid": "e3b4ec7a-974a-42ef-b9be-80b93317c858",
"$type": "Spawner",
"guid": "01be3c7a-f159-4721-aa38-5861f526d3b8",
"name": "Spawner (302)",
"location": [129, 1530, -28],
"map": "Ilshenar",
"count": 9,
"minDelay": "00:05:00",
"maxDelay": "00:10:00",
"team": 0,
"homeRange": 20,
"walkingRange": 40,
"entries": [
{ "name": "Ratman", "maxCount": 5, "probability": 100 },
{ "name": "RatmanArcher", "maxCount": 2, "probability": 100 },
{ "name": "RatmanMage", "maxCount": 2, "probability": 100 }
]
},
{
"type": "Spawner",
"guid": "6fa3458d-ad0c-4ec0-a058-1d831df7b112",
"name": "Spawner (302)",
"location": [71, 1521, -28],
"map": "Ilshenar",
"count": 5,
"minDelay": "00:05:00",
"maxDelay": "00:10:00",
"team": 0,
"homeRange": 15,
"walkingRange": 40,
"entries": [
{ "name": "HellHound", "maxCount": 2, "probability": 100 },
{ "name": "Imp", "maxCount": 3, "probability": 100 }
]
},
{
"type": "Spawner",
"guid": "c37e0854-914d-46a7-850f-4b8072add151",
"name": "Spawner (302)",
"location": [87, 1471, -22],
"map": "Ilshenar",
"count": 3,
"minDelay": "00:05:00",
"maxDelay": "00:10:00",
"team": 0,
"homeRange": 7,
"walkingRange": 7,
"entries": [{ "name": "EvilMage", "maxCount": 3, "probability": 100 }]
},
{
"type": "Spawner",
"guid": "cb063844-675e-4177-9da9-7e3c1d2704af",
"name": "Spawner (302)",
"location": [63, 1471, -28],
"location": [45, 905, -29],
"map": "Ilshenar",
"count": 2,
"minDelay": "00:05:00",
"maxDelay": "00:10:00",
"team": 0,
"homeRange": 7,
"walkingRange": 7,
"entries": [{ "name": "EvilMage", "maxCount": 2, "probability": 100 }]
"homeRange": 15,
"walkingRange": 30,
"entries": [
{ "name": "FireElemental", "maxCount": 2, "probability": 100 }
]
},
{
"type": "Spawner",
"guid": "32d38b45-94a0-429d-b06d-da2941222110",
"$type": "Spawner",
"guid": "1146a823-b233-4508-89f4-802dc5f67112",
"name": "Spawner (302)",
"location": [39, 1491, -28],
"location": [66, 1171, -28],
"map": "Ilshenar",
"count": 1,
"minDelay": "00:05:00",
"maxDelay": "00:10:00",
"team": 0,
"homeRange": 10,
"homeRange": 5,
"walkingRange": 10,
"entries": [{ "name": "Daemon", "maxCount": 1, "probability": 100 }]
"entries": [
{ "name": "Nightmare", "maxCount": 1, "probability": 100 }
]
},
{
"type": "Spawner",
"guid": "9debe6c0-b766-4c9a-94c7-2f4db1d1a7fd",
"$type": "Spawner",
"guid": "12ed93c0-e81b-4de2-afe6-0c5c34975a41",
"name": "Spawner (302)",
"location": [34, 1462, -28],
"location": [49, 846, -30],
"map": "Ilshenar",
"count": 3,
"count": 1,
"minDelay": "00:05:00",
"maxDelay": "00:10:00",
"team": 0,
"homeRange": 10,
"walkingRange": 10,
"entries": [{ "name": "BloodElemental", "maxCount": 3, "probability": 100 }]
"homeRange": 12,
"walkingRange": 12,
"entries": [
{ "name": "PoisonElemental", "maxCount": 1, "probability": 100 }
]
},
{
"type": "Spawner",
"$type": "Spawner",
"guid": "192e009f-b329-47a3-b290-cd1af4504db6",
"name": "Spawner (302)",
"location": [148, 953, -29],
"map": "Ilshenar",
"count": 1,
"minDelay": "00:05:00",
"maxDelay": "00:10:00",
"team": 0,
"homeRange": 15,
"walkingRange": 15,
"entries": [
{ "name": "Kirin", "maxCount": 1, "probability": 100 }
]
},
{
"$type": "Spawner",
"guid": "250608fe-f32f-472e-a726-d5e1e3924160",
"name": "Spawner (302)",
"location": [15, 1503, -27],
@ -102,10 +75,12 @@
"team": 0,
"homeRange": 7,
"walkingRange": 7,
"entries": [{ "name": "Imp", "maxCount": 4, "probability": 100 }]
"entries": [
{ "name": "Imp", "maxCount": 4, "probability": 100 }
]
},
{
"type": "Spawner",
"$type": "Spawner",
"guid": "2de1d85a-a211-4c0d-8cdb-3820a2f4cde3",
"name": "Spawner (302)",
"location": [62, 1446, -28],
@ -116,80 +91,28 @@
"team": 0,
"homeRange": 7,
"walkingRange": 7,
"entries": [{ "name": "EvilMage", "maxCount": 3, "probability": 100 }]
"entries": [
{ "name": "EvilMage", "maxCount": 3, "probability": 100 }
]
},
{
"type": "Spawner",
"guid": "e87e63f3-2c4e-4a95-ad84-7cae33a9071b",
"$type": "Spawner",
"guid": "32d38b45-94a0-429d-b06d-da2941222110",
"name": "Spawner (302)",
"location": [31, 1410, -28],
"location": [39, 1491, -28],
"map": "Ilshenar",
"count": 4,
"minDelay": "00:05:00",
"maxDelay": "00:10:00",
"team": 0,
"homeRange": 7,
"walkingRange": 11,
"entries": [{ "name": "SkeletalKnight", "maxCount": 4, "probability": 100 }]
},
{
"type": "Spawner",
"guid": "675f68f6-58e7-4a9c-adef-31aa0ecdbc00",
"name": "Spawner (302)",
"location": [86, 1447, -28],
"map": "Ilshenar",
"count": 3,
"minDelay": "00:05:00",
"maxDelay": "00:10:00",
"team": 0,
"homeRange": 7,
"walkingRange": 15,
"entries": [{ "name": "Mummy", "maxCount": 3, "probability": 100 }]
},
{
"type": "Spawner",
"guid": "94a6eb04-35cb-49c0-8697-ac3caf736e1a",
"name": "Spawner (302)",
"location": [124, 1435, -16],
"map": "Ilshenar",
"count": 4,
"minDelay": "00:05:00",
"maxDelay": "00:10:00",
"team": 0,
"homeRange": 10,
"walkingRange": 20,
"entries": [{ "name": "SkeletalMage", "maxCount": 4, "probability": 100 }]
},
{
"type": "Spawner",
"guid": "fc88bc1a-1266-4ae7-ad00-50ecb32896ff",
"name": "Spawner (302)",
"location": [124, 1436, -16],
"map": "Ilshenar",
"count": 2,
"minDelay": "00:05:00",
"maxDelay": "00:10:00",
"team": 0,
"homeRange": 7,
"walkingRange": 15,
"entries": [{ "name": "Lich", "maxCount": 2, "probability": 100 }]
},
{
"type": "Spawner",
"guid": "ed586ce9-3e23-403c-b92a-013f2e88fb3c",
"name": "Spawner (302)",
"location": [107, 1390, -28],
"map": "Ilshenar",
"count": 2,
"count": 1,
"minDelay": "00:05:00",
"maxDelay": "00:10:00",
"team": 0,
"homeRange": 10,
"walkingRange": 10,
"entries": [{ "name": "Lich", "maxCount": 2, "probability": 100 }]
"entries": [
{ "name": "Daemon", "maxCount": 1, "probability": 100 }
]
},
{
"type": "Spawner",
"$type": "Spawner",
"guid": "40c9f7b2-9ce5-4c7c-ba9c-d78cbd9a7eab",
"name": "Spawner (302)",
"location": [131, 1331, -28],
@ -206,21 +129,7 @@
]
},
{
"type": "Spawner",
"guid": "fd6c1c50-ee38-412c-8957-3f74936e8020",
"name": "Spawner (302)",
"location": [132, 1332, -28],
"map": "Ilshenar",
"count": 4,
"minDelay": "00:05:00",
"maxDelay": "00:10:00",
"team": 0,
"homeRange": 20,
"walkingRange": 20,
"entries": [{ "name": "Zombie", "maxCount": 4, "probability": 100 }]
},
{
"type": "Spawner",
"$type": "Spawner",
"guid": "5644d83b-87a0-46aa-a23d-5166d9d4f2f7",
"name": "Spawner (302)",
"location": [55, 1303, -28],
@ -238,91 +147,7 @@
]
},
{
"type": "Spawner",
"guid": "01be3c7a-f159-4721-aa38-5861f526d3b8",
"name": "Spawner (302)",
"location": [45, 905, -29],
"map": "Ilshenar",
"count": 2,
"minDelay": "00:05:00",
"maxDelay": "00:10:00",
"team": 0,
"homeRange": 15,
"walkingRange": 30,
"entries": [{ "name": "FireElemental", "maxCount": 2, "probability": 100 }]
},
{
"type": "Spawner",
"guid": "12ed93c0-e81b-4de2-afe6-0c5c34975a41",
"name": "Spawner (302)",
"location": [49, 846, -30],
"map": "Ilshenar",
"count": 1,
"minDelay": "00:05:00",
"maxDelay": "00:10:00",
"team": 0,
"homeRange": 12,
"walkingRange": 12,
"entries": [{ "name": "PoisonElemental", "maxCount": 1, "probability": 100 }]
},
{
"type": "Spawner",
"guid": "e73b0e45-8071-4f7a-8126-d3b05a6ac95e",
"name": "Spawner (302)",
"location": [114, 908, -26],
"map": "Ilshenar",
"count": 3,
"minDelay": "00:05:00",
"maxDelay": "00:10:00",
"team": 0,
"homeRange": 15,
"walkingRange": 25,
"entries": [{ "name": "EarthElemental", "maxCount": 3, "probability": 100 }]
},
{
"type": "Spawner",
"guid": "9a01a01c-2e9f-46ad-b9a9-0d1af7d3b211",
"name": "Spawner (302)",
"location": [102, 959, -43],
"map": "Ilshenar",
"count": 2,
"minDelay": "00:05:00",
"maxDelay": "00:10:00",
"team": 0,
"homeRange": 15,
"walkingRange": 20,
"entries": [{ "name": "PoisonElemental", "maxCount": 2, "probability": 100 }]
},
{
"type": "Spawner",
"guid": "192e009f-b329-47a3-b290-cd1af4504db6",
"name": "Spawner (302)",
"location": [148, 953, -29],
"map": "Ilshenar",
"count": 1,
"minDelay": "00:05:00",
"maxDelay": "00:10:00",
"team": 0,
"homeRange": 15,
"walkingRange": 15,
"entries": [{ "name": "Kirin", "maxCount": 1, "probability": 100 }]
},
{
"type": "Spawner",
"guid": "a474135e-6997-45bf-a865-77ca9c5c854b",
"name": "Spawner (302)",
"location": [29, 1009, -27],
"map": "Ilshenar",
"count": 2,
"minDelay": "00:05:00",
"maxDelay": "00:10:00",
"team": 0,
"homeRange": 15,
"walkingRange": 35,
"entries": [{ "name": "Dragon", "maxCount": 2, "probability": 100 }]
},
{
"type": "Spawner",
"$type": "Spawner",
"guid": "5c977889-217c-4562-bc51-d549a3ce616d",
"name": "Spawner (302)",
"location": [29, 1016, -27],
@ -333,10 +158,141 @@
"team": 0,
"homeRange": 15,
"walkingRange": 35,
"entries": [{ "name": "Drake", "maxCount": 4, "probability": 100 }]
"entries": [
{ "name": "Drake", "maxCount": 4, "probability": 100 }
]
},
{
"type": "Spawner",
"$type": "Spawner",
"guid": "675f68f6-58e7-4a9c-adef-31aa0ecdbc00",
"name": "Spawner (302)",
"location": [86, 1447, -28],
"map": "Ilshenar",
"count": 3,
"minDelay": "00:05:00",
"maxDelay": "00:10:00",
"team": 0,
"homeRange": 7,
"walkingRange": 15,
"entries": [
{ "name": "Mummy", "maxCount": 3, "probability": 100 }
]
},
{
"$type": "Spawner",
"guid": "6fa3458d-ad0c-4ec0-a058-1d831df7b112",
"name": "Spawner (302)",
"location": [71, 1521, -28],
"map": "Ilshenar",
"count": 5,
"minDelay": "00:05:00",
"maxDelay": "00:10:00",
"team": 0,
"homeRange": 15,
"walkingRange": 40,
"entries": [
{ "name": "HellHound", "maxCount": 2, "probability": 100 },
{ "name": "Imp", "maxCount": 3, "probability": 100 }
]
},
{
"$type": "Spawner",
"guid": "94a6eb04-35cb-49c0-8697-ac3caf736e1a",
"name": "Spawner (302)",
"location": [124, 1435, -16],
"map": "Ilshenar",
"count": 4,
"minDelay": "00:05:00",
"maxDelay": "00:10:00",
"team": 0,
"homeRange": 10,
"walkingRange": 20,
"entries": [
{ "name": "SkeletalMage", "maxCount": 4, "probability": 100 }
]
},
{
"$type": "Spawner",
"guid": "94b96b34-65e4-4af5-a3cd-56c2c8e8478f",
"name": "Spawner (302)",
"location": [473, 1552, -28],
"map": "Ilshenar",
"count": 1,
"minDelay": "00:05:00",
"maxDelay": "00:10:00",
"team": 0,
"homeRange": 15,
"walkingRange": 20,
"entries": [
{ "name": "SerpentineDragon", "maxCount": 1, "probability": 100 }
]
},
{
"$type": "Spawner",
"guid": "9a01a01c-2e9f-46ad-b9a9-0d1af7d3b211",
"name": "Spawner (302)",
"location": [102, 959, -43],
"map": "Ilshenar",
"count": 2,
"minDelay": "00:05:00",
"maxDelay": "00:10:00",
"team": 0,
"homeRange": 15,
"walkingRange": 20,
"entries": [
{ "name": "PoisonElemental", "maxCount": 2, "probability": 100 }
]
},
{
"$type": "Spawner",
"guid": "9bca331b-b283-436c-b46b-446d69e18593",
"name": "Spawner (302)",
"location": [108, 1110, -27],
"map": "Ilshenar",
"count": 4,
"minDelay": "00:05:00",
"maxDelay": "00:10:00",
"team": 0,
"homeRange": 20,
"walkingRange": 30,
"entries": [
{ "name": "EarthElemental", "maxCount": 4, "probability": 100 }
]
},
{
"$type": "Spawner",
"guid": "9debe6c0-b766-4c9a-94c7-2f4db1d1a7fd",
"name": "Spawner (302)",
"location": [34, 1462, -28],
"map": "Ilshenar",
"count": 3,
"minDelay": "00:05:00",
"maxDelay": "00:10:00",
"team": 0,
"homeRange": 10,
"walkingRange": 10,
"entries": [
{ "name": "BloodElemental", "maxCount": 3, "probability": 100 }
]
},
{
"$type": "Spawner",
"guid": "a474135e-6997-45bf-a865-77ca9c5c854b",
"name": "Spawner (302)",
"location": [29, 1009, -27],
"map": "Ilshenar",
"count": 2,
"minDelay": "00:05:00",
"maxDelay": "00:10:00",
"team": 0,
"homeRange": 15,
"walkingRange": 35,
"entries": [
{ "name": "Dragon", "maxCount": 2, "probability": 100 }
]
},
{
"$type": "Spawner",
"guid": "b88fa850-e3c3-468e-b8e4-afc1430e5ad6",
"name": "Spawner (302)",
"location": [87, 1051, -29],
@ -353,21 +309,23 @@
]
},
{
"type": "Spawner",
"guid": "9bca331b-b283-436c-b46b-446d69e18593",
"$type": "Spawner",
"guid": "c37e0854-914d-46a7-850f-4b8072add151",
"name": "Spawner (302)",
"location": [108, 1110, -27],
"location": [87, 1471, -22],
"map": "Ilshenar",
"count": 4,
"count": 3,
"minDelay": "00:05:00",
"maxDelay": "00:10:00",
"team": 0,
"homeRange": 20,
"walkingRange": 30,
"entries": [{ "name": "EarthElemental", "maxCount": 4, "probability": 100 }]
"homeRange": 7,
"walkingRange": 7,
"entries": [
{ "name": "EvilMage", "maxCount": 3, "probability": 100 }
]
},
{
"type": "Spawner",
"$type": "Spawner",
"guid": "c5dbed97-1066-4bcc-8b21-cffeb33843ad",
"name": "Spawner (302)",
"location": [99, 1158, -23],
@ -378,24 +336,46 @@
"team": 0,
"homeRange": 10,
"walkingRange": 10,
"entries": [{ "name": "AcidElemental", "maxCount": 2, "probability": 100 }]
"entries": [
{ "name": "AcidElemental", "maxCount": 2, "probability": 100 }
]
},
{
"type": "Spawner",
"guid": "1146a823-b233-4508-89f4-802dc5f67112",
"$type": "Spawner",
"guid": "cb063844-675e-4177-9da9-7e3c1d2704af",
"name": "Spawner (302)",
"location": [66, 1171, -28],
"location": [63, 1471, -28],
"map": "Ilshenar",
"count": 1,
"count": 2,
"minDelay": "00:05:00",
"maxDelay": "00:10:00",
"team": 0,
"homeRange": 5,
"walkingRange": 10,
"entries": [{ "name": "Nightmare", "maxCount": 1, "probability": 100 }]
"homeRange": 7,
"walkingRange": 7,
"entries": [
{ "name": "EvilMage", "maxCount": 2, "probability": 100 }
]
},
{
"type": "Spawner",
"$type": "Spawner",
"guid": "e3b4ec7a-974a-42ef-b9be-80b93317c858",
"name": "Spawner (302)",
"location": [129, 1530, -28],
"map": "Ilshenar",
"count": 9,
"minDelay": "00:05:00",
"maxDelay": "00:10:00",
"team": 0,
"homeRange": 20,
"walkingRange": 40,
"entries": [
{ "name": "Ratman", "maxCount": 5, "probability": 100 },
{ "name": "RatmanArcher", "maxCount": 2, "probability": 100 },
{ "name": "RatmanMage", "maxCount": 2, "probability": 100 }
]
},
{
"$type": "Spawner",
"guid": "e4279b1a-4375-4a93-9ed5-df2a8f9ade4a",
"name": "Spawner (302)",
"location": [438, 1552, -28],
@ -406,10 +386,60 @@
"team": 0,
"homeRange": 10,
"walkingRange": 20,
"entries": [{ "name": "Pixie", "maxCount": 5, "probability": 100 }]
"entries": [
{ "name": "Pixie", "maxCount": 5, "probability": 100 }
]
},
{
"type": "Spawner",
"$type": "Spawner",
"guid": "e73b0e45-8071-4f7a-8126-d3b05a6ac95e",
"name": "Spawner (302)",
"location": [114, 908, -26],
"map": "Ilshenar",
"count": 3,
"minDelay": "00:05:00",
"maxDelay": "00:10:00",
"team": 0,
"homeRange": 15,
"walkingRange": 25,
"entries": [
{ "name": "EarthElemental", "maxCount": 3, "probability": 100 }
]
},
{
"$type": "Spawner",
"guid": "e87e63f3-2c4e-4a95-ad84-7cae33a9071b",
"name": "Spawner (302)",
"location": [31, 1410, -28],
"map": "Ilshenar",
"count": 4,
"minDelay": "00:05:00",
"maxDelay": "00:10:00",
"team": 0,
"homeRange": 7,
"walkingRange": 11,
"entries": [
{ "name": "SkeletalKnight", "maxCount": 4, "probability": 100 }
]
},
{
"$type": "Spawner",
"guid": "ed586ce9-3e23-403c-b92a-013f2e88fb3c",
"name": "Spawner (302)",
"location": [107, 1390, -28],
"map": "Ilshenar",
"count": 2,
"minDelay": "00:05:00",
"maxDelay": "00:10:00",
"team": 0,
"homeRange": 10,
"walkingRange": 10,
"entries": [
{ "name": "Lich", "maxCount": 2, "probability": 100 }
]
},
{
"$type": "Spawner",
"guid": "f444295e-dedd-4070-8386-54b35d4ac4bb",
"name": "Spawner (302)",
"location": [451, 1552, -27],
@ -420,20 +450,40 @@
"team": 0,
"homeRange": 15,
"walkingRange": 25,
"entries": [{ "name": "Wisp", "maxCount": 3, "probability": 100 }]
"entries": [
{ "name": "Wisp", "maxCount": 3, "probability": 100 }
]
},
{
"type": "Spawner",
"guid": "94b96b34-65e4-4af5-a3cd-56c2c8e8478f",
"$type": "Spawner",
"guid": "fc88bc1a-1266-4ae7-ad00-50ecb32896ff",
"name": "Spawner (302)",
"location": [473, 1552, -28],
"location": [124, 1436, -16],
"map": "Ilshenar",
"count": 1,
"count": 2,
"minDelay": "00:05:00",
"maxDelay": "00:10:00",
"team": 0,
"homeRange": 15,
"homeRange": 7,
"walkingRange": 15,
"entries": [
{ "name": "Lich", "maxCount": 2, "probability": 100 }
]
},
{
"$type": "Spawner",
"guid": "fd6c1c50-ee38-412c-8957-3f74936e8020",
"name": "Spawner (302)",
"location": [132, 1332, -28],
"map": "Ilshenar",
"count": 4,
"minDelay": "00:05:00",
"maxDelay": "00:10:00",
"team": 0,
"homeRange": 20,
"walkingRange": 20,
"entries": [{ "name": "SerpentineDragon", "maxCount": 1, "probability": 100 }]
"entries": [
{ "name": "Zombie", "maxCount": 4, "probability": 100 }
]
}
]

View file

@ -1,20 +1,6 @@
[
{
"type": "Spawner",
"guid": "b36243ef-5201-4922-972a-8e0dd78ffe11",
"name": "Spawner (303)",
"location": [2114, 846, -28],
"map": "Ilshenar",
"count": 4,
"minDelay": "00:05:00",
"maxDelay": "00:10:00",
"team": 0,
"homeRange": 15,
"walkingRange": 30,
"entries": [{ "name": "Imp", "maxCount": 4, "probability": 100 }]
},
{
"type": "Spawner",
"$type": "Spawner",
"guid": "1a119f51-d328-4a35-bda3-d63ca1d2f3f0",
"name": "Spawner (303)",
"location": [2051, 837, -28],
@ -25,122 +11,12 @@
"team": 0,
"homeRange": 7,
"walkingRange": 15,
"entries": [{ "name": "Imp", "maxCount": 4, "probability": 100 }]
"entries": [
{ "name": "Imp", "maxCount": 4, "probability": 100 }
]
},
{
"type": "Spawner",
"guid": "8540d8e1-53c7-416b-b611-94127a44d56c",
"name": "Spawner (303)",
"location": [2050, 859, -28],
"map": "Ilshenar",
"count": 2,
"minDelay": "00:05:00",
"maxDelay": "00:10:00",
"team": 0,
"homeRange": 7,
"walkingRange": 15,
"entries": [{ "name": "HellCat", "maxCount": 2, "probability": 100 }]
},
{
"type": "Spawner",
"guid": "2e60ce70-cd00-4d8d-82fb-0733b5b06c32",
"name": "Spawner (303)",
"location": [2090, 868, -14],
"map": "Ilshenar",
"count": 3,
"minDelay": "00:05:00",
"maxDelay": "00:10:00",
"team": 0,
"homeRange": 7,
"walkingRange": 15,
"entries": [{ "name": "HellCat", "maxCount": 3, "probability": 100 }]
},
{
"type": "Spawner",
"guid": "d0c57916-16d8-4135-ba77-c43c93947a9d",
"name": "Spawner (303)",
"location": [2177, 836, -28],
"map": "Ilshenar",
"count": 4,
"minDelay": "00:05:00",
"maxDelay": "00:10:00",
"team": 0,
"homeRange": 7,
"walkingRange": 15,
"entries": [{ "name": "Imp", "maxCount": 4, "probability": 100 }]
},
{
"type": "Spawner",
"guid": "7d083d0d-b124-4567-96ec-6a35ac8cbc06",
"name": "Spawner (303)",
"location": [2177, 858, -28],
"map": "Ilshenar",
"count": 4,
"minDelay": "00:05:00",
"maxDelay": "00:10:00",
"team": 0,
"homeRange": 7,
"walkingRange": 15,
"entries": [{ "name": "Imp", "maxCount": 4, "probability": 100 }]
},
{
"type": "Spawner",
"guid": "b73a5bca-9065-495e-8801-7b776732c00b",
"name": "Spawner (303)",
"location": [2138, 868, -14],
"map": "Ilshenar",
"count": 4,
"minDelay": "00:05:00",
"maxDelay": "00:10:00",
"team": 0,
"homeRange": 7,
"walkingRange": 15,
"entries": [{ "name": "Imp", "maxCount": 4, "probability": 100 }]
},
{
"type": "Spawner",
"guid": "49e5b4ac-570c-43eb-85cc-8c113c5bea7b",
"name": "Spawner (303)",
"location": [2138, 868, -14],
"map": "Ilshenar",
"count": 1,
"minDelay": "00:05:00",
"maxDelay": "00:10:00",
"team": 0,
"homeRange": 7,
"walkingRange": 7,
"entries": [{ "name": "BloodElemental", "maxCount": 1, "probability": 100 }]
},
{
"type": "Spawner",
"guid": "1f6a7fef-6d82-4d17-8505-0927a56a3302",
"name": "Spawner (303)",
"location": [2083, 911, -28],
"map": "Ilshenar",
"count": 1,
"minDelay": "00:05:00",
"maxDelay": "00:10:00",
"team": 0,
"homeRange": 10,
"walkingRange": 10,
"entries": [{ "name": "Balron", "maxCount": 1, "probability": 100 }]
},
{
"type": "Spawner",
"guid": "a670a008-155a-4420-ab3c-e15e3e951f83",
"name": "Spawner (303)",
"location": [2115, 917, -23],
"map": "Ilshenar",
"count": 1,
"minDelay": "00:05:00",
"maxDelay": "00:10:00",
"team": 0,
"homeRange": 15,
"walkingRange": 15,
"entries": [{ "name": "Balron", "maxCount": 1, "probability": 100 }]
},
{
"type": "Spawner",
"$type": "Spawner",
"guid": "1e39d497-d9f3-48ba-8c6a-af1bf7a9ba3d",
"name": "Spawner (303)",
"location": [2145, 911, -28],
@ -151,80 +27,28 @@
"team": 0,
"homeRange": 10,
"walkingRange": 10,
"entries": [{ "name": "Balron", "maxCount": 1, "probability": 100 }]
"entries": [
{ "name": "Balron", "maxCount": 1, "probability": 100 }
]
},
{
"type": "Spawner",
"guid": "2eada9f7-16d7-4d0e-9418-9e013208bf4f",
"$type": "Spawner",
"guid": "1f6a7fef-6d82-4d17-8505-0927a56a3302",
"name": "Spawner (303)",
"location": [2170, 920, -28],
"location": [2083, 911, -28],
"map": "Ilshenar",
"count": 1,
"minDelay": "00:05:00",
"maxDelay": "00:10:00",
"team": 0,
"homeRange": 10,
"walkingRange": 32,
"entries": [{ "name": "BloodElemental", "maxCount": 1, "probability": 100 }]
"walkingRange": 10,
"entries": [
{ "name": "Balron", "maxCount": 1, "probability": 100 }
]
},
{
"type": "Spawner",
"guid": "7f72864a-7c9b-41ce-abda-a205be287234",
"name": "Spawner (303)",
"location": [2058, 907, -28],
"map": "Ilshenar",
"count": 1,
"minDelay": "00:05:00",
"maxDelay": "00:10:00",
"team": 0,
"homeRange": 10,
"walkingRange": 20,
"entries": [{ "name": "BloodElemental", "maxCount": 1, "probability": 100 }]
},
{
"type": "Spawner",
"guid": "d08b49a2-1291-4d12-a97f-50aee1c289ff",
"name": "Spawner (303)",
"location": [2058, 932, -28],
"map": "Ilshenar",
"count": 1,
"minDelay": "00:05:00",
"maxDelay": "00:10:00",
"team": 0,
"homeRange": 10,
"walkingRange": 20,
"entries": [{ "name": "BloodElemental", "maxCount": 1, "probability": 100 }]
},
{
"type": "Spawner",
"guid": "8b88f31e-d1c6-4a32-b719-2062d630872f",
"name": "Spawner (303)",
"location": [2083, 951, -28],
"map": "Ilshenar",
"count": 1,
"minDelay": "00:05:00",
"maxDelay": "00:10:00",
"team": 0,
"homeRange": 10,
"walkingRange": 15,
"entries": [{ "name": "Daemon", "maxCount": 1, "probability": 100 }]
},
{
"type": "Spawner",
"guid": "ac12b1c2-8db2-4664-91db-7381d677eb02",
"name": "Spawner (303)",
"location": [2114, 951, -28],
"map": "Ilshenar",
"count": 1,
"minDelay": "00:05:00",
"maxDelay": "00:10:00",
"team": 0,
"homeRange": 10,
"walkingRange": 15,
"entries": [{ "name": "Daemon", "maxCount": 1, "probability": 100 }]
},
{
"type": "Spawner",
"$type": "Spawner",
"guid": "2dbb818a-3f08-4752-bfb0-414d433fa5d3",
"name": "Spawner (303)",
"location": [2146, 951, -28],
@ -235,10 +59,44 @@
"team": 0,
"homeRange": 10,
"walkingRange": 15,
"entries": [{ "name": "Daemon", "maxCount": 1, "probability": 100 }]
"entries": [
{ "name": "Daemon", "maxCount": 1, "probability": 100 }
]
},
{
"type": "Spawner",
"$type": "Spawner",
"guid": "2e60ce70-cd00-4d8d-82fb-0733b5b06c32",
"name": "Spawner (303)",
"location": [2090, 868, -14],
"map": "Ilshenar",
"count": 3,
"minDelay": "00:05:00",
"maxDelay": "00:10:00",
"team": 0,
"homeRange": 7,
"walkingRange": 15,
"entries": [
{ "name": "HellCat", "maxCount": 3, "probability": 100 }
]
},
{
"$type": "Spawner",
"guid": "2eada9f7-16d7-4d0e-9418-9e013208bf4f",
"name": "Spawner (303)",
"location": [2170, 920, -28],
"map": "Ilshenar",
"count": 1,
"minDelay": "00:05:00",
"maxDelay": "00:10:00",
"team": 0,
"homeRange": 10,
"walkingRange": 32,
"entries": [
{ "name": "BloodElemental", "maxCount": 1, "probability": 100 }
]
},
{
"$type": "Spawner",
"guid": "3a8ab857-7a3d-4cd4-9466-567fd9d0b3ea",
"name": "Spawner (303)",
"location": [2113, 1017, -28],
@ -253,5 +111,181 @@
{ "name": "Succubus", "maxCount": 2, "probability": 100 },
{ "name": "HellCat", "maxCount": 4, "probability": 100 }
]
},
{
"$type": "Spawner",
"guid": "49e5b4ac-570c-43eb-85cc-8c113c5bea7b",
"name": "Spawner (303)",
"location": [2138, 868, -14],
"map": "Ilshenar",
"count": 1,
"minDelay": "00:05:00",
"maxDelay": "00:10:00",
"team": 0,
"homeRange": 7,
"walkingRange": 7,
"entries": [
{ "name": "BloodElemental", "maxCount": 1, "probability": 100 }
]
},
{
"$type": "Spawner",
"guid": "7d083d0d-b124-4567-96ec-6a35ac8cbc06",
"name": "Spawner (303)",
"location": [2177, 858, -28],
"map": "Ilshenar",
"count": 4,
"minDelay": "00:05:00",
"maxDelay": "00:10:00",
"team": 0,
"homeRange": 7,
"walkingRange": 15,
"entries": [
{ "name": "Imp", "maxCount": 4, "probability": 100 }
]
},
{
"$type": "Spawner",
"guid": "7f72864a-7c9b-41ce-abda-a205be287234",
"name": "Spawner (303)",
"location": [2058, 907, -28],
"map": "Ilshenar",
"count": 1,
"minDelay": "00:05:00",
"maxDelay": "00:10:00",
"team": 0,
"homeRange": 10,
"walkingRange": 20,
"entries": [
{ "name": "BloodElemental", "maxCount": 1, "probability": 100 }
]
},
{
"$type": "Spawner",
"guid": "8540d8e1-53c7-416b-b611-94127a44d56c",
"name": "Spawner (303)",
"location": [2050, 859, -28],
"map": "Ilshenar",
"count": 2,
"minDelay": "00:05:00",
"maxDelay": "00:10:00",
"team": 0,
"homeRange": 7,
"walkingRange": 15,
"entries": [
{ "name": "HellCat", "maxCount": 2, "probability": 100 }
]
},
{
"$type": "Spawner",
"guid": "8b88f31e-d1c6-4a32-b719-2062d630872f",
"name": "Spawner (303)",
"location": [2083, 951, -28],
"map": "Ilshenar",
"count": 1,
"minDelay": "00:05:00",
"maxDelay": "00:10:00",
"team": 0,
"homeRange": 10,
"walkingRange": 15,
"entries": [
{ "name": "Daemon", "maxCount": 1, "probability": 100 }
]
},
{
"$type": "Spawner",
"guid": "a670a008-155a-4420-ab3c-e15e3e951f83",
"name": "Spawner (303)",
"location": [2115, 917, -23],
"map": "Ilshenar",
"count": 1,
"minDelay": "00:05:00",
"maxDelay": "00:10:00",
"team": 0,
"homeRange": 15,
"walkingRange": 15,
"entries": [
{ "name": "Balron", "maxCount": 1, "probability": 100 }
]
},
{
"$type": "Spawner",
"guid": "ac12b1c2-8db2-4664-91db-7381d677eb02",
"name": "Spawner (303)",
"location": [2114, 951, -28],
"map": "Ilshenar",
"count": 1,
"minDelay": "00:05:00",
"maxDelay": "00:10:00",
"team": 0,
"homeRange": 10,
"walkingRange": 15,
"entries": [
{ "name": "Daemon", "maxCount": 1, "probability": 100 }
]
},
{
"$type": "Spawner",
"guid": "b36243ef-5201-4922-972a-8e0dd78ffe11",
"name": "Spawner (303)",
"location": [2114, 846, -28],
"map": "Ilshenar",
"count": 4,
"minDelay": "00:05:00",
"maxDelay": "00:10:00",
"team": 0,
"homeRange": 15,
"walkingRange": 30,
"entries": [
{ "name": "Imp", "maxCount": 4, "probability": 100 }
]
},
{
"$type": "Spawner",
"guid": "b73a5bca-9065-495e-8801-7b776732c00b",
"name": "Spawner (303)",
"location": [2138, 868, -14],
"map": "Ilshenar",
"count": 4,
"minDelay": "00:05:00",
"maxDelay": "00:10:00",
"team": 0,
"homeRange": 7,
"walkingRange": 15,
"entries": [
{ "name": "Imp", "maxCount": 4, "probability": 100 }
]
},
{
"$type": "Spawner",
"guid": "d08b49a2-1291-4d12-a97f-50aee1c289ff",
"name": "Spawner (303)",
"location": [2058, 932, -28],
"map": "Ilshenar",
"count": 1,
"minDelay": "00:05:00",
"maxDelay": "00:10:00",
"team": 0,
"homeRange": 10,
"walkingRange": 20,
"entries": [
{ "name": "BloodElemental", "maxCount": 1, "probability": 100 }
]
},
{
"$type": "Spawner",
"guid": "d0c57916-16d8-4135-ba77-c43c93947a9d",
"name": "Spawner (303)",
"location": [2177, 836, -28],
"map": "Ilshenar",
"count": 4,
"minDelay": "00:05:00",
"maxDelay": "00:10:00",
"team": 0,
"homeRange": 7,
"walkingRange": 15,
"entries": [
{ "name": "Imp", "maxCount": 4, "probability": 100 }
]
}
]

View file

@ -1,82 +1,6 @@
[
{
"type": "Spawner",
"guid": "8e098781-936b-42cc-adfa-806111b9a9f2",
"name": "Spawner (304)",
"location": [1980, 116, -28],
"map": "Ilshenar",
"count": 2,
"minDelay": "00:05:00",
"maxDelay": "00:10:00",
"team": 0,
"homeRange": 30,
"walkingRange": 40,
"entries": [
{ "name": "EnslavedGargoyle", "maxCount": 2, "probability": 100 },
{ "name": "GolemController", "maxCount": 2, "probability": 100 }
]
},
{
"type": "Spawner",
"guid": "d39e9374-f30b-488c-8a12-5f3a6315f391",
"name": "Spawner (304)",
"location": [1939, 116, -28],
"map": "Ilshenar",
"count": 1,
"minDelay": "00:05:00",
"maxDelay": "00:10:00",
"team": 0,
"homeRange": 15,
"walkingRange": 25,
"entries": [{ "name": "ExodusMinion", "maxCount": 1, "probability": 100 }]
},
{
"type": "Spawner",
"guid": "35394aa1-6039-446f-b247-51d9f7926f6c",
"name": "Spawner (304)",
"location": [1894, 116, -28],
"map": "Ilshenar",
"count": 2,
"minDelay": "00:05:00",
"maxDelay": "00:10:00",
"team": 0,
"homeRange": 20,
"walkingRange": 30,
"entries": [
{ "name": "GolemController", "maxCount": 2, "probability": 100 },
{ "name": "ExodusOverseer", "maxCount": 2, "probability": 100 }
]
},
{
"type": "Spawner",
"guid": "a35646cf-0460-48d2-a4ea-5e9cc33ad9ab",
"name": "Spawner (304)",
"location": [1964, 164, -28],
"map": "Ilshenar",
"count": 1,
"minDelay": "00:05:00",
"maxDelay": "00:10:00",
"team": 0,
"homeRange": 10,
"walkingRange": 15,
"entries": [{ "name": "GolemController", "maxCount": 1, "probability": 100 }]
},
{
"type": "Spawner",
"guid": "918f3735-8e70-42c3-a633-2fe03730f22d",
"name": "Spawner (304)",
"location": [1932, 164, -28],
"map": "Ilshenar",
"count": 1,
"minDelay": "00:05:00",
"maxDelay": "00:10:00",
"team": 0,
"homeRange": 10,
"walkingRange": 15,
"entries": [{ "name": "GolemController", "maxCount": 1, "probability": 100 }]
},
{
"type": "Spawner",
"$type": "Spawner",
"guid": "1156d090-d270-4d2c-99b5-d9801dc4b411",
"name": "Spawner (304)",
"location": [1932, 196, -28],
@ -93,10 +17,26 @@
]
},
{
"type": "Spawner",
"guid": "75b1d8fc-c98d-42c5-9445-12024a38f167",
"$type": "Spawner",
"guid": "21934889-6573-468b-b7e9-40d090d3e9e3",
"name": "Spawner (304)",
"location": [1895, 160, -28],
"location": [1995, 67, -28],
"map": "Ilshenar",
"count": 1,
"minDelay": "00:05:00",
"maxDelay": "00:10:00",
"team": 0,
"homeRange": 10,
"walkingRange": 20,
"entries": [
{ "name": "GolemController", "maxCount": 1, "probability": 100 }
]
},
{
"$type": "Spawner",
"guid": "224edb01-38bc-4e79-abee-c1754f857c1a",
"name": "Spawner (304)",
"location": [2063, 183, -28],
"map": "Ilshenar",
"count": 1,
"minDelay": "00:05:00",
@ -104,10 +44,12 @@
"team": 0,
"homeRange": 10,
"walkingRange": 15,
"entries": [{ "name": "GargoyleDestroyer", "maxCount": 1, "probability": 100 }]
"entries": [
{ "name": "GargoyleEnforcer", "maxCount": 1, "probability": 100 }
]
},
{
"type": "Spawner",
"$type": "Spawner",
"guid": "2704244a-7822-45bf-bc70-120c145fc097",
"name": "Spawner (304)",
"location": [1979, 192, -28],
@ -124,160 +66,7 @@
]
},
{
"type": "Spawner",
"guid": "f7bef168-af9c-4bc5-9928-348891d481c0",
"name": "Spawner (304)",
"location": [1996, 164, -28],
"map": "Ilshenar",
"count": 1,
"minDelay": "00:05:00",
"maxDelay": "00:10:00",
"team": 0,
"homeRange": 10,
"walkingRange": 15,
"entries": [{ "name": "GolemController", "maxCount": 1, "probability": 100 }]
},
{
"type": "Spawner",
"guid": "3a54ee30-73d0-4f81-8889-ace39a951514",
"name": "Spawner (304)",
"location": [2032, 184, -28],
"map": "Ilshenar",
"count": 1,
"minDelay": "00:05:00",
"maxDelay": "00:10:00",
"team": 0,
"homeRange": 10,
"walkingRange": 15,
"entries": [{ "name": "GargoyleEnforcer", "maxCount": 1, "probability": 100 }]
},
{
"type": "Spawner",
"guid": "224edb01-38bc-4e79-abee-c1754f857c1a",
"name": "Spawner (304)",
"location": [2063, 183, -28],
"map": "Ilshenar",
"count": 1,
"minDelay": "00:05:00",
"maxDelay": "00:10:00",
"team": 0,
"homeRange": 10,
"walkingRange": 15,
"entries": [{ "name": "GargoyleEnforcer", "maxCount": 1, "probability": 100 }]
},
{
"type": "Spawner",
"guid": "dddffe92-2c45-4677-9690-2e782e597530",
"name": "Spawner (304)",
"location": [2058, 163, -28],
"map": "Ilshenar",
"count": 2,
"minDelay": "00:05:00",
"maxDelay": "00:10:00",
"team": 0,
"homeRange": 10,
"walkingRange": 15,
"entries": [
{ "name": "GargoyleEnforcer", "maxCount": 2, "probability": 100 },
{ "name": "GargoyleDestroyer", "maxCount": 2, "probability": 100 }
]
},
{
"type": "Spawner",
"guid": "a2ff6be0-f521-42fa-8cda-0ca0436133d6",
"name": "Spawner (304)",
"location": [2044, 143, -28],
"map": "Ilshenar",
"count": 3,
"minDelay": "00:05:00",
"maxDelay": "00:10:00",
"team": 0,
"homeRange": 10,
"walkingRange": 15,
"entries": [
{ "name": "GolemController", "maxCount": 3, "probability": 100 },
{ "name": "Golem", "maxCount": 3, "probability": 100 },
{ "name": "ExodusMinion", "maxCount": 3, "probability": 100 }
]
},
{
"type": "Spawner",
"guid": "98c1814f-9347-402a-93aa-0d3fad5ca8e5",
"name": "Spawner (304)",
"location": [2071, 116, -28],
"map": "Ilshenar",
"count": 2,
"minDelay": "00:05:00",
"maxDelay": "00:10:00",
"team": 0,
"homeRange": 10,
"walkingRange": 15,
"entries": [
{ "name": "GolemController", "maxCount": 2, "probability": 100 },
{ "name": "ExodusOverseer", "maxCount": 2, "probability": 100 }
]
},
{
"type": "Spawner",
"guid": "8c609c03-ea54-4cb3-8e79-ae67bbe813a8",
"name": "Spawner (304)",
"location": [2044, 87, -28],
"map": "Ilshenar",
"count": 2,
"minDelay": "00:05:00",
"maxDelay": "00:10:00",
"team": 0,
"homeRange": 10,
"walkingRange": 15,
"entries": [
{ "name": "ExodusMinion", "maxCount": 2, "probability": 100 },
{ "name": "Golem", "maxCount": 2, "probability": 100 }
]
},
{
"type": "Spawner",
"guid": "e3d9b4cc-ec56-4fed-a26c-330d2bd4edac",
"name": "Spawner (304)",
"location": [2031, 48, -28],
"map": "Ilshenar",
"count": 1,
"minDelay": "00:05:00",
"maxDelay": "00:10:00",
"team": 0,
"homeRange": 10,
"walkingRange": 15,
"entries": [{ "name": "GargoyleEnforcer", "maxCount": 1, "probability": 100 }]
},
{
"type": "Spawner",
"guid": "f226735c-6233-4aba-989b-d6f90cd48e2f",
"name": "Spawner (304)",
"location": [2038, 67, -28],
"map": "Ilshenar",
"count": 1,
"minDelay": "00:05:00",
"maxDelay": "00:10:00",
"team": 0,
"homeRange": 10,
"walkingRange": 15,
"entries": [{ "name": "GargoyleDestroyer", "maxCount": 1, "probability": 100 }]
},
{
"type": "Spawner",
"guid": "21934889-6573-468b-b7e9-40d090d3e9e3",
"name": "Spawner (304)",
"location": [1995, 67, -28],
"map": "Ilshenar",
"count": 1,
"minDelay": "00:05:00",
"maxDelay": "00:10:00",
"team": 0,
"homeRange": 10,
"walkingRange": 20,
"entries": [{ "name": "GolemController", "maxCount": 1, "probability": 100 }]
},
{
"type": "Spawner",
"$type": "Spawner",
"guid": "2cddb981-ac94-4967-9c20-2cc3de1a52c9",
"name": "Spawner (304)",
"location": [1980, 39, -28],
@ -295,10 +84,27 @@
]
},
{
"type": "Spawner",
"guid": "b44ae0e1-0a06-4385-b550-c72c58b6b41f",
"$type": "Spawner",
"guid": "35394aa1-6039-446f-b247-51d9f7926f6c",
"name": "Spawner (304)",
"location": [1931, 67, -28],
"location": [1894, 116, -28],
"map": "Ilshenar",
"count": 2,
"minDelay": "00:05:00",
"maxDelay": "00:10:00",
"team": 0,
"homeRange": 20,
"walkingRange": 30,
"entries": [
{ "name": "GolemController", "maxCount": 2, "probability": 100 },
{ "name": "ExodusOverseer", "maxCount": 2, "probability": 100 }
]
},
{
"$type": "Spawner",
"guid": "3a54ee30-73d0-4f81-8889-ace39a951514",
"name": "Spawner (304)",
"location": [2032, 184, -28],
"map": "Ilshenar",
"count": 1,
"minDelay": "00:05:00",
@ -306,10 +112,12 @@
"team": 0,
"homeRange": 10,
"walkingRange": 15,
"entries": [{ "name": "GolemController", "maxCount": 1, "probability": 100 }]
"entries": [
{ "name": "GargoyleEnforcer", "maxCount": 1, "probability": 100 }
]
},
{
"type": "Spawner",
"$type": "Spawner",
"guid": "412a318b-d693-413c-86e6-92996375f16e",
"name": "Spawner (304)",
"location": [1896, 72, -28],
@ -320,10 +128,145 @@
"team": 0,
"homeRange": 10,
"walkingRange": 15,
"entries": [{ "name": "GargoyleDestroyer", "maxCount": 1, "probability": 100 }]
"entries": [
{ "name": "GargoyleDestroyer", "maxCount": 1, "probability": 100 }
]
},
{
"type": "Spawner",
"$type": "Spawner",
"guid": "75b1d8fc-c98d-42c5-9445-12024a38f167",
"name": "Spawner (304)",
"location": [1895, 160, -28],
"map": "Ilshenar",
"count": 1,
"minDelay": "00:05:00",
"maxDelay": "00:10:00",
"team": 0,
"homeRange": 10,
"walkingRange": 15,
"entries": [
{ "name": "GargoyleDestroyer", "maxCount": 1, "probability": 100 }
]
},
{
"$type": "Spawner",
"guid": "8c609c03-ea54-4cb3-8e79-ae67bbe813a8",
"name": "Spawner (304)",
"location": [2044, 87, -28],
"map": "Ilshenar",
"count": 2,
"minDelay": "00:05:00",
"maxDelay": "00:10:00",
"team": 0,
"homeRange": 10,
"walkingRange": 15,
"entries": [
{ "name": "ExodusMinion", "maxCount": 2, "probability": 100 },
{ "name": "Golem", "maxCount": 2, "probability": 100 }
]
},
{
"$type": "Spawner",
"guid": "8e098781-936b-42cc-adfa-806111b9a9f2",
"name": "Spawner (304)",
"location": [1980, 116, -28],
"map": "Ilshenar",
"count": 2,
"minDelay": "00:05:00",
"maxDelay": "00:10:00",
"team": 0,
"homeRange": 30,
"walkingRange": 40,
"entries": [
{ "name": "EnslavedGargoyle", "maxCount": 2, "probability": 100 },
{ "name": "GolemController", "maxCount": 2, "probability": 100 }
]
},
{
"$type": "Spawner",
"guid": "918f3735-8e70-42c3-a633-2fe03730f22d",
"name": "Spawner (304)",
"location": [1932, 164, -28],
"map": "Ilshenar",
"count": 1,
"minDelay": "00:05:00",
"maxDelay": "00:10:00",
"team": 0,
"homeRange": 10,
"walkingRange": 15,
"entries": [
{ "name": "GolemController", "maxCount": 1, "probability": 100 }
]
},
{
"$type": "Spawner",
"guid": "98c1814f-9347-402a-93aa-0d3fad5ca8e5",
"name": "Spawner (304)",
"location": [2071, 116, -28],
"map": "Ilshenar",
"count": 2,
"minDelay": "00:05:00",
"maxDelay": "00:10:00",
"team": 0,
"homeRange": 10,
"walkingRange": 15,
"entries": [
{ "name": "GolemController", "maxCount": 2, "probability": 100 },
{ "name": "ExodusOverseer", "maxCount": 2, "probability": 100 }
]
},
{
"$type": "Spawner",
"guid": "a2ff6be0-f521-42fa-8cda-0ca0436133d6",
"name": "Spawner (304)",
"location": [2044, 143, -28],
"map": "Ilshenar",
"count": 3,
"minDelay": "00:05:00",
"maxDelay": "00:10:00",
"team": 0,
"homeRange": 10,
"walkingRange": 15,
"entries": [
{ "name": "GolemController", "maxCount": 3, "probability": 100 },
{ "name": "Golem", "maxCount": 3, "probability": 100 },
{ "name": "ExodusMinion", "maxCount": 3, "probability": 100 }
]
},
{
"$type": "Spawner",
"guid": "a35646cf-0460-48d2-a4ea-5e9cc33ad9ab",
"name": "Spawner (304)",
"location": [1964, 164, -28],
"map": "Ilshenar",
"count": 1,
"minDelay": "00:05:00",
"maxDelay": "00:10:00",
"team": 0,
"homeRange": 10,
"walkingRange": 15,
"entries": [
{ "name": "GolemController", "maxCount": 1, "probability": 100 }
]
},
{
"$type": "Spawner",
"guid": "b44ae0e1-0a06-4385-b550-c72c58b6b41f",
"name": "Spawner (304)",
"location": [1931, 67, -28],
"map": "Ilshenar",
"count": 1,
"minDelay": "00:05:00",
"maxDelay": "00:10:00",
"team": 0,
"homeRange": 10,
"walkingRange": 15,
"entries": [
{ "name": "GolemController", "maxCount": 1, "probability": 100 }
]
},
{
"$type": "Spawner",
"guid": "c6d64523-ac82-4ad3-9f39-f2d80731a5a3",
"name": "Spawner (304)",
"location": [1932, 35, -28],
@ -338,5 +281,86 @@
{ "name": "GolemController", "maxCount": 2, "probability": 100 },
{ "name": "Golem", "maxCount": 2, "probability": 100 }
]
},
{
"$type": "Spawner",
"guid": "d39e9374-f30b-488c-8a12-5f3a6315f391",
"name": "Spawner (304)",
"location": [1939, 116, -28],
"map": "Ilshenar",
"count": 1,
"minDelay": "00:05:00",
"maxDelay": "00:10:00",
"team": 0,
"homeRange": 15,
"walkingRange": 25,
"entries": [
{ "name": "ExodusMinion", "maxCount": 1, "probability": 100 }
]
},
{
"$type": "Spawner",
"guid": "dddffe92-2c45-4677-9690-2e782e597530",
"name": "Spawner (304)",
"location": [2058, 163, -28],
"map": "Ilshenar",
"count": 2,
"minDelay": "00:05:00",
"maxDelay": "00:10:00",
"team": 0,
"homeRange": 10,
"walkingRange": 15,
"entries": [
{ "name": "GargoyleEnforcer", "maxCount": 2, "probability": 100 },
{ "name": "GargoyleDestroyer", "maxCount": 2, "probability": 100 }
]
},
{
"$type": "Spawner",
"guid": "e3d9b4cc-ec56-4fed-a26c-330d2bd4edac",
"name": "Spawner (304)",
"location": [2031, 48, -28],
"map": "Ilshenar",
"count": 1,
"minDelay": "00:05:00",
"maxDelay": "00:10:00",
"team": 0,
"homeRange": 10,
"walkingRange": 15,
"entries": [
{ "name": "GargoyleEnforcer", "maxCount": 1, "probability": 100 }
]
},
{
"$type": "Spawner",
"guid": "f226735c-6233-4aba-989b-d6f90cd48e2f",
"name": "Spawner (304)",
"location": [2038, 67, -28],
"map": "Ilshenar",
"count": 1,
"minDelay": "00:05:00",
"maxDelay": "00:10:00",
"team": 0,
"homeRange": 10,
"walkingRange": 15,
"entries": [
{ "name": "GargoyleDestroyer", "maxCount": 1, "probability": 100 }
]
},
{
"$type": "Spawner",
"guid": "f7bef168-af9c-4bc5-9928-348891d481c0",
"name": "Spawner (304)",
"location": [1996, 164, -28],
"map": "Ilshenar",
"count": 1,
"minDelay": "00:05:00",
"maxDelay": "00:10:00",
"team": 0,
"homeRange": 10,
"walkingRange": 15,
"entries": [
{ "name": "GolemController", "maxCount": 1, "probability": 100 }
]
}
]

View file

@ -1,34 +1,6 @@
[
{
"type": "Spawner",
"guid": "46803838-2abe-45af-8c77-4deccf2e99d7",
"name": "Spawner (305)",
"location": [1444, 1519, -28],
"map": "Ilshenar",
"count": 4,
"minDelay": "00:02:00",
"maxDelay": "00:05:00",
"team": 0,
"homeRange": 10,
"walkingRange": 10,
"entries": [{ "name": "MushroomTrap", "maxCount": 4, "probability": 100 }]
},
{
"type": "Spawner",
"guid": "f5a9bc72-4489-4b3a-83ee-7c476d4c9396",
"name": "Spawner (305)",
"location": [1441, 1532, -28],
"map": "Ilshenar",
"count": 4,
"minDelay": "00:02:00",
"maxDelay": "00:05:00",
"team": 0,
"homeRange": 10,
"walkingRange": 10,
"entries": [{ "name": "MushroomTrap", "maxCount": 4, "probability": 100 }]
},
{
"type": "Spawner",
"$type": "Spawner",
"guid": "3e3f2805-df30-4d29-abdd-e8a53002b1d2",
"name": "Spawner (305)",
"location": [1415, 1498, -27],
@ -39,10 +11,28 @@
"team": 0,
"homeRange": 4,
"walkingRange": 4,
"entries": [{ "name": "MushroomTrap", "maxCount": 4, "probability": 100 }]
"entries": [
{ "name": "MushroomTrap", "maxCount": 4, "probability": 100 }
]
},
{
"type": "Spawner",
"$type": "Spawner",
"guid": "46803838-2abe-45af-8c77-4deccf2e99d7",
"name": "Spawner (305)",
"location": [1444, 1519, -28],
"map": "Ilshenar",
"count": 4,
"minDelay": "00:02:00",
"maxDelay": "00:05:00",
"team": 0,
"homeRange": 10,
"walkingRange": 10,
"entries": [
{ "name": "MushroomTrap", "maxCount": 4, "probability": 100 }
]
},
{
"$type": "Spawner",
"guid": "e7588959-6148-4bf7-a0b1-829b3dca6e1b",
"name": "Spawner (305)",
"location": [1464, 1495, -28],
@ -53,6 +43,24 @@
"team": 0,
"homeRange": 10,
"walkingRange": 10,
"entries": [{ "name": "MushroomTrap", "maxCount": 4, "probability": 100 }]
"entries": [
{ "name": "MushroomTrap", "maxCount": 4, "probability": 100 }
]
},
{
"$type": "Spawner",
"guid": "f5a9bc72-4489-4b3a-83ee-7c476d4c9396",
"name": "Spawner (305)",
"location": [1441, 1532, -28],
"map": "Ilshenar",
"count": 4,
"minDelay": "00:02:00",
"maxDelay": "00:05:00",
"team": 0,
"homeRange": 10,
"walkingRange": 10,
"entries": [
{ "name": "MushroomTrap", "maxCount": 4, "probability": 100 }
]
}
]

File diff suppressed because it is too large Load diff

View file

@ -1,42 +1,6 @@
[
{
"type": "Spawner",
"guid": "46e1d6c5-ebe2-4575-beeb-3e35754caef7",
"name": "Spawner (307)",
"location": [1310, 1519, -28],
"map": "Ilshenar",
"count": 21,
"minDelay": "00:05:00",
"maxDelay": "00:10:00",
"team": 0,
"homeRange": 50,
"walkingRange": 50,
"entries": [
{ "name": "Ratman", "maxCount": 7, "probability": 100 },
{ "name": "RatmanMage", "maxCount": 7, "probability": 100 },
{ "name": "EarthElemental", "maxCount": 7, "probability": 100 }
]
},
{
"type": "Spawner",
"guid": "a96346f4-9aca-4dd6-b1ff-e823d8741a19",
"name": "Spawner (307)",
"location": [1224, 1524, -28],
"map": "Ilshenar",
"count": 10,
"minDelay": "00:05:00",
"maxDelay": "00:10:00",
"team": 0,
"homeRange": 30,
"walkingRange": 30,
"entries": [
{ "name": "SkeletalKnight", "maxCount": 5, "probability": 100 },
{ "name": "BoneKnight", "maxCount": 5, "probability": 100 },
{ "name": "LavaLizard", "maxCount": 5, "probability": 100 }
]
},
{
"type": "Spawner",
"$type": "Spawner",
"guid": "0182a6f9-bdde-4ca6-95c8-a7c146b1a0e1",
"name": "Spawner (307)",
"location": [1224, 1494, -28],
@ -54,21 +18,7 @@
]
},
{
"type": "Spawner",
"guid": "c6766cd8-89ac-4169-a2d9-8216a74369a4",
"name": "Spawner (307)",
"location": [1181, 1512, -68],
"map": "Ilshenar",
"count": 1,
"minDelay": "00:05:00",
"maxDelay": "00:10:00",
"team": 0,
"homeRange": 10,
"walkingRange": 10,
"entries": [{ "name": "SkeletalDragon", "maxCount": 1, "probability": 100 }]
},
{
"type": "Spawner",
"$type": "Spawner",
"guid": "2694470a-4c93-488c-96a3-87938993d306",
"name": "Spawner (307)",
"location": [1171, 1511, -68],
@ -83,5 +33,57 @@
{ "name": "TreasureChestLevel3", "maxCount": 1, "probability": 100 },
{ "name": "TreasureChestLevel4", "maxCount": 1, "probability": 100 }
]
},
{
"$type": "Spawner",
"guid": "46e1d6c5-ebe2-4575-beeb-3e35754caef7",
"name": "Spawner (307)",
"location": [1310, 1519, -28],
"map": "Ilshenar",
"count": 21,
"minDelay": "00:05:00",
"maxDelay": "00:10:00",
"team": 0,
"homeRange": 50,
"walkingRange": 50,
"entries": [
{ "name": "Ratman", "maxCount": 7, "probability": 100 },
{ "name": "RatmanMage", "maxCount": 7, "probability": 100 },
{ "name": "EarthElemental", "maxCount": 7, "probability": 100 }
]
},
{
"$type": "Spawner",
"guid": "a96346f4-9aca-4dd6-b1ff-e823d8741a19",
"name": "Spawner (307)",
"location": [1224, 1524, -28],
"map": "Ilshenar",
"count": 10,
"minDelay": "00:05:00",
"maxDelay": "00:10:00",
"team": 0,
"homeRange": 30,
"walkingRange": 30,
"entries": [
{ "name": "SkeletalKnight", "maxCount": 5, "probability": 100 },
{ "name": "BoneKnight", "maxCount": 5, "probability": 100 },
{ "name": "LavaLizard", "maxCount": 5, "probability": 100 }
]
},
{
"$type": "Spawner",
"guid": "c6766cd8-89ac-4169-a2d9-8216a74369a4",
"name": "Spawner (307)",
"location": [1181, 1512, -68],
"map": "Ilshenar",
"count": 1,
"minDelay": "00:05:00",
"maxDelay": "00:10:00",
"team": 0,
"homeRange": 10,
"walkingRange": 10,
"entries": [
{ "name": "SkeletalDragon", "maxCount": 1, "probability": 100 }
]
}
]

View file

@ -1,6 +1,55 @@
[
{
"type": "Spawner",
"$type": "Spawner",
"guid": "027c989e-9d77-4dfd-85d8-b7c83f722bde",
"name": "Spawner (308)",
"location": [2094, 56, -32],
"map": "Ilshenar",
"count": 1,
"minDelay": "00:05:00",
"maxDelay": "00:10:00",
"team": 0,
"homeRange": 5,
"walkingRange": 10,
"entries": [
{ "name": "Executioner", "maxCount": 1, "probability": 100 }
]
},
{
"$type": "Spawner",
"guid": "148839f1-8a5b-44bd-b6f5-3b310de5c951",
"name": "Spawner (308)",
"location": [2155, 24, -32],
"map": "Ilshenar",
"count": 2,
"minDelay": "00:05:00",
"maxDelay": "00:10:00",
"team": 0,
"homeRange": 5,
"walkingRange": 10,
"entries": [
{ "name": "PoisonElemental", "maxCount": 2, "probability": 100 },
{ "name": "LichLord", "maxCount": 2, "probability": 100 }
]
},
{
"$type": "Spawner",
"guid": "151afe47-a4c7-42d9-999b-6be7ab8ed901",
"name": "Spawner (308)",
"location": [2125, 137, -32],
"map": "Ilshenar",
"count": 1,
"minDelay": "00:05:00",
"maxDelay": "00:10:00",
"team": 0,
"homeRange": 5,
"walkingRange": 10,
"entries": [
{ "name": "Lich", "maxCount": 1, "probability": 100 }
]
},
{
"$type": "Spawner",
"guid": "2481376c-f54f-453e-aa09-b0ae25a2da13",
"name": "Spawner (308)",
"location": [2188, 310, -7],
@ -18,111 +67,7 @@
]
},
{
"type": "Spawner",
"guid": "93052936-70f8-43b4-a20c-d68adab714c6",
"name": "Spawner (308)",
"location": [2220, 75, -27],
"map": "Ilshenar",
"count": 1,
"minDelay": "00:05:00",
"maxDelay": "00:10:00",
"team": 0,
"homeRange": 5,
"walkingRange": 10,
"entries": [{ "name": "AcidElemental", "maxCount": 1, "probability": 100 }]
},
{
"type": "Spawner",
"guid": "6d9c7f2b-dc74-4ca3-9396-fb2a38fa2d80",
"name": "Spawner (308)",
"location": [2207, 113, -27],
"map": "Ilshenar",
"count": 1,
"minDelay": "00:05:00",
"maxDelay": "00:10:00",
"team": 0,
"homeRange": 5,
"walkingRange": 10,
"entries": [{ "name": "StoneGargoyle", "maxCount": 1, "probability": 100 }]
},
{
"type": "Spawner",
"guid": "5c78812b-3143-4ab4-a3e2-950a74181fb1",
"name": "Spawner (308)",
"location": [2185, 168, -32],
"map": "Ilshenar",
"count": 1,
"minDelay": "00:05:00",
"maxDelay": "00:10:00",
"team": 0,
"homeRange": 5,
"walkingRange": 10,
"entries": [{ "name": "TreasureChestLevel4", "maxCount": 1, "probability": 100 }]
},
{
"type": "Spawner",
"guid": "8d713f41-daf3-40c4-9d90-bbf4befb1b08",
"name": "Spawner (308)",
"location": [2229, 143, -32],
"map": "Ilshenar",
"count": 2,
"minDelay": "00:05:00",
"maxDelay": "00:10:00",
"team": 0,
"homeRange": 5,
"walkingRange": 10,
"entries": [
{ "name": "EvilMage", "maxCount": 2, "probability": 100 },
{ "name": "Executioner", "maxCount": 2, "probability": 100 }
]
},
{
"type": "Spawner",
"guid": "bdba50d6-07c1-40ad-ba33-ac67631712cc",
"name": "Spawner (308)",
"location": [2154, 163, -27],
"map": "Ilshenar",
"count": 1,
"minDelay": "00:05:00",
"maxDelay": "00:10:00",
"team": 0,
"homeRange": 5,
"walkingRange": 10,
"entries": [{ "name": "Lich", "maxCount": 1, "probability": 100 }]
},
{
"type": "Spawner",
"guid": "fc19f651-1312-4542-80df-b9f2362974da",
"name": "Spawner (308)",
"location": [2125, 165, -32],
"map": "Ilshenar",
"count": 2,
"minDelay": "00:05:00",
"maxDelay": "00:10:00",
"team": 0,
"homeRange": 5,
"walkingRange": 10,
"entries": [
{ "name": "EvilMage", "maxCount": 2, "probability": 100 },
{ "name": "Executioner", "maxCount": 2, "probability": 100 }
]
},
{
"type": "Spawner",
"guid": "151afe47-a4c7-42d9-999b-6be7ab8ed901",
"name": "Spawner (308)",
"location": [2125, 137, -32],
"map": "Ilshenar",
"count": 1,
"minDelay": "00:05:00",
"maxDelay": "00:10:00",
"team": 0,
"homeRange": 5,
"walkingRange": 10,
"entries": [{ "name": "Lich", "maxCount": 1, "probability": 100 }]
},
{
"type": "Spawner",
"$type": "Spawner",
"guid": "3a6c8beb-1840-4e1a-b55e-b44158315f59",
"name": "Spawner (308)",
"location": [2138, 97, -27],
@ -133,10 +78,12 @@
"team": 0,
"homeRange": 5,
"walkingRange": 10,
"entries": [{ "name": "ElderGazer", "maxCount": 1, "probability": 100 }]
"entries": [
{ "name": "ElderGazer", "maxCount": 1, "probability": 100 }
]
},
{
"type": "Spawner",
"$type": "Spawner",
"guid": "4f5851f5-110c-4155-91a4-5d6715b52c2f",
"name": "Spawner (308)",
"location": [2183, 85, -32],
@ -153,10 +100,42 @@
]
},
{
"type": "Spawner",
"guid": "148839f1-8a5b-44bd-b6f5-3b310de5c951",
"$type": "Spawner",
"guid": "5c78812b-3143-4ab4-a3e2-950a74181fb1",
"name": "Spawner (308)",
"location": [2155, 24, -32],
"location": [2185, 168, -32],
"map": "Ilshenar",
"count": 1,
"minDelay": "00:05:00",
"maxDelay": "00:10:00",
"team": 0,
"homeRange": 5,
"walkingRange": 10,
"entries": [
{ "name": "TreasureChestLevel4", "maxCount": 1, "probability": 100 }
]
},
{
"$type": "Spawner",
"guid": "6d9c7f2b-dc74-4ca3-9396-fb2a38fa2d80",
"name": "Spawner (308)",
"location": [2207, 113, -27],
"map": "Ilshenar",
"count": 1,
"minDelay": "00:05:00",
"maxDelay": "00:10:00",
"team": 0,
"homeRange": 5,
"walkingRange": 10,
"entries": [
{ "name": "StoneGargoyle", "maxCount": 1, "probability": 100 }
]
},
{
"$type": "Spawner",
"guid": "8d713f41-daf3-40c4-9d90-bbf4befb1b08",
"name": "Spawner (308)",
"location": [2229, 143, -32],
"map": "Ilshenar",
"count": 2,
"minDelay": "00:05:00",
@ -165,12 +144,12 @@
"homeRange": 5,
"walkingRange": 10,
"entries": [
{ "name": "PoisonElemental", "maxCount": 2, "probability": 100 },
{ "name": "LichLord", "maxCount": 2, "probability": 100 }
{ "name": "EvilMage", "maxCount": 2, "probability": 100 },
{ "name": "Executioner", "maxCount": 2, "probability": 100 }
]
},
{
"type": "Spawner",
"$type": "Spawner",
"guid": "92ee0621-2fc1-434c-a2d8-11b9359e01bc",
"name": "Spawner (308)",
"location": [2124, 96, -32],
@ -188,10 +167,10 @@
]
},
{
"type": "Spawner",
"guid": "027c989e-9d77-4dfd-85d8-b7c83f722bde",
"$type": "Spawner",
"guid": "93052936-70f8-43b4-a20c-d68adab714c6",
"name": "Spawner (308)",
"location": [2094, 56, -32],
"location": [2220, 75, -27],
"map": "Ilshenar",
"count": 1,
"minDelay": "00:05:00",
@ -199,10 +178,28 @@
"team": 0,
"homeRange": 5,
"walkingRange": 10,
"entries": [{ "name": "Executioner", "maxCount": 1, "probability": 100 }]
"entries": [
{ "name": "AcidElemental", "maxCount": 1, "probability": 100 }
]
},
{
"type": "Spawner",
"$type": "Spawner",
"guid": "bdba50d6-07c1-40ad-ba33-ac67631712cc",
"name": "Spawner (308)",
"location": [2154, 163, -27],
"map": "Ilshenar",
"count": 1,
"minDelay": "00:05:00",
"maxDelay": "00:10:00",
"team": 0,
"homeRange": 5,
"walkingRange": 10,
"entries": [
{ "name": "Lich", "maxCount": 1, "probability": 100 }
]
},
{
"$type": "Spawner",
"guid": "f8cc122d-1672-4b14-a6bf-4dcc6178efdc",
"name": "Spawner (308)",
"location": [2127, 29, -32],
@ -217,5 +214,22 @@
{ "name": "HellHound", "maxCount": 1, "probability": 100 },
{ "name": "LichLord", "maxCount": 1, "probability": 100 }
]
},
{
"$type": "Spawner",
"guid": "fc19f651-1312-4542-80df-b9f2362974da",
"name": "Spawner (308)",
"location": [2125, 165, -32],
"map": "Ilshenar",
"count": 2,
"minDelay": "00:05:00",
"maxDelay": "00:10:00",
"team": 0,
"homeRange": 5,
"walkingRange": 10,
"entries": [
{ "name": "EvilMage", "maxCount": 2, "probability": 100 },
{ "name": "Executioner", "maxCount": 2, "probability": 100 }
]
}
]

View file

@ -1,20 +1,188 @@
[
{
"type": "Spawner",
"guid": "fc22ceb7-01e2-4389-a356-ffc788071f0f",
"$type": "Spawner",
"guid": "091c33d5-ee75-471b-aafc-78ea45d3dd66",
"name": "Spawner (309)",
"location": [427, 67, -28],
"location": [336, 48, -28],
"map": "Ilshenar",
"count": 2,
"minDelay": "00:05:00",
"maxDelay": "00:10:00",
"team": 0,
"homeRange": 25,
"walkingRange": 25,
"entries": [{ "name": "Zombie", "maxCount": 2, "probability": 100 }]
"homeRange": 8,
"walkingRange": 10,
"entries": [
{ "name": "Gargoyle", "maxCount": 2, "probability": 100 }
]
},
{
"type": "Spawner",
"$type": "Spawner",
"guid": "10f6b4be-fd86-480d-b285-d00f0f55b59c",
"name": "Spawner (309)",
"location": [155, 21, -28],
"map": "Ilshenar",
"count": 3,
"minDelay": "00:05:00",
"maxDelay": "00:10:00",
"team": 0,
"homeRange": 10,
"walkingRange": 15,
"entries": [
{ "name": "Balron", "maxCount": 2, "probability": 100 },
{ "name": "ElderGazer", "maxCount": 1, "probability": 100 }
]
},
{
"$type": "Spawner",
"guid": "1fb85d78-33ef-42c5-8145-e0af0ec5bd33",
"name": "Spawner (309)",
"location": [336, 37, -28],
"map": "Ilshenar",
"count": 2,
"minDelay": "00:05:00",
"maxDelay": "00:10:00",
"team": 0,
"homeRange": 8,
"walkingRange": 10,
"entries": [
{ "name": "Gargoyle", "maxCount": 2, "probability": 100 }
]
},
{
"$type": "Spawner",
"guid": "20ea3ede-9733-4b69-a2a9-44312ad782df",
"name": "Spawner (309)",
"location": [254, 20, -28],
"map": "Ilshenar",
"count": 1,
"minDelay": "00:05:00",
"maxDelay": "00:10:00",
"team": 0,
"homeRange": 5,
"walkingRange": 10,
"entries": [
{ "name": "BloodElemental", "maxCount": 1, "probability": 100 }
]
},
{
"$type": "Spawner",
"guid": "271dd12c-8c89-4962-8766-27c6e23ab039",
"name": "Spawner (309)",
"location": [427, 95, -28],
"map": "Ilshenar",
"count": 6,
"minDelay": "00:05:00",
"maxDelay": "00:10:00",
"team": 0,
"homeRange": 10,
"walkingRange": 15,
"entries": [
{ "name": "Skeleton", "maxCount": 4, "probability": 100 },
{ "name": "BoneKnight", "maxCount": 2, "probability": 100 }
]
},
{
"$type": "Spawner",
"guid": "27540523-3186-413d-99da-3fd4ef5be98f",
"name": "Spawner (309)",
"location": [281, 20, -28],
"map": "Ilshenar",
"count": 7,
"minDelay": "00:05:00",
"maxDelay": "00:10:00",
"team": 0,
"homeRange": 10,
"walkingRange": 15,
"entries": [
{ "name": "ElderGazer", "maxCount": 1, "probability": 100 },
{ "name": "Gazer", "maxCount": 4, "probability": 100 },
{ "name": "GazerLarva", "maxCount": 2, "probability": 100 }
]
},
{
"$type": "Spawner",
"guid": "29b9ab63-8072-4a0e-b8b2-cbdbe54a74c6",
"name": "Spawner (309)",
"location": [462, 16, -28],
"map": "Ilshenar",
"count": 4,
"minDelay": "00:05:00",
"maxDelay": "00:10:00",
"team": 0,
"homeRange": 5,
"walkingRange": 15,
"entries": [
{ "name": "HeadlessOne", "maxCount": 2, "probability": 100 },
{ "name": "Mongbat", "maxCount": 2, "probability": 100 }
]
},
{
"$type": "Spawner",
"guid": "37598c17-946c-4a0a-8e86-20d1329e4bca",
"name": "Spawner (309)",
"location": [133, 114, -28],
"map": "Ilshenar",
"count": 1,
"minDelay": "00:05:00",
"maxDelay": "00:10:00",
"team": 0,
"homeRange": 20,
"walkingRange": 25,
"entries": [
{ "name": "BoneKnight", "maxCount": 1, "probability": 100 },
{ "name": "SkeletalKnight", "maxCount": 1, "probability": 100 }
]
},
{
"$type": "Spawner",
"guid": "3aad6c47-815b-4713-93f2-c57156aded30",
"name": "Spawner (309)",
"location": [305, 20, -28],
"map": "Ilshenar",
"count": 5,
"minDelay": "00:05:00",
"maxDelay": "00:10:00",
"team": 0,
"homeRange": 10,
"walkingRange": 20,
"entries": [
{ "name": "DullCopperElemental", "maxCount": 5, "probability": 100 }
]
},
{
"$type": "Spawner",
"guid": "492af6a5-b034-4a8b-aa7b-923b906321e2",
"name": "Spawner (309)",
"location": [133, 114, -28],
"map": "Ilshenar",
"count": 1,
"minDelay": "00:05:00",
"maxDelay": "00:10:00",
"team": 0,
"homeRange": 10,
"walkingRange": 20,
"entries": [
{ "name": "BloodElemental", "maxCount": 1, "probability": 100 }
]
},
{
"$type": "Spawner",
"guid": "5bceb873-73d8-4326-8ec1-46bb48c65d60",
"name": "Spawner (309)",
"location": [214, 67, -28],
"map": "Ilshenar",
"count": 4,
"minDelay": "00:05:00",
"maxDelay": "00:10:00",
"team": 0,
"homeRange": 15,
"walkingRange": 40,
"entries": [
{ "name": "Imp", "maxCount": 4, "probability": 100 }
]
},
{
"$type": "Spawner",
"guid": "5e285fc0-52a0-4f23-ad8f-bc7e90b2fcec",
"name": "Spawner (309)",
"location": [455, 70, -28],
@ -32,7 +200,7 @@
]
},
{
"type": "Spawner",
"$type": "Spawner",
"guid": "6433de3a-a4f6-4d51-9249-0dc37a3fe107",
"name": "Spawner (309)",
"location": [394, 69, -28],
@ -49,7 +217,25 @@
]
},
{
"type": "Spawner",
"$type": "Spawner",
"guid": "65f92121-fb34-4f86-9722-4ee64ec64b61",
"name": "Spawner (309)",
"location": [109, 44, -28],
"map": "Ilshenar",
"count": 7,
"minDelay": "00:05:00",
"maxDelay": "00:10:00",
"team": 0,
"homeRange": 15,
"walkingRange": 20,
"entries": [
{ "name": "BoneMagi", "maxCount": 4, "probability": 100 },
{ "name": "SkeletalMage", "maxCount": 4, "probability": 100 },
{ "name": "Mummy", "maxCount": 3, "probability": 100 }
]
},
{
"$type": "Spawner",
"guid": "72c10d82-2c9a-489b-8bad-28dac3e794c9",
"name": "Spawner (309)",
"location": [447, 19, -28],
@ -68,7 +254,23 @@
]
},
{
"type": "Spawner",
"$type": "Spawner",
"guid": "73b4ab03-1cab-4034-a3c0-9b8ba61d3600",
"name": "Spawner (309)",
"location": [160, 59, -28],
"map": "Ilshenar",
"count": 1,
"minDelay": "00:05:00",
"maxDelay": "00:10:00",
"team": 0,
"homeRange": 10,
"walkingRange": 10,
"entries": [
{ "name": "BloodElemental", "maxCount": 1, "probability": 100 }
]
},
{
"$type": "Spawner",
"guid": "73efa56f-434e-4fc0-8822-c028c9a83e6c",
"name": "Spawner (309)",
"location": [431, 16, -28],
@ -85,101 +287,7 @@
]
},
{
"type": "Spawner",
"guid": "29b9ab63-8072-4a0e-b8b2-cbdbe54a74c6",
"name": "Spawner (309)",
"location": [462, 16, -28],
"map": "Ilshenar",
"count": 4,
"minDelay": "00:05:00",
"maxDelay": "00:10:00",
"team": 0,
"homeRange": 5,
"walkingRange": 15,
"entries": [
{ "name": "HeadlessOne", "maxCount": 2, "probability": 100 },
{ "name": "Mongbat", "maxCount": 2, "probability": 100 }
]
},
{
"type": "Spawner",
"guid": "86f27a56-95cc-49ed-8483-38c07cd31de2",
"name": "Spawner (309)",
"location": [462, 16, -28],
"map": "Ilshenar",
"count": 2,
"minDelay": "00:05:00",
"maxDelay": "00:10:00",
"team": 0,
"homeRange": 5,
"walkingRange": 15,
"entries": [{ "name": "EarthElemental", "maxCount": 2, "probability": 100 }]
},
{
"type": "Spawner",
"guid": "9cebea0f-1699-45af-a9b1-0d79d99d290e",
"name": "Spawner (309)",
"location": [383, 29, -28],
"map": "Ilshenar",
"count": 1,
"minDelay": "00:05:00",
"maxDelay": "00:10:00",
"team": 0,
"homeRange": 5,
"walkingRange": 10,
"entries": [{ "name": "BloodElemental", "maxCount": 1, "probability": 100 }]
},
{
"type": "Spawner",
"guid": "271dd12c-8c89-4962-8766-27c6e23ab039",
"name": "Spawner (309)",
"location": [427, 95, -28],
"map": "Ilshenar",
"count": 6,
"minDelay": "00:05:00",
"maxDelay": "00:10:00",
"team": 0,
"homeRange": 10,
"walkingRange": 15,
"entries": [
{ "name": "Skeleton", "maxCount": 4, "probability": 100 },
{ "name": "BoneKnight", "maxCount": 2, "probability": 100 }
]
},
{
"type": "Spawner",
"guid": "20ea3ede-9733-4b69-a2a9-44312ad782df",
"name": "Spawner (309)",
"location": [254, 20, -28],
"map": "Ilshenar",
"count": 1,
"minDelay": "00:05:00",
"maxDelay": "00:10:00",
"team": 0,
"homeRange": 5,
"walkingRange": 10,
"entries": [{ "name": "BloodElemental", "maxCount": 1, "probability": 100 }]
},
{
"type": "Spawner",
"guid": "27540523-3186-413d-99da-3fd4ef5be98f",
"name": "Spawner (309)",
"location": [281, 20, -28],
"map": "Ilshenar",
"count": 7,
"minDelay": "00:05:00",
"maxDelay": "00:10:00",
"team": 0,
"homeRange": 10,
"walkingRange": 15,
"entries": [
{ "name": "ElderGazer", "maxCount": 1, "probability": 100 },
{ "name": "Gazer", "maxCount": 4, "probability": 100 },
{ "name": "GazerLarva", "maxCount": 2, "probability": 100 }
]
},
{
"type": "Spawner",
"$type": "Spawner",
"guid": "807fd985-4f3a-4d1f-a7be-942294f1d782",
"name": "Spawner (309)",
"location": [238, 48, -28],
@ -196,39 +304,187 @@
]
},
{
"type": "Spawner",
"guid": "5bceb873-73d8-4326-8ec1-46bb48c65d60",
"$type": "Spawner",
"guid": "818d30b9-0568-4e69-9b00-a8567e0f63b4",
"name": "Spawner (309)",
"location": [214, 67, -28],
"location": [143, 70, -28],
"map": "Ilshenar",
"count": 4,
"count": 1,
"minDelay": "00:05:00",
"maxDelay": "00:10:00",
"team": 0,
"homeRange": 15,
"walkingRange": 40,
"entries": [{ "name": "Imp", "maxCount": 4, "probability": 100 }]
},
{
"type": "Spawner",
"guid": "f4631bce-3338-4d0e-88c9-9faa4ba968ef",
"name": "Spawner (309)",
"location": [262, 84, -28],
"map": "Ilshenar",
"count": 3,
"minDelay": "00:05:00",
"maxDelay": "00:10:00",
"team": 0,
"homeRange": 15,
"walkingRange": 25,
"homeRange": 10,
"walkingRange": 20,
"entries": [
{ "name": "Shade", "maxCount": 1, "probability": 100 },
{ "name": "Zombie", "maxCount": 1, "probability": 100 },
{ "name": "DreadSpider", "maxCount": 1, "probability": 100 }
{ "name": "EvilMageLord", "maxCount": 1, "probability": 100 }
]
},
{
"type": "Spawner",
"$type": "Spawner",
"guid": "846a8862-f62b-4814-856b-88a6f9cb0a27",
"name": "Spawner (309)",
"location": [350, 42, -28],
"map": "Ilshenar",
"count": 1,
"minDelay": "00:05:00",
"maxDelay": "00:10:00",
"team": 0,
"homeRange": 10,
"walkingRange": 10,
"entries": [
{ "name": "BloodElemental", "maxCount": 1, "probability": 100 }
]
},
{
"$type": "Spawner",
"guid": "86f27a56-95cc-49ed-8483-38c07cd31de2",
"name": "Spawner (309)",
"location": [462, 16, -28],
"map": "Ilshenar",
"count": 2,
"minDelay": "00:05:00",
"maxDelay": "00:10:00",
"team": 0,
"homeRange": 5,
"walkingRange": 15,
"entries": [
{ "name": "EarthElemental", "maxCount": 2, "probability": 100 }
]
},
{
"$type": "Spawner",
"guid": "94e38cf9-78c0-498b-927d-f7151001f811",
"name": "Spawner (309)",
"location": [67, 70, -28],
"map": "Ilshenar",
"count": 6,
"minDelay": "00:05:00",
"maxDelay": "00:10:00",
"team": 0,
"homeRange": 15,
"walkingRange": 15,
"entries": [
{ "name": "LichLord", "maxCount": 2, "probability": 100 },
{ "name": "Mummy", "maxCount": 4, "probability": 100 }
]
},
{
"$type": "Spawner",
"guid": "99674640-e132-4cdd-a69e-1607e28dc5f3",
"name": "Spawner (309)",
"location": [310, 92, -13],
"map": "Ilshenar",
"count": 2,
"minDelay": "00:05:00",
"maxDelay": "00:10:00",
"team": 0,
"homeRange": 20,
"walkingRange": 20,
"entries": [
{ "name": "LichLord", "maxCount": 2, "probability": 100 }
]
},
{
"$type": "Spawner",
"guid": "9ce0e4a2-0318-4e8d-83a6-48933b302425",
"name": "Spawner (309)",
"location": [336, 42, -28],
"map": "Ilshenar",
"count": 1,
"minDelay": "00:05:00",
"maxDelay": "00:10:00",
"team": 0,
"homeRange": 8,
"walkingRange": 15,
"entries": [
{ "name": "Lich", "maxCount": 1, "probability": 100 },
{ "name": "Shade", "maxCount": 1, "probability": 100 }
]
},
{
"$type": "Spawner",
"guid": "9cebea0f-1699-45af-a9b1-0d79d99d290e",
"name": "Spawner (309)",
"location": [383, 29, -28],
"map": "Ilshenar",
"count": 1,
"minDelay": "00:05:00",
"maxDelay": "00:10:00",
"team": 0,
"homeRange": 5,
"walkingRange": 10,
"entries": [
{ "name": "BloodElemental", "maxCount": 1, "probability": 100 }
]
},
{
"$type": "Spawner",
"guid": "a1b6deba-e340-4851-bba7-4f5ad2d03353",
"name": "Spawner (309)",
"location": [146, 83, -28],
"map": "Ilshenar",
"count": 1,
"minDelay": "00:05:00",
"maxDelay": "00:10:00",
"team": 0,
"homeRange": 10,
"walkingRange": 20,
"entries": [
{ "name": "BloodElemental", "maxCount": 1, "probability": 100 }
]
},
{
"$type": "Spawner",
"guid": "a304cb5f-12b3-401a-96d3-104ee9f33544",
"name": "Spawner (309)",
"location": [311, 91, -13],
"map": "Ilshenar",
"count": 2,
"minDelay": "00:05:00",
"maxDelay": "00:10:00",
"team": 0,
"homeRange": 20,
"walkingRange": 20,
"entries": [
{ "name": "PoisonElemental", "maxCount": 1, "probability": 100 },
{ "name": "BloodElemental", "maxCount": 1, "probability": 100 }
]
},
{
"$type": "Spawner",
"guid": "b6f7c48e-faa6-44a6-b59f-d0b47313a4f6",
"name": "Spawner (309)",
"location": [278, 53, -28],
"map": "Ilshenar",
"count": 7,
"minDelay": "00:05:00",
"maxDelay": "00:10:00",
"team": 0,
"homeRange": 10,
"walkingRange": 20,
"entries": [
{ "name": "HellHound", "maxCount": 6, "probability": 100 },
{ "name": "BloodElemental", "maxCount": 1, "probability": 100 }
]
},
{
"$type": "Spawner",
"guid": "bd576d60-5525-4ee3-b85f-8439e6dd38fd",
"name": "Spawner (309)",
"location": [160, 9, -28],
"map": "Ilshenar",
"count": 2,
"minDelay": "00:05:00",
"maxDelay": "00:10:00",
"team": 0,
"homeRange": 4,
"walkingRange": 5,
"entries": [
{ "name": "Efreet", "maxCount": 2, "probability": 100 }
]
},
{
"$type": "Spawner",
"guid": "bed076f5-fb2b-4342-9423-6a3228cc1978",
"name": "Spawner (309)",
"location": [262, 84, -28],
@ -246,263 +502,37 @@
]
},
{
"type": "Spawner",
"guid": "b6f7c48e-faa6-44a6-b59f-d0b47313a4f6",
"$type": "Spawner",
"guid": "f4631bce-3338-4d0e-88c9-9faa4ba968ef",
"name": "Spawner (309)",
"location": [278, 53, -28],
"map": "Ilshenar",
"count": 7,
"minDelay": "00:05:00",
"maxDelay": "00:10:00",
"team": 0,
"homeRange": 10,
"walkingRange": 20,
"entries": [
{ "name": "HellHound", "maxCount": 6, "probability": 100 },
{ "name": "BloodElemental", "maxCount": 1, "probability": 100 }
]
},
{
"type": "Spawner",
"guid": "3aad6c47-815b-4713-93f2-c57156aded30",
"name": "Spawner (309)",
"location": [305, 20, -28],
"map": "Ilshenar",
"count": 5,
"minDelay": "00:05:00",
"maxDelay": "00:10:00",
"team": 0,
"homeRange": 10,
"walkingRange": 20,
"entries": [{ "name": "DullCopperElemental", "maxCount": 5, "probability": 100 }]
},
{
"type": "Spawner",
"guid": "99674640-e132-4cdd-a69e-1607e28dc5f3",
"name": "Spawner (309)",
"location": [310, 92, -13],
"map": "Ilshenar",
"count": 2,
"minDelay": "00:05:00",
"maxDelay": "00:10:00",
"team": 0,
"homeRange": 20,
"walkingRange": 20,
"entries": [{ "name": "LichLord", "maxCount": 2, "probability": 100 }]
},
{
"type": "Spawner",
"guid": "a304cb5f-12b3-401a-96d3-104ee9f33544",
"name": "Spawner (309)",
"location": [311, 91, -13],
"map": "Ilshenar",
"count": 2,
"minDelay": "00:05:00",
"maxDelay": "00:10:00",
"team": 0,
"homeRange": 20,
"walkingRange": 20,
"entries": [
{ "name": "PoisonElemental", "maxCount": 1, "probability": 100 },
{ "name": "BloodElemental", "maxCount": 1, "probability": 100 }
]
},
{
"type": "Spawner",
"guid": "1fb85d78-33ef-42c5-8145-e0af0ec5bd33",
"name": "Spawner (309)",
"location": [336, 37, -28],
"map": "Ilshenar",
"count": 2,
"minDelay": "00:05:00",
"maxDelay": "00:10:00",
"team": 0,
"homeRange": 8,
"walkingRange": 10,
"entries": [{ "name": "Gargoyle", "maxCount": 2, "probability": 100 }]
},
{
"type": "Spawner",
"guid": "091c33d5-ee75-471b-aafc-78ea45d3dd66",
"name": "Spawner (309)",
"location": [336, 48, -28],
"map": "Ilshenar",
"count": 2,
"minDelay": "00:05:00",
"maxDelay": "00:10:00",
"team": 0,
"homeRange": 8,
"walkingRange": 10,
"entries": [{ "name": "Gargoyle", "maxCount": 2, "probability": 100 }]
},
{
"type": "Spawner",
"guid": "9ce0e4a2-0318-4e8d-83a6-48933b302425",
"name": "Spawner (309)",
"location": [336, 42, -28],
"map": "Ilshenar",
"count": 1,
"minDelay": "00:05:00",
"maxDelay": "00:10:00",
"team": 0,
"homeRange": 8,
"walkingRange": 15,
"entries": [
{ "name": "Lich", "maxCount": 1, "probability": 100 },
{ "name": "Shade", "maxCount": 1, "probability": 100 }
]
},
{
"type": "Spawner",
"guid": "846a8862-f62b-4814-856b-88a6f9cb0a27",
"name": "Spawner (309)",
"location": [350, 42, -28],
"map": "Ilshenar",
"count": 1,
"minDelay": "00:05:00",
"maxDelay": "00:10:00",
"team": 0,
"homeRange": 10,
"walkingRange": 10,
"entries": [{ "name": "BloodElemental", "maxCount": 1, "probability": 100 }]
},
{
"type": "Spawner",
"guid": "a1b6deba-e340-4851-bba7-4f5ad2d03353",
"name": "Spawner (309)",
"location": [146, 83, -28],
"map": "Ilshenar",
"count": 1,
"minDelay": "00:05:00",
"maxDelay": "00:10:00",
"team": 0,
"homeRange": 10,
"walkingRange": 20,
"entries": [{ "name": "BloodElemental", "maxCount": 1, "probability": 100 }]
},
{
"type": "Spawner",
"guid": "818d30b9-0568-4e69-9b00-a8567e0f63b4",
"name": "Spawner (309)",
"location": [143, 70, -28],
"map": "Ilshenar",
"count": 1,
"minDelay": "00:05:00",
"maxDelay": "00:10:00",
"team": 0,
"homeRange": 10,
"walkingRange": 20,
"entries": [{ "name": "EvilMageLord", "maxCount": 1, "probability": 100 }]
},
{
"type": "Spawner",
"guid": "73b4ab03-1cab-4034-a3c0-9b8ba61d3600",
"name": "Spawner (309)",
"location": [160, 59, -28],
"map": "Ilshenar",
"count": 1,
"minDelay": "00:05:00",
"maxDelay": "00:10:00",
"team": 0,
"homeRange": 10,
"walkingRange": 10,
"entries": [{ "name": "BloodElemental", "maxCount": 1, "probability": 100 }]
},
{
"type": "Spawner",
"guid": "10f6b4be-fd86-480d-b285-d00f0f55b59c",
"name": "Spawner (309)",
"location": [155, 21, -28],
"location": [262, 84, -28],
"map": "Ilshenar",
"count": 3,
"minDelay": "00:05:00",
"maxDelay": "00:10:00",
"team": 0,
"homeRange": 10,
"walkingRange": 15,
"homeRange": 15,
"walkingRange": 25,
"entries": [
{ "name": "Balron", "maxCount": 2, "probability": 100 },
{ "name": "ElderGazer", "maxCount": 1, "probability": 100 }
{ "name": "Shade", "maxCount": 1, "probability": 100 },
{ "name": "Zombie", "maxCount": 1, "probability": 100 },
{ "name": "DreadSpider", "maxCount": 1, "probability": 100 }
]
},
{
"type": "Spawner",
"guid": "bd576d60-5525-4ee3-b85f-8439e6dd38fd",
"$type": "Spawner",
"guid": "fc22ceb7-01e2-4389-a356-ffc788071f0f",
"name": "Spawner (309)",
"location": [160, 9, -28],
"location": [427, 67, -28],
"map": "Ilshenar",
"count": 2,
"minDelay": "00:05:00",
"maxDelay": "00:10:00",
"team": 0,
"homeRange": 4,
"walkingRange": 5,
"entries": [{ "name": "Efreet", "maxCount": 2, "probability": 100 }]
},
{
"type": "Spawner",
"guid": "65f92121-fb34-4f86-9722-4ee64ec64b61",
"name": "Spawner (309)",
"location": [109, 44, -28],
"map": "Ilshenar",
"count": 7,
"minDelay": "00:05:00",
"maxDelay": "00:10:00",
"team": 0,
"homeRange": 15,
"walkingRange": 20,
"entries": [
{ "name": "BoneMagi", "maxCount": 4, "probability": 100 },
{ "name": "SkeletalMage", "maxCount": 4, "probability": 100 },
{ "name": "Mummy", "maxCount": 3, "probability": 100 }
]
},
{
"type": "Spawner",
"guid": "94e38cf9-78c0-498b-927d-f7151001f811",
"name": "Spawner (309)",
"location": [67, 70, -28],
"map": "Ilshenar",
"count": 6,
"minDelay": "00:05:00",
"maxDelay": "00:10:00",
"team": 0,
"homeRange": 15,
"walkingRange": 15,
"entries": [
{ "name": "LichLord", "maxCount": 2, "probability": 100 },
{ "name": "Mummy", "maxCount": 4, "probability": 100 }
]
},
{
"type": "Spawner",
"guid": "37598c17-946c-4a0a-8e86-20d1329e4bca",
"name": "Spawner (309)",
"location": [133, 114, -28],
"map": "Ilshenar",
"count": 1,
"minDelay": "00:05:00",
"maxDelay": "00:10:00",
"team": 0,
"homeRange": 20,
"homeRange": 25,
"walkingRange": 25,
"entries": [
{ "name": "BoneKnight", "maxCount": 1, "probability": 100 },
{ "name": "SkeletalKnight", "maxCount": 1, "probability": 100 }
{ "name": "Zombie", "maxCount": 2, "probability": 100 }
]
},
{
"type": "Spawner",
"guid": "492af6a5-b034-4a8b-aa7b-923b906321e2",
"name": "Spawner (309)",
"location": [133, 114, -28],
"map": "Ilshenar",
"count": 1,
"minDelay": "00:05:00",
"maxDelay": "00:10:00",
"team": 0,
"homeRange": 10,
"walkingRange": 20,
"entries": [{ "name": "BloodElemental", "maxCount": 1, "probability": 100 }]
}
]

View file

@ -1,6 +1,6 @@
[
{
"type": "Spawner",
"$type": "Spawner",
"guid": "70e1e800-e6dc-4f5c-a610-269930e6c327",
"name": "Spawner (310)",
"location": [1981, 1059, -28],

View file

@ -1,6 +1,23 @@
[
{
"type": "Spawner",
"$type": "Spawner",
"guid": "418a0be2-0604-4ff8-b39d-1fb570fa7b7e",
"name": "Spawner (314)",
"location": [2177, 1210, -60],
"map": "Ilshenar",
"count": 2,
"minDelay": "00:05:00",
"maxDelay": "00:10:00",
"team": 0,
"homeRange": 10,
"walkingRange": 10,
"entries": [
{ "name": "LadyLissith", "maxCount": 2, "probability": 100 },
{ "name": "LadySabrix", "maxCount": 2, "probability": 100 }
]
},
{
"$type": "Spawner",
"guid": "46dc7fc7-d062-4be9-8134-f960352fbca3",
"name": "Spawner (314)",
"location": [2171, 1181, -44],
@ -20,73 +37,7 @@
]
},
{
"type": "Spawner",
"guid": "418a0be2-0604-4ff8-b39d-1fb570fa7b7e",
"name": "Spawner (314)",
"location": [2177, 1210, -60],
"map": "Ilshenar",
"count": 2,
"minDelay": "00:05:00",
"maxDelay": "00:10:00",
"team": 0,
"homeRange": 10,
"walkingRange": 10,
"entries": [
{ "name": "LadyLissith", "maxCount": 2, "probability": 100 },
{ "name": "LadySabrix", "maxCount": 2, "probability": 100 }
]
},
{
"type": "Spawner",
"guid": "cd6dab60-126f-4877-b9dd-f786cbfad49b",
"name": "Spawner (314)",
"location": [2138, 1212, -60],
"map": "Ilshenar",
"count": 8,
"minDelay": "00:05:00",
"maxDelay": "00:10:00",
"team": 0,
"homeRange": 20,
"walkingRange": 20,
"entries": [
{ "name": "Quagmire", "maxCount": 8, "probability": 100 },
{ "name": "WhippingVine", "maxCount": 8, "probability": 100 },
{ "name": "Irk", "maxCount": 8, "probability": 100 }
]
},
{
"type": "Spawner",
"guid": "5cb54b29-10c4-448f-a036-5ba23dcb350a",
"name": "Spawner (314)",
"location": [2224, 1218, 13],
"map": "Ilshenar",
"count": 1,
"minDelay": "00:05:00",
"maxDelay": "00:10:00",
"team": 0,
"homeRange": 8,
"walkingRange": 8,
"entries": [{ "name": "Swoop", "maxCount": 1, "probability": 100 }]
},
{
"type": "Spawner",
"guid": "aa8d4c55-b124-40ae-a0db-31b873c7a549",
"name": "Spawner (314)",
"location": [2134, 1174, -46],
"map": "Ilshenar",
"count": 5,
"minDelay": "00:05:00",
"maxDelay": "00:10:00",
"team": 0,
"homeRange": 5,
"walkingRange": 5,
"entries": [
{ "name": "Gnaw", "maxCount": 5, "probability": 100 },
{ "name": "DireWolf", "maxCount": 5, "probability": 100 }
]
},
{
"type": "Spawner",
"$type": "Spawner",
"guid": "5a47595b-b894-4d45-b35d-5714de0c8c51",
"name": "Spawner (314)",
"location": [2206, 1220, -8],
@ -97,59 +48,44 @@
"team": 0,
"homeRange": 20,
"walkingRange": 20,
"entries": [{ "name": "CuSidhe", "maxCount": 2, "probability": 100 }]
},
{
"type": "Spawner",
"guid": "fe2db064-ee88-4f04-8c49-b3d7a56fd9a2",
"name": "Spawner (314)",
"location": [2159, 1223, -60],
"map": "Ilshenar",
"count": 5,
"minDelay": "00:05:00",
"maxDelay": "00:10:00",
"team": 0,
"homeRange": 10,
"walkingRange": 10,
"entries": [
{ "name": "Corpser", "maxCount": 5, "probability": 100 },
{ "name": "Spite", "maxCount": 5, "probability": 100 },
{ "name": "Guile", "maxCount": 5, "probability": 100 }
{ "name": "CuSidhe", "maxCount": 2, "probability": 100 }
]
},
{
"type": "Spawner",
"guid": "eea8e238-78e9-4d84-a91b-95360f6f2fd6",
"$type": "Spawner",
"guid": "5cb54b29-10c4-448f-a036-5ba23dcb350a",
"name": "Spawner (314)",
"location": [2170, 1247, -60],
"location": [2224, 1218, 13],
"map": "Ilshenar",
"count": 4,
"minDelay": "00:05:00",
"maxDelay": "00:10:00",
"team": 0,
"homeRange": 10,
"walkingRange": 10,
"entries": [
{ "name": "Malefic", "maxCount": 4, "probability": 100 },
{ "name": "Virulent", "maxCount": 4, "probability": 100 }
]
},
{
"type": "Spawner",
"guid": "852fa8b9-931f-493c-9ee0-200a3f229110",
"name": "Spawner (314)",
"location": [2163, 1189, -48],
"map": "Ilshenar",
"count": 2,
"count": 1,
"minDelay": "00:05:00",
"maxDelay": "00:10:00",
"team": 0,
"homeRange": 8,
"walkingRange": 8,
"entries": [{ "name": "Changeling", "maxCount": 2, "probability": 100 }]
"entries": [
{ "name": "Swoop", "maxCount": 1, "probability": 100 }
]
},
{
"type": "Spawner",
"$type": "Spawner",
"guid": "68b1853c-f9bd-4bd1-852f-dcc9977a3049",
"name": "Spawner (314)",
"location": [2167, 1266, -60],
"map": "Ilshenar",
"count": 3,
"minDelay": "00:05:00",
"maxDelay": "00:10:00",
"team": 0,
"homeRange": 5,
"walkingRange": 5,
"entries": [
{ "name": "Malefic", "maxCount": 3, "probability": 100 }
]
},
{
"$type": "Spawner",
"guid": "6eee069d-8662-415f-bdcc-78975f1a54ae",
"name": "Spawner (314)",
"location": [2207, 1180, -36],
@ -169,7 +105,7 @@
]
},
{
"type": "Spawner",
"$type": "Spawner",
"guid": "76ef21c0-5e48-410e-84f5-57023fa7efb4",
"name": "Spawner (314)",
"location": [2137, 1216, -60],
@ -180,24 +116,12 @@
"team": 0,
"homeRange": 5,
"walkingRange": 5,
"entries": [{ "name": "Silk", "maxCount": 4, "probability": 100 }]
"entries": [
{ "name": "Silk", "maxCount": 4, "probability": 100 }
]
},
{
"type": "Spawner",
"guid": "b5b1118b-161e-43fb-80dd-f890dec2be53",
"name": "Spawner (314)",
"location": [2177, 1229, -60],
"map": "Ilshenar",
"count": 4,
"minDelay": "00:05:00",
"maxDelay": "00:10:00",
"team": 0,
"homeRange": 5,
"walkingRange": 5,
"entries": [{ "name": "GiantBlackWidow", "maxCount": 4, "probability": 100 }]
},
{
"type": "Spawner",
"$type": "Spawner",
"guid": "7e991368-4fad-4e2b-af3b-88d360b88627",
"name": "Spawner (314)",
"location": [2157, 1206, -60],
@ -208,20 +132,110 @@
"team": 0,
"homeRange": 5,
"walkingRange": 5,
"entries": [{ "name": "Virulent", "maxCount": 3, "probability": 100 }]
"entries": [
{ "name": "Virulent", "maxCount": 3, "probability": 100 }
]
},
{
"type": "Spawner",
"guid": "68b1853c-f9bd-4bd1-852f-dcc9977a3049",
"$type": "Spawner",
"guid": "852fa8b9-931f-493c-9ee0-200a3f229110",
"name": "Spawner (314)",
"location": [2167, 1266, -60],
"location": [2163, 1189, -48],
"map": "Ilshenar",
"count": 3,
"count": 2,
"minDelay": "00:05:00",
"maxDelay": "00:10:00",
"team": 0,
"homeRange": 8,
"walkingRange": 8,
"entries": [
{ "name": "Changeling", "maxCount": 2, "probability": 100 }
]
},
{
"$type": "Spawner",
"guid": "aa8d4c55-b124-40ae-a0db-31b873c7a549",
"name": "Spawner (314)",
"location": [2134, 1174, -46],
"map": "Ilshenar",
"count": 5,
"minDelay": "00:05:00",
"maxDelay": "00:10:00",
"team": 0,
"homeRange": 5,
"walkingRange": 5,
"entries": [{ "name": "Malefic", "maxCount": 3, "probability": 100 }]
"entries": [
{ "name": "Gnaw", "maxCount": 5, "probability": 100 },
{ "name": "DireWolf", "maxCount": 5, "probability": 100 }
]
},
{
"$type": "Spawner",
"guid": "b5b1118b-161e-43fb-80dd-f890dec2be53",
"name": "Spawner (314)",
"location": [2177, 1229, -60],
"map": "Ilshenar",
"count": 4,
"minDelay": "00:05:00",
"maxDelay": "00:10:00",
"team": 0,
"homeRange": 5,
"walkingRange": 5,
"entries": [
{ "name": "GiantBlackWidow", "maxCount": 4, "probability": 100 }
]
},
{
"$type": "Spawner",
"guid": "cd6dab60-126f-4877-b9dd-f786cbfad49b",
"name": "Spawner (314)",
"location": [2138, 1212, -60],
"map": "Ilshenar",
"count": 8,
"minDelay": "00:05:00",
"maxDelay": "00:10:00",
"team": 0,
"homeRange": 20,
"walkingRange": 20,
"entries": [
{ "name": "Quagmire", "maxCount": 8, "probability": 100 },
{ "name": "WhippingVine", "maxCount": 8, "probability": 100 },
{ "name": "Irk", "maxCount": 8, "probability": 100 }
]
},
{
"$type": "Spawner",
"guid": "eea8e238-78e9-4d84-a91b-95360f6f2fd6",
"name": "Spawner (314)",
"location": [2170, 1247, -60],
"map": "Ilshenar",
"count": 4,
"minDelay": "00:05:00",
"maxDelay": "00:10:00",
"team": 0,
"homeRange": 10,
"walkingRange": 10,
"entries": [
{ "name": "Malefic", "maxCount": 4, "probability": 100 },
{ "name": "Virulent", "maxCount": 4, "probability": 100 }
]
},
{
"$type": "Spawner",
"guid": "fe2db064-ee88-4f04-8c49-b3d7a56fd9a2",
"name": "Spawner (314)",
"location": [2159, 1223, -60],
"map": "Ilshenar",
"count": 5,
"minDelay": "00:05:00",
"maxDelay": "00:10:00",
"team": 0,
"homeRange": 10,
"walkingRange": 10,
"entries": [
{ "name": "Corpser", "maxCount": 5, "probability": 100 },
{ "name": "Spite", "maxCount": 5, "probability": 100 },
{ "name": "Guile", "maxCount": 5, "probability": 100 }
]
}
]

View file

@ -1,140 +1,22 @@
[
{
"type": "Spawner",
"guid": "ae956108-5118-4793-94d7-ac885617030f",
"$type": "Spawner",
"guid": "0d68da82-d86a-4c40-8085-174a01c65f55",
"name": "Spawner (313)",
"location": [681, 1535, -28],
"map": "Ilshenar",
"count": 20,
"minDelay": "00:05:00",
"maxDelay": "00:10:00",
"team": 0,
"homeRange": 40,
"walkingRange": 60,
"entries": [{ "name": "Wisp", "maxCount": 20, "probability": 100 }]
},
{
"type": "Spawner",
"guid": "67d46705-d56a-4098-bafb-ede2658118a8",
"name": "Spawner (313)",
"location": [855, 1567, -28],
"map": "Ilshenar",
"count": 1,
"minDelay": "00:05:00",
"maxDelay": "00:10:00",
"team": 0,
"homeRange": 6,
"walkingRange": 6,
"entries": [{ "name": "SkeletalMage", "maxCount": 1, "probability": 100 }]
},
{
"type": "Spawner",
"guid": "2ef49b9b-97d6-431a-946d-ffd2f1615697",
"name": "Spawner (313)",
"location": [855, 1566, -28],
"map": "Ilshenar",
"count": 1,
"minDelay": "00:05:00",
"maxDelay": "00:10:00",
"team": 0,
"homeRange": 6,
"walkingRange": 10,
"entries": [{ "name": "Shade", "maxCount": 1, "probability": 100 }]
},
{
"type": "Spawner",
"guid": "a90852cd-b672-4c4f-83bb-99dcc861ead0",
"name": "Spawner (313)",
"location": [851, 1498, -28],
"map": "Ilshenar",
"count": 1,
"minDelay": "00:05:00",
"maxDelay": "00:10:00",
"team": 0,
"homeRange": 5,
"walkingRange": 20,
"entries": [{ "name": "RottingCorpse", "maxCount": 1, "probability": 100 }]
},
{
"type": "Spawner",
"guid": "6cf2cd79-9f0f-4f25-af2b-333a9317171f",
"name": "Spawner (313)",
"location": [827, 1479, -28],
"map": "Ilshenar",
"count": 6,
"minDelay": "00:05:00",
"maxDelay": "00:10:00",
"team": 0,
"homeRange": 10,
"walkingRange": 15,
"entries": [
{ "name": "Shade", "maxCount": 2, "probability": 100 },
{ "name": "BoneKnight", "maxCount": 2, "probability": 100 },
{ "name": "BoneMagi", "maxCount": 2, "probability": 100 }
]
},
{
"type": "Spawner",
"guid": "add779de-8361-4edb-b214-900500ca81f0",
"name": "Spawner (313)",
"location": [888, 1471, -28],
"map": "Ilshenar",
"count": 8,
"minDelay": "00:05:00",
"maxDelay": "00:10:00",
"team": 0,
"homeRange": 12,
"walkingRange": 20,
"entries": [
{ "name": "Shade", "maxCount": 3, "probability": 100 },
{ "name": "BoneKnight", "maxCount": 3, "probability": 100 },
{ "name": "BoneMagi", "maxCount": 2, "probability": 100 }
]
},
{
"type": "Spawner",
"guid": "f4f8aa84-982a-4bd5-a708-9724d8598dd7",
"name": "Spawner (313)",
"location": [901, 1507, 2],
"location": [775, 1479, -28],
"map": "Ilshenar",
"count": 2,
"minDelay": "00:05:00",
"maxDelay": "00:10:00",
"team": 0,
"homeRange": 10,
"walkingRange": 10,
"entries": [{ "name": "EvilMageLord", "maxCount": 2, "probability": 100 }]
"homeRange": 15,
"walkingRange": 15,
"entries": [
{ "name": "Balron", "maxCount": 2, "probability": 100 }
]
},
{
"type": "Spawner",
"guid": "79272788-c6c6-4216-84e0-985b942af71b",
"name": "Spawner (313)",
"location": [871, 1531, -28],
"map": "Ilshenar",
"count": 3,
"minDelay": "00:05:00",
"maxDelay": "00:10:00",
"team": 0,
"homeRange": 6,
"walkingRange": 10,
"entries": [{ "name": "EvilMage", "maxCount": 3, "probability": 100 }]
},
{
"type": "Spawner",
"guid": "14b1989d-ec45-42db-af1b-92f49023b981",
"name": "Spawner (313)",
"location": [891, 1537, -28],
"map": "Ilshenar",
"count": 1,
"minDelay": "00:05:00",
"maxDelay": "00:10:00",
"team": 0,
"homeRange": 6,
"walkingRange": 10,
"entries": [{ "name": "RottingCorpse", "maxCount": 1, "probability": 100 }]
},
{
"type": "Spawner",
"$type": "Spawner",
"guid": "0fe99fe0-a203-4563-994b-5e02d70f1b3f",
"name": "Spawner (313)",
"location": [888, 1568, -28],
@ -151,21 +33,71 @@
]
},
{
"type": "Spawner",
"guid": "be42e727-e04f-47a2-b07c-49046fdafcb9",
"$type": "Spawner",
"guid": "14b1989d-ec45-42db-af1b-92f49023b981",
"name": "Spawner (313)",
"location": [939, 1531, -28],
"location": [891, 1537, -28],
"map": "Ilshenar",
"count": 2,
"count": 1,
"minDelay": "00:05:00",
"maxDelay": "00:10:00",
"team": 0,
"homeRange": 5,
"homeRange": 6,
"walkingRange": 10,
"entries": [{ "name": "Imp", "maxCount": 2, "probability": 100 }]
"entries": [
{ "name": "RottingCorpse", "maxCount": 1, "probability": 100 }
]
},
{
"type": "Spawner",
"$type": "Spawner",
"guid": "2ef49b9b-97d6-431a-946d-ffd2f1615697",
"name": "Spawner (313)",
"location": [855, 1566, -28],
"map": "Ilshenar",
"count": 1,
"minDelay": "00:05:00",
"maxDelay": "00:10:00",
"team": 0,
"homeRange": 6,
"walkingRange": 10,
"entries": [
{ "name": "Shade", "maxCount": 1, "probability": 100 }
]
},
{
"$type": "Spawner",
"guid": "33185d9f-afa2-43d7-a7aa-7d221c1a309a",
"name": "Spawner (313)",
"location": [964, 1554, -28],
"map": "Ilshenar",
"count": 1,
"minDelay": "00:05:00",
"maxDelay": "00:10:00",
"team": 0,
"homeRange": 7,
"walkingRange": 10,
"entries": [
{ "name": "LichLord", "maxCount": 1, "probability": 100 }
]
},
{
"$type": "Spawner",
"guid": "4b9e3103-e83e-48a4-88b3-c72acbe5e173",
"name": "Spawner (313)",
"location": [936, 1559, -22],
"map": "Ilshenar",
"count": 1,
"minDelay": "00:05:00",
"maxDelay": "00:10:00",
"team": 0,
"homeRange": 10,
"walkingRange": 25,
"entries": [
{ "name": "Balron", "maxCount": 1, "probability": 100 }
]
},
{
"$type": "Spawner",
"guid": "516473d8-b5d1-4924-ad74-556649f98470",
"name": "Spawner (313)",
"location": [939, 1492, -28],
@ -176,24 +108,12 @@
"team": 0,
"homeRange": 5,
"walkingRange": 10,
"entries": [{ "name": "Imp", "maxCount": 1, "probability": 100 }]
"entries": [
{ "name": "Imp", "maxCount": 1, "probability": 100 }
]
},
{
"type": "Spawner",
"guid": "fc4fedc9-9489-4385-bf85-9ec569081f4a",
"name": "Spawner (313)",
"location": [961, 1502, -28],
"map": "Ilshenar",
"count": 2,
"minDelay": "00:05:00",
"maxDelay": "00:10:00",
"team": 0,
"homeRange": 6,
"walkingRange": 10,
"entries": [{ "name": "Ettin", "maxCount": 2, "probability": 100 }]
},
{
"type": "Spawner",
"$type": "Spawner",
"guid": "530dcd87-a132-4cdb-a9c9-0f78758e4ad0",
"name": "Spawner (313)",
"location": [962, 1480, -28],
@ -211,24 +131,76 @@
]
},
{
"type": "Spawner",
"guid": "b19163ea-9376-48e7-9bf7-b0a6a2e75d38",
"$type": "Spawner",
"guid": "5ce9d857-549d-4bf0-8e70-2dd6902c4ccf",
"name": "Spawner (313)",
"location": [1000, 1512, 0],
"location": [775, 1480, -28],
"map": "Ilshenar",
"count": 5,
"minDelay": "00:05:00",
"maxDelay": "00:10:00",
"team": 0,
"homeRange": 15,
"walkingRange": 15,
"homeRange": 40,
"walkingRange": 40,
"entries": [
{ "name": "Titan", "maxCount": 2, "probability": 100 },
{ "name": "Cyclops", "maxCount": 3, "probability": 100 }
{ "name": "Imp", "maxCount": 5, "probability": 100 }
]
},
{
"type": "Spawner",
"$type": "Spawner",
"guid": "649b7645-25f8-4c0c-ac2d-896e558b2767",
"name": "Spawner (313)",
"location": [776, 1480, -28],
"map": "Ilshenar",
"count": 15,
"minDelay": "00:05:00",
"maxDelay": "00:10:00",
"team": 0,
"homeRange": 3,
"walkingRange": 3,
"entries": [
{ "name": "TreasureChestLevel1", "maxCount": 9, "probability": 100 },
{ "name": "TreasureChestLevel2", "maxCount": 9, "probability": 100 },
{ "name": "TreasureChestLevel3", "maxCount": 6, "probability": 100 },
{ "name": "TreasureChestLevel4", "maxCount": 6, "probability": 100 }
]
},
{
"$type": "Spawner",
"guid": "67d46705-d56a-4098-bafb-ede2658118a8",
"name": "Spawner (313)",
"location": [855, 1567, -28],
"map": "Ilshenar",
"count": 1,
"minDelay": "00:05:00",
"maxDelay": "00:10:00",
"team": 0,
"homeRange": 6,
"walkingRange": 6,
"entries": [
{ "name": "SkeletalMage", "maxCount": 1, "probability": 100 }
]
},
{
"$type": "Spawner",
"guid": "6cf2cd79-9f0f-4f25-af2b-333a9317171f",
"name": "Spawner (313)",
"location": [827, 1479, -28],
"map": "Ilshenar",
"count": 6,
"minDelay": "00:05:00",
"maxDelay": "00:10:00",
"team": 0,
"homeRange": 10,
"walkingRange": 15,
"entries": [
{ "name": "Shade", "maxCount": 2, "probability": 100 },
{ "name": "BoneKnight", "maxCount": 2, "probability": 100 },
{ "name": "BoneMagi", "maxCount": 2, "probability": 100 }
]
},
{
"$type": "Spawner",
"guid": "74498688-9c91-4ea3-8abd-adcd2beb3b8d",
"name": "Spawner (313)",
"location": [959, 1530, -28],
@ -247,78 +219,134 @@
]
},
{
"type": "Spawner",
"guid": "4b9e3103-e83e-48a4-88b3-c72acbe5e173",
"$type": "Spawner",
"guid": "79272788-c6c6-4216-84e0-985b942af71b",
"name": "Spawner (313)",
"location": [936, 1559, -22],
"location": [871, 1531, -28],
"map": "Ilshenar",
"count": 1,
"count": 3,
"minDelay": "00:05:00",
"maxDelay": "00:10:00",
"team": 0,
"homeRange": 10,
"walkingRange": 25,
"entries": [{ "name": "Balron", "maxCount": 1, "probability": 100 }]
},
{
"type": "Spawner",
"guid": "33185d9f-afa2-43d7-a7aa-7d221c1a309a",
"name": "Spawner (313)",
"location": [964, 1554, -28],
"map": "Ilshenar",
"count": 1,
"minDelay": "00:05:00",
"maxDelay": "00:10:00",
"team": 0,
"homeRange": 7,
"homeRange": 6,
"walkingRange": 10,
"entries": [{ "name": "LichLord", "maxCount": 1, "probability": 100 }]
"entries": [
{ "name": "EvilMage", "maxCount": 3, "probability": 100 }
]
},
{
"type": "Spawner",
"guid": "0d68da82-d86a-4c40-8085-174a01c65f55",
"$type": "Spawner",
"guid": "a90852cd-b672-4c4f-83bb-99dcc861ead0",
"name": "Spawner (313)",
"location": [775, 1479, -28],
"location": [851, 1498, -28],
"map": "Ilshenar",
"count": 2,
"count": 1,
"minDelay": "00:05:00",
"maxDelay": "00:10:00",
"team": 0,
"homeRange": 15,
"walkingRange": 15,
"entries": [{ "name": "Balron", "maxCount": 2, "probability": 100 }]
"homeRange": 5,
"walkingRange": 20,
"entries": [
{ "name": "RottingCorpse", "maxCount": 1, "probability": 100 }
]
},
{
"type": "Spawner",
"guid": "5ce9d857-549d-4bf0-8e70-2dd6902c4ccf",
"$type": "Spawner",
"guid": "add779de-8361-4edb-b214-900500ca81f0",
"name": "Spawner (313)",
"location": [775, 1480, -28],
"location": [888, 1471, -28],
"map": "Ilshenar",
"count": 8,
"minDelay": "00:05:00",
"maxDelay": "00:10:00",
"team": 0,
"homeRange": 12,
"walkingRange": 20,
"entries": [
{ "name": "Shade", "maxCount": 3, "probability": 100 },
{ "name": "BoneKnight", "maxCount": 3, "probability": 100 },
{ "name": "BoneMagi", "maxCount": 2, "probability": 100 }
]
},
{
"$type": "Spawner",
"guid": "ae956108-5118-4793-94d7-ac885617030f",
"name": "Spawner (313)",
"location": [681, 1535, -28],
"map": "Ilshenar",
"count": 20,
"minDelay": "00:05:00",
"maxDelay": "00:10:00",
"team": 0,
"homeRange": 40,
"walkingRange": 60,
"entries": [
{ "name": "Wisp", "maxCount": 20, "probability": 100 }
]
},
{
"$type": "Spawner",
"guid": "b19163ea-9376-48e7-9bf7-b0a6a2e75d38",
"name": "Spawner (313)",
"location": [1000, 1512, 0],
"map": "Ilshenar",
"count": 5,
"minDelay": "00:05:00",
"maxDelay": "00:10:00",
"team": 0,
"homeRange": 40,
"walkingRange": 40,
"entries": [{ "name": "Imp", "maxCount": 5, "probability": 100 }]
"homeRange": 15,
"walkingRange": 15,
"entries": [
{ "name": "Titan", "maxCount": 2, "probability": 100 },
{ "name": "Cyclops", "maxCount": 3, "probability": 100 }
]
},
{
"type": "Spawner",
"guid": "649b7645-25f8-4c0c-ac2d-896e558b2767",
"$type": "Spawner",
"guid": "be42e727-e04f-47a2-b07c-49046fdafcb9",
"name": "Spawner (313)",
"location": [776, 1480, -28],
"location": [939, 1531, -28],
"map": "Ilshenar",
"count": 15,
"count": 2,
"minDelay": "00:05:00",
"maxDelay": "00:10:00",
"team": 0,
"homeRange": 3,
"walkingRange": 3,
"homeRange": 5,
"walkingRange": 10,
"entries": [
{ "name": "TreasureChestLevel1", "maxCount": 9, "probability": 100 },
{ "name": "TreasureChestLevel2", "maxCount": 9, "probability": 100 },
{ "name": "TreasureChestLevel3", "maxCount": 6, "probability": 100 },
{ "name": "TreasureChestLevel4", "maxCount": 6, "probability": 100 }
{ "name": "Imp", "maxCount": 2, "probability": 100 }
]
},
{
"$type": "Spawner",
"guid": "f4f8aa84-982a-4bd5-a708-9724d8598dd7",
"name": "Spawner (313)",
"location": [901, 1507, 2],
"map": "Ilshenar",
"count": 2,
"minDelay": "00:05:00",
"maxDelay": "00:10:00",
"team": 0,
"homeRange": 10,
"walkingRange": 10,
"entries": [
{ "name": "EvilMageLord", "maxCount": 2, "probability": 100 }
]
},
{
"$type": "Spawner",
"guid": "fc4fedc9-9489-4385-bf85-9ec569081f4a",
"name": "Spawner (313)",
"location": [961, 1502, -28],
"map": "Ilshenar",
"count": 2,
"minDelay": "00:05:00",
"maxDelay": "00:10:00",
"team": 0,
"homeRange": 6,
"walkingRange": 10,
"entries": [
{ "name": "Ettin", "maxCount": 2, "probability": 100 }
]
}
]

View file

@ -1,6 +1,6 @@
[
{
"type": "Spawner",
"$type": "Spawner",
"guid": "8c2124f4-e175-4198-8cf8-9cf64804fc22",
"name": "Spawner (408)",
"location": [98, 1656, 0],
@ -24,7 +24,7 @@
]
},
{
"type": "Spawner",
"$type": "Spawner",
"guid": "eaa69fa3-4076-4fc5-950a-fe175713bbd3",
"name": "Spawner (408)",
"location": [104, 1655, 0],
@ -35,6 +35,8 @@
"team": 0,
"homeRange": 15,
"walkingRange": 15,
"entries": [{ "name": "RedDeath", "maxCount": 1, "probability": 100 }]
"entries": [
{ "name": "RedDeath", "maxCount": 1, "probability": 100 }
]
}
]

View file

@ -1,6 +1,39 @@
[
{
"type": "Spawner",
"$type": "Spawner",
"guid": "114d80f2-07e6-49e3-9418-6e7c30291028",
"name": "Spawner (406)",
"location": [142, 1921, 0],
"map": "Malas",
"count": 4,
"minDelay": "00:05:00",
"maxDelay": "00:10:00",
"team": 0,
"homeRange": 5,
"walkingRange": 5,
"entries": [
{ "name": "tigersclawthief", "maxCount": 4, "probability": 100 }
]
},
{
"$type": "Spawner",
"guid": "15b169f9-fe9e-4ef7-973b-cb0619b9937d",
"name": "Spawner (406)",
"location": [82, 1875, 0],
"map": "Malas",
"count": 4,
"minDelay": "00:05:00",
"maxDelay": "00:10:00",
"team": 0,
"homeRange": 5,
"walkingRange": 5,
"entries": [
{ "name": "serpentsfangassassin", "maxCount": 4, "probability": 100 },
{ "name": "eliteninjawarrior", "maxCount": 4, "probability": 100 }
]
},
{
"$type": "Spawner",
"guid": "17abd9d2-3b7a-44ec-a724-57c202236788",
"name": "Spawner (406)",
"location": [179, 1869, 0],
@ -17,38 +50,7 @@
]
},
{
"type": "Spawner",
"guid": "856c181e-183a-4231-acc8-5146301c56c6",
"name": "Spawner (406)",
"location": [185, 1920, 0],
"map": "Malas",
"count": 2,
"minDelay": "00:05:00",
"maxDelay": "00:10:00",
"team": 0,
"homeRange": 7,
"walkingRange": 7,
"entries": [{ "name": "magedragonsflamemage", "maxCount": 2, "probability": 100 }]
},
{
"type": "Spawner",
"guid": "4854c18d-7bf2-4f8c-a69f-7679204b6d7d",
"name": "Spawner (406)",
"location": [155, 1869, 0],
"map": "Malas",
"count": 4,
"minDelay": "00:05:00",
"maxDelay": "00:10:00",
"team": 0,
"homeRange": 10,
"walkingRange": 10,
"entries": [
{ "name": "serpentsfangassassin", "maxCount": 4, "probability": 100 },
{ "name": "dragonsflamemage", "maxCount": 4, "probability": 100 }
]
},
{
"type": "Spawner",
"$type": "Spawner",
"guid": "28535caf-b979-4745-8a87-e62691d7cb11",
"name": "Spawner (406)",
"location": [145, 1898, 0],
@ -66,25 +68,7 @@
]
},
{
"type": "Spawner",
"guid": "b5fed5a1-35db-4f09-b4b6-8017146a8bf3",
"name": "Spawner (406)",
"location": [174, 1900, 0],
"map": "Malas",
"count": 5,
"minDelay": "00:05:00",
"maxDelay": "00:10:00",
"team": 0,
"homeRange": 20,
"walkingRange": 20,
"entries": [
{ "name": "serpentsfangassassin", "maxCount": 5, "probability": 100 },
{ "name": "dragonsflamemage", "maxCount": 5, "probability": 100 },
{ "name": "eliteninjawarrior", "maxCount": 5, "probability": 100 }
]
},
{
"type": "Spawner",
"$type": "Spawner",
"guid": "2d629e57-63db-4610-b6d2-cf3bfd34f0b0",
"name": "Spawner (406)",
"location": [181, 1883, 0],
@ -103,7 +87,7 @@
]
},
{
"type": "Spawner",
"$type": "Spawner",
"guid": "347e85d2-c752-4abf-86c7-bda2a2051033",
"name": "Spawner (406)",
"location": [183, 1961, 0],
@ -120,7 +104,92 @@
]
},
{
"type": "Spawner",
"$type": "Spawner",
"guid": "364cc553-16d0-428d-bf97-061c3ddfd120",
"name": "Spawner (406)",
"location": [170, 1974, 0],
"map": "Malas",
"count": 3,
"minDelay": "00:05:00",
"maxDelay": "00:10:00",
"team": 0,
"homeRange": 5,
"walkingRange": 5,
"entries": [
{ "name": "serpentsfangassassin", "maxCount": 3, "probability": 100 },
{ "name": "serpentsfanghighexecutioner", "maxCount": 3, "probability": 100 }
]
},
{
"$type": "Spawner",
"guid": "435ce67b-aad4-4ed0-b621-73ab4b6b0042",
"name": "Spawner (406)",
"location": [81, 1892, 0],
"map": "Malas",
"count": 4,
"minDelay": "00:05:00",
"maxDelay": "00:10:00",
"team": 0,
"homeRange": 5,
"walkingRange": 5,
"entries": [
{ "name": "eliteninjawarrior", "maxCount": 4, "probability": 100 },
{ "name": "serpentsfangassassin", "maxCount": 4, "probability": 100 },
{ "name": "dragonsflamemage", "maxCount": 4, "probability": 100 }
]
},
{
"$type": "Spawner",
"guid": "4854c18d-7bf2-4f8c-a69f-7679204b6d7d",
"name": "Spawner (406)",
"location": [155, 1869, 0],
"map": "Malas",
"count": 4,
"minDelay": "00:05:00",
"maxDelay": "00:10:00",
"team": 0,
"homeRange": 10,
"walkingRange": 10,
"entries": [
{ "name": "serpentsfangassassin", "maxCount": 4, "probability": 100 },
{ "name": "dragonsflamemage", "maxCount": 4, "probability": 100 }
]
},
{
"$type": "Spawner",
"guid": "50c87975-6021-46cb-bbe1-58c45ba776b7",
"name": "Spawner (406)",
"location": [77, 1883, 0],
"map": "Malas",
"count": 4,
"minDelay": "00:05:00",
"maxDelay": "00:10:00",
"team": 0,
"homeRange": 5,
"walkingRange": 5,
"entries": [
{ "name": "dragonsflamemage", "maxCount": 4, "probability": 100 },
{ "name": "eliteninjawarrior", "maxCount": 4, "probability": 100 }
]
},
{
"$type": "Spawner",
"guid": "856c181e-183a-4231-acc8-5146301c56c6",
"name": "Spawner (406)",
"location": [185, 1920, 0],
"map": "Malas",
"count": 2,
"minDelay": "00:05:00",
"maxDelay": "00:10:00",
"team": 0,
"homeRange": 7,
"walkingRange": 7,
"entries": [
{ "name": "magedragonsflamemage", "maxCount": 2, "probability": 100 }
]
},
{
"$type": "Spawner",
"guid": "9f02ef97-7699-4a65-8cd6-615aa50cc318",
"name": "Spawner (406)",
"location": [139, 1868, 0],
@ -140,24 +209,25 @@
]
},
{
"type": "Spawner",
"guid": "364cc553-16d0-428d-bf97-061c3ddfd120",
"$type": "Spawner",
"guid": "b5fed5a1-35db-4f09-b4b6-8017146a8bf3",
"name": "Spawner (406)",
"location": [170, 1974, 0],
"location": [174, 1900, 0],
"map": "Malas",
"count": 3,
"count": 5,
"minDelay": "00:05:00",
"maxDelay": "00:10:00",
"team": 0,
"homeRange": 5,
"walkingRange": 5,
"homeRange": 20,
"walkingRange": 20,
"entries": [
{ "name": "serpentsfangassassin", "maxCount": 3, "probability": 100 },
{ "name": "serpentsfanghighexecutioner", "maxCount": 3, "probability": 100 }
{ "name": "serpentsfangassassin", "maxCount": 5, "probability": 100 },
{ "name": "dragonsflamemage", "maxCount": 5, "probability": 100 },
{ "name": "eliteninjawarrior", "maxCount": 5, "probability": 100 }
]
},
{
"type": "Spawner",
"$type": "Spawner",
"guid": "dccae74d-bafa-40ff-9984-7a2a0af5d8de",
"name": "Spawner (406)",
"location": [140, 1974, 0],
@ -174,7 +244,7 @@
]
},
{
"type": "Spawner",
"$type": "Spawner",
"guid": "fb6ee275-c5c2-4780-9180-76e3a4ac1a97",
"name": "Spawner (406)",
"location": [134, 1947, 0],
@ -189,71 +259,5 @@
{ "name": "magedragonsflamemage", "maxCount": 2, "probability": 100 },
{ "name": "dragonsflamemage", "maxCount": 2, "probability": 100 }
]
},
{
"type": "Spawner",
"guid": "435ce67b-aad4-4ed0-b621-73ab4b6b0042",
"name": "Spawner (406)",
"location": [81, 1892, 0],
"map": "Malas",
"count": 4,
"minDelay": "00:05:00",
"maxDelay": "00:10:00",
"team": 0,
"homeRange": 5,
"walkingRange": 5,
"entries": [
{ "name": "eliteninjawarrior", "maxCount": 4, "probability": 100 },
{ "name": "serpentsfangassassin", "maxCount": 4, "probability": 100 },
{ "name": "dragonsflamemage", "maxCount": 4, "probability": 100 }
]
},
{
"type": "Spawner",
"guid": "114d80f2-07e6-49e3-9418-6e7c30291028",
"name": "Spawner (406)",
"location": [142, 1921, 0],
"map": "Malas",
"count": 4,
"minDelay": "00:05:00",
"maxDelay": "00:10:00",
"team": 0,
"homeRange": 5,
"walkingRange": 5,
"entries": [{ "name": "tigersclawthief", "maxCount": 4, "probability": 100 }]
},
{
"type": "Spawner",
"guid": "50c87975-6021-46cb-bbe1-58c45ba776b7",
"name": "Spawner (406)",
"location": [77, 1883, 0],
"map": "Malas",
"count": 4,
"minDelay": "00:05:00",
"maxDelay": "00:10:00",
"team": 0,
"homeRange": 5,
"walkingRange": 5,
"entries": [
{ "name": "dragonsflamemage", "maxCount": 4, "probability": 100 },
{ "name": "eliteninjawarrior", "maxCount": 4, "probability": 100 }
]
},
{
"type": "Spawner",
"guid": "15b169f9-fe9e-4ef7-973b-cb0619b9937d",
"name": "Spawner (406)",
"location": [82, 1875, 0],
"map": "Malas",
"count": 4,
"minDelay": "00:05:00",
"maxDelay": "00:10:00",
"team": 0,
"homeRange": 5,
"walkingRange": 5,
"entries": [
{ "name": "serpentsfangassassin", "maxCount": 4, "probability": 100 },
{ "name": "eliteninjawarrior", "maxCount": 4, "probability": 100 }
]
}
]

View file

@ -1,6 +1,22 @@
[
{
"type": "Spawner",
"$type": "Spawner",
"guid": "0b34e749-7a4e-4dc8-8b84-6d6444d80525",
"name": "Spawner (407)",
"location": [335, 1920, 0],
"map": "Malas",
"count": 1,
"minDelay": "00:05:00",
"maxDelay": "00:10:00",
"team": 0,
"homeRange": 5,
"walkingRange": 5,
"entries": [
{ "name": "Miasma", "maxCount": 1, "probability": 100 }
]
},
{
"$type": "Spawner",
"guid": "3ba58850-4bed-4935-8a37-e0c703f3b768",
"name": "Spawner (407)",
"location": [366, 1928, 0],
@ -19,86 +35,43 @@
]
},
{
"type": "Spawner",
"guid": "ebe544f9-9a54-4822-bb37-ee11ba1059db",
"$type": "Spawner",
"guid": "3d9f4f90-3f61-4dc1-9715-0d614c34c99c",
"name": "Spawner (407)",
"location": [384, 1932, 10],
"location": [382, 1913, 0],
"map": "Malas",
"count": 5,
"minDelay": "00:05:00",
"maxDelay": "00:10:00",
"team": 0,
"homeRange": 10,
"walkingRange": 10,
"entries": [
{ "name": "Minotaur", "maxCount": 3, "probability": 100 },
{ "name": "Snake", "maxCount": 2, "probability": 100 }
]
},
{
"type": "Spawner",
"guid": "0b34e749-7a4e-4dc8-8b84-6d6444d80525",
"name": "Spawner (407)",
"location": [335, 1920, 0],
"map": "Malas",
"count": 1,
"count": 0,
"minDelay": "00:05:00",
"maxDelay": "00:10:00",
"team": 0,
"homeRange": 5,
"walkingRange": 5,
"entries": [{ "name": "Miasma", "maxCount": 1, "probability": 100 }]
"entries": [
{ "name": "Meraktus", "maxCount": 0, "probability": 100 }
]
},
{
"type": "Spawner",
"guid": "eea9dc24-23e1-4198-8dbc-b22bddb5a71c",
"$type": "Spawner",
"guid": "46f54372-6b9c-4e33-9f35-99fc01204b59",
"name": "Spawner (407)",
"location": [1740, 990, -80],
"map": "Malas",
"count": 1,
"minDelay": "00:05:00",
"maxDelay": "00:10:00",
"team": 0,
"homeRange": 15,
"walkingRange": 15,
"entries": [{ "name": "WanderingHealer", "maxCount": 1, "probability": 100 }]
},
{
"type": "Spawner",
"guid": "b9da636f-3e98-4924-a7da-8ac3d0141e5f",
"name": "Spawner (407)",
"location": [1742, 990, -84],
"location": [427, 1923, 5],
"map": "Malas",
"count": 10,
"minDelay": "00:05:00",
"maxDelay": "00:10:00",
"team": 0,
"homeRange": 12,
"walkingRange": 12,
"homeRange": 15,
"walkingRange": 15,
"entries": [
{ "name": "Scorpion", "maxCount": 6, "probability": 100 },
{ "name": "Snake", "maxCount": 4, "probability": 100 }
{ "name": "MinotaurCaptain", "maxCount": 6, "probability": 100 },
{ "name": "Minotaur", "maxCount": 6, "probability": 100 },
{ "name": "MinotaurScout", "maxCount": 6, "probability": 100 },
{ "name": "Rat", "maxCount": 4, "probability": 100 },
{ "name": "TropicalBird", "maxCount": 4, "probability": 100 }
]
},
{
"type": "Spawner",
"guid": "b8196e8f-9f8f-4362-b188-6bb06dbfea9d",
"name": "Spawner (407)",
"location": [336, 1891, 0],
"map": "Malas",
"count": 5,
"minDelay": "00:05:00",
"maxDelay": "00:10:00",
"team": 0,
"homeRange": 12,
"walkingRange": 12,
"entries": [
{ "name": "Minotaur", "maxCount": 5, "probability": 100 },
{ "name": "MinotaurScout", "maxCount": 5, "probability": 100 }
]
},
{
"type": "Spawner",
"$type": "Spawner",
"guid": "4a9e68d9-d730-402c-96cc-979f6d08fcfa",
"name": "Spawner (407)",
"location": [357, 1898, 5],
@ -109,27 +82,83 @@
"team": 0,
"homeRange": 10,
"walkingRange": 10,
"entries": [{ "name": "Rend", "maxCount": 1, "probability": 100 }]
"entries": [
{ "name": "Rend", "maxCount": 1, "probability": 100 }
]
},
{
"type": "Spawner",
"guid": "f5972024-9852-4b8d-96a2-bf1afc05c588",
"$type": "Spawner",
"guid": "50f241e9-15cf-4561-9a1c-0fe8522d01cd",
"name": "Spawner (407)",
"location": [346, 1959, 0],
"location": [415, 1897, 5],
"map": "Malas",
"count": 4,
"count": 1,
"minDelay": "00:05:00",
"maxDelay": "00:10:00",
"team": 0,
"homeRange": 5,
"walkingRange": 5,
"entries": [
{ "name": "Pyre", "maxCount": 1, "probability": 100 }
]
},
{
"$type": "Spawner",
"guid": "613eb20c-bb8e-4f16-8561-bd839851b463",
"name": "Spawner (407)",
"location": [390, 1883, 0],
"map": "Malas",
"count": 8,
"minDelay": "00:05:00",
"maxDelay": "00:10:00",
"team": 0,
"homeRange": 10,
"walkingRange": 10,
"entries": [
{ "name": "Squirrel", "maxCount": 4, "probability": 100 },
{ "name": "Rat", "maxCount": 4, "probability": 100 }
{ "name": "Eagle", "maxCount": 8, "probability": 100 },
{ "name": "Cat", "maxCount": 8, "probability": 100 },
{ "name": "Bird", "maxCount": 8, "probability": 100 },
{ "name": "TropicalBird", "maxCount": 8, "probability": 100 }
]
},
{
"type": "Spawner",
"$type": "Spawner",
"guid": "a451b30c-88b8-411d-99b6-3a66cdfd0cd5",
"name": "Spawner (407)",
"location": [355, 1918, 0],
"map": "Malas",
"count": 6,
"minDelay": "00:05:00",
"maxDelay": "00:10:00",
"team": 0,
"homeRange": 20,
"walkingRange": 20,
"entries": [
{ "name": "Reptalon", "maxCount": 6, "probability": 100 },
{ "name": "Minotaur", "maxCount": 6, "probability": 100 }
]
},
{
"$type": "Spawner",
"guid": "aaca3ab0-5ea8-4af5-ac5f-c037d41b00fa",
"name": "Spawner (407)",
"location": [347, 1888, 5],
"map": "Malas",
"count": 4,
"minDelay": "00:05:00",
"maxDelay": "00:10:00",
"team": 0,
"homeRange": 20,
"walkingRange": 20,
"entries": [
{ "name": "flurry", "maxCount": 4, "probability": 100 },
{ "name": "grim", "maxCount": 4, "probability": 100 },
{ "name": "mistral", "maxCount": 4, "probability": 100 },
{ "name": "tempest", "maxCount": 4, "probability": 100 }
]
},
{
"$type": "Spawner",
"guid": "af7b61de-563e-4559-9905-d5113f0e7c02",
"name": "Spawner (407)",
"location": [392, 1917, 0],
@ -151,43 +180,41 @@
]
},
{
"type": "Spawner",
"guid": "613eb20c-bb8e-4f16-8561-bd839851b463",
"$type": "Spawner",
"guid": "b8196e8f-9f8f-4362-b188-6bb06dbfea9d",
"name": "Spawner (407)",
"location": [390, 1883, 0],
"location": [336, 1891, 0],
"map": "Malas",
"count": 8,
"count": 5,
"minDelay": "00:05:00",
"maxDelay": "00:10:00",
"team": 0,
"homeRange": 10,
"walkingRange": 10,
"homeRange": 12,
"walkingRange": 12,
"entries": [
{ "name": "Eagle", "maxCount": 8, "probability": 100 },
{ "name": "Cat", "maxCount": 8, "probability": 100 },
{ "name": "Bird", "maxCount": 8, "probability": 100 },
{ "name": "TropicalBird", "maxCount": 8, "probability": 100 }
{ "name": "Minotaur", "maxCount": 5, "probability": 100 },
{ "name": "MinotaurScout", "maxCount": 5, "probability": 100 }
]
},
{
"type": "Spawner",
"guid": "a451b30c-88b8-411d-99b6-3a66cdfd0cd5",
"$type": "Spawner",
"guid": "b9da636f-3e98-4924-a7da-8ac3d0141e5f",
"name": "Spawner (407)",
"location": [355, 1918, 0],
"location": [1742, 990, -84],
"map": "Malas",
"count": 6,
"count": 10,
"minDelay": "00:05:00",
"maxDelay": "00:10:00",
"team": 0,
"homeRange": 20,
"walkingRange": 20,
"homeRange": 12,
"walkingRange": 12,
"entries": [
{ "name": "Reptalon", "maxCount": 6, "probability": 100 },
{ "name": "Minotaur", "maxCount": 6, "probability": 100 }
{ "name": "Scorpion", "maxCount": 6, "probability": 100 },
{ "name": "Snake", "maxCount": 4, "probability": 100 }
]
},
{
"type": "Spawner",
"$type": "Spawner",
"guid": "c1e202ec-c6ba-4c76-8a26-29767ae27907",
"name": "Spawner (407)",
"location": [346, 1910, 0],
@ -204,27 +231,40 @@
]
},
{
"type": "Spawner",
"guid": "46f54372-6b9c-4e33-9f35-99fc01204b59",
"$type": "Spawner",
"guid": "ebe544f9-9a54-4822-bb37-ee11ba1059db",
"name": "Spawner (407)",
"location": [427, 1923, 5],
"location": [384, 1932, 10],
"map": "Malas",
"count": 10,
"count": 5,
"minDelay": "00:05:00",
"maxDelay": "00:10:00",
"team": 0,
"homeRange": 10,
"walkingRange": 10,
"entries": [
{ "name": "Minotaur", "maxCount": 3, "probability": 100 },
{ "name": "Snake", "maxCount": 2, "probability": 100 }
]
},
{
"$type": "Spawner",
"guid": "eea9dc24-23e1-4198-8dbc-b22bddb5a71c",
"name": "Spawner (407)",
"location": [1740, 990, -80],
"map": "Malas",
"count": 1,
"minDelay": "00:05:00",
"maxDelay": "00:10:00",
"team": 0,
"homeRange": 15,
"walkingRange": 15,
"entries": [
{ "name": "MinotaurCaptain", "maxCount": 6, "probability": 100 },
{ "name": "Minotaur", "maxCount": 6, "probability": 100 },
{ "name": "MinotaurScout", "maxCount": 6, "probability": 100 },
{ "name": "Rat", "maxCount": 4, "probability": 100 },
{ "name": "TropicalBird", "maxCount": 4, "probability": 100 }
{ "name": "WanderingHealer", "maxCount": 1, "probability": 100 }
]
},
{
"type": "Spawner",
"$type": "Spawner",
"guid": "ef581668-95b6-4e1b-b43f-06e805e2e646",
"name": "Spawner (407)",
"location": [348, 1948, 0],
@ -244,50 +284,20 @@
]
},
{
"type": "Spawner",
"guid": "50f241e9-15cf-4561-9a1c-0fe8522d01cd",
"$type": "Spawner",
"guid": "f5972024-9852-4b8d-96a2-bf1afc05c588",
"name": "Spawner (407)",
"location": [415, 1897, 5],
"map": "Malas",
"count": 1,
"minDelay": "00:05:00",
"maxDelay": "00:10:00",
"team": 0,
"homeRange": 5,
"walkingRange": 5,
"entries": [{ "name": "Pyre", "maxCount": 1, "probability": 100 }]
},
{
"type": "Spawner",
"guid": "aaca3ab0-5ea8-4af5-ac5f-c037d41b00fa",
"name": "Spawner (407)",
"location": [347, 1888, 5],
"location": [346, 1959, 0],
"map": "Malas",
"count": 4,
"minDelay": "00:05:00",
"maxDelay": "00:10:00",
"team": 0,
"homeRange": 20,
"walkingRange": 20,
"homeRange": 10,
"walkingRange": 10,
"entries": [
{ "name": "flurry", "maxCount": 4, "probability": 100 },
{ "name": "grim", "maxCount": 4, "probability": 100 },
{ "name": "mistral", "maxCount": 4, "probability": 100 },
{ "name": "tempest", "maxCount": 4, "probability": 100 }
{ "name": "Squirrel", "maxCount": 4, "probability": 100 },
{ "name": "Rat", "maxCount": 4, "probability": 100 }
]
},
{
"type": "Spawner",
"guid": "3d9f4f90-3f61-4dc1-9715-0d614c34c99c",
"name": "Spawner (407)",
"location": [382, 1913, 0],
"map": "Malas",
"count": 0,
"minDelay": "00:05:00",
"maxDelay": "00:10:00",
"team": 0,
"homeRange": 5,
"walkingRange": 5,
"entries": [{ "name": "Meraktus", "maxCount": 0, "probability": 100 }]
}
]

View file

@ -0,0 +1,349 @@
[
{
"$type": "Spawner",
"guid": "0c628980-8a6f-45e1-bdc8-ed7ea16765fd",
"name": "Spawner (402)",
"location": [2205, 145, -90],
"map": "Malas",
"count": 3,
"minDelay": "00:05:00",
"maxDelay": "00:10:00",
"team": 0,
"homeRange": 50,
"walkingRange": 50,
"entries": [
{ "name": "Unicorn", "maxCount": 3, "probability": 100 }
]
},
{
"$type": "Spawner",
"guid": "135a20a6-0885-4a61-9a55-a01593f3e874",
"name": "Spawner (402)",
"location": [1482, 347, -50],
"map": "Malas",
"count": 7,
"minDelay": "00:05:00",
"maxDelay": "00:10:00",
"team": 0,
"homeRange": 40,
"walkingRange": 60,
"entries": [
{ "name": "Bird", "maxCount": 2, "probability": 100 },
{ "name": "Walrus", "maxCount": 1, "probability": 100 },
{ "name": "SnowLeopard", "maxCount": 1, "probability": 100 },
{ "name": "WhiteWolf", "maxCount": 1, "probability": 100 },
{ "name": "IceSnake", "maxCount": 1, "probability": 100 },
{ "name": "PolarBear", "maxCount": 1, "probability": 100 }
]
},
{
"$type": "Spawner",
"guid": "2db6869c-b590-4f82-a5eb-edb7afdd18e4",
"name": "Spawner (402)",
"location": [1274, 300, -50],
"map": "Malas",
"count": 7,
"minDelay": "00:05:00",
"maxDelay": "00:10:00",
"team": 0,
"homeRange": 40,
"walkingRange": 40,
"entries": [
{ "name": "Bird", "maxCount": 2, "probability": 100 },
{ "name": "Walrus", "maxCount": 1, "probability": 100 },
{ "name": "SnowLeopard", "maxCount": 1, "probability": 100 },
{ "name": "WhiteWolf", "maxCount": 1, "probability": 100 },
{ "name": "IceSnake", "maxCount": 1, "probability": 100 },
{ "name": "PolarBear", "maxCount": 1, "probability": 100 }
]
},
{
"$type": "Spawner",
"guid": "2efda65d-ebc3-4251-b6a3-356d42cca581",
"name": "Spawner (402)",
"location": [2291, 295, -90],
"map": "Malas",
"count": 6,
"minDelay": "00:05:00",
"maxDelay": "00:10:00",
"team": 0,
"homeRange": 60,
"walkingRange": 60,
"entries": [
{ "name": "GoreFiend", "maxCount": 6, "probability": 100 }
]
},
{
"$type": "Spawner",
"guid": "41a269ac-0492-4a16-bbc3-449c7efa9ef9",
"name": "Spawner (402)",
"location": [2121, 597, -50],
"map": "Malas",
"count": 7,
"minDelay": "00:05:00",
"maxDelay": "00:10:00",
"team": 0,
"homeRange": 20,
"walkingRange": 20,
"entries": [
{ "name": "Bird", "maxCount": 2, "probability": 100 },
{ "name": "Walrus", "maxCount": 1, "probability": 100 },
{ "name": "SnowLeopard", "maxCount": 1, "probability": 100 },
{ "name": "WhiteWolf", "maxCount": 1, "probability": 100 },
{ "name": "IceSnake", "maxCount": 1, "probability": 100 },
{ "name": "PolarBear", "maxCount": 1, "probability": 100 }
]
},
{
"$type": "Spawner",
"guid": "63d80004-20f7-48d8-bcc8-639a8d83c099",
"name": "Spawner (402)",
"location": [766, 114, -90],
"map": "Malas",
"count": 7,
"minDelay": "00:05:00",
"maxDelay": "00:10:00",
"team": 0,
"homeRange": 20,
"walkingRange": 20,
"entries": [
{ "name": "Bird", "maxCount": 2, "probability": 100 },
{ "name": "Walrus", "maxCount": 1, "probability": 100 },
{ "name": "SnowLeopard", "maxCount": 1, "probability": 100 },
{ "name": "WhiteWolf", "maxCount": 1, "probability": 100 },
{ "name": "IceSnake", "maxCount": 1, "probability": 100 },
{ "name": "PolarBear", "maxCount": 1, "probability": 100 }
]
},
{
"$type": "Spawner",
"guid": "67b52f00-d808-4db4-91ca-9313330d0b40",
"name": "Spawner (402)",
"location": [1710, 403, -50],
"map": "Malas",
"count": 7,
"minDelay": "00:05:00",
"maxDelay": "00:10:00",
"team": 0,
"homeRange": 20,
"walkingRange": 40,
"entries": [
{ "name": "Bird", "maxCount": 2, "probability": 100 },
{ "name": "Walrus", "maxCount": 1, "probability": 100 },
{ "name": "SnowLeopard", "maxCount": 1, "probability": 100 },
{ "name": "WhiteWolf", "maxCount": 1, "probability": 100 },
{ "name": "IceSnake", "maxCount": 1, "probability": 100 },
{ "name": "PolarBear", "maxCount": 1, "probability": 100 }
]
},
{
"$type": "Spawner",
"guid": "6ef4d890-5038-4d39-977c-5eb15448543a",
"name": "Spawner (402)",
"location": [1659, 112, -80],
"map": "Malas",
"count": 7,
"minDelay": "00:05:00",
"maxDelay": "00:10:00",
"team": 0,
"homeRange": 20,
"walkingRange": 20,
"entries": [
{ "name": "Bird", "maxCount": 2, "probability": 100 },
{ "name": "Walrus", "maxCount": 1, "probability": 100 },
{ "name": "SnowLeopard", "maxCount": 1, "probability": 100 },
{ "name": "WhiteWolf", "maxCount": 1, "probability": 100 },
{ "name": "IceSnake", "maxCount": 1, "probability": 100 },
{ "name": "PolarBear", "maxCount": 1, "probability": 100 }
]
},
{
"$type": "Spawner",
"guid": "a1e3857a-952a-47c2-9311-d6bcc3806c45",
"name": "Spawner (402)",
"location": [1980, 660, -50],
"map": "Malas",
"count": 7,
"minDelay": "00:05:00",
"maxDelay": "00:10:00",
"team": 0,
"homeRange": 40,
"walkingRange": 40,
"entries": [
{ "name": "Bird", "maxCount": 2, "probability": 100 },
{ "name": "Walrus", "maxCount": 1, "probability": 100 },
{ "name": "SnowLeopard", "maxCount": 1, "probability": 100 },
{ "name": "WhiteWolf", "maxCount": 1, "probability": 100 },
{ "name": "IceSnake", "maxCount": 1, "probability": 100 },
{ "name": "PolarBear", "maxCount": 1, "probability": 100 }
]
},
{
"$type": "Spawner",
"guid": "a7be1139-619a-4d84-a98a-8ea04c550827",
"name": "Spawner (402)",
"location": [1869, 480, -50],
"map": "Malas",
"count": 7,
"minDelay": "00:05:00",
"maxDelay": "00:10:00",
"team": 0,
"homeRange": 40,
"walkingRange": 40,
"entries": [
{ "name": "Bird", "maxCount": 2, "probability": 100 },
{ "name": "Walrus", "maxCount": 1, "probability": 100 },
{ "name": "SnowLeopard", "maxCount": 1, "probability": 100 },
{ "name": "WhiteWolf", "maxCount": 1, "probability": 100 },
{ "name": "IceSnake", "maxCount": 1, "probability": 100 },
{ "name": "PolarBear", "maxCount": 1, "probability": 100 }
]
},
{
"$type": "Spawner",
"guid": "abaa8a2d-9997-4bff-b033-a9dfa78d46f9",
"name": "Spawner (402)",
"location": [2030, 500, -50],
"map": "Malas",
"count": 7,
"minDelay": "00:05:00",
"maxDelay": "00:10:00",
"team": 0,
"homeRange": 40,
"walkingRange": 70,
"entries": [
{ "name": "Bird", "maxCount": 2, "probability": 100 },
{ "name": "Walrus", "maxCount": 1, "probability": 100 },
{ "name": "SnowLeopard", "maxCount": 1, "probability": 100 },
{ "name": "WhiteWolf", "maxCount": 1, "probability": 100 },
{ "name": "IceSnake", "maxCount": 1, "probability": 100 },
{ "name": "PolarBear", "maxCount": 1, "probability": 100 }
]
},
{
"$type": "Spawner",
"guid": "b48ebb7a-8bfc-4c48-a0ab-f82ca41e38ac",
"name": "Spawner (402)",
"location": [1381, 340, -50],
"map": "Malas",
"count": 7,
"minDelay": "00:05:00",
"maxDelay": "00:10:00",
"team": 0,
"homeRange": 40,
"walkingRange": 40,
"entries": [
{ "name": "Bird", "maxCount": 2, "probability": 100 },
{ "name": "Walrus", "maxCount": 1, "probability": 100 },
{ "name": "SnowLeopard", "maxCount": 1, "probability": 100 },
{ "name": "WhiteWolf", "maxCount": 1, "probability": 100 },
{ "name": "IceSnake", "maxCount": 1, "probability": 100 },
{ "name": "PolarBear", "maxCount": 1, "probability": 100 }
]
},
{
"$type": "Spawner",
"guid": "b85a0fee-612e-4bd9-8489-28fbcdc60ef5",
"name": "Spawner (402)",
"location": [1137, 223, -50],
"map": "Malas",
"count": 7,
"minDelay": "00:05:00",
"maxDelay": "00:10:00",
"team": 0,
"homeRange": 20,
"walkingRange": 40,
"entries": [
{ "name": "Bird", "maxCount": 2, "probability": 100 },
{ "name": "Walrus", "maxCount": 1, "probability": 100 },
{ "name": "SnowLeopard", "maxCount": 1, "probability": 100 },
{ "name": "WhiteWolf", "maxCount": 1, "probability": 100 },
{ "name": "IceSnake", "maxCount": 1, "probability": 100 },
{ "name": "PolarBear", "maxCount": 1, "probability": 100 }
]
},
{
"$type": "Spawner",
"guid": "be2f76e8-a03b-42b9-b39c-c641f03ee091",
"name": "Spawner (402)",
"location": [1206, 191, -50],
"map": "Malas",
"count": 7,
"minDelay": "00:05:00",
"maxDelay": "00:10:00",
"team": 0,
"homeRange": 20,
"walkingRange": 40,
"entries": [
{ "name": "Bird", "maxCount": 2, "probability": 100 },
{ "name": "Walrus", "maxCount": 1, "probability": 100 },
{ "name": "SnowLeopard", "maxCount": 1, "probability": 100 },
{ "name": "WhiteWolf", "maxCount": 1, "probability": 100 },
{ "name": "IceSnake", "maxCount": 1, "probability": 100 },
{ "name": "PolarBear", "maxCount": 1, "probability": 100 }
]
},
{
"$type": "Spawner",
"guid": "dec6f03f-a79d-414a-8b8b-c46af723371a",
"name": "Spawner (402)",
"location": [1587, 386, -50],
"map": "Malas",
"count": 7,
"minDelay": "00:05:00",
"maxDelay": "00:10:00",
"team": 0,
"homeRange": 40,
"walkingRange": 40,
"entries": [
{ "name": "Bird", "maxCount": 2, "probability": 100 },
{ "name": "Walrus", "maxCount": 1, "probability": 100 },
{ "name": "SnowLeopard", "maxCount": 1, "probability": 100 },
{ "name": "WhiteWolf", "maxCount": 1, "probability": 100 },
{ "name": "IceSnake", "maxCount": 1, "probability": 100 },
{ "name": "PolarBear", "maxCount": 1, "probability": 100 }
]
},
{
"$type": "Spawner",
"guid": "e2af6c55-d320-4255-a85f-c694f8d9886b",
"name": "Spawner (402)",
"location": [2233, 555, -90],
"map": "Malas",
"count": 7,
"minDelay": "00:05:00",
"maxDelay": "00:10:00",
"team": 0,
"homeRange": 60,
"walkingRange": 60,
"entries": [
{ "name": "Bird", "maxCount": 2, "probability": 100 },
{ "name": "Walrus", "maxCount": 1, "probability": 100 },
{ "name": "SnowLeopard", "maxCount": 1, "probability": 100 },
{ "name": "WhiteWolf", "maxCount": 1, "probability": 100 },
{ "name": "IceSnake", "maxCount": 1, "probability": 100 },
{ "name": "PolarBear", "maxCount": 1, "probability": 100 }
]
},
{
"$type": "Spawner",
"guid": "e3f940e1-3225-416f-a9ba-717b28fb944a",
"name": "Spawner (402)",
"location": [1023, 192, -50],
"map": "Malas",
"count": 7,
"minDelay": "00:05:00",
"maxDelay": "00:10:00",
"team": 0,
"homeRange": 20,
"walkingRange": 20,
"entries": [
{ "name": "Bird", "maxCount": 2, "probability": 100 },
{ "name": "Walrus", "maxCount": 1, "probability": 100 },
{ "name": "SnowLeopard", "maxCount": 1, "probability": 100 },
{ "name": "WhiteWolf", "maxCount": 1, "probability": 100 },
{ "name": "IceSnake", "maxCount": 1, "probability": 100 },
{ "name": "PolarBear", "maxCount": 1, "probability": 100 }
]
}
]

View file

@ -1,34 +1,6 @@
[
{
"type": "Spawner",
"guid": "f4837b68-40f5-42db-b907-f4fe65248cf9",
"name": "Spawner (403)",
"location": [1343, 1248, -90],
"map": "Malas",
"count": 2,
"minDelay": "00:05:00",
"maxDelay": "00:15:00",
"team": 0,
"homeRange": 4,
"walkingRange": 4,
"entries": [{ "name": "Orc", "maxCount": 2, "probability": 100 }]
},
{
"type": "Spawner",
"guid": "9a42ced3-e2d2-4d65-bb78-3903a4092c9f",
"name": "Spawner (403)",
"location": [1343, 1196, -90],
"map": "Malas",
"count": 2,
"minDelay": "00:05:00",
"maxDelay": "00:15:00",
"team": 0,
"homeRange": 4,
"walkingRange": 4,
"entries": [{ "name": "Orc", "maxCount": 2, "probability": 100 }]
},
{
"type": "Spawner",
"$type": "Spawner",
"guid": "0cb005f6-2405-41d4-b9b6-9ad8c75a6851",
"name": "Spawner (403)",
"location": [1372, 1225, -90],
@ -48,7 +20,131 @@
]
},
{
"type": "Spawner",
"$type": "Spawner",
"guid": "2abcbbd3-3842-46f6-95f1-41b743bb7083",
"name": "Spawner (403)",
"location": [1364, 596, -86],
"map": "Malas",
"count": 4,
"minDelay": "00:05:00",
"maxDelay": "00:15:00",
"team": 0,
"homeRange": 4,
"walkingRange": 15,
"entries": [
{ "name": "Orc", "maxCount": 2, "probability": 100 },
{ "name": "OrcishLord", "maxCount": 1, "probability": 100 },
{ "name": "OrcishMage", "maxCount": 1, "probability": 100 }
]
},
{
"$type": "Spawner",
"guid": "415080ef-9b67-4529-8651-f82d4e0fba5c",
"name": "Spawner (403)",
"location": [1205, 700, -88],
"map": "Malas",
"count": 4,
"minDelay": "00:05:00",
"maxDelay": "00:15:00",
"team": 0,
"homeRange": 4,
"walkingRange": 20,
"entries": [
{ "name": "Orc", "maxCount": 2, "probability": 100 },
{ "name": "OrcishLord", "maxCount": 1, "probability": 100 },
{ "name": "OrcishMage", "maxCount": 1, "probability": 100 }
]
},
{
"$type": "Spawner",
"guid": "778c0ad2-e39e-4c61-9b1c-e45b6a9b554e",
"name": "Spawner (403)",
"location": [1257, 1326, -90],
"map": "Malas",
"count": 4,
"minDelay": "00:05:00",
"maxDelay": "00:15:00",
"team": 0,
"homeRange": 4,
"walkingRange": 15,
"entries": [
{ "name": "Orc", "maxCount": 2, "probability": 100 },
{ "name": "OrcishLord", "maxCount": 1, "probability": 100 },
{ "name": "OrcishMage", "maxCount": 1, "probability": 100 }
]
},
{
"$type": "Spawner",
"guid": "9a42ced3-e2d2-4d65-bb78-3903a4092c9f",
"name": "Spawner (403)",
"location": [1343, 1196, -90],
"map": "Malas",
"count": 2,
"minDelay": "00:05:00",
"maxDelay": "00:15:00",
"team": 0,
"homeRange": 4,
"walkingRange": 4,
"entries": [
{ "name": "Orc", "maxCount": 2, "probability": 100 }
]
},
{
"$type": "Spawner",
"guid": "b187d6bd-fe00-43c8-906d-f18ee330f432",
"name": "Spawner (403)",
"location": [911, 194, -79],
"map": "Malas",
"count": 4,
"minDelay": "00:05:00",
"maxDelay": "00:15:00",
"team": 0,
"homeRange": 4,
"walkingRange": 15,
"entries": [
{ "name": "Orc", "maxCount": 2, "probability": 100 },
{ "name": "OrcishLord", "maxCount": 1, "probability": 100 },
{ "name": "OrcishMage", "maxCount": 1, "probability": 100 }
]
},
{
"$type": "Spawner",
"guid": "c4a2ba68-4d62-480c-a9a9-b893f56a1a5d",
"name": "Spawner (403)",
"location": [1598, 1819, -110],
"map": "Malas",
"count": 4,
"minDelay": "00:05:00",
"maxDelay": "00:15:00",
"team": 0,
"homeRange": 4,
"walkingRange": 15,
"entries": [
{ "name": "Orc", "maxCount": 2, "probability": 100 },
{ "name": "OrcishLord", "maxCount": 1, "probability": 100 },
{ "name": "OrcishMage", "maxCount": 1, "probability": 100 }
]
},
{
"$type": "Spawner",
"guid": "c93264b1-19bd-47d9-ae8d-261e8d36fc30",
"name": "Spawner (403)",
"location": [1665, 356, -50],
"map": "Malas",
"count": 4,
"minDelay": "00:05:00",
"maxDelay": "00:15:00",
"team": 0,
"homeRange": 4,
"walkingRange": 25,
"entries": [
{ "name": "Orc", "maxCount": 2, "probability": 100 },
{ "name": "OrcishLord", "maxCount": 1, "probability": 100 },
{ "name": "OrcishMage", "maxCount": 1, "probability": 100 }
]
},
{
"$type": "Spawner",
"guid": "ea44d8fe-27b9-4650-9c22-fed0630e5cc3",
"name": "Spawner (403)",
"location": [1322, 1225, -90],
@ -68,111 +164,19 @@
]
},
{
"type": "Spawner",
"guid": "b187d6bd-fe00-43c8-906d-f18ee330f432",
"$type": "Spawner",
"guid": "f4837b68-40f5-42db-b907-f4fe65248cf9",
"name": "Spawner (403)",
"location": [911, 194, -79],
"location": [1343, 1248, -90],
"map": "Malas",
"count": 4,
"count": 2,
"minDelay": "00:05:00",
"maxDelay": "00:15:00",
"team": 0,
"homeRange": 4,
"walkingRange": 15,
"walkingRange": 4,
"entries": [
{ "name": "Orc", "maxCount": 2, "probability": 100 },
{ "name": "OrcishLord", "maxCount": 1, "probability": 100 },
{ "name": "OrcishMage", "maxCount": 1, "probability": 100 }
]
},
{
"type": "Spawner",
"guid": "c93264b1-19bd-47d9-ae8d-261e8d36fc30",
"name": "Spawner (403)",
"location": [1665, 356, -50],
"map": "Malas",
"count": 4,
"minDelay": "00:05:00",
"maxDelay": "00:15:00",
"team": 0,
"homeRange": 4,
"walkingRange": 25,
"entries": [
{ "name": "Orc", "maxCount": 2, "probability": 100 },
{ "name": "OrcishLord", "maxCount": 1, "probability": 100 },
{ "name": "OrcishMage", "maxCount": 1, "probability": 100 }
]
},
{
"type": "Spawner",
"guid": "2abcbbd3-3842-46f6-95f1-41b743bb7083",
"name": "Spawner (403)",
"location": [1364, 596, -86],
"map": "Malas",
"count": 4,
"minDelay": "00:05:00",
"maxDelay": "00:15:00",
"team": 0,
"homeRange": 4,
"walkingRange": 15,
"entries": [
{ "name": "Orc", "maxCount": 2, "probability": 100 },
{ "name": "OrcishLord", "maxCount": 1, "probability": 100 },
{ "name": "OrcishMage", "maxCount": 1, "probability": 100 }
]
},
{
"type": "Spawner",
"guid": "415080ef-9b67-4529-8651-f82d4e0fba5c",
"name": "Spawner (403)",
"location": [1205, 700, -88],
"map": "Malas",
"count": 4,
"minDelay": "00:05:00",
"maxDelay": "00:15:00",
"team": 0,
"homeRange": 4,
"walkingRange": 20,
"entries": [
{ "name": "Orc", "maxCount": 2, "probability": 100 },
{ "name": "OrcishLord", "maxCount": 1, "probability": 100 },
{ "name": "OrcishMage", "maxCount": 1, "probability": 100 }
]
},
{
"type": "Spawner",
"guid": "778c0ad2-e39e-4c61-9b1c-e45b6a9b554e",
"name": "Spawner (403)",
"location": [1257, 1326, -90],
"map": "Malas",
"count": 4,
"minDelay": "00:05:00",
"maxDelay": "00:15:00",
"team": 0,
"homeRange": 4,
"walkingRange": 15,
"entries": [
{ "name": "Orc", "maxCount": 2, "probability": 100 },
{ "name": "OrcishLord", "maxCount": 1, "probability": 100 },
{ "name": "OrcishMage", "maxCount": 1, "probability": 100 }
]
},
{
"type": "Spawner",
"guid": "c4a2ba68-4d62-480c-a9a9-b893f56a1a5d",
"name": "Spawner (403)",
"location": [1598, 1819, -110],
"map": "Malas",
"count": 4,
"minDelay": "00:05:00",
"maxDelay": "00:15:00",
"team": 0,
"homeRange": 4,
"walkingRange": 15,
"entries": [
{ "name": "Orc", "maxCount": 2, "probability": 100 },
{ "name": "OrcishLord", "maxCount": 1, "probability": 100 },
{ "name": "OrcishMage", "maxCount": 1, "probability": 100 }
{ "name": "Orc", "maxCount": 2, "probability": 100 }
]
}
]

View file

@ -0,0 +1,22 @@
[
{
"$type": "Spawner",
"guid": "6bbb82e4-b0d1-4a23-a9da-5d35911ce762",
"name": "Spawner (404)",
"location": [2231, 1605, -95],
"map": "Malas",
"count": 33,
"minDelay": "00:05:00",
"maxDelay": "00:10:00",
"team": 0,
"homeRange": 160,
"walkingRange": 160,
"entries": [
{ "name": "Mongbat", "maxCount": 7, "probability": 100 },
{ "name": "SandVortex", "maxCount": 6, "probability": 100 },
{ "name": "Scorpion", "maxCount": 7, "probability": 100 },
{ "name": "Snake", "maxCount": 6, "probability": 100 },
{ "name": "Spectre", "maxCount": 7, "probability": 100 }
]
}
]

View file

@ -0,0 +1,484 @@
[
{
"$type": "Spawner",
"guid": "0d9c6323-7da9-4e12-861a-76fd30b3e3dc",
"name": "Spawner (405)",
"location": [950, 520, -55],
"map": "Malas",
"count": 2,
"minDelay": "00:05:00",
"maxDelay": "00:10:00",
"team": 0,
"homeRange": 0,
"walkingRange": 5,
"entries": [
{ "name": "Healer", "maxCount": 1, "probability": 100 },
{ "name": "HealerGuildmaster", "maxCount": 1, "probability": 100 }
]
},
{
"$type": "Spawner",
"guid": "0db7d5e9-beaa-43a2-ac46-2802ac962fcb",
"name": "Spawner (405)",
"location": [2068, 1372, -75],
"map": "Malas",
"count": 2,
"minDelay": "00:05:00",
"maxDelay": "00:10:00",
"team": 0,
"homeRange": 0,
"walkingRange": 5,
"entries": [
{ "name": "Healer", "maxCount": 1, "probability": 100 },
{ "name": "HealerGuildmaster", "maxCount": 1, "probability": 100 }
]
},
{
"$type": "Spawner",
"guid": "3d301c77-1ade-4823-b2ff-f1d4e0ed2bf0",
"name": "Spawner (405)",
"location": [1029, 520, -55],
"map": "Malas",
"count": 2,
"minDelay": "00:05:00",
"maxDelay": "00:10:00",
"team": 0,
"homeRange": 0,
"walkingRange": 5,
"entries": [
{ "name": "Healer", "maxCount": 1, "probability": 100 },
{ "name": "HealerGuildmaster", "maxCount": 1, "probability": 100 }
]
},
{
"$type": "Spawner",
"guid": "3d69c950-70d4-48c5-8729-be8848330c1a",
"name": "Spawner (405)",
"location": [2045, 1397, -90],
"map": "Malas",
"count": 1,
"minDelay": "00:05:00",
"maxDelay": "00:10:00",
"team": 0,
"homeRange": 0,
"walkingRange": 5,
"entries": [
{ "name": "Jeweler", "maxCount": 1, "probability": 100 }
]
},
{
"$type": "Spawner",
"guid": "3dcbbc92-14ac-4798-8753-41aa3b6b2450",
"name": "Spawner (405)",
"location": [2023, 1379, -80],
"map": "Malas",
"count": 3,
"minDelay": "00:05:00",
"maxDelay": "00:10:00",
"team": 0,
"homeRange": 0,
"walkingRange": 5,
"entries": [
{ "name": "Mage", "maxCount": 1, "probability": 100 },
{ "name": "Alchemist", "maxCount": 1, "probability": 100 },
{ "name": "MageGuildmaster", "maxCount": 1, "probability": 100 }
]
},
{
"$type": "Spawner",
"guid": "54deea77-bc1e-446b-8274-ea23bddb369e",
"name": "Spawner (405)",
"location": [989, 527, -50],
"map": "Malas",
"count": 1,
"minDelay": "00:05:00",
"maxDelay": "00:10:00",
"team": 0,
"homeRange": 0,
"walkingRange": 5,
"entries": [
{ "name": "InnKeeper", "maxCount": 1, "probability": 100 }
]
},
{
"$type": "Spawner",
"guid": "55fab330-6d2a-4823-a666-3dfa33dceeb6",
"name": "Spawner (405)",
"location": [1977, 1365, -80],
"map": "Malas",
"count": 2,
"minDelay": "00:05:00",
"maxDelay": "00:10:00",
"team": 0,
"homeRange": 0,
"walkingRange": 5,
"entries": [
{ "name": "Blacksmith", "maxCount": 1, "probability": 100 },
{ "name": "BlacksmithGuildmaster", "maxCount": 1, "probability": 100 }
]
},
{
"$type": "Spawner",
"guid": "72a7f47d-d4cb-4530-9ba6-3878482279b2",
"name": "Spawner (405)",
"location": [2078, 1327, -80],
"map": "Malas",
"count": 2,
"minDelay": "00:05:00",
"maxDelay": "00:10:00",
"team": 0,
"homeRange": 0,
"walkingRange": 5,
"entries": [
{ "name": "Tanner", "maxCount": 1, "probability": 100 },
{ "name": "Furtrader", "maxCount": 1, "probability": 100 }
]
},
{
"$type": "Spawner",
"guid": "79aa68b8-d2bd-4a4d-ad84-422588aeaaff",
"name": "Spawner (405)",
"location": [1016, 514, -70],
"map": "Malas",
"count": 1,
"minDelay": "00:05:00",
"maxDelay": "00:10:00",
"team": 0,
"homeRange": 0,
"walkingRange": 5,
"entries": [
{ "name": "KeeperOfChivalry", "maxCount": 1, "probability": 100 }
]
},
{
"$type": "Spawner",
"guid": "7a10d380-53e3-48c9-9c5b-07a3c215032a",
"name": "Spawner (405)",
"location": [1027, 494, -70],
"map": "Malas",
"count": 1,
"minDelay": "00:05:00",
"maxDelay": "00:10:00",
"team": 0,
"homeRange": 0,
"walkingRange": 5,
"entries": [
{ "name": "AnimalTrainer", "maxCount": 1, "probability": 100 }
]
},
{
"$type": "Spawner",
"guid": "7bd799ab-c7fb-446e-b6cb-580d1fe2f7fb",
"name": "Spawner (405)",
"location": [976, 512, -50],
"map": "Malas",
"count": 2,
"minDelay": "00:05:00",
"maxDelay": "00:10:00",
"team": 0,
"homeRange": 0,
"walkingRange": 5,
"entries": [
{ "name": "Blacksmith", "maxCount": 1, "probability": 100 },
{ "name": "BlacksmithGuildmaster", "maxCount": 1, "probability": 100 }
]
},
{
"$type": "Spawner",
"guid": "81e4af4e-3310-4864-8517-a032d487716f",
"name": "Spawner (405)",
"location": [1992, 1315, -90],
"map": "Malas",
"count": 1,
"minDelay": "00:05:00",
"maxDelay": "00:10:00",
"team": 0,
"homeRange": 0,
"walkingRange": 5,
"entries": [
{ "name": "AnimalTrainer", "maxCount": 1, "probability": 100 }
]
},
{
"$type": "Spawner",
"guid": "8ed338dc-51c1-40b3-a91a-7ebd8d91131b",
"name": "Spawner (405)",
"location": [2060, 1283, -80],
"map": "Malas",
"count": 3,
"minDelay": "00:05:00",
"maxDelay": "00:10:00",
"team": 0,
"homeRange": 0,
"walkingRange": 5,
"entries": [
{ "name": "Carpenter", "maxCount": 1, "probability": 100 },
{ "name": "Architect", "maxCount": 1, "probability": 100 },
{ "name": "RealEstateBroker", "maxCount": 1, "probability": 100 }
]
},
{
"$type": "Spawner",
"guid": "9347985d-23a4-4044-9a5c-79939c9f7df0",
"name": "Spawner (405)",
"location": [2037, 1311, -85],
"map": "Malas",
"count": 1,
"minDelay": "00:05:00",
"maxDelay": "00:10:00",
"team": 0,
"homeRange": 0,
"walkingRange": 5,
"entries": [
{ "name": "InnKeeper", "maxCount": 1, "probability": 100 }
]
},
{
"$type": "Spawner",
"guid": "99cde945-5b9c-4cff-924f-a59142bcbdb9",
"name": "Spawner (405)",
"location": [989, 520, -50],
"map": "Malas",
"count": 2,
"minDelay": "00:05:00",
"maxDelay": "00:10:00",
"team": 0,
"homeRange": 0,
"walkingRange": 5,
"entries": [
{ "name": "Banker", "maxCount": 1, "probability": 100 },
{ "name": "Minter", "maxCount": 1, "probability": 100 }
]
},
{
"$type": "Spawner",
"guid": "9ad9f47f-17bc-4790-9a84-1e9aee66478f",
"name": "Spawner (405)",
"location": [2011, 1326, -80],
"map": "Malas",
"count": 2,
"minDelay": "00:05:00",
"maxDelay": "00:10:00",
"team": 0,
"homeRange": 0,
"walkingRange": 5,
"entries": [
{ "name": "Provisioner", "maxCount": 1, "probability": 100 },
{ "name": "Cobbler", "maxCount": 1, "probability": 100 }
]
},
{
"$type": "Spawner",
"guid": "a0c83d13-2354-494f-9535-d4e587cc7a6b",
"name": "Spawner (405)",
"location": [2025, 1387, -80],
"map": "Malas",
"count": 3,
"minDelay": "00:05:00",
"maxDelay": "00:10:00",
"team": 0,
"homeRange": 0,
"walkingRange": 5,
"entries": [
{ "name": "Herbalist", "maxCount": 1, "probability": 100 },
{ "name": "Alchemist", "maxCount": 1, "probability": 100 },
{ "name": "CustomHairstylist", "maxCount": 1, "probability": 100 }
]
},
{
"$type": "Spawner",
"guid": "a263ccfc-65e1-4181-93b6-6e1c7914f5b8",
"name": "Spawner (405)",
"location": [2048, 1343, -85],
"map": "Malas",
"count": 2,
"minDelay": "00:05:00",
"maxDelay": "00:10:00",
"team": 0,
"homeRange": 0,
"walkingRange": 5,
"entries": [
{ "name": "Banker", "maxCount": 1, "probability": 100 },
{ "name": "Minter", "maxCount": 1, "probability": 100 }
]
},
{
"$type": "Spawner",
"guid": "a4200c02-8035-48bc-b79d-8b45225ffd4f",
"name": "Spawner (405)",
"location": [1004, 527, -50],
"map": "Malas",
"count": 4,
"minDelay": "00:05:00",
"maxDelay": "00:10:00",
"team": 0,
"homeRange": 0,
"walkingRange": 5,
"entries": [
{ "name": "HolyMage", "maxCount": 1, "probability": 100 },
{ "name": "Herbalist", "maxCount": 1, "probability": 100 },
{ "name": "Alchemist", "maxCount": 1, "probability": 100 },
{ "name": "CustomHairstylist", "maxCount": 1, "probability": 100 }
]
},
{
"$type": "Spawner",
"guid": "b4f71d7e-8c76-47db-a1c1-b81c4484741b",
"name": "Spawner (405)",
"location": [2027, 1353, -90],
"map": "Malas",
"count": 4,
"minDelay": "00:05:00",
"maxDelay": "00:10:00",
"team": 0,
"homeRange": 0,
"walkingRange": 5,
"entries": [
{ "name": "TavernKeeper", "maxCount": 1, "probability": 100 },
{ "name": "Waiter", "maxCount": 1, "probability": 100 },
{ "name": "Cook", "maxCount": 1, "probability": 100 },
{ "name": "Barkeeper", "maxCount": 1, "probability": 100 }
]
},
{
"$type": "Spawner",
"guid": "b7226aea-9fd0-4079-8e21-70a7a443d0b2",
"name": "Spawner (405)",
"location": [1056, 1434, -85],
"map": "Malas",
"count": 1,
"minDelay": "00:05:00",
"maxDelay": "00:10:00",
"team": 0,
"homeRange": 0,
"walkingRange": 5,
"entries": [
{ "name": "InnKeeper", "maxCount": 1, "probability": 100 }
]
},
{
"$type": "Spawner",
"guid": "c1bda852-fee4-45ee-aa79-630980bc44e9",
"name": "Spawner (405)",
"location": [1003, 512, -50],
"map": "Malas",
"count": 5,
"minDelay": "00:05:00",
"maxDelay": "00:10:00",
"team": 0,
"homeRange": 0,
"walkingRange": 5,
"entries": [
{ "name": "Tinker", "maxCount": 1, "probability": 100 },
{ "name": "TinkerGuildmaster", "maxCount": 1, "probability": 100 },
{ "name": "Carpenter", "maxCount": 1, "probability": 100 },
{ "name": "Architect", "maxCount": 1, "probability": 100 },
{ "name": "RealEstateBroker", "maxCount": 1, "probability": 100 }
]
},
{
"$type": "Spawner",
"guid": "d6de9f30-1ee9-4914-853c-be075bd119ce",
"name": "Spawner (405)",
"location": [2083, 1322, -80],
"map": "Malas",
"count": 3,
"minDelay": "00:05:00",
"maxDelay": "00:10:00",
"team": 0,
"homeRange": 0,
"walkingRange": 5,
"entries": [
{ "name": "Tailor", "maxCount": 1, "probability": 100 },
{ "name": "Weaver", "maxCount": 1, "probability": 100 },
{ "name": "TailorGuildmaster", "maxCount": 1, "probability": 100 }
]
},
{
"$type": "Spawner",
"guid": "df531ac9-d2b0-48ae-bc19-dc043d6af3de",
"name": "Spawner (405)",
"location": [993, 511, -50],
"map": "Malas",
"count": 3,
"minDelay": "00:05:00",
"maxDelay": "00:10:00",
"team": 0,
"homeRange": 0,
"walkingRange": 5,
"entries": [
{ "name": "Provisioner", "maxCount": 1, "probability": 100 },
{ "name": "Cobbler", "maxCount": 1, "probability": 100 },
{ "name": "Jeweler", "maxCount": 1, "probability": 100 }
]
},
{
"$type": "Spawner",
"guid": "e08eb38c-0b25-4387-8999-f9d8a0a877aa",
"name": "Spawner (405)",
"location": [961, 517, -70],
"map": "Malas",
"count": 1,
"minDelay": "00:05:00",
"maxDelay": "00:10:00",
"team": 0,
"homeRange": 0,
"walkingRange": 5,
"entries": [
{ "name": "KeeperOfChivalry", "maxCount": 1, "probability": 100 }
]
},
{
"$type": "Spawner",
"guid": "eb50b13f-dd98-42c1-bfa0-2599bdc85de3",
"name": "Spawner (405)",
"location": [2066, 1282, -80],
"map": "Malas",
"count": 2,
"minDelay": "00:05:00",
"maxDelay": "00:10:00",
"team": 0,
"homeRange": 0,
"walkingRange": 5,
"entries": [
{ "name": "Tinker", "maxCount": 1, "probability": 100 },
{ "name": "TinkerGuildmaster", "maxCount": 1, "probability": 100 }
]
},
{
"$type": "Spawner",
"guid": "f62df843-74a7-4bb0-a101-53d1dc4cb806",
"name": "Spawner (405)",
"location": [976, 527, -50],
"map": "Malas",
"count": 5,
"minDelay": "00:05:00",
"maxDelay": "00:10:00",
"team": 0,
"homeRange": 0,
"walkingRange": 5,
"entries": [
{ "name": "Tailor", "maxCount": 1, "probability": 100 },
{ "name": "Weaver", "maxCount": 1, "probability": 100 },
{ "name": "TailorGuildmaster", "maxCount": 1, "probability": 100 },
{ "name": "Tanner", "maxCount": 1, "probability": 100 },
{ "name": "Furtrader", "maxCount": 1, "probability": 100 }
]
},
{
"$type": "Spawner",
"guid": "fa9dbd50-e0ca-4206-a197-0fe85a349900",
"name": "Spawner (405)",
"location": [2017, 1356, -90],
"map": "Malas",
"count": 1,
"minDelay": "00:05:00",
"maxDelay": "00:10:00",
"team": 0,
"homeRange": 0,
"walkingRange": 5,
"entries": [
{ "name": "Baker", "maxCount": 1, "probability": 100 }
]
}
]

View file

@ -1,56 +1,6 @@
[
{
"type": "Spawner",
"guid": "42b06210-4cd3-49d6-a5d1-74e5ab0cc77a",
"name": "Spawner (501)",
"location": [977, 218, 23],
"map": "Tokuno",
"count": 4,
"minDelay": "00:05:00",
"maxDelay": "00:10:00",
"team": 0,
"homeRange": 15,
"walkingRange": 15,
"entries": [{ "name": "FanDancer", "maxCount": 4, "probability": 100 }]
},
{
"type": "Spawner",
"guid": "47256f4f-b709-4c19-866b-2d7bdd896fd6",
"name": "Spawner (501)",
"location": [78, 546, -1],
"map": "Malas",
"count": 4,
"minDelay": "00:05:00",
"maxDelay": "00:10:00",
"team": 0,
"homeRange": 15,
"walkingRange": 15,
"entries": [
{ "name": "Daemon", "maxCount": 1, "probability": 100 },
{ "name": "HellHound", "maxCount": 2, "probability": 100 },
{ "name": "Succubus", "maxCount": 1, "probability": 100 }
]
},
{
"type": "Spawner",
"guid": "66d98c69-5788-471a-b93c-d82e252471b2",
"name": "Spawner (501)",
"location": [110, 540, -1],
"map": "Malas",
"count": 4,
"minDelay": "00:05:00",
"maxDelay": "00:10:00",
"team": 0,
"homeRange": 15,
"walkingRange": 15,
"entries": [
{ "name": "Daemon", "maxCount": 1, "probability": 100 },
{ "name": "HellHound", "maxCount": 2, "probability": 100 },
{ "name": "Succubus", "maxCount": 1, "probability": 100 }
]
},
{
"type": "Spawner",
"$type": "Spawner",
"guid": "005c0fd4-1893-489e-9a09-14c53b246567",
"name": "Spawner (501)",
"location": [96, 540, -1],
@ -67,169 +17,7 @@
]
},
{
"type": "Spawner",
"guid": "f093fd90-d7b6-48a7-b5f3-c2524f4ca2ca",
"name": "Spawner (501)",
"location": [100, 499, -1],
"map": "Malas",
"count": 2,
"minDelay": "00:05:00",
"maxDelay": "00:10:00",
"team": 0,
"homeRange": 20,
"walkingRange": 20,
"entries": [
{ "name": "Daemon", "maxCount": 1, "probability": 100 },
{ "name": "HeadlessOne", "maxCount": 1, "probability": 100 }
]
},
{
"type": "Spawner",
"guid": "24b2ba02-2289-4cb3-a957-e3ec7629c0b1",
"name": "Spawner (501)",
"location": [64, 488, -1],
"map": "Malas",
"count": 6,
"minDelay": "00:05:00",
"maxDelay": "00:10:00",
"team": 0,
"homeRange": 20,
"walkingRange": 20,
"entries": [
{ "name": "Daemon", "maxCount": 2, "probability": 100 },
{ "name": "HeadlessOne", "maxCount": 2, "probability": 100 },
{ "name": "HordeMinion", "maxCount": 2, "probability": 100 }
]
},
{
"type": "Spawner",
"guid": "f3743807-ec35-485a-82b5-f3cd3ba27bc0",
"name": "Spawner (501)",
"location": [70, 376, -1],
"map": "Malas",
"count": 10,
"minDelay": "00:05:00",
"maxDelay": "00:10:00",
"team": 0,
"homeRange": 25,
"walkingRange": 30,
"entries": [
{ "name": "FanDancer", "maxCount": 3, "probability": 100 },
{ "name": "HeadlessOne", "maxCount": 3, "probability": 100 },
{ "name": "HordeMinion", "maxCount": 3, "probability": 100 },
{ "name": "Daemon", "maxCount": 1, "probability": 100 }
]
},
{
"type": "Spawner",
"guid": "06d87817-7b93-45c4-9696-35c26ca4218d",
"name": "Spawner (501)",
"location": [120, 415, -1],
"map": "Malas",
"count": 4,
"minDelay": "00:05:00",
"maxDelay": "00:10:00",
"team": 0,
"homeRange": 10,
"walkingRange": 10,
"entries": [
{ "name": "HordeMinion", "maxCount": 2, "probability": 100 },
{ "name": "HeadlessOne", "maxCount": 2, "probability": 100 }
]
},
{
"type": "Spawner",
"guid": "bd077bc0-b36c-41b4-9e4c-e3f067d6846e",
"name": "Spawner (501)",
"location": [99, 387, -1],
"map": "Malas",
"count": 4,
"minDelay": "00:05:00",
"maxDelay": "00:10:00",
"team": 0,
"homeRange": 10,
"walkingRange": 10,
"entries": [{ "name": "HeadlessOne", "maxCount": 4, "probability": 100 }]
},
{
"type": "Spawner",
"guid": "4849d20d-77fc-4f0d-a513-a352a5849467",
"name": "Spawner (501)",
"location": [123, 387, -1],
"map": "Malas",
"count": 4,
"minDelay": "00:05:00",
"maxDelay": "00:10:00",
"team": 0,
"homeRange": 10,
"walkingRange": 10,
"entries": [{ "name": "HeadlessOne", "maxCount": 4, "probability": 100 }]
},
{
"type": "Spawner",
"guid": "183ea3fc-7ed8-4f54-bd05-513be50f48d6",
"name": "Spawner (501)",
"location": [79, 521, -1],
"map": "Malas",
"count": 3,
"minDelay": "00:05:00",
"maxDelay": "00:10:00",
"team": 0,
"homeRange": 5,
"walkingRange": 10,
"entries": [{ "name": "HeadlessOne", "maxCount": 3, "probability": 100 }]
},
{
"type": "Spawner",
"guid": "624d9055-be69-4b42-9aa8-f8876a6c3cbf",
"name": "Spawner (501)",
"location": [88, 332, -1],
"map": "Malas",
"count": 4,
"minDelay": "00:05:00",
"maxDelay": "00:10:00",
"team": 0,
"homeRange": 20,
"walkingRange": 20,
"entries": [{ "name": "FanDancer", "maxCount": 4, "probability": 100 }]
},
{
"type": "Spawner",
"guid": "cdd374da-1e88-4389-ac42-a6c63ba30d0b",
"name": "Spawner (501)",
"location": [97, 467, -1],
"map": "Malas",
"count": 4,
"minDelay": "00:05:00",
"maxDelay": "00:10:00",
"team": 0,
"homeRange": 20,
"walkingRange": 20,
"entries": [
{ "name": "HordeMinion", "maxCount": 2, "probability": 100 },
{ "name": "HeadlessOne", "maxCount": 1, "probability": 100 },
{ "name": "Succubus", "maxCount": 1, "probability": 100 }
]
},
{
"type": "Spawner",
"guid": "752b7dfa-4f8c-4bb6-875e-2f18385881b5",
"name": "Spawner (501)",
"location": [111, 352, -1],
"map": "Malas",
"count": 4,
"minDelay": "00:05:00",
"maxDelay": "00:10:00",
"team": 0,
"homeRange": 10,
"walkingRange": 10,
"entries": [
{ "name": "HordeMinion", "maxCount": 2, "probability": 100 },
{ "name": "HeadlessOne", "maxCount": 2, "probability": 100 }
]
},
{
"type": "Spawner",
"$type": "Spawner",
"guid": "016a082b-0696-40c3-815b-c3dbef373d50",
"name": "Spawner (501)",
"location": [173, 615, -1],
@ -248,7 +36,58 @@
]
},
{
"type": "Spawner",
"$type": "Spawner",
"guid": "06d87817-7b93-45c4-9696-35c26ca4218d",
"name": "Spawner (501)",
"location": [120, 415, -1],
"map": "Malas",
"count": 4,
"minDelay": "00:05:00",
"maxDelay": "00:10:00",
"team": 0,
"homeRange": 10,
"walkingRange": 10,
"entries": [
{ "name": "HordeMinion", "maxCount": 2, "probability": 100 },
{ "name": "HeadlessOne", "maxCount": 2, "probability": 100 }
]
},
{
"$type": "Spawner",
"guid": "12d3edf5-87f3-4fb4-9ca5-8462b94a81d8",
"name": "Spawner (501)",
"location": [134, 651, -1],
"map": "Malas",
"count": 4,
"minDelay": "00:05:00",
"maxDelay": "00:10:00",
"team": 0,
"homeRange": 5,
"walkingRange": 5,
"entries": [
{ "name": "FanDancer", "maxCount": 2, "probability": 100 },
{ "name": "HellHound", "maxCount": 1, "probability": 100 },
{ "name": "HellCat", "maxCount": 1, "probability": 100 }
]
},
{
"$type": "Spawner",
"guid": "183ea3fc-7ed8-4f54-bd05-513be50f48d6",
"name": "Spawner (501)",
"location": [79, 521, -1],
"map": "Malas",
"count": 3,
"minDelay": "00:05:00",
"maxDelay": "00:10:00",
"team": 0,
"homeRange": 5,
"walkingRange": 10,
"entries": [
{ "name": "HeadlessOne", "maxCount": 3, "probability": 100 }
]
},
{
"$type": "Spawner",
"guid": "1a384aa9-3d4c-4df7-82df-58e658f4bf5f",
"name": "Spawner (501)",
"location": [147, 646, -1],
@ -265,10 +104,10 @@
]
},
{
"type": "Spawner",
"guid": "9a661087-c1ec-4503-9451-8873a8c24bd9",
"$type": "Spawner",
"guid": "24b2ba02-2289-4cb3-a957-e3ec7629c0b1",
"name": "Spawner (501)",
"location": [175, 660, -1],
"location": [64, 488, -1],
"map": "Malas",
"count": 6,
"minDelay": "00:05:00",
@ -277,44 +116,47 @@
"homeRange": 20,
"walkingRange": 20,
"entries": [
{ "name": "HordeMinion", "maxCount": 3, "probability": 100 },
{ "name": "HellCat", "maxCount": 2, "probability": 100 },
{ "name": "Balron", "maxCount": 1, "probability": 100 }
{ "name": "Daemon", "maxCount": 2, "probability": 100 },
{ "name": "HeadlessOne", "maxCount": 2, "probability": 100 },
{ "name": "HordeMinion", "maxCount": 2, "probability": 100 }
]
},
{
"type": "Spawner",
"guid": "e3fd2f65-4f1e-4af5-8cab-26493d9c5350",
"$type": "Spawner",
"guid": "42b06210-4cd3-49d6-a5d1-74e5ab0cc77a",
"name": "Spawner (501)",
"location": [142, 692, -1],
"map": "Malas",
"count": 2,
"location": [977, 218, 23],
"map": "Tokuno",
"count": 4,
"minDelay": "00:05:00",
"maxDelay": "00:10:00",
"team": 0,
"homeRange": 20,
"walkingRange": 20,
"entries": [{ "name": "FanDancer", "maxCount": 2, "probability": 100 }]
"homeRange": 15,
"walkingRange": 15,
"entries": [
{ "name": "FanDancer", "maxCount": 4, "probability": 100 }
]
},
{
"type": "Spawner",
"guid": "a4392a5c-8729-40c2-bd31-4b145cba2f1a",
"$type": "Spawner",
"guid": "47256f4f-b709-4c19-866b-2d7bdd896fd6",
"name": "Spawner (501)",
"location": [106, 685, -1],
"location": [78, 546, -1],
"map": "Malas",
"count": 2,
"count": 4,
"minDelay": "00:05:00",
"maxDelay": "00:10:00",
"team": 0,
"homeRange": 20,
"walkingRange": 20,
"homeRange": 15,
"walkingRange": 15,
"entries": [
{ "name": "HellHound", "maxCount": 1, "probability": 100 },
{ "name": "Daemon", "maxCount": 1, "probability": 100 },
{ "name": "HellHound", "maxCount": 2, "probability": 100 },
{ "name": "Succubus", "maxCount": 1, "probability": 100 }
]
},
{
"type": "Spawner",
"$type": "Spawner",
"guid": "477ff85e-e7ac-4a85-a99f-75d1b3e9eb74",
"name": "Spawner (501)",
"location": [110, 649, -1],
@ -332,21 +174,191 @@
]
},
{
"type": "Spawner",
"guid": "12d3edf5-87f3-4fb4-9ca5-8462b94a81d8",
"$type": "Spawner",
"guid": "4849d20d-77fc-4f0d-a513-a352a5849467",
"name": "Spawner (501)",
"location": [134, 651, -1],
"location": [123, 387, -1],
"map": "Malas",
"count": 4,
"minDelay": "00:05:00",
"maxDelay": "00:10:00",
"team": 0,
"homeRange": 5,
"walkingRange": 5,
"homeRange": 10,
"walkingRange": 10,
"entries": [
{ "name": "HeadlessOne", "maxCount": 4, "probability": 100 }
]
},
{
"$type": "Spawner",
"guid": "624d9055-be69-4b42-9aa8-f8876a6c3cbf",
"name": "Spawner (501)",
"location": [88, 332, -1],
"map": "Malas",
"count": 4,
"minDelay": "00:05:00",
"maxDelay": "00:10:00",
"team": 0,
"homeRange": 20,
"walkingRange": 20,
"entries": [
{ "name": "FanDancer", "maxCount": 4, "probability": 100 }
]
},
{
"$type": "Spawner",
"guid": "66d98c69-5788-471a-b93c-d82e252471b2",
"name": "Spawner (501)",
"location": [110, 540, -1],
"map": "Malas",
"count": 4,
"minDelay": "00:05:00",
"maxDelay": "00:10:00",
"team": 0,
"homeRange": 15,
"walkingRange": 15,
"entries": [
{ "name": "Daemon", "maxCount": 1, "probability": 100 },
{ "name": "HellHound", "maxCount": 2, "probability": 100 },
{ "name": "Succubus", "maxCount": 1, "probability": 100 }
]
},
{
"$type": "Spawner",
"guid": "752b7dfa-4f8c-4bb6-875e-2f18385881b5",
"name": "Spawner (501)",
"location": [111, 352, -1],
"map": "Malas",
"count": 4,
"minDelay": "00:05:00",
"maxDelay": "00:10:00",
"team": 0,
"homeRange": 10,
"walkingRange": 10,
"entries": [
{ "name": "HordeMinion", "maxCount": 2, "probability": 100 },
{ "name": "HeadlessOne", "maxCount": 2, "probability": 100 }
]
},
{
"$type": "Spawner",
"guid": "9a661087-c1ec-4503-9451-8873a8c24bd9",
"name": "Spawner (501)",
"location": [175, 660, -1],
"map": "Malas",
"count": 6,
"minDelay": "00:05:00",
"maxDelay": "00:10:00",
"team": 0,
"homeRange": 20,
"walkingRange": 20,
"entries": [
{ "name": "HordeMinion", "maxCount": 3, "probability": 100 },
{ "name": "HellCat", "maxCount": 2, "probability": 100 },
{ "name": "Balron", "maxCount": 1, "probability": 100 }
]
},
{
"$type": "Spawner",
"guid": "a4392a5c-8729-40c2-bd31-4b145cba2f1a",
"name": "Spawner (501)",
"location": [106, 685, -1],
"map": "Malas",
"count": 2,
"minDelay": "00:05:00",
"maxDelay": "00:10:00",
"team": 0,
"homeRange": 20,
"walkingRange": 20,
"entries": [
{ "name": "FanDancer", "maxCount": 2, "probability": 100 },
{ "name": "HellHound", "maxCount": 1, "probability": 100 },
{ "name": "HellCat", "maxCount": 1, "probability": 100 }
{ "name": "Succubus", "maxCount": 1, "probability": 100 }
]
},
{
"$type": "Spawner",
"guid": "bd077bc0-b36c-41b4-9e4c-e3f067d6846e",
"name": "Spawner (501)",
"location": [99, 387, -1],
"map": "Malas",
"count": 4,
"minDelay": "00:05:00",
"maxDelay": "00:10:00",
"team": 0,
"homeRange": 10,
"walkingRange": 10,
"entries": [
{ "name": "HeadlessOne", "maxCount": 4, "probability": 100 }
]
},
{
"$type": "Spawner",
"guid": "cdd374da-1e88-4389-ac42-a6c63ba30d0b",
"name": "Spawner (501)",
"location": [97, 467, -1],
"map": "Malas",
"count": 4,
"minDelay": "00:05:00",
"maxDelay": "00:10:00",
"team": 0,
"homeRange": 20,
"walkingRange": 20,
"entries": [
{ "name": "HordeMinion", "maxCount": 2, "probability": 100 },
{ "name": "HeadlessOne", "maxCount": 1, "probability": 100 },
{ "name": "Succubus", "maxCount": 1, "probability": 100 }
]
},
{
"$type": "Spawner",
"guid": "e3fd2f65-4f1e-4af5-8cab-26493d9c5350",
"name": "Spawner (501)",
"location": [142, 692, -1],
"map": "Malas",
"count": 2,
"minDelay": "00:05:00",
"maxDelay": "00:10:00",
"team": 0,
"homeRange": 20,
"walkingRange": 20,
"entries": [
{ "name": "FanDancer", "maxCount": 2, "probability": 100 }
]
},
{
"$type": "Spawner",
"guid": "f093fd90-d7b6-48a7-b5f3-c2524f4ca2ca",
"name": "Spawner (501)",
"location": [100, 499, -1],
"map": "Malas",
"count": 2,
"minDelay": "00:05:00",
"maxDelay": "00:10:00",
"team": 0,
"homeRange": 20,
"walkingRange": 20,
"entries": [
{ "name": "Daemon", "maxCount": 1, "probability": 100 },
{ "name": "HeadlessOne", "maxCount": 1, "probability": 100 }
]
},
{
"$type": "Spawner",
"guid": "f3743807-ec35-485a-82b5-f3cd3ba27bc0",
"name": "Spawner (501)",
"location": [70, 376, -1],
"map": "Malas",
"count": 10,
"minDelay": "00:05:00",
"maxDelay": "00:10:00",
"team": 0,
"homeRange": 25,
"walkingRange": 30,
"entries": [
{ "name": "FanDancer", "maxCount": 3, "probability": 100 },
{ "name": "HeadlessOne", "maxCount": 3, "probability": 100 },
{ "name": "HordeMinion", "maxCount": 3, "probability": 100 },
{ "name": "Daemon", "maxCount": 1, "probability": 100 }
]
}
]

View file

@ -1,23 +1,9 @@
[
{
"type": "Spawner",
"guid": "660e512a-833e-42d2-b13d-8d9aee2b8e68",
"$type": "Spawner",
"guid": "03f1632a-1fc5-4132-92a7-288b4baf9ae9",
"name": "Spawner (503)",
"location": [667, 1343, 25],
"map": "Tokuno",
"count": 4,
"minDelay": "00:05:00",
"maxDelay": "00:10:00",
"team": 0,
"homeRange": 20,
"walkingRange": 20,
"entries": [{ "name": "Rabbit", "maxCount": 4, "probability": 100 }]
},
{
"type": "Spawner",
"guid": "7c22ce54-d85a-48c0-8388-25f26d1c7904",
"name": "Spawner (503)",
"location": [756, 1236, 25],
"location": [744, 1347, 25],
"map": "Tokuno",
"count": 4,
"minDelay": "00:05:00",
@ -26,70 +12,14 @@
"homeRange": 20,
"walkingRange": 20,
"entries": [
{ "name": "Sheep", "maxCount": 1, "probability": 100 },
{ "name": "Dog", "maxCount": 1, "probability": 100 },
{ "name": "Cat", "maxCount": 1, "probability": 100 },
{ "name": "Bird", "maxCount": 1, "probability": 100 },
{ "name": "Rat", "maxCount": 1, "probability": 100 }
]
},
{
"type": "Spawner",
"guid": "787cabab-33ea-4938-a32d-5f3443af22c4",
"name": "Spawner (503)",
"location": [712, 1238, 25],
"map": "Tokuno",
"count": 1,
"minDelay": "00:05:00",
"maxDelay": "00:10:00",
"team": 0,
"homeRange": 20,
"walkingRange": 20,
"entries": [{ "name": "Cat", "maxCount": 1, "probability": 100 }]
},
{
"type": "Spawner",
"guid": "8065872f-41df-44a9-b382-d5d16e9f31da",
"name": "Spawner (503)",
"location": [683, 1231, 25],
"map": "Tokuno",
"count": 1,
"minDelay": "00:05:00",
"maxDelay": "00:10:00",
"team": 0,
"homeRange": 20,
"walkingRange": 20,
"entries": [{ "name": "Rat", "maxCount": 1, "probability": 100 }]
},
{
"type": "Spawner",
"guid": "3eced8bf-e4d6-484b-ae5f-6519e827a787",
"name": "Spawner (503)",
"location": [679, 1224, 25],
"map": "Tokuno",
"count": 2,
"minDelay": "00:05:00",
"maxDelay": "00:10:00",
"team": 0,
"homeRange": 20,
"walkingRange": 20,
"entries": [{ "name": "Bird", "maxCount": 2, "probability": 100 }]
},
{
"type": "Spawner",
"guid": "dbffb36b-2e05-4968-96f6-c8f53ddeb50a",
"name": "Spawner (503)",
"location": [706, 1208, 25],
"map": "Tokuno",
"count": 2,
"minDelay": "00:05:00",
"maxDelay": "00:10:00",
"team": 0,
"homeRange": 20,
"walkingRange": 20,
"entries": [{ "name": "Bird", "maxCount": 2, "probability": 100 }]
},
{
"type": "Spawner",
"$type": "Spawner",
"guid": "217af4ba-df3f-4134-b360-8f8f921e8a01",
"name": "Spawner (503)",
"location": [721, 1274, 25],
@ -108,84 +38,7 @@
]
},
{
"type": "Spawner",
"guid": "9780bf47-bc15-4bdd-8dc6-0a5ec2abddea",
"name": "Spawner (503)",
"location": [740, 1284, 25],
"map": "Tokuno",
"count": 2,
"minDelay": "00:05:00",
"maxDelay": "00:10:00",
"team": 0,
"homeRange": 20,
"walkingRange": 20,
"entries": [{ "name": "Bird", "maxCount": 2, "probability": 100 }]
},
{
"type": "Spawner",
"guid": "ae3ea0e1-e260-4f24-a0f2-7a617cbe7bbe",
"name": "Spawner (503)",
"location": [713, 1351, 25],
"map": "Tokuno",
"count": 4,
"minDelay": "00:05:00",
"maxDelay": "00:10:00",
"team": 0,
"homeRange": 20,
"walkingRange": 20,
"entries": [
{ "name": "Crane", "maxCount": 2, "probability": 100 },
{ "name": "Cat", "maxCount": 2, "probability": 100 }
]
},
{
"type": "Spawner",
"guid": "d9dd4287-6d11-4e50-b4b1-834c431ee60b",
"name": "Spawner (503)",
"location": [710, 1350, 25],
"map": "Tokuno",
"count": 4,
"minDelay": "00:05:00",
"maxDelay": "00:10:00",
"team": 0,
"homeRange": 20,
"walkingRange": 20,
"entries": [{ "name": "Bird", "maxCount": 4, "probability": 100 }]
},
{
"type": "Spawner",
"guid": "cb8699ff-ca4c-44b1-9dd8-422e6e31fbbf",
"name": "Spawner (503)",
"location": [636, 1331, 25],
"map": "Tokuno",
"count": 4,
"minDelay": "00:05:00",
"maxDelay": "00:10:00",
"team": 0,
"homeRange": 20,
"walkingRange": 20,
"entries": [
{ "name": "Rabbit", "maxCount": 2, "probability": 100 },
{ "name": "Pig", "maxCount": 1, "probability": 100 },
{ "name": "Crane", "maxCount": 1, "probability": 100 }
]
},
{
"type": "Spawner",
"guid": "59135bf6-67a7-42db-ae76-2d9d53ee8d6b",
"name": "Spawner (503)",
"location": [628, 1310, 25],
"map": "Tokuno",
"count": 4,
"minDelay": "00:05:00",
"maxDelay": "00:10:00",
"team": 0,
"homeRange": 20,
"walkingRange": 20,
"entries": [{ "name": "Bird", "maxCount": 4, "probability": 100 }]
},
{
"type": "Spawner",
"$type": "Spawner",
"guid": "366108fc-b366-4620-b8bb-4d04014d40b7",
"name": "Spawner (503)",
"location": [625, 1317, 25],
@ -203,43 +56,56 @@
]
},
{
"type": "Spawner",
"guid": "63828858-74cb-4daa-a2e3-d79928cd539d",
"$type": "Spawner",
"guid": "3cc46920-b82d-4c3b-b622-611f7b416b66",
"name": "Spawner (503)",
"location": [630, 1225, 25],
"location": [803, 1338, 25],
"map": "Tokuno",
"count": 8,
"count": 4,
"minDelay": "00:05:00",
"maxDelay": "00:10:00",
"team": 0,
"homeRange": 20,
"walkingRange": 20,
"entries": [
{ "name": "Bird", "maxCount": 3, "probability": 100 },
{ "name": "Crane", "maxCount": 3, "probability": 100 },
{ "name": "Rabbit", "maxCount": 2, "probability": 100 }
{ "name": "Rat", "maxCount": 2, "probability": 100 },
{ "name": "Bird", "maxCount": 2, "probability": 100 }
]
},
{
"type": "Spawner",
"guid": "8ca277a3-a47d-4c33-8470-60d67d1fb2c7",
"$type": "Spawner",
"guid": "3eced8bf-e4d6-484b-ae5f-6519e827a787",
"name": "Spawner (503)",
"location": [628, 1272, 25],
"location": [679, 1224, 25],
"map": "Tokuno",
"count": 8,
"count": 2,
"minDelay": "00:05:00",
"maxDelay": "00:10:00",
"team": 0,
"homeRange": 20,
"walkingRange": 20,
"entries": [
{ "name": "Bird", "maxCount": 3, "probability": 100 },
{ "name": "Cat", "maxCount": 3, "probability": 100 },
{ "name": "Dog", "maxCount": 2, "probability": 100 }
{ "name": "Bird", "maxCount": 2, "probability": 100 }
]
},
{
"type": "Spawner",
"$type": "Spawner",
"guid": "40d3a681-f9b5-4866-b6c2-93aa8d211660",
"name": "Spawner (503)",
"location": [834, 1351, 25],
"map": "Tokuno",
"count": 4,
"minDelay": "00:05:00",
"maxDelay": "00:10:00",
"team": 0,
"homeRange": 20,
"walkingRange": 20,
"entries": [
{ "name": "Crane", "maxCount": 4, "probability": 100 }
]
},
{
"$type": "Spawner",
"guid": "40d40ab7-a774-4443-8225-790b81878d3a",
"name": "Spawner (503)",
"location": [768, 1192, 25],
@ -258,119 +124,7 @@
]
},
{
"type": "Spawner",
"guid": "f3688f98-8e8b-4100-8952-4f90020ab8ae",
"name": "Spawner (503)",
"location": [857, 1166, 25],
"map": "Tokuno",
"count": 4,
"minDelay": "00:05:00",
"maxDelay": "00:10:00",
"team": 0,
"homeRange": 20,
"walkingRange": 20,
"entries": [
{ "name": "Sheep", "maxCount": 1, "probability": 100 },
{ "name": "Rat", "maxCount": 1, "probability": 100 },
{ "name": "Bird", "maxCount": 1, "probability": 100 },
{ "name": "Dog", "maxCount": 1, "probability": 100 }
]
},
{
"type": "Spawner",
"guid": "c7bce2da-6a6c-4d0f-9c79-70c5a2895962",
"name": "Spawner (503)",
"location": [853, 1287, 25],
"map": "Tokuno",
"count": 4,
"minDelay": "00:05:00",
"maxDelay": "00:10:00",
"team": 0,
"homeRange": 20,
"walkingRange": 20,
"entries": [
{ "name": "Sheep", "maxCount": 1, "probability": 100 },
{ "name": "Rat", "maxCount": 1, "probability": 100 },
{ "name": "Bird", "maxCount": 1, "probability": 100 },
{ "name": "Dog", "maxCount": 1, "probability": 100 }
]
},
{
"type": "Spawner",
"guid": "e828c6c6-2867-4d26-a77a-ab581c4a7dcc",
"name": "Spawner (503)",
"location": [843, 1247, 25],
"map": "Tokuno",
"count": 4,
"minDelay": "00:05:00",
"maxDelay": "00:10:00",
"team": 0,
"homeRange": 20,
"walkingRange": 20,
"entries": [
{ "name": "Sheep", "maxCount": 2, "probability": 100 },
{ "name": "Pig", "maxCount": 2, "probability": 100 }
]
},
{
"type": "Spawner",
"guid": "962520ff-e7ec-4187-b385-5550af5428cb",
"name": "Spawner (503)",
"location": [844, 1198, 25],
"map": "Tokuno",
"count": 4,
"minDelay": "00:05:00",
"maxDelay": "00:10:00",
"team": 0,
"homeRange": 20,
"walkingRange": 20,
"entries": [
{ "name": "Dog", "maxCount": 1, "probability": 100 },
{ "name": "Rat", "maxCount": 1, "probability": 100 },
{ "name": "Rabbit", "maxCount": 1, "probability": 100 },
{ "name": "Horse", "maxCount": 1, "probability": 100 },
{ "name": "Pig", "maxCount": 1, "probability": 100 }
]
},
{
"type": "Spawner",
"guid": "4400a7f9-33b7-4f12-993e-6370e8422577",
"name": "Spawner (503)",
"location": [812, 1222, 25],
"map": "Tokuno",
"count": 4,
"minDelay": "00:05:00",
"maxDelay": "00:10:00",
"team": 0,
"homeRange": 20,
"walkingRange": 20,
"entries": [
{ "name": "Bird", "maxCount": 2, "probability": 100 },
{ "name": "Cat", "maxCount": 1, "probability": 100 },
{ "name": "JackRabbit", "maxCount": 1, "probability": 100 }
]
},
{
"type": "Spawner",
"guid": "9bf1fbe7-640a-44fe-89c5-b8771032d64b",
"name": "Spawner (503)",
"location": [807, 1287, 25],
"map": "Tokuno",
"count": 4,
"minDelay": "00:05:00",
"maxDelay": "00:10:00",
"team": 0,
"homeRange": 20,
"walkingRange": 20,
"entries": [
{ "name": "Bird", "maxCount": 1, "probability": 100 },
{ "name": "Dog", "maxCount": 1, "probability": 100 },
{ "name": "Cat", "maxCount": 1, "probability": 100 },
{ "name": "JackRabbit", "maxCount": 1, "probability": 100 }
]
},
{
"type": "Spawner",
"$type": "Spawner",
"guid": "42f392a2-faec-48fe-b8fa-44c394ce4f31",
"name": "Spawner (503)",
"location": [844, 1318, 25],
@ -390,24 +144,10 @@
]
},
{
"type": "Spawner",
"guid": "40d3a681-f9b5-4866-b6c2-93aa8d211660",
"$type": "Spawner",
"guid": "4400a7f9-33b7-4f12-993e-6370e8422577",
"name": "Spawner (503)",
"location": [834, 1351, 25],
"map": "Tokuno",
"count": 4,
"minDelay": "00:05:00",
"maxDelay": "00:10:00",
"team": 0,
"homeRange": 20,
"walkingRange": 20,
"entries": [{ "name": "Crane", "maxCount": 4, "probability": 100 }]
},
{
"type": "Spawner",
"guid": "3cc46920-b82d-4c3b-b622-611f7b416b66",
"name": "Spawner (503)",
"location": [803, 1338, 25],
"location": [812, 1222, 25],
"map": "Tokuno",
"count": 4,
"minDelay": "00:05:00",
@ -416,15 +156,16 @@
"homeRange": 20,
"walkingRange": 20,
"entries": [
{ "name": "Rat", "maxCount": 2, "probability": 100 },
{ "name": "Bird", "maxCount": 2, "probability": 100 }
{ "name": "Bird", "maxCount": 2, "probability": 100 },
{ "name": "Cat", "maxCount": 1, "probability": 100 },
{ "name": "JackRabbit", "maxCount": 1, "probability": 100 }
]
},
{
"type": "Spawner",
"guid": "df568bbb-9e8c-4594-a32b-85dd93540f10",
"$type": "Spawner",
"guid": "59135bf6-67a7-42db-ae76-2d9d53ee8d6b",
"name": "Spawner (503)",
"location": [770, 1344, 25],
"location": [628, 1310, 25],
"map": "Tokuno",
"count": 4,
"minDelay": "00:05:00",
@ -433,33 +174,11 @@
"homeRange": 20,
"walkingRange": 20,
"entries": [
{ "name": "Bird", "maxCount": 1, "probability": 100 },
{ "name": "Goat", "maxCount": 1, "probability": 100 },
{ "name": "Pig", "maxCount": 1, "probability": 100 },
{ "name": "Horse", "maxCount": 1, "probability": 100 }
{ "name": "Bird", "maxCount": 4, "probability": 100 }
]
},
{
"type": "Spawner",
"guid": "03f1632a-1fc5-4132-92a7-288b4baf9ae9",
"name": "Spawner (503)",
"location": [744, 1347, 25],
"map": "Tokuno",
"count": 4,
"minDelay": "00:05:00",
"maxDelay": "00:10:00",
"team": 0,
"homeRange": 20,
"walkingRange": 20,
"entries": [
{ "name": "Sheep", "maxCount": 1, "probability": 100 },
{ "name": "Dog", "maxCount": 1, "probability": 100 },
{ "name": "Bird", "maxCount": 1, "probability": 100 },
{ "name": "Rat", "maxCount": 1, "probability": 100 }
]
},
{
"type": "Spawner",
"$type": "Spawner",
"guid": "5a5e3558-be1a-49cc-a5cc-201c160e6c7b",
"name": "Spawner (503)",
"location": [711, 1322, 25],
@ -478,7 +197,312 @@
]
},
{
"type": "Spawner",
"$type": "Spawner",
"guid": "63828858-74cb-4daa-a2e3-d79928cd539d",
"name": "Spawner (503)",
"location": [630, 1225, 25],
"map": "Tokuno",
"count": 8,
"minDelay": "00:05:00",
"maxDelay": "00:10:00",
"team": 0,
"homeRange": 20,
"walkingRange": 20,
"entries": [
{ "name": "Bird", "maxCount": 3, "probability": 100 },
{ "name": "Crane", "maxCount": 3, "probability": 100 },
{ "name": "Rabbit", "maxCount": 2, "probability": 100 }
]
},
{
"$type": "Spawner",
"guid": "660e512a-833e-42d2-b13d-8d9aee2b8e68",
"name": "Spawner (503)",
"location": [667, 1343, 25],
"map": "Tokuno",
"count": 4,
"minDelay": "00:05:00",
"maxDelay": "00:10:00",
"team": 0,
"homeRange": 20,
"walkingRange": 20,
"entries": [
{ "name": "Rabbit", "maxCount": 4, "probability": 100 }
]
},
{
"$type": "Spawner",
"guid": "787cabab-33ea-4938-a32d-5f3443af22c4",
"name": "Spawner (503)",
"location": [712, 1238, 25],
"map": "Tokuno",
"count": 1,
"minDelay": "00:05:00",
"maxDelay": "00:10:00",
"team": 0,
"homeRange": 20,
"walkingRange": 20,
"entries": [
{ "name": "Cat", "maxCount": 1, "probability": 100 }
]
},
{
"$type": "Spawner",
"guid": "7c22ce54-d85a-48c0-8388-25f26d1c7904",
"name": "Spawner (503)",
"location": [756, 1236, 25],
"map": "Tokuno",
"count": 4,
"minDelay": "00:05:00",
"maxDelay": "00:10:00",
"team": 0,
"homeRange": 20,
"walkingRange": 20,
"entries": [
{ "name": "Dog", "maxCount": 1, "probability": 100 },
{ "name": "Cat", "maxCount": 1, "probability": 100 },
{ "name": "Bird", "maxCount": 1, "probability": 100 },
{ "name": "Rat", "maxCount": 1, "probability": 100 }
]
},
{
"$type": "Spawner",
"guid": "8065872f-41df-44a9-b382-d5d16e9f31da",
"name": "Spawner (503)",
"location": [683, 1231, 25],
"map": "Tokuno",
"count": 1,
"minDelay": "00:05:00",
"maxDelay": "00:10:00",
"team": 0,
"homeRange": 20,
"walkingRange": 20,
"entries": [
{ "name": "Rat", "maxCount": 1, "probability": 100 }
]
},
{
"$type": "Spawner",
"guid": "8ca277a3-a47d-4c33-8470-60d67d1fb2c7",
"name": "Spawner (503)",
"location": [628, 1272, 25],
"map": "Tokuno",
"count": 8,
"minDelay": "00:05:00",
"maxDelay": "00:10:00",
"team": 0,
"homeRange": 20,
"walkingRange": 20,
"entries": [
{ "name": "Bird", "maxCount": 3, "probability": 100 },
{ "name": "Cat", "maxCount": 3, "probability": 100 },
{ "name": "Dog", "maxCount": 2, "probability": 100 }
]
},
{
"$type": "Spawner",
"guid": "962520ff-e7ec-4187-b385-5550af5428cb",
"name": "Spawner (503)",
"location": [844, 1198, 25],
"map": "Tokuno",
"count": 4,
"minDelay": "00:05:00",
"maxDelay": "00:10:00",
"team": 0,
"homeRange": 20,
"walkingRange": 20,
"entries": [
{ "name": "Dog", "maxCount": 1, "probability": 100 },
{ "name": "Rat", "maxCount": 1, "probability": 100 },
{ "name": "Rabbit", "maxCount": 1, "probability": 100 },
{ "name": "Horse", "maxCount": 1, "probability": 100 },
{ "name": "Pig", "maxCount": 1, "probability": 100 }
]
},
{
"$type": "Spawner",
"guid": "9780bf47-bc15-4bdd-8dc6-0a5ec2abddea",
"name": "Spawner (503)",
"location": [740, 1284, 25],
"map": "Tokuno",
"count": 2,
"minDelay": "00:05:00",
"maxDelay": "00:10:00",
"team": 0,
"homeRange": 20,
"walkingRange": 20,
"entries": [
{ "name": "Bird", "maxCount": 2, "probability": 100 }
]
},
{
"$type": "Spawner",
"guid": "9bf1fbe7-640a-44fe-89c5-b8771032d64b",
"name": "Spawner (503)",
"location": [807, 1287, 25],
"map": "Tokuno",
"count": 4,
"minDelay": "00:05:00",
"maxDelay": "00:10:00",
"team": 0,
"homeRange": 20,
"walkingRange": 20,
"entries": [
{ "name": "Bird", "maxCount": 1, "probability": 100 },
{ "name": "Dog", "maxCount": 1, "probability": 100 },
{ "name": "Cat", "maxCount": 1, "probability": 100 },
{ "name": "JackRabbit", "maxCount": 1, "probability": 100 }
]
},
{
"$type": "Spawner",
"guid": "ae3ea0e1-e260-4f24-a0f2-7a617cbe7bbe",
"name": "Spawner (503)",
"location": [713, 1351, 25],
"map": "Tokuno",
"count": 4,
"minDelay": "00:05:00",
"maxDelay": "00:10:00",
"team": 0,
"spawnBounds": {
"start": { "x": 693, "y": 1331, "z": -128 },
"end": { "x": 733, "y": 1371, "z": 40 }
},
"walkingRange": 20,
"entries": [
{ "name": "Crane", "maxCount": 2, "probability": 100 },
{ "name": "Cat", "maxCount": 2, "probability": 100 }
]
},
{
"$type": "Spawner",
"guid": "c7bce2da-6a6c-4d0f-9c79-70c5a2895962",
"name": "Spawner (503)",
"location": [853, 1287, 25],
"map": "Tokuno",
"count": 4,
"minDelay": "00:05:00",
"maxDelay": "00:10:00",
"team": 0,
"homeRange": 20,
"walkingRange": 20,
"entries": [
{ "name": "Sheep", "maxCount": 1, "probability": 100 },
{ "name": "Rat", "maxCount": 1, "probability": 100 },
{ "name": "Bird", "maxCount": 1, "probability": 100 },
{ "name": "Dog", "maxCount": 1, "probability": 100 }
]
},
{
"$type": "Spawner",
"guid": "cb8699ff-ca4c-44b1-9dd8-422e6e31fbbf",
"name": "Spawner (503)",
"location": [636, 1331, 25],
"map": "Tokuno",
"count": 4,
"minDelay": "00:05:00",
"maxDelay": "00:10:00",
"team": 0,
"homeRange": 20,
"walkingRange": 20,
"entries": [
{ "name": "Rabbit", "maxCount": 2, "probability": 100 },
{ "name": "Pig", "maxCount": 1, "probability": 100 },
{ "name": "Crane", "maxCount": 1, "probability": 100 }
]
},
{
"$type": "Spawner",
"guid": "d9dd4287-6d11-4e50-b4b1-834c431ee60b",
"name": "Spawner (503)",
"location": [710, 1350, 25],
"map": "Tokuno",
"count": 4,
"minDelay": "00:05:00",
"maxDelay": "00:10:00",
"team": 0,
"spawnBounds": {
"start": { "x": 690, "y": 1330, "z": -128 },
"end": { "x": 730, "y": 1370, "z": 40 }
},
"walkingRange": 20,
"entries": [
{ "name": "Bird", "maxCount": 4, "probability": 100 }
]
},
{
"$type": "Spawner",
"guid": "dbffb36b-2e05-4968-96f6-c8f53ddeb50a",
"name": "Spawner (503)",
"location": [706, 1208, 25],
"map": "Tokuno",
"count": 2,
"minDelay": "00:05:00",
"maxDelay": "00:10:00",
"team": 0,
"homeRange": 20,
"walkingRange": 20,
"entries": [
{ "name": "Bird", "maxCount": 2, "probability": 100 }
]
},
{
"$type": "Spawner",
"guid": "df568bbb-9e8c-4594-a32b-85dd93540f10",
"name": "Spawner (503)",
"location": [770, 1344, 25],
"map": "Tokuno",
"count": 4,
"minDelay": "00:05:00",
"maxDelay": "00:10:00",
"team": 0,
"homeRange": 20,
"walkingRange": 20,
"entries": [
{ "name": "Bird", "maxCount": 1, "probability": 100 },
{ "name": "Goat", "maxCount": 1, "probability": 100 },
{ "name": "Pig", "maxCount": 1, "probability": 100 },
{ "name": "Horse", "maxCount": 1, "probability": 100 }
]
},
{
"$type": "Spawner",
"guid": "e828c6c6-2867-4d26-a77a-ab581c4a7dcc",
"name": "Spawner (503)",
"location": [843, 1247, 25],
"map": "Tokuno",
"count": 4,
"minDelay": "00:05:00",
"maxDelay": "00:10:00",
"team": 0,
"homeRange": 20,
"walkingRange": 20,
"entries": [
{ "name": "Sheep", "maxCount": 2, "probability": 100 },
{ "name": "Pig", "maxCount": 2, "probability": 100 }
]
},
{
"$type": "Spawner",
"guid": "f3688f98-8e8b-4100-8952-4f90020ab8ae",
"name": "Spawner (503)",
"location": [857, 1166, 25],
"map": "Tokuno",
"count": 4,
"minDelay": "00:05:00",
"maxDelay": "00:10:00",
"team": 0,
"homeRange": 20,
"walkingRange": 20,
"entries": [
{ "name": "Sheep", "maxCount": 1, "probability": 100 },
{ "name": "Rat", "maxCount": 1, "probability": 100 },
{ "name": "Bird", "maxCount": 1, "probability": 100 },
{ "name": "Dog", "maxCount": 1, "probability": 100 }
]
},
{
"$type": "Spawner",
"guid": "f4d88eea-fd00-4029-bf05-960a4a03a082",
"name": "Spawner (503)",
"location": [690, 1288, 25],

View file

@ -1,44 +1,6 @@
[
{
"type": "Spawner",
"guid": "9cbd85a7-68f8-401d-a8c3-b2a220b2e431",
"name": "Spawner (506)",
"location": [16, 56, -1],
"map": "Malas",
"count": 6,
"minDelay": "00:05:00",
"maxDelay": "00:10:00",
"team": 0,
"homeRange": 30,
"walkingRange": 30,
"entries": [
{ "name": "EarthElemental", "maxCount": 2, "probability": 100 },
{ "name": "DullCopperElemental", "maxCount": 2, "probability": 100 },
{ "name": "YomotsuWarrior", "maxCount": 1, "probability": 100 },
{ "name": "YomotsuPriest", "maxCount": 1, "probability": 100 }
]
},
{
"type": "Spawner",
"guid": "ee2e7f73-a95f-40e6-8493-80dbe4c2ea65",
"name": "Spawner (506)",
"location": [25, 19, -1],
"map": "Malas",
"count": 6,
"minDelay": "00:05:00",
"maxDelay": "00:10:00",
"team": 0,
"homeRange": 30,
"walkingRange": 30,
"entries": [
{ "name": "EarthElemental", "maxCount": 1, "probability": 100 },
{ "name": "DullCopperElemental", "maxCount": 1, "probability": 100 },
{ "name": "YomotsuWarrior", "maxCount": 2, "probability": 100 },
{ "name": "YomotsuPriest", "maxCount": 2, "probability": 100 }
]
},
{
"type": "Spawner",
"$type": "Spawner",
"guid": "115246e3-d444-4ad4-ad93-ba5f98ade944",
"name": "Spawner (506)",
"location": [71, 29, -1],
@ -57,83 +19,7 @@
]
},
{
"type": "Spawner",
"guid": "922f0ff6-6efc-4787-aab1-32b8c41831ab",
"name": "Spawner (506)",
"location": [54, 67, -1],
"map": "Malas",
"count": 6,
"minDelay": "00:05:00",
"maxDelay": "00:10:00",
"team": 0,
"homeRange": 30,
"walkingRange": 30,
"entries": [
{ "name": "EarthElemental", "maxCount": 1, "probability": 100 },
{ "name": "DullCopperElemental", "maxCount": 1, "probability": 100 },
{ "name": "YomotsuWarrior", "maxCount": 2, "probability": 100 },
{ "name": "YomotsuPriest", "maxCount": 2, "probability": 100 }
]
},
{
"type": "Spawner",
"guid": "ba3f54b0-b977-476e-be97-2ad5ba39dc19",
"name": "Spawner (506)",
"location": [83, 114, -1],
"map": "Malas",
"count": 6,
"minDelay": "00:05:00",
"maxDelay": "00:10:00",
"team": 0,
"homeRange": 30,
"walkingRange": 30,
"entries": [
{ "name": "EarthElemental", "maxCount": 2, "probability": 100 },
{ "name": "DullCopperElemental", "maxCount": 2, "probability": 100 },
{ "name": "YomotsuWarrior", "maxCount": 1, "probability": 100 },
{ "name": "YomotsuPriest", "maxCount": 1, "probability": 100 }
]
},
{
"type": "Spawner",
"guid": "a0525c92-565a-4b15-b6b0-61c6be263ae7",
"name": "Spawner (506)",
"location": [102, 84, -1],
"map": "Malas",
"count": 6,
"minDelay": "00:05:00",
"maxDelay": "00:10:00",
"team": 0,
"homeRange": 30,
"walkingRange": 30,
"entries": [
{ "name": "EarthElemental", "maxCount": 1, "probability": 100 },
{ "name": "DullCopperElemental", "maxCount": 1, "probability": 100 },
{ "name": "YomotsuWarrior", "maxCount": 2, "probability": 100 },
{ "name": "YomotsuPriest", "maxCount": 2, "probability": 100 }
]
},
{
"type": "Spawner",
"guid": "dc1fe934-3641-4ddd-8b6c-207ccf0861ef",
"name": "Spawner (506)",
"location": [116, 61, -1],
"map": "Malas",
"count": 6,
"minDelay": "00:05:00",
"maxDelay": "00:10:00",
"team": 0,
"homeRange": 30,
"walkingRange": 30,
"entries": [
{ "name": "EarthElemental", "maxCount": 2, "probability": 100 },
{ "name": "DullCopperElemental", "maxCount": 2, "probability": 100 },
{ "name": "YomotsuWarrior", "maxCount": 1, "probability": 100 },
{ "name": "YomotsuPriest", "maxCount": 1, "probability": 100 }
]
},
{
"type": "Spawner",
"$type": "Spawner",
"guid": "6561df12-d971-4504-9a84-fba65d6e3339",
"name": "Spawner (506)",
"location": [111, 20, -1],
@ -152,7 +38,7 @@
]
},
{
"type": "Spawner",
"$type": "Spawner",
"guid": "694ea01f-680b-4054-9f13-bf9e7317a6d8",
"name": "Spawner (506)",
"location": [107, 23, -1],
@ -163,6 +49,122 @@
"team": 0,
"homeRange": 5,
"walkingRange": 5,
"entries": [{ "name": "FireBeetle", "maxCount": 1, "probability": 100 }]
"entries": [
{ "name": "FireBeetle", "maxCount": 1, "probability": 100 }
]
},
{
"$type": "Spawner",
"guid": "922f0ff6-6efc-4787-aab1-32b8c41831ab",
"name": "Spawner (506)",
"location": [54, 67, -1],
"map": "Malas",
"count": 6,
"minDelay": "00:05:00",
"maxDelay": "00:10:00",
"team": 0,
"homeRange": 30,
"walkingRange": 30,
"entries": [
{ "name": "EarthElemental", "maxCount": 1, "probability": 100 },
{ "name": "DullCopperElemental", "maxCount": 1, "probability": 100 },
{ "name": "YomotsuWarrior", "maxCount": 2, "probability": 100 },
{ "name": "YomotsuPriest", "maxCount": 2, "probability": 100 }
]
},
{
"$type": "Spawner",
"guid": "9cbd85a7-68f8-401d-a8c3-b2a220b2e431",
"name": "Spawner (506)",
"location": [16, 56, -1],
"map": "Malas",
"count": 6,
"minDelay": "00:05:00",
"maxDelay": "00:10:00",
"team": 0,
"homeRange": 30,
"walkingRange": 30,
"entries": [
{ "name": "EarthElemental", "maxCount": 2, "probability": 100 },
{ "name": "DullCopperElemental", "maxCount": 2, "probability": 100 },
{ "name": "YomotsuWarrior", "maxCount": 1, "probability": 100 },
{ "name": "YomotsuPriest", "maxCount": 1, "probability": 100 }
]
},
{
"$type": "Spawner",
"guid": "a0525c92-565a-4b15-b6b0-61c6be263ae7",
"name": "Spawner (506)",
"location": [102, 84, -1],
"map": "Malas",
"count": 6,
"minDelay": "00:05:00",
"maxDelay": "00:10:00",
"team": 0,
"homeRange": 30,
"walkingRange": 30,
"entries": [
{ "name": "EarthElemental", "maxCount": 1, "probability": 100 },
{ "name": "DullCopperElemental", "maxCount": 1, "probability": 100 },
{ "name": "YomotsuWarrior", "maxCount": 2, "probability": 100 },
{ "name": "YomotsuPriest", "maxCount": 2, "probability": 100 }
]
},
{
"$type": "Spawner",
"guid": "ba3f54b0-b977-476e-be97-2ad5ba39dc19",
"name": "Spawner (506)",
"location": [83, 114, -1],
"map": "Malas",
"count": 6,
"minDelay": "00:05:00",
"maxDelay": "00:10:00",
"team": 0,
"homeRange": 30,
"walkingRange": 30,
"entries": [
{ "name": "EarthElemental", "maxCount": 2, "probability": 100 },
{ "name": "DullCopperElemental", "maxCount": 2, "probability": 100 },
{ "name": "YomotsuWarrior", "maxCount": 1, "probability": 100 },
{ "name": "YomotsuPriest", "maxCount": 1, "probability": 100 }
]
},
{
"$type": "Spawner",
"guid": "dc1fe934-3641-4ddd-8b6c-207ccf0861ef",
"name": "Spawner (506)",
"location": [116, 61, -1],
"map": "Malas",
"count": 6,
"minDelay": "00:05:00",
"maxDelay": "00:10:00",
"team": 0,
"homeRange": 30,
"walkingRange": 30,
"entries": [
{ "name": "EarthElemental", "maxCount": 2, "probability": 100 },
{ "name": "DullCopperElemental", "maxCount": 2, "probability": 100 },
{ "name": "YomotsuWarrior", "maxCount": 1, "probability": 100 },
{ "name": "YomotsuPriest", "maxCount": 1, "probability": 100 }
]
},
{
"$type": "Spawner",
"guid": "ee2e7f73-a95f-40e6-8493-80dbe4c2ea65",
"name": "Spawner (506)",
"location": [25, 19, -1],
"map": "Malas",
"count": 6,
"minDelay": "00:05:00",
"maxDelay": "00:10:00",
"team": 0,
"homeRange": 30,
"walkingRange": 30,
"entries": [
{ "name": "EarthElemental", "maxCount": 1, "probability": 100 },
{ "name": "DullCopperElemental", "maxCount": 1, "probability": 100 },
{ "name": "YomotsuWarrior", "maxCount": 2, "probability": 100 },
{ "name": "YomotsuPriest", "maxCount": 2, "probability": 100 }
]
}
]

View file

@ -1,67 +1,6 @@
[
{
"type": "Spawner",
"guid": "b04cfa39-6091-49da-bdba-1b737c266a0a",
"name": "Spawner (101)",
"location": [6507, 843, 0],
"map": "Trammel",
"count": 8,
"minDelay": "00:05:00",
"maxDelay": "00:10:00",
"team": 0,
"homeRange": 10,
"walkingRange": 10,
"entries": [
{ "name": "Tangle", "maxCount": 8, "probability": 100 },
{ "name": "Bogling", "maxCount": 8, "probability": 100 },
{ "name": "Alligator", "maxCount": 8, "probability": 100 },
{ "name": "Reaper", "maxCount": 8, "probability": 100 },
{ "name": "Corpser", "maxCount": 8, "probability": 100 },
{ "name": "Changeling", "maxCount": 8, "probability": 100 },
{ "name": "WhippingVine", "maxCount": 8, "probability": 100 }
]
},
{
"type": "Spawner",
"guid": "713c3e56-33d2-430c-b182-364da7607c83",
"name": "Spawner (101)",
"location": [6513, 871, 0],
"map": "Trammel",
"count": 17,
"minDelay": "00:05:00",
"maxDelay": "00:10:00",
"team": 0,
"homeRange": 35,
"walkingRange": 35,
"entries": [
{ "name": "GiantSerpent", "maxCount": 17, "probability": 100 },
{ "name": "GiantToad", "maxCount": 17, "probability": 100 },
{ "name": "Harpy", "maxCount": 17, "probability": 100 },
{ "name": "SilverSerpent", "maxCount": 17, "probability": 100 },
{ "name": "Snake", "maxCount": 17, "probability": 100 },
{ "name": "Bogling", "maxCount": 17, "probability": 100 }
]
},
{
"type": "Spawner",
"guid": "c6215ac8-c0e3-4c83-9394-984deb29c773",
"name": "Spawner (101)",
"location": [6585, 878, 0],
"map": "Trammel",
"count": 7,
"minDelay": "00:05:00",
"maxDelay": "00:10:00",
"team": 0,
"homeRange": 10,
"walkingRange": 10,
"entries": [
{ "name": "Harpy", "maxCount": 7, "probability": 100 },
{ "name": "Saliva", "maxCount": 7, "probability": 100 },
{ "name": "Changeling", "maxCount": 7, "probability": 100 }
]
},
{
"type": "Spawner",
"$type": "Spawner",
"guid": "025ff0bc-ff5b-4175-b963-1e393ee70543",
"name": "Spawner (101)",
"location": [6535, 848, 0],
@ -79,10 +18,10 @@
]
},
{
"type": "Spawner",
"guid": "e840761f-1c5b-45f2-ba91-2a273e9739c2",
"$type": "Spawner",
"guid": "04ebab40-c487-455f-8bea-211ae849bf2a",
"name": "Spawner (101)",
"location": [6540, 872, 0],
"location": [6518, 879, 0],
"map": "Trammel",
"count": 1,
"minDelay": "00:05:00",
@ -90,10 +29,28 @@
"team": 0,
"homeRange": 5,
"walkingRange": 5,
"entries": [{ "name": "InsaneDryad", "maxCount": 1, "probability": 100 }]
"entries": [
{ "name": "InsaneDryad", "maxCount": 1, "probability": 100 }
]
},
{
"type": "Spawner",
"$type": "Spawner",
"guid": "3dbad35b-a4c0-4d6d-b1d7-ad4cb792da01",
"name": "Spawner (101)",
"location": [6489, 897, 0],
"map": "Trammel",
"count": 1,
"minDelay": "00:05:00",
"maxDelay": "00:10:00",
"team": 0,
"homeRange": 5,
"walkingRange": 5,
"entries": [
{ "name": "InsaneDryad", "maxCount": 1, "probability": 100 }
]
},
{
"$type": "Spawner",
"guid": "43cff389-a041-49a6-89fb-cd5111466e70",
"name": "Spawner (101)",
"location": [6518, 870, 0],
@ -115,24 +72,71 @@
]
},
{
"type": "Spawner",
"guid": "3dbad35b-a4c0-4d6d-b1d7-ad4cb792da01",
"$type": "Spawner",
"guid": "713c3e56-33d2-430c-b182-364da7607c83",
"name": "Spawner (101)",
"location": [6489, 897, 0],
"location": [6513, 871, 0],
"map": "Trammel",
"count": 1,
"count": 17,
"minDelay": "00:05:00",
"maxDelay": "00:10:00",
"team": 0,
"homeRange": 5,
"walkingRange": 5,
"entries": [{ "name": "InsaneDryad", "maxCount": 1, "probability": 100 }]
"homeRange": 35,
"walkingRange": 35,
"entries": [
{ "name": "GiantSerpent", "maxCount": 17, "probability": 100 },
{ "name": "GiantToad", "maxCount": 17, "probability": 100 },
{ "name": "Harpy", "maxCount": 17, "probability": 100 },
{ "name": "SilverSerpent", "maxCount": 17, "probability": 100 },
{ "name": "Snake", "maxCount": 17, "probability": 100 },
{ "name": "Bogling", "maxCount": 17, "probability": 100 }
]
},
{
"type": "Spawner",
"guid": "04ebab40-c487-455f-8bea-211ae849bf2a",
"$type": "Spawner",
"guid": "b04cfa39-6091-49da-bdba-1b737c266a0a",
"name": "Spawner (101)",
"location": [6518, 879, 0],
"location": [6507, 843, 0],
"map": "Trammel",
"count": 8,
"minDelay": "00:05:00",
"maxDelay": "00:10:00",
"team": 0,
"homeRange": 10,
"walkingRange": 10,
"entries": [
{ "name": "Tangle", "maxCount": 8, "probability": 100 },
{ "name": "Bogling", "maxCount": 8, "probability": 100 },
{ "name": "Alligator", "maxCount": 8, "probability": 100 },
{ "name": "Reaper", "maxCount": 8, "probability": 100 },
{ "name": "Corpser", "maxCount": 8, "probability": 100 },
{ "name": "Changeling", "maxCount": 8, "probability": 100 },
{ "name": "WhippingVine", "maxCount": 8, "probability": 100 }
]
},
{
"$type": "Spawner",
"guid": "c6215ac8-c0e3-4c83-9394-984deb29c773",
"name": "Spawner (101)",
"location": [6585, 878, 0],
"map": "Trammel",
"count": 7,
"minDelay": "00:05:00",
"maxDelay": "00:10:00",
"team": 0,
"homeRange": 10,
"walkingRange": 10,
"entries": [
{ "name": "Harpy", "maxCount": 7, "probability": 100 },
{ "name": "Saliva", "maxCount": 7, "probability": 100 },
{ "name": "Changeling", "maxCount": 7, "probability": 100 }
]
},
{
"$type": "Spawner",
"guid": "e840761f-1c5b-45f2-ba91-2a273e9739c2",
"name": "Spawner (101)",
"location": [6540, 872, 0],
"map": "Trammel",
"count": 1,
"minDelay": "00:05:00",
@ -140,6 +144,8 @@
"team": 0,
"homeRange": 5,
"walkingRange": 5,
"entries": [{ "name": "InsaneDryad", "maxCount": 1, "probability": 100 }]
"entries": [
{ "name": "InsaneDryad", "maxCount": 1, "probability": 100 }
]
}
]

View file

@ -1,186 +1,6 @@
[
{
"type": "Spawner",
"guid": "308b2558-7402-46d1-a300-11eb22feaf0b",
"name": "Spawner (102)",
"location": [6049, 1447, 4],
"map": "Trammel",
"count": 3,
"minDelay": "00:00:00",
"maxDelay": "00:01:00",
"team": 0,
"homeRange": 2,
"walkingRange": 15,
"entries": [
{ "name": "SewerRat", "maxCount": 2, "probability": 100 },
{ "name": "Rat", "maxCount": 2, "probability": 100 },
{ "name": "BullFrog", "maxCount": 1, "probability": 100 }
]
},
{
"type": "Spawner",
"guid": "4502169a-d510-4779-a2d4-7ecff61c32a1",
"name": "Spawner (102)",
"location": [6091, 1491, 10],
"map": "Trammel",
"count": 3,
"minDelay": "00:00:00",
"maxDelay": "00:01:00",
"team": 0,
"homeRange": 2,
"walkingRange": 15,
"entries": [
{ "name": "SewerRat", "maxCount": 2, "probability": 100 },
{ "name": "Rat", "maxCount": 2, "probability": 100 },
{ "name": "BullFrog", "maxCount": 1, "probability": 100 }
]
},
{
"type": "Spawner",
"guid": "70842ac5-c2f9-490b-8fca-603396002f94",
"name": "Spawner (102)",
"location": [6048, 1487, 5],
"map": "Trammel",
"count": 3,
"minDelay": "00:00:00",
"maxDelay": "00:01:00",
"team": 0,
"homeRange": 2,
"walkingRange": 15,
"entries": [
{ "name": "SewerRat", "maxCount": 2, "probability": 100 },
{ "name": "Rat", "maxCount": 2, "probability": 100 },
{ "name": "BullFrog", "maxCount": 1, "probability": 100 }
]
},
{
"type": "Spawner",
"guid": "73483117-862e-472d-b856-13fbe0f2cb04",
"name": "Spawner (102)",
"location": [6065, 1459, 5],
"map": "Trammel",
"count": 3,
"minDelay": "00:00:00",
"maxDelay": "00:01:00",
"team": 0,
"homeRange": 2,
"walkingRange": 15,
"entries": [
{ "name": "SewerRat", "maxCount": 2, "probability": 100 },
{ "name": "Rat", "maxCount": 2, "probability": 100 },
{ "name": "BullFrog", "maxCount": 1, "probability": 100 }
]
},
{
"type": "Spawner",
"guid": "2059e438-3ea0-4553-9d80-2c4b3e179956",
"name": "Spawner (102)",
"location": [6092, 1448, 5],
"map": "Trammel",
"count": 3,
"minDelay": "00:00:00",
"maxDelay": "00:01:00",
"team": 0,
"homeRange": 2,
"walkingRange": 15,
"entries": [
{ "name": "SewerRat", "maxCount": 2, "probability": 100 },
{ "name": "Rat", "maxCount": 2, "probability": 100 },
{ "name": "BullFrog", "maxCount": 1, "probability": 100 }
]
},
{
"type": "Spawner",
"guid": "cb41f5b8-add1-42be-8bbe-48b76f9a4dd9",
"name": "Spawner (102)",
"location": [6107, 1452, 25],
"map": "Trammel",
"count": 3,
"minDelay": "00:00:00",
"maxDelay": "00:01:00",
"team": 0,
"homeRange": 2,
"walkingRange": 15,
"entries": [
{ "name": "SewerRat", "maxCount": 2, "probability": 100 },
{ "name": "Rat", "maxCount": 2, "probability": 100 },
{ "name": "BullFrog", "maxCount": 1, "probability": 100 }
]
},
{
"type": "Spawner",
"guid": "5e47e277-8d43-46c0-b43a-a15ce3623ceb",
"name": "Spawner (102)",
"location": [6112, 1462, 5],
"map": "Trammel",
"count": 3,
"minDelay": "00:00:00",
"maxDelay": "00:01:00",
"team": 0,
"homeRange": 2,
"walkingRange": 15,
"entries": [
{ "name": "SewerRat", "maxCount": 2, "probability": 100 },
{ "name": "Rat", "maxCount": 2, "probability": 100 },
{ "name": "BullFrog", "maxCount": 1, "probability": 100 }
]
},
{
"type": "Spawner",
"guid": "5873288f-b3d0-4ffc-b937-1a938b0ab0a3",
"name": "Spawner (102)",
"location": [6039, 1438, 4],
"map": "Trammel",
"count": 3,
"minDelay": "00:00:00",
"maxDelay": "00:01:00",
"team": 0,
"homeRange": 2,
"walkingRange": 15,
"entries": [
{ "name": "SewerRat", "maxCount": 2, "probability": 100 },
{ "name": "Rat", "maxCount": 2, "probability": 100 },
{ "name": "BullFrog", "maxCount": 1, "probability": 100 }
]
},
{
"type": "Spawner",
"guid": "31142c48-f400-4b04-a235-4df3d184432a",
"name": "Spawner (102)",
"location": [6115, 1491, -15],
"map": "Trammel",
"count": 3,
"minDelay": "00:00:00",
"maxDelay": "00:01:00",
"team": 0,
"homeRange": 2,
"walkingRange": 15,
"entries": [
{ "name": "SewerRat", "maxCount": 2, "probability": 100 },
{ "name": "Rat", "maxCount": 2, "probability": 100 },
{ "name": "BullFrog", "maxCount": 1, "probability": 100 }
]
},
{
"type": "Spawner",
"guid": "ed8f2188-17cd-4235-86d8-54d62209ef93",
"name": "Spawner (102)",
"location": [6084, 1470, 5],
"map": "Trammel",
"count": 3,
"minDelay": "00:00:00",
"maxDelay": "00:01:00",
"team": 0,
"homeRange": 1,
"walkingRange": 15,
"entries": [
{ "name": "SewerRat", "maxCount": 2, "probability": 100 },
{ "name": "Rat", "maxCount": 2, "probability": 100 },
{ "name": "BullFrog", "maxCount": 1, "probability": 100 }
]
},
{
"type": "Spawner",
"$type": "Spawner",
"guid": "16798caa-188d-4b2d-8494-c3b56cbaa4c7",
"name": "Spawner (102)",
"location": [6049, 1470, 5],
@ -198,10 +18,26 @@
]
},
{
"type": "Spawner",
"guid": "bda24adb-c409-4c12-a0c0-8aacd36ab3d9",
"$type": "Spawner",
"guid": "1fb937a2-b8d8-4796-8f7e-62dc86db4541",
"name": "Spawner (102)",
"location": [6034, 1467, 5],
"location": [6052, 1463, 0],
"map": "Trammel",
"count": 8,
"minDelay": "00:00:00",
"maxDelay": "00:01:00",
"team": 0,
"homeRange": 5,
"walkingRange": 25,
"entries": [
{ "name": "Alligator", "maxCount": 8, "probability": 100 }
]
},
{
"$type": "Spawner",
"guid": "2059e438-3ea0-4553-9d80-2c4b3e179956",
"name": "Spawner (102)",
"location": [6092, 1448, 5],
"map": "Trammel",
"count": 3,
"minDelay": "00:00:00",
@ -216,10 +52,10 @@
]
},
{
"type": "Spawner",
"guid": "dd1b022d-98d5-4f06-aac1-7751d2416304",
"$type": "Spawner",
"guid": "308b2558-7402-46d1-a300-11eb22feaf0b",
"name": "Spawner (102)",
"location": [6059, 1438, 4],
"location": [6049, 1447, 4],
"map": "Trammel",
"count": 3,
"minDelay": "00:00:00",
@ -234,7 +70,179 @@
]
},
{
"type": "Spawner",
"$type": "Spawner",
"guid": "31142c48-f400-4b04-a235-4df3d184432a",
"name": "Spawner (102)",
"location": [6115, 1491, -15],
"map": "Trammel",
"count": 3,
"minDelay": "00:00:00",
"maxDelay": "00:01:00",
"team": 0,
"homeRange": 2,
"walkingRange": 15,
"entries": [
{ "name": "SewerRat", "maxCount": 2, "probability": 100 },
{ "name": "Rat", "maxCount": 2, "probability": 100 },
{ "name": "BullFrog", "maxCount": 1, "probability": 100 }
]
},
{
"$type": "Spawner",
"guid": "424591d5-810a-4248-b002-51d2d993b09e",
"name": "Spawner (102)",
"location": [6108, 1471, 0],
"map": "Trammel",
"count": 2,
"minDelay": "00:00:00",
"maxDelay": "00:01:00",
"team": 0,
"homeRange": 4,
"walkingRange": 10,
"entries": [
{ "name": "Alligator", "maxCount": 2, "probability": 100 }
]
},
{
"$type": "Spawner",
"guid": "4502169a-d510-4779-a2d4-7ecff61c32a1",
"name": "Spawner (102)",
"location": [6091, 1491, 10],
"map": "Trammel",
"count": 3,
"minDelay": "00:00:00",
"maxDelay": "00:01:00",
"team": 0,
"homeRange": 2,
"walkingRange": 15,
"entries": [
{ "name": "SewerRat", "maxCount": 2, "probability": 100 },
{ "name": "Rat", "maxCount": 2, "probability": 100 },
{ "name": "BullFrog", "maxCount": 1, "probability": 100 }
]
},
{
"$type": "Spawner",
"guid": "551ba232-804a-48e2-a289-048ac2c5952c",
"name": "Spawner (102)",
"location": [6087, 1443, 0],
"map": "Trammel",
"count": 2,
"minDelay": "00:00:00",
"maxDelay": "00:01:00",
"team": 0,
"homeRange": 4,
"walkingRange": 10,
"entries": [
{ "name": "Alligator", "maxCount": 2, "probability": 100 }
]
},
{
"$type": "Spawner",
"guid": "5873288f-b3d0-4ffc-b937-1a938b0ab0a3",
"name": "Spawner (102)",
"location": [6039, 1438, 4],
"map": "Trammel",
"count": 3,
"minDelay": "00:00:00",
"maxDelay": "00:01:00",
"team": 0,
"homeRange": 2,
"walkingRange": 15,
"entries": [
{ "name": "SewerRat", "maxCount": 2, "probability": 100 },
{ "name": "Rat", "maxCount": 2, "probability": 100 },
{ "name": "BullFrog", "maxCount": 1, "probability": 100 }
]
},
{
"$type": "Spawner",
"guid": "5e47e277-8d43-46c0-b43a-a15ce3623ceb",
"name": "Spawner (102)",
"location": [6112, 1462, 5],
"map": "Trammel",
"count": 3,
"minDelay": "00:00:00",
"maxDelay": "00:01:00",
"team": 0,
"homeRange": 2,
"walkingRange": 15,
"entries": [
{ "name": "SewerRat", "maxCount": 2, "probability": 100 },
{ "name": "Rat", "maxCount": 2, "probability": 100 },
{ "name": "BullFrog", "maxCount": 1, "probability": 100 }
]
},
{
"$type": "Spawner",
"guid": "639f6230-75e9-4633-8e2b-cfd6f35f4fdc",
"name": "Spawner (102)",
"location": [6077, 1492, 0],
"map": "Trammel",
"count": 2,
"minDelay": "00:00:00",
"maxDelay": "00:01:00",
"team": 0,
"homeRange": 4,
"walkingRange": 10,
"entries": [
{ "name": "Alligator", "maxCount": 2, "probability": 100 }
]
},
{
"$type": "Spawner",
"guid": "70842ac5-c2f9-490b-8fca-603396002f94",
"name": "Spawner (102)",
"location": [6048, 1487, 5],
"map": "Trammel",
"count": 3,
"minDelay": "00:00:00",
"maxDelay": "00:01:00",
"team": 0,
"homeRange": 2,
"walkingRange": 15,
"entries": [
{ "name": "SewerRat", "maxCount": 2, "probability": 100 },
{ "name": "Rat", "maxCount": 2, "probability": 100 },
{ "name": "BullFrog", "maxCount": 1, "probability": 100 }
]
},
{
"$type": "Spawner",
"guid": "73483117-862e-472d-b856-13fbe0f2cb04",
"name": "Spawner (102)",
"location": [6065, 1459, 5],
"map": "Trammel",
"count": 3,
"minDelay": "00:00:00",
"maxDelay": "00:01:00",
"team": 0,
"homeRange": 2,
"walkingRange": 15,
"entries": [
{ "name": "SewerRat", "maxCount": 2, "probability": 100 },
{ "name": "Rat", "maxCount": 2, "probability": 100 },
{ "name": "BullFrog", "maxCount": 1, "probability": 100 }
]
},
{
"$type": "Spawner",
"guid": "80883952-a77b-466d-b01e-f810fca42c11",
"name": "Spawner (102)",
"location": [6090, 1483, 0],
"map": "Trammel",
"count": 8,
"minDelay": "00:00:00",
"maxDelay": "00:01:00",
"team": 0,
"homeRange": 5,
"walkingRange": 25,
"entries": [
{ "name": "Alligator", "maxCount": 8, "probability": 100 }
]
},
{
"$type": "Spawner",
"guid": "82cbfa7a-4ea4-476b-ac8c-a3963746fffd",
"name": "Spawner (102)",
"location": [6061, 1490, 5],
@ -252,7 +260,77 @@
]
},
{
"type": "Spawner",
"$type": "Spawner",
"guid": "bda24adb-c409-4c12-a0c0-8aacd36ab3d9",
"name": "Spawner (102)",
"location": [6034, 1467, 5],
"map": "Trammel",
"count": 3,
"minDelay": "00:00:00",
"maxDelay": "00:01:00",
"team": 0,
"homeRange": 2,
"walkingRange": 15,
"entries": [
{ "name": "SewerRat", "maxCount": 2, "probability": 100 },
{ "name": "Rat", "maxCount": 2, "probability": 100 },
{ "name": "BullFrog", "maxCount": 1, "probability": 100 }
]
},
{
"$type": "Spawner",
"guid": "cb41f5b8-add1-42be-8bbe-48b76f9a4dd9",
"name": "Spawner (102)",
"location": [6107, 1452, 25],
"map": "Trammel",
"count": 3,
"minDelay": "00:00:00",
"maxDelay": "00:01:00",
"team": 0,
"homeRange": 2,
"walkingRange": 15,
"entries": [
{ "name": "SewerRat", "maxCount": 2, "probability": 100 },
{ "name": "Rat", "maxCount": 2, "probability": 100 },
{ "name": "BullFrog", "maxCount": 1, "probability": 100 }
]
},
{
"$type": "Spawner",
"guid": "dd1b022d-98d5-4f06-aac1-7751d2416304",
"name": "Spawner (102)",
"location": [6059, 1438, 4],
"map": "Trammel",
"count": 3,
"minDelay": "00:00:00",
"maxDelay": "00:01:00",
"team": 0,
"homeRange": 2,
"walkingRange": 15,
"entries": [
{ "name": "SewerRat", "maxCount": 2, "probability": 100 },
{ "name": "Rat", "maxCount": 2, "probability": 100 },
{ "name": "BullFrog", "maxCount": 1, "probability": 100 }
]
},
{
"$type": "Spawner",
"guid": "e40a3f76-4701-425c-a806-06cb36504593",
"name": "Spawner (102)",
"location": [6037, 1494, 0],
"map": "Trammel",
"count": 2,
"minDelay": "00:00:00",
"maxDelay": "00:01:00",
"team": 0,
"homeRange": 4,
"walkingRange": 10,
"entries": [
{ "name": "Alligator", "maxCount": 2, "probability": 100 }
]
},
{
"$type": "Spawner",
"guid": "e5f0bff7-c610-4923-8c17-b32a00911628",
"name": "Spawner (102)",
"location": [6041, 1486, 5],
@ -270,87 +348,21 @@
]
},
{
"type": "Spawner",
"guid": "80883952-a77b-466d-b01e-f810fca42c11",
"$type": "Spawner",
"guid": "ed8f2188-17cd-4235-86d8-54d62209ef93",
"name": "Spawner (102)",
"location": [6090, 1483, 0],
"location": [6084, 1470, 5],
"map": "Trammel",
"count": 8,
"count": 3,
"minDelay": "00:00:00",
"maxDelay": "00:01:00",
"team": 0,
"homeRange": 5,
"walkingRange": 25,
"entries": [{ "name": "Alligator", "maxCount": 8, "probability": 100 }]
},
{
"type": "Spawner",
"guid": "639f6230-75e9-4633-8e2b-cfd6f35f4fdc",
"name": "Spawner (102)",
"location": [6077, 1492, 0],
"map": "Trammel",
"count": 2,
"minDelay": "00:00:00",
"maxDelay": "00:01:00",
"team": 0,
"homeRange": 4,
"walkingRange": 10,
"entries": [{ "name": "Alligator", "maxCount": 2, "probability": 100 }]
},
{
"type": "Spawner",
"guid": "424591d5-810a-4248-b002-51d2d993b09e",
"name": "Spawner (102)",
"location": [6108, 1471, 0],
"map": "Trammel",
"count": 2,
"minDelay": "00:00:00",
"maxDelay": "00:01:00",
"team": 0,
"homeRange": 4,
"walkingRange": 10,
"entries": [{ "name": "Alligator", "maxCount": 2, "probability": 100 }]
},
{
"type": "Spawner",
"guid": "551ba232-804a-48e2-a289-048ac2c5952c",
"name": "Spawner (102)",
"location": [6087, 1443, 0],
"map": "Trammel",
"count": 2,
"minDelay": "00:00:00",
"maxDelay": "00:01:00",
"team": 0,
"homeRange": 4,
"walkingRange": 10,
"entries": [{ "name": "Alligator", "maxCount": 2, "probability": 100 }]
},
{
"type": "Spawner",
"guid": "1fb937a2-b8d8-4796-8f7e-62dc86db4541",
"name": "Spawner (102)",
"location": [6052, 1463, 0],
"map": "Trammel",
"count": 8,
"minDelay": "00:00:00",
"maxDelay": "00:01:00",
"team": 0,
"homeRange": 5,
"walkingRange": 25,
"entries": [{ "name": "Alligator", "maxCount": 8, "probability": 100 }]
},
{
"type": "Spawner",
"guid": "e40a3f76-4701-425c-a806-06cb36504593",
"name": "Spawner (102)",
"location": [6037, 1494, 0],
"map": "Trammel",
"count": 2,
"minDelay": "00:00:00",
"maxDelay": "00:01:00",
"team": 0,
"homeRange": 4,
"walkingRange": 10,
"entries": [{ "name": "Alligator", "maxCount": 2, "probability": 100 }]
"homeRange": 1,
"walkingRange": 15,
"entries": [
{ "name": "SewerRat", "maxCount": 2, "probability": 100 },
{ "name": "Rat", "maxCount": 2, "probability": 100 },
{ "name": "BullFrog", "maxCount": 1, "probability": 100 }
]
}
]

View file

@ -1,224 +1,6 @@
[
{
"type": "Spawner",
"guid": "2d985bfc-95b3-40be-8a3e-f1919da38b19",
"name": "Spawner (106)",
"location": [5209, 965, -40],
"map": "Trammel",
"count": 4,
"minDelay": "00:05:00",
"maxDelay": "00:10:00",
"team": 0,
"homeRange": 30,
"walkingRange": 50,
"entries": [
{ "name": "Dragon", "maxCount": 1, "probability": 100 },
{ "name": "Drake", "maxCount": 2, "probability": 100 },
{ "name": "Wyvern", "maxCount": 1, "probability": 100 }
]
},
{
"type": "Spawner",
"guid": "34426acb-8319-4eb6-8760-d0acb86670f9",
"name": "Spawner (106)",
"location": [5247, 956, -40],
"map": "Trammel",
"count": 3,
"minDelay": "00:05:00",
"maxDelay": "00:10:00",
"team": 0,
"homeRange": 30,
"walkingRange": 50,
"entries": [
{ "name": "Dragon", "maxCount": 1, "probability": 100 },
{ "name": "Drake", "maxCount": 2, "probability": 100 }
]
},
{
"type": "Spawner",
"guid": "fce8877c-f374-4e24-9d3f-65205427600e",
"name": "Spawner (106)",
"location": [5280, 955, -40],
"map": "Trammel",
"count": 4,
"minDelay": "00:05:00",
"maxDelay": "00:10:00",
"team": 0,
"homeRange": 30,
"walkingRange": 50,
"entries": [
{ "name": "Dragon", "maxCount": 1, "probability": 100 },
{ "name": "Drake", "maxCount": 2, "probability": 100 },
{ "name": "Wyvern", "maxCount": 1, "probability": 100 }
]
},
{
"type": "Spawner",
"guid": "95541af6-6526-41ad-8329-a9819a3a1f8d",
"name": "Spawner (106)",
"location": [5284, 926, -40],
"map": "Trammel",
"count": 4,
"minDelay": "00:05:00",
"maxDelay": "00:10:00",
"team": 0,
"homeRange": 30,
"walkingRange": 50,
"entries": [
{ "name": "Dragon", "maxCount": 1, "probability": 100 },
{ "name": "Drake", "maxCount": 2, "probability": 100 },
{ "name": "Wyvern", "maxCount": 1, "probability": 100 }
]
},
{
"type": "Spawner",
"guid": "da94b96f-7cbc-4016-91b5-6ec3df649e23",
"name": "Spawner (106)",
"location": [5214, 917, -40],
"map": "Trammel",
"count": 4,
"minDelay": "00:05:00",
"maxDelay": "00:10:00",
"team": 0,
"homeRange": 30,
"walkingRange": 50,
"entries": [
{ "name": "Dragon", "maxCount": 1, "probability": 100 },
{ "name": "Drake", "maxCount": 2, "probability": 100 },
{ "name": "Wyvern", "maxCount": 1, "probability": 100 }
]
},
{
"type": "Spawner",
"guid": "4a321325-9193-4861-a25e-c6f075636a07",
"name": "Spawner (106)",
"location": [5241, 945, -40],
"map": "Trammel",
"count": 3,
"minDelay": "00:05:00",
"maxDelay": "00:10:00",
"team": 0,
"homeRange": 10,
"walkingRange": 10,
"entries": [
{ "name": "TreasureChestLevel3", "maxCount": 1, "probability": 100 },
{ "name": "TreasureChestLevel4", "maxCount": 2, "probability": 100 }
]
},
{
"type": "Spawner",
"guid": "68815cec-f528-4033-be41-04c6d75415e1",
"name": "Spawner (106)",
"location": [5161, 940, 0],
"map": "Trammel",
"count": 6,
"minDelay": "00:05:00",
"maxDelay": "00:10:00",
"team": 0,
"homeRange": 20,
"walkingRange": 50,
"entries": [{ "name": "GiantSerpent", "maxCount": 6, "probability": 100 }]
},
{
"type": "Spawner",
"guid": "b705ea99-1d95-42ff-8287-fb6588d0ed24",
"name": "Spawner (106)",
"location": [5149, 907, 0],
"map": "Trammel",
"count": 3,
"minDelay": "00:05:00",
"maxDelay": "00:10:00",
"team": 0,
"homeRange": 15,
"walkingRange": 20,
"entries": [
{ "name": "Wyvern", "maxCount": 1, "probability": 100 },
{ "name": "Dragon", "maxCount": 1, "probability": 100 },
{ "name": "Drake", "maxCount": 1, "probability": 100 }
]
},
{
"type": "Spawner",
"guid": "6b05c0d4-7c45-4d29-9f15-adbb0b12686f",
"name": "Spawner (106)",
"location": [5141, 915, 0],
"map": "Trammel",
"count": 3,
"minDelay": "00:05:00",
"maxDelay": "00:10:00",
"team": 0,
"homeRange": 25,
"walkingRange": 25,
"entries": [
{ "name": "TreasureChestLevel2", "maxCount": 1, "probability": 100 },
{ "name": "TreasureChestLevel3", "maxCount": 2, "probability": 100 }
]
},
{
"type": "Spawner",
"guid": "0f5e5a38-ef2b-4e49-8574-b5349c21dd06",
"name": "Spawner (106)",
"location": [5352, 934, -5],
"map": "Trammel",
"count": 4,
"minDelay": "00:05:00",
"maxDelay": "00:10:00",
"team": 0,
"homeRange": 10,
"walkingRange": 10,
"entries": [{ "name": "WaterElemental", "maxCount": 4, "probability": 100 }]
},
{
"type": "Spawner",
"guid": "9f0b2731-3658-43bd-9fe4-3bca1a7c5212",
"name": "Spawner (106)",
"location": [5317, 980, 0],
"map": "Trammel",
"count": 3,
"minDelay": "00:05:00",
"maxDelay": "00:10:00",
"team": 0,
"homeRange": 7,
"walkingRange": 7,
"entries": [
{ "name": "TreasureChestLevel3", "maxCount": 1, "probability": 100 },
{ "name": "TreasureChestLevel4", "maxCount": 2, "probability": 100 }
]
},
{
"type": "Spawner",
"guid": "8a097ab1-3372-4f64-bb50-b18470eed97f",
"name": "Spawner (106)",
"location": [5250, 869, 0],
"map": "Trammel",
"count": 6,
"minDelay": "00:05:00",
"maxDelay": "00:10:00",
"team": 0,
"homeRange": 30,
"walkingRange": 50,
"entries": [{ "name": "GiantSerpent", "maxCount": 6, "probability": 100 }]
},
{
"type": "Spawner",
"guid": "89faa9ac-5c83-4145-a239-0f1bbc3088a6",
"name": "Spawner (106)",
"location": [5226, 826, 0],
"map": "Trammel",
"count": 4,
"minDelay": "00:05:00",
"maxDelay": "00:10:00",
"team": 0,
"homeRange": 30,
"walkingRange": 50,
"entries": [
{ "name": "Dragon", "maxCount": 1, "probability": 100 },
{ "name": "Drake", "maxCount": 2, "probability": 100 },
{ "name": "Wyvern", "maxCount": 1, "probability": 100 }
]
},
{
"type": "Spawner",
"$type": "Spawner",
"guid": "01f5635b-645f-43a3-b8e1-de505a9805f4",
"name": "Spawner (106)",
"location": [5269, 811, 7],
@ -236,83 +18,7 @@
]
},
{
"type": "Spawner",
"guid": "d6da5d02-55ae-4e14-8aec-e02b2e7cf188",
"name": "Spawner (106)",
"location": [5307, 815, 0],
"map": "Trammel",
"count": 2,
"minDelay": "00:05:00",
"maxDelay": "00:10:00",
"team": 0,
"homeRange": 30,
"walkingRange": 50,
"entries": [
{ "name": "Drake", "maxCount": 1, "probability": 100 },
{ "name": "Wyvern", "maxCount": 1, "probability": 100 }
]
},
{
"type": "Spawner",
"guid": "31627f88-cd2c-42f2-8aa2-df019970100f",
"name": "Spawner (106)",
"location": [5294, 842, 0],
"map": "Trammel",
"count": 4,
"minDelay": "00:05:00",
"maxDelay": "00:10:00",
"team": 0,
"homeRange": 30,
"walkingRange": 50,
"entries": [{ "name": "WaterElemental", "maxCount": 4, "probability": 100 }]
},
{
"type": "Spawner",
"guid": "cde84676-d0c3-44e7-8be3-9335dc9c5b1b",
"name": "Spawner (106)",
"location": [5203, 787, 5],
"map": "Trammel",
"count": 4,
"minDelay": "00:05:00",
"maxDelay": "00:10:00",
"team": 0,
"homeRange": 12,
"walkingRange": 25,
"entries": [{ "name": "WaterElemental", "maxCount": 4, "probability": 100 }]
},
{
"type": "Spawner",
"guid": "45519d94-93ec-4b71-9fcd-0693de58f4e8",
"name": "Spawner (106)",
"location": [5229, 841, 1],
"map": "Trammel",
"count": 3,
"minDelay": "00:05:00",
"maxDelay": "00:10:00",
"team": 0,
"homeRange": 10,
"walkingRange": 10,
"entries": [
{ "name": "TreasureChestLevel2", "maxCount": 1, "probability": 100 },
{ "name": "TreasureChestLevel3", "maxCount": 2, "probability": 100 }
]
},
{
"type": "Spawner",
"guid": "def6615c-5bbd-4b7c-ab81-4dc4327c97d8",
"name": "Spawner (106)",
"location": [5205, 778, 0],
"map": "Trammel",
"count": 1,
"minDelay": "00:05:00",
"maxDelay": "00:10:00",
"team": 0,
"homeRange": 7,
"walkingRange": 7,
"entries": [{ "name": "TreasureChestLevel3", "maxCount": 1, "probability": 100 }]
},
{
"type": "Spawner",
"$type": "Spawner",
"guid": "039faa13-d838-4ccf-8790-13500ac30d28",
"name": "Spawner (106)",
"location": [5141, 834, 0],
@ -330,24 +36,23 @@
]
},
{
"type": "Spawner",
"guid": "73ef3d35-2c97-40c0-8c9d-f949bedf4bf0",
"$type": "Spawner",
"guid": "0f5e5a38-ef2b-4e49-8574-b5349c21dd06",
"name": "Spawner (106)",
"location": [5169, 836, 0],
"location": [5352, 934, -5],
"map": "Trammel",
"count": 3,
"count": 4,
"minDelay": "00:05:00",
"maxDelay": "00:10:00",
"team": 0,
"homeRange": 30,
"walkingRange": 50,
"homeRange": 10,
"walkingRange": 10,
"entries": [
{ "name": "Daemon", "maxCount": 2, "probability": 100 },
{ "name": "EvilMage", "maxCount": 1, "probability": 100 }
{ "name": "WaterElemental", "maxCount": 4, "probability": 100 }
]
},
{
"type": "Spawner",
"$type": "Spawner",
"guid": "22700190-506d-4f55-be3b-4ecf63f097a1",
"name": "Spawner (106)",
"location": [5152, 869, 0],
@ -365,7 +70,92 @@
]
},
{
"type": "Spawner",
"$type": "Spawner",
"guid": "2d985bfc-95b3-40be-8a3e-f1919da38b19",
"name": "Spawner (106)",
"location": [5209, 965, -40],
"map": "Trammel",
"count": 4,
"minDelay": "00:05:00",
"maxDelay": "00:10:00",
"team": 0,
"homeRange": 30,
"walkingRange": 50,
"entries": [
{ "name": "Dragon", "maxCount": 1, "probability": 100 },
{ "name": "Drake", "maxCount": 2, "probability": 100 },
{ "name": "Wyvern", "maxCount": 1, "probability": 100 }
]
},
{
"$type": "Spawner",
"guid": "31627f88-cd2c-42f2-8aa2-df019970100f",
"name": "Spawner (106)",
"location": [5294, 842, 0],
"map": "Trammel",
"count": 4,
"minDelay": "00:05:00",
"maxDelay": "00:10:00",
"team": 0,
"homeRange": 30,
"walkingRange": 50,
"entries": [
{ "name": "WaterElemental", "maxCount": 4, "probability": 100 }
]
},
{
"$type": "Spawner",
"guid": "34426acb-8319-4eb6-8760-d0acb86670f9",
"name": "Spawner (106)",
"location": [5247, 956, -40],
"map": "Trammel",
"count": 3,
"minDelay": "00:05:00",
"maxDelay": "00:10:00",
"team": 0,
"homeRange": 30,
"walkingRange": 50,
"entries": [
{ "name": "Dragon", "maxCount": 1, "probability": 100 },
{ "name": "Drake", "maxCount": 2, "probability": 100 }
]
},
{
"$type": "Spawner",
"guid": "45519d94-93ec-4b71-9fcd-0693de58f4e8",
"name": "Spawner (106)",
"location": [5229, 841, 1],
"map": "Trammel",
"count": 3,
"minDelay": "00:05:00",
"maxDelay": "00:10:00",
"team": 0,
"homeRange": 10,
"walkingRange": 10,
"entries": [
{ "name": "TreasureChestLevel2", "maxCount": 1, "probability": 100 },
{ "name": "TreasureChestLevel3", "maxCount": 2, "probability": 100 }
]
},
{
"$type": "Spawner",
"guid": "4a321325-9193-4861-a25e-c6f075636a07",
"name": "Spawner (106)",
"location": [5241, 945, -40],
"map": "Trammel",
"count": 3,
"minDelay": "00:05:00",
"maxDelay": "00:10:00",
"team": 0,
"homeRange": 10,
"walkingRange": 10,
"entries": [
{ "name": "TreasureChestLevel3", "maxCount": 1, "probability": 100 },
{ "name": "TreasureChestLevel4", "maxCount": 2, "probability": 100 }
]
},
{
"$type": "Spawner",
"guid": "4dea2c35-b11b-43cd-9916-6ee8d03105c7",
"name": "Spawner (106)",
"location": [5153, 842, 0],
@ -383,21 +173,159 @@
]
},
{
"type": "Spawner",
"guid": "fb1e3f19-4276-4b1f-b87f-0b4e38ebd70b",
"$type": "Spawner",
"guid": "58412982-34fe-41e4-9c85-cd0d5691d62c",
"name": "Spawner (106)",
"location": [5141, 968, 0],
"location": [5143, 986, 0],
"map": "Trammel",
"count": 1,
"count": 3,
"minDelay": "00:05:00",
"maxDelay": "00:10:00",
"team": 0,
"homeRange": 10,
"walkingRange": 10,
"entries": [{ "name": "FireElemental", "maxCount": 1, "probability": 100 }]
"entries": [
{ "name": "TreasureChestLevel3", "maxCount": 1, "probability": 100 },
{ "name": "TreasureChestLevel4", "maxCount": 2, "probability": 100 }
]
},
{
"type": "Spawner",
"$type": "Spawner",
"guid": "68815cec-f528-4033-be41-04c6d75415e1",
"name": "Spawner (106)",
"location": [5161, 940, 0],
"map": "Trammel",
"count": 6,
"minDelay": "00:05:00",
"maxDelay": "00:10:00",
"team": 0,
"homeRange": 20,
"walkingRange": 50,
"entries": [
{ "name": "GiantSerpent", "maxCount": 6, "probability": 100 }
]
},
{
"$type": "Spawner",
"guid": "699dfc8f-e891-4978-9c73-a74cb1762849",
"name": "Spawner (106)",
"location": [5185, 1006, 0],
"map": "Trammel",
"count": 1,
"minDelay": "00:05:00",
"maxDelay": "00:10:00",
"team": 0,
"homeRange": 20,
"walkingRange": 50,
"entries": [
{ "name": "AncientWyrm", "maxCount": 1, "probability": 100 }
]
},
{
"$type": "Spawner",
"guid": "6b05c0d4-7c45-4d29-9f15-adbb0b12686f",
"name": "Spawner (106)",
"location": [5141, 915, 0],
"map": "Trammel",
"count": 3,
"minDelay": "00:05:00",
"maxDelay": "00:10:00",
"team": 0,
"homeRange": 25,
"walkingRange": 25,
"entries": [
{ "name": "TreasureChestLevel2", "maxCount": 1, "probability": 100 },
{ "name": "TreasureChestLevel3", "maxCount": 2, "probability": 100 }
]
},
{
"$type": "Spawner",
"guid": "73ef3d35-2c97-40c0-8c9d-f949bedf4bf0",
"name": "Spawner (106)",
"location": [5169, 836, 0],
"map": "Trammel",
"count": 3,
"minDelay": "00:05:00",
"maxDelay": "00:10:00",
"team": 0,
"homeRange": 30,
"walkingRange": 50,
"entries": [
{ "name": "Daemon", "maxCount": 2, "probability": 100 },
{ "name": "EvilMage", "maxCount": 1, "probability": 100 }
]
},
{
"$type": "Spawner",
"guid": "89faa9ac-5c83-4145-a239-0f1bbc3088a6",
"name": "Spawner (106)",
"location": [5226, 826, 0],
"map": "Trammel",
"count": 4,
"minDelay": "00:05:00",
"maxDelay": "00:10:00",
"team": 0,
"homeRange": 30,
"walkingRange": 50,
"entries": [
{ "name": "Dragon", "maxCount": 1, "probability": 100 },
{ "name": "Drake", "maxCount": 2, "probability": 100 },
{ "name": "Wyvern", "maxCount": 1, "probability": 100 }
]
},
{
"$type": "Spawner",
"guid": "8a097ab1-3372-4f64-bb50-b18470eed97f",
"name": "Spawner (106)",
"location": [5250, 869, 0],
"map": "Trammel",
"count": 6,
"minDelay": "00:05:00",
"maxDelay": "00:10:00",
"team": 0,
"homeRange": 30,
"walkingRange": 50,
"entries": [
{ "name": "GiantSerpent", "maxCount": 6, "probability": 100 }
]
},
{
"$type": "Spawner",
"guid": "95541af6-6526-41ad-8329-a9819a3a1f8d",
"name": "Spawner (106)",
"location": [5284, 926, -40],
"map": "Trammel",
"count": 4,
"minDelay": "00:05:00",
"maxDelay": "00:10:00",
"team": 0,
"homeRange": 30,
"walkingRange": 50,
"entries": [
{ "name": "Dragon", "maxCount": 1, "probability": 100 },
{ "name": "Drake", "maxCount": 2, "probability": 100 },
{ "name": "Wyvern", "maxCount": 1, "probability": 100 }
]
},
{
"$type": "Spawner",
"guid": "9f0b2731-3658-43bd-9fe4-3bca1a7c5212",
"name": "Spawner (106)",
"location": [5317, 980, 0],
"map": "Trammel",
"count": 3,
"minDelay": "00:05:00",
"maxDelay": "00:10:00",
"team": 0,
"homeRange": 7,
"walkingRange": 7,
"entries": [
{ "name": "TreasureChestLevel3", "maxCount": 1, "probability": 100 },
{ "name": "TreasureChestLevel4", "maxCount": 2, "probability": 100 }
]
},
{
"$type": "Spawner",
"guid": "b2eb4c30-1f29-4ef5-b80a-851c68af76f9",
"name": "Spawner (106)",
"location": [5157, 997, 0],
@ -415,38 +343,76 @@
]
},
{
"type": "Spawner",
"guid": "699dfc8f-e891-4978-9c73-a74cb1762849",
"$type": "Spawner",
"guid": "b705ea99-1d95-42ff-8287-fb6588d0ed24",
"name": "Spawner (106)",
"location": [5185, 1006, 0],
"map": "Trammel",
"count": 1,
"minDelay": "00:05:00",
"maxDelay": "00:10:00",
"team": 0,
"homeRange": 20,
"walkingRange": 50,
"entries": [{ "name": "AncientWyrm", "maxCount": 1, "probability": 100 }]
},
{
"type": "Spawner",
"guid": "58412982-34fe-41e4-9c85-cd0d5691d62c",
"name": "Spawner (106)",
"location": [5143, 986, 0],
"location": [5149, 907, 0],
"map": "Trammel",
"count": 3,
"minDelay": "00:05:00",
"maxDelay": "00:10:00",
"team": 0,
"homeRange": 10,
"walkingRange": 10,
"homeRange": 15,
"walkingRange": 20,
"entries": [
{ "name": "TreasureChestLevel3", "maxCount": 1, "probability": 100 },
{ "name": "TreasureChestLevel4", "maxCount": 2, "probability": 100 }
{ "name": "Wyvern", "maxCount": 1, "probability": 100 },
{ "name": "Dragon", "maxCount": 1, "probability": 100 },
{ "name": "Drake", "maxCount": 1, "probability": 100 }
]
},
{
"type": "Spawner",
"$type": "Spawner",
"guid": "cde84676-d0c3-44e7-8be3-9335dc9c5b1b",
"name": "Spawner (106)",
"location": [5203, 787, 5],
"map": "Trammel",
"count": 4,
"minDelay": "00:05:00",
"maxDelay": "00:10:00",
"team": 0,
"homeRange": 12,
"walkingRange": 25,
"entries": [
{ "name": "WaterElemental", "maxCount": 4, "probability": 100 }
]
},
{
"$type": "Spawner",
"guid": "d6da5d02-55ae-4e14-8aec-e02b2e7cf188",
"name": "Spawner (106)",
"location": [5307, 815, 0],
"map": "Trammel",
"count": 2,
"minDelay": "00:05:00",
"maxDelay": "00:10:00",
"team": 0,
"homeRange": 30,
"walkingRange": 50,
"entries": [
{ "name": "Drake", "maxCount": 1, "probability": 100 },
{ "name": "Wyvern", "maxCount": 1, "probability": 100 }
]
},
{
"$type": "Spawner",
"guid": "da94b96f-7cbc-4016-91b5-6ec3df649e23",
"name": "Spawner (106)",
"location": [5214, 917, -40],
"map": "Trammel",
"count": 4,
"minDelay": "00:05:00",
"maxDelay": "00:10:00",
"team": 0,
"homeRange": 30,
"walkingRange": 50,
"entries": [
{ "name": "Dragon", "maxCount": 1, "probability": 100 },
{ "name": "Drake", "maxCount": 2, "probability": 100 },
{ "name": "Wyvern", "maxCount": 1, "probability": 100 }
]
},
{
"$type": "Spawner",
"guid": "db505150-0a76-44ac-8b27-841ef6b87321",
"name": "Spawner (106)",
"location": [5183, 1007, 0],
@ -461,5 +427,55 @@
{ "name": "TreasureChestLevel3", "maxCount": 1, "probability": 100 },
{ "name": "TreasureChestLevel4", "maxCount": 2, "probability": 100 }
]
},
{
"$type": "Spawner",
"guid": "def6615c-5bbd-4b7c-ab81-4dc4327c97d8",
"name": "Spawner (106)",
"location": [5205, 778, 0],
"map": "Trammel",
"count": 1,
"minDelay": "00:05:00",
"maxDelay": "00:10:00",
"team": 0,
"homeRange": 7,
"walkingRange": 7,
"entries": [
{ "name": "TreasureChestLevel3", "maxCount": 1, "probability": 100 }
]
},
{
"$type": "Spawner",
"guid": "fb1e3f19-4276-4b1f-b87f-0b4e38ebd70b",
"name": "Spawner (106)",
"location": [5141, 968, 0],
"map": "Trammel",
"count": 1,
"minDelay": "00:05:00",
"maxDelay": "00:10:00",
"team": 0,
"homeRange": 10,
"walkingRange": 10,
"entries": [
{ "name": "FireElemental", "maxCount": 1, "probability": 100 }
]
},
{
"$type": "Spawner",
"guid": "fce8877c-f374-4e24-9d3f-65205427600e",
"name": "Spawner (106)",
"location": [5280, 955, -40],
"map": "Trammel",
"count": 4,
"minDelay": "00:05:00",
"maxDelay": "00:10:00",
"team": 0,
"homeRange": 30,
"walkingRange": 50,
"entries": [
{ "name": "Dragon", "maxCount": 1, "probability": 100 },
{ "name": "Drake", "maxCount": 2, "probability": 100 },
{ "name": "Wyvern", "maxCount": 1, "probability": 100 }
]
}
]

View file

@ -1,6 +1,45 @@
[
{
"type": "Spawner",
"$type": "Spawner",
"guid": "003c2e17-0f7e-4af8-9b25-de6ccc4c6e24",
"name": "Spawner (108)",
"location": [4545, 1317, 8],
"map": "Trammel",
"count": 4,
"minDelay": "00:05:00",
"maxDelay": "00:10:00",
"team": 0,
"spawnBounds": {
"start": { "x": 4535, "y": 1307, "z": -128 },
"end": { "x": 4555, "y": 1327, "z": 25 }
},
"walkingRange": 20,
"entries": [
{ "name": "Spectre", "maxCount": 1, "probability": 100 },
{ "name": "Shade", "maxCount": 1, "probability": 100 },
{ "name": "Wraith", "maxCount": 1, "probability": 100 },
{ "name": "Skeleton", "maxCount": 1, "probability": 100 },
{ "name": "Zombie", "maxCount": 2, "probability": 100 }
]
},
{
"$type": "Spawner",
"guid": "18ac21c4-da65-4449-997e-6e3952e53d46",
"name": "Spawner (108)",
"location": [722, 1119, 0],
"map": "Trammel",
"count": 10,
"minDelay": "00:05:00",
"maxDelay": "00:10:00",
"team": 0,
"homeRange": 20,
"walkingRange": 20,
"entries": [
{ "name": "Skeleton", "maxCount": 10, "probability": 100 }
]
},
{
"$type": "Spawner",
"guid": "1b1194dd-0699-4c9e-9cbe-1332a477a90d",
"name": "Spawner (108)",
"location": [1369, 1475, 10],
@ -19,27 +58,29 @@
]
},
{
"type": "Spawner",
"guid": "f40bd4f4-6ff1-40ba-84c3-296ade50ab51",
"$type": "Spawner",
"guid": "69dd4fe9-7f47-466e-b290-963f9bf240d8",
"name": "Spawner (108)",
"location": [1285, 3731, 0],
"location": [2758, 867, 0],
"map": "Trammel",
"count": 9,
"minDelay": "00:05:00",
"maxDelay": "00:10:00",
"team": 0,
"homeRange": 10,
"walkingRange": 15,
"spawnBounds": {
"start": { "x": 2738, "y": 847, "z": -128 },
"end": { "x": 2778, "y": 887, "z": 15 }
},
"walkingRange": 20,
"entries": [
{ "name": "Spectre", "maxCount": 2, "probability": 100 },
{ "name": "Wraith", "maxCount": 2, "probability": 100 },
{ "name": "Shade", "maxCount": 2, "probability": 100 },
{ "name": "Skeleton", "maxCount": 3, "probability": 100 },
{ "name": "Zombie", "maxCount": 4, "probability": 100 }
]
},
{
"type": "Spawner",
"$type": "Spawner",
"guid": "eb0eb81f-7aea-4162-bf8d-1a0e614ecbcd",
"name": "Spawner (108)",
"location": [2438, 1100, 8],
@ -48,7 +89,10 @@
"minDelay": "00:05:00",
"maxDelay": "00:10:00",
"team": 0,
"homeRange": 10,
"spawnBounds": {
"start": { "x": 2428, "y": 1090, "z": -128 },
"end": { "x": 2448, "y": 1110, "z": 30 }
},
"walkingRange": 20,
"entries": [
{ "name": "Spectre", "maxCount": 1, "probability": 100 },
@ -60,60 +104,30 @@
]
},
{
"type": "Spawner",
"guid": "003c2e17-0f7e-4af8-9b25-de6ccc4c6e24",
"$type": "Spawner",
"guid": "f40bd4f4-6ff1-40ba-84c3-296ade50ab51",
"name": "Spawner (108)",
"location": [4545, 1317, 8],
"map": "Trammel",
"count": 4,
"minDelay": "00:05:00",
"maxDelay": "00:10:00",
"team": 0,
"homeRange": 10,
"walkingRange": 20,
"entries": [
{ "name": "Spectre", "maxCount": 1, "probability": 100 },
{ "name": "Shade", "maxCount": 1, "probability": 100 },
{ "name": "Wraith", "maxCount": 1, "probability": 100 },
{ "name": "Skeleton", "maxCount": 1, "probability": 100 },
{ "name": "Zombie", "maxCount": 2, "probability": 100 }
]
},
{
"type": "Spawner",
"guid": "69dd4fe9-7f47-466e-b290-963f9bf240d8",
"name": "Spawner (108)",
"location": [2758, 867, 0],
"location": [1285, 3731, 0],
"map": "Trammel",
"count": 9,
"minDelay": "00:05:00",
"maxDelay": "00:10:00",
"team": 0,
"homeRange": 20,
"walkingRange": 20,
"spawnBounds": {
"start": { "x": 1275, "y": 3721, "z": -128 },
"end": { "x": 1295, "y": 3741, "z": 15 }
},
"walkingRange": 15,
"entries": [
{ "name": "Spectre", "maxCount": 2, "probability": 100 },
{ "name": "Wraith", "maxCount": 2, "probability": 100 },
{ "name": "Shade", "maxCount": 2, "probability": 100 },
{ "name": "Skeleton", "maxCount": 3, "probability": 100 },
{ "name": "Zombie", "maxCount": 4, "probability": 100 }
]
},
{
"type": "Spawner",
"guid": "18ac21c4-da65-4449-997e-6e3952e53d46",
"name": "Spawner (108)",
"location": [722, 1119, 0],
"map": "Trammel",
"count": 10,
"minDelay": "00:05:00",
"maxDelay": "00:10:00",
"team": 0,
"homeRange": 20,
"walkingRange": 20,
"entries": [{ "name": "Skeleton", "maxCount": 10, "probability": 100 }]
},
{
"type": "Spawner",
"$type": "Spawner",
"guid": "fd6f9733-1638-485e-a3fd-3d6f9550591e",
"name": "Spawner (108)",
"location": [3407, 2652, 48],

Some files were not shown because too many files have changed in this diff Show more