Compare commits

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

1235 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
Kamron Batman
09d6420320
fix: Fixes Item ID so it doesn't gain from non-identifiable items (#2075) 2025-01-18 10:58:22 -08:00
Kamron Batman
c98b8fdeca
fix: Fixes some buff icons off by 1s (#2073) 2025-01-18 10:40:01 -08:00
Kamron Batman
b90f9e51ac
fix: Fixes buff icons showing up when you log in (#2072) 2025-01-17 21:47:44 -08:00
Kamron Batman
b6950fc77c
fix: Removes unused usings (#2071) 2025-01-17 16:23:30 -08:00
Reetus
d15263c7d0
fix: Fix FillableContainer default so it is None (#2065) 2025-01-17 15:22:47 -08:00
Kamron Batman
66a257ece2
feat: Converts OnLogin to a coded generated event (#2070) 2025-01-17 15:21:45 -08:00
Erik Askov Mousing
3b698de556
feat: Adds Pre-UOTD single click support for weapons, armor, wands, and clothing (#2053) 2025-01-13 17:04:24 -08:00
Nathan Oines
863b4b5460
fix: Ensure ShouldSerializeCrafter serializes when _crafter is not null or empty (#2062) 2025-01-13 13:22:56 -08:00
Bohica
25431aa7f5
fix: Fixes loading spawns from subdirs using AdminGump (#2061) 2025-01-12 11:31:29 -08:00
Bohica
5c1573193b
feat: Adds AdminGump Shutdown with 15m Delay & Save (#2058) 2025-01-10 18:28:17 -08:00
Guyute
1da5a49baf
feat: Adds delegate events for Help system (#2054) 2025-01-10 12:34:02 -08:00
Kamron Batman
cc61c1d6d2
fix: Rewrites AddDoor command, fixes gump and returning to correct page. (#2056) 2025-01-08 10:32:37 -08:00
Kamron Batman
e708bd7f69
fix: Fixes loading the world when types are deleted (#2055) 2025-01-07 22:33:09 -08:00
Kamron Batman
9f50ae431a
fix: Attempts to fix IsEnemy with NPC summons again again. (#2052) 2025-01-03 19:58:37 -08:00
Bohica
5d53825c21
fix: Properly splits out farmable crop spawners for Felucca/Trammel on uoml/post-uoml (#2049) 2025-01-03 09:43:52 -08:00
3HMonkey
e58462a117
fix: Fix for decreasing pet loyalty on feed + message (#2050) 2025-01-03 09:21:30 -08:00
Bohica
6df76a7730
fix: Adds missing Champion teleporter (#2048) 2025-01-02 19:35:19 -08:00
Kamron Batman
f3c7a0e035
fix: Fixes explosion potion timer offset (#2047) 2025-01-01 20:28:12 -08:00
Kamron Batman
57a7861d5e
fix: Timer Remaining Count off by 1 fix (#2046) 2025-01-01 20:19:38 -08:00
Kamron Batman
1586a8e6ba
fix: Fixes dropping gold/bank checks in a bank box when it is full (#2044) 2025-01-01 16:47:32 -08:00
Kamron Batman
867dd6eb78
fix: Fixes NPC summon IsEnemy check against other mobs (#2041) 2024-12-31 13:38:58 -08:00
Kamron Batman
b6e31217a2
chore(license): Removes CLA requirement. All contributors moving forward will retain copyright. (#2038) 2024-12-31 02:52:58 -08:00
Guyute
9d0d454b4e
feat: Moves logger to a separate assembly for reuse (#2001) 2024-12-30 20:25:34 -08:00
Bohica
333b40872a
fix: Add Iron Gate decorations to Vesper Graveyard in Felucca and Trammel (#2008) 2024-12-30 19:45:21 -08:00
Kamron Batman
ade7393e87
chore(deps): Bumps modernuo schema generator to 2.12.18 (#2039) 2024-12-30 19:04:46 -08:00
Kamron Batman
2fded43888
feat: Adds exception message to logger for TCP Server and streamlines logger messages. (#2037) 2024-12-30 13:54:39 -08:00
Kamron Batman
60b97f80d8
chore(deps): Bumps xunit to 3.0, serialization generator to 2.12.18, high performance to 8.4, and mailkit to 4.9 (#2032) 2024-12-27 12:56:41 -08:00
Nathan Oines
e8a199e498
fix: Allow guildstone movement within the same house (#2031)
- Adjusted condition to prevent unnecessary "one guildstone per house" restriction when moving the guildstone inside the same house since the guildstone does not disappear after teleporter generation.

Refactors the teleportation logic for improved user experience.
2024-12-27 12:29:28 -08:00
Kamron Batman
2bff1d2cbc
feat: Converts player deletion handling to a generated event (#2029) 2024-12-21 20:55:10 -08:00
Kamron Batman
059f008b48
fix: Fixes blood oath duration (#2027) 2024-12-18 08:41:52 -08:00
Kamron Batman
e27773e3d9
feat: Adds wall z-offset detection for doorgen (#2025) 2024-12-17 10:40:02 -08:00
Reetus
f33e218c0b
fix: Fix HolidayTree not serializing components (#2024) 2024-12-17 08:59:50 -08:00
Bohica
d9e5ec91e1
fix: Update Hythloth teleport locations in Felucca and Trammel to new coordinates to prevent looping and stuck. (#2012) 2024-12-17 08:59:36 -08:00
Reetus
e662b78f5a
fix: Fix missing LabelNumber on DisguiseKit (#2021) 2024-12-16 12:32:26 -08:00
Reetus
fefa06d4e0
fix: Fix Lord/Lady title display in paperdoll when fame >= 10000 (#2022) 2024-12-16 12:28:52 -08:00
Reetus
94040f142c
fix: Fixes overlapping text in TithingGump (#2020) 2024-12-16 12:21:09 -08:00
Bohica
ec12eb037d
Update britain.cfg - add missing stone fireplace (#2010) 2024-12-14 00:18:32 -08:00
Bohica
f0b41d9b5e
fix: Add Dark Wood Door decoration to Moonglow; comment out unused bed and archery butte coordinates (#2009) 2024-12-14 00:17:58 -08:00
Kamron Batman
54cf52f8ec
feat: Adds missing door types to [adddoor (#2017) 2024-12-12 23:34:41 -08:00
Kamron Batman
c75514cc04
feat: Updates to .NET 9 (#1984)
### Summary

* Bumps to .NET 9 with updated dependencies
* Comparing a value type against null is no longer allowed
* CI/CD now uses the version specified in global.json
* Serialization generator updated to .NET 9 with bug fixes, fixes to turkish language, and parallelization
2024-12-08 10:16:34 -08:00
Kamron Batman
51d15d264c
fix: Removes redundant conditional check in house demolish gump (#2004) 2024-12-03 18:15:58 -08:00
Kamron Batman
ba2a41236c
fix: Fixes demolishing house for a deed (#2003)
### Summary

* Fixes players with no bank box failing to demolish their house.
* Fixes players getting a deed, and a house infinitely.
2024-12-03 17:58:31 -08:00
Kamron Batman
1d08572926
fix: Fixes monster ability firing after dying (#1990) 2024-11-11 08:53:49 -05:00
Kamron Batman
b475e17c92
fix: Forces InvariantCulture on server start for DefaultThreadCurrentCulture (#1988)
Co-authored-by: Stefano Merotta <97297186+stefanomerotta@users.noreply.github.com>
2024-11-01 21:21:10 -07:00
Kamron Batman
dabaa1eb5a
fix: Fixes honor check crash on target killed (#1987) 2024-10-30 16:33:43 -07:00
Kamron Batman
cc6d029add
fix: Fixes timer Delay/Next not handling MinValue inputs (#1985)
### Summary

Fixes a few minor issues with timers:
- Timer.Delay and Timer.Interval was not reflecting the actual tick time (aligned to the next 8ms)
- Negative delay values were causing a crash when DateTime.Now - delay was below DateTime.MinValue
- Timer.Next now reflects the correct wall clock tick time based on the adjusted Delay.
- Timer.Next is not assigned when the timer is started. This was important for timers that were created, but started later.
2024-10-28 17:48:16 -07:00
Kamron Batman
ac06a0d52d
fix: Adds BWT cliloc support for v7.0.104+ (#1982) 2024-10-19 22:01:35 -07:00
Kamron Batman
b17ebda975
fix: Converts BarkeeperTitleGump to static gump and localizes entries (#1980) 2024-10-19 20:36:53 -07:00
Kamron Batman
8bea1496f4
fix: Converts accept protection gump to StaticGump (#1979) 2024-10-19 00:30:59 -07:00
Kamron Batman
318407f9be
fix: Fixes AddProtection backward arguments (#1978) 2024-10-19 00:24:26 -07:00
Kamron Batman
3fc55e4db3
fix: Fixes searching null map crash (#1975) 2024-10-15 17:52:27 -07:00
Kamron Batman
9f985b0c38
fix: Fixes negative acts from explosion spell. Fixes Orc Brute ability (#1974) 2024-10-15 17:51:14 -07:00
Kamron Batman
26c7f5c49b
chore(deps): Bumps xunit to 2.9.2, Serilog to 4.0.2, and MailKit to 4.0.8 (#1973) 2024-10-15 17:04:33 -07:00
Kamron Batman
f16bce60e6
fix: Fixes ethics causing crashes when not used (#1972) 2024-10-15 17:02:11 -07:00
Bohica
b7316a5bd6
fix: Fixes Occlo so it uses the actual addon when decorations are generated. (#1970) 2024-10-08 13:08:23 -07:00
Kamron Batman
b9d63e4160
fix: Fixes ObjectPropertyList double return issue (#1969)
- Fixes double return issue with object property list that is causing corruption.
- Adds DEBUG_ARRAYPOOL define constant which will crash on double return or invalid return scenarios.

> [!IMPORTANT]
> **Developer Notes**
> STArrayPool rented arrays **MUST NOT** be returned **ONLY ONCE** otherwise there will be corruption from double-use.
> Use `DEBUG_ARRAYPOOL` to test potential broken STArrayPool use cases.

> [!NOTE]
> **Why can't I enable the debug all the time?**
> Other than the fact that it will crash due to bad code, the actual tracking system is highly detrimental/problematic for performance and memory consumption by creating objects that have a stack trace.
2024-10-07 21:21:21 -07:00
Derek Gooding
4b3b2839c6
chore(docs): Adds UnmanagedDataReader & BinaryFileReader documentation (#1968) 2024-10-01 17:07:51 -07:00
Joshua Kappers
766b46bcc1
fix: Actions on gump crash when Runebook has no entries (#1964) 2024-09-27 10:17:22 -07:00
Kamron Batman
9d9a5220d1
fix: Fixes PlayerZombies null pointer exception (#1963) 2024-09-25 15:27:42 -07:00
Kamron Batman
4e2fcee7a2
chore(deps): Bumps xunit to 2.9.1 and CommunityToolkit.HighPerformance to 8.3.2 (#1962) 2024-09-25 10:27:37 -07:00
Joshua Kappers
3a3c651b85
fix: Ensure server listing address is used when set in config (#1960) 2024-09-21 22:34:56 -07:00
Kamron Batman
2a7aa1905b
fix: Fixes edge cases with the network packet loop (#1959) 2024-09-19 18:25:30 -07:00
Kamron Batman
e0fcde885c
fix: Fixes networking issues (#1958)
### Summary
- Puts back `EventSink.SocketConnect`.
- Reverts networking change to push the networking to a separate thread.
- Reverts changes to the firewall by removing the firewall queue.
- Fixes listeners not shutting down with the server.
- Fixes race condition causing connections to get stuck even after they are disposed.

> [!NOTE]
> **Developer Note**
> Networking has been reverted back to using the main thread instead of a background thread. This alleviated complexity and the requirement for concurrent queues all over the place.
2024-09-19 16:54:49 -07:00
Kamron Batman
97c53e656e
chore (README.md): Update .NET requirements in README (#1956) 2024-09-18 09:17:16 -07:00
Kamron Batman
113a6b1f7c
fix (README.md): Fixes the supported operating systems. (#1955) 2024-09-18 09:13:47 -07:00
Derek Gooding
893441f74a
feat: Adds partial keyword to the Server.Utility class (#1954) 2024-09-16 08:57:24 -07:00
Kamron Batman
465d3c8187
feat: Upgrades serialization v4 (Threaded Heap Serialization) (#1947)
### Summary

- `GenericEntityPersistence` is now a type of `GenericPersistence`. This allows developers to serialize both entities and non-entities in the same system. 🎉
- Each `SerializationThreadWorker` now allocates 1MB of heap for serialization _permanently_. If more memory is needed, that thread will double it's memory, not to exceed increments of 64MB.
- Several bugs with serialization introduced with the pure MMF implementation have been fixed.
- `BinaryFileReader` has been added back. 🎉
- Adds `world.useMultithreadedSaves` to allow disabling threaded saves.

> [!IMPORTANT]
> **Developer Note**
> The split file serialization has been deprecated and is no longer used. We have effectively gone back to the same file writing we had before the pure MMF implementation.
2024-09-14 09:57:43 -07:00
Derek Gooding
aaac0c596c
chore: Remove some of the warnings/suggestions that can't be fixed due to legacy (#1953) 2024-09-13 12:22:50 -07:00
Kamron Batman
8639ed2210
fix: Fixes bad password with SHA1/SHA2 (#1952) 2024-09-12 17:20:44 -07:00
Kamron Batman
257825dc23
fix: Removes invalidation of tickcount and now in core (#1949) 2024-09-11 22:51:02 -07:00
Kamron Batman
2e6ddcd31a
fix: Removes profiling. Streamlines core tick count. (#1948)
### Summary
- Removes `Profiling` - Recommend using Visual Studio Performance Profiler,  [dotnet-trace](https://learn.microsoft.com/en-us/dotnet/core/diagnostics/dotnet-trace) or [JetBrains dotTrace](https://www.jetbrains.com/profiler/)
- Cleans up the core tick count, sampling, etc.
2024-09-11 00:55:18 -07:00
Kamron Batman
65bc264402
fix: Fixes Advanced Search crash (#1946) 2024-09-08 20:37:51 -07:00
Kamron Batman
47e16a03fe
feat: Moves network related events to UOContent using code generation (#1945)
### Summary
- Moves `CharacterCreated` event to UOContent using code generated event _CharacterCreatedEvent_.
- Moves `TargetByResourceMacro` event to UOContent using code generated event _TargetByResourceMacro_.
- Moves `GameLogin` event to UOContent using code generated event _GameLoginEvent_.
- Moves `ServerList` event to UOContent using code generated event _ServerListEvent_.
- Streamlines ProfessionInfo


> [!IMPORTANT]
> **Developer Note**
> Custom scripts that use any of these event sinks will need to be updated to use the new code generated events.
2024-09-06 16:57:10 -07:00
Kamron Batman
4110556e89
chore(deps): Bumps dependencies (#1944) 2024-09-05 21:24:47 -07:00
Guyute
95cd8749a3
feat: Adds PlayerDeathEvent and CreatureDeathEvent using code generated events (#1927)
### Summary
- Adds [Code Generated Events](https://github.com/modernuo/CodeGeneratedEvents)
- Removes EventSink.PlayerDeath
- Adds `PlayerDeathEvent` and `CreatureDeathEvent` using code generated events

> [!Important]
> **Developer Note**
> Use `[OnEvent(nameof(PlayerMobile.PlayerDeathEvent))]` instead of `EventSink.PlayerDeath` delegate
> Check this commit for examples of how to use this.
2024-09-05 21:23:03 -07:00
Reetus
d5984ce058
fix: Releasing a Talisman summon deletes your character (#1940) 2024-08-23 09:04:14 -07:00
Reetus
8a41d84239
fix: Fixes Velocity property not displaying value (#1938) 2024-08-20 21:15:12 -07:00
Kamron Batman
35342e296e
fix: Fixes targeting ground with spells (#1937) 2024-08-20 15:51:33 -07:00
Reetus
4c5947e654
fix: Fixes missing CopyTo in FixHtmlFormattable (#1935) 2024-08-19 07:09:07 -07:00
Kamron Batman
36c373b76a
fix: Fixes spell targeting (#1934) 2024-08-17 22:45:08 -07:00
dependabot[bot]
25dfb5cca3
chore(deps): Bump Nerdbank.GitVersioning from 3.6.139 to 3.6.141 (#1928) 2024-08-17 15:16:36 -07:00
Kamron Batman
eecb2f0f44
fix: Fixes focus cliloc name (#1933) 2024-08-15 19:13:09 -07:00
Kamron Batman
779dc9d37b
fix: Fixes null crash on deserialization for Champs (#1932) 2024-08-15 18:55:31 -07:00
Kamron Batman
1272be7be9
fix: Fixes damage scale for AOS (#1931) 2024-08-15 18:32:25 -07:00
Reetus
71a68126da
fix: Set VendorItem Valid AfterDeserialize (#1930) 2024-08-15 07:25:38 -07:00
Guyute
b612c443df
fix: Migrate BOD gumps to DynamicGump (#1926) 2024-08-13 08:13:20 -07:00
Kamron Batman
16d7961dc9
fix: Fixes profession indexing (#1929) 2024-08-12 23:13:53 -07:00
Reetus
29265c2fed
fix: GateTravel sends you to the origin map (#1924) 2024-08-12 09:27:23 -07:00
Kamron Batman
03a6edad45
fix: Disconnect if user tries to give themselves too much skill (#1922) 2024-08-10 12:49:39 -07:00
Guyute
a86601ffea
feat: Restyle young player gumps (#1917)
## Summary

Restyles the three "young player" gumps: `YoungDungeonWarningGump` `YoungDeathNoticeGump` and `RenounceYoungGump` to a cleaner, more consistent style I made up.

### Screenshots
<img width="265" alt="image" src="https://github.com/user-attachments/assets/bd872023-71eb-44ba-8421-f721df1198a5">
<br \>
This one is spooky because they're dead.
<br \>
<img width="443" alt="image" src="https://github.com/user-attachments/assets/ec3729a1-026f-4edc-aaf0-9b353283117b">
<br \>
<img width="467" alt="image" src="https://github.com/user-attachments/assets/f7821050-22dd-408a-a519-789bba813df2">
2024-08-10 11:06:18 -07:00
Guyute
12a75d3c9c
fix: Convert house gumps to static/dynamic gumps (#1912) 2024-08-10 10:56:19 -07:00
Stefano Merotta
aa68d96807
fix: Removes redundant code in GumpSystem (#1921) 2024-08-10 10:20:34 -07:00
Guyute
76bdc7ac02
fix: Moves pricing from PricedHealer to BaseHealer - Combines Res Gumps (#1920) 2024-08-10 10:16:47 -07:00
Stefano Merotta
6fe01488ef
fix: Fix NetStateGumps and other Gumps cleanup (#1919) 2024-08-10 09:57:28 -07:00
Kamron Batman
8282b00ca2
feat: Moves gumps out of the core (#1916)
> [!Important]
> **Developer Note**
> This code change will **completely move gumps out of the core**


### Summary

- Adds `GetGumps()` convenience which exposes methods to Find/Close/Send multiple gumps. This helper is a performance improvement by eliminating the Dictionary<Player, List> lookup for gumps.
2024-08-09 19:07:32 -07:00
Guyute
40d99f6d1c
fix: Converts WhoGump to DynamicGump (#1918) 2024-08-09 18:50:21 -07:00
Guyute
873c25bbc5
fix: Migrates RunebookGump to DynamicGump (#1915) 2024-08-09 18:39:36 -07:00
Kamron Batman
0ce58deefb
fix: Fixes seed issues. (#1914) 2024-08-08 22:31:39 -07:00
Kamron Batman
9b7fea2c20
fix: Magic NPCs now use spell range (#1870)
### Summary
- Drastically streamlines the spell targeting by collapsing the target classes into a single `SpellTarget<T>` class.
- Moves TargetRange to the spell itself so it can be used by MageAI.
- Changes range check in MageAI to use TargetRange. This should fix target acquisition bugs
2024-08-08 07:05:27 -07:00
mdodkins
91e37fb8d4
fix: Hair and facial hair "teleporting" when mobile dies several times (#1901)
### Summary

* Added World.NewVirtual for creating virtual serial numbers
* Reserved range 0x7EEEEEEE to 0x7FFFFFFF for virtual serials
* Hair and Facial hair (for mobiles) now use virtual serials instead of FakeSerial() functions
* Consolidated virtual hair to a single `VirtualHairInfo` class.

Corpse hair and facial hair now persists across save/load and hair and facial hair no longer teleport to newest corpse.
2024-08-07 20:10:23 -07:00
Guyute
883d873c1c
fix: Converts ConfirmHouseResizeGump, ConfirmDryDockGump to StaticGump (#1909)
### Summary
- Converts ConfirmHouseResizeGump, ConfirmDryDockGump to StaticGump
- ConfirmHouseResizeGump now supports the account gold system.
2024-08-07 16:54:45 -07:00
Guyute
f5c980b28f
fix: Converts AddGump (for [add <searchString>) to DynamicGump (#1911) 2024-08-07 09:40:58 -07:00
Guyute
82c81b2adf
fix: Converts ReportMurdererGump to static Gump (#1910) 2024-08-07 09:12:13 -07:00
Guyute
9014f3d4dc
fix: Converts ResurrectGump to Static/Dynamic Gumps (#1904) 2024-08-06 19:17:33 -07:00
Kamron Batman
808e721e3d
fix: Fixes timer remaining count (#1908) 2024-08-06 16:25:21 -07:00
Kamron Batman
28860b7f53
fix: Fixes timer index offset (#1907)
### Summary

- Fixes timer intervals not continuing
- Fixes `Timer.Index` being off by 1. Should start at 0 for the first OnTick
- Simplifies Gift of Renewal end check
- Fixes force of nature not applying at the proper time and simplifies the timer logic.
2024-08-06 15:46:40 -07:00
Kamron Batman
2c9b3ef112
fix: Fixes timers detaching if they are stopped/started in OnTick (#1906) 2024-08-05 22:00:48 -07:00
Kamron Batman
6a476e6254
fix: Deletes Account serialization extensions since they are incompatible. (#1903) 2024-08-05 14:52:10 -07:00
Kamron Batman
3e70d073ad
fix: Fixes CloseGump returning true when no gump is closed. (#1902) 2024-08-05 12:28:24 -07:00
Kamron Batman
5d9d1a2118
fix: Fixes PooledRefList ToList returning wrong size (#1899)
### Summary

- Fixed a bug caused by a bad assumption. If `m_List[index]` is sparse and null values are casted, the server does not crash.
- Fixed a bug where `PooledRefList.ToList` extension method returned the wrong list size.
2024-08-05 08:50:56 -07:00
Kamron Batman
a6a647551f
fix: Updates polymorph gump to use StaticGump (#1898) 2024-08-04 16:35:11 -07:00
Kamron Batman
8f39af9579
fix: Fixes Timer.Pause not running continuation (#1893) 2024-08-04 15:40:54 -07:00
Kamron Batman
4d47b3849f
fix: Changes IPAddress comparison to use UInt128 (#1897) 2024-08-04 09:32:41 -07:00
Reetus
83db1f91f6
fix: CrimsonCinture Add missing LabelNumber (#1895) 2024-08-04 07:40:42 -07:00
Reetus
94c00ce714
fix: Add missing 'AddLocalized' for summong Talisman (#1894) 2024-08-04 07:39:32 -07:00
Kamron Batman
2eac11f535
fix: Reverts timer change in Detach (#1892) 2024-07-28 15:55:05 -07:00
dependabot[bot]
07d6e6b491
chore(deps): Bump Serilog from 4.0.0 to 4.0.1 (#1889) 2024-07-28 14:49:45 -07:00
Kamron Batman
2d9872b53a
fix: Fixes timers not stopping before OnTick (#1890) 2024-07-28 14:35:41 -07:00
Kamron Batman
4610712205
fix: Fixes respawn command when spawner is off (#1891) 2024-07-28 14:34:23 -07:00
Mink80
4a83b651f4
feat: Loads BaseHouse.DecayEnabled from modernuo.json (default to true) (#1888) 2024-07-24 09:32:03 -07:00
Kamron Batman
fd067febd7
fix: Fixes exit threads preventing reboots (#1886) 2024-07-23 20:57:13 -07:00
Kamron Batman
bbcf5da7d1
fix: Fixes professions indexing (#1885) 2024-07-23 20:45:34 -07:00
Kamron Batman
7dfb45d4eb
fix: Removes wrong error message from Fireflies (#1884) 2024-07-23 20:31:02 -07:00
Kamron Batman
2ff51dbeb6
fix: Fixes quest messages so they are all clilocs (#1883) 2024-07-22 20:44:32 -07:00
Kamron Batman
0a07109cc1
fix: Fixes invalid professions. (#1882) 2024-07-22 20:15:24 -07:00
Kamron Batman
e41d35d3d7
feat: Adds Pre-Pub 81 (UOML) Force of Nature (#1842) 2024-07-21 21:53:58 -07:00
Kamron Batman
4d030950a1
fix: Fixes camps so they properly respawn (#1881) 2024-07-21 21:50:00 -07:00
Kamron Batman
f58117a877
fix: Moves ContextMenu out of core, streamlines code, fixes bugs (#1873)
## Summary
- Removes allocation of a `List<ContextMenuEntry>` every time a context menu is created.
- Moves packet/context menu creation logic out of the core
- Fixes tame entry

## BREAKING CHANGE
> [!Important]
> **Developer Note**
> ```cs
> public virtual void GetContextMenuEntries(Mobile from, List<ContextMenuEntry> list)
> ```
> and similar functions changed to
> ```cs
> public virtual void GetContextMenuEntries(Mobile from, ref PooledRefList<ContextMenuEntry> list)
> ```
2024-07-20 21:33:23 -07:00
dependabot[bot]
bb24b330e1
chore(deps): Bump MailKit from 4.7.1 to 4.7.1.1 (#1872) 2024-07-20 18:04:18 -07:00
Kamron Batman
b0ae52b4e2
fix(Tests): Fixes a test run in parallel that crashes. (#1879) 2024-07-20 17:55:43 -07:00
Kamron Batman
428ae46bf4
fix: Fixes boat turning abandoning entities (#1878) 2024-07-20 17:51:29 -07:00
Kamron Batman
b4611ab7eb
fix: Fixes link list not crashing when broken. (#1877) 2024-07-20 17:38:22 -07:00
Kamron Batman
3cb688a2fb
fix: Fixes commodity resource property hue (#1875)
### Summary

- Fixes resource commodities so the hue changes when setting the resource
- Fixes some commodities not being able to be modified in-game.
2024-07-20 15:37:16 -07:00
Mink80
eed11be012
fix(Flax.cs): Makes flax flippable and fixes stacking (#1874) 2024-07-19 15:21:44 -07:00
Kamron Batman
db31c07017
fix: Updates xunit to 2.9, runner to 2.8.2, and mailkit to 4.7.1 (#1871) 2024-07-12 19:59:33 -07:00
Kamron Batman
8ac9ec27e4
fix: Fixes boat component locations when moving (#1868) 2024-07-09 19:34:57 -07:00
Kamron Batman
21836cf62f
fix: Fixes massive capacity bug with pooled ref queue/list (#1865) 2024-07-07 16:18:56 -07:00
Kamron Batman
96de1fdaa1
fix: Fixes NPE in PooledRefList sort (#1864) 2024-07-07 12:09:13 -07:00
Kamron Batman
20855ddff3
fix: Fixes strength potion candrink check (#1863) 2024-07-06 16:49:23 -07:00
Kamron Batman
38731d98b6
fix: Fixes splitting potions before error (#1862)
### Summary

Most potions have various checks, such as a cooldown. This fixes an edge case where potions get split from their stack before erroring.
2024-07-06 16:39:47 -07:00
Kamron Batman
e6bfc45228
fix: Consolidates and fixes movement/direction checks (#1861) 2024-07-06 12:41:45 -07:00
Kamron Batman
06e05f2e52
fix: Fixes random skill maximum (#1860) 2024-07-06 12:37:33 -07:00
Kamron Batman
c914d3d722
chore: Bumps MailKit to 4.7.0 & Cleans up bad code formatting (#1857) 2024-07-04 19:55:14 -07:00
Marcelo Paez Sequeira
0374b74f67
fix: Correctly removing active meditation buff when at peace (#1856) 2024-07-04 13:28:18 -07:00
Kamron Batman
70b2e9820d
fix: Fixes compile issue from TithingGump (#1855) 2024-07-04 11:56:57 -07:00
Kamron Batman
ed0ce4a292
fix: Converts ConfirmRelease, Tithing, Young gumps to static (#1854) 2024-07-04 11:53:02 -07:00
Kamron Batman
8b1014f395
fix: Cleans up HonorGump and Honor checks (#1853)
### Summary
- Moves `HonorSelf` gump to the Virtues folder.
- Renames `HonorSelf` to `HonorSelfGump`
- Makes `HonorSelfGump` a static gump.
- Changes the text in the gump to a cliloc.
- Collapses some of the checks for honoring to simplify the logic.
- Moves the player check higher to fix the wrong error message displaying when trying to honor damaged players.
2024-07-04 11:23:42 -07:00
Kamron Batman
ac3e4fcc97
Fixes more potion throwing edge cases (#1852) 2024-07-03 18:04:03 -07:00
Kamron Batman
cbaa349a87
fix: Streamlines potion drink effect (#1851) 2024-07-03 16:29:05 -07:00
Kamron Batman
d40041d631
chore: Adds Quodana for code quality (#1849) 2024-06-26 08:23:49 -07:00
Kamron Batman
5d18a194b8
fix: Deletes unused serialization files (#1848) 2024-06-26 07:57:00 -07:00
Kamron Batman
46c7c77b41
fix: Adds random bard skill option (#1847) 2024-06-23 16:58:23 -07:00
Kamron Batman
16de17e8a6
fix: Fixes random skills and groupings (#1846) 2024-06-23 16:49:17 -07:00
Kamron Batman
8ec203f387
feat: Updates serialization to use MMF (considerable memory savings) (#1841)
### Summary
Updates the serialization strategy to use `MemoryMappedFile` instead of thick buffers. This has the benefit of being on-par with the current implementation (based on hardware/OS), however won't incur the double-memory issue.

> [!Important]
> **Developer Note**
> The `BinaryFileWriter` and `BinaryFileReader` has been removed in favor of `MemoryMapFileWriter` and `UnmanagedDataReader`
2024-06-23 10:51:20 -07:00
Kamron Batman
3b84c8052c
fix: Fixes craft/help gump not appearing (#1845) 2024-06-23 09:54:04 -07:00
Kamron Batman
b8ad5c671d
fix: Fixes double calls with Target cancel and spell sequences (#1840)
### Summary
- Cleans up target cancellation being called incorrectly.
- An invalid target type (which should never happen), now calls `OnTargetUntargetable` instead of `OnTargetCanceled`
- Removes double calls to `FinishSequence` in Spells.
2024-06-17 15:19:06 -07:00
Kamron Batman
9c0b57213a
fix: Converts PageResponse/Prompt gumps (#1838) 2024-06-16 21:16:15 -07:00
Kamron Batman
c302ff23bc
fix: Updates help gump, converts to dynamic gump (#1837)
### Summary
- Updates help gump to use clilocs
- Updates help gump to use new DynamicGump API


> [!Note]
> The help gump needs to be overhauled. The current one is not accurate to OSI, and we should not follow OSI for this feature.
2024-06-16 17:04:26 -07:00
Kamron Batman
49824592eb
fix: Converts CraftGump to DynamicGump API (#1836) 2024-06-16 12:06:48 -07:00
Kamron Batman
221312be4c
fix: Stages faction code for serialization conversion (#1835) 2024-06-16 11:53:15 -07:00
Kamron Batman
20596e52fa
fix: Converts ethics system to generic entity persistence (#1824)
### Summary

Converts ethics system to entity persistence and removes the persistence item.


### TODO

1. The enable/disable toggle doesn't propagate to all code that does Ethics checks (notoriety, pvp, etc).
2. We need commands to remove them from an ethic.
2024-06-16 10:26:09 -07:00
Kamron Batman
f87c8f73ab
fix: Bumps PollGroup to 1.5.1 to fix edge case with sequential struct layout (#1834) 2024-06-16 10:24:41 -07:00
Kamron Batman
b745ee1e7a
fix: Fixes skills property not editable in-game (#1833) 2024-06-13 22:00:02 -07:00
Kamron Batman
19f65dcabe
fix: Fixes edge cases with on off items (#1832)
### Summary
- Generalizes the On/Off toggle items concept
- Updates the OnOffGump so it is static
- Standardizes OnOff items so they can be used by staff


### Notes
Decided to not fix #1417 because it is not clear that the clilocs or errors are for that purpose. Can't test this on OSI anyways.
2024-06-10 18:11:35 -07:00
dependabot[bot]
f756cce0c7
chore(deps): Bump Serilog.Sinks.Console from 5.0.1 to 6.0.0 (#1831)
Bumps [Serilog.Sinks.Console](https://github.com/serilog/serilog-sinks-console) from 5.0.1 to 6.0.0.
- [Release notes](https://github.com/serilog/serilog-sinks-console/releases)
- [Commits](https://github.com/serilog/serilog-sinks-console/compare/v5.0.1...v6.0.0)

---
updated-dependencies:
- dependency-name: Serilog.Sinks.Console
  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>
2024-06-10 15:37:26 -07:00
Kamron Batman
cf4c8a9b71
fix: Creates new staff properly on creation (#1830) 2024-06-09 12:26:22 -07:00
Daniel Dias Rodrigues
9847efbb1a
fix: refactor GoGump, moves LocationTree to GoLocations (#1804) 2024-06-08 19:59:31 -07:00
Daniel Dias Rodrigues
75a3f0a92c
fix: Adds 'Do everything' option to AdminGump (#1817) 2024-06-08 19:05:34 -07:00
Kamron Batman
6f444488a5
fix: Fixes crashing due to bad packet assumptions. (#1829)
### Summary
- Fixes various exploits that can crash the shard when the client misbehaves.
- Clients will now be disconnected if they send packets that are marked as out of game only (new flag), while they are in-game.


> [!Note]
> **Developer Note**
> Added an `OutOfGameOnly` which should be used to flag packets as only available out of the game.
> This is the opposite of, yet not the converse to `InGameOnly`.
2024-06-07 18:08:07 -07:00
dependabot[bot]
2429b00638
chore(deps): Bump Serilog.Sinks.Async from 1.5.0 to 2.0.0 (#1828)
Bumps [Serilog.Sinks.Async](https://github.com/serilog/serilog-sinks-async) from 1.5.0 to 2.0.0.
- [Release notes](https://github.com/serilog/serilog-sinks-async/releases)
- [Commits](https://github.com/serilog/serilog-sinks-async/compare/v1.5.0...v2.0.0)

---
updated-dependencies:
- dependency-name: Serilog.Sinks.Async
  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>
2024-06-07 17:09:37 -07:00
Kamron Batman
1f8ab0e783
fix: Console Input is disabled on Linux when no standard input is attached. (#1809) 2024-06-06 21:20:22 -07:00
Marcelo Paez Sequeira
832bb24f11
fix: Fixes ProximitySpawner and RegionSpawner ToJson (#1827) 2024-06-06 16:31:48 -07:00
Kamron Batman
5a65a6b8ea
fix: Fixes exporting spawners. (#1826) 2024-06-06 14:19:26 -07:00
Kamron Batman
99e33d0463
fix: Fixes character creation issues. Adds Royal City (#1825)
### Summary

- Removes new player "Haven Only" starting city.
- Fixed New/Old haven placement.
- Removed force-profession starting location for SA+.
- Added Royal City for SA+
- Removed Occlo for Pre-AOS.
2024-06-04 20:58:30 -07:00
dependabot[bot]
4a0c35b408
chore(deps): Bump Serilog from 3.1.1 to 4.0.0 (#1822)
Bumps [Serilog](https://github.com/serilog/serilog) from 3.1.1 to 4.0.0.
- [Release notes](https://github.com/serilog/serilog/releases)
- [Commits](https://github.com/serilog/serilog/compare/v3.1.1...v4.0.0)

---
updated-dependencies:
- dependency-name: Serilog
  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>
2024-06-03 16:15:33 -07:00
dependabot[bot]
13cace1a2d
chore(deps): Bump Nerdbank.GitVersioning from 3.6.133 to 3.6.139 (#1821)
Bumps [Nerdbank.GitVersioning](https://github.com/dotnet/Nerdbank.GitVersioning) from 3.6.133 to 3.6.139.
- [Release notes](https://github.com/dotnet/Nerdbank.GitVersioning/releases)
- [Commits](https://github.com/dotnet/Nerdbank.GitVersioning/commits)

---
updated-dependencies:
- dependency-name: Nerdbank.GitVersioning
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-06-03 16:11:24 -07:00
Kamron Batman
0b802dbe1b
fix: Fixes duping containers and removes copying private setter properties (#1816)
### Summary
- Removes copying private setters
- Fixes duping containers
- Adds public `Dupe.DoDupe` functions for external scripts to hook into the existing logic.
2024-06-03 15:44:56 -07:00
Daniel Dias Rodrigues
c69d16a90e
fix: Fixes parsing booleans in commands. Adds ClearAll command (#1818) 2024-06-03 15:42:52 -07:00
Kamron Batman
ad26f0a1cb
chore(build): Updates supported linux operating systems (#1820)
### Summary
- Removes CentOS 7/8
- Removes RedHat 7
- Removes Fedora 37/38
- Adds Fedora 40
- Bumps minimum .NET to 8.0.6 (8.0.301)
2024-06-03 15:40:49 -07:00
Kamron Batman
db6e852c91
fix: Fixes reading incoming string packets (#1819) 2024-06-03 14:49:26 -07:00
Kamron Batman
04a15e9620
fix: Adds missing dupe ignore (#1815) 2024-06-02 15:09:35 -07:00
Kamron Batman
df1db05054
Use github generated release notes instead (#1810) 2024-06-02 15:05:09 -07:00
Kamron Batman
9c7cb5d778
fix: Fixes dupe property copying. Adds IgnoreDupe (#1811)
## Summary

### Changes
- Adds `[IgnoreDupe]` and `[SerializedIgnoreDupe]`
- Updates all _known_ classes that need the attribute. Some might be missing, please helps us find them!
- Adds `Item.Dupe()` command and encapsulates `CopyProperties` and `OnAfterDuped`. This is also overridable.
- Updates Dupe command to use the new logic.
- Fixes duping multiple kinds of objects that used to be outright broken.

### Bug Fixes
- Fixes issue with durability after duping
- Fixes issue with hue after duping

> [!Note]
> **Developer Note**
> Customizing how duping an item works now requires two steps:
> 1. Add `[IgnoreDupe]` or `[SerializedIgnoreDupe]` to the property/field
> 2. Add custom logic in an `OnAfterDuped` override
>
> When do you need to do this?
> *When the property being copied is not a primitive, and you need to manually deep-clone the contents of the property such as with Lists, Dictionaries, or sub classes.*
2024-06-02 15:04:54 -07:00
Kamron Batman
a8d3d2773e
fix: Fixes libdeflate threading issue (#1813) 2024-06-02 15:02:47 -07:00
Kamron Batman
aeba2261d2
fix: Fixes reading multiple strings (books) (#1814) 2024-06-02 15:01:18 -07:00
Kamron Batman
5f8f7982eb
fix: Fixes publishing (#1808)
### Summary
- Fixes publishing issues when the Application project was created.
- Fixes missing dotnet clean file deletions
2024-05-31 12:59:59 -07:00
Kamron Batman
b2695b9f6c
chore(build): Fixes release workflow (#1807) 2024-05-30 23:38:47 -07:00
Kamron Batman
60a3a6f501
feat: Splits Server in Core and Application (#1806)
> [!Note]
> **Developer Note**
> Now developers will only need to build/run the Application project instead of everything.
> When adding new projects, make sure to: 
> 1. Add a reference to that project in the Application project.
> 2. Add the dll file to the Distribution/Data/assemblies.json file

### Summary
- Adds an application project
- Consolidates process restarts to use `Core.Kill(true)`
- Removes some old messaging, for example processor optimization
- Fixes missing build cleanup
2024-05-30 23:34:03 -07:00
Daniel Dias Rodrigues
e85c56ec2d
fix: Fix ConsoleInput for Linux users with try/catch. (#1803) 2024-05-30 14:03:28 -07:00
Daniel Dias Rodrigues
84c68bf73c
fix: Fixes publish on Linux with non-bash terminals (#1802) 2024-05-29 14:37:09 -07:00
Kamron Batman
96a398784f
fix: Fixes incorrect interpolation in legacy gumps (#1800) 2024-05-28 18:38:51 -07:00
Kamron Batman
b3a817ee2d
fix: Fixes relesae flows (#1799) 2024-05-28 15:24:03 -07:00
Kamron Batman
5843ec0784
chore: Updates release flow (#1798) 2024-05-28 15:00:38 -07:00
Kamron Batman
a4522b9d43
fix: Fixes stalled connections and infinite throttle (#1796)
> [!Warning]
> **Developer Warning**
> The `PacketThrottle` callback return value is now reversed. `true` indicates the connection is _throttled_.

### Summary
- Fixes an issue where connections get stalled forever
- Fixes an issue where the throttler is not working properly
- Removes account attack limiter
- Rewrites IP limiter
- Removes IP restrictions (they weren't used, and not practical)
- Fixes issue where IP limiter was counting before firewall was blocking.

View without whitespace:
https://github.com/modernuo/ModernUO/pull/1796/files?diff=split&w=1
2024-05-27 21:32:55 -07:00
Kamron Batman
ccad915464
fix: Adds more NetState dumping for debug (#1795) 2024-05-27 12:10:27 -07:00
Kamron Batman
21c5e46565
fix: Fixes libdeflate gc pin issue (#1794) 2024-05-25 18:11:46 -07:00
Kamron Batman
21347c8b6b
fix: Fixes binary file writer position increment (#1793) 2024-05-24 16:59:24 -07:00
Kamron Batman
9f1f314077
fix: Fixes infinite loop in BinaryFileWriter (#1792)
### Summary
- Fixes infinite loop with binary file writer
- Removes extra buffer copying with binary file writer
- Removes storing type counts during world save file writing
- Fixes display cache self-deletion warning during world load
2024-05-24 14:36:43 -07:00
Kamron Batman
0011db7f47
fix: Fixes house update range bug (#1790) 2024-05-23 16:07:33 -07:00
Kamron Batman
6596046216
fix: Bumps PollGroup to .Net 8 (#1789) 2024-05-22 18:17:20 -07:00
dependabot[bot]
2a9e24ee5b
chore(deps): Bump Microsoft.NET.Test.Sdk from 17.9.0 to 17.10.0 (#1788)
Bumps [Microsoft.NET.Test.Sdk](https://github.com/microsoft/vstest) from 17.9.0 to 17.10.0.
- [Release notes](https://github.com/microsoft/vstest/releases)
- [Changelog](https://github.com/microsoft/vstest/blob/main/docs/releases.md)
- [Commits](https://github.com/microsoft/vstest/compare/v17.9.0...v17.10.0)

---
updated-dependencies:
- dependency-name: Microsoft.NET.Test.Sdk
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-05-22 17:57:47 -07:00
Kamron Batman
a552cf4138
fix: Fixes various memory leaks and spells (#1786)
### Summary
- Fixes various memory leaks related to spells
- Fixes Spell Plague
- Added ability to determine if `sdi` should take effect for Spell Damage.
- Fixes animal form timer ticking non-stop while logged out.
2024-05-21 19:44:42 -07:00
Mink80
6747168db0
fix: Fixes NPE in BuffTable and standardizes BuffIcon packets (#1785) 2024-05-21 14:43:55 -07:00
Stefano Merotta
50d3779d9b
fix: Adds dispose call to LibDeflateBinding on process exits (#1784) 2024-05-21 13:40:03 -07:00
Marcelo Paez Sequeira
00908d030f
fix: Moves many EventSinks out of core. (#1783) 2024-05-21 10:02:30 -07:00
Marcelo Paez Sequeira
1cb798d807
fix: Removed StunRequest and DisarmRequest EventSinks (#1781) 2024-05-19 14:12:58 -07:00
Reetus
f2a33706ab
fix: Fixes RecipeScroll recipe name OPL (#1780) 2024-05-19 00:12:12 -07:00
dependabot[bot]
8054b29e5c
chore(deps): Bump MailKit from 4.5.0 to 4.6.0 (#1775)
Bumps [MailKit](https://github.com/jstedfast/MailKit) from 4.5.0 to 4.6.0.
- [Changelog](https://github.com/jstedfast/MailKit/blob/master/ReleaseNotes.md)
- [Commits](https://github.com/jstedfast/MailKit/commits)

---
updated-dependencies:
- dependency-name: MailKit
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-05-18 10:24:40 -07:00
Kamron Batman
173b2d5834
fix: Fixes WeaponAbility GetTactics override (#1778) 2024-05-18 10:24:29 -07:00
Kamron Batman
416c96d8cd
fix: Fixes spelling of HasManaOverride (#1777) 2024-05-17 21:40:43 -07:00
Kamron Batman
2d5ef2fad9
fix: Removes weapon ability context (#1776)
### Summary
- Removes weapon ability context
- Renames context related functions to Cooldown

Note: View with [whitespace off](https://github.com/modernuo/ModernUO/pull/1776/files?diff=split&w=1).
2024-05-17 21:30:55 -07:00
Kamron Batman
1f701e7b55
feat: Replaces Zlib with LibDeflate (#1774)
> [!Warning]
> Users on Linux/OSX will need to follow the Readme
> and make sure `libdeflate` is properly installed

> [!Note]
> **Developer Note**
> The API for compression has changed. Use `Deflate.Standard` for the same functionality.

### Summary
* Replaces Zlib with LibDeflate for a 50% performance improvement!
* Adds MacOS 14 to properly test Arm64
2024-05-16 22:53:01 -07:00
Kamron Batman
35a292d2c7
fix: Fixes NPCs attacking their own summons (#1773) 2024-05-11 16:32:55 -07:00
Kamron Batman
d6c87a4da4
fix: Fixes infinite loop in gump builders (#1772)
### Summary
- Fixes infinite loop in gump builders
- Removes allocations for centering/coloring html in builders
2024-05-11 11:13:57 -07:00
Kamron Batman
622250b8f4
fix: Fixes issue with cached static gump strings. Fixes bad gump colors (#1771)
### Summary
- Fixes an issue that causes CUO to crash due to bad string caching in static gumps
- Fixes wrong/bad 16bit html gump hues.
- Moves `C16232` (16-bit to 32-bit) and `C32216` (32-bit to 16-bit) to Utility class for broader use.

TODO:
- Some gumps have different 32bit (for string content) vs 16bit (for localized content) strings. Does this matter?
2024-05-10 22:44:16 -07:00
Kamron Batman
a001eb03da
fix: Fixes silver serialization (#1770) 2024-05-09 17:17:50 -07:00
Kamron Batman
39ba7b3fd4
fix: Fixes dirty tracking for player vendors (#1769) 2024-05-09 16:52:13 -07:00
Kamron Batman
aec75d0810
fix: Fixes GetInRange to use GetAt instead (#1768) 2024-05-09 14:01:25 -07:00
Kamron Batman
fb8563aa60
fix: Codegens house/vendors (#1767)
### Summary
- Cleans up ShardPoller
- Breaks up player vendor file
- Codegens houses/vendors
2024-05-08 22:45:59 -07:00
Kamron Batman
8fd04ac5aa
fix: Codegens gifts. Unifies staff names. Thank you to the community! (#1766)
### Summary
* Adds contributors/sponsors to the "staff" list for holiday items.
* Unifies all of the staff lists
* Moves 2004 winter gifts to the appropriate folder
* Codegens the gift items

## Thank you!
Thank you to the RunUO/ServUO community, Owyn, Jaedan, the Outlands community, and the ModernUO contributors/community. Without all of you, this would not be possible! ❤️
2024-05-08 20:44:00 -07:00
Kamron Batman
8efa90b3ce
fix: Codegens spell items (#1765)
### Summary
- Fixes some serialization issues with spell items
- Fixes accessibility issues with field spells and removes the awkward "InternalItem" concept
- Codegens the fields/items used in spells
2024-05-08 19:48:39 -07:00
Kamron Batman
542b672d9d
fix: Codegens faction items and questers (#1764)
### Summary
- Codegens some ConPVP items
- Codegens some faction items/mobs
- Codegens questers
2024-05-08 17:16:36 -07:00
Kamron Batman
26dfde19ee
fix: Consolidates Color/Center html (#1762)
### Summary
- Fixes bad color in virtual check gump
- Consolidates the Color/Center html strings for all gumps
2024-05-07 23:56:32 -07:00
Kamron Batman
05535ec9d1
fix: Fixes continuing after deserialization error (#1761) 2024-05-07 16:56:32 -07:00
Kamron Batman
328c6daa60
fix: Fixes spawner deserialization (#1760) 2024-05-05 13:10:42 -07:00
Kamron Batman
d77668d77a
fix: Cleans up some string allocations from trim (#1759) 2024-05-03 22:43:11 -07:00
Kamron Batman
9d2f471e91
fix: Fixes HtmlLocalized color for gump layouts (#1758) 2024-05-03 21:42:52 -07:00
Kamron Batman
becd7aad05
fix: Cleans up FixHtml (#1757) 2024-05-03 21:24:31 -07:00
Bohica
af67c48621
fix: Fixes EvilMage/Lord spawning naked, without hair (#1755) 2024-05-03 18:27:16 -07:00
Kamron Batman
4ea1d79cad
fix: Removes broken OrderedHashSet and adds a simple OrderedSet (#1756)
> [!CAUTION]
> **BREAKING CHANGE**
> Removed `OrderdHashSet` and `PooledOrderedHashSet` due to bugs.

> [!NOTE]
> **Developer Note**
> The OrderedSet is not a full data structure. It is not particularly efficient. Pull Requests are welcome for a better implementation, especially if it ends up supporting `ISet<T>` and `IReadOnlySet<T>`

## Summary

The ordered hash set was buggy. It's kind of painful to implement, so for now, I added a simple `OrderedSet` to suffice for gumps. Please reach out if this causes disruption!
2024-05-03 18:03:26 -07:00
kaczy93
dc118d836f
feat: Cleans up UOP file handling. Adds automatic bounds.bin generation (#1744)
### Summary
* Unifies reading UOP File indexes
* Adds ArtData file reader to get graphic bounds
* Automatically generates Bounds.bin from art file if missing
* Adds [GenBounds command to regenerate the bounds.bin file. Useful for when the art file changes.
2024-05-01 20:02:06 -07:00
Kamron Batman
db0621b884
fix: Fixes NPE with stun attack (#1751) 2024-04-30 19:20:06 -07:00
dependabot[bot]
347ac28ffe
chore(deps): Bump xunit.runner.visualstudio from 2.5.8 to 2.8.0 (#1750)
Bumps [xunit.runner.visualstudio](https://github.com/xunit/visualstudio.xunit) from 2.5.8 to 2.8.0.
- [Release notes](https://github.com/xunit/visualstudio.xunit/releases)
- [Commits](https://github.com/xunit/visualstudio.xunit/compare/2.5.8...2.8.0)

---
updated-dependencies:
- dependency-name: xunit.runner.visualstudio
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-04-29 17:54:12 -07:00
dependabot[bot]
67cb15a193
chore(deps): Bump xunit from 2.7.1 to 2.8.0 (#1749)
Bumps [xunit](https://github.com/xunit/xunit) from 2.7.1 to 2.8.0.
- [Commits](https://github.com/xunit/xunit/compare/2.7.1...2.8.0)

---
updated-dependencies:
- dependency-name: xunit
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-04-29 17:41:30 -07:00
Bohica
248af59e62
fix: Update Nails.cs with correct graphic (#1746) 2024-04-28 23:54:04 -07:00
Kamron Batman
d23d92d278
fix: Fixes reading prof.txt (#1745)
### Summary
* Fixes reading the professions file.
2024-04-27 19:10:37 -07:00
Kamron Batman
c4fbc6b88e
fix: Adds text command to throttle. Fixes throttle itself. (#1743) 2024-04-26 17:54:33 -07:00
Kamron Batman
bb7bc57e42
fix: Adds static warning/notice gumps. (#1741)
### Summary
* Adds `StaticNoticeGump<T>` and `StaticWarningGump<T>`
* Converts NoticeGump/WarningGump to use `DynamicGump`
* Changes various uses of notice gump and warning gump to their static counterpart.
2024-04-26 17:19:16 -07:00
Kamron Batman
65532ea887
feat: Adds optimized dynamic/static layout gumps (#1652)
# New Gump API
We are pleased to release a new API that is faster, allocates nearly zero memory, and still feels very similar to the original API. The API is broken into 3 types of gumps, dynamic, static with placeholders, and static without placeholders. 

### Dynamic Gumps
These gumps will inherit `DynamicGump` and are meant for gumps that have a dynamic layout. This includes specifying dynamic arguments to HtmlLocalized entries.

## Static Gumps 
Static gumps are those where the function to the build the layout is called only once and cached forever. They can optionally have placeholders. These placeholders allow the developer to specify the string values later, dynamically in a `BuildStrings` method on the gump. If a gump does not have any placeholders, the string entries will also be cached forever.

## Benchmarks
To make sure we were going in the right direction and not wasting time, we took copious benchmarks. Here are the final benchmarks for a really simple gump. 

Note:
* The majority of creating a gump is compressing the layout and the strings. Compressing each section takes ~6,000ns (12us total).

```cs
| Method                         | Mean         | Error        | StdDev       | Median       | Ratio | RatioSD | Gen0   | Allocated | Alloc Ratio |
|------------------------------- |-------------:|-------------:|-------------:|-------------:|------:|--------:|-------:|----------:|------------:|
| OldGump                        | 13,308.29 ns | 1,059.695 ns | 1,883.608 ns | 14,330.72 ns | 1.000 |    0.00 | 0.1526 |    2400 B |        1.00 |
| DynamicLayoutGump              | 13,357.86 ns |   129.144 ns |   226.185 ns | 13,323.60 ns | 1.029 |    0.17 |      - |      48 B |        0.02 |
| StaticLayoutDynamicStringsGump |  6,653.10 ns |    81.815 ns |   143.292 ns |  6,617.45 ns | 0.514 |    0.09 |      - |      40 B |        0.02 |
| StaticLayoutGump               |     86.33 ns |     0.760 ns |     1.350 ns |     86.07 ns | 0.007 |    0.00 | 0.0020 |      32 B |        0.01 |
```

# Non-Breaking Changes
* All gump components in the core have been moved to `Gumps/Legacy`.
* All legacy gumps will still inherit `Gump`, which now inherits `BaseGump`

# Special Thanks

Thank you to @stefanomerotta for considerable contributions/benchmarking/testing to make this effort a reality! We collectively went through over 10 iterations, but it is finally ready.
2024-04-25 22:40:30 -07:00
Kamron Batman
1c10b6dbed
fix: Adds support for ROS to HashUtility (#1740) 2024-04-25 00:42:10 -07:00
Kamron Batman
9cd84ba3d6
fix: Removes support for v4 Client and old gump packet. (#1739)
### Summary
* Removes old gump packet support
* Removes support for v4 clients
* Removes `Unpack` flag and assumes it is always true.
* Removes StringToBuffer since this is built into .NET now.

> [!Note]
> View the file changes with white space off: https://github.com/modernuo/ModernUO/pull/1739/files?diff=split&w=1
2024-04-24 19:39:37 -07:00
Kamron Batman
98d31bc707
fix: Fixes crafting items with IHasQuality attribute (#1738) 2024-04-23 23:33:29 -07:00
Kamron Batman
7a160bb0fd
fix: Fixes STArray size and tests (#1737) 2024-04-23 16:18:24 -07:00
Kamron Batman
3a27ab8810
fix: Improves STArray and SpanWriter with packets (#1736) 2024-04-21 20:44:25 -07:00
Kamron Batman
44df304575
fix: Archive locally is not an async function (#1735) 2024-04-21 08:08:12 -07:00
dependabot[bot]
72261a6b9c
chore(deps): Bump MailKit from 4.4.0 to 4.5.0 (#1732)
Bumps [MailKit](https://github.com/jstedfast/MailKit) from 4.4.0 to 4.5.0.
- [Changelog](https://github.com/jstedfast/MailKit/blob/master/ReleaseNotes.md)
- [Commits](https://github.com/jstedfast/MailKit/compare/4.4.0...4.5.0)

---
updated-dependencies:
- dependency-name: MailKit
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-04-15 18:31:16 -07:00
Kamron Batman
ada2378e33
fix: Fixes champion drop logic (#1731)
### Summary
- Fixes which maps can drop SoT and PS
- Fixes the percentage chance that SoT/PS can drop
2024-04-14 16:40:09 -07:00
dependabot[bot]
4140209df2
chore(deps): Bump xunit from 2.7.0 to 2.7.1 (#1730)
Bumps [xunit](https://github.com/xunit/xunit) from 2.7.0 to 2.7.1.
- [Commits](https://github.com/xunit/xunit/compare/2.7.0...2.7.1)

---
updated-dependencies:
- dependency-name: xunit
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-04-12 17:33:16 -07:00
dependabot[bot]
cbb25dfe62
chore(deps): Bump xunit.runner.visualstudio from 2.5.7 to 2.5.8 (#1729)
Bumps [xunit.runner.visualstudio](https://github.com/xunit/visualstudio.xunit) from 2.5.7 to 2.5.8.
- [Release notes](https://github.com/xunit/visualstudio.xunit/releases)
- [Commits](https://github.com/xunit/visualstudio.xunit/compare/2.5.7...2.5.8)

---
updated-dependencies:
- dependency-name: xunit.runner.visualstudio
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-04-12 16:00:14 -07:00
Kamron Batman
3e3b08e666
fix: Fixes max items serializatio n issue (#1728)
### Summary

Fixes an issue where max items is deserialized as 0 instead of -1. To fix broken containers, run the following in-game:
`[global set maxitems -1 where container maxitems = 0`

In vanilla MUO, there are no containers that actually have max items set to 0.
2024-04-12 14:11:58 -07:00
srosellj
7ec19cf39a
fix: BaseEscortable Destination not returned properly. (#1727) 2024-04-08 09:12:38 -07:00
Kamron Batman
389d154576
Update BaseEscortable.cs (#1726) 2024-04-07 16:05:49 -07:00
Kamron Batman
d115716a60
fix: Fixes missing destinations crashing (#1724) 2024-04-07 15:48:09 -07:00
srosellj
6c07673a37
fix: Change Decorate FindItem range from 0 to 1 (#1725) 2024-04-07 14:56:39 -07:00
srosellj
d6fe50233a
fix: Fix unguarded/termur regions in regions.json (#1723) 2024-04-07 13:30:35 -07:00
Kamron Batman
ac562e67dc
fix: Adds back socket connected event (#1688)
### Summary
Adds back the `SocketConnected` event. This event only fires asynchronously! (TcpServer thread).

Example:
```cs

public static void Configure()
{
    TcpServer.EventSink.SocketConnected += OnSocketConnected;
}

// WARNING: Executed on the TcpServer thread!
private static void OnSocketConnected(TcpServer.SocketConnectedEventArgs args)
{
    if (... logic here...)
    {
        AdminFirewall.Add(((IPEndPoint)Socket.RemoteEndPoint)!.Address);
    }
}
```
2024-04-06 10:26:46 -07:00
Kamron Batman
90fa2e09de
fix: Fixes mount stamina, adds Pub46+ Ethereal stamina (#1716)
### Summary
* Makes mount stamina significantly faster.
* Reduces absolute reset from 24 hours to 4 hours.
* Adds Pub 46+ Ethereal stamina where the stamina is global to all mounts for the player
* Removes serialization of stamina entirely because it's ok if it gets reset during shard restarts.
2024-04-05 21:05:16 -07:00
Kamron Batman
c63bf84ad4
fix: Fixes bad localized messages (#1722)
### Summary
- Apparently affixes and other old packets do not support unicode, so we can't translate them. On OSI they use English for quite a bit.
2024-04-05 20:28:15 -07:00
Kamron Batman
6cb18f87fa
fix: Simplifies create food and localizes. Fixes localization fallback (#1721) 2024-04-05 19:45:42 -07:00
Kamron Batman
912eaab460
fix: Fixes unresponsive console input (#1720)
### Summary
Fixes an issue where the call priority of `Initialize` functions causes a dead lock because the console input handler was not initialized.
2024-04-05 15:07:43 -07:00
Kamron Batman
974062b0d4
feat: Adds leather/metal food, fixes weapons/armors with wrong materials (#1718)
### Summary
- Fixes the name `Vegies` to `Veggies`
- Adds feeding leather to goats
- Adds metal for Lava Lizards
- Fixes Bone Helm being classified as plate instead of bone.
- Fixes wooden shields not classified as wood.

### Note
On OSI, the preferred foods on the animal lore gump doesn't contain all possible favorite food categories. Furthermore, the one listed is not necessarily always the "first" in the list of possible categories. We aren't going to try and fix that since it isn't important.
2024-04-04 19:43:02 -07:00
Kamron Batman
29ea813242
fix: Cleans up code with feeding pets and adds batch coin flips random check (#1717) 2024-04-04 15:09:15 -07:00
Kamron Batman
183f6fa4ac
fix: Fixes stuck connections (#1708) 2024-04-03 08:14:41 -07:00
Kamron Batman
39215935cf
fix: Fixes amount error spam from scissors (#1715) 2024-03-30 09:52:40 -07:00
Kamron Batman
e638fb7893
feat: Adds console commands (#1714)
### Summary
* Adds console command support.
* Adds `save`, `restart`, and `shutdown` commands.
2024-03-30 09:40:43 -07:00
Kamron Batman
855f8c67ae
chore: Adds [AddonGen to commands page on modernuo.com (#1713) 2024-03-28 16:52:00 -07:00
Kamron Batman
1c5813d8f4
feat: Adds [AddonGen (#1712)
### Summary

* Adds Arya's addon generator.
* Updates it for MUO


### ToDos
* Update to use search values when .NET 9 is introduced
* Add ability to codegen basic items from data files (JSON?)
2024-03-28 16:50:27 -07:00
Kamron Batman
1a7e7c7c70
fix: Fixes spawner timer deserialization, decimal deserialization, and adds potion keg reverse lookup (#1711)
### Summary
* Fixes spawner timer deserialization
* Adds a check for a null timer and allows the timer to get recreated
* Adds PotionKeg reverse lookup
* Heavily optimizes decimal serialize/deserialize
2024-03-28 13:34:12 -07:00
Kamron Batman
70575e1517
fix: Fixes weapon and armor quality default value (#1710) 2024-03-28 10:30:42 -07:00
Kamron Batman
f3d1fb82d8
fix: Consolidates commodity interface for reagents. (#1709) 2024-03-21 11:31:14 -07:00
Kamron Batman
3332fabc39
fix: Reverts to RunUO speeds, fixes direction glitching, adds movement speed interpolation (#1695)
> [!IMPORTANT]  
> Please read through these changes, as they changed certain expectations for how the internal AI thinking/movement work.
> Note that some mobs still don't have smooth movement on ClassicUO due to how the client handles animations/movement.

### Summary
- **Important Change**: Mobs will now move at a more regular pace. If the OnThink results in a move, but the cooldown would otherwise prevent the move, then the movement is scheduled on another timer.
- Added a 400ms delay to mobs turning to face a player to attack in order to avoid glitching between moving and turning.
- Reverted AI thinking speeds back to RunUO specific speeds.
- Reverted the AI thinking to moving conversion delays back to RunUO.
- For thinking speeds that are not exactly the predefined speeds from RunUO, there is a new calculation to determine the correct conversion to movement speed. This stops speeds like `0.35` from being faster than `0.3`

> [!NOTE]  
> **Developer Note**
> BaseAI.CheckMove() no longer contains the check for whether or not a mob is on movement cooldown. This function can now be overwritten without causing issues to figuring out that cooldown.
2024-03-18 14:13:41 -07:00
Kamron Batman
9e37879f19
fix: Fixes iterating with multiple multis in a sector (#1706) 2024-03-10 11:35:57 -07:00
Kamron Batman
b5342a82e1
fix: Fixes language casing and adds localization reset command (#1703) 2024-03-09 11:02:56 -08:00
Stefano Merotta
1147a80b34
fix: Fixes missing reader seek for switches in gump response (#1700) 2024-03-09 09:00:32 -08:00
Stefano Merotta
76f132fd49
fix: Make RelayInfo zero allocation (#1699) 2024-03-08 22:58:32 -08:00
Kamron Batman
cfe9e04c78
fix: Optimizes gump relay info (Prep for Static Gumps) (#1698) 2024-03-07 20:54:53 -08:00
dependabot[bot]
50fcee12dc
chore(deps): Bump MailKit from 4.3.0 to 4.4.0 (#1694)
Bumps [MailKit](https://github.com/jstedfast/MailKit) from 4.3.0 to 4.4.0.
- [Changelog](https://github.com/jstedfast/MailKit/blob/master/ReleaseNotes.md)
- [Commits](https://github.com/jstedfast/MailKit/compare/4.3.0...4.4.0)

---
updated-dependencies:
- dependency-name: MailKit
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-03-04 21:10:10 -08:00
Kamron Batman
f50a94e5a7
fix: Fixes spawn entry migration (#1696) 2024-03-04 21:09:58 -08:00
Kamron Batman
c42c20aef9
fix: Fixes spawn entry deserialization (#1693) 2024-03-03 08:45:16 -08:00
Kamron Batman
7a71e8b340
fix: Fixes special scroll logic and display (#1691)
### Summary
- Fixes edge case calculations with values that aren't exact.
- Fixes skill text casing.
- Fixes cliloc error for stat cap scroll gump.
- Adds `AosSkillBonuses.GetLowercaseLabel()` for lower case localized skill names.
- Fixes initial value for constructing a stat scroll so it isn't negative.
- Fixes message for Scroll of Alacrity gump.
- Fixes missing property invalidation for Skill/Value.
2024-03-01 21:36:37 -08:00
Kamron Batman
de0d205fd3
fix: Fixes importing spawners (#1689) 2024-02-19 10:07:51 -08:00
Kamron Batman
1fdef9f389
fix: Codegens Heartwood quest givers (#1687)
### Summary
- Codegens quest givers (mostly heartwood)
- Removes their `RandomList` calls in favor of random bool or switch.
2024-02-18 20:22:28 -08:00
Kamron Batman
e994505ad0
fix: Adds safety to corpses and cleans up quests (#1685)
### Summary
- Adds some more checks in case a corpse's owner is somehow null. Would like to eventually allow null owner corpses, but more work is needed.
- Cleans up variable unboxing and reassignment in quests. Also flattens quest logic. Still more work needs to be done.
- Codegens more quest items/mobiles.
2024-02-18 19:33:08 -08:00
Kamron Batman
0d5aa1d022
fix: Fixes region duplicates (#1684) 2024-02-18 12:01:16 -08:00
Kamron Batman
d0d7be8de0
fix: Fixes codegenned spawners and adds RunUO spawner import support (#1682)
> [!IMPORTANT]  
> This code change includes important fixes for all spawners after they were converted to codegen!

> [!NOTE] 
> **Developer Note**
> Usage/Description/Aliases attributes were moved to the core so they can be used by the command registration system.

### Summary
- **Fixes spawners not registering their spawns on world load**
- Changes `[GenerateSpawners` to `[ImportSpawners`
- Removes `ConvertPremiumSpawners`
- Adds Premium spawner import support to `[ImportSpawners`
- Adds RunUO XML import support: https://github.com/ruaduck/xmlspawner/blob/ServUOMaster/XmlSpawner/XmlSpawner%20Core/SpawnerExporter.cs
- Fixed an issue where not manually registering an alias meant it wasn't available as a command
2024-02-17 10:46:55 -08:00
dependabot[bot]
cc064a27a5
chore(deps): Bump xunit from 2.6.6 to 2.7.0 (#1680)
Bumps [xunit](https://github.com/xunit/xunit) from 2.6.6 to 2.7.0.
- [Commits](https://github.com/xunit/xunit/compare/2.6.6...2.7.0)

---
updated-dependencies:
- dependency-name: xunit
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-02-17 00:35:24 -08:00
dependabot[bot]
6fe27baa42
chore(deps): Bump xunit.runner.visualstudio from 2.5.6 to 2.5.7 (#1681)
Bumps [xunit.runner.visualstudio](https://github.com/xunit/visualstudio.xunit) from 2.5.6 to 2.5.7.
- [Release notes](https://github.com/xunit/visualstudio.xunit/releases)
- [Commits](https://github.com/xunit/visualstudio.xunit/compare/2.5.6...2.5.7)

---
updated-dependencies:
- dependency-name: xunit.runner.visualstudio
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-02-17 00:33:43 -08:00
Kamron Batman
8f4d86ca1f
fix: Fixes OPL for a few items (#1679) 2024-02-14 19:47:30 -08:00
Kamron Batman
5ada7a4e50
fix: Codegens spawners (#1678) 2024-02-12 21:14:42 -08:00
Kamron Batman
48e47f9352
fix: Fixes duplicate command listed. (#1677) 2024-02-11 18:13:45 -08:00
Kamron Batman
4479555156
fix: Fixes missing methods. Adds more admin gump world building (#1675)
### Summary
- Fixe commands missing in helpinfo.
- Adds more admin gump world building options. 

### Screenshots

<img src="https://github.com/modernuo/ModernUO/assets/3953314/3001aa65-4f46-4ba4-89ed-745fefdd27c3" width="50%">
2024-02-10 20:31:25 -08:00
Kamron Batman
cf989151f1
fix: Fixes the tooltip for IOS (#1674) 2024-02-10 18:56:45 -08:00
Kamron Batman
effc2c103f
fix: Fixes powerscroll properties label (#1673) 2024-02-10 10:47:27 -08:00
Kamron Batman
5decc0d1cd
fix: Fixes commands webpage table spacing (#1672) 2024-02-10 02:34:51 -08:00
Kamron Batman
d9d3a36815
fix: Fixes commands webpage table size (#1671) 2024-02-10 01:53:41 -08:00
Kamron Batman
ac61b22f4b
fix: Fixes commands.html tooltip (#1670) 2024-02-10 01:28:55 -08:00
Kamron Batman
fffda53263
fix: Adds command help, webpage, and fixes issues with other commands (#1669)
### Summary

- Fixes `[AdvancedSearch` being accessible by players 😱 
- Adds `[GenCommands` to generate the same commands html page on https://muo.gg/commands.
- Fixes `[helpinfo` so all commands properly show up!

> [!WARNING]  
> ### Developer Warning:
> Commands must now be registered in the `Configure` bootup phase.
> If a command is not registered early enough, it may not be available to systems like [helpinfo
> that cache their information.

> [!NOTE]  
> ### Developer Note:
> Various commands related to generating content have been changed to _Developer_ and above access level.

### Screenshots
<img width="673" alt="image" src="https://github.com/modernuo/ModernUO/assets/3953314/b105b5c9-5eb4-4ace-93ff-1bfb31e7132f">

<img width="547" alt="image" src="https://github.com/modernuo/ModernUO/assets/3953314/e97487e8-47a5-4aa7-89cc-9fe3deda584d">
2024-02-10 00:19:19 -08:00
Kamron Batman
3244704ea2
fix: Fixes directory copying (#1668) 2024-02-09 23:25:20 -08:00
dependabot[bot]
00332e542d
chore(deps): Bump Microsoft.NET.Test.Sdk from 17.8.0 to 17.9.0 (#1667)
Bumps [Microsoft.NET.Test.Sdk](https://github.com/microsoft/vstest) from 17.8.0 to 17.9.0.
- [Release notes](https://github.com/microsoft/vstest/releases)
- [Changelog](https://github.com/microsoft/vstest/blob/main/docs/releases.md)
- [Commits](https://github.com/microsoft/vstest/compare/v17.8.0...v17.9.0)

---
updated-dependencies:
- dependency-name: Microsoft.NET.Test.Sdk
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-02-06 14:48:53 -08:00
Kamron Batman
3f00f6d610
fix: Fixes loot with zero amount (#1666)
### Summary

* Adds logging if Amount goes to zero. It should never go to zero!
* Deletes an item if it mutates with zero amount.
* Eliminates possible allocation from `params` for `Type[][]` loot pack construction.
* Renames `Loot.ChestOfHeirloomsContains` to `Loot.RandomChestOfHeirloomsContent`.
2024-01-29 23:23:42 -08:00
Kamron Batman
64a8d6cb5d
fix: Fixes order of execution with AutoSave and TZ (#1665) 2024-01-24 17:17:45 -08:00
Kamron Batman
3812a783ca
fix: Fixes issue with duping items and serialization, and sector lists. (#1662) 2024-01-23 23:08:52 -08:00
Kamron Batman
497ea87943
fix: Fixes disconnects due to bad seeding (#1663) 2024-01-23 19:01:02 -08:00
Kamron Batman
4cd668ef61
feat: Moves TcpServer to another thread. Rewrites Firewall (#1660)
## Breaking Changes
* The Firewall and IP Limiter have been rewritten. Please read the notes carefully!
* `TcpServer.Instances` moved back to `NetState.Instances` - sorry - it was stupid to move it to begin with.

> [!Note]
> Sockets that fail the IP Limiter or Firewall will be immediately and forcibly disconnected.
> This means they will be stuck at "Verifying account..." if it was a real client.

### Summary
- Removes firewall wildcard support.
- Removes `AccessRestrictions`.
- Moves Firewall/IPLimiter to the core.
- Moves `TcpServer` to its own thread.
- Removes the `SocketConnect` and `SocketDisconnect` event sinks.
- Moves `Instances` back to `NetState.Instances`.
- Fixes a long standing bug with bad handling of duplicate listener addresses.

#### Firewall
The firewall has been completely rewritten. There is now an "Admin Firewall" which saves to the config file. Secondarily, there is an internal firewall used exclusively by the TcpServer while processing sockets. The Admin firewall mirrors it's additions/deletions to the internal firewall by adding requests to a queue.

> [!IMPORTANT]  
> **Wildcard firewall entries, such as `X`, `*`, `?` are not allowed.**
> **Ranges in between IP classes or sextets are not allowed.**
> **Please make sure to use one of the following:**
> * IP Address - `192.168.1.1`
> * CIDR - `192.168.1.0/24`
> * Range - `192.168.1.1-192.168.1.100`

#### IP Limiter
The IP Limiter has been completely rewritten. The available configurations are:
```json
"ipLimiter.enable": "True",
"ipLimiter.maxConnectionsPerIP": 10,
"ipLimiter.clearConnectionAttemptsDuration": "00:00:00:10",
"ipLimiter.clearThrottledDuration": "00:00:02:00",
```

The IP Limiter is set up to prevent spamming connections from the same IP. Every time an IP connects, it is added to a connection list. After 10 attempts, the IP is added to the throttle list. To keep the system fast, the connection list is entirely wiped every 10 seconds, and the throttle list is entirely wiped every 2 minutes.
2024-01-20 14:25:12 -08:00
Kamron Batman
5f3de6537b
chore: Update README.md (#1661) 2024-01-20 14:24:44 -08:00
dependabot[bot]
ee8d13f70b
chore(deps): Bump xunit from 2.6.5 to 2.6.6 (#1659)
Bumps [xunit](https://github.com/xunit/xunit) from 2.6.5 to 2.6.6.
- [Commits](https://github.com/xunit/xunit/compare/2.6.5...2.6.6)

---
updated-dependencies:
- dependency-name: xunit
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-01-16 10:08:27 -08:00
Kamron Batman
823023275b
fix: Stops trying to perform snapshots when the world is not saving (#1658) 2024-01-15 08:29:54 -08:00
Kamron Batman
0ffea2e912
fix: Fixes Feint so it properly reduces damage (#1657) 2024-01-11 00:01:52 -08:00
Eric Vintimilla
49118556d3
fix: Fixes hues for Bags of Sending (#1654) 2024-01-06 08:50:36 -08:00
dependabot[bot]
1dbf8a0834
chore(deps): Bump xunit from 2.6.4 to 2.6.5 (#1655)
Bumps [xunit](https://github.com/xunit/xunit) from 2.6.4 to 2.6.5.
- [Commits](https://github.com/xunit/xunit/compare/2.6.4...2.6.5)

---
updated-dependencies:
- dependency-name: xunit
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-01-05 19:55:05 -08:00
Kamron Batman
aaad909c89
fix: Fixes GetItemsAt, GetMobilesAt, and GetClientsAt (#1653) 2023-12-30 09:47:21 -08:00
Kamron Batman
2084374318
fix: Adds Mage AI invis chance variable (#1651) 2023-12-28 23:53:55 -08:00
Kamron Batman
fbbc407954
feat: Adds AdvancedSearch (XmlFind) (#1649)
### Summary

Adds Advanced Search (XmlFind) using `[AS` command.


Notable differences from XmlFind:
* No save to file.
* No "Display from" option (might add that later).
* No advanced XmlSpawner property test syntax. Only basic property operator comparisons.
* This version will freeze your shard, but fans out to all processors just like world saves. (So it should be stupid fast, especially if you have lots of cores)
* Does not support spawn entry/type/error searching. We have a spawn search command to do that already.
* I haven't set up correct "defaults" for the options when you first open it. I'll do that later.

### Screenshots
<img width="575" alt="image" src="https://github.com/modernuo/ModernUO/assets/3953314/2b08e586-0ea6-41c4-b7cd-f70e2ee62b86">

<img width="575" alt="image" src="https://github.com/modernuo/ModernUO/assets/3953314/da3f16b9-6a9e-4d2c-9d93-6cdfa67b4ddc">

<img width="572" alt="image" src="https://github.com/modernuo/ModernUO/assets/3953314/ae1d0a3f-a30d-497c-99ea-2cc988147f8f">
2023-12-28 18:55:47 -08:00
Kamron Batman
fe6e9dfe5c
fix: Adds required changes for Advanced Search feature (#1650) 2023-12-28 18:16:58 -08:00
Kamron Batman
96c3f2eb91
fix: Use pooled ref for corpse instanced loot (#1645) 2023-12-26 10:51:58 -08:00
dependabot[bot]
449dba3ac7
chore(deps): Bump xunit.runner.visualstudio from 2.5.5 to 2.5.6 (#1647)
Bumps [xunit.runner.visualstudio](https://github.com/xunit/visualstudio.xunit) from 2.5.5 to 2.5.6.
- [Release notes](https://github.com/xunit/visualstudio.xunit/releases)
- [Commits](https://github.com/xunit/visualstudio.xunit/compare/2.5.5...2.5.6)

---
updated-dependencies:
- dependency-name: xunit.runner.visualstudio
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-12-26 10:50:53 -08:00
dependabot[bot]
10605d5027
chore(deps): Bump xunit from 2.6.3 to 2.6.4 (#1646)
Bumps [xunit](https://github.com/xunit/xunit) from 2.6.3 to 2.6.4.
- [Commits](https://github.com/xunit/xunit/compare/2.6.3...2.6.4)

---
updated-dependencies:
- dependency-name: xunit
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-12-26 10:48:30 -08:00
Kamron Batman
d4282367ba
fix: Fixes crash with combatant and mobs facing each other (#1644)
### Summary
- Fixes crash bugs with combatant checks in AI
- Fixes mobs not facing each other while in combat. (Old RunUO bug)
- Fixes predators not actually going into guard when their combatant dies.
- Adds missing checks for combatant in various AI.

Note: Much easier to understand changes if whitespace is off - https://github.com/modernuo/ModernUO/pull/1644/files?diff=split&w=1
2023-12-20 14:48:24 -08:00
Kamron Batman
1e73802d1e
fix: Fixes network issue with incoming packet length (#1643) 2023-12-20 13:19:00 -08:00
Kamron Batman
61e9dd29ac
fix: Adds missing migration files. (#1642)
### Summary

- Bumps serialization generator. v2.10.9 fixes an issue with nuget publishing where files were missing.
- Fixes camps missing serialization of their prisoner.
- Adds missing serialization migration files.
2023-12-20 13:02:01 -08:00
Kamron Batman
7d9bc9ff0a
fix: Fixes thread guard and cleans up incoming packet reader (#1641)
### Summary
* Fixes syntax compile error when THREADGUARD is enabled.
* Removes `int packetLength` from incoming packet handles since they aren't needed.

### Developer Notes
Incoming packet handler `SpanReader` is now properly scoped to that packet by length.
2023-12-19 17:04:09 -08:00
Gnai
25a75fdf24
fix: Update Main.cs to accept utf8 on console (#1639) 2023-12-16 00:33:56 -08:00
Stefano Merotta
1ab367d637
fix: Fixes pooled ref collections dispose on defaults (#1638) 2023-12-15 13:46:52 -08:00
Eric Vintimilla
0d65464c9f
fix: Fixes crash when banker tries to consume checks & gold from bankbox (#1635)
Co-authored-by: Stefano Merotta <97297186+stefanomerotta@users.noreply.github.com>
Co-authored-by: Kamron Batman <3953314+kamronbatman@users.noreply.github.com>
2023-12-15 09:13:21 -08:00
Kamron Batman
c487aab3f9
doc(chore): Update README.md - Fix another link (#1637) 2023-12-14 12:18:27 -08:00
FiftyTifty
49ccc8cfee
doc(chore): Update README.md - .Net 8.0 Link (#1636) 2023-12-14 12:15:19 -08:00
dependabot[bot]
e73c599cc6
chore(deps): Bump xunit.runner.visualstudio from 2.5.4 to 2.5.5 (#1633)
Bumps [xunit.runner.visualstudio](https://github.com/xunit/visualstudio.xunit) from 2.5.4 to 2.5.5.
- [Release notes](https://github.com/xunit/visualstudio.xunit/releases)
- [Commits](https://github.com/xunit/visualstudio.xunit/compare/2.5.4...2.5.5)

---
updated-dependencies:
- dependency-name: xunit.runner.visualstudio
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-12-12 09:02:04 -08:00
dependabot[bot]
442bdd77d9
chore(deps): Bump xunit from 2.6.2 to 2.6.3 (#1634)
Bumps [xunit](https://github.com/xunit/xunit) from 2.6.2 to 2.6.3.
- [Commits](https://github.com/xunit/xunit/compare/2.6.2...2.6.3)

---
updated-dependencies:
- dependency-name: xunit
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-12-12 09:00:16 -08:00
dependabot[bot]
a012f72205
chore(deps): Bump Serilog.Sinks.Console from 5.0.0 to 5.0.1 (#1632)
Bumps [Serilog.Sinks.Console](https://github.com/serilog/serilog-sinks-console) from 5.0.0 to 5.0.1.
- [Release notes](https://github.com/serilog/serilog-sinks-console/releases)
- [Commits](https://github.com/serilog/serilog-sinks-console/compare/v5.0.0...v5.0.1)

---
updated-dependencies:
- dependency-name: Serilog.Sinks.Console
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-12-08 16:02:37 -08:00
Stefano Merotta
ca5aa80fcd
fix: Fixes default GumpID and DropSound serialization for containers (#1631) 2023-12-08 16:02:23 -08:00
Kamron Batman
f35af0de2f
fix: Codegens camps (#1628) 2023-12-04 09:11:52 -08:00
Kamron Batman
b7882df136
fix: Fixes serialization and dirty tracking conveniences (#1627)
### Summary
- Fixes issues with non-_ISerializable_ objects having bad serialization (Skill/Stat. Mods)
- Fixes issues with MarkDirty and namespaces: https://github.com/modernuo/SerializationGenerator/issues/29
- Fixes missing dirty tracking for some data structures.
- Fixes cascading MarkDirty for non-serializable types that have owners that are also non-serializable types.

### New Codegenned API
When a data structure (Array, List, Dictionary, HashSet, etc) is source generated for serialization, new methods are added which will handle dirty tracking. Use these instead of the built in methods for that data structure.

_All Data Structures_
```cs
public void Clear<PropertyName>();
```

_Lists and Sets_
```cs
public void AddTo<PropertyName>(V value);
public void RemoveFrom<PropertyName>(V value);
```

_Lists_
```cs
public void InsertInto<PropertyName>(int index, V value);
public void RemoveFrom<PropertyName>At(int index);
```

_Dictionaries_
```cs
public void AddTo<PropertyName>(T key, V value);
public void RemoveFrom<PropertyName>(T key);
public void ReplaceIn<PropertyName>(T key, V value);
```

Over time, they will be cleaned up and more variants added such as `bool RemoveFrom<PropertyName>(T key, out V value)`
2023-12-03 15:55:31 -08:00
Kamron Batman
3a0d4b171b
fix: Codegens boats (#1625) 2023-12-02 12:55:38 -08:00
Kamron Batman
b29a79ac2d
fix: Fixes migration check (#1626) 2023-12-02 12:50:42 -08:00
Kamron Batman
e21ff0eb28
fix: Codegens BaseMulti, Container, and VirtualCheck (#1624) 2023-12-02 10:00:02 -08:00
mark1145
4ebdc75bdf
fix: Fixes PainSpike, ManaVampire, Evil Omen, and Curse (#1622)
Co-authored-by: Kamron Batman <3953314+kamronbatman@users.noreply.github.com>
2023-12-02 09:59:42 -08:00
Stefano Merotta
fec78040bc
fix: Fixes wrong sector shift for tilematrix iteration (#1623) 2023-12-01 18:09:26 -06:00
Kamron Batman
130a5674bf
fix: Fixes negative weight issue (#1621)
Closes #73
2023-11-26 10:04:29 -08:00
Kamron Batman
4f6afad10b
chore: Update README.md for Sonoma support (#1620) 2023-11-26 09:41:41 -08:00
Kamron Batman
f21c9687b2
fix: Refactors getting static tiles to use enumerators (#1611)
### Summary
* Refactors getting static/multi tiles to not use allocations.
* `TileList` is now only used during bootstrapping and uses rented buffers to eliminate extra allocations.
* Replaces `StaticTile[] GetStaticTiles` with:
    ```cs
        Map.StaticTileEnumerable GetStaticTiles(int x, int y);
        Map.StaticTileEnumerable GetStaticAndMultiTiles(int x, int y);
        Map.StaticTileEnumerable GetMultiTiles(int x, int y);
    ```
* Removes `Synchronized` and `lock` from TileMatrix. It is no longer considered a multi-thread safe system.
2023-11-26 09:39:46 -08:00
Kamron Batman
3b1637b2da
fix: Removes redundant fixture check (#1619) 2023-11-26 09:24:52 -08:00
Kamron Batman
6a0fcd62c1
fix: Fixes various mobile packets and setting serials (#1618)
### Summary
- Removes old death packet that isn't used. Doubtful this causes issues with clients that are v4+.
- Removes duplicate incoming packets. Again, probably to fix some old client issues, doubtful it affects clients v4+.
- Fixes setting serials and entities in props/commands. Note: Disabled setting `Parent` since the new sector code has issues. We shouldn't rely on it anyway!
- Reverts a recent change to healthbars that should not have been made. Oops!
2023-11-26 00:42:15 -08:00
Kamron Batman
332e22f509
fix: Fix compiling (#1617) 2023-11-25 23:41:09 -08:00
Kamron Batman
674b8dca83
fix: Fixes character starting cities. (#1615)
### Summary

- Moves starting city info from AccountHandler to CharacterCreation
- Simplifies the logic of determining the starting cities
- Adds support for Trammel & Felucca for non-young accounts
- Fixes fall through starting city for v6+ in UOR era.
- All staff are force-sent to GA.

Closes #1408
2023-11-25 23:31:54 -08:00
Kamron Batman
a93a363c5f
fix: Fixes shutdown+save (#1614) 2023-11-25 10:11:24 -08:00
Kamron Batman
21a067c187
fix: Update RUNUO_TO_MODERNUO.md (#1613) 2023-11-24 23:49:43 -08:00
Kamron Batman
88b8851156
fix: Fixes argument out of range error from empty tile list (#1612) 2023-11-24 21:06:36 -08:00
Voxpire
3109f59d4d
fix: Updates container searches for non-generic types (#1567) 2023-11-23 09:29:45 -08:00
Kamron Batman
331f24cb71
fix: Fixes TimeSpan rounding issues (#1610)
### Summary

TLDR - .NET Framework rounded TimeSpan (and some DateTime) methods to the nearest millisecond. This was actually _expected_ accidentally for parts of RunUO.

We are aiming to fix this and clean up some other bad assumptions.

Closes #1604
2023-11-22 18:03:56 -08:00
mark1145
98727d13b3
fix: Fixes vendor buy int overflow (#1586) 2023-11-22 14:41:50 -08:00
Kamron Batman
d93e363017
fix: Fixes VS support by reverting source generators back to net standard 2.0 (#1609) 2023-11-21 17:10:13 -08:00
Kamron Batman
d5253c2bae
chore: Cleans up unused imports (#1608) 2023-11-21 12:32:51 -08:00
mdodkins
e680bdb4b5
fix: Adds boolean support for Server.TryParse (#1605) 2023-11-21 12:21:59 -08:00
Kamron Batman
03f850fe03
fix: Fixes sending packets and sidesteps a major issue with stackalloc and PGO in .NET 8 (#1607)
### Summary
- Works around a sneaky edge case bug in the JIT with stackalloc where sometimes the buffer is not zero'd.
- Fixes SendDisplayBoatHS
- Fixes sending health bars in the `SendEverything()` logic.
- Fixes a bug in sizing for some string helper functions.

### Developer Note
We are enabled `SkipLocalsInit` - do not rely on `stackalloc` to be zero'd. To zero the buffer, use `span.Clear();`

Closes #1606
2023-11-21 12:18:20 -08:00
Kamron Batman
79ba1ea917
fix: Fixes tests (#1603) 2023-11-19 10:11:04 -08:00
Kamron Batman
0da6e46d67
feat: Updates to .NET Standard 2.1 source generators (#1602) 2023-11-19 10:03:28 -08:00
Kamron Batman
021ebd0a88
fix: Fixes multi search (#1601) 2023-11-18 12:37:26 -08:00
Kamron Batman
b5c984e5de
fix: Fixes negative random numbers (#1600) 2023-11-18 10:22:40 -08:00
Kamron Batman
dba3ec2db5
fix: Use built-in RNG (#1599)
### Summary

.NET 8 supports Xoroshiro 256** off the shelf and added Shuffle. Switching to that implementation.

### Developer Notes

* Removed many convenience methods that weren't used.
2023-11-17 17:45:18 -08:00
Kamron Batman
1769d47ba5
fix: Removes Standart XxHash in favor of the built in one (#1598) 2023-11-17 16:11:35 -08:00
Kamron Batman
e9642d61f1
fix: Removes custom BitArray (#1597)
### Summary

The BitArray class will be optimized over the next several years for various platforms/hardware and maintaining a duplicate for serialization is not practical. Removing the custom implementation. Recommend against using BitArray for serialization unless it is absolutely necessary.
2023-11-17 14:37:07 -08:00
Kamron Batman
a1dffd10ce
fix: Acquired recipes are now a Set (#1596) 2023-11-17 00:04:46 -08:00
Quick
6f0c33bd10
fix: Fixes serialization on disguise kits (#1595) 2023-11-17 00:04:04 -08:00
Kamron Batman
f7626b64f3
chore: Bump fedora version in README.md (#1594) 2023-11-14 17:35:00 -08:00
Kamron Batman
2e4668dbe5
feat: Updates to .NET 8. (#1542)
### Breaking Changes
- Updating to .NET 8 - Required O/S's have slightly changed.
2023-11-14 17:08:09 -08:00
dependabot[bot]
bd79509400
chore(deps): Bump MailKit from 4.2.0 to 4.3.0 (#1590) 2023-11-13 22:11:41 -08:00
dependabot[bot]
24e018995c
chore(deps): Bump Serilog.Sinks.Console from 4.1.0 to 5.0.0 (#1587) 2023-11-10 22:46:17 -08:00
dependabot[bot]
172d4a3642
chore(deps): Bump Serilog from 3.0.1 to 3.1.1 (#1589) 2023-11-10 21:31:03 -08:00
Eric Vintimilla
f71d5bcff1
fix: Fixes missing ethereal horse label (#1585) 2023-11-09 14:16:41 -08:00
dependabot[bot]
1201f04321
chore(deps): Bump Microsoft.NET.Test.Sdk from 17.7.2 to 17.8.0 (#1584) 2023-11-08 22:36:06 -08:00
Kamron Batman
c8463e2eaf
fix: Fixes lightning arrow (#1583)
### Summary
- [X] Lightning arrow no longer allows SDI
- [X] Lightning arrow now proc's even without arrows
- [X] Fixed NPE with lightning arrow when a monster is already killed.
2023-11-05 17:36:09 -08:00
Kamron Batman
ca3df9cfa7
fix: Adds multis to map iterators, fixes searching empty nested containers (#1581)
### Summary
Eliminates `IPooledEnumerable<BaseMulti>` and `eable.Free()` from `Map` for multis. This drastically simplifies code that iterates in range, for example:

```cs
foreach (var m in m.GetMultisInRange(5))
{
}
```
The code above no longer requires an eable and calling `Free()`.


### Bug Fixes
- [X] Fixes an issue with searching through nested empty containers.
2023-11-05 08:17:34 -08:00
Kamron Batman
1f04a13f67
fix: Fixes poison field duration, cleans up skill calculations and poison calculations (#1582)
### Summary
- [X] Fixes poison field duration
- [X] Adds SA+ poison spell calculations
- [X] Cleans up skill calculations
- [X] Adjusts poison level thresholds to properly reflect OSI
2023-11-04 12:32:54 -07:00
Kamron Batman
54431f05b5
fix: Fixes memory leak with houses (#1580) 2023-11-03 15:05:08 -07:00
Kamron Batman
977fdc2c5a
fix: Removes GetObjectsInRange and fixes boat planks closing (#1579)
## BREAKING CHANGE

- Deletes `map.GetObjectsInRange` and `map.GetObejctsInBounds`

### Notes

Developers are expected to enumerate mobiles and items separately now using `map.GetMobilesInRange` and `map.GetItemsInRange`. This helps keep the code streamlined so we don't have to maintain multiple copies of ref struct enumerators that do the same thing.


### Fixes

- [X] Fixes bug with planks closing
- [X] Fixes issue with iterating items/mobiles from a null map
2023-11-03 13:45:18 -07:00
dependabot[bot]
cb1638591b
chore(deps): Bump xunit from 2.5.3 to 2.6.1 (#1578)
Bumps [xunit](https://github.com/xunit/xunit) from 2.5.3 to 2.6.1.
- [Commits](https://github.com/xunit/xunit/compare/2.5.3...2.6.1)

---
updated-dependencies:
- dependency-name: xunit
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-11-03 10:52:13 -07:00
Kamron Batman
c53c842c4b
fix: Fixes releases (#1576) 2023-10-30 19:26:10 -07:00
Kamron Batman
cde59a82f2
fix: Adds GetClients to map iterators (#1574)
### Summary

Eliminates `IPooledEnumerable<NetState>` and `eable.Free()` from `Map` for clients. This drastically simplifies code that iterates in range, for example:

```cs
foreach (var m in m.GetClientsInRange(5))
{
}
```
The code above no longer requires an eable and calling `Free()`.
2023-10-30 18:49:00 -07:00
Kamron Batman
469b89370d
fix: Adds back Average CPS to Admin Gump (#1573) 2023-10-30 18:05:32 -07:00
Quick
c32aa39931
fix: Fixes crash on single-core machines (#1572) 2023-10-30 14:44:16 -07:00
Kamron Batman
28c06c1cc0
fix: Changes Map.Sector.Mobiles to link list & Fixes various related crash bugs (#1553)
### Summary

Eliminates `IPooledEnumerable<T>` and `eable.Free()` from `Map` for mobiles. This drastically simplifies code that iterates in range, for example:

```cs
foreach (var m in m.GetMobilesInRange(5))
{
}
```
The code above no longer requires an eable and calling `Free()`.

- [X] Fixed several locations where an NPC that was damaged would cause a server crash.
- [X] Removed an unnecessary allocation in guard fake calls (NPCs calling guards on you)
- [X] Fixes damage precision loss in Poison Strike Spell
- [X] BogThing no longer attempts to "search" for boglings to eat when it is at full health
2023-10-29 22:42:46 -07:00
Kamron Batman
27f0cec1fa
fix: Cleans up Tracking skill and makes it much faster (#1571) 2023-10-29 19:38:59 -07:00
Kamron Batman
8c305261b3
fix: Fixes enabling factions causing NPE (#1569) 2023-10-29 08:32:06 -07:00
Kamron Batman
d919f71149
fix: Fixes map iterators for Items (#1564)
### Summary

Modifying a ValueLinkList using one of the methods will bump the "version". This field is used by iterators (foreach loops) to determine if the link list was modified while iterating. The sector.Items (and in the future other lists), will no longer be safe to modify while iterating. The server will _CRASH_ if the ValueLinkList is modified.

Thanks to @stefanomerotta for help!


### Screenshots
<img width="588" alt="image" src="https://github.com/modernuo/ModernUO/assets/3953314/83ee0b6e-ff4f-4768-9e29-84456e04b1ec">
2023-10-26 17:49:15 -07:00
Kamron Batman
658f564f34
fix: Fix the namespace for serialization ext (#1566) 2023-10-25 20:34:11 -07:00
Kamron Batman
e6d30fef0f
fix: Reverts GetAccount to use IAccount. Adds IAccount to serialization. (#1565)
### Summary

- Fixes usernames not being `Intern`ed
- Reverts methods related to getting accounts from returning `Account` to `IAccount`.
- Makes `IAccount` also `ISerializable`
- Adds `IGenericReader.ReadAccount()` and `IGenericWriter.Write(IAccount)` -> The read method supports the original serialization of username, and using `IAccount.Serial`. The write method only serializes the `Serial`.
- Exposes `ReadStringRaw()` to allow some advanced scenarios.
2023-10-25 19:17:57 -07:00
Eric Vintimilla
a4e2226a5c
fix: FlippableAttribute with empty list will crash the client (#1563) 2023-10-23 10:38:47 -07:00
Kamron Batman
4d8e9fc515
fix: Fixes map item iteration when multis are moved (#1562) 2023-10-22 13:22:11 -07:00
mark1145
f6491c0fe5
fix: Fixes mana vampire creating mana from thin air (#1561) 2023-10-22 12:07:14 -07:00
Kamron Batman
68f3e15e5b
fix: Cleans up doors and house placement (#1560) 2023-10-21 14:20:16 -07:00
Kamron Batman
6239b418a8
fix: Fixes process delta calls during death/res (#1559) 2023-10-21 10:39:13 -07:00
Kamron Batman
aaf0fc32fa
fix: Fixes skill gain issue (#1558) 2023-10-21 10:03:50 -07:00
Kamron Batman
74f67487c4
fix: Adds DupeExcludedProperty to Items (#1556) 2023-10-17 20:14:05 -07:00
Eric Vintimilla
85e23d8d83
fix: Fix for witch's apprentice quest - was not "beatable" (#1554) 2023-10-17 07:57:59 -07:00
Kamron Batman
24858f3989
fix: Changes Map.Sector.Items to link list. (#1547)
### Summary

Eliminates `IPooledEnumerable<T>` and `eable.Free()` from `Map` for items. This drastically simplifies code that iterates in range, for example:

```cs
foreach (var item in m.GetItemsInRange(5))
{
}
```
The code above no longer requires an eable and calling `Free()`.
2023-10-16 20:51:02 -07:00
dependabot[bot]
1330c4a830
chore(deps): Bump xunit from 2.5.2 to 2.5.3 (#1552)
Bumps [xunit](https://github.com/xunit/xunit) from 2.5.2 to 2.5.3.
- [Commits](https://github.com/xunit/xunit/commits)

---
updated-dependencies:
- dependency-name: xunit
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-10-16 20:50:39 -07:00
Kamron Batman
6178f2851a
fix: Fixes item serials (#1551) 2023-10-15 16:58:51 -07:00
Kamron Batman
5b99402666
chore: Cleans up unused imports (#1550) 2023-10-15 11:45:42 -07:00
Kamron Batman
e2f5728258
fix: Fixes null enumerable compile erorr (#1549) 2023-10-15 11:39:00 -07:00
Kamron Batman
d57f1fecc1
fix: Prepares for IPooledEnumerable removal (#1548)
### Summary

- Removes `IPooledEnumerable` (non-generic)
- Changes `IPooledEnumerable<T>` so that  `Free()` is replaced with the `IDisposable` pattern
2023-10-15 11:20:49 -07:00
Kamron Batman
c2d6578955
feat: Adds Next/Prev for future link list support (#1546) 2023-10-14 19:43:10 -07:00
Kamron Batman
fbd194339a
feat: Adds ValueLinkList (#1545) 2023-10-14 18:58:36 -07:00
Vitalii Zurian
f9517854d0
feat: Adds configurable skill and stat gain (#1489)
### Summary

- Adds the following configurations:
```json
"stats.statMax": "125",
"stats.gainChanceMultiplier": 1.0,
"stats.primaryStatGainChance": 0.75,
"stats.gainDelay": "00:10:00",
"stats.petGainDelay": "00:05:00",
"stats.usePub45StatGain": "False"
```

- Adds Pub 45 stat gain rate for UOML+.
- Reduces the gain delay for legacy stat gain from 15 minutes to 10 minutes.
- Legacy gain no longer requires a gain in skill to gain in a stat.
2023-10-14 12:02:36 -07:00
dependabot[bot]
29c7d839a7
chore(deps): Bump xunit from 2.5.1 to 2.5.2 (#1543)
Bumps [xunit](https://github.com/xunit/xunit) from 2.5.1 to 2.5.2.
- [Commits](https://github.com/xunit/xunit/compare/2.5.1...2.5.2)

---
updated-dependencies:
- dependency-name: xunit
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-10-13 17:07:10 -07:00
dependabot[bot]
b4ee44fe44
chore(deps): Bump xunit.runner.visualstudio from 2.5.1 to 2.5.3 (#1544)
Bumps [xunit.runner.visualstudio](https://github.com/xunit/visualstudio.xunit) from 2.5.1 to 2.5.3.
- [Release notes](https://github.com/xunit/visualstudio.xunit/releases)
- [Commits](https://github.com/xunit/visualstudio.xunit/compare/2.5.1...2.5.3)

---
updated-dependencies:
- dependency-name: xunit.runner.visualstudio
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-10-13 17:06:07 -07:00
Kamron Batman
75c58e2526
fix: Codegens doom items and mobs (#1541) 2023-10-11 23:53:51 -07:00
Kamron Batman
28068c8241
fix: Fixes world save decay (#1540) 2023-10-11 14:04:13 -07:00
Kamron Batman
1ebaf5390d
fix: Fixes span reader ReadString (#1539) 2023-10-11 13:41:24 -07:00
Kamron Batman
0018901a79
fix: Codegens more quest entities (#1538) 2023-10-09 23:58:04 -07:00
Kamron Batman
0086d674e7
fix: Codegens special systems (#1537) 2023-10-09 22:06:09 -07:00
Kamron Batman
e1f22d9694
fix: Adds back missing world state (#1536) 2023-10-09 10:02:39 -07:00
Kamron Batman
10a69bf754
feat: Adds a memory mirrored ring buffer for networking. (#1533)
## Breaking Changes

Incoming packet registration signature has changed to:
```cs
delegate* void OnReceiveCallback(NetState state, SpanReader reader, int packetLength);

IncomingPackets.Register(int packetID, int length, bool ingame, OnReceiveCallback onReceive);
```

For example, an incoming packet handler signature would now look like this:
```cs
public static void SomeIncomingPacket(NetState state, SpanReader reader, int packetLength)
{
    // Parse the data
}
```

## Summary

Updates the network Pipe class to use a mirrored memory technique. This technique involves mapping the same physical memory to two contiguous virtual memory spaces so the byte buffer appears duplicated. This allows writing to a double-sized array to wrap around without the need for the `CircularBuffer` classes.

In practice this allows us to use `Span<byte>` as if the buffer was a regular array.


### Bug Fixes

- [X] Fixes bad fixed length string parsing
2023-10-09 00:57:53 -07:00
Kamron Batman
629a5008c3
fix: Fixes hanging tests (#1535) 2023-10-09 00:22:59 -07:00
Eric Vintimilla
d0d81d0d5f
fix: Fix ConsumeTotal always failing (#1534) 2023-10-06 11:02:59 -07:00
Eric Vintimilla
f4db1e3aad
fix: Fix animal trainer pet list only showing first pet. (#1532) 2023-10-04 11:03:25 -07:00
Kamron Batman
9b87049658
fix: Codegens BulkOrderBooks (#1531) 2023-10-03 20:44:15 -07:00
Kamron Batman
1f0acddc46
fix: Fixes serialization threading, moves world save to end of loop, and eliminates Parallel.ForEach. (#1530) 2023-10-03 00:42:49 -07:00
Kamron Batman
e18115dd94
fix: Fixes extension method refactors causing crash (#1529) 2023-10-02 22:24:16 -07:00
Kamron Batman
f2de2fbb77
fix: Cleans up entity persistence. Generalizes Mobiles/Items/Guilds (#1528)
### Summary
- [X] Fixed a bug where entity persistence was serialized out of order, causing world corruption.
- [X] Fixed LastSerialized not being utilized properly and dangling references still becoming an issue.
- [X] Added a new `GenericEntityPersistence<T>` type to encapsulate `ISerializable` serialization.
- [X] Removing the custom logic and moved Items, Mobiles, Guilds, and Accounts  to GenericEntityPersistence.
- [X] Changed serialization to use the singleton pattern to reduce calling methods from stored variables.
2023-10-01 22:10:47 -07:00
Kamron Batman
e0225c6e59
fix: Adds generic entity persistence support and BOBEntry as entities (#1527)
### Summary
Adds a generic entity persistence. This can be used to create new entity types that have a `Serial`.

Here is an example:

```cs
public class BOBEntries : GenericEntitySerialization<IBOBEntry>
{
    public static void Configure()
    {
        Configure("BOBEntries");
    }
}
```

The annotation tells the system what folder to serialize the entries to. The class/interface (`IBOBEntry`) is the root type that implements `ISerializable`.
2023-10-01 17:38:52 -07:00
Mink80
666b83a3dd
fix: ForceOfNature should damage the target (#1526) 2023-10-01 08:21:28 -07:00
Kamron Batman
d55406337f
fix: Fixes ML Escortables and codegens (#1525)
### Summary
- [X] Adds tot.json to ignore list.
- [X] Fixes bias in escortable random quest selection.
- [X] Adds New Haven specific destination/payment messages for escortable quests.
- [X] Codegens escortables.
2023-09-30 14:11:48 -07:00
Kamron Batman
7d05e3b1cb
fix: Codegens ToT and replaces persistence with config (#1524) 2023-09-30 02:09:28 -07:00
Kamron Batman
5ea5fb43cc
fix: Codegens character statues (#1523) 2023-09-30 01:30:47 -07:00
Kamron Batman
27cd214d58
fix: Codegens ML Quest items (#1522) 2023-09-30 00:46:07 -07:00
Kamron Batman
145c93ccf7
fix: Codegens Khaldun and Doom (#1521) 2023-09-30 00:27:06 -07:00
Kamron Batman
5dc2d09392
fix: Codegens construction items (#1520) 2023-09-29 23:28:51 -07:00
Kamron Batman
972949455b
fix: Changes BaseWeapon to source generator and changes crafter to string (#1519) 2023-09-29 22:45:37 -07:00
Kamron Batman
d6823309d4
fix: Fixes missing inscription message (#1518) 2023-09-29 21:38:06 -07:00
Kamron Batman
0d21459e85
fix: Cleans up ConsumeTotal and ClockworkAssembly (#1517) 2023-09-28 23:01:35 -07:00
Kamron Batman
d77dac9516
fix: Cleans up FindItems and removes allocations (#1516) 2023-09-28 22:20:36 -07:00
Kamron Batman
a4cabe2fa4
fix: Optimizes FindItemsByType by removing allocations. (#1515)
### Summary
Container enumeration is in dire need of optimization. Thanks to @stefanomerotta for initiating this work with PR #1443. This PR handles a small part of what Stefan started. Also included are some bug fixes.

### Method Signatures

```cs
// Use with foreach without moving/deleting items
FindItemsByTypeEnumerator<T> FindItemsByType<T>(bool recurse = true, Predicate<T> predicate = null)

// Use with foreach when moving/deleting items
QueuedItemsEnumerator<T> EnumerateItemsByType<T>(bool recurse = true, Predicate<T> predicate = null)

// Use when iterating multiple times or queuing
PooledRefQueue<T> QueueItemsByType<T>(bool recurse = true, Predicate<T> predicate = null)

// Use when iterating multiples times or manipulating elements without traversing
PooledRefList<T> ListItemsByType<T>(bool recurse = true, Predicate<T> predicate = null)
```

* `FindItemsByType<T>` has changed from returning `List<T>` to `FindItemsByTypeEnumerator<T>` - This method is not safe to use in situations where an item may get consumed, deleted, or moved.

* `EnumerateItemsByType<T>` was added as a safe way to iterate and manipulate items.
  * **Note**: EnumerateItemsByType will _completely traverse the container_ before iteration starts because it uses `QueueItemsByType` under the hood.

* `QueueItemsByType<T>`  and `ListItemsByType<T>` was added to return a queue or list of items to iterate multiple times and manipulate the items. This isn't the most efficient since it uses a predicate and can result in 2 or 3 total iterations unnecessarily.

### Bug Fixes
- [X] Fishing had an error in the random check that may have caused slight bias.
2023-09-28 19:36:45 -07:00
Kamron Batman
146abad36e
feat: Adds a movement throttle system. Removes Fastwalk system. (#1511)
### Summary

- Removes Fastwalk system
  - Removed the following settings:
    - `movement.enableFastWalkPrevention`
    - `movement.fastwalkExemptionLevel`
- Adds movement throttle system.
  - Adds the following settings:
    - `movement.throttleReset` - Default value is `1000` (1 second).
    - `movement.throttleThreshold` - Default value is `400` (400ms).

### Movement Throttling

This new system will trigger if a player requests 400ms (configurable) worth of movements quicker than wall clock time. When this happens, the player is throttled (all incoming packets to the server are halted) until wall clock time catches up with the requests. Upon each throttle, the player receives enough credit to handle up to 400ms of "lag" as a grace/catch-up.


### Developer Notes
We use two throttle queues to prevent an infinite loop.
2023-09-27 20:53:22 -07:00
Mink80
27df6344d9
fix: Optimizes redundant spell damage logic 2023-09-27 13:24:46 -07:00
Kamron Batman
0f41a3d1b1
fix: Fixes throttle receive packet issues (#1513) 2023-09-25 18:35:59 -07:00
Kamron Batman
5d78941ddf
fix: Fixes dropped packets (#1512) 2023-09-23 12:15:42 -07:00
Kamron Batman
7003552e71
fix: Fixes throttle state (#1510) 2023-09-23 11:07:14 -07:00
Kamron Batman
38557342f4
fix: Fixes region child level (#1508) 2023-09-19 10:35:50 -07:00
Kamron Batman
2a3e048b86
fix: Fixes more packet initializations (#1507) 2023-09-19 00:02:17 -07:00
Kamron Batman
0bec97639f
feat: Adds KR/EC client versions (No actual support yet). (#1506)
### Summary

- Adds KR/EC client versions to ClientVersion
- Adds distinction for enhanced versions in the page queue
- Adds KR Expansion flags
- Adds ProtocolChange enum support
- Adds missing Moongate checks for TerMur
- Adds better message for why a client version is not supported, and which ones are supported.
2023-09-18 23:53:18 -07:00
dependabot[bot]
cee4d0fb15
chore(deps): Bump xunit.runner.visualstudio from 2.5.0 to 2.5.1 (#1504)
Bumps [xunit.runner.visualstudio](https://github.com/xunit/xunit) from 2.5.0 to 2.5.1.
- [Commits](https://github.com/xunit/xunit/compare/2.5.0...2.5.1)

---
updated-dependencies:
- dependency-name: xunit.runner.visualstudio
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-09-18 16:37:53 -07:00
dependabot[bot]
a616d0fcd2
chore(deps): Bump xunit from 2.5.0 to 2.5.1 (#1505)
Bumps [xunit](https://github.com/xunit/xunit) from 2.5.0 to 2.5.1.
- [Commits](https://github.com/xunit/xunit/compare/2.5.0...2.5.1)

---
updated-dependencies:
- dependency-name: xunit
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-09-18 16:36:49 -07:00
sp1r17
11e1b425d9
fix: Hireables should not drop their pack (#1503) 2023-09-17 17:24:59 -07:00
Kamron Batman
856db6a64c
fix: Fixes variable name in stamina system (#1501) 2023-09-16 18:10:55 -07:00
Kamron Batman
0f2870e94a
feat: Adds Stamina System to overhaul overweight. (#1465)
## Overhaul to Stamina (Overweight) System

### Added configurations

```json
{
  "settings": {
    "stamina.enableMountStamina": "True",
    "stamina.cannotMoveWhenFatigued": "True",
    "stamina.stonesPerOverweightLoss": "25",
    "stamina.stonesOverweightAllowance": "4",
    "stamina.baseOverweightLoss": "5",
    "stamina.additionalLossWhenBelow": "0.1",
    "stamina.mountLastMoveStepsReset": "01:00:00:00",
    "stamina.enableMountStamina": "True",
    "stamina.useMountStaminaOnlyWhenOverloaded": "False",
  },
}
```
- `stamina.cannotMoveWhenFatigued` - By default, Pre-AOS expansions will outright block a player if they are fatigued. A player is fatigued when they run out of stamina by any mechanism.
- `stamina.stonesPerOverweightLoss` - The amount of stamina lost for every X stones above overweight. Example, if a person is overweight 28 stones overweight, then there there is 1 additional stamina. 28 / 25 = 1 (no decimals, no rounding)
- `stamina.stonesOverweightAllowance` - The number of stones allowed before overweight penalties take affect. This _does not_ substract from the overweight stones calculation.
- `stamina.baseOverweightLoss` - The base amount of stamina loss for being overweight. While running, the final amount is multiplied by 2. If mount stamina is turned off, then the final amount is divided by 3 while mounted. 

### Mount stamina

Mounts have a new property `StepsMax` to determine the maximum steps they can take before being fatigued. To regain steps, the player _must stay on the mount and not move_ (per OSI). The gain rates are configurable. If a mount is dismounted, or the player logs off, the mount is considered inactive and mounts regain all of their steps after _24 hours_.

### Changes to player stamina

Players now have a proper inactivity time reset for their steps. If a player is idle for 16 seconds (including logging off, or the server being offline), then the steps counter is rest.

### Developer Notes

- `StepsTaken` - This field has been removed. It was not serialized and could not be used reliably. If a developer was using it, then the recommendation is to build a mechanism to track total steps another way.
- `IHasStamina` - This new interface was added and currently `IMount` and `PlayerMobile` are valid types.

Important: Entities that are sent to the StaminaSystem for tracking (mounts, players, or something else), must be an `ISerializable` to be serialized properly. If they are not, then the serialization system will record a null, and nothing will be deserialized upon world load. No errors will be given.
2023-09-16 17:52:54 -07:00
Kamron Batman
25eaff0b68
fix: Fixes arbitrage by adjusting prices. (#1500) 2023-09-15 19:22:35 -07:00
Kamron Batman
3481f60f5b
fix: Updates PollGroup to use the refactored/fixed changes by stefanomerotta (#1499) 2023-09-13 23:58:28 -07:00
Kamron Batman
dfca90e076
chore: Adds VC++ Redist 17 to README (#1498) 2023-09-13 22:54:32 -07:00
Kamron Batman
0d6a77ea74
fix: Bumps PollGroup to fix NPE error (#1497) 2023-09-13 12:51:12 -07:00
Kamron Batman
f4b329f203
fix: Bumps PollGroup to fix arm support (#1496) 2023-09-13 10:26:15 -07:00
Kamron Batman
319d82fec9
fix: Renames WeightOverloading to StaminaSystem (#1495) 2023-09-06 18:58:45 -07:00
Kamron Batman
6820efe0ca
fix: Codegens and fixes bugs with plant system (#1490) 2023-09-05 19:20:03 -07:00
dependabot[bot]
01b864f955
chore(deps): Bump MailKit from 4.1.0 to 4.2.0 (#1493)
Bumps [MailKit](https://github.com/jstedfast/MailKit) from 4.1.0 to 4.2.0.
- [Changelog](https://github.com/jstedfast/MailKit/blob/master/ReleaseNotes.md)
- [Commits](https://github.com/jstedfast/MailKit/compare/4.1.0...4.2.0)

---
updated-dependencies:
- dependency-name: MailKit
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-09-04 22:37:30 -07:00
Kamron Batman
2d7a65d479
fix: Adds gate travel migration file (#1491) 2023-09-03 21:28:50 -07:00
Christian
5d6045eefd
docs: Adds starting a server notes and tips. (#1487) 2023-09-03 09:49:42 -07:00
Kamron Batman
74c6d91d34
fix: Fixes NPE in FilterAccess (#1488) 2023-09-03 09:33:12 -07:00
Kamron Batman
60a6796903
feat: Adds ping listener on port 12000 for server list (#1485) 2023-09-03 09:29:27 -07:00
Kamron Batman
15227a53ec
fix: Fixes gate travel location issue (#1484) 2023-09-01 09:23:10 -07:00
Kamron Batman
1b07b00510
fix: Fixes hellsteed name (#1480) 2023-08-30 09:05:38 -07:00
dependabot[bot]
083bfe7294
chore(deps): Bump Microsoft.NET.Test.Sdk from 17.7.1 to 17.7.2 (#1479)
Bumps [Microsoft.NET.Test.Sdk](https://github.com/microsoft/vstest) from 17.7.1 to 17.7.2.
- [Release notes](https://github.com/microsoft/vstest/releases)
- [Changelog](https://github.com/microsoft/vstest/blob/main/docs/releases.md)
- [Commits](https://github.com/microsoft/vstest/compare/v17.7.1...v17.7.2)

---
updated-dependencies:
- dependency-name: Microsoft.NET.Test.Sdk
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-08-30 08:35:42 -07:00
Kamron Batman
e5439c1a8f
fix: Adds a timeout and retry to public IP detection (#1478) 2023-08-29 08:56:50 -07:00
Kamron Batman
ea7302de59
fix: Removes echo in publish that is not needed (#1477) 2023-08-29 07:53:10 -07:00
Kamron Batman
fc3aa13b8e
fix: Fixes memory leak in virtual mounts and codegens specials (#1476) 2023-08-25 22:42:23 -07:00
Kamron Batman
701deb53c1
fix: Codegens healers and fixes flamespurt trap (#1475) 2023-08-25 21:20:13 -07:00
Kamron Batman
daa154d6de
fix: Codegens guards (#1474) 2023-08-25 20:59:11 -07:00
Kamron Batman
877595c513
fix: Codegens familiars (#1473) 2023-08-25 20:51:38 -07:00
Kamron Batman
06309d9d59
fix: Codegens traps (#1472) 2023-08-25 20:44:37 -07:00
mdodkins
c341d6ad3e
fix: Fixes expansions.json to correctly reflect name of supported feature flags (#1471) 2023-08-25 11:55:36 -07:00
Kamron Batman
c5d26eb8cd
fix: Removes unused SetOldSaveFlag method (#1470) 2023-08-23 22:48:26 -07:00
Kamron Batman
ffb0b24e1a
fix: Codegens talismans - Fixes memory leak (#1469) 2023-08-23 22:41:48 -07:00
Kamron Batman
ca89b8b08b
fix: Codegens suits and adds StaffRobe to replace the others (#1468) 2023-08-23 20:25:54 -07:00
Kamron Batman
f56cb31db6
fix: Codegens rest of special items (#1467) 2023-08-23 20:02:46 -07:00
mdodkins
976680699c
fix: Fixes JSON TypeConverter so that it can deserialize full name types (#1466) 2023-08-21 21:25:09 -07:00
Stefano Merotta
52ca1fe686
fix: Replaces FindItem(s)ByType(s) implementation with BFS strategy (#1454) 2023-08-18 22:05:28 -07:00
Kamron Batman
cd5fddd8d1
fix: Fixes stat mod having no owner (#1463) 2023-08-17 22:01:44 -07:00
Kamron Batman
35b7c35167
fix: Fixes tests to use C# 11 syntax (#1462) 2023-08-17 00:58:44 -07:00
Kamron Batman
2c8d54549c
fix: Changes C# version to 11 (#1461) 2023-08-16 20:12:56 -07:00
Kamron Batman
2eebf4f48d
fix: Fixes crashing while trying to detect old clients (#1459) 2023-08-16 00:20:26 -07:00
Kamron Batman
1e422db9e8
fix: Fixes boat movement switching (#1458) 2023-08-16 00:05:07 -07:00
Kamron Batman
6e23afe7c6
fix: Fixes boat movement bugs & distorted entity placement. (#1457) 2023-08-15 21:14:56 -07:00
Stefano Merotta
431e7fb8fe
fix: Fixes FindItemsByType<T> that block recursion (#1453) 2023-08-12 15:37:13 -07:00
Kamron Batman
455b11de9c
fix: Updates schema generator to 2.6.4 (#1452) 2023-08-11 00:09:56 -07:00
Kamron Batman
09fc30546b
fix: Fixes RunebookEntry serialization and removes unused migrations (#1449)
### Summary

- [X] Removes migration files were committed that aren't actually (and have never) been used.
- [X] Moves runebook entry from being manually serialized to using the serialization generator.
2023-08-10 19:27:01 -07:00
mdodkins
23c729f31b
fix: Fixes PlayerVendor customizations not working due to constructor issues (#1451) 2023-08-10 19:24:45 -07:00
Kamron Batman
8389bfacfe
chore: Updates copyright (#1448) 2023-08-09 09:09:26 -07:00
dependabot[bot]
b4b3af84d4
chore(deps): Bump Serilog from 2.12.0 to 3.0.1 (#1447)
Bumps [Serilog](https://github.com/serilog/serilog) from 2.12.0 to 3.0.1.
- [Release notes](https://github.com/serilog/serilog/releases)
- [Commits](https://github.com/serilog/serilog/compare/v2.12.0...v3.0.1)

---
updated-dependencies:
- dependency-name: Serilog
  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>
2023-08-09 08:51:52 -07:00
Kamron Batman
c120783fba
chore: Adds dependabot.yml (#1446) 2023-08-09 08:21:01 -07:00
Kamron Batman
bde5357f89
fix: Removes Moq due to privacy concerns (#1445)
Due to privacy/security concerns, Moq has been removed.

See: https://github.com/moq/moq/issues/1372
2023-08-09 08:12:45 -07:00
Kamron Batman
0fa2e5cedb
fix: Fixes publish architecture parameter (#1444) 2023-08-08 09:15:35 -07:00
Kamron Batman
bd661da81c
fix: Fix sending blockers to GMs (#1441) 2023-08-05 19:44:47 -07:00
Kamron Batman
29ffc9cd52
fix: Fixes champs stop (#1440) 2023-08-03 20:36:01 -07:00
Kamron Batman
4b8e2d407b
fix: Fixes champs start (#1439) 2023-08-01 19:46:54 -07:00
mdodkins
a9a2a89908
feat: Customize expansion and set maps on first boot (#1425) 2023-07-31 20:46:49 -07:00
Kamron Batman
3ffe049e08
fix: Fixes champ spawns using wrong constructor (#1438) 2023-07-31 13:53:24 -07:00
Kamron Batman
56c40fffbd
fix: Fixes RemoveFollowers called twice (#1437) 2023-07-29 15:44:23 -07:00
vexyl
d6ebe67dd8
fix: Fixes untamed creatures not entering houses (#1436) 2023-07-29 15:43:39 -07:00
Kamron Batman
b8df42721f
fix: Fixes inaccessible items. (#1435)
### Summary
- [X] Fighters and Paladins did not have the correct strength to equip certain items in AOS+
- [X] Removed Chaos/Order shields from Fighters
- [X] Moved all item equips so they are done after stats are assigned.
2023-07-28 19:29:06 -07:00
Kamron Batman
6547239fa6
fix: Fixes NPE in champ titles (#1434) 2023-07-28 00:39:10 -07:00
Kamron Batman
51ecee6caa
fix: Overhauls champion titles & codegen champion system (#1430)
## MAJOR CHANGE

Added a champion title system to facilitate the existing champion titles. This should make it easier to extend or create other related game content. Champion titles will be saved in a folder called _ChampionTitles_.

### Motivation

The motivation to refactor was two-folder, but mostly related to performance in two ways.
First, every player had a ChampionTitleInfo object with an array of ChamptionTitleInfo. We want to eliminate the need for this information unless a player actually uses it. This should save a considerable amount of memory.

Second, to facilitate the atrophy mechanic, the champion titles would run atrophy post-world save, adding to the time that the server is frozen. Eliminating this post-world save side effect unlocks our ability to further optimize the world save process since there are no direct side effects.


### Bugs fixed

- [X] Fixed titles getting cut off on the paperdoll
- [X] Fixed champion title not displaying overhead (OPL)

### Screenshots
<img width="216" alt="image" src="https://github.com/modernuo/ModernUO/assets/3953314/8916f895-8d68-4fb0-892e-108a0c43be90">
2023-07-27 21:44:06 -07:00
Kamron Batman
4dddc8d3f1
fix: Fixes NPE in BaseRanged.OnMiss (#1433) 2023-07-27 09:17:52 -07:00
Kamron Batman
72557ffb97
fix (readme): Update README.md (#1432) 2023-07-26 20:54:10 -07:00
Kamron Batman
d9d04f1f38
fix: Adds better arm64 support (#1409) 2023-07-26 20:43:42 -07:00
mdodkins
d366e02251
fix: Fixes missing ordinal string comparisons arguments. (#1431)
---------

Co-authored-by: Kamron Batman <3953314+kamronbatman@users.noreply.github.com>
2023-07-23 20:12:38 -07:00
Kamron Batman
8eb480bdf5
fix: Fixes out of range crash with murder context (#1429) 2023-07-22 09:12:04 -07:00
Kamron Batman
f77dd91811
fix: Overhauls virtue system (#1376)
## MAJOR CHANGE (API BREAKING)

Moved the virtues to it's own system _VirtueSystem_. This should make it easier to extend or remove the virtue system. Virtues will be saved to a new folder called _Virtues_.

### Motivation

The motivation was also two-fold, performance/stability, and to fix bugs.

First, virtues is the second system (first is murders), that has a pre-world-save check on _every mobile in the game_ to atrophy virtue stats. This is taxing since it freexes the world and makes world saves take longer. Every mobile had Gain/Loss dates for each virtue, whether they needed them or not. Most players don't even use the virtue system, so this will increase performance considerably.

Second, when I tried to optimize/refactor the code, I found several bugs that needed to be fixed.

### Major API Changes

- [X] The properties on players related to virtues are gone. Use `pm.GetVirtues()?.<PropertyName>` instead.
- [X] Virtues were removed from non-player Mobiles.
- [X] `pm.JusticeProtectors` was removed. Use `JusticeVirtue.GetProtector()` or `JusticeVirtue.GetProtected()` instead.

### Screenshots

<img width="221" alt="Props-1" src="https://github.com/modernuo/ModernUO/assets/3953314/5c09cb83-b8d5-44a2-b899-a7ed7cd736ac">
<img width="220" alt="props-2" src="https://github.com/modernuo/ModernUO/assets/3953314/aeade4d3-8df0-47bd-955a-a864d0946cf7">
2023-07-19 21:43:47 -07:00
Kamron Batman
063d878276
fix: Cleans up murder system (#1428) 2023-07-16 00:04:30 -07:00
Kamron Batman
95779a27fb
fix: Removes unused RecentlyReported from PlayerMobile (#1427) 2023-07-15 23:15:00 -07:00
Kamron Batman
26f784f45d
fix: Overhauls murder system (#1419)
## MAJOR CHANGE (API BREAKING)

Added a player murder system to facilitate reporting murders. This should make it easier to extend to create a bounty system or  other related game content. Player murders will be saved in a folder called _PlayerMurders_.

### Motivation

The motivation was two-fold, performance, and bug fixes.

First, murders are one of two systems that do a pre-world-save check on _every mobile in the game_ to decay kills and set their expiring murders. This is taxing since it freezes the world and makes world saves take longer. Every mobile has ShortTermMurders even though it is a player concept. And next, 90%+ of players are not murderers but had an ever increasing MurderElapse time that was being tracked against GameTime. These properties were also serialized unnecessarily for all mobs.

Second, when I tried to optimize/refactor the code, it was obvious that the system has bugs.

### Major API Changes
- [X] Created a player murder system and moved `ShortTermMurders`, `ShortTermElapse`, and `LongTermElapse` to the system.
- [X] Added convenience property `PlayerMobile.ShortTermMurders`.
- [X] Added convenience properties `PlayerMobile.ShortTermMurderExpiration` and `PlayerMobile.LongTermMurderExpiration`
- [X] Moved ReportMurdererGump.cs
- [X] Adds an `EventSink.PlayerDeleted` event.

### Notes
The system currently does not support NPCs. To support expiring murders on NPCs I highly recommend a different architecture for large servers (500k+ mobs including players). Specifically switching from looping through all MurderContext to a time-order link list.
2023-07-15 22:42:49 -07:00
Mink80
d65e5bb37b
fix: Exclude higher privileged mobiles in AdminGump (#1423)
---------

Co-authored-by: Kamron Batman <3953314+kamronbatman@users.noreply.github.com>
2023-07-15 22:12:38 -07:00
Mink80
e9fa2bf9fa
fix: Prevent setting properties of higher privileged mobiles (#1420)
---------

Co-authored-by: Kamron Batman <3953314+kamronbatman@users.noreply.github.com>
2023-07-15 22:01:33 -07:00
Kamron Batman
8ff105ac46
fix: Fixes NPE in boomerang (#1426) 2023-07-15 10:07:35 -07:00
Mink80
3825b16132
fix: Fixes GetAllSharedAccounts and other list conversions in AdminGump (#1422)
---------

Co-authored-by: Kamron Batman <3953314+kamronbatman@users.noreply.github.com>
2023-07-08 15:20:00 -07:00
Mink80
27e00cc73f
fix: Fixes spellbook not showing SpellCount when added non-empty. (#1424) 2023-07-04 16:14:04 -07:00
Kamron Batman
3b97e8d39c
fix: Fixes stabled, abilities targeting self, and unsummon memory leak (#1418)
## **MAJOR CHANGE**
* `Stabled` has been moved to `PlayerMobile`.
* New methods added, `PlayerMobile.AddStabled` and `PlayerMobile.RemoveStabled`.
* Added `PlayerMobile.AddFollower` and `PlayerMobile.RemoveFollower`.
* `Stabled`, `AutoStabled`, and `AllFollowers` are now `HashSet` and **_CAN BE NULL_**.

### Summary
* Fixes monster abilities causing harm to the monster through reflect
* Adds `CanTriggerAgainstSelf` to override this for healing or some other self-affecting ability
* Fixes a major memory leak where `UnsummonTimer` from animated dead spell lasts up-to 24hrs and therefore holds onto references of dead/deleted mobs.
* Fixes another minor leak where a mob is not unregistered from the animated dead spell list until the next spell cast.
2023-07-03 09:45:22 -07:00
vexyl
d99e72db8f
fix: Fixes drydocking when EffectItem type items are on the deck (#1416) 2023-06-27 18:12:58 -07:00
Kamron Batman
db4b01567f
fix: Fixes crash with antimacro code (#1415) 2023-06-23 17:10:47 -07:00
Kamron Batman
c93fa004a3
chore: Bumps dependencies (#1414) 2023-06-20 22:57:16 -07:00
vexyl
8f26ad4ac1
fix: Fixes mana drain crash on debug builds (#1413) 2023-06-20 21:22:47 -07:00
mark1145
5e16149b81
fix: Explosion doesn't properly cleanup if mobile fails harmful check (#1412) 2023-06-17 19:42:46 -07:00
Kamron Batman
baed849f10
feat: Moves skills to json file (#1411) 2023-06-16 05:55:55 -07:00
Gnai
c18d0d23ff
fix: Fixes NPE crash from "i wish to duel" (#1410) 2023-06-05 22:50:15 +02:00
Kamron Batman
f2d0f6616e
fix: Fixes NPE with effect controller (#1406) 2023-05-21 23:29:16 -07:00
Kamron Batman
a7edba3712
fix: Removes scoped for gump AppendTo (#1405)
### Summary
.NET 6 had a bug that required the `scoped` keyword for the gump AppendTo. It looks like this was fixed since then.
2023-05-21 00:46:25 -07:00
Kamron Batman
35237ff7d1
fix: Fixes leaking/errors caused by categorization (#1404)
### Summary
- [X] Fixes 4 TormentedMinotaur created at `(0, 0, 0) [null]` when `[add` is used.
- [X] Fixes client crash from a bug in HouseRaffleStone when `[add` is used.

Recommend viewing with [_whitespace disabled_](https://github.com/modernuo/ModernUO/pull/1404/files?diff=split&w=1)
2023-05-19 18:41:31 -07:00
Kamron Batman
f92d821168
fix: Fixes timezone issue (#1388)
### Summary
Removes the timezone specific logic in favor of globalization non-invariance. This should fix culture issues too.
2023-05-19 16:40:06 -07:00
Kamron Batman
0a9bfb0558
fix: Drastically simplifies regions (#1400)
### Summary
- [X] Gets rid of the Dtos
- [X] Simplifies the json serializer registration
- [X] Adds a custom `RegionByName` JsonConverter that can look up other regions that have already been registered.


### BREAKING CHANGE
1. **Child regions must appear after their parents in the JSON file**
2. In the regions json file, _"Parent"_ can no longer be just a string. It must be an object that includes the map.
      - ```json
        "Parent": { "Name": "Britain", "Map": "Felucca" }
        ```
2023-05-19 16:39:40 -07:00
Kamron Batman
734d20cbaf
fix: Overhauls antimacro system (#1403)
### Summary
- [X] Moves AntiMacro to it's own system.
- [X] Removes antimacro from PlayerMobile.
- [X] Adds `LastExpiration` to antimacro to easily clear out all antimacro tracking when a player logs out, or during world save.
- [X] Optimizes the code somewhat.
2023-05-19 16:32:51 -07:00
Kamron Batman
372820bb49
fix: Vendors are now invulnerable for LBR+ or by override (#1399)
### Summary

Changes vendors so they are invulnerable with LBR+ era or with the `vendor.isInvulnerable` setting added.
2023-04-28 20:51:39 -07:00
Kamron Batman
4242c446c8
fix: Creates an override for disabling cast paralyze (#1398) 2023-04-28 20:44:30 -07:00
mark1145
55dc4a7da5
fix: Fixes casting by sending ParalyzeFlag to client while animating (#1397) 2023-04-27 23:12:07 -07:00
Kamron Batman
ba4c4627c3
fix: Fixes the limitation on static tiles for harvesting (#1396)
* fix: Fixes the limitation on static tiles for harvesting

* Fixes dirt targeting
2023-04-22 09:14:42 -07:00
Kamron Batman
79c6f0375c
fix: Fixes doors as decorations & optimizes them (#1392) 2023-04-18 19:28:54 -07:00
Kamron Batman
6c09e5a006
fix: Fixes infinite loop in bulletin board cleanup (#1394) 2023-04-18 19:27:40 -07:00
mark1145
5b3ff89e70
fix: Ambiguous AddRes signatures causing bad necro regs display (#1395) 2023-04-18 14:42:01 -07:00
Kamron Batman
cc6fee28d4
feat: Adds hireables (#1393) 2023-04-16 20:24:14 -07:00
Kamron Batman
fea7aef95f
fix: Fixes NPE with setting stats in-game (#1390) 2023-04-13 23:43:09 -07:00
Kamron Batman
5ce657a842
fix: Fixes speech throttle (#1387) 2023-04-05 09:21:44 -07:00
Kamron Batman
41ccc51caa
fix: Fixes death strike never ending (#1386) 2023-04-05 08:58:28 -07:00
romanlysenko
3c86af4ff3
fix: Removes duplicate CheckTransform static method (#1384) 2023-04-03 23:03:40 -07:00
romanlysenko
5c6525de46
fix: Removes duplicate UnderTransformation static method (#1383) 2023-04-03 22:07:12 -07:00
Kamron Batman
a1a50603da
fix: Fixes releases (#1382) 2023-03-26 01:00:10 -07:00
Kamron Batman
e08efad9be
fix: Updates messages with clilocs (#1381) 2023-03-26 00:54:09 -07:00
Kamron Batman
b8b283c210
fix: Fixes mining tiles (#1380)
### Summary
- [X] Fixes missing RangedTiles flag
- [X] Fixes typo in one of the mountain tiles
- [X] Changes sand to be a range.
2023-03-23 21:46:40 -07:00
Kamron Batman
b1416e655d
fix: Fixes static minings (#1379) 2023-03-23 13:03:49 -07:00
Kamron Batman
96e4d6c651
fix: Fixes mining tiles (#1378)
### Summary
- [X] Adds missing 295 tile
- [X] Removes duplicate 296 tile
- [X] Adds missing 602 -> 609 tiles
2023-03-22 22:05:51 -07:00
Kamron Batman
a55b213393
fix: Fixes fastwalk (#1377) 2023-03-20 21:12:33 -07:00
Kamron Batman
6daf536c28
fix: Fixes serializing float (#1375) 2023-03-15 00:37:01 -07:00
Kamron Batman
127ca5e51e
fix: Fixes veteran rewards (#1374)
### Summary
- [X] Fixes a bug in wall banner East setter: [[ServerUO#5046]](https://github.com/ServUO/ServUO/pull/5046)
- [X] Eliminates the timer in tree stump.
- [X] Codegens the rewards
2023-03-12 17:28:51 -07:00
Kamron Batman
e797ec7f53
fix: Fixes special scrolls (#1373) 2023-03-12 13:47:12 -07:00
Kamron Batman
7635db4834
fix: Fixes double call to AfterDeserialization (#1372) 2023-03-12 10:21:04 -07:00
Kamron Batman
6fcd606947
fix: Codegens rares and solen items (#1371) 2023-03-11 19:25:48 -08:00
Kamron Batman
3514651c3c
fix: Codegens mutation core (#1370) 2023-03-11 17:33:31 -08:00
Kamron Batman
f8ab7cfdf8
fix: Optimizes timer in house raffle stone (#1369) 2023-03-11 12:37:40 -08:00
Kamron Batman
88a9e4ea51
docs: Updates FAQ for time zone issues. (#1368) 2023-03-10 00:18:36 -08:00
Kamron Batman
2d30ea4cc5
fix: Updates BufferWriter with more streamlined code (#1367)
### Summary
Updates BufferWriter with more standard ways of writing primitives. Eliminates looping to write a string per @jaedan's suggestion.

Note: This change assumes we don't have crazy large strings.
2023-03-10 00:05:16 -08:00
Kamron Batman
dda55889f6
fix: Fixes tick count calculcations (#1366) 2023-03-09 21:00:17 -08:00
Kamron Batman
0645251ec7
fix: Fixes scales, heritage items, and codegens holiday items (#1365) 2023-03-07 22:38:35 -08:00
Kamron Batman
f1e8d4c6f7
fix: Codegens heritage items (#1364) 2023-03-07 21:29:13 -08:00
Kamron Batman
82b5fa6af3
fix: Codegens gifts (#1363) 2023-03-07 21:07:45 -08:00
Kamron Batman
551f6cca9d
fix: Codegens evil decor. Fixes trash items (#1362) 2023-03-06 19:28:58 -08:00
Kamron Batman
84b3e5cfdb
fix: Codegens BOD rewards. (#1361) 2023-03-06 19:01:14 -08:00
Kamron Batman
3097eb4fa7
fix: Fixes Dice Infinite loop and deserialization of some items (#1360)
### Summary
- Fixes deserialization issues with some items.
- Changes Utility.Dice to prevent long iteration by passing `(uint)-1`
2023-03-05 18:29:48 -08:00
Kamron Batman
9b6aa1deed
fix: Fixes missed codegen for potions (#1359) 2023-03-02 21:27:42 -08:00
Kamron Batman
50d52fb25e
fix: Makes gump type id stable (#1358) 2023-03-02 19:29:57 -08:00
Kamron Batman
57ff488d51
fix: Removes run flag from NPC movement (#1357) 2023-03-02 10:58:17 -08:00
Kamron Batman
0150e1a994
fix: Fixes broken chair (#1356) 2023-02-27 09:01:50 -08:00
Kamron Batman
8e3facc57b
fix: Codegens promo items (#1355) 2023-02-26 00:48:27 -08:00
Kamron Batman
bf6da39176
fix: Codegens tools (#1354) 2023-02-25 13:54:05 -08:00
Kamron Batman
5c99d0cb38
Adds github token to release action 2023-02-25 12:09:31 -08:00
Kamron Batman
d14e864c85
fix: Updates disguises and codegens tinkering items (#1353) 2023-02-25 12:01:06 -08:00
Kamron Batman
f2fdc0321c
fix: Localizes specialized books, codegens instruments (#1352) 2023-02-25 10:56:05 -08:00
Kamron Batman
ac7c3522f4
fix: Updates .NET 7 SDK to 7.0.201 for CI/CD (#1351) 2023-02-25 00:57:18 -08:00
Kamron Batman
42012538bc
fix: Fixes crafter deserialize, cleans up bandages, and codegens misc items (#1350) 2023-02-25 00:54:12 -08:00
Kamron Batman
22376cfc20
Adds Chia to sponsors page 2023-02-24 19:16:27 -08:00
Kamron Batman
10b26741c7
fix: Codegens spellbooks (#1349) 2023-02-24 19:04:21 -08:00
Kamron Batman
8e49fb4da4
fix: Codegens scrolls (#1348) 2023-02-24 18:17:29 -08:00
Kamron Batman
8c4aaeeda6
fix: Codegens potions and fixes a few minor bugs (#1347) 2023-02-24 18:06:59 -08:00
Kamron Batman
42bcf3c95a
fix: Fixes stack overflow from death explosion (#1346) 2023-02-24 16:05:16 -08:00
Kamron Batman
95e55e994f
fix: Fixes young healing each other (#1345) 2023-02-21 18:15:57 -08:00
Kamron Batman
5fd0e94b7d
fix: Fixes negative numbers in commands (#1343) 2023-02-17 00:34:24 -08:00
Kamron Batman
8a0b6c9731
fix: Deletes unsupported WebStatus (#1341) 2023-02-15 19:57:58 -08:00
Kamron Batman
65c196a8ae
fix: Fixes Point2D ctor (#1340) 2023-02-13 23:28:23 -08:00
Kamron Batman
f24c6a08dd
fix: Fixes string interpolation in value string builder (#1339) 2023-02-13 23:09:50 -08:00
Marcelo Paez Sequeira
b46544d752
fix: Moves the rest of the Incoming packets (#1338) 2023-02-10 22:59:36 -08:00
Marcelo Paez Sequeira
a8d8db8f59
fix: Moves accounting, entity, 0xBF, and house packets to UOContent project (#1337) 2023-02-09 18:42:57 -08:00
Kamron Batman
3cf6db73f0
fix: Fixes provocation for pre-pub 16 (#1335) 2023-02-07 23:33:45 -08:00
Kamron Batman
44c69620a6
fix: Tidy up socket dispose. (#1334) 2023-02-07 21:22:04 -08:00
Kamron Batman
14ef1ab7e5
fix: Do not double fetch root parent (#1333) 2023-02-05 12:09:08 -08:00
Kamron Batman
9bb0dbf1ac
fix: Fixes LOS house bug with large items in a container (#1332) 2023-02-05 12:04:52 -08:00
Kamron Batman
2db7513059
fix: Fixes smithy tool craft NPE (#1331) 2023-02-04 09:38:36 -08:00
Kamron Batman
df29fca0c2
fix: Fixes plant issue with old client (#1330) 2023-02-04 09:33:07 -08:00
Kamron Batman
0b4c73a8dd
fix: Fixes infinite loop in PlantItem GetProperties (#1329) 2023-02-04 09:11:33 -08:00
Kamron Batman
a0d5f132b4
fix: Fixes identation in docs note (#1328) 2023-02-02 19:40:15 -08:00
Kamron Batman
1d376029dc
docs: Updates serialization docs (#1327) 2023-02-02 19:37:18 -08:00
Kamron Batman
8b3a2f9f5f
fix: Cleans up create guild gump (#1326) 2023-01-31 23:51:01 -08:00
Kamron Batman
b11ca29987
fix: Fixes text helpers (#1325) 2023-01-31 23:41:36 -08:00
Kamron Batman
982058c224
fix: Fixes weapon proc on 0 damage (#1324)
Weapons should not proc their abilities on 0 damage given.
2023-01-28 10:39:19 -08:00
Kamron Batman
4707666890
feat: Moves antimacro to a configuration (#1323)
### Summary

- [X] Moves antimacro to `Distribution/Configuration/antimacro.json`.
- [X] Disables antimacro by default.
2023-01-23 21:51:05 -08:00
Kamron Batman
4f67b1174f
fix: Some computers are missing timezones apparently (#1322)
Some windows servers are not updated properly. Specifically Windows 10/2016 and older that is missing the September 2020 rollup will throw an error:

See [here](https://support.microsoft.com/en-us/topic/dst-changes-in-windows-for-yukon-canada-september-8-2020-ae74d7b8-82ed-1d35-e131-fb0796b73b10) for information about this update and how to get it.
2023-01-21 19:12:53 -08:00
Kamron Batman
68bd9529f9
Adds hue support to StaticTile (#1321)
### Summary
Fixes missing properties while reading static tiles.

### Screenshots
<img width="463" alt="Screenshot 2023-01-21 093050" src="https://user-images.githubusercontent.com/3953314/213879788-05d2ad7c-c197-4690-9f5b-660bded416cc.png">

Closes #1320
2023-01-21 11:19:11 -08:00
Kamron Batman
b18dfcbb63
fix: Simplifies ore stack logic (#1319) 2023-01-03 18:07:42 -08:00
Kamron Batman
f47560e56c
fix: Stops invisibility timer on successful hide (#1318) 2023-01-01 10:11:51 -08:00
Kamron Batman
3f400fc965
docs: Adds tzdata to README (#1317) 2022-12-30 15:32:54 -08:00
Kamron Batman
33aa257344
fix: Fixes missing codegen for ShipWright (#1316) 2022-12-30 10:48:42 -08:00
Kamron Batman
40c6ed2cb7
fix: Adds codegen migration for Forest Ostard and Acid (#1315) 2022-12-30 09:43:52 -08:00
Kamron Batman
a465c06ddf
fix: Fixes forest ostard codegen (#1314) 2022-12-30 09:39:51 -08:00
Kamron Batman
6bdbf8ce56
fix: Removes AfterDeserialization for some monsters (#1313) 2022-12-28 09:50:33 -08:00
Kamron Batman
7591d687cd
chore: Fixes release (#1312) 2022-12-27 16:59:17 -08:00
Kamron Batman
e8396d25ec
fix: Fixes ValueStringBuilder infinite loop (#1311) 2022-12-27 16:56:59 -08:00
Kamron Batman
e855ca1e61
fix: Removes PoolOfAcid (#1310)
## BREAKING CHANGES
You will receive a deserialization error saying cannot find type PoolOfAcid. This is ok, just hit yes to delete all.
2022-12-22 13:20:34 -08:00
Arutosio
801f139568
fix: Changes on Vendor>NPC to use code generation (#1308) 2022-12-22 12:40:26 -08:00
Kamron Batman
94199f1186
fix: Adds migration checks to CICD, updates dependencies, adds Fedora 37, alpine 3.17 support (#1309)
- [X] Automatically collapses migration files in github PRs.
- [X] CI/CD now checks to see if migrations are missing or modified.
- [X] Updates dependencies
- [X] Adds Fedora 37, Alpine 3.17 support
2022-12-22 12:12:47 -08:00
Arutosio
75d8a5147c
fix: Changes on Vendors>NPC>Guildmasters to use code generation (#1307) 2022-12-21 20:35:18 -08:00
Arutosio
6eebf358da
fix: Changes on Animals to use code generation (#1306) 2022-12-20 23:31:44 -08:00
Kamron Batman
7a1d5131c8
fix: Smooths out throwing explosion potions (#1303)
* Fix: The potion explodes on the user if it is thrown with not enough time. Instead, the timer is stopped, and the potion explodes instantly at the target location.
* Fix: When the potion appears based on how far you throw it.
* Fix: Fixes total timer amount of time.
2022-12-19 14:44:07 -08:00
Kamron Batman
d172b5f742
docs: Fixes README build badge 2022-12-18 11:50:31 -08:00
Arutosio
8a9c6dc882
fix: Changes on Monsters to use code generation (#1304) 2022-12-17 19:40:04 -08:00
Kamron Batman
c266294e31
fix: Fixes Point3D.Parse (#1305) 2022-12-17 17:06:12 -08:00
Kamron Batman
43d33759df
fix: Fixes travel restriction messages (#1302) 2022-12-13 16:47:04 -08:00
Arutosio
b03e2efdd6
fix: Changes magic humanoid monsters to use code gen (#1301) 2022-12-13 16:13:38 -08:00
Arutosio
00b3a849b6
fix: Changes on Mele Humanoid Monsters to use code generation (#1300) 2022-12-13 14:33:05 -08:00
Kamron Batman
899c2e868d
fix: Codegens tools and magical skill items (#1299) 2022-12-07 20:46:41 -08:00
Kamron Batman
f314c63175
fix: Fixes Point3D TryParse (#1298) 2022-12-07 20:11:32 -08:00
Kamron Batman
8e80c7f9c5
fix: Fixes CanTrigger infinite recursion (#1297) 2022-12-07 18:01:01 -08:00
Kamron Batman
bfb987c758
fix: Fixes client crash while selling items to NPC (#1296) 2022-12-05 20:16:31 -08:00
Mink80
f82f61b4c5
fix: Fixes old version not being passed properly to OnRegionChange (#1295) 2022-12-05 17:11:51 -08:00
Kamron Batman
b1cfb10ef8
fix: Removes duplicate stat offsets (#1293) 2022-12-04 14:23:02 -08:00
Kamron Batman
bd1db66858
fix: Fixes stat offset effects (#1292) 2022-12-04 14:20:17 -08:00
Kamron Batman
284186359d
fix: Fixes buff icons for confidence, honorable excution, and divine fury (#1291) 2022-12-04 13:52:44 -08:00
Kamron Batman
1745faa28c
fix: Fixes divine fury (#1290) 2022-12-04 12:39:30 -08:00
Kamron Batman
81839beedd
fix: Removes serial ctor that isnt needed (#1289) 2022-11-28 23:48:46 -08:00
Kamron Batman
af29fa0243
fix: Codegens fishing pole (#1288) 2022-11-28 23:42:32 -08:00
Kamron Batman
0b4cd460de
fix: Codegens corpses (#1287) 2022-11-28 23:08:38 -08:00
Kamron Batman
c8a804d0b1
feat: Adds allow skill gain to regions (#1286) 2022-11-28 20:46:10 -08:00
Fabrizio
537526a328
fix: Update LabelTo, SendMessage, etc to Interpolated Strings (#1283) 2022-11-28 19:58:12 -08:00
Kamron Batman
0facd73842
fix: Fixes travel issues (#1285) 2022-11-28 17:36:00 -08:00
Mink80
6d34cf8824
fix: Fix typo in MerchantTitle (#1284) 2022-11-27 23:17:52 -08:00
Mink80
7ea3200678
fix: Fixes division by zero exception with skill checks (#1282) 2022-11-26 12:18:47 -08:00
Kamron Batman
eae624120a
fix: Fixes pre-codegen deserialization of base quiver (#1281) 2022-11-24 11:34:37 -08:00
Kamron Batman
c9bdaf8069
fix: Fixes pre-codegen deserialiation of base quiver (#1280) 2022-11-24 11:31:31 -08:00
Kamron Batman
930431a6c5
fix: Fixes resources, SOS messages, and codegens fishing (#1279) 2022-11-24 11:12:44 -08:00
Kamron Batman
b006501535
fix: Codegens blacksmith, camping, and carpenter skill items (#1278) 2022-11-24 00:57:23 -08:00
Kamron Batman
9c74276240
fix: Fixes Dot abilities. Adds blood bath attack (#1277) 2022-11-23 19:40:48 -08:00
Kamron Batman
f884193b70
fix: Fixes single target abilities. Adds drain life ability. (#1276) 2022-11-23 19:06:27 -08:00
Kamron Batman
46afffd7d0
fix: Adds summon pixies. Fixes other abilities (#1274) 2022-11-23 17:39:23 -08:00
Kamron Batman
b75a8f72ec
fix: Codegens shields (#1273) 2022-11-23 16:42:17 -08:00
Kamron Batman
82637a77cd
fix: Codegens resources (#1272) 2022-11-23 13:45:32 -08:00
Kamron Batman
59ed0a5bb4
fix: Codegens quivers (#1271) 2022-11-23 13:37:39 -08:00
Kamron Batman
c546b2ec70
fix: Codegens potted plants (#1270) 2022-11-23 13:04:30 -08:00
Kamron Batman
80d7b49bcc
fix: Codegen new haven quest rewards (#1269) 2022-11-23 13:00:28 -08:00
Kamron Batman
520f15e46c
feat: Adds ability groups, DOT abilities, and combat abilities (#1264)
- [X] Adds `MonsterAbilitySingleTarget`
- [X] Adds `MonsterAbilitySingleTargetDoT`
- [X] Adds monster ability groups with weighted abilities
- [X] Adds rune corruption ability
- [X] Adds poison gas counter ability
- [X] Adds stun attack ability
- [X] Adds throw hatchet counter ability
- [X] Adds fanning fire ability
- [X] Fixes destroy equipment to skip spell channel items
2022-11-23 12:33:27 -08:00
Kamron Batman
df9a58dd49
feat: Adds IAosItem for items with AosAttributes (#1268) 2022-11-23 10:49:30 -08:00
Kamron Batman
02cee4815e
feat: Adds monster ability groups with weights (#1267) 2022-11-22 20:13:40 -08:00
Kamron Batman
8b5bd9ec13
feat: Adds monster abilities to combat actions (#1266) 2022-11-22 20:05:19 -08:00
Kamron Batman
efaf2e947a
feat: Adds weighted values (#1265) 2022-11-22 19:53:20 -08:00
Kamron Batman
1d8e8ec0a8
fix: Fixes travel restriction messages (#1263)
- [X] `BaseRegion.CheckTravel` now properly cascades through parent regions
- [X] `SpellHelper.CheckTravel` now returns a failure message instead of sending one internally.
- [X] Consolidates travel restriction messages so they aren't duplicated.
2022-11-22 16:57:42 -08:00
Kamron Batman
2057fe74a2
fix: Fixes no logout delay regions. (#1262) 2022-11-22 13:16:30 -08:00
Kamron Batman
645c1949a1
fix: Fixes ToBoolean (#1261) 2022-11-22 13:12:54 -08:00
Mink80
654f96c44e
fix: NullReference in ChampionTileInfo (#1258) 2022-11-21 20:35:45 -08:00
Kamron Batman
1f778cfacf
fix: Fixes regions having the wrong type (#1260)
## Possible Breaking Change
* Reverted regions.json back to RunUO until newer regions are implemented and proper expansion checks are added.

### Other Changes
- [X] Adds `Region.IsPartOf<T1, T2>()` to check for multiple types.
- [X] Fixes regions.json being wrong
- [X] Adds lots of missing regions. **They are not implemented properly yet**
- [X] Adds regions.xml -> regions.json (check ModernUO Discord)
- [X] Adds expansion specific regions.
2022-11-21 16:47:40 -08:00
Mink80
f9c72a62df
fix: Check for same map in BaseCreature.TeleportPets() (#1259) 2022-11-21 15:30:30 -08:00
Kamron Batman
18c4459e6b
fix: Fixes region priority and resolution. (#1254)
- [X] Fixes world location JSON deserializer.
- [X] Fixes region resolver and priorities.
- [X] Removes DynamicJson from regions.
- [X] Adds missing region music.
2022-11-21 10:38:20 -08:00
Voxpire
139be1bc23
fix: Fixes mount block NPE (#1257) 2022-11-21 07:11:02 -08:00
Kamron Batman
4c734a23b9
fix: Makes NPC active movement half think speed (#1256) 2022-11-20 20:05:43 -08:00
Mink80
10446426c0
fix: Fixes duping of spawners (#1255) 2022-11-19 17:46:44 -08:00
Kamron Batman
0abf72ed2f
feat: Adds JSON convertibles (#1253) 2022-11-15 18:19:05 -08:00
Kamron Batman
ff79d3db40
fix: Fixes NPE from AddStatMod (#1252) 2022-11-15 15:53:41 -08:00
Kamron Batman
32c15f214b
feat: Adds GUID support for codegen (#1251) 2022-11-14 21:25:51 -08:00
Kamron Batman
58acfcabad
fix: Codegens teleporters (#1250) 2022-11-14 20:56:08 -08:00
Kamron Batman
936000ecf7
feat: Adds SerializedCommandProperty (#1249)
## Breaking Change
* Removes `SerializableFieldAttrAttribute` in favor of `SerializedPropertyAttr`.

## Important Change
* Adds `SerializedCommandProperty`

Example:
```cs
    [InvalidateProperties]
    [SerializableField(0)]
    [SerializedCommandProperty(AccessLevel.GameMaster)]
    private Mobile _completedBy;
```
2022-11-14 18:24:33 -08:00
Kamron Batman
5660f4636e
fix: Finishes code gen for misc items (#1248) 2022-11-13 23:10:13 -08:00
Kamron Batman
6d00b2caa9
fix: Fixes critical serial dupe bug and deserialization/serialization issues. (#1245) 2022-11-13 15:54:58 -08:00
Kamron Batman
e66efefff3
fix: Removes the old ore info. (#1247) 2022-11-13 00:41:26 -08:00
Kamron Batman
d8cfb6b935
fix: Makes logging more consistent (#1246) 2022-11-13 00:36:23 -08:00
Kamron Batman
8e0d01d4be
fix: Fixes TryParse for Race (#1244) 2022-11-12 00:31:25 -08:00
Kamron Batman
03fb36c869
fix: Adds ISpanParsable and fixes command conditionals (#1241)
* Adds `ISpanParsable<T>`
* Removes `[Parsable]`
* Fixes querying by serial, body, and a few others.
* Adds `Parse` to `Rectangle3D`
* Fixes AutoArchive NPE


Closes #1209
2022-11-12 00:26:02 -08:00
Kamron Batman
d494a9c78c
fix: Fixes plants crashing on older clients (#1243) 2022-11-11 22:49:03 -08:00
Kamron Batman
ed8f2438fd
fix: No acid damage for SC weapons (#1242) 2022-11-11 17:57:13 -08:00
Kamron Batman
e9c6faab33
fix: Updates dependencies for .NET 7 (#1240) 2022-11-11 11:54:29 -08:00
Kamron Batman
6426996f29
chore: Code Cleanup (#1239) 2022-11-10 22:49:25 -08:00
Kamron Batman
4eee508358
fix: Adds logging for possible duplicate objects being added (#1238)
Adds logging for `World.AddEntity<T>` and `World.AddGuild` just in case.
2022-11-10 20:49:51 -08:00
Kamron Batman
712f089f81
fix: Fixes serilog logging (#1237) 2022-11-10 14:47:56 -08:00
Kamron Batman
dc6a70fedd
fix: Fixes the chance of an effect for champion gold (#1236) 2022-11-10 02:12:36 -08:00
Kamron Batman
8e01db8555
fix: Fixes random bias & Adds back champion arties for UOML (#1235)
* Fixes RNG bias, we should never do `Utility.RandomDouble() <=`
* Adds back champion artifacts behind UOML flag.
2022-11-09 16:33:27 -08:00
Kamron Batman
810061db8c
chore: Update from .NET 7 preview to release. (#1234) 2022-11-08 09:46:48 -08:00
Kamron Batman
59dcd24bed
fix: Fixes DeltaQueue recursion, StatMod bug, and memory leak (#1233)
* Fixes recursion with DeltaQueue causing multiple sets of packets to be sent to the client.
* Fixes StatMods that are expired not being checked properly.
* Adds a timer to properly expire/remove stat mods instead of relying on a side-effect.
2022-11-08 09:08:05 -08:00
Kamron Batman
d7d914df6c
fix: Adds ISpanFormattable to Geometry structs (#1231)
* Adds ISpanFormattable to geometry structs and makes ToString() near-zero-allocation.
* Adds IEquatable, and Parsable to Rectangle3D to get it in-line with the other structs.

Closes #1067
2022-11-06 17:37:30 -08:00
Kamron Batman
c75bde55da
fix: Fixes possible NPE in bounce logic (#1232) 2022-11-06 13:55:07 -08:00
Kamron Batman
ab4aaa0fa8
chore: Adds .NET 7 to release workflow (#1230) 2022-11-06 10:04:32 -08:00
Kamron Batman
5bd41d5b68
feat: Adds .NET 7 & Arm64 support (#1229)
## BREAKING CHANGE
**Publishing for linux will no longer include runtimes**

## Major Changes
* Adds .NET 7 support.
* Adds ARM64 support (experimental).
* Changes linux support to be open-ended against anything .NET 7 supports.
* Fixes native library resolutions. (Make sure to install `dev` versions of the libraries)
* Updates README
* Adds Ubuntu 22, CentOS 8/9 Stream to CI/CD.

## TODOs:
* Update modernuo.com docs

Closes #1173
Closes #1159
2022-11-06 09:59:11 -08:00
Kamron Batman
119eec6b61
fix: Fixes serializing strings in GenericPersistence (#1228) 2022-11-04 19:14:50 -07:00
Kamron Batman
448e5bec7b
fix: Fixes multithreading in AllianceInfo serialize (#1227) 2022-11-03 21:00:34 -07:00
Kamron Batman
de39b1036f
fix: Fixes IDE modifying arcane circle migration. (#1226) 2022-11-02 22:20:52 -07:00
Kamron Batman
e47b62c0ae
fix: Fixes serious stacking issue (#1225)
* Fixes a major bug where players can stack items into their backpack infinitely even if they get a message saying they can't.
* Fixes other minor issues and cleans up code.
2022-11-02 22:18:44 -07:00
Kamron Batman
d0ba94aa83
fix: Fixes addons that do not give deeds (#1224) 2022-11-02 19:26:51 -07:00
Kamron Batman
fd6f4239e2
fix: Adds FindItemOnLayer generic (#1223) 2022-11-01 00:49:20 -07:00
mark1145
053dbcbad0
fix: Fixes ignoring mobs, champions, party crash, slayer rarity, boat speedhack, etc (#1222)
* Movie ignore mobiles to Mobile class
* Allow necromancer familiars to ignore mobiles
* ChampionSpawn should not quietly fail when creating new spawn
* Fix client party crash bug: 2 people partied, the leader logs out, other client is hung
* Fix spellbooks creating with magery/meditation only and creating magery multiple times
* Fix BaseRunicTool.GetRandomSlayer() creating more undead slayers than intended
* Do not allow players to dismount each other whilst mounted
* Fix AOS onwards damage increase tooltip and wrong formula in AOS
* Fix monsters killing other monster revenants + animate dead should not attack player pets + other animates
* Fix fire steed having loot pack added twice
* Fix oaks spawn not able to create Unicorns + Kirins via Activator.CreateInstace() due to constructor having name as parametr
* Fix ignoreMobiles flag not propagated to client for non-players.
* Fix harrower tents not leeching from players
* Fix boats speedhacking around the map
* Fix bless, agility etc. not renewing duration
* Fix reveal always worked
* Fix empty constructor for steeds so they don't throw now.
2022-10-31 22:16:50 -07:00
Kamron Batman
910e06767b
fix: Optimizes TextDefinition to eliminate allocations. Removes TextDefinition ctor. (#1221)
### BREAKING CHANGE ###
The constructor for `TextDefinition` has been removed. Instead use `TextDefinition.Of()` or cast the integer/string to TextDefinition.
2022-10-30 16:53:23 -07:00
Kamron Batman
213b7025af
fix: Updates branding of BuyMeACoffee 2022-10-30 02:42:42 -07:00
Kamron Batman
169876fb63
docs: Updates Funding (#1220) 2022-10-30 02:37:35 -07:00
Kamron Batman
8a26afe28b
fix: Fixes regression in primitive UO codegen (#1219) 2022-10-30 01:58:45 -07:00
Kamron Batman
2e78a07cab
fix: Fixes NPE in Champion Titles (#1218) 2022-10-30 01:49:13 -07:00
Kamron Batman
d791feb583
feat: Adds TextDefinition serialization support. (#1217) 2022-10-30 01:42:48 -07:00
Kamron Batman
39e6f62ec9
fix: Codegens more misc items (#1216) 2022-10-29 16:08:23 -07:00
Kamron Batman
32a5825ddd
fix: Bumps serialization generator to add CanBeNull support (#1215) 2022-10-29 15:25:33 -07:00
Kamron Batman
212ce8bc24
fix: Codegens more misc items (#1214) 2022-10-29 00:43:30 -07:00
Kamron Batman
505865e3ed
fix: Codegens misc ML items (#1213) 2022-10-28 20:44:08 -07:00
Kamron Batman
ebbffc3755
fix: Fixes world load not deleting bad objects (#1212) 2022-10-28 16:58:52 -07:00
Kamron Batman
5151ff66b5
fix: Fixes typo in ResetRng command (#1211) 2022-10-27 23:29:51 -07:00
Kamron Batman
49e6c6f2d1
fix: Adds AfterSerialize support. Removes BeforeSerialize support. (#1208)
### Changes
* Implements an AfterSerialize method that is executed synchronously.
* Removes `BeforeSerialize` support since it was dangerous in its current implementation.
* Moves PlayerMobile kill/virtual decay to AfterSerialize.
* Adds kill decay to after Deserialize.
2022-10-27 23:27:22 -07:00
Kamron Batman
deabab575a
fix: Fixes champion titles atrophying immediately (#1210) 2022-10-27 22:15:18 -07:00
Harley Holt
be19d9aae6
fix: Optimizes Point2D by implementing ISpanFormattable (#1203)
Point2D implements a basic TryFormat function. Only a single format is supported: "(X, Y)" where X and Y are base 10 integers.

Uses recent improvements in string interpolation to write the characters to the destination without first allocating memory for boxed arguments or intermediate strings.

This is a partial solution to issue #1067. A similar solution well be implemented in other classes in Geometry if it looks promising.

Benchmarks
---
d6f8dabf3c/Program.cs

Before change:
```
BenchmarkDotNet=v0.13.2, OS=ubuntu 22.04
Intel Core i7-9700K CPU 3.60GHz (Coffee Lake), 1 CPU, 8 logical and 8 physical cores
.NET SDK=6.0.402
  [Host]     : .NET 6.0.10 (6.0.1022.47605), X64 RyuJIT AVX2
  DefaultJob : .NET 6.0.10 (6.0.1022.47605), X64 RyuJIT AVX2


|                           Method |      Mean |    Error |   StdDev |   Gen0 | Allocated |
|--------------------------------- |----------:|---------:|---------:|-------:|----------:|
|                     CallToString |  44.27 ns | 0.068 ns | 0.063 ns | 0.0063 |      40 B |
|               InterpolatedString | 110.64 ns | 0.316 ns | 0.295 ns | 0.0126 |      80 B |
| InterpolatedStringMultiplePoints | 211.36 ns | 0.349 ns | 0.326 ns | 0.0293 |     184 B |
```

After change:
```
BenchmarkDotNet=v0.13.2, OS=ubuntu 22.04
Intel Core i7-9700K CPU 3.60GHz (Coffee Lake), 1 CPU, 8 logical and 8 physical cores
.NET SDK=6.0.402
  [Host]     : .NET 6.0.10 (6.0.1022.47605), X64 RyuJIT AVX2
  DefaultJob : .NET 6.0.10 (6.0.1022.47605), X64 RyuJIT AVX2

|                           Method |      Mean |    Error |   StdDev |   Gen0 | Allocated |
|--------------------------------- |----------:|---------:|---------:|-------:|----------:|
|                     CallToString |  55.13 ns | 0.826 ns | 0.772 ns | 0.0063 |      40 B |
|               InterpolatedString |  55.73 ns | 0.954 ns | 0.892 ns | 0.0063 |      40 B |
| InterpolatedStringMultiplePoints | 105.42 ns | 0.151 ns | 0.134 ns | 0.0101 |      64 B |
```
2022-10-25 12:08:35 -07:00
Kamron Batman
5797c85d75
fix: Handles timers outside of the wheels max capacity of 17yrs (#1204) 2022-10-23 20:29:18 -07:00
Kamron Batman
bae017c45b
fix: Use TryWrite for Serial formatting (#1205) 2022-10-23 18:17:08 -07:00
Kamron Batman
27e8f37294
fix: Bonding is only enabled on LBR+ (#1202) 2022-10-22 21:56:19 -07:00
Kamron Batman
24e2696510
fix: Adds reset RNG command (#1201) 2022-10-22 14:46:23 -07:00
Kamron Batman
28040582bc
fix: Fixes skill mods NPE with Skills (#1200) 2022-10-22 09:56:52 -07:00
Kamron Batman
c711e7ab02
fix: Fixes NPE with resistance mods in Mobile (#1199) 2022-10-22 09:11:56 -07:00
Kamron Batman
f936314326
fix: Removes debug message on timers for attach (#1198) 2022-10-19 21:10:18 -07:00
Kamron Batman
ca3b173c4c
fix: Removes timer error for world in initial state, and removes OPL requirement for spellbooks (#1197) 2022-10-19 02:32:04 -07:00
Kamron Batman
791128f238
fix: Fixes timer orphaning issue (#1196)
* Fixes an edge case where a check to see if a timer was being executed resulted in it not being properly detached on stop. Fixed this by changing how we determine what timer is currently being executed.
* Adds execution count to #DEBUG_TIMERS so shards can get notified (rudimentarily for now) about sequentially executing long chains of timers.

Unfortunately, for now, there is no good option when it comes to executing timers. If we execute let's say 500, and defer the rest, that makes all timers effectively 8ms off until it "catches up". Depending on the shard, that may never happen.
2022-10-18 18:14:26 -07:00
Kamron Batman
919b7e9416
fix: Adds more debugging to the timer system. Simplifies the timer pool. (#1195) 2022-10-17 23:25:49 -07:00
Kamron Batman
d5416b6dec
fix: Fix direction change speed hack (#1194) 2022-10-16 20:17:18 -07:00
Kamron Batman
9b2b34f5fb
fix: Fixes debug say (#1193) 2022-10-16 17:57:14 -07:00
Kamron Batman
5e7dbb53c1
fix: Updates Github Actions & Azure Pipelines (#1192) 2022-10-16 10:10:55 -07:00
Kamron Batman
3396338926
fix: Fixes blocking sockets (#1191) 2022-10-15 22:23:34 -07:00
Kamron Batman
53ebc66a93
fix: Fixes code gen of nested classes (#1190) 2022-10-15 19:58:34 -07:00
Kamron Batman
8479306279
fix: Removes SetTypeRef for generated code. (#1189) 2022-10-15 15:12:49 -07:00
Kamron Batman
ffdc08259f
fix: Adds name support for all mods (#1128) 2022-10-15 10:46:31 -07:00
Kamron Batman
b1cf95f810
fix: Fixes public moongate expansion checks (#1188) 2022-10-14 20:28:16 -07:00
Kamron Batman
e1e30998ba
fix: Adds ReadType/Write(Type) and improves type referencing (#1172)
## Changes
* Improves type hashing by introducing xxHash3 (64bit)
* Removes individual `tdb` files in favor of a single `SerializedTypes.db` file. This file is only used to identify a type that is being deserialized, which doesn't exist.
* Adds duplicate type alias detection
* Adds `AssemblyHandler.FindTypeByHash`

View changed files whitespaces: https://github.com/modernuo/ModernUO/pull/1172/files?diff=split&w=1

## SerializedTypes.db
The serialized types file is used to get back the original name of a type in case it no longer exists in code. This can easily be necessary if a class is renamed in code and no `TypeAlias` is provided.

### Format
byte[4] - version
byte[4] - count
--array--
byte[8] - xxHash
byte[1] - flag, 0 - null, 1 - not null
byte[n] - Full class name in UTF8

### Example
<img width="472" alt="SerializedTypes_Example" src="https://user-images.githubusercontent.com/3953314/195255429-31d24293-6bd1-419e-811b-07874dd0f78d.png">

## Benchmarks
Serialized 500 Type fields. The 8192bytes comes from the _ConcurrentQueue_ that would later be used for SerializedTypes.
Note that the queue is never cleared, so it's size grew considerably.
```cs
|               Method |     Mean |    Error |   StdDev | Allocated |
|--------------------- |---------:|---------:|---------:|----------:|
|      BenchmarkXXHash | 18.44 us | 0.278 us | 0.260 us |    8192 B |
| BenchmarkTypeStrings | 25.09 us | 0.292 us | 0.259 us |         - |
```

TODO:
* Add support in the Serialization Generator for `ReadType()` and `Write(Type)`
* Remove `SetTypeRef` from Serialization Generator
2022-10-11 22:17:22 -07:00
Kamron Batman
f268d5d4e2
fix: Cleans up core code (#1187)
**Only one functional change**
* Fixes a bug in LogFactory where `Warning` is being logged as `Information`

Non-functional changes:
* Updates/Fixes copyright headers
* Removes namespace scopes for core files.

View with [whitespace off](https://github.com/modernuo/ModernUO/pull/1187/files?w=1).
2022-10-10 21:47:08 -07:00
Kamron Batman
0138d40bda
fix: Fixes cast delay issue being 0 (#1186) 2022-10-10 13:30:41 -07:00
Kamron Batman
2806e5d4b7
feat: Adds reflect damage ability. Fixes magical barrier (#1184) 2022-10-08 10:39:28 -07:00
Kamron Batman
3c5e9eaec9
fix: Removes console log for has access (#1183) 2022-10-06 13:53:32 -07:00
Kamron Batman
3c636b04fd
feat: Adds magical barrier and ebolt counter ability (#1182)
* Adds magical barrier ability (Pre-UOML)
* Adds ebolt counter (Post-UOML)
2022-10-02 20:48:53 -07:00
Kamron Batman
1cf5d59116
feat: Adds drain life ability (#1181) 2022-10-02 11:36:15 -07:00
Kamron Batman
dc6f75c3b4
feat: Adds stun, death explosion, poison gas, throw hatchet abilities (#1180)
* Adds Colossal Blow
* Adds Death Explosion
* Adds Poison Gas
* Adds AOE Poison Gas (Counter)
* Adds Stun
* Adds Throw Hatchet
2022-10-02 10:16:37 -07:00
Kamron Batman
43e7fe29c2
feat: Adds Monster Abilities (#1179)
* Adds ability to create monster abilities
* Adds Fire/Cold/Chaos Breath
* Adds Grasping Claw
* Adds Summon Skeletons & Summon Undead w/ Polymorph
2022-10-01 21:09:36 -07:00
Kamron Batman
8a2c0ead3d
fix: Fixes cast spell speeds for various expansions (#1178)
* Adds 0.25s for all spells for SA+ Era
* Fixes Summon Creature & Blade Spirits cast delay
* Fixes minimum cast delay for chivalry spells
* Fixes poison strike and wither spells
2022-10-01 12:25:40 -07:00
mark1145
aa270ac868
fix: Abusing summons offscreen (#1175) 2022-09-30 14:54:19 -07:00
Kamron Batman
f5122fff2a
docs: Adds contributors section to readme 2022-09-30 14:49:28 -07:00
Kamron Batman
4946afee51
fix: Fixes loading map and static files (#1177) 2022-09-29 16:02:41 -07:00
Kamron Batman
cd5541ec74
fix: Fixes compiling with .NET 7 SDK (#1176) 2022-09-28 21:17:30 -07:00
mark1145
060edaa76a
fix: Fixes disarm, strangle, curse, summons, looting rights, creatures attacking, and items going to the floor (#1174)
* Fixes disarm
* Fixes strangle not refreshing
* Fixes curse not refreshing
* Fixes 125% bonus to looting rights
* Fixes mobs attacking each other, or not attacking each other
* Fixes items dropping to the floor
* Fixes protection spell
* Fixes cure sending wrong message
2022-09-27 22:19:15 -07:00
Kamron Batman
906ec095b9
fix: Use the latest number for the next serial after world load. (#1171) 2022-09-13 21:25:21 -07:00
Kamron Batman
fe94aa050a
fix: Fixes gauntlet spawner and map item deserialization (#1170) 2022-09-13 18:20:10 -07:00
Kamron Batman
4499c8397d
fix: Fixes pooled ref list resizing (#1169) 2022-09-12 09:06:50 -07:00
Kamron Batman
f31cc275bd
fix: Code gens artifacts (#1168) 2022-09-10 14:40:19 -07:00
Kamron Batman
2c294b463c
fix: Fixes default values from serialization for basearmor (#1167) 2022-09-07 19:45:11 -07:00
Kamron Batman
b44c69bcee
fix: Source generates map item serialization (#1166) 2022-09-07 01:13:20 -07:00
Ricky Taylor
b5a06d0545
feat: Implement Oil Flask & Refueling (#1165) 2022-09-07 00:35:47 -07:00
Kamron Batman
6b3a9f08e9
fix: Fixes readme syntax 2022-09-06 10:28:44 -07:00
Kamron Batman
5f91598c15
chore: Add generic linux target (#1164) 2022-09-06 10:28:19 -07:00
Kamron Batman
93c2c824dd
feat: Bumps serialization generator (v2.3) to add diagnostics (#1163) 2022-09-06 00:18:15 -07:00
Kamron Batman
2dec2f2fd2
fix: Fixes ore stacking (#1162) 2022-09-05 18:11:27 -07:00
Kamron Batman
5a374e75c9
chore: Updates workflow to fix Fedora 35/36 (#1161) 2022-09-04 22:21:50 -07:00
Kamron Batman
caa72bcc81
chore: Update README to bump required Rider version (#1160) 2022-09-04 09:08:16 -07:00
Kamron Batman
0bd7d31702
chore: Updates README.md to specify new .NET 6 version. (#1158) 2022-09-04 09:04:57 -07:00
Kamron Batman
89815ad4d3
feat: Updates to Serialization Generator v2.2 (#1157)
* Adds `SerializationProperty` - This will generate the private variable for serialization. Also provides an option `useField` to specify your own. Example:
  ```cs
  [SerializationProperty(0, useField: nameof(_resource)]
  ```
2022-09-04 08:45:27 -07:00
Stefano Merotta
d82028a071
fix: Fix wrong min/max delay for detect hidden NPC ability (#1154) 2022-09-01 23:27:49 -07:00
Mink80
c34bcbc515
fix: Fix spawners missing parameters/properties (#1155) 2022-09-01 12:55:23 -07:00
mark1145
1e50ff9966
fix: Blood Oath, Wraith form, & Strangle (#1153)
Fixes blood oath mechanic, and wraith form not properly leeching.
2022-09-01 09:19:40 -07:00
Kamron Batman
c5d2a58917
fix: Fixes the number of rounds for Strangle (#1151) 2022-08-28 06:19:49 -07:00
Kamron Batman
1a44d824d9
fix: Adds better error message for missing assemblies (#1150)
Adds better error messaging for when the server cannot find a dependency. The most common error is MimeKit is missing because the assemblyDirectories field in modernuo.json does not have any valid paths.
2022-08-27 18:55:43 -07:00
mark1145
b8146f26f1
fix: Fixes mobile titles not displaying properly (#1149) 2022-08-27 13:31:22 -07:00
mark1145
42f2c7f6ae
* After casting a spell, the player was always faced south (#1148) 2022-08-26 17:49:39 -07:00
mark1145
76ac5ce3aa
fix: Fixes craft menu not showing gfx of entry (#1146) 2022-08-25 18:39:33 -07:00
Mink80
6eec72e0ca
chore: Cleans up treasure maps (#1144) 2022-08-22 23:20:30 -07:00
Kamron Batman
2d95fb20a6
fix: Adds expansion specific mobile status version (#1145) 2022-08-22 21:04:59 -07:00
Daniel Knight
61f77df892
fix: Town Crier NPR Crash (#1143) 2022-08-22 08:41:24 -07:00
Kamron Batman
946a2acabf
fix: Fixes ref list infinite loop (#1142) 2022-08-21 14:04:29 -07:00
mark1145
b4d4706c90
fix: Solen teleporter deserialization null pointer exception (#1140) 2022-08-21 11:29:22 -07:00
Daniel Knight
6e168c137b
fix: Fixes random skill chosen based on expansion (#1139) 2022-08-17 08:52:18 -07:00
mark1145
23ae7a3c26
fix: Players casting is sometimes not disturbed (#1138) 2022-08-16 22:17:39 -07:00
Kamron Batman
2ae762904f
fix: Fixes serializing strings and uses memory mapped files for loading (#1133) 2022-08-16 08:59:00 -07:00
mark1145
bc746dbf14
fix: Fixes exceptional crafting chance (#1136) 2022-08-06 06:53:02 -07:00
mark1145
41cc7e60d9
fix: Fixes undead slayer chance (#1135) 2022-08-02 21:21:37 -07:00
Kamron Batman
c4b814cc07
fix: Fixes damage from AR (#1134) 2022-07-28 17:39:21 -07:00
Mink80
ce5db11273
SpecialFishingNet: fixes indexing bug (#1132) 2022-07-26 09:50:07 -07:00
Kamron Batman
ec422988f5
fix: Fixes logging of world load (#1131) 2022-07-23 16:14:54 -07:00
Kamron Batman
03f0605a0a
fix: Adds Cleansing Winds Spell (#1126) 2022-07-17 07:49:55 -07:00
Kamron Batman
84f1084e44
fix: Fixes stoneform spell (#1125) 2022-07-17 07:40:43 -07:00
Kamron Batman
fbf60b65a5
fix: Updates mysticism spells and adds bombard (#1124) 2022-07-17 00:04:41 -07:00
Kamron Batman
54da2c94a5
fix: Updates Mysticism to use circles (#1123) 2022-07-16 23:25:48 -07:00
Kamron Batman
c5d1927df3
fix: Cleans up more spells (#1122) 2022-07-16 23:15:45 -07:00
Kamron Batman
068bafd9b6
fix: Cleans up more spells (#1121) 2022-07-16 22:32:51 -07:00
Kamron Batman
fa3515f930
fix: Cleans up some comments and spell code (#1120) 2022-07-16 19:45:41 -07:00
Kamron Batman
56c97ca84a
fix: Fixes spell durations for older expansions (#1119) 2022-07-15 12:49:14 -07:00
Kamron Batman
d6d02de296
fix: Fixes spell mechanics and misc bugs (#1118)
- [X] Fixes NPE from account tags.
- [X] Fixes bad skill check due to missing cast to double.
- [X] Fixes water elemental duration.
- [X] Standardizes spell summon duration by expansion.
2022-07-14 22:01:59 -07:00
Kamron Batman
2ab0a0e063
chore: Adds changed mechanics (#1117) 2022-07-14 21:13:03 -07:00
Kamron Batman
0d19a5ff99
fix: Do not use unsafe work queue for auto archiving (#1116) 2022-07-14 11:27:29 -07:00
Kamron Batman
a4168dc219
fix: Fixes pooled timer detach (#1115)
- [X] Fixes timers not returning on detach
2022-07-14 11:14:37 -07:00
Kamron Batman
9c374861d9
fix: Delays getting item id of craftables until they are needed (#1114)
- [X] Fixes world loading for multi.mul. It was taking 10-15s, now takes 1s or less.
- [X] Fixes loading the crafting system by offloading getting a label number to when the item needs it.
2022-07-10 22:44:24 -07:00
mark1145
bd4e6fc30e
fix: Fixes curses and spell effects (#1112)
* * Fix statmod naming mismatch: everything in code searches for "[Magic] {type} Offset" but stat reductions are being added as "[Magic] {type} Curse"

* Clarifies and cleans up curses

* Fixes blood oath

* * NobleSacrifice remove all curses in one go

* Dont need a method

* Fix extra parentheses

Co-authored-by: Kamron Batman <3953314+kamronbatman@users.noreply.github.com>
2022-07-07 23:59:46 -07:00
nullptr-w8
ad0e1702a3
fix: Fixes [spawn gump for older clients (#1108) 2022-07-04 07:20:20 -07:00
Kamron Batman
8303c74379
fix: Fixes ValueStringBuilder initial rented buffer (#1111)
Fixes issue with ValueStringBuilder having an empty initial buffer.
2022-07-03 08:14:54 -07:00
Kamron Batman
c6c644c966
fix: Fixes crafting so it doesn't create items before world load. (#1110) 2022-07-02 02:18:28 -07:00
Quick
04c44e2968
fix: Fixes bug with town crier NPE from random entry (#1106) 2022-07-02 01:54:58 -07:00
Kamron Batman
7c981b2d10
fix: Adds throwing weapons (#1107) 2022-07-01 17:03:27 -07:00
Kamron Batman
38686c09b2
fix: Updates more logging to Serilog (#1105)
Adds more serilog logging and cleans up some server files.
2022-07-01 11:57:08 -07:00
nullptr-w8
1cf4a43c2c
fix: Fixes [spawn gump headers (#1103) 2022-07-01 11:13:44 -07:00
Kamron Batman
a9f7f2b7e9
fix: Fixes LearnAllRecipes (#1104) 2022-07-01 11:11:37 -07:00
nullptr-w8
31fb29cf0b
feat: Adds [Spawn command & Gump Grids (#917)
Adds `[spawn` command
<img width="1018" alt="Screen Shot 2022-06-30 at 10 13 15 PM" src="https://user-images.githubusercontent.com/3953314/176828843-eda4da40-b7f5-494b-a0bc-667aba3293bb.png">
2022-06-30 22:22:51 -07:00
Quick
6601101e2e
fix: Updates spawner gump visuals! Thanks Quick! (#1102)
This update changes the default RunUO SpawnerGump's overall look and feel and expands slightly some functionality. Inspired by the XmlSpawner gump.

![image](https://user-images.githubusercontent.com/577652/176790254-cc1fb6bb-b281-49c9-a7cd-ec5376a69921.png)

![image](https://user-images.githubusercontent.com/577652/176790147-408ef682-9670-4753-872c-db843be1ab05.png)

1. The name of the spawner is displayed
2. This displays the total current spawn with the current allowed max spawn
3. Cleans up the display of the current count and the max count
4. The ability to turn on/off the spawner
5. Several changes to buttons 
   - Simple Save and Cancel instead of Okay, Cancel and Apply.
   - Props button to display spawner properties
   - Goto button to teleport to the spawner
   - Reset button that will clear all the spawned mobs and turn off the spawner
   - Repositioned the buttons to make a little more sense
6. The sum of all the max spawn values per entry, from all pages
2022-06-30 20:48:54 -07:00
Kamron Batman
cb474712f8
fix: Removes side effect of setting skill mod when changing Owner (#1101)
## Breaking Change!
Setting `mod.Owner` will no longer update a skill mod.
**Please stick to the API and use `mobile.AddSkillMod(mod)` for all situations, including equipping.**


## Fixes
- [X] Fixes memory leak in factions.
- [X] Changes `List<SkillMod>` to `HashSet<SkillMod>`.
- [X] Eliminates skill mods adding/removing twice.
2022-06-30 12:07:08 -07:00
Kamron Batman
414ef8d8d6
fix: Fixes skill mods (#1099) 2022-06-30 08:46:54 -07:00
Kamron Batman
f2253e7f0d
chore: Updates to RunUO to ModernUO (#1098) 2022-06-29 09:44:22 -07:00
Kamron Batman
2d00f6d27a
fix: Fixes old haven and new haven starting cities (#1097)
Adds OldHaven cities for shards that use clients older than 6.x
2022-06-29 01:23:57 -07:00
Stefano Merotta
f4cbe82d17
fix: Added comment for magery circle formula (#1095) 2022-06-28 09:58:51 -07:00
Kamron Batman
a37b8b285c
fix: Adds AssistVersion packet support for Razor (#1094)
- [X] Fixes razor negotiations via 0xF0 packet (open negotiation)
- [X] Adds AssistVersion handling for Razor
- [X] Displays Assistant in Client/Admin gump
2022-06-28 01:05:00 -07:00
Kamron Batman
7099329dfe
fix: Fixes disallowed features enum serialization (#1093) 2022-06-27 18:30:31 -07:00
Kamron Batman
5343260895
fix: Fixes wrong amount cleared for mobiles (#1092) 2022-06-26 17:12:42 -07:00
Kamron Batman
8d3aaaeb2a
fix: Removes razor negotiations since they arent maintained (#1090) 2022-06-26 08:33:53 -07:00
Kamron Batman
4f28e1e3c4
fix: Adds back razor/assistuo negotiations (#1089)
* [X] Adds Razor negotiations
* [X] Adds AssistUO "handshake" 🙄
2022-06-25 01:47:45 -07:00
Kamron Batman
bc2ed44440
fix: Fixes starting champion with valor (#1088) 2022-06-21 22:43:08 -07:00
Kamron Batman
087cc15f39
fix: Reverts changes to fists (#1087) 2022-06-21 21:24:31 -07:00
Kamron Batman
792f04a323
fix: Fixed lantern deserialization (#1086) 2022-06-21 21:04:02 -07:00
Kamron Batman
abdd868b7a
chore: Updates release permissions (#1085) 2022-06-21 16:00:06 -07:00
Kamron Batman
38b45d5e0b
fix: Cleans up AI code (#1084) 2022-06-20 19:32:16 -07:00
Kamron Batman
da877f59a7
fix: Fixes possibel NPE with misbehaving Weapon property (#1083) 2022-06-20 16:31:39 -07:00
Kamron Batman
8509b1f8a0
chore: Updates whats changed (#1082) 2022-06-19 19:55:55 -07:00
6440 changed files with 435713 additions and 460912 deletions

View file

@ -3,7 +3,7 @@
"isRoot": true,
"tools": {
"modernuoschemagenerator": {
"version": "2.1.4",
"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.

View file

@ -135,6 +135,19 @@ dotnet_style_qualification_for_field=false:suggestion
dotnet_style_qualification_for_method=false:suggestion
dotnet_style_qualification_for_property=false:suggestion
dotnet_style_require_accessibility_modifiers=for_non_interface_members:suggestion
# Added by Derek
dotnet_diagnostic.IDE0022.severity = none # expression bodies
dotnet_diagnostic.IDE0130.severity = none # namespace not matching file location
dotnet_diagnostic.IDE0290.severity = none # primary constructors
dotnet_diagnostic.IDE1006.severity = none # naming violations with "_"
dotnet_diagnostic.IDE0038.severity = none # pattern matching
dotnet_diagnostic.IDE0008.severity = none # using var
# probably should be ruled var or not var across the board but it's just suggestions anyway.
# Leave this here in case we want to turn it on in the future
# csharp_style_var_for_built_in_types = true:suggestion # suggest using var (implied) variables
# csharp_style_var_when_type_is_apparent = true:suggestion # suggest using var (implied) variables
# csharp_style_var_elsewhere = true:suggestion # suggest using var (implied) variables
dotnet_diagnostic.RCS1123.severity = none
# ReSharper properties
resharper_apply_auto_detected_rules=false

3
.gitattributes vendored
View file

@ -31,3 +31,6 @@ publish.cmd text eol=lf
/.github export-ignore
.gitignore export-ignore
.gitattributes export-ignore
# Collapse in Github
**/Migrations/*.v*.json linguist-generated=true

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.

2
.github/FUNDING.yml vendored
View file

@ -9,4 +9,4 @@ community_bridge: # Replace with a single Community Bridge project-name e.g., cl
liberapay: # Replace with a single Liberapay username
issuehunt: # Replace with a single IssueHunt username
otechie: # Replace with a single Otechie username
custom: ["https://paypal.me/sabresite", "https://venmo.com/code?user_id=1834446441414656966"]
custom: ["https://muo.gg/paypal", "https://muo.gg/venmo", "https://muo.gg/coffee"]

15
.github/dependabot.yml vendored Normal file
View file

@ -0,0 +1,15 @@
# To get started with Dependabot version updates, you'll need to specify which
# package ecosystems to update and where the package manifests are located.
# Please see the documentation for all configuration options:
# https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates
version: 2
updates:
- package-ecosystem: "nuget" # See documentation for possible values
directory: "/" # Location of package manifests
schedule:
interval: "daily"
- package-ecosystem: "github-actions"
directory: "/" # Watches .github/workflows/**
schedule:
interval: "weekly"

10
.github/porcelain.ps1 vendored Normal file
View file

@ -0,0 +1,10 @@
$untrackedOrModified = git status --porcelain | Select-String -Pattern "^\?\? |^ M"
if ($untrackedOrModified) {
Write-Host "Untracked or modified files found."
Exit 1
}
else {
Write-Host "No untracked or modified files."
Exit 0
}

View file

@ -3,31 +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:
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-11
name: MacOS 11
- os: macos-12
name: MacOS 12
- os: macos-15
name: MacOS 15
- os: macos-26
name: MacOS 26
steps:
- uses: actions/checkout@v2
- uses: actions/checkout@v7
with:
fetch-depth: 0 # avoid shallow clone so nbgv can do its work.
- name: Setup .NET 6
uses: actions/setup-dotnet@v1
- name: Install .NET
uses: actions/setup-dotnet@v6
with:
dotnet-version: 6.0.300
global-json-file: global.json
- name: Install Prerequisites
run: |
brew update
brew install icu4c libdeflate argon2
- name: Set Library Path
run: echo "DYLD_LIBRARY_PATH=/opt/homebrew/lib:$DYLD_LIBRARY_PATH" >> $GITHUB_ENV
- name: Build
run: ./publish.cmd
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
# 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: debian:trixie
name: Debian 13
packageManager: apt
- container: debian:bookworm
name: Debian 12
packageManager: apt
- 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:
# 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 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[0-9]+$' libdeflate0 libargon2-1 tzdata
if: ${{ matrix.packageManager == 'apt' }}
# 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
uses: actions/setup-dotnet@v6
with:
global-json-file: global.json
- name: Build
run: dotnet run --project Projects/BuildTool -- --config Release --skip-prereqs
- name: Test
# 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

@ -10,65 +10,29 @@ jobs:
name: Create Release
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: actions/checkout@v7
with:
fetch-depth: 0 # avoid shallow clone so nbgv can do its work.
token: ${{ secrets.GITHUB_TOKEN }}
- name: Install NGBV
uses: dotnet/nbgv@master
token: ${{ secrets.PERSONAL_ACCESS_TOKEN }}
- name: Install .NET
uses: actions/setup-dotnet@v6
with:
global-json-file: global.json
- name: Compute version
uses: dotnet/nbgv@v0.5.2
id: nbgv
- name: Get Latest Release
id: last_release
uses: pozetroninc/github-action-get-latest-release@master
with:
owner: ModernUO
repo: ModernUO
excludes: prerelease, draft
- name: Create fake tag for diffing
- name: Push version tag
run: |
git config --global user.name "Fake Tag Action"
git config --global user.email "hi@modernuo.com"
git checkout ${{ steps.last_release.outputs.release }}
git tag -fa v0.1.0 -m "Fake release"
git checkout -
- name: Push git changes
uses: ad-m/github-push-action@master
with:
GITHUB_TOKEN: ${{ secrets.WORKFLOW_TOKEN }}
tags: true
- name: Conventional Changelog
id: changelog
uses: TriPSs/conventional-changelog-action@v3
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
output-file: false
skip-version-file: true
skip-commit: true
release-count: 1
git tag ${{ steps.nbgv.outputs.Version }}
git push origin ${{ steps.nbgv.outputs.Version }}
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Delete v${{ steps.changelog.outputs.version }} Tag
uses: dev-drprasad/delete-tag-and-release@v0.1.3
with:
delete_release: true
tag_name: v${{ steps.changelog.outputs.version }}
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Delete v0.1.0 Tag
uses: dev-drprasad/delete-tag-and-release@v0.1.3
with:
delete_release: true
tag_name: v0.1.0
env:
GITHUB_TOKEN: ${{ secrets.WORKFLOW_TOKEN }}
GITHUB_TOKEN: ${{ secrets.PERSONAL_ACCESS_TOKEN }}
- name: Create Release
id: create_release
uses: actions/create-release@v1
uses: softprops/action-gh-release@v3
with:
body: ${{ steps.changelog.outputs.clean_changelog }}
tag_name: ${{ steps.nbgv.outputs.Version }}
release_name: ${{ steps.nbgv.outputs.Version }}
name: ${{ steps.nbgv.outputs.Version }}
draft: false
prerelease: false
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
generate_release_notes: true
token: ${{ secrets.PERSONAL_ACCESS_TOKEN }}

View file

@ -0,0 +1,18 @@
name: Post Release to Discord
on:
release:
types:
- published
jobs:
post-release-discord:
runs-on: ubuntu-latest
steps:
- name: Post Release on Discord
uses: SethCohen/github-releases-to-discord@v1.20.0
with:
webhook_url: ${{ secrets.WEBHOOK_URL }}
username: "Release Changelog"
avatar_url: "https://cdn.discordapp.com/icons/751317910504603701/ec26ab4881753077e06122da7b78bf7c.webp"
max_description: 2000

View file

@ -1,17 +0,0 @@
name: Updates Docs
on:
repository_dispatch:
types: [docs]
workflow_dispatch:
jobs:
update-docs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: actions/setup-python@v2
with:
python-version: 3.x
- run: pip install mkdocs-material
- run: mkdocs gh-deploy --force

31
.gitignore vendored
View file

@ -1,35 +1,66 @@
# Distribution Files
/Distribution/Logger
/Distribution/Logger.*
/Distribution/ModernUO
/Distribution/ModernUO.*
/Distribution/Server
/Distribution/Server.*
/Distribution/Assemblies
/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
/Distribution/*.dll
/Distribution/*.exe
/Distribution/runtimes
/Distribution/nohup.out
/Distribution/ref
/Distribution/web
/Projects/*/obj
/Projects/*/bin
/Projects/*/Generated
*.swp
*.log
*.user
/.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

@ -28,7 +28,6 @@ We accept fixes and features! Here are some resources to help you get started on
If you don't know what a pull request is read this article: https://help.github.com/articles/using-pull-requests. Make sure the repository can build and all tests pass. Familiarize yourself with the project workflow and our coding conventions.
1. Sign a [Contributor License Agreement](https://cla-assistant.io/modernuo/ModernUO) when submitting your pull request.
1. Ensure all files have the appropriate [LICENSE](/LICENSE) header (Line 634-649)
1. Ensure any install or build dependencies are removed.
1. Update the README.md with details of changes to the interface, this includes new build process,
@ -38,6 +37,5 @@ If you don't know what a pull request is read this article: https://help.github.
1. You may merge the Pull Request in once you have the sign-off of two other developers, or if you
do not have permission to do that, you may request the second reviewer to merge it for you.
[aspnet-contributing]: https://github.com/aspnet/AspNetCore/blob/456dbf1309f9fcae1d7b376784088dcd7818c01e/CONTRIBUTING.md
[homepage]: http://contributor-covenant.org
[version]: http://contributor-covenant.org/version/1/4/

View file

@ -3,15 +3,13 @@
<PropertyGroup>
<Authors>Kamron Batman</Authors>
<Company>ModernUO</Company>
<Copyright>2019-2020</Copyright>
<TargetFramework>net6.0</TargetFramework>
<Platforms>x64</Platforms>
<PlatformTarget>x64</PlatformTarget>
<LangVersion>preview</LangVersion>
<Copyright>2019-2026</Copyright>
<TargetFramework>net10.0</TargetFramework>
<LangVersion>14</LangVersion>
<PublicRelease>true</PublicRelease>
<AutoGenerateBindingRedirects>true</AutoGenerateBindingRedirects>
<NoWarn>NU1603</NoWarn>
<RuntimeIdentifiers>win-x64;debian.10-x64;debian.11-x64;ubuntu.16.04-x64;ubuntu.18.04-x64;ubuntu.20.04-x64;centos.7-x64;centos.8-x64;fedora.32-x64;fedora.33-x64;fedora.34-x64;rhel.7-x64;rhel.8-x64;linuxmint.17-x64;linuxmint.18-x64;linuxmint.19-x64;osx-x64</RuntimeIdentifiers>
<RuntimeIdentifiers>win-x64;win-arm64;osx-x64;osx-arm64;linux-x64;linux-arm64</RuntimeIdentifiers>
<Configurations>Debug;Release;Analyze</Configurations>
<PackageRequireLicenseAcceptance>false</PackageRequireLicenseAcceptance>
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
@ -21,27 +19,38 @@
<IsWindows Condition="'$([System.Runtime.InteropServices.RuntimeInformation]::IsOSPlatform($([System.Runtime.InteropServices.OSPlatform]::Windows)))' == 'true'">true</IsWindows>
<IsOSX Condition="'$([System.Runtime.InteropServices.RuntimeInformation]::IsOSPlatform($([System.Runtime.InteropServices.OSPlatform]::OSX)))' == 'true'">true</IsOSX>
<IsLinux Condition="'$([System.Runtime.InteropServices.RuntimeInformation]::IsOSPlatform($([System.Runtime.InteropServices.OSPlatform]::Linux)))' == 'true'">true</IsLinux>
<IsX64 Condition="'$([System.Runtime.InteropServices.RuntimeInformation]::ProcessArchitecture)' == 'X64'">true</IsX64>
<IsArm64 Condition="'$([System.Runtime.InteropServices.RuntimeInformation]::ProcessArchitecture)' == 'ARM64'">true</IsArm64>
<DefineConstants Condition="'$(IsWindows)'=='true'">WINDOWS</DefineConstants>
<DefineConstants Condition="'$(IsOSX)'=='true'">OSX</DefineConstants>
<DefineConstants Condition="'$(IsLinux)'=='true'">LINUX</DefineConstants>
<DefineConstants Condition="'$(IsOSX)'=='true' OR '$(IsLinux)'=='true'">UNIX</DefineConstants>
<DefineConstants Condition="'$(SkipLocalsInitAttribute)'=='true'">NO_LOCAL_INIT</DefineConstants>
<DefineConstants Condition="'$(IsX64)'=='true'">CPU_X64</DefineConstants>
<DefineConstants Condition="'$(IsArm64)'=='true'">CPU_ARM64</DefineConstants>
<DefineConstants>MUO</DefineConstants>
<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>true</InvariantGlobalization>
<InvariantGlobalization>false</InvariantGlobalization>
</PropertyGroup>
<PropertyGroup Condition="'$(IsWindows)'=='true' AND '$(RuntimeIdentifier)'==''">
<PropertyGroup Condition="'$(IsWindows)'=='true' AND '$(IsX64)'=='true' AND '$(RuntimeIdentifier)'==''">
<RuntimeIdentifier>win-x64</RuntimeIdentifier>
</PropertyGroup>
<PropertyGroup Condition="'$(IsOSX)'=='true' AND '$(RuntimeIdentifier)'==''">
<PropertyGroup Condition="'$(IsWindows)'=='true' AND '$(IsArm64)'=='true' AND '$(RuntimeIdentifier)'==''">
<RuntimeIdentifier>win-arm64</RuntimeIdentifier>
</PropertyGroup>
<PropertyGroup Condition="'$(IsOSX)'=='true' AND '$(IsX64)'=='true' AND '$(RuntimeIdentifier)'==''">
<RuntimeIdentifier>osx-x64</RuntimeIdentifier>
</PropertyGroup>
<PropertyGroup Condition="'$(IsOSX)'=='true' AND '$(isArm64)'=='true' AND '$(RuntimeIdentifier)'==''">
<RuntimeIdentifier>osx-arm64</RuntimeIdentifier>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)'=='Debug'">
<DefineConstants>TRACE;DEBUG</DefineConstants>
<Optimize>false</Optimize>
@ -54,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="2.11.0" />
<PackageReference Include="Serilog.Sinks.Async" Version="1.5.0" />
<PackageReference Include="Serilog.Sinks.Console" Version="4.0.1" />
<PackageReference Include="Serilog" Version="4.4.0" />
<PackageReference Include="Serilog.Sinks.Async" Version="2.1.0" />
<PackageReference Include="Serilog.Sinks.Console" Version="6.1.1" />
<PackageReference Include="Nerdbank.GitVersioning" Condition="!Exists('packages.config')">
<Version>3.5.107</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

@ -57,6 +57,10 @@ Static 0x07C3
Static 0x07C4
1442 1548 30
# fireplace
StoneFireplaceSouthAddon
1137 1816 0
# fireplace
Static 0x08CF
1248 1704 0
@ -5370,4 +5374,4 @@ Static 0x1F07
# pitcher of water
Pitcher 0x1F9D (Content=Water)
1193 1709 4
1512 1425 21
1512 1425 21

View file

@ -2766,4 +2766,4 @@ Static 0x1BD7
# bulletin board
BulletinBoard 0x1E5E
1452 3768 0
1452 3768 0

View file

@ -514,13 +514,13 @@ Static 0x0A6B
# bed
LargeBedSouthAddon 0x0A7D
4448 1058 0
# 4448 1058 0
4470 1218 0
4681 1172 0
# bed
LargeBedEastAddon 0x0A83
4551 918 20
# 4551 918 20
4693 1217 0
# bookcase
@ -1613,7 +1613,7 @@ PewterMug 0x1002
# archery butte
ArcheryButte 0x100B
4489 1118 0
# 4489 1118 0
# spinning wheel
SpinningWheelSouthAddon 0x1015
@ -1987,4 +1987,4 @@ LocalizedSign 0x1F29 (LabelNumber=1016062)
# no draw
Blocker 0x21A4
4704 1120 0
4708 1121 0
4708 1121 0

View file

@ -34,6 +34,9 @@ Blocker 0x21A4
6082 144 -15
6082 145 -15
6082 146 -15
5920 168 16
5920 169 16
5920 170 16
6058 88 29
6058 89 29
6058 90 29
@ -138,4 +141,4 @@ Static 0x179A
3446 548 -5
3456 538 -5
3472 512 -5
3488 580 -5
3488 580 -5

View file

@ -0,0 +1,7 @@
# Iron Gate right
IronGateShort 0x0856 (Facing=NorthCCW)
2783 867 0
# Iron Gate left
IronGateShort 0x0854 (Facing=SouthCW)
2783 868 0

View file

@ -1060,7 +1060,7 @@ TreatiseOnAlchemy 0x0FF4
3672 2475 4
# spinning wheel
Static 0x1019
SpinningWheelEastAddon 0x1019
3667 2597 0
# pile of wool

View file

@ -0,0 +1,7 @@
# Iron Gate right
IronGateShort 0x0856 (Facing=NorthCCW)
2783 867 0
# Iron Gate left
IronGateShort 0x0854 (Facing=SouthCW)
2783 868 0

View file

@ -0,0 +1,3 @@
# teleporter
Teleporter 0x1BC3 (PointDest=(5977, 169, 0))
5905 97 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.

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

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

View file

@ -1,578 +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"
},
{
"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"
}
]

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],

Some files were not shown because too many files have changed in this diff Show more