Moving Shot charged its mana in `OnBeforeSwing`, before `BaseRanged.OnSwing` decided whether to fire. A swing that fired nothing (a paralyzed or frozen archer, no ammo, a failed harmful check) still took the mana and left the move armed. Every swing after that charged again, so inside the 3-second window you paid twice, until the archer ran out of mana.
`OnBeforeSwing` now only checks the mana. Moving Shot's own `OnHit`/`OnMiss` charge it, and those only run once a shot has fired. This is how the other weapon abilities pay. A miss still costs mana. A parried shot no longer does: the parry clears the move before `OnHit` runs, the same as for every other ability.
## Summary
With T2A packet crafting menus enabled (`t2aCraftMenus`, default for pre-UOTD eras), cooking did nothing. Double-clicking a Skillet set a target, and `T2ACraftSystem.ShowMenu` had no Cooking branch, so the target was accepted and silently dropped. Nothing else could cook either: the old RunUO "use food on a heat source" code was never hooked up and was removed in #1054.
T2A cooking never used a menu, so this restores the item-on-item / item-on-heat-source flow:
- **Per-item handling:** each ingredient (`CookableFood`, `Dough`, `SweetDough`, `SackFlour`, `BowlFlour`, `JarHoney`) implements `IT2ACombinable.TryCombine` and switches on the target. `T2ACooking` holds the shared target, water, combine and heat-timer helpers.
- **Heat source:** all `CookableFood` (raw meats, fish, eggs, unbaked pies/quiche/pizzas, cookie and cake mix), dough → bread loaf, sweet dough → muffins. One unit is used up at the start, then a 5 s timer; the Cooking check runs from `CookingLevel` to 100 (bread and muffins use `DefCooking`'s range). The food burns on failure or if the cook moves more than 3 tiles away.
- **Combinations:**
- Open flour sack or bowl of flour + water → dough
- Dough + pear / peach / apple / pumpkin / eggs / meat / sausage → the matching unbaked pie, quiche or pizza
- Dough + honey → sweet dough
- Sweet dough + honey → cookie mix
- Flour + sweet dough → cake mix
- The honey and flour + sweet dough recipes work in either direction.
- **Water:** water drinks (pitchers), buckets, barrels, tubs and trough add-ons, in the pack, in hand, or on the ground within 2 tiles and in line of sight.
- **Cooking tools are passive:** with T2A menus on, a tool whose system has no T2A menu (`T2ACraftSystem.HasMenu`) falls through to `ShowCraftMenu`, which does nothing under T2A. No dead target is left.
Behaviour with T2A menus off is unchanged.
## Follow-ups
- Static troughs/barrels and open water aren't water sources here or for pitcher filling (no static water ID list exists); to be fixed separately.
- Whether a bowl of flour returns an empty bowl is unconfirmed (`//TODO`).
- Cooking tools that must merely be in the pack for some recipes are not modelled yet.
## Testing
- `dotnet build`: 0 warnings, 0 errors
- New `T2ACookingTests`: flour + water, closed sack refused, dough + apple, honey in both directions, cookie mix, cake mix, wrong target consumes nothing, water in another player's pack refused
- Not covered: the heat timer (untested); not yet exercised in game
`BaseRanged.OnCraft` passed the raw resource type to `ApplyWoodBonuses`, which can be null; `CraftResources.GetFromType` threw on the null key and crashed the server. Every other `OnCraft` override already falls back to `craftItem.Resources[0].ItemType` — `BaseRanged` now does the same.
`GetFromType` also returns `CraftResource.None` for a null type, matching its documented contract for unregistered types, so other callers can't hit the same throw.
Part of #2687, after #2709. One commit per section, in the same order.
### Failures, maps, bottles and colored nets
A GM's cast fails about a quarter of the time in any water, from the fishing table's last row, and deep water is no harder than a river. Maps and bottles are 0.5% per roll from T2A on, and 1 net in 100 is colored in every era.
This removes the last row, so a cast only fails its skill check. From UO:R a cast in deep water checks skill from 65 to 115, through a `Fishing.MutateResource` override. Publish 5, which "was known as the Renaissance Publish", made deep water harder: "because the difficulty level is higher, the rate of success for pulling up SoS bottles will be lower." [O3] From LBR maps and bottles come up more often and 1 net in 10 is colored, both from Publish 16, which came out in parts during LBR [O14].
| Change | Era | Source |
|---|---|---|
| No "nothing" row | All | #2687 found no source for it; "A prepatch, you never failed catching something at GM" [S15]; a GM succeeds every time in lakes and rivers [C1] |
| Deep water: nothing below 65 skill | UO:R on | "The minimum required skill level to catch *anything* in deep water is 65." [S5–S7] |
| Deep water: 10% at 70, 70% at GM | UO:R on | "Even at 70, it is only about a 10% chance of success in deep water." [S14]; 29% of casts fail before Publish 16 and 30% after [F1, F2]; 70% success at 100 offshore [C1] |
| Maps (skill − 80) / 2400, bottles (skill − 80) / 1500 | LBR on | A bottle "about once per hour" until October 2002 [S1–S6], then "a special item (a net, a message in a bottle or a treasure map) about five times per hour" [S7]; bottles went from 0.4% of catches before Publish 16 [F1] to 1.7% after [F2] |
| Colored nets: none, then 10% | LBR on | "With Publish 16, we got a nice surprise--colored special fish nets!" [F4]; "Any net created has a 10% chance of being colored." [F3] |
- From LBR a GM's deep-water catches are 0.8% maps and 1.3% bottles. Chrome counted 0.9% and 1.3% [C1], the FCB 0.7% and 1.7% after Publish 16 [F2]. Nets and big fish stay at 0.5%, close to both counts.
- At Stratics' 280 casts an hour [S1–S6], that is about one bottle an hour from UO:R to UOTD and five nets, maps or bottles an hour from LBR, the two rates Stratics gives.
- The gain chance rises as the success chance falls, so above 65 a cast in deep water gains faster than one near land, Publish 5's "best method for gaining at high levels" [O3]. Below 65 it neither catches nor gains.
- Chrome's table also has three kinds of water near land, with difficulties of 15, 35 and 55. No other source has them, so near land stays at 0–100.
### Deep water is at least 14 tiles from land
`SpecialFishingNet.FullValidation` decides what counts as deep water: where a special fishing net works, where maps, bottles, nets and big fish can be caught, and from UO:R where a cast is harder. It checks the target and 20 points on its four diagonals, 5 to 25 tiles out. Nothing off those points is read, so a spot 8 tiles from shore can count as deep, while one with land 15 to 25 tiles away on a diagonal does not.
The sources give one rule in every era: land at least 14 tiles away. This checks every land tile within 13 of the target. ModernUO has no server lines inside a map, so the only boundary is the map's edge, which counts as land as before. Only land tiles are read, so statics such as rocks are ignored. `SOS.FindLocation` had its own copy of the old check and now calls `FullValidation`, so wrecks follow the same rule.
- 1999 to 2000: "which means you must be at least 14 tiles away from all land" [S1–S4]
- March 2001: "14 tiles away from any land mass" [F5]
- 2001 to 2002: "which means you must be at least 14 tiles away from all land and server lines" [S5–S7]
- Today: "You need to be a minimum of 14 tiles from land or a server boundary to be in ‘deep water’." [O9]
One guide from January 2000 says "16 tiles away from land" [S9]; every other source says 14.
- In the area an SOS picks from on Felucca, 59% of spots pass the old check and 63% pass the new one; in Ilshenar's two sea regions, 16% and 30%.
- The check reads up to 729 land tiles instead of 21: about 6 µs per check in a Release build, against under 0.1 µs before; a cast makes at most two.
### Sea serpents
In ModernUO a sea serpent comes up from fishing only with a treasure map, a bottle or a net, which it carries. A quarter are deep sea serpents. It has no timer, and nothing makes it attack the fisherman first.
This follows the notes of March 28, 1999 and Publish 4. A fished serpent attacks the fisherman at once. Five minutes after it surfaced it goes back to deep water, taking what it carries, unless someone is still fighting it: it took damage in the last two minutes and has not healed to full since, which is what ModernUO's damage entries record. From T2A, at 80 skill or more in deep water, 1% of casts also bring up a serpent that carries nothing, whether the cast catches something or not. 1 serpent in 10 is a deep sea serpent.
| Change | Era | Source |
|---|---|---|
| Attacks the fisherman at once | All | "Sea Serpents will now immediately attack the fisherman that brought them from the deep." [O1] |
| Goes back after 5 minutes unless fought | All | "Sea Serpents will return to deep water after 5 minutes if not attacked." [O1]; "will return to the depths after a brief period if they are not engaged" [S10] |
| 1% of casts bring up an empty serpent | T2A on | "Fishermen at over 80 skill will have a chance to pull up a sea serpent regardless of whether they succeed or fail at fishing when in deep sea." [O2]; 1% at 80 or more, before the skill check [B1] |
| 1 in 10 is a deep sea serpent | All | "A small percentage of Sea Serpents will be Deep-Sea Serpents." [O1]; about 10% [B1]; 9.8% and 10.8% of the serpents counted [F1, F2] |
- Publish 4 gives no rate. The 1% comes from a post by Hance, a UO developer, in December 2000 [B1]; T2A uses it too, since it is the rule Publish 4 brought in (our inference). The FCB's counts agree: 1.76% of casts brought up a serpent and 55% of serpents carried nothing, so about 1% of casts brought up an empty one [F1] (our calculation).
- Before Publish 4 serpents came up "on failure" [O1], and Stratics kept "if you are fishing with 80+ skill and you fail" from July 1999 to 2002 [S1–S7]. Publish 4 is the last rule in T2A, so it is the one used from T2A.
- Hance also relays a side comment that the empty serpent cancels the rest of the cast [B1]. If it did, no cast could bring up two serpents, but the FCB saw two from one cast on 0.96% of serpents [F1], and Chrome caught empty serpents together with fish and footwear [C1]. So the cast goes on.
- Hance's post also needs room in the backpack for a fish and fish left in the spot [B1]. A spot with no fish left already ends the cast before the roll, unless an SOS keeps the cast going (next section). A full backpack doesn't: ModernUO doesn't check for room before a cast, so neither does the serpent.
- Serpents from a special fishing net are unchanged; Hance's post says nets don't follow its formula [B1].
### The SOS chest
In ModernUO, a fisherman carrying an SOS within 60 tiles of its spot brings up the chest on 1 catch in 8 and a shipwrecked item on the other 7. The SOS also lets a cast go on where the fish have run out.
This makes the chest 1 catch in 4, in every era. From UO:R an SOS needs 65 skill, and below that the cast is an ordinary one.
| Change | Era | Source |
|---|---|---|
| The chest on 1 catch in 4 | All | "You have a 25% chance of getting the chest and a 75% chance of getting something else with each cast." [F3]; "a 25% chance to fish up the actual treasure chest" [S7] |
| An SOS needs 65 skill | UO:R on | 65 in the chest's row of Stratics' table: "The skills mentioned above are minimum skills needed to fish up the corresponding item." [S5–S7] |
- No source gives a number before October 2002, or any for ModernUO's 1 in 8, so the 25% applies in every era. The other catches keep ModernUO's seven kinds of shipwrecked items, equally likely. Hanse's die splits them in three: "One roll is body parts; another roll is shells, barrel staves, globes, etc.; another roll is paintings, broken clocks, etc.; and another is the chest itself." [F3]
- Stratics' guide of November 2000 has neither the chest's row nor the 65 for deep water [S4]. The May 2001 version, during Third Dawn, has both: "The minimum required skill level to catch *anything* in deep water is 65." [S5] The first section applies that from UO:R, after Publish 5 made deep water harder [O3], so the chest follows it (our inference). An SOS's spot is picked in deep water, but a cast within 60 tiles of it can be nearer land, so the 65 is checked on its own.
- Below 65 there are no shipwrecked items either, because Hanse's die is rolled once you "can even attempt to get a treasure chest" [F3] (our reading). Before UO:R an SOS still works at any skill; the only period line on it is "You do not have to be GM Fisherman to actually fish up the treasure from a bottle, only to fish up A bottle." [S9]
### Fishing reveals from T2A
ModernUO reveals a hidden fisherman from ML. The notes of March 28, 1999 already say "If you are hidden, fishing will now unhide you." [O1], so this reveals from T2A, at the same two points: the splash and the end of the cast.
### Big fish and the leviathan's artifact
In ModernUO every big fish weighs 3 to 200 stones and gives max(16, weight) / 4 steaks, and the net thrower has a 1 in 4 chance at an artifact at any skill.
| Change | Era | Source |
|---|---|---|
| Big fish weigh 100 stones | Before SE | "They weigh 100 stones each" [S5–S7]; Publish 28 "Added variable weight and fisherman's mark on big fish" [O6] |
| A steak per 5 stones, at least 4 | SE on | "now its weight / 5 will determine number of fish steaks (minimum of 4)" [O7] |
| The net thrower's own chance at an artifact: 1 in 4, only at GM | SE on | "Give the fabled net thrower (if a GM Fisherman) a 1 in 4 chance of getting a minor artifact (not modified by luck or damage)." [O6] |
- Before SE a big fish keeps ModernUO's weight / 4 steaks, 25 for 100 stones: no source gives a number before Publish 29.
- Publish 27 names only the attackers' chance: "Minor Artifacts will have a chance of spawning in each attacker's backpack after a paragon creature or a leviathan is slain." [O5] Publish 28 adds the thrower's roll [O6], so before SE there is none (our inference). The attackers' chance is unchanged.
### Magic fish
From SE the prized, wondrous and truly rare fish give +1 for 12 seconds instead of +5 for a minute [C1]. A Stratics FAQ of January 5, 2005 already says "The stat boost fish used to provide the person who ate it a +5 boost, but now it's only one or two points, and lasts only a short while." [S16]
- Stratics still gives +5 in December 2002 [S7] and no note dates the change. [S16], during SE, is the earliest source with the new values, so the eras before SE keep +5 and ModernUO's minute (our inference).
- Today's guide gives "Adds 8 – 12% to your Dexterity for 150 seconds when eaten" [O9]. No note dates that change either, so it is left out.
### The white pearl at an SOS's spot
From ML the white pearl is a bonus on top of the catch, but it went through `Fishing.Construct`'s SOS branch: at an SOS's spot the fisherman read "You have found a white pearl!" and got a shipwrecked item or the chest, a second SOS roll in the same cast. The pearl now skips that branch.
The keys are #2687's, plus:
- [O14] uo.com, the 2002 publishes: Publish 16 in parts on April 23, July 23, September 20 and December 2, 2002. https://uo.com/wiki/ultima-online-wiki/technical/previous-publishes/2002-2/
- [S14] Stratics fishing board, FAQ 1.2 "How do I train fishing?", January 5, 2005. https://web.archive.org/web/20050410171430/http://boards.stratics.com/php-bin/uo/showthreaded.php?Cat=&Board=uofisherman&Number=5622892
- [S15] Stratics fishing board, a post on Publish 5's SOS change, April 25, 2000. https://web.archive.org/web/20000511000424/http://boards.stratics.com:80/cgi-bin/forums/showflat.pl?Cat=21&Board=uofisherman&Number=144300&page=0&view=collapsed&sb=5
- [S16] The FAQ of [S14], 1.4 "What's with the horned leather and scales? (Resources)", January 5, 2005, in the FAQ's flat view. https://web.archive.org/web/20050410171356/http://boards.stratics.com/php-bin/uo/showflat.php?Cat=&Number=5622877&page=0&view=collapsed&sb=5&o=&fpart=1
- [F1] The Fishin' Statistician, 11,101 casts from August 2001 to April 2002, before Publish 16. Skill and water not stated; most likely GM and mostly deep water, since the counts include big fish and serpents. https://web.archive.org/web/20020609120859/http://www.thefcb.com:80/novellettes/stats.html
- [F2] The same page, 7,371 casts from July 2002 to February 2003, after Publish 16, with nets, maps and bottles counted too. https://web.archive.org/web/20030216074120/http://www.thefcb.com:80/novellettes/stats.html
- [F3] The FCB's interview with Hanse, an OSI designer, October 21, 2002. https://web.archive.org/web/20021104194424/http://www.thefcb.com:80/Hanse.htm
- [F4] The FCB's encyclopaedia, colored nets, last modified July 13, 2002. https://web.archive.org/web/20020715111230/http://www.thefcb.com:80/encyclopaedia/colornets.html
- [F5] The FCB's "Illustrated Treatise on Deep Water Defined", by Captain Pickles and Mason, last modified March 20, 2001. https://web.archive.org/web/20010419135823/http://www.thefcb.com:80/novellettes/deep-water.html
- [B1] Brikan's skill guide, which quotes a post on Origin's boards by Hance, a UO developer, on December 20, 2000; most likely Hanse, the OSI designer the FCB interviewed in 2002. Page last modified February 3, 2007. https://web.archive.org/web/20070715201811/http://www.jgnn.com:80/uo/bk/2skill/skill.htm
- [C1] Chrome's fishing page on OSI's Yamato shard, captured November 25, 2007: 5,000 offshore catches at GM, a table of success by water and what each magic fish does. https://web.archive.org/web/20071125144104/http://www.geocities.co.jp:80/Playtown-Yoyo/4812/fishing02.html
## Summary
Removing an item from a container now reaches only the clients that were sent the item. This PR also adds container-scoped skip serialization, makes container deletion linear, and closes a lift-rejection resync gap. It is the groundwork for the NPC vendor buyback fix (#2677), and it is safe to merge by itself.
### Container child removal reaches only the clients that were sent the item
Adds and updates for an item inside a private container have always gone only to the root mobile, the secure-trade partner, and the container's `Openers` (`ProcessDelta`, the same as RunUO). Removes, though, were broadcast to every client in range. That too is inherited from RunUO; #326 only converted the packets. Two costs:
- **Bandwidth.** One packet per nearby client for every reagent, bandage or arrow stack that runs out, and every container-to-container move.
- **Disclosure.** It sent the serial and timing of items moved in someone else's pack to every nearby client.
Now:
- **`Item.SendRemovePacket`:** a private container child's remove goes only to the root mobile, both secure-trade parties (found by walking up to the trade container, as `ProcessDelta` does), and the container's `Openers`. Each recipient is deduplicated and must be on the same map and in range.
- **`Container.IsChildPublic(Item child)`** (default `IsPublicContainer`) marks children that were sent to every client in range. Their removal still broadcasts. Overrides can only widen: a public container's children always broadcast.
- **`Corpse`** marks its worn gear public on human corpses. `SendInfoTo` shows that gear to everyone who sees the corpse, so bystanders still see looted gear disappear. Loot stays private, and corpses are not made public.
- **`IsPublicContainer` is unchanged** and remains the escape hatch.
- **Unchanged:** items equipped on mobiles, children of non-container items (spellbooks, bulletin boards), and ground items.
- **`Item.Map` no longer sends removes for contained items.** A contained item only changes map along with its parent or root, and that ancestor's own remove already clears the subtree for everyone who knew it. The owner keeps their nested bags across a facet change, as before, and bystanders no longer get a remove per nested item.
**For forks:** a custom container that sends its contents to clients other than the root, trade parties and openers must override `IsChildPublic` or `IsPublicContainer`. Otherwise those clients keep the item until they reopen the container, it leaves their range, or they log out.
- **`Item.MoveToWorld` no longer sends a second remove for an item that had a parent.** It used to detach through the parent's `RemoveItem` (correctly scoped), then broadcast another remove from the old location. Every lift goes through `Internalize()`, so every lift from a pack or container broadcast the item's serial. Ground items are unchanged.
- **Property-only updates** (`ItemDelta.Properties` without `Update`) of a private child now send the object property list revision only to the same recipients, not every client in range. Gump tooltips that reference such an item without the client holding it no longer get the revision nudge; their cached tooltip refreshes when re-requested.
### Lift rejection resyncs only items the requester was sent
A rejected lift re-sent the item's full data to the requester without checking that their client had ever been sent it. `Item.IsSentTo(Mobile)` now applies the same rule as the adds, including `CanSee`:
- **Private container children:** the root mobile, the trade parties, or the openers.
- **Everything else:** visible and in update range.
`Mobile.Lift` still always sends the lift-reject packet, but resyncs the item only when `IsSentTo` holds.
### Container content packets stay within the protocol limit
`SendContainerContent` stack-allocated `items.Count × 20` bytes, which overflowed the stack for a container with hundreds of thousands of items. The `0x3C` length and count fields are 16-bit anyway. It now caps entries so the packet fits (3448 entries, or 3276 with grid lines), and rents a pooled buffer above a small size. Output is byte-identical for normal containers.
### Container-scoped skip serialization
**`Container.SkipsChildSerialization`** (default `false`). When a container returns true:
- its direct children are left out of world saves (the base `Item.SkipSerialization` checks the parent);
- it writes no child list, so a skipped child's serial can never resolve to an unrelated item at load;
- an item that loads naming such a container as its parent is deleted by the existing missing-parent path.
**Known limitation:** `SkipSerialization` is read while the save files are written, after the world unfreezes. An item that enters an opted-in container in that window can be left out of that save. The next save is correct. Moving the decision into the freeze is a follow-up.
### Linear container deletion
`Item.Delete` removes children back to front, but `RemoveItem` searched from the front with `List.Remove`, so deleting a container of *n* items was O(n²). `RemoveItem` now checks the last entry first.
- 100,000 children took 8.0s before and 162ms after.
- Other removals cost the same as before.
**Save format unchanged for every existing type** (the schema generator produces no diff).
## Test plan
- [x] `ContainerChildRemovalTests`:
- a private child's removal isn't broadcast;
- openers, the root owner's client and a trade party (who never opened the pouch) still receive it;
- public containers and per-child `IsChildPublic` overrides still broadcast;
- an override can't narrow a public container;
- no duplicate packets when root and opener overlap;
- deleting the owner sends no remove per child;
- a facet change sends no remove for nested children to the owner or bystanders;
- lifting from a backpack or moving a contained item to the ground sends no remove to bystanders;
- equipped items and ground items keep their broadcast.
- [x] `PrivateChildPropertiesTests`: a property-only update of a private child reaches the owner and openers, not bystanders; public-container children and ground items still broadcast.
- [x] `CorpseChildPublicTests`: human corpse worn gear is public and loot isn't, bones and non-human bodies aren't; end to end, worn gear leaving the corpse reaches a bystander and deleted loot doesn't.
- [x] `LiftRejectResyncTests`:
- a bystander and an out-of-range requester get no resync;
- the owner, an opener and a trade party do;
- an invisible item isn't resynced to a non-staff owner, but is to staff;
- equipment on a hidden mobile and an invisible ground item aren't resynced.
- [x] `ContainerPacketTests`: large containers are capped at the protocol limit (with and without grid lines) and the pooled-buffer path is byte-identical.
- [x] `ContainerChildSkipTests`, `ContainerBulkRemovalTests`.
- [x] Server.Tests (944) and UOContent.Tests (1203) green on current `main`; schema generator clean.
- [x] Manual in-game pass with three clients:
- consumption;
- snooping;
- co-opened containers;
- bank;
- trade with and without opening the pouch;
- facet change with nested bags;
- pack animals;
- player vendors;
- human corpse looting by drag and by script;
- every lift-rejection reason.
Verified with a ClassicUO build instrumented to count:
- removes for unknown serials;
- adds dropped for an unknown parent;
- items learned inside containers the client never opened;
- lift resyncs of items the client never had.
A corpse's worn-gear list was fixed at creation, and clients learn worn gear only from the 0x89 packet sent on first sight. A piece lifted and dropped back showed as loose loot to everyone watching, but came back "worn" after a relog. Lifting now takes the piece off the list, so every view agrees it is loot.
Programmatic moves (the duel gather-and-re-equip in `DuelContext`) don't go through `OnItemLifted` and still see the original list.
## Summary
Digging up a decoded treasure map of level 0, or of level 2 and above, throws inside `DigTimer.OnTick` once the chest surfaces. The exception escapes the timer, so the server goes down:
```
System.NullReferenceException: Object reference not set to an instance of an object.
at Server.Items.TreasureMap.DigTimer.OnTick() in ...\Items\Maps\TreasureMap.cs:line 809
```
#2568 made `TreasureMapChest._guardians` lazily created: the constructor no longer initialises it, and migration nulls an empty list. #2569 updated the read in `CheckLocked` to match, but the dig timer still adds each spawned guardian directly:
```csharp
if (bc != null)
{
m_Chest.Guardians.Add(bc); // null on a freshly dug chest
}
```
Level 1 maps survive only because they spawn no guardians. I confirmed the regression by running the dig test below at #2568's parent, where it passes, and at #2568, where it fails.
The dig timer now calls the generated `AddToGuardians`, which creates the list on first use.
## Testing
- `dotnet build`: 0 warnings, 0 errors.
- New `TreasureMapChestGuardianTests`:
- `Dig_AddsSpawnedGuardiansToTheChest` (levels 0 and 2) drives the real `DigTimer` tick by tick until the chest surfaces. Before the fix both cases fail with the `NullReferenceException` above. It needs client map data, so it calls `TileDataRequirement.SkipIfMissing()` and skips on CI.
- `AddToGuardians_OnNewChest_KeepsLevelZeroChestShut` needs no client data, so it runs on CI: the list starts null, the first guardian creates it, and a level 0 chest stays shut to players until that guardian is dead.
- Full `UOContent.Tests`: 1196 passed and 2 skipped with client data; 1105 passed and 93 skipped without.
The fishing mutate table offers every special catch in every era, so a pre-T2A shard fishes up boots, magic fish, treasure maps and bottles (added in the March 1999 fishing update), and a T2A shard gets the special fishing net and big fish, which no T2A publish or guide lists.
Each entry now carries the expansion that added it; the chances are unchanged. The nets in SOS chests and on krakens need UO:R as well, and before SE ancient SOS roll 1 in 40 and their chests aren't hued white, since Publish 28 changed both.
Two fixes from the same notes come with it. An SOS now only works in the facet it was found in, so kraken and leviathan bottles are made at death with the creature's facet. Big fish, their trophy deed and their trophy show the fisherman's mark and weight only from SE.
## Why each change
### The rule
- `Core.T2A`, `Core.UOR`, `Core.SE`… mean "the shard is this expansion **or newer**".
- An expansion includes every publish that came out while it was the newest one. Publish 27 (September 2004) came out before Samurai Empire (November 2004), so it belongs to AOS.
- An item can't drop on a shard older than the expansion that added it.
- When the sources only narrow it down to a window that spans two expansions, the item takes the older one, so no shard loses something it may really have had.
### When each fishing item arrived
| Expansion | Released | What it added to fishing | Sources |
|---|---|---|---|
| T2A | Oct 1, 1998 | Boots, magic fish, treasure maps, bottles (update of March 17, 1999) | [1] |
| UO:R | May 4, 2000 | Special fishing net (first listed in August 2000) | [2], [6], [7] |
| UO:R or Third Dawn | | Big fish (first listed between November 2000 and March 2001) | [3], [8], [9] |
| AOS | Feb 11, 2003 | Ancient SOS at 1 in 40, fabled fishing net (Publish 27, September 14, 2004) | [4] |
| SE | Nov 2, 2004 | Ancient SOS at 1 in 25, white ancient chests, the fisherman's mark on big fish (Publish 28, January 12, 2005) | [5] |
Release dates: [13].
### `Fishing.cs`
**`MutateEntry` gets an `Expansion` field and constructor argument.**
Each row has to know which expansion added it. `MutateEntry` is private to `Fishing`, so no other code is affected.
**`MutateType` skips rows newer than the shard:**
```csharp
if (Core.Expansion < entry.m_Expansion)
{
continue;
}
```
A skipped row behaves as if it weren't in the table. Every other row rolls exactly as before.
**The rows:**
| Row | Expansion | Why |
|---|---|---|
| Boots, shoes, sandals, thigh boots | T2A | The March 17, 1999 update is the first time fishing gives anything but fish: "you now have a chance of pulling up … One of any of several types of boots". [1] |
| Magic fish (prized, wondrous, truly rare, peculiar) | T2A | Same update: "Magical fish, which can boost stamina, strength, intelligence, or dexterity". [1] |
| Treasure map | T2A | Same update: "Treasure maps". [1] |
| Message in a bottle | T2A | Same update: "Messages in bottles". [1] |
| Special fishing net | UO:R | Missing from the 1999 list [1] and from the March 2000 fishing changes, which only mention treasure maps and SOS bottles [2]. Stratics' fishing guide doesn't have it from July 1999 to July 8, 2000 [6] and has it on August 10, 2000 [7], after UO:R came out. |
| Big fish | UO:R | Stratics' fishing guide doesn't have it from July 1999 [6] to November 7, 2000 [8] and has it on May 24, 2001 [9]. Publish 11 (March 14, 2001) already lets taxidermy kits use it [3]. So it is certainly not T2A. It came at the end of UO:R or with Third Dawn (March 7, 2001); the sources can't tell which, so it takes UO:R, the older one. |
| Nothing (the empty row) | None | Exists in every expansion; unchanged. |
**SOS chest, white hue: `if (sos.IsAncient && Core.SE)`**
Publish 28: "Hued level 4 fishing chests white." [5] It came out after SE, so before SE the ancient chest keeps its normal colour.
**SOS chest, net: `if (Core.UOR) { chest.DropItem(...); }`**
You can fish up an SOS chest from T2A on, because bottles are T2A [1]. The net inside it is UO:R (see the table), so before UO:R the chest comes without a net. The fabled net only goes in ancient chests, and those already need AOS [4].
**SOS: `from.Map == sos.TargetMap`, in `CheckResources` and `Construct`**
Both accepted an SOS whenever the fisher was in Felucca or Trammel, without comparing the SOS's `TargetMap`, so a Trammel SOS paid out in Felucca at the same spot. Publish 5: "SOS bottles and treasure maps will only work in the land they are found in (for instance, a map found in Felucca will only work in Felucca)." [14] Treasure maps already check their facet.
### `MessageInABottle.cs`
**`GetRandomLevel`: `Utility.Random(Core.SE ? 25 : 40) < 1`**
Publish 27 added ancient SOS at "a 1 in 40 chance" [4]. Publish 28 "Changed Ancient SoS spawn chance from 1 in 40 to 1 in 25" [5]. So AOS rolls 1 in 40, SE and newer 1 in 25. The `Core.AOS` check was already there.
### `Kraken.cs`
**Bottle, now made in `OnBeforeDeath`: `if (Core.T2A && Utility.RandomDouble() < .05)`**
Bottles don't exist before T2A [1], so a kraken can't carry one there. The 5% is unchanged; no official source gives a number. The kraken is in Stratics' 1999 creature list [10]; its kraken page says "Loot: Unknown" in January 2000 [11] and "Loot: Messages in a Bottle, Treasure Maps, 300 - 600 gold" in October 2000 [12].
The constructor packed the bottle before the kraken had a map, so it always pointed to Trammel; with the SOS check above, a kraken killed in Felucca would drop an SOS that only works in Trammel. It is now made at death with the kraken's facet, as `BaseCreature.OnBeforeDeath` does for treasure maps; the net already does the same for the kraken it brings up.
**Net: `if (Core.UOR) { PackItem(new SpecialFishingNet()); // Confirm? }`**
Nets are UO:R [6], [7]. The `// Confirm?` stays: in 2000 Stratics says the net was "loot on Sea Serpents" [7], [12], not on krakens. Which creature drops the net is out of scope; this PR only fixes the expansion.
### `Leviathan.cs`
**Bottle, now made in `OnBeforeDeath`**
Same as the kraken: it was packed in the constructor and pointed to Trammel. It is now made at death with the leviathan's facet, and still drops every time.
### `BigFish.cs` and `TaxidermyKit.cs`
**Mark and weight: `if (Core.SE && Weight >= 20)`, and the same in the trophy and its deed**
Publish 28: "Added variable weight and fisherman's mark on big fish, big fish trophy deed, and big fish trophy. Any big fish caught that is below 20 stones will not display weight or fisherman's mark." [5] It came out after SE, so before SE none of the three shows them.
### What this PR doesn't change
No chance, skill threshold or row order changes. The open questions (which creature drops the net, the exact expansion of big fish, its weight before Publish 28, the rates) are in #2687.
## References
1. Origin Update Center, update of March 17, 1999: the fishing overhaul that adds boots, magic fish, treasure maps and bottles. https://web.archive.org/web/19990417083713/http://update.owo.com/latest.html
2. Publish 4, March 8, 2000: "Every time you successfully fish up a treasure map or SOS bottle a sea serpent will surface and attack." https://uo.com/wiki/ultima-online-wiki/technical/previous-publishes/2000-2/2000-publish-04-8th-march/ (copy from the time: https://web.archive.org/web/20000408162342/http://update.uo.com/design_180.html)
3. Publish 11, March 14, 2001: taxidermy kits accept "Big Fish". https://uo.com/wiki/ultima-online-wiki/technical/previous-publishes/2001-2/2001-publish-11-14th-march/ (copy from the time: https://web.archive.org/web/20010331072129/http://update.uo.com/design_321.html)
4. Publish 27, September 14, 2004: "There is a 1 in 40 chance that any message in a bottle becomes 'an ancient SOS'", "Added fabled fishing nets and ancient sos treasure". https://uo.com/wiki/ultima-online-wiki/technical/previous-publishes/2004-2/publish-27/
5. Publish 28, January 12, 2005: "Hued level 4 fishing chests white", "Changed Ancient SoS spawn chance from 1 in 40 to 1 in 25". https://uo.com/wiki/ultima-online-wiki/technical/previous-publishes/2005-2/publish-28-12th-january/
6. Stratics fishing guide without the net or big fish, the same text from July 24, 1999 to July 8, 2000: last modified July 24, 1999, https://web.archive.org/web/19991009203653/http://uo.stratics.com:80/fishing.htm ; last modified July 8, 2000, https://web.archive.org/web/20000711071050/http://uo.stratics.com:80/fishing.shtml
7. Stratics fishing guide, last modified August 10, 2000: lists the special fishing net, "found as loot on sea serpents". https://web.archive.org/web/20000831173915/http://uo.stratics.com:80/fishing.shtml
8. Stratics fishing guide, last modified November 7, 2000: still no big fish. https://web.archive.org/web/20001209013100/http://uo.stratics.com:80/fishing.shtml
9. Stratics fishing guide, last modified May 24, 2001: lists big fish. https://web.archive.org/web/20010608153326/http://uo.stratics.com:80/content/skills/fishing.shtml
10. Stratics Hunter's Guide menu, last modified July 4, 1999: the kraken is in the creature list. https://web.archive.org/web/19991013090543/http://uo.stratics.com:80/hunters/huntmenu.htm
11. Stratics kraken page, last modified January 1, 2000: "Loot: Unknown". https://web.archive.org/web/20000119061647/http://uo.stratics.com:80/hunters/kraken.html
12. Stratics kraken page, last modified October 15, 2000: "Loot: Messages in a Bottle, Treasure Maps, 300 - 600 gold"; the net "can be found as loot on Sea Serpents". https://web.archive.org/web/20001017190252/http://uo.stratics.com:80/hunters/kraken.shtml
13. Expansion release dates: https://en.wikipedia.org/wiki/Ultima_Online#Expansions_and_follow-up_releases
14. Publish 5, April 27, 2000: "SOS bottles and treasure maps will only work in the land they are found in". https://uo.com/wiki/ultima-online-wiki/technical/previous-publishes/2000-2/2000-publish-05-27th-april/ (copy from the time: https://web.archive.org/web/20000510032101/http://update.uo.com/design_196.html)
Fixes#2682. Thanks @DatMoshu for the detailed report and repro.
While fixing it I audited the other map, item and multi enumerators for the same class of defect. None repeats #2682 itself, but four others turned up. All are fixed here.
## Fixes
- **`StaticTileEnumerator` (#2682)**: `SetMulti` returned `SetTile()` for the first multi whose bounds contained the point. If that multi had no tile at the cell, the enumeration ended and every later multi in the sector was skipped. Movement and line of sight lost a floor that stood inside another multi's bounding box. It now moves on to the next multi.
- **`MultiBoundsEnumerator`**: deduplication used one static `HashSet` that every new query cleared. A query run inside another made the outer query skip multis or return them twice. Each multi is now yielded only from the sector that holds its `Location`; the bounds filter guarantees that sector is visited. A per-enumerator set covers a multi that isn't registered in that sector (components changed after placement). Normal queries allocate nothing.
- **`Item`/`Mobile`/`ClientDistanceEnumerator`**: ring sectors were bounded only by `sx >= _sectorStartX`. Ring cells past the map edge resolved to the shared invalid sector, once per cell. Entities at off-map coordinates were returned more than once, and sectors outside the query were created lazily. The check now covers all four sides. Results match the bounds enumerators, which never returned off-map entities.
- **`BaseBoat.MovingEntitiesEnumerator`**: `GetMovingEntities` handles a null map, but the enumerator constructor still called `boat.Map.GetMobilesInBounds` and threw. A boat with no map now gets empty enumerators.
- **`BaseDoor.ChainEnumerator`**: the walk stopped only on returning to the start door. Links are set in pairs, so `[ChainLink A B C` followed by `[Link B C` leaves A→B→C→B, and enumerating that chain never ended. The walk now stops on any door it has already yielded. Only reachable when `UseChainedFunctionality` is true, which is off in base and in all shipped content.
## Testing
- A regression test for each fix except the boat null guard. Each failed before its fix and passes after.
- Server.Tests 910/910. UOContent.Tests 1185 passed, 2 skipped.
- Test note: the `MultiComponentList(List<MultiTileEntry>)` constructor drops entries after the first whose `Flags` are 0. The new test multis use non-zero flags.
## Summary
After #2670, `BaseCreature` stores one master. Callers still reached it through three differently gated names plus hand-rolled combinations of the `Controlled`/`Summoned` flags. Each gate is now one named view:
| Member | Definition | Meaning |
|---|---|---|
| `Master` | the reference; its setter does the follower bookkeeping; serialized as before | whoever the creature answers to |
| `ControlMaster` | `Controlled ? Master : null` | the owner |
| `SummonMaster` | `Summoned ? Master : null` | the summoner |
| `GetMaster()` | `ControlMaster ?? SummonMaster` | who answers for the creature |
`ControlMaster` and `SummonMaster` stay settable so custom code keeps compiling: both setters assign `Master`. There is no guard, so a write through either one can read back as null until the matching flag is set.
An enraged creature is the one shape with a master and neither flag. It keeps its meer through `Master`, while `GetMaster()` stays null, so the meer never answers for it (notoriety, kill credit).
Readers move onto the view they mean:
- "Owner or summoner" pairs and `ControlMaster ?? SummonMaster` collapse into `GetMaster()`: pack instinct, familiars, golem, house access, mounts, pack horse, Solen friendship, spell target and guild checks, and the `[` pet command.
- Checks such as `Controlled && ControlMaster == from` drop the flag the view already applies. Checks that don't reduce keep it: `Controlled && ControlMaster != from`, a possibly-null `from`, and `MageAI.CanDispel`'s `Summoned &&`.
**No behavior change, save format unchanged** (the schema generator produces no diff). Edge cases for custom code and shard operators:
- `SummonMaster = x` on a plain pet now replaces its owner (#2670 made that a no-op), and `ControlMaster = null` on an uncontrolled summon now clears its caster (it did nothing before).
- `SpellHelper.GetGuildFor` no longer falls back to an enraged creature's meer. Meer mages have no guild.
## Docs
- `dev-docs/content-patterns.md` § Masters: which view answers which question, how to set a master, and which flag checks are redundant.
- `dev-docs/runuo-migration-docs/09-items-mobiles-creatures.md` § Masters: RunUO's two-field reads mapped onto the views, plus the gotcha for creatures that set `SummonMaster` without `Summoned` (the write compiles, the read now returns null; use `Master`).
- The `modernuo-content-patterns` and `migrate-items-mobiles` skills point at both.
## Test plan
- [x] `MasterViewsTests`: all four views across wild, pet, controlled summon, uncontrolled summon and enraged shapes; reassigning `Master` moves follower slots; `SetControlMaster(null)` keeps an uncontrolled summon's caster; deleting a summon returns the caster's slots
- [x] Serialization tests updated to the new views (round-trip, legacy v22, v23 migration)
- [x] UOContent.Tests (1178 passed, 2 skipped) and Server.Tests (907) green
## Summary
`BaseCreature` stored the pet owner (`ControlMaster`) and the summoner (`SummonMaster`) in two fields. Every flow that manages pets kept them in step by hand: summon, transfer, stable, claim, ball of summoning and GM obey. They now share one `_master` field, and the two properties read it through the Controlled and Summoned flags:
```csharp
ControlMaster => Controlled ? master : null
SummonMaster => Controlled && !Summoned ? null : master
```
Every existing reader keeps its meaning:
- `ControlMaster` is still "the owner".
- `SummonMaster` is still "the summoner, controlled or not".
Blade spirits, energy vortexes, clones, undead minions and enraged creatures (a summon master without `Summoned`) never become visible through `ControlMaster`, so the ungated readers (ML notoriety, aggressor entries and similar) behave as before.
- Setting `SummonMaster` on a plain pet does nothing, instead of quietly replacing the owner.
- `SetControlMaster` now keeps both masters in step, so the hand-written `SummonMaster` copies are removed from ball of summoning, GM obey, pet transfer, auto-stable and claim, the animal trainer's stable and claim, and jail stabling.
## Serialization
BaseCreature moves to v24 with one serialized master (slot 15); the old `SummonMaster` slot is dropped.
- `MigrateFrom(V23Content)` resolves absent fields through the same default-value methods the live reader uses.
- A save that carried both masters (a summon traded before ownership followed transfers) resolves to the owner. Legacy pre-codegen streams follow the same rule.
Requires `ModernUO.Serialization.Generator` 4.2.0 (#2671). `UOContent.Tests` was still on 4.1.0 and is bumped to match.
## Test plan
- [x] `MasterShapes_RoundTrip`: controlled pet, controlled summon, energy vortex, enraged creature
- [x] `SummonMaster_OnPlainPet_KeepsTheOwner`: owner and follower counts unchanged
- [x] `LegacyV22Stream_DifferingMasters_OwnerWins`
- [x] `V23Stream_MigratesToOneMaster_AndDefaultsAbsentFields`: both master slots -> owner; flags past bit 32; absent fields take class defaults
- [x] UOContent.Tests (1168 passed, 2 skipped) and Server.Tests (907) green
Readers of `ControlMaster` that act on "this creature is a player's pet" now also require
`Controlled`. Nothing sets a master on an uncontrolled creature today, so behavior does not
change, except that the ML notoriety branch was the one read with nothing in front of it.
The guard keeps these readers correct if a creature ever keeps a master without being
controlled (a suspended-control state such as a charm or turn-pet ability, or folding
SummonMaster into one master field).
- Notoriety (ML): a creature only takes its master's notoriety while controlled
- BaseCreature.AggressiveAction: the aggressor link to the master only applies to controlled pets
- PlayerMobile.AutoStablePets: uncontrolled summons (blade spirits, energy vortexes) are skipped
- CrystalCaveBarrier: only a controlled pet passes on its owner's quest progress
- Discordance: the own-pet exception only applies to a controlled pet
- Dismount (ML): the remount block only goes to the owner of a controlled pet
Also removes the "Summons from monsters can attack players" branch in Mobile_AllowHarmful.
It has been unreachable since #2000: a creature with no player master returns from the
NPC check above it.
Tests: UOContent.Tests 1158 passed.