_openQueue = new();
+
+ private static int _xOffset;
+ private static int _yOffset;
+
+ private Point3D _goal;
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public int Heuristic(int x, int y, int z)
{
- private const int MaxDepth = 300;
- private const int AreaSize = 38;
+ x -= _goal.X - _xOffset;
+ y -= _goal.Y - _yOffset;
+ z -= _goal.Z;
- private const int NodeCount = AreaSize * AreaSize * PlaneCount;
+ x *= 11;
+ y *= 11;
- private const int PlaneOffset = 128;
- private const int PlaneCount = 13;
- private const int PlaneHeight = 20;
- public static PathAlgorithm Instance = new FastAStarAlgorithm();
+ return x * x + y * y + z * z;
+ }
- private static readonly Direction[] _path = new Direction[AreaSize * AreaSize];
- private static readonly PathNode[] _nodes = new PathNode[NodeCount];
- private static readonly BitArray _touched = new(NodeCount);
- private static readonly BitArray _onOpen = new(NodeCount);
- private static readonly int[] _successors = new int[8];
+ public override bool CheckCondition(Mobile m, Map map, Point3D start, Point3D goal) =>
+ Utility.InRange(start, goal, AreaSize);
- private static int _xOffset;
- private static int _yOffset;
- private static int _openList;
-
- private Point3D _goal;
-
- public int Heuristic(int x, int y, int z)
+ public override Direction[] Find(Mobile m, Map map, Point3D start, Point3D goal)
+ {
+ if (!Utility.InRange(start, goal, AreaSize))
{
- x -= _goal.X - _xOffset;
- y -= _goal.Y - _yOffset;
- z -= _goal.Z;
-
- x *= 11;
- y *= 11;
-
- return x * x + y * y + z * z;
- }
-
- public override bool CheckCondition(Mobile m, Map map, Point3D start, Point3D goal) =>
- Utility.InRange(start, goal, AreaSize);
-
- private void RemoveFromChain(int node)
- {
- if (node is < 0 or >= NodeCount)
- {
- return;
- }
-
- if (!_touched[node] || !_onOpen[node])
- {
- return;
- }
-
- var prev = _nodes[node].prev;
- var next = _nodes[node].next;
-
- if (_openList == node)
- {
- _openList = next;
- }
-
- if (prev != -1)
- {
- _nodes[prev].next = next;
- }
-
- if (next != -1)
- {
- _nodes[next].prev = prev;
- }
-
- _nodes[node].prev = -1;
- _nodes[node].next = -1;
- }
-
- private void AddToChain(int node)
- {
- if (node is < 0 or >= NodeCount)
- {
- return;
- }
-
- RemoveFromChain(node);
-
- if (_openList != -1)
- {
- _nodes[_openList].prev = node;
- }
-
- _nodes[node].next = _openList;
- _nodes[node].prev = -1;
-
- _openList = node;
-
- _touched[node] = true;
- _onOpen[node] = true;
- }
-
- public override Direction[] Find(Mobile m, Map map, Point3D start, Point3D goal)
- {
- if (!Utility.InRange(start, goal, AreaSize))
- {
- return null;
- }
-
- _touched.SetAll(false);
- _onOpen.SetAll(false);
-
- _goal = goal;
-
- _xOffset = (start.X + goal.X - AreaSize) / 2;
- _yOffset = (start.Y + goal.Y - AreaSize) / 2;
-
- var fromNode = GetIndex(start.X, start.Y, start.Z);
- var destNode = GetIndex(goal.X, goal.Y, goal.Z);
-
- _openList = fromNode;
-
- _nodes[_openList].cost = 0;
- _nodes[_openList].total = Heuristic(start.X - _xOffset, start.Y - _yOffset, start.Z);
- _nodes[_openList].parent = -1;
- _nodes[_openList].next = -1;
- _nodes[_openList].prev = -1;
- _nodes[_openList].z = start.Z;
-
- _onOpen[_openList] = true;
- _touched[_openList] = true;
-
- var bc = m as BaseCreature;
-
- int backtrack = 0, depth = 0;
-
- var path = _path;
-
- while (_openList != -1)
- {
- var bestNode = FindBest(_openList);
-
- if (++depth > MaxDepth)
- {
- break;
- }
-
- if (bc != null)
- {
- MoveImpl.AlwaysIgnoreDoors = bc.CanOpenDoors;
- MoveImpl.IgnoreMovableImpassables = bc.CanMoveOverObstacles;
- }
-
- MoveImpl.Goal = goal;
-
- var vals = _successors;
- var count = GetSuccessors(bestNode, m, map);
-
- MoveImpl.AlwaysIgnoreDoors = false;
- MoveImpl.IgnoreMovableImpassables = false;
- MoveImpl.Goal = Point3D.Zero;
-
- if (count == 0)
- {
- break;
- }
-
- for (var i = 0; i < count; ++i)
- {
- var newNode = vals[i];
-
- var wasTouched = _touched[newNode];
-
- if (wasTouched)
- {
- continue;
- }
-
- var newCost = _nodes[bestNode].cost + 1;
- var newTotal = newCost + Heuristic(
- newNode % AreaSize,
- newNode / AreaSize % AreaSize,
- _nodes[newNode].z
- );
-
- _nodes[newNode].parent = bestNode;
- _nodes[newNode].cost = newCost;
- _nodes[newNode].total = newTotal;
-
- if (_onOpen[newNode])
- {
- continue;
- }
-
- AddToChain(newNode);
-
- if (newNode != destNode)
- {
- continue;
- }
-
- var pathCount = 0;
- var parent = _nodes[newNode].parent;
-
- while (parent != -1)
- {
- path[pathCount++] = GetDirection(
- parent % AreaSize,
- parent / AreaSize % AreaSize,
- newNode % AreaSize,
- newNode / AreaSize % AreaSize
- );
- newNode = parent;
- parent = _nodes[newNode].parent;
-
- if (newNode == fromNode)
- {
- break;
- }
- }
-
- var dirs = new Direction[pathCount];
-
- while (pathCount > 0)
- {
- dirs[backtrack++] = path[--pathCount];
- }
-
- return dirs;
- }
- }
-
return null;
}
- private int GetIndex(int x, int y, int z)
+ Array.Clear(_nodeStates);
+
+ _goal = goal;
+
+ _xOffset = (start.X + goal.X - AreaSize) / 2;
+ _yOffset = (start.Y + goal.Y - AreaSize) / 2;
+
+ var fromNode = GetIndex(start.X, start.Y, start.Z);
+ var destNode = GetIndex(goal.X, goal.Y, goal.Z);
+
+ _nodes[fromNode].cost = 0;
+ _nodes[fromNode].total = Heuristic(start.X - _xOffset, start.Y - _yOffset, start.Z);
+ _nodes[fromNode].parent = -1;
+ _nodes[fromNode].z = start.Z;
+
+ _openQueue.Enqueue(fromNode, _nodes[fromNode].total);
+ _nodeStates[fromNode] = 1;
+
+ var bc = m as BaseCreature;
+
+ int backtrack = 0, depth = 0;
+
+ var path = _path;
+
+ while (_openQueue.Count > 0)
{
- x -= _xOffset;
- y -= _yOffset;
- z += PlaneOffset;
- z /= PlaneHeight;
-
- return x + y * AreaSize + z * AreaSize * AreaSize;
- }
-
- private int FindBest(int node)
- {
- var least = _nodes[node].total;
- var leastNode = node;
-
- while (node != -1)
+ if (++depth > MaxDepth)
{
- if (_nodes[node].total < least)
- {
- least = _nodes[node].total;
- leastNode = node;
- }
-
- node = _nodes[node].next;
+ break;
}
- RemoveFromChain(leastNode);
+ if (!_openQueue.TryDequeue(out var bestNode, out var bestTotal))
+ {
+ break;
+ }
- _touched[leastNode] = true;
- _onOpen[leastNode] = false;
+ // Duplicate, lower priority
+ if (_nodeStates[bestNode] == 2 || _nodes[bestNode].total != bestTotal)
+ {
+ continue;
+ }
- return leastNode;
- }
+ _nodeStates[bestNode] = 2;
- public int GetSuccessors(int p, Mobile m, Map map)
- {
- var px = p % AreaSize;
- var py = p / AreaSize % AreaSize;
- var pz = _nodes[p].z;
+ if (bc != null)
+ {
+ MoveImpl.AlwaysIgnoreDoors = bc.CanOpenDoors;
+ MoveImpl.IgnoreMovableImpassables = bc.CanMoveOverObstacles;
+ }
- var p3D = new Point3D(px + _xOffset, py + _yOffset, pz);
+ MoveImpl.Goal = goal;
var vals = _successors;
- var count = 0;
+ var count = GetSuccessors(bestNode, m, map);
- for (var i = 0; i < 8; ++i)
+ MoveImpl.AlwaysIgnoreDoors = false;
+ MoveImpl.IgnoreMovableImpassables = false;
+ MoveImpl.Goal = Point3D.Zero;
+
+ if (count == 0)
{
- int x;
- int y;
- switch (i)
- {
- default: // 0
- x = 0;
- y = -1;
- break;
- case 1:
- x = 1;
- y = -1;
- break;
- case 2:
- x = 1;
- y = 0;
- break;
- case 3:
- x = 1;
- y = 1;
- break;
- case 4:
- x = 0;
- y = 1;
- break;
- case 5:
- x = -1;
- y = 1;
- break;
- case 6:
- x = -1;
- y = 0;
- break;
- case 7:
- x = -1;
- y = -1;
- break;
- }
+ continue;
+ }
- x += px;
- y += py;
+ for (var i = 0; i < count; ++i)
+ {
+ var newNode = vals[i];
- if (x is < 0 or >= AreaSize || y is < 0 or >= AreaSize)
+ // Skip if the node is already closed
+ if (_nodeStates[newNode] == 2)
{
continue;
}
- if (CalcMoves.CheckMovement(m, map, p3D, (Direction)i, out var z))
- {
- var idx = GetIndex(x + _xOffset, y + _yOffset, z);
+ var isDiagonal = i % 2 == 1;
+ var moveCost = isDiagonal ? 14 : 10;
+ var newCost = _nodes[bestNode].cost + moveCost;
+ var newTotal = newCost + Heuristic(
+ newNode % AreaSize,
+ newNode / AreaSize % AreaSize,
+ _nodes[newNode].z
+ );
- if (idx >= 0 && idx < NodeCount)
+ if (_nodeStates[newNode] == 0 || newTotal < _nodes[newNode].total)
+ {
+ _nodes[newNode].parent = bestNode;
+ _nodes[newNode].cost = newCost;
+ _nodes[newNode].total = newTotal;
+
+ // Requeue (duplicates allowed), and mark as open
+ _openQueue.Enqueue(newNode, newTotal);
+ _nodeStates[newNode] = 1;
+ }
+
+ if (newNode != destNode)
+ {
+ continue;
+ }
+
+ var pathCount = 0;
+ var parent = _nodes[newNode].parent;
+
+ while (parent != -1)
+ {
+ path[pathCount++] = GetDirection(
+ parent % AreaSize,
+ parent / AreaSize % AreaSize,
+ newNode % AreaSize,
+ newNode / AreaSize % AreaSize
+ );
+ newNode = parent;
+ parent = _nodes[newNode].parent;
+
+ if (newNode == fromNode)
{
- _nodes[idx].z = z;
- vals[count++] = idx;
+ break;
}
}
+
+ var dirs = new Direction[pathCount];
+
+ while (pathCount > 0)
+ {
+ dirs[backtrack++] = path[--pathCount];
+ }
+
+ _openQueue.Clear();
+ return dirs;
+ }
+ }
+
+ _openQueue.Clear();
+ return null;
+ }
+
+ private static int GetIndex(int x, int y, int z)
+ {
+ x -= _xOffset;
+ y -= _yOffset;
+ z += PlaneOffset;
+ z /= PlaneHeight;
+
+ return x + y * AreaSize + z * AreaSize * AreaSize;
+ }
+
+ private static int GetSuccessors(int p, Mobile m, Map map)
+ {
+ var px = p % AreaSize;
+ var py = p / AreaSize % AreaSize;
+ var pz = _nodes[p].z;
+
+ var p3D = new Point3D(px + _xOffset, py + _yOffset, pz);
+
+ var vals = _successors;
+ var count = 0;
+
+ for (var i = 0; i < 8; ++i)
+ {
+ int x;
+ int y;
+ switch (i)
+ {
+ default: // 0
+ x = 0;
+ y = -1;
+ break;
+ case 1:
+ x = 1;
+ y = -1;
+ break;
+ case 2:
+ x = 1;
+ y = 0;
+ break;
+ case 3:
+ x = 1;
+ y = 1;
+ break;
+ case 4:
+ x = 0;
+ y = 1;
+ break;
+ case 5:
+ x = -1;
+ y = 1;
+ break;
+ case 6:
+ x = -1;
+ y = 0;
+ break;
+ case 7:
+ x = -1;
+ y = -1;
+ break;
}
- return count;
+ x += px;
+ y += py;
+
+ if (x is < 0 or >= AreaSize || y is < 0 or >= AreaSize)
+ {
+ continue;
+ }
+
+ if (CalcMoves.CheckMovement(m, map, p3D, (Direction)i, out var z))
+ {
+ var idx = GetIndex(x + _xOffset, y + _yOffset, z);
+
+ if (idx >= 0 && idx < NodeCount)
+ {
+ _nodes[idx].z = z;
+ vals[count++] = idx;
+ }
+ }
}
+
+ return count;
}
}
diff --git a/version.json b/version.json
index 227ebd3dd..d36382b04 100644
--- a/version.json
+++ b/version.json
@@ -1,4 +1,4 @@
{
"$schema": "https://raw.githubusercontent.com/dotnet/Nerdbank.GitVersioning/master/src/NerdBank.GitVersioning/version.schema.json",
- "version": "0.15.0"
+ "version": "0.15.1"
}
From 52e309ef95350e2465d7b5ff92c26cf7446ffd56 Mon Sep 17 00:00:00 2001
From: Kamron Batman <3953314+kamronbatman@users.noreply.github.com>
Date: Sun, 27 Jul 2025 11:04:32 -0700
Subject: [PATCH 20/47] chore: Updates readme files (#2247)
---
README.md | 47 ++++++++---------
RUNUO_TO_MODERNUO.md | 121 -------------------------------------------
SPONSORS.md | 2 +-
global.json | 2 +-
4 files changed, 24 insertions(+), 148 deletions(-)
delete mode 100644 RUNUO_TO_MODERNUO.md
diff --git a/README.md b/README.md
index 2184b2181..4108f12c9 100644
--- a/README.md
+++ b/README.md
@@ -9,22 +9,22 @@ ModernUO [](https://github.com/modernuo/ModernUO/blob/master/LICENSE)
[](https://github.com/modernuo/ModernUO/stargazers)
[](https://github.com/modernuo/ModernUO/issues)
-
+
[](https://github.com/modernuo/ModernUO/actions)
[](https://dev.azure.com/modernuo/modernuo/_build/latest?definitionId=1&branchName=main)
## Requirements
#### Supported Operating Systems
-[](https://www.microsoft.com/en-US/evalcenter/evaluate-windows-server-2022)
-
-[](https://www.debian.org/distrib/)
-[](https://ubuntu.com/download/server)
-
-[](https://alpinelinux.org/downloads/)
+[](https://www.microsoft.com/en-US/evalcenter/evaluate-windows-server-2022)
+
+[](https://www.debian.org/distrib/)
+[](https://ubuntu.com/download/server)
+
+[](https://alpinelinux.org/downloads/)
[](https://getfedora.org/en/server/download/)
[](https://access.redhat.com/downloads)
-[](https://www.centos.org/download/)
-[](https://get.opensuse.org/)
+[](https://www.centos.org/download/)
+[](https://get.opensuse.org/)
[](https://www.suse.com/download/sles/)
[](https://linuxmint.com/download.php)
[](https://archlinux.org/download/)
@@ -38,19 +38,16 @@ ModernUO [](https://git-scm.com/downloads)
-[](https://dotnet.microsoft.com/download/dotnet/9.0)
+[](https://dotnet.microsoft.com/download/dotnet/9.0)
#### Supported IDEs
-
-




+
+
+
+
+
+
+
## Getting Started
- Install prerequisite [requirements](https://github.com/modernuo/ModernUO#requirements)
@@ -60,9 +57,9 @@ ModernUO [] [os] [arch (default: x64)]`
- - `os` - [Supported operating systems](https://github.com/dotnet/core/blob/main/release-notes/7.0/supported-os.md)
- - `win` - Windows 10/11/2019/2022
- - `osx` - MacOS 12/13/14 (Sonoma, Big Sur, Monterey)
+ - `os` - [Supported operating systems](https://github.com/dotnet/core/blob/main/release-notes/9.0/supported-os.md)
+ - `win` - Windows 10/11/2019/2022/2025
+ - `osx` - MacOS 13/14/15 (Sequoia, Sonoma, Big Sur)
- `linux` - Linux
- `arch`
- `x64` - Intel 64-bit
@@ -107,7 +104,7 @@ Thank you for supporting us! You can find out how by visiting the [sponsors](./S
- [Voxpire](https://github.com/Voxpire), the ServUO Team & Community
- [Karasho](https://github.com/andreakarasho), [Jaedan](https://github.com/jaedan) and the ClassicUO Community
-
-Development Tools & Plugins provided with ♥ by

+
+
Development Tools & Plugins provided with ♥ by

diff --git a/RUNUO_TO_MODERNUO.md b/RUNUO_TO_MODERNUO.md
deleted file mode 100644
index 6c553f563..000000000
--- a/RUNUO_TO_MODERNUO.md
+++ /dev/null
@@ -1,121 +0,0 @@
-# From RunUO to ModernUO
-RunUO was built using C# from .NET 1.1 in 2002. There have been massive changes to technology in the past 20+ years.
-We believe it is time for Ultima Online to take advantage of this technology so a server can provide a richer experience with never before seen scale.
-
-While it is possible (and many have done it) to migrate an _active server_ from RunUO to ModernUO, it is a daunting task.
-Please ask for help in our discord!
-
-## Technology
-| | RunUO | ModernUO |
-|:-------------|:------------------------------------------------------------------------------------------------------|:------------------------------------------------------------|
-| Language | C# 4 | C# 11 (.NET 8) |
-| Supported OS | 32 & 64bit Windows or Mono | 64bit Windows, MacOS & Linux |
-| IDEs | [VS](https://visualstudio.microsoft.com/downloads/), [VSCode](https://code.visualstudio.com/download) | VS 2022+ or [Rider 2023+](https://www.jetbrains.com/rider/) |
-
-## Code API Changes
-* **ModernUO can use source generators to [serialization/deserialization](https://github.com/modernuo/SerializationGenerator#basic-usage) automatically.**
- * This is the biggest change to the API! While intimidating and different, it unlocks the ability for ModernUO to only serialize _data that has changed_.
- * We estimate a world save with 10mill objects on a busy server will take less than 1 second.
- * This feature is _optional and will remain optional indefinitely_.
-* `Serialize(GenericWriter writer)` and equiv deserialize was changed to `Serialize(IGenericWriter writer)`.
-* The following now use generics, e.g. `BeginAction(typeof(X))` is now `BeginAction`.
- * CanBeginAction, BeginAction and EndAction
- * FindRegion and IsPartOf
- * FindGump, HasGump, and CloseGump
-* Functions such as `OnAdded(object)` for both Items/Mobiles are now `OnAdded(IEntity)`.
-* Most `delegate` have been changed to `Action`.
- * Example: `EventSink.PlayerDeath += new PlayerDeathEventHandler(EventSink_PlayerDeath);` is now `EventSink.PlayerDeath += EventSink_PlayerDeath;`.
-* `ObjectPropertyList` is now `IPropertyList`,
- * Example: `GetProperties(ObjectPropertyList list)` is now `GetProperties(IPropertyList list)`.
-* `[Constructable]` attribute is now `[Constructible]`.
-
-### Object Property List API Changes
-ObjectPropertyList has been drastically optimized, which means the API has been modernized:
-
-❌ _Not valid_
-```cs
-list.Add(1061837, "{0}\t{1}", m_CurArcaneCharges, m_MaxArcaneCharges);
-```
-✅
-```cs
-list.Add(1061837, $"{_curArcaneCharges}\t{_maxArcaneCharges}");
-```
-
-Clilocs that use arguments:
-
-❌ _Not valid_
-```cs
-list.Add(1060659, "Level\t{1}", m_Level);
-```
-✅ - _Note the string as an argument, this is mandatory!_
-```cs
-list.Add(1060659, $"{"Level"}\t{Level}"); // ~1_val~: ~2_val~
-```
-
-Clilocs that use other clilocs as an argument:
-
-❌ _Do not prepend #_
-```cs
-list.Add(1060830, $"#{dirt.ToString()}");
-```
-✅ _Use the new custom cliloc argument formatter_
-```cs
-list.Add(1060830, $"{dirt:#}");
-```
-
-## Core Changes
-* ModernUO is not inherently thread safe. Overall infrastructure improved with CPU and minimizing memory garbage in mind.
-* Timer system improved drastically by using [Timer Wheels](http://www.cs.columbia.edu/~nahum/w6998/papers/sosp87-timing-wheels.pdf).
-* Networking is 5-10x faster by using fixed [Circular Buffers](https://en.wikipedia.org/wiki/Circular_buffer).
-* Network packets are no longer objects and write directly to the network buffer, improving performance by 10x.
-* World saves are 30x faster by saving to memory and flushing to disk in the background.
-* Improved RNG accuracy and performance by 5x using [Xoshiro256++](https://prng.di.unimi.it/)
-* Converted quite a bit of configuration to JSON with a central settings file.
-* Logging changed from Console.WriteLine to Serilog (still work in progress).
-* Eliminated calling `DateTime.Now` which had a huge performance penalty.
-* Accounts are now saved to a binary file (will eventually change to a databse) to improve world save performance by eliminating XML.
-* Strings are now built using the new interpolated string syntax, and highly performant string builders.
-
-## New Features
-* IPv6 support.
-* Generic serialization that is automatically done in parallel with the world save.
- * The [faction system](https://github.com/modernuo/ModernUO/blob/7adf52ef48df7ae2b034c27e67b0c332b37fb053/Projects/UOContent/Engines/Factions/Core/FactionSystem.cs#L15) is no longer powered by a single item and instead uses generic persistence.
-* Timezone support for custom scripts that might need it.
-* Hourly/daily/weekly/monthly backups & archiving using [Z-Standard](https://facebook.github.io/zstd).
-* Client version detection (including CUO) for easy configuration.
-* Localization (Cliloc) support.
-* Better encryption for passwords using [Argon2](https://en.wikipedia.org/wiki/Argon2).
-* Packet throttling that is configurable per packet and per connection.
-* Enable packet logging per connection.
-* Owner accounts can be protected from being locked out. The first account created is automatically added to this list.
-* Use optional arguments from constructors for Add command.
-* Captures Razor version and displays it in client gump
-* Spawners can be exported/imported using commands and use a GUID for replacement.
-
-## Major Feature Changes
-* By default, world saves are now every 5th minute of a real world hour.
- * E.g. 5:00, 5:05, 5:10, regardless of when the server is booted.
-* Some items have their constructor arguments rearranged.
-
-## Changes to UO Mechanics
-* Min/max skill requirements for magery adjusted to be accurate to OSI
-* Buffs/Curses now apply appropriately
-
-## Removed Features
-* Reporting
-* Remote Admin
-* My RunUO
-* Event Log
-* DocsGen command
-
-## New Development Features & Changes
-* Added a shared list/queue for temporary processing such as accumulating players to damage/kill.
- * [PooledRefList](https://github.com/modernuo/ModernUO/blob/main/Projects/Server/Collections/PooledRefList.cs)
- * [PooledRefQueue](https://github.com/modernuo/ModernUO/blob/main/Projects/Server/Collections/PooledRefQueue.cs)
-* Adds HashSet that is ordered by insertion time
- * [OrderedHashSet](https://github.com/modernuo/ModernUO/blob/main/Projects/Server/Collections/OrderedHashSet.cs)
- * [PooledOrderedHashSet](https://github.com/modernuo/ModernUO/blob/main/Projects/Server/Collections/PooledOrderedHashSet.cs)
-* Adds [StringBuilder](https://github.com/modernuo/ModernUO/blob/main/Projects/Server/Buffers/ValueStringBuilder.cs) that is fast and does not impose garbage collection.
-* Adds performant [JSON](https://github.com/modernuo/ModernUO/blob/main/Projects/Server/Json/JsonConfig.cs) support with simple API.
-* Adds performant, thread _unsafe_, [ArrayPool](https://github.com/modernuo/ModernUO/blob/main/Projects/Server/Buffers/STArrayPool.cs)
-* Adds highly performant and easy to use converters for [HexString](https://github.com/modernuo/ModernUO/blob/main/Projects/Server/Text/HexStringConverter.cs) representation of data
diff --git a/SPONSORS.md b/SPONSORS.md
index 4264670ca..f832b6a72 100644
--- a/SPONSORS.md
+++ b/SPONSORS.md
@@ -5,6 +5,7 @@ Thank you to all of our generous sponsors that make ModernUO possible.
**A special thank you to the following sponsors for their considerable contributions:**
* [Age of Shadows](https://ageofshadows.gg)
* [UO Outlands](https://uooutlands.com)
+* [UO Sagas](https://uosagas.com)
* Prayer ([MagnUm-Opus](https://discord.gg/CzDEq3vv2N))
### Looking to sponsor ModernUO?
@@ -31,6 +32,5 @@ xch1vzk93t538m4xukg955v3ltjtgz6rapev3evnmnxdzf4hjj5y7cuq7tkf6z
#### Bitcoin
37QmRWTCjVoNWycMpo1t5JnYVDmJTGSJ9W
-
#### Ethereum
0x7A9D76F497d4Ee150Ada0ea0455fF3f6e8F3b6b8
diff --git a/global.json b/global.json
index 1739913f7..733b653c1 100644
--- a/global.json
+++ b/global.json
@@ -1,6 +1,6 @@
{
"sdk": {
- "version": "9.0.101",
+ "version": "9.0.100",
"rollForward": "latestMajor",
"allowPrerelease": false
}
From bbb7dc2294d766928873b40ac8da52ab9e99d024 Mon Sep 17 00:00:00 2001
From: Kamron Batman <3953314+kamronbatman@users.noreply.github.com>
Date: Sat, 9 Aug 2025 20:55:36 -0700
Subject: [PATCH 21/47] fix: Fixes dropping a stack of scrolls on a spellbook
(#2248)
---
Projects/UOContent/Items/Skill Items/Magical/Spellbook.cs | 6 +++---
1 file changed, 3 insertions(+), 3 deletions(-)
diff --git a/Projects/UOContent/Items/Skill Items/Magical/Spellbook.cs b/Projects/UOContent/Items/Skill Items/Magical/Spellbook.cs
index f96a8980e..cdd60bd51 100644
--- a/Projects/UOContent/Items/Skill Items/Magical/Spellbook.cs
+++ b/Projects/UOContent/Items/Skill Items/Magical/Spellbook.cs
@@ -497,7 +497,7 @@ public partial class Spellbook : Item, ICraftable, ISlayer, IAosItem
public override bool OnDragDrop(Mobile from, Item dropped)
{
- if (dropped is not SpellScroll { Amount: 1 } scroll)
+ if (dropped is not SpellScroll scroll)
{
return false;
}
@@ -524,10 +524,10 @@ public partial class Spellbook : Item, ICraftable, ISlayer, IAosItem
InvalidateProperties();
- scroll.Delete();
+ scroll.Consume();
from.SendSound(0x249, GetWorldLocation());
- return true;
+ return scroll.Deleted;
}
return false;
From 340acafcb3e56b960bcbb1e30200ec5683ea2993 Mon Sep 17 00:00:00 2001
From: Kamron Batman <3953314+kamronbatman@users.noreply.github.com>
Date: Wed, 20 Aug 2025 15:30:04 -0700
Subject: [PATCH 22/47] fix: Fixes mining/lumberjacking multi-harvest (#2249)
---
Projects/UOContent/Engines/Harvest/Lumberjacking.cs | 2 ++
Projects/UOContent/Engines/Harvest/Mining.cs | 2 ++
2 files changed, 4 insertions(+)
diff --git a/Projects/UOContent/Engines/Harvest/Lumberjacking.cs b/Projects/UOContent/Engines/Harvest/Lumberjacking.cs
index b9275cda0..9640f68d8 100644
--- a/Projects/UOContent/Engines/Harvest/Lumberjacking.cs
+++ b/Projects/UOContent/Engines/Harvest/Lumberjacking.cs
@@ -175,6 +175,8 @@ namespace Server.Engines.Harvest
}
}
+ public override object GetLock(Mobile from, Item tool, HarvestDefinition def, object toHarvest) => this;
+
public override void OnHarvestStarted(Mobile from, Item tool, HarvestDefinition def, object toHarvest)
{
base.OnHarvestStarted(from, tool, def, toHarvest);
diff --git a/Projects/UOContent/Engines/Harvest/Mining.cs b/Projects/UOContent/Engines/Harvest/Mining.cs
index e79de9fed..dd7f5c747 100644
--- a/Projects/UOContent/Engines/Harvest/Mining.cs
+++ b/Projects/UOContent/Engines/Harvest/Mining.cs
@@ -435,6 +435,8 @@ namespace Server.Engines.Harvest
}
}
+ public override object GetLock(Mobile from, Item tool, HarvestDefinition def, object toHarvest) => this;
+
public override bool BeginHarvesting(Mobile from, Item tool)
{
if (!base.BeginHarvesting(from, tool))
From 641a3eb1f853b731884d9337d24c94eaeda4ee6a Mon Sep 17 00:00:00 2001
From: Kamron Batman <3953314+kamronbatman@users.noreply.github.com>
Date: Sun, 7 Sep 2025 12:50:20 -0700
Subject: [PATCH 23/47] 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.
---
.../Engines/Bulk Orders/Books/BOBGump.cs | 1310 ++++++++---------
.../Engines/Bulk Orders/Books/BODBuyGump.cs | 209 +--
2 files changed, 750 insertions(+), 769 deletions(-)
diff --git a/Projects/UOContent/Engines/Bulk Orders/Books/BOBGump.cs b/Projects/UOContent/Engines/Bulk Orders/Books/BOBGump.cs
index 28ee596c2..8dd3073f1 100644
--- a/Projects/UOContent/Engines/Bulk Orders/Books/BOBGump.cs
+++ b/Projects/UOContent/Engines/Bulk Orders/Books/BOBGump.cs
@@ -6,603 +6,598 @@ using Server.Mobiles;
using Server.Network;
using Server.Prompts;
-namespace Server.Engines.BulkOrders
+namespace Server.Engines.BulkOrders;
+
+public class BOBGump : DynamicGump
{
- public class BOBGump : Gump
+ private const int LabelColor = 0x7FFF;
+
+ private readonly PlayerMobile _from;
+ private int _page;
+
+ public BulkOrderBook Book { get; }
+ public List List { get; private set; }
+ public override bool Singleton => true;
+
+ public BOBGump(PlayerMobile from, BulkOrderBook book) : base(12, 24)
{
- private const int LabelColor = 0x7FFF;
- private readonly BulkOrderBook _book;
- private readonly PlayerMobile _from;
- private readonly List _list;
+ _from = from;
+ Book = book;
+ }
- private int _page;
+ public void ResetList() => List = null;
- public override bool Singleton => true;
-
- public BOBGump(PlayerMobile from, BulkOrderBook book, int page = 0, List list = null) : base(12, 24)
+ protected override void BuildLayout(ref DynamicGumpBuilder builder)
+ {
+ if (List == null)
{
- _from = from;
- _book = book;
- _page = page;
+ List = new List(Book.Entries.Count);
- if (list == null)
+ for (var i = 0; i < Book.Entries.Count; ++i)
{
- list = new List(book.Entries.Count);
-
- for (var i = 0; i < book.Entries.Count; ++i)
- {
- var entry = book.Entries[i];
-
- if (CheckFilter(entry))
- {
- list.Add(entry);
- }
- }
- }
-
- _list = list;
-
- var index = GetIndexForPage(page);
- var count = GetCountForIndex(index);
-
- var tableIndex = 0;
-
- var pv = book.RootParent as PlayerVendor;
-
- var canDrop = book.IsChildOf(from.Backpack);
- var canBuy = pv != null;
- var canPrice = canDrop || canBuy;
-
- if (canBuy)
- {
- var vi = pv.GetVendorItem(book);
-
- canBuy = vi?.IsForSale == false;
- }
-
- var width = 600;
-
- if (!canPrice)
- {
- width = 516;
- }
-
- X = (624 - width) / 2;
-
- AddPage(0);
-
- AddBackground(10, 10, width, 439, 5054);
- AddImageTiled(18, 20, width - 17, 420, 2624);
-
- if (canPrice)
- {
- AddImageTiled(573, 64, 24, 352, 200);
- AddImageTiled(493, 64, 78, 352, 1416);
- }
-
- if (canDrop)
- {
- AddImageTiled(24, 64, 32, 352, 1416);
- }
-
- AddImageTiled(58, 64, 36, 352, 200);
- AddImageTiled(96, 64, 133, 352, 1416);
- AddImageTiled(231, 64, 80, 352, 200);
- AddImageTiled(313, 64, 100, 352, 1416);
- AddImageTiled(415, 64, 76, 352, 200);
-
- for (var i = index; i < index + count && i >= 0 && i < list.Count; ++i)
- {
- var entry = list[i];
-
- if (!CheckFilter(entry))
- {
- continue;
- }
-
- AddImageTiled(24, 94 + tableIndex * 32, canPrice ? 573 : 489, 2, 2624);
- tableIndex += entry is BOBLargeEntry largeEntry ? largeEntry.Entries.Length : 1;
- }
-
- AddAlphaRegion(18, 20, width - 17, 420);
- AddImage(5, 5, 10460);
- AddImage(width - 15, 5, 10460);
- AddImage(5, 424, 10460);
- AddImage(width - 15, 424, 10460);
-
- AddHtmlLocalized(canPrice ? 266 : 224, 32, 200, 32, 1062220, LabelColor); // Bulk Order Book
- AddHtmlLocalized(63, 64, 200, 32, 1062213, LabelColor); // Type
- AddHtmlLocalized(147, 64, 200, 32, 1062214, LabelColor); // Item
- AddHtmlLocalized(246, 64, 200, 32, 1062215, LabelColor); // Quality
- AddHtmlLocalized(336, 64, 200, 32, 1062216, LabelColor); // Material
- AddHtmlLocalized(429, 64, 200, 32, 1062217, LabelColor); // Amount
-
- AddButton(35, 32, 4005, 4007, 1);
- AddHtmlLocalized(70, 32, 200, 32, 1062476, LabelColor); // Set Filter
-
- var f = from.UseOwnFilter ? from.BOBFilter : book.Filter;
-
- if (f.IsDefault)
- {
- AddHtmlLocalized(canPrice ? 470 : 386, 32, 120, 32, 1062475, 16927); // Using No Filter
- }
- else if (from.UseOwnFilter)
- {
- AddHtmlLocalized(canPrice ? 470 : 386, 32, 120, 32, 1062451, 16927); // Using Your Filter
- }
- else
- {
- AddHtmlLocalized(canPrice ? 470 : 386, 32, 120, 32, 1062230, 16927); // Using Book Filter
- }
-
- AddButton(375, 416, 4017, 4018, 0);
- AddHtmlLocalized(410, 416, 120, 20, 1011441, LabelColor); // EXIT
-
- if (canDrop)
- {
- AddHtmlLocalized(26, 64, 50, 32, 1062212, LabelColor); // Drop
- }
-
- if (canPrice)
- {
- AddHtmlLocalized(516, 64, 200, 32, 1062218, LabelColor); // Price
-
- if (canBuy)
- {
- AddHtmlLocalized(576, 64, 200, 32, 1062219, LabelColor); // Buy
- }
- else
- {
- AddHtmlLocalized(576, 64, 200, 32, 1062227, LabelColor); // Set
-
- AddButton(450, 416, 4005, 4007, 4);
- AddHtml(485, 416, 120, 20, "Price all");
- }
- }
-
- tableIndex = 0;
-
- if (page > 0)
- {
- AddButton(75, 416, 4014, 4016, 2);
- AddHtmlLocalized(110, 416, 150, 20, 1011067, LabelColor); // Previous page
- }
-
- if (GetIndexForPage(page + 1) < list.Count)
- {
- AddButton(225, 416, 4005, 4007, 3);
- AddHtmlLocalized(260, 416, 150, 20, 1011066, LabelColor); // Next page
- }
-
- for (var i = index; i < index + count && i >= 0 && i < list.Count; ++i)
- {
- var entry = list[i];
-
- if (!CheckFilter(entry))
- {
- continue;
- }
-
- if (entry is BOBLargeEntry largeEntry)
- {
- var y = 96 + tableIndex * 32;
-
- if (canDrop)
- {
- AddButton(35, y + 2, 5602, 5606, 5 + i * 2);
- }
-
- if (canDrop || canBuy && entry.Price > 0)
- {
- AddButton(579, y + 2, 2117, 2118, 6 + i * 2);
- AddLabel(495, y, 1152, entry.Price.ToString());
- }
-
- AddHtmlLocalized(61, y, 50, 32, 1062225, LabelColor); // Large
-
- for (var j = 0; j < largeEntry.Entries.Length; ++j)
- {
- var sub = largeEntry.Entries[j];
-
- AddHtmlLocalized(103, y, 130, 32, sub.Number, LabelColor);
-
- if (entry.RequireExceptional)
- {
- AddHtmlLocalized(235, y, 80, 20, 1060636, LabelColor); // exceptional
- }
- else
- {
- AddHtmlLocalized(235, y, 80, 20, 1011542, LabelColor); // normal
- }
-
- var name = GetMaterialName(entry.Material, entry.DeedType, sub.ItemType);
-
- if (name.Number > 0)
- {
- AddHtmlLocalized(316, y, 100, 20, name, LabelColor);
- }
- else
- {
- AddLabel(316, y, 1152, name);
- }
-
- AddLabel(421, y, 1152, $"{sub.AmountCur} / {entry.AmountMax}");
-
- ++tableIndex;
- y += 32;
- }
- }
- else
- {
- var smallEntry = (BOBSmallEntry)entry;
-
- var y = 96 + tableIndex++ * 32;
-
- if (canDrop)
- {
- AddButton(35, y + 2, 5602, 5606, 5 + i * 2);
- }
-
- if (canDrop || canBuy && smallEntry.Price > 0)
- {
- AddButton(579, y + 2, 2117, 2118, 6 + i * 2);
- AddLabel(495, y, 1152, smallEntry.Price.ToString());
- }
-
- AddHtmlLocalized(61, y, 50, 32, 1062224, LabelColor); // Small
-
- AddHtmlLocalized(103, y, 130, 32, smallEntry.Number, LabelColor);
-
- if (smallEntry.RequireExceptional)
- {
- AddHtmlLocalized(235, y, 80, 20, 1060636, LabelColor); // exceptional
- }
- else
- {
- AddHtmlLocalized(235, y, 80, 20, 1011542, LabelColor); // normal
- }
-
- var name = GetMaterialName(smallEntry.Material, smallEntry.DeedType, smallEntry.ItemType);
-
- if (name.Number > 0)
- {
- AddHtmlLocalized(316, y, 100, 20, name, LabelColor);
- }
- else
- {
- AddLabel(316, y, 1152, name);
- }
-
- AddLabel(421, y, 1152, $"{smallEntry.AmountCur} / {smallEntry.AmountMax}");
- }
- }
- }
-
- public bool CheckFilter(IBOBEntry entry)
- {
- if (entry is BOBLargeEntry largeEntry)
- {
- return CheckFilter(
- entry.Material,
- entry.AmountMax,
- true,
- entry.RequireExceptional,
- entry.DeedType,
- largeEntry.Entries.Length > 0 ? largeEntry.Entries[0].ItemType : null
- );
- }
-
- if (entry is BOBSmallEntry smallEntry)
- {
- return CheckFilter(
- entry.Material,
- entry.AmountMax,
- false,
- entry.RequireExceptional,
- entry.DeedType,
- smallEntry.ItemType
- );
- }
-
- return false;
- }
-
- public bool CheckFilter(
- BulkMaterialType mat, int amountMax, bool isLarge, bool reqExc, BODType deedType,
- Type itemType
- )
- {
- var f = _from.UseOwnFilter ? _from.BOBFilter : _book.Filter;
-
- if (f.IsDefault)
- {
- return true;
- }
-
- if (f.Quality == 1 && reqExc)
- {
- return false;
- }
-
- if (f.Quality == 2 && !reqExc)
- {
- return false;
- }
-
- if (f.Quantity == 1 && amountMax != 10)
- {
- return false;
- }
-
- if (f.Quantity == 2 && amountMax != 15)
- {
- return false;
- }
-
- if (f.Quantity == 3 && amountMax != 20)
- {
- return false;
- }
-
- if (f.Type == 1 && isLarge)
- {
- return false;
- }
-
- if (f.Type == 2 && !isLarge)
- {
- return false;
- }
-
- return f.Material switch
- {
- 1 => deedType == BODType.Smith,
- 2 => deedType == BODType.Tailor,
- 3 => mat == BulkMaterialType.None && BGTClassifier.Classify(deedType, itemType) == BulkGenericType.Iron,
- 4 => mat == BulkMaterialType.DullCopper,
- 5 => mat == BulkMaterialType.ShadowIron,
- 6 => mat == BulkMaterialType.Copper,
- 7 => mat == BulkMaterialType.Bronze,
- 8 => mat == BulkMaterialType.Gold,
- 9 => mat == BulkMaterialType.Agapite,
- 10 => mat == BulkMaterialType.Verite,
- 11 => mat == BulkMaterialType.Valorite,
- 12 => mat == BulkMaterialType.None && BGTClassifier.Classify(deedType, itemType) == BulkGenericType.Cloth,
- 13 => mat == BulkMaterialType.None && BGTClassifier.Classify(deedType, itemType) == BulkGenericType.Leather,
- 14 => mat == BulkMaterialType.Spined,
- 15 => mat == BulkMaterialType.Horned,
- 16 => mat == BulkMaterialType.Barbed,
- _ => true
- };
- }
-
- public int GetIndexForPage(int page)
- {
- var index = 0;
-
- while (page-- > 0)
- {
- index += GetCountForIndex(index);
- }
-
- return index;
- }
-
- public int GetCountForIndex(int index)
- {
- var slots = 0;
- var count = 0;
-
- var list = _list;
-
- for (var i = index; i >= 0 && i < list.Count; ++i)
- {
- var entry = list[i];
+ var entry = Book.Entries[i];
if (CheckFilter(entry))
{
- var add = entry is BOBLargeEntry largeEntry ? largeEntry.Entries.Length : 1;
-
- if (slots + add > 10)
- {
- break;
- }
-
- slots += add;
+ List.Add(entry);
}
-
- ++count;
}
-
- return count;
}
- public int GetPageForIndex(int index, int sizeDropped)
+ var index = GetIndexForPage(_page);
+ var count = GetCountForIndex(index);
+
+ var tableIndex = 0;
+
+ var canDrop = Book.IsChildOf(_from.Backpack);
+ var pv = Book.RootParent as PlayerVendor;
+ var canBuy = pv != null;
+ var canPrice = canDrop || canBuy;
+
+ if (canBuy)
{
- if (index <= 0)
+ var vi = pv.GetVendorItem(Book);
+
+ canBuy = vi?.IsForSale == false;
+ }
+
+ var width = 600;
+
+ if (!canPrice)
+ {
+ width = 516;
+ }
+
+ X = (624 - width) / 2;
+
+ builder.AddPage();
+
+ builder.AddBackground(10, 10, width, 439, 5054);
+ builder.AddImageTiled(18, 20, width - 17, 420, 2624);
+
+ if (canPrice)
+ {
+ builder.AddImageTiled(573, 64, 24, 352, 200);
+ builder.AddImageTiled(493, 64, 78, 352, 1416);
+ }
+
+ if (canDrop)
+ {
+ builder.AddImageTiled(24, 64, 32, 352, 1416);
+ }
+
+ builder.AddImageTiled(58, 64, 36, 352, 200);
+ builder.AddImageTiled(96, 64, 133, 352, 1416);
+ builder.AddImageTiled(231, 64, 80, 352, 200);
+ builder.AddImageTiled(313, 64, 100, 352, 1416);
+ builder.AddImageTiled(415, 64, 76, 352, 200);
+
+ for (var i = index; i < index + count && i >= 0 && i < List.Count; ++i)
+ {
+ var entry = List[i];
+
+ if (!CheckFilter(entry))
{
- return 0;
+ continue;
}
- var count = 0;
- var page = 0;
- int i;
+ builder.AddImageTiled(24, 94 + tableIndex * 32, canPrice ? 573 : 489, 2, 2624);
+ tableIndex += entry is BOBLargeEntry largeEntry ? largeEntry.Entries.Length : 1;
+ }
- var list = _list;
- for (i = 0; i < index && i < list.Count; i++)
+ builder.AddAlphaRegion(18, 20, width - 17, 420);
+ builder.AddImage(5, 5, 10460);
+ builder.AddImage(width - 15, 5, 10460);
+ builder.AddImage(5, 424, 10460);
+ builder.AddImage(width - 15, 424, 10460);
+
+ builder.AddHtmlLocalized(canPrice ? 266 : 224, 32, 200, 32, 1062220, LabelColor); // Bulk Order Book
+ builder.AddHtmlLocalized(63, 64, 200, 32, 1062213, LabelColor); // Type
+ builder.AddHtmlLocalized(147, 64, 200, 32, 1062214, LabelColor); // Item
+ builder.AddHtmlLocalized(246, 64, 200, 32, 1062215, LabelColor); // Quality
+ builder.AddHtmlLocalized(336, 64, 200, 32, 1062216, LabelColor); // Material
+ builder.AddHtmlLocalized(429, 64, 200, 32, 1062217, LabelColor); // Amount
+
+ builder.AddButton(35, 32, 4005, 4007, 1);
+ builder.AddHtmlLocalized(70, 32, 200, 32, 1062476, LabelColor); // Set Filter
+
+ var f = _from.UseOwnFilter ? _from.BOBFilter : Book.Filter;
+
+ if (f.IsDefault)
+ {
+ builder.AddHtmlLocalized(canPrice ? 470 : 386, 32, 120, 32, 1062475, 16927); // Using No Filter
+ }
+ else if (_from.UseOwnFilter)
+ {
+ builder.AddHtmlLocalized(canPrice ? 470 : 386, 32, 120, 32, 1062451, 16927); // Using Your Filter
+ }
+ else
+ {
+ builder.AddHtmlLocalized(canPrice ? 470 : 386, 32, 120, 32, 1062230, 16927); // Using Book Filter
+ }
+
+ builder.AddButton(375, 416, 4017, 4018, 0);
+ builder.AddHtmlLocalized(410, 416, 120, 20, 1011441, LabelColor); // EXIT
+
+ if (canDrop)
+ {
+ builder.AddHtmlLocalized(26, 64, 50, 32, 1062212, LabelColor); // Drop
+ }
+
+ if (canPrice)
+ {
+ builder.AddHtmlLocalized(516, 64, 200, 32, 1062218, LabelColor); // Price
+
+ if (canBuy)
+ {
+ builder.AddHtmlLocalized(576, 64, 200, 32, 1062219, LabelColor); // Buy
+ }
+ else
+ {
+ builder.AddHtmlLocalized(576, 64, 200, 32, 1062227, LabelColor); // Set
+
+ builder.AddButton(450, 416, 4005, 4007, 4);
+ builder.AddHtml(485, 416, 120, 20, "Price all");
+ }
+ }
+
+ tableIndex = 0;
+
+ if (_page > 0)
+ {
+ builder.AddButton(75, 416, 4014, 4016, 2);
+ builder.AddHtmlLocalized(110, 416, 150, 20, 1011067, LabelColor); // Previous page
+ }
+
+ if (GetIndexForPage(_page + 1) < List.Count)
+ {
+ builder.AddButton(225, 416, 4005, 4007, 3);
+ builder.AddHtmlLocalized(260, 416, 150, 20, 1011066, LabelColor); // Next page
+ }
+
+ for (var i = index; i < index + count && i >= 0 && i < List.Count; ++i)
+ {
+ var entry = List[i];
+
+ if (!CheckFilter(entry))
+ {
+ continue;
+ }
+
+ if (entry is BOBLargeEntry largeEntry)
+ {
+ var y = 96 + tableIndex * 32;
+
+ if (canDrop)
+ {
+ builder.AddButton(35, y + 2, 5602, 5606, 5 + i * 2);
+ }
+
+ if (canDrop || canBuy && entry.Price > 0)
+ {
+ builder.AddButton(579, y + 2, 2117, 2118, 6 + i * 2);
+ builder.AddLabel(495, y, 1152, entry.Price.ToString());
+ }
+
+ builder.AddHtmlLocalized(61, y, 50, 32, 1062225, LabelColor); // Large
+
+ for (var j = 0; j < largeEntry.Entries.Length; ++j)
+ {
+ var sub = largeEntry.Entries[j];
+
+ builder.AddHtmlLocalized(103, y, 130, 32, sub.Number, LabelColor);
+
+ builder.AddHtmlLocalized(
+ 235,
+ y,
+ 80,
+ 20,
+ entry.RequireExceptional
+ ? 1060636 // exceptional
+ : 1011542, // normal
+ LabelColor
+ );
+
+ var name = GetMaterialName(entry.Material, entry.DeedType, sub.ItemType);
+
+ name.AddHtmlText(
+ ref builder,
+ 316,
+ y,
+ 100,
+ 20,
+ false,
+ false,
+ 1152,
+ LabelColor
+ );
+
+ builder.AddLabel(421, y, 1152, $"{sub.AmountCur} / {entry.AmountMax}");
+
+ ++tableIndex;
+ y += 32;
+ }
+ }
+ else
+ {
+ var smallEntry = (BOBSmallEntry)entry;
+
+ var y = 96 + tableIndex++ * 32;
+
+ if (canDrop)
+ {
+ builder.AddButton(35, y + 2, 5602, 5606, 5 + i * 2);
+ }
+
+ if (canDrop || canBuy && smallEntry.Price > 0)
+ {
+ builder.AddButton(579, y + 2, 2117, 2118, 6 + i * 2);
+ builder.AddLabel(495, y, 1152, $"{smallEntry.Price}");
+ }
+
+ builder.AddHtmlLocalized(61, y, 50, 32, 1062224, LabelColor); // Small
+
+ builder.AddHtmlLocalized(103, y, 130, 32, smallEntry.Number, LabelColor);
+
+ builder.AddHtmlLocalized(
+ 235,
+ y,
+ 80,
+ 20,
+ smallEntry.RequireExceptional
+ ? 1060636 // exceptional
+ : 1011542, // normal
+ LabelColor
+ );
+
+ var name = GetMaterialName(smallEntry.Material, smallEntry.DeedType, smallEntry.ItemType);
+
+ name.AddHtmlText(
+ ref builder,
+ 316,
+ y,
+ 100,
+ 20,
+ false,
+ false,
+ 1152,
+ LabelColor
+ );
+
+ builder.AddLabel(421, y, 1152, $"{smallEntry.AmountCur} / {smallEntry.AmountMax}");
+ }
+ }
+ }
+
+ public bool CheckFilter(IBOBEntry entry)
+ {
+ if (entry is BOBLargeEntry largeEntry)
+ {
+ return CheckFilter(
+ entry.Material,
+ entry.AmountMax,
+ true,
+ entry.RequireExceptional,
+ entry.DeedType,
+ largeEntry.Entries.Length > 0 ? largeEntry.Entries[0].ItemType : null
+ );
+ }
+
+ if (entry is BOBSmallEntry smallEntry)
+ {
+ return CheckFilter(
+ entry.Material,
+ entry.AmountMax,
+ false,
+ entry.RequireExceptional,
+ entry.DeedType,
+ smallEntry.ItemType
+ );
+ }
+
+ return false;
+ }
+
+ public bool CheckFilter(
+ BulkMaterialType mat, int amountMax, bool isLarge, bool reqExc, BODType deedType,
+ Type itemType
+ )
+ {
+ var f = _from.UseOwnFilter ? _from.BOBFilter : Book.Filter;
+
+ if (f.IsDefault)
+ {
+ return true;
+ }
+
+ if (f.Quality == 1 && reqExc)
+ {
+ return false;
+ }
+
+ if (f.Quality == 2 && !reqExc)
+ {
+ return false;
+ }
+
+ if (f.Quantity == 1 && amountMax != 10)
+ {
+ return false;
+ }
+
+ if (f.Quantity == 2 && amountMax != 15)
+ {
+ return false;
+ }
+
+ if (f.Quantity == 3 && amountMax != 20)
+ {
+ return false;
+ }
+
+ if (f.Type == 1 && isLarge)
+ {
+ return false;
+ }
+
+ if (f.Type == 2 && !isLarge)
+ {
+ return false;
+ }
+
+ return f.Material switch
+ {
+ 1 => deedType == BODType.Smith,
+ 2 => deedType == BODType.Tailor,
+ 3 => mat == BulkMaterialType.None && BGTClassifier.Classify(deedType, itemType) == BulkGenericType.Iron,
+ 4 => mat == BulkMaterialType.DullCopper,
+ 5 => mat == BulkMaterialType.ShadowIron,
+ 6 => mat == BulkMaterialType.Copper,
+ 7 => mat == BulkMaterialType.Bronze,
+ 8 => mat == BulkMaterialType.Gold,
+ 9 => mat == BulkMaterialType.Agapite,
+ 10 => mat == BulkMaterialType.Verite,
+ 11 => mat == BulkMaterialType.Valorite,
+ 12 => mat == BulkMaterialType.None && BGTClassifier.Classify(deedType, itemType) == BulkGenericType.Cloth,
+ 13 => mat == BulkMaterialType.None && BGTClassifier.Classify(deedType, itemType) == BulkGenericType.Leather,
+ 14 => mat == BulkMaterialType.Spined,
+ 15 => mat == BulkMaterialType.Horned,
+ 16 => mat == BulkMaterialType.Barbed,
+ _ => true
+ };
+ }
+
+ public int GetIndexForPage(int page)
+ {
+ var index = 0;
+
+ while (page-- > 0)
+ {
+ index += GetCountForIndex(index);
+ }
+
+ return index;
+ }
+
+ public int GetCountForIndex(int index)
+ {
+ var slots = 0;
+ var count = 0;
+
+ var list = List;
+
+ for (var i = index; i >= 0 && i < list.Count; ++i)
+ {
+ var entry = list[i];
+
+ if (CheckFilter(entry))
+ {
+ var add = entry is BOBLargeEntry largeEntry ? largeEntry.Entries.Length : 1;
+
+ if (slots + add > 10)
+ {
+ break;
+ }
+
+ slots += add;
+ }
+
+ ++count;
+ }
+
+ return count;
+ }
+
+ public int GetPageForIndex(int index, int sizeDropped)
+ {
+ if (index <= 0)
+ {
+ return 0;
+ }
+
+ var count = 0;
+ var page = 0;
+ int i;
+
+ var list = List;
+ for (i = 0; i < index && i < list.Count; i++)
+ {
+ var entry = list[i];
+ if (!CheckFilter(entry))
+ {
+ continue;
+ }
+
+ var add = entry is BOBLargeEntry largeEntry ? largeEntry.Entries.Length : 1;
+ count += add;
+ if (count > 10)
+ {
+ page++;
+ count = add;
+ }
+ }
+
+ /* now we are on the page of the bod preceding the dropped one.
+ * next step: checking whether we have to remain where we are.
+ * The counter i needs to be incremented as the bod to this very moment
+ * has not yet been removed from m_List */
+ i++;
+
+ /* if, for instance, a big bod of size 6 has been removed, smaller bods
+ * might fall back into this page. Depending on their sizes, the page needs
+ * to be adjusted accordingly. This is done now.
+ */
+ if (count + sizeDropped > 10)
+ {
+ while (i < list.Count && count <= 10)
{
var entry = list[i];
- if (!CheckFilter(entry))
+ if (CheckFilter(entry))
{
- continue;
+ count += entry is BOBLargeEntry largeEntry ? largeEntry.Entries.Length : 1;
}
- var add = entry is BOBLargeEntry largeEntry ? largeEntry.Entries.Length : 1;
- count += add;
- if (count > 10)
- {
- page++;
- count = add;
- }
+ i++;
}
- /* now we are on the page of the bod preceding the dropped one.
- * next step: checking whether we have to remain where we are.
- * The counter i needs to be incremented as the bod to this very moment
- * has not yet been removed from m_List */
- i++;
-
- /* if, for instance, a big bod of size 6 has been removed, smaller bods
- * might fall back into this page. Depending on their sizes, the page needs
- * to be adjusted accordingly. This is done now.
- */
- if (count + sizeDropped > 10)
+ if (count > 10)
{
- while (i < list.Count && count <= 10)
- {
- var entry = list[i];
- if (CheckFilter(entry))
- {
- count += entry is BOBLargeEntry largeEntry ? largeEntry.Entries.Length : 1;
- }
-
- i++;
- }
-
- if (count > 10)
- {
- page++;
- }
+ page++;
}
-
- return page;
}
- public TextDefinition GetMaterialName(BulkMaterialType mat, BODType type, Type itemType)
+ return page;
+ }
+
+ public static TextDefinition GetMaterialName(BulkMaterialType mat, BODType type, Type itemType) =>
+ type switch
{
- switch (type)
+ BODType.Smith => mat switch
{
- case BODType.Smith:
- {
- switch (mat)
- {
- case BulkMaterialType.None: return 1062226;
- case BulkMaterialType.DullCopper: return 1018332;
- case BulkMaterialType.ShadowIron: return 1018333;
- case BulkMaterialType.Copper: return 1018334;
- case BulkMaterialType.Bronze: return 1018335;
- case BulkMaterialType.Gold: return 1018336;
- case BulkMaterialType.Agapite: return 1018337;
- case BulkMaterialType.Verite: return 1018338;
- case BulkMaterialType.Valorite: return 1018339;
- }
-
- break;
- }
- case BODType.Tailor:
- {
- switch (mat)
- {
- case BulkMaterialType.None:
- {
- if (itemType.IsSubclassOf(typeof(BaseArmor)) || itemType.IsSubclassOf(typeof(BaseShoes)))
- {
- return 1062235;
- }
-
- return 1044286;
- }
- case BulkMaterialType.Spined: return 1062236;
- case BulkMaterialType.Horned: return 1062237;
- case BulkMaterialType.Barbed: return 1062238;
- }
-
- break;
- }
- }
-
- return "Invalid";
- }
-
- public override void SendTo(NetState ns)
- {
- ns.CloseGump();
-
- base.SendTo(ns);
- }
-
- public override void OnResponse(NetState sender, in RelayInfo info)
- {
- var index = info.ButtonID;
-
- switch (index)
+ BulkMaterialType.None => 1062226,
+ BulkMaterialType.DullCopper => 1018332,
+ BulkMaterialType.ShadowIron => 1018333,
+ BulkMaterialType.Copper => 1018334,
+ BulkMaterialType.Bronze => 1018335,
+ BulkMaterialType.Gold => 1018336,
+ BulkMaterialType.Agapite => 1018337,
+ BulkMaterialType.Verite => 1018338,
+ BulkMaterialType.Valorite => 1018339,
+ _ => 1062226
+ },
+ BODType.Tailor => mat switch
{
- case 0: // EXIT
+ BulkMaterialType.Spined => 1062236,
+ BulkMaterialType.Horned => 1062237,
+ BulkMaterialType.Barbed => 1062238,
+ _ when itemType.IsSubclassOf(typeof(BaseArmor)) ||
+ itemType.IsSubclassOf(typeof(BaseShoes)) => 1062235,
+ _ => 1044286
+ },
+ _ => TextDefinition.Empty
+ };
+
+ public override void SendTo(NetState ns)
+ {
+ ns.CloseGump();
+
+ base.SendTo(ns);
+ }
+
+ public override void OnResponse(NetState sender, in RelayInfo info)
+ {
+ var index = info.ButtonID;
+
+ switch (index)
+ {
+ case 0: // EXIT
+ {
+ break;
+ }
+ case 1: // Set Filter
+ {
+ _from.SendGump(new BOBFilterGump(_from, Book));
+
+ break;
+ }
+ case 2: // Previous page
+ {
+ if (_page > 0)
+ {
+ _page--;
+ _from.SendGump(this);
+ }
+
+ return;
+ }
+ case 3: // Next page
+ {
+ if (GetIndexForPage(_page + 1) < List.Count)
+ {
+ _page++;
+ _from.SendGump(this);
+ }
+
+ break;
+ }
+ case 4: // Price all
+ {
+ if (Book.IsChildOf(_from.Backpack))
+ {
+ _from.Prompt = new SetPricePrompt(this, null);
+ _from.SendMessage("Type in a price for all deeds in the book:");
+ }
+
+ break;
+ }
+ default:
+ {
+ index -= 5;
+
+ var type = index % 2;
+ index /= 2;
+
+ if (index < 0 || index >= List.Count)
{
break;
}
- case 1: // Set Filter
- {
- _from.SendGump(new BOBFilterGump(_from, _book));
+ var bobEntry = List[index];
+
+ if (!Book.Entries.Contains(bobEntry))
+ {
+ _from.SendLocalizedMessage(1062382); // The deed selected is not available.
break;
}
- case 2: // Previous page
+
+ if (Book.IsChildOf(_from.Backpack))
{
- if (_page > 0)
- {
- _from.SendGump(new BOBGump(_from, _book, _page - 1, _list));
- }
-
- return;
- }
- case 3: // Next page
- {
- if (GetIndexForPage(_page + 1) < _list.Count)
- {
- _from.SendGump(new BOBGump(_from, _book, _page + 1, _list));
- }
-
- break;
- }
- case 4: // Price all
- {
- if (_book.IsChildOf(_from.Backpack))
- {
- _from.Prompt = new SetPricePrompt(_book, null, _page, _list);
- _from.SendMessage("Type in a price for all deeds in the book:");
- }
-
- break;
- }
- default:
- {
- index -= 5;
-
- var type = index % 2;
- index /= 2;
-
- if (index < 0 || index >= _list.Count)
- {
- break;
- }
-
- var bobEntry = _list[index];
-
- if (!_book.Entries.Contains(bobEntry))
- {
- _from.SendLocalizedMessage(1062382); // The deed selected is not available.
- break;
- }
-
if (type == 0) // Drop
{
- if (_book.IsChildOf(_from.Backpack))
- {
- var item = bobEntry.Reconstruct();
+ var item = bobEntry.Reconstruct();
- var pack = _from.Backpack;
- if (pack?.CheckHold(
+ var pack = _from.Backpack;
+ if (pack?.CheckHold(
_from,
item,
true,
@@ -610,147 +605,126 @@ namespace Server.Engines.BulkOrders
0,
item.PileWeight + item.TotalWeight
) != true)
+ {
+ _from.SendLocalizedMessage(503204); // You do not have room in your backpack for this
+ ResetList();
+ _from.SendGump(this);
+ }
+ else
+ {
+ var sizeOfDroppedBod = bobEntry is BOBLargeEntry entry ? entry.Entries.Length : 1;
+
+ _from.AddToBackpack(item);
+
+ // The bulk order deed has been placed in your backpack.
+ _from.SendLocalizedMessage(1045152);
+
+ Book.RemoveEntry(bobEntry);
+
+ if (Book.Entries.Count / 5 < Book.ItemCount)
{
- _from.SendLocalizedMessage(503204); // You do not have room in your backpack for this
- _from.SendGump(new BOBGump(_from, _book, _page));
+ Book.ItemCount--;
+ Book.InvalidateItems();
+ }
+
+ if (Book.Entries.Count > 0)
+ {
+ _page = GetPageForIndex(index, sizeOfDroppedBod);
+ ResetList();
+ _from.SendGump(this);
}
else
{
- if (_book.IsChildOf(_from.Backpack))
- {
- var sizeOfDroppedBod = bobEntry is BOBLargeEntry entry ? entry.Entries.Length : 1;
-
- _from.AddToBackpack(item);
-
- // The bulk order deed has been placed in your backpack.
- _from.SendLocalizedMessage(1045152);
-
- _book.Entries.Remove(bobEntry);
- _book.InvalidateProperties();
-
- if (_book.Entries.Count / 5 < _book.ItemCount)
- {
- _book.ItemCount--;
- _book.InvalidateItems();
- }
-
- if (_book.Entries.Count > 0)
- {
- _page = GetPageForIndex(index, sizeOfDroppedBod);
- _from.SendGump(new BOBGump(_from, _book, _page));
- }
- else
- {
- _from.SendLocalizedMessage(1062381); // The book is empty.
- }
- }
+ _from.SendLocalizedMessage(1062381); // The book is empty.
}
}
}
else // Set Price | Buy
{
- if (_book.IsChildOf(_from.Backpack))
- {
- _from.Prompt = new SetPricePrompt(_book, bobEntry, _page, _list);
- _from.SendLocalizedMessage(1062383); // Type in a price for the deed:
- }
- else if (_book.RootParent is PlayerVendor pv)
- {
- var vi = pv.GetVendorItem(_book);
+ _from.Prompt = new SetPricePrompt(this, bobEntry);
+ _from.SendLocalizedMessage(1062383); // Type in a price for the deed:
+ }
+ }
+ else if (Book.RootParent is PlayerVendor pv)
+ {
+ var vi = pv.GetVendorItem(Book);
- if (vi?.IsForSale != false)
- {
- return;
- }
-
- var sizeOfDroppedBod = bobEntry is BOBLargeEntry largeEntry ? largeEntry.Entries.Length : 1;
- var price = bobEntry.Price;
-
- if (price == 0)
- {
- _from.SendLocalizedMessage(1062382); // The deed selected is not available.
- }
- else
- {
- if (_book.Entries.Count > 0)
- {
- _page = GetPageForIndex(index, sizeOfDroppedBod);
- _from.SendGump(new BODBuyGump(_from, _book, bobEntry, _page, price));
- }
- else
- {
- _from.SendLocalizedMessage(1062381); // The book is emptz
- }
- }
- }
+ if (vi?.IsForSale != false)
+ {
+ return;
}
- break;
+ var sizeOfDroppedBod = bobEntry is BOBLargeEntry largeEntry ? largeEntry.Entries.Length : 1;
+ var price = bobEntry.Price;
+
+ if (price == 0)
+ {
+ _from.SendLocalizedMessage(1062382); // The deed selected is not available.
+ }
+ else if (Book.Entries.Count > 0)
+ {
+ _page = GetPageForIndex(index, sizeOfDroppedBod);
+ _from.SendGump(new BODBuyGump(this, bobEntry, price));
+ }
+ else
+ {
+ _from.SendLocalizedMessage(1062381); // The book is empty
+ }
}
- }
+
+ break;
+ }
+ }
+ }
+
+ private class SetPricePrompt : Prompt
+ {
+ private readonly BOBGump _gump;
+ private readonly IBOBEntry _entry;
+
+ public SetPricePrompt(BOBGump gump, IBOBEntry entry)
+ {
+ _gump = gump;
+ _entry = entry;
}
- private class SetPricePrompt : Prompt
+ public override void OnResponse(Mobile from, string text)
{
- private readonly BulkOrderBook m_Book;
- private readonly IBOBEntry m_Entry;
- private readonly List m_List;
- private readonly int m_Page;
-
- public SetPricePrompt(BulkOrderBook book, IBOBEntry entry, int page, List list)
+ if (_entry != null && !_gump.Book.Entries.Contains(_entry))
{
- m_Book = book;
- m_Entry = entry;
- m_Page = page;
- m_List = list;
+ from.SendLocalizedMessage(1062382); // The deed selected is not available.
+ return;
}
- public override void OnResponse(Mobile from, string text)
+ var price = Utility.ToInt32(text);
+
+ if (price is < 0 or > 250000000)
{
- if (m_Entry != null && !m_Book.Entries.Contains(m_Entry))
- {
- from.SendLocalizedMessage(1062382); // The deed selected is not available.
- return;
- }
+ from.SendLocalizedMessage(1062390); // The price you requested is outrageous!
+ return;
+ }
- var price = Utility.ToInt32(text);
+ if (_entry == null)
+ {
+ for (var i = 0; i < _gump.List.Count; ++i)
+ {
+ var entry = _gump.List[i];
- if (price is < 0 or > 250000000)
- {
- from.SendLocalizedMessage(1062390); // The price you requested is outrageous!
- }
- else if (m_Entry == null)
- {
- for (var i = 0; i < m_List.Count; ++i)
+ if (!_gump.Book.Entries.Contains(entry))
{
- var entry = m_List[i];
-
- if (!m_Book.Entries.Contains(entry))
- {
- continue;
- }
-
- entry.Price = price;
+ continue;
}
- // Deed price set.
- from.SendLocalizedMessage(1062384);
-
- if (from is PlayerMobile mobile)
- {
- mobile.SendGump(new BOBGump(mobile, m_Book, m_Page, m_List));
- }
- }
- else
- {
- m_Entry.Price = price;
- from.SendLocalizedMessage(1062384); // Deed price set.
- if (from is PlayerMobile mobile)
- {
- mobile.SendGump(new BOBGump(mobile, m_Book, m_Page, m_List));
- }
+ entry.Price = price;
}
}
+ else
+ {
+ _entry.Price = price;
+ }
+
+ from.SendLocalizedMessage(1062384); // Deed price set.
+ from.SendGump(_gump);
}
}
}
diff --git a/Projects/UOContent/Engines/Bulk Orders/Books/BODBuyGump.cs b/Projects/UOContent/Engines/Bulk Orders/Books/BODBuyGump.cs
index 06ab0f72d..7f1120b8f 100644
--- a/Projects/UOContent/Engines/Bulk Orders/Books/BODBuyGump.cs
+++ b/Projects/UOContent/Engines/Bulk Orders/Books/BODBuyGump.cs
@@ -3,135 +3,142 @@ using Server.Items;
using Server.Mobiles;
using Server.Network;
-namespace Server.Engines.BulkOrders
+namespace Server.Engines.BulkOrders;
+
+public class BODBuyGump : StaticGump
{
- public class BODBuyGump : Gump
+ private BOBGump _gump;
+ private readonly IBOBEntry _entry;
+ private readonly int _price;
+
+ public BODBuyGump(BOBGump gump, IBOBEntry entry, int price) : base(100, 200)
{
- private readonly BulkOrderBook m_Book;
- private readonly IBOBEntry m_Entry;
- private readonly PlayerMobile m_From;
- private readonly int m_Page;
- private readonly int m_Price;
+ _gump = gump;
+ _entry = entry;
+ _price = price;
+ }
- public BODBuyGump(PlayerMobile from, BulkOrderBook book, IBOBEntry entry, int page, int price) : base(100, 200)
+ protected override void BuildLayout(ref StaticGumpBuilder builder)
+ {
+ builder.AddPage();
+
+ builder.AddBackground(100, 10, 300, 150, 5054);
+
+ builder.AddHtmlLocalized(125, 20, 250, 24, 1019070); // You have agreed to purchase:
+ builder.AddHtmlLocalized(125, 45, 250, 24, 1045151); // a bulk order deed
+
+ builder.AddHtmlLocalized(125, 70, 250, 24, 1019071); // for the amount of:
+ builder.AddLabelPlaceholder(125, 95, 0, "price");
+
+ builder.AddButton(250, 130, 4005, 4007, 1);
+ builder.AddHtmlLocalized(282, 130, 100, 24, 1011012); // CANCEL
+
+ builder.AddButton(120, 130, 4005, 4007, 2);
+ builder.AddHtmlLocalized(152, 130, 100, 24, 1011036); // OKAY
+ }
+
+ protected override void BuildStrings(ref GumpStringsBuilder builder)
+ {
+ builder.SetStringSlot("price", $"{_price:N0}");
+ }
+
+ public override void OnResponse(NetState sender, in RelayInfo info)
+ {
+ if (sender.Mobile is not PlayerMobile pm)
{
- m_From = from;
- m_Book = book;
- m_Entry = entry;
- m_Price = price;
- m_Page = page;
-
- AddPage(0);
-
- AddBackground(100, 10, 300, 150, 5054);
-
- AddHtmlLocalized(125, 20, 250, 24, 1019070); // You have agreed to purchase:
- AddHtmlLocalized(125, 45, 250, 24, 1045151); // a bulk order deed
-
- AddHtmlLocalized(125, 70, 250, 24, 1019071); // for the amount of:
- AddLabel(125, 95, 0, price.ToString());
-
- AddButton(250, 130, 4005, 4007, 1);
- AddHtmlLocalized(282, 130, 100, 24, 1011012); // CANCEL
-
- AddButton(120, 130, 4005, 4007, 2);
- AddHtmlLocalized(152, 130, 100, 24, 1011036); // OKAY
+ return;
}
- public override void OnResponse(NetState sender, in RelayInfo info)
+ if (info.ButtonID != 2)
{
- if (info.ButtonID != 2)
- {
- m_From.SendLocalizedMessage(503207); // Cancelled purchase.
- return;
- }
+ pm.SendLocalizedMessage(503207); // Cancelled purchase.
+ return;
+ }
- if (m_Book.RootParent is not PlayerVendor pv)
- {
- m_From.SendLocalizedMessage(1062382); // The deed selected is not available.
- return;
- }
+ var book = _gump.Book;
- if (!m_Book.Entries.Contains(m_Entry))
- {
- pv.SayTo(m_From, 1062382); // The deed selected is not available.
- return;
- }
+ if (book.RootParent is not PlayerVendor pv)
+ {
+ pm.SendLocalizedMessage(1062382); // The deed selected is not available.
+ return;
+ }
- var price = 0;
+ if (!book.Entries.Contains(_entry))
+ {
+ pv.SayTo(pm, 1062382); // The deed selected is not available.
+ return;
+ }
- if (pv.GetVendorItem(m_Book)?.IsForSale == false)
- {
- price = m_Entry.Price;
- }
+ var price = 0;
- if (price != m_Price)
- {
- pv.SayTo(
- m_From,
- "The price has been been changed. If you like, you may offer to purchase the item again."
- );
- return;
- }
+ if (pv.GetVendorItem(book)?.IsForSale == false)
+ {
+ price = _entry.Price;
+ }
- if (price == 0)
- {
- pv.SayTo(m_From, 1062382); // The deed selected is not available.
- return;
- }
+ if (price != _price)
+ {
+ pv.SayTo(
+ pm,
+ "The price has been been changed. If you like, you may offer to purchase the item again."
+ );
+ return;
+ }
- var item = m_Entry.Reconstruct();
+ if (price == 0)
+ {
+ pv.SayTo(pm, 1062382); // The deed selected is not available.
+ return;
+ }
- pv.Say(m_From.Name);
+ var item = _entry.Reconstruct();
- var pack = m_From.Backpack;
+ pv.Say(pm.Name);
- if (pack?.CheckHold(
- m_From,
+ var pack = pm.Backpack;
+
+ if (pack?.CheckHold(
+ pm,
item,
true,
true,
0,
item.PileWeight + item.TotalWeight
) != true)
+ {
+ pv.SayTo(pm, 503204); // You do not have room in your backpack for this
+ pm.SendGump(_gump);
+ item.Delete();
+ }
+ else if (pack.ConsumeTotal(typeof(Gold), price) || Banker.Withdraw(pm, price))
+ {
+ book.RemoveEntry(_entry);
+ pv.HoldGold += price;
+ pm.AddToBackpack(item);
+
+ // The bulk order deed has been placed in your backpack.
+ pm.SendLocalizedMessage(1045152);
+
+ if (book.Entries.Count / 5 < book.ItemCount)
{
- pv.SayTo(m_From, 503204); // You do not have room in your backpack for this
- m_From.SendGump(new BOBGump(m_From, m_Book, m_Page));
- item.Delete();
+ book.ItemCount--;
+ book.InvalidateItems();
+ }
+
+ if (book.Entries.Count > 0)
+ {
+ _gump.ResetList();
+ pm.SendGump(_gump);
}
else
{
- if (pack.ConsumeTotal(typeof(Gold), price) || Banker.Withdraw(m_From, price))
- {
- m_Book.RemoveEntry(m_Entry);
- m_Book.InvalidateProperties();
- pv.HoldGold += price;
- m_From.AddToBackpack(item);
-
- // The bulk order deed has been placed in your backpack.
- m_From.SendLocalizedMessage(1045152);
-
- if (m_Book.Entries.Count / 5 < m_Book.ItemCount)
- {
- m_Book.ItemCount--;
- m_Book.InvalidateItems();
- }
-
- if (m_Book.Entries.Count > 0)
- {
- m_From.SendGump(new BOBGump(m_From, m_Book, m_Page));
- }
- else
- {
- m_From.SendLocalizedMessage(1062381); // The book is empty.
- }
- }
- else
- {
- pv.SayTo(m_From, 503205); // You cannot afford this item.
- item.Delete();
- }
+ pm.SendLocalizedMessage(1062381); // The book is empty.
}
}
+ else
+ {
+ pv.SayTo(pm, 503205); // You cannot afford this item.
+ item.Delete();
+ }
}
}
From df171926bb06c90cc12d01a20032e90ee8fb4363 Mon Sep 17 00:00:00 2001
From: Kamron Batman <3953314+kamronbatman@users.noreply.github.com>
Date: Thu, 11 Sep 2025 06:32:48 -0700
Subject: [PATCH 24/47] fix: Fixes potential notoriety caching issue in
Mobile.cs (#2251)
Replaces the 2D packet cache with a simpler one outlining the packet flag changes
---
Projects/Server/Mobiles/Mobile.cs | 13 ++---
.../Network/Packets/OutgoingMobilePackets.cs | 58 ++++++++++---------
2 files changed, 35 insertions(+), 36 deletions(-)
diff --git a/Projects/Server/Mobiles/Mobile.cs b/Projects/Server/Mobiles/Mobile.cs
index 7c998e887..855244da6 100644
--- a/Projects/Server/Mobiles/Mobile.cs
+++ b/Projects/Server/Mobiles/Mobile.cs
@@ -2737,8 +2737,7 @@ public partial class Mobile : IHued, IComparable, ISpawnable, IObjectPro
const int cacheLength = OutgoingMobilePackets.MobileMovingPacketCacheByteLength;
- Span mobileMovingCache = stackalloc byte[cacheLength];
- mobileMovingCache.Clear();
+ Span mobileMovingCache = stackalloc byte[cacheLength].InitializePacket();
var ourState = m_NetState;
@@ -2887,6 +2886,7 @@ public partial class Mobile : IHued, IComparable, ISpawnable, IObjectPro
Span deadBuffer = stackalloc byte[OutgoingMobilePackets.BondedStatusPacketLength].InitializePacket();
Span removeEntity = stackalloc byte[OutgoingEntityPackets.RemoveEntityLength].InitializePacket();
Span hitsPacket = stackalloc byte[OutgoingMobilePackets.MobileAttributePacketLength].InitializePacket();
+ mobileMovingCache.InitializePacket();
foreach (var state in Map.GetClientsInRange(m_Location))
{
@@ -4328,7 +4328,7 @@ public partial class Mobile : IHued, IComparable, ISpawnable, IObjectPro
const int cacheLength = OutgoingMobilePackets.MobileMovingPacketCacheByteLength;
Span mobileMovingCache = stackalloc byte[cacheLength];
- mobileMovingCache.Clear();
+ mobileMovingCache.InitializePacket();
while (moveClientQueue.Count > 0)
{
@@ -6963,12 +6963,9 @@ public partial class Mobile : IHued, IComparable, ISpawnable, IObjectPro
flags |= 0x04;
}
}
- else
+ else if (m_Poison != null)
{
- if (m_Poison != null)
- {
- flags |= 0x04;
- }
+ flags |= 0x04;
}
if (m_Blessed || m_YellowHealthbar)
diff --git a/Projects/Server/Network/Packets/OutgoingMobilePackets.cs b/Projects/Server/Network/Packets/OutgoingMobilePackets.cs
index 2e41e656d..9345b6be9 100644
--- a/Projects/Server/Network/Packets/OutgoingMobilePackets.cs
+++ b/Projects/Server/Network/Packets/OutgoingMobilePackets.cs
@@ -25,8 +25,10 @@ public static class OutgoingMobilePackets
public const int BondedStatusPacketLength = 11;
public const int DeathAnimationPacketLength = 13;
public const int MobileMovingPacketLength = 17;
- public const int MobileMovingPacketCacheHeight = 7 * 2; // 7 notoriety, 2 client versions
- public const int MobileMovingPacketCacheByteLength = MobileMovingPacketLength * MobileMovingPacketCacheHeight;
+
+ // Mobile Moving Packet plus 2 bytes for regular/stygian flags
+ public const int MobileMovingPacketCacheByteLength = MobileMovingPacketLength + 2;
+
public const int AttributeMaximum = 100;
public const int MobileAttributePacketLength = 9;
public const int MobileAttributesPacketLength = 17;
@@ -99,27 +101,26 @@ public static class OutgoingMobilePackets
ns.Send(span);
}
- public static void CreateMobileMoving(Span buffer, Mobile m, int noto, bool stygianAbyss)
+ public static void CreateMobileMoving(Span buffer, Mobile m, int noto, byte packetFlags)
{
- if (buffer[0] != 0)
+ if (buffer[0] == 0)
{
- return;
+ var loc = m.Location;
+ var hue = m.SolidHueOverride >= 0 ? m.SolidHueOverride : m.Hue;
+
+ var writer = new SpanWriter(buffer);
+ writer.Write((byte)0x77); // Packet ID
+ writer.Write(m.Serial);
+ writer.Write((short)m.Body);
+ writer.Write((short)loc.m_X);
+ writer.Write((short)loc.m_Y);
+ writer.Write((sbyte)loc.m_Z);
+ writer.Write((byte)m.Direction);
+ writer.Write((short)hue);
}
- var loc = m.Location;
- var hue = m.SolidHueOverride >= 0 ? m.SolidHueOverride : m.Hue;
-
- var writer = new SpanWriter(buffer);
- writer.Write((byte)0x77); // Packet ID
- writer.Write(m.Serial);
- writer.Write((short)m.Body);
- writer.Write((short)loc.m_X);
- writer.Write((short)loc.m_Y);
- writer.Write((sbyte)loc.m_Z);
- writer.Write((byte)m.Direction);
- writer.Write((short)hue);
- writer.Write((byte)m.GetPacketFlags(stygianAbyss));
- writer.Write((byte)noto);
+ buffer[15] = packetFlags;
+ buffer[16] = (byte)noto;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
@@ -134,7 +135,8 @@ public static class OutgoingMobilePackets
}
Span buffer = stackalloc byte[MobileMovingPacketLength].InitializePacket();
- CreateMobileMoving(buffer, target, noto, ns.StygianAbyss);
+ var packetFlags = (byte)target.GetPacketFlags(ns.StygianAbyss);
+ CreateMobileMoving(buffer, target, noto, packetFlags);
ns.Send(buffer);
}
@@ -142,8 +144,6 @@ public static class OutgoingMobilePackets
public static void SendMobileMovingUsingCache(this NetState ns, Span cache, Mobile source, Mobile target) =>
ns.SendMobileMovingUsingCache(cache, target, Notoriety.Compute(source, target));
- // Requires a buffer of 14 packets, 17 bytes per packet (238 bytes).
- // Requires cache to have the first byte of each packet initially zeroed.
public static void SendMobileMovingUsingCache(this NetState ns, Span cache, Mobile target, int noto)
{
if (ns.CannotSendPackets())
@@ -151,13 +151,15 @@ public static class OutgoingMobilePackets
return;
}
- var stygianAbyss = ns.StygianAbyss;
- // Indexes 0-6 for pre-SA, and 7-13 for SA
- var row = noto + (stygianAbyss ? 6 : -1);
- var buffer = cache.Slice(row * MobileMovingPacketLength, MobileMovingPacketLength);
- CreateMobileMoving(buffer, target, noto, stygianAbyss);
+ // Cache the packet flags for regular/stygian if the packet hasn't been built yet
+ if (cache[0] == 0)
+ {
+ cache[17] = (byte)target.GetPacketFlags(false);
+ cache[18] = (byte)target.GetPacketFlags(true);
+ }
- ns.Send(buffer);
+ CreateMobileMoving(cache, target, noto, ns.StygianAbyss ? cache[18] : cache[17]);
+ ns.Send(cache[..17]);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
From fbf8497c59c63e633b7c118803c76c313773a692 Mon Sep 17 00:00:00 2001
From: Copilot <198982749+Copilot@users.noreply.github.com>
Date: Fri, 31 Oct 2025 00:53:47 -0700
Subject: [PATCH 25/47] 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>
---
Projects/UOContent/Engines/Plants/PlantSystem.cs | 10 ++++++++--
1 file changed, 8 insertions(+), 2 deletions(-)
diff --git a/Projects/UOContent/Engines/Plants/PlantSystem.cs b/Projects/UOContent/Engines/Plants/PlantSystem.cs
index 61d017c0f..cbbd0cb9c 100644
--- a/Projects/UOContent/Engines/Plants/PlantSystem.cs
+++ b/Projects/UOContent/Engines/Plants/PlantSystem.cs
@@ -320,7 +320,10 @@ namespace Server.Engines.Plants
}
[SerializableFieldSaveFlag(17)]
- private bool ShouldSerializeLeftSeeds() => _leftSeeds != 0;
+ private bool ShouldSerializeLeftSeeds() => _leftSeeds != 8;
+
+ [SerializableFieldDefault(17)]
+ private int LeftSeedsDefaultValue() => 8;
[SerializableProperty(18)]
public int AvailableResources
@@ -340,7 +343,10 @@ namespace Server.Engines.Plants
}
[SerializableFieldSaveFlag(19)]
- private bool ShouldSerializeLeftResources() => _leftResources != 0;
+ private bool ShouldSerializeLeftResources() => _leftResources != 8;
+
+ [SerializableFieldDefault(19)]
+ private int LeftResourcesDefaultValue() => 8;
public void Reset(bool potions)
{
From 3a3f5ee518ea37f1c23996ca7ffe87e8e3c5f725 Mon Sep 17 00:00:00 2001
From: Kamron Batman <3953314+kamronbatman@users.noreply.github.com>
Date: Tue, 11 Nov 2025 11:26:34 -0800
Subject: [PATCH 26/47] feat: Adds .NET 10 / C# 14 support. (#2258)
### Summary
* Adds .NET 10 support
* Bumps to C# 14
---
.config/dotnet-tools.json | 2 +-
.github/workflows/build-test.yml | 4 +-
.github/workflows/create-release.yml | 2 +-
Directory.Build.props | 10 +-
Projects/Server.Tests/Server.Tests.csproj | 4 +-
Projects/Server/Random/BuiltInSecureRng.cs | 28 ------
Projects/Server/Server.csproj | 4 +-
.../UOContent.Tests/UOContent.Tests.csproj | 4 +-
.../Accounting/Security/AccountSecurity.cs | 91 +++++++++----------
.../Security/Argon2PasswordProtection.cs | 21 ++---
.../HashAlgorithmPasswordProtection.cs | 35 ++++---
.../Security/PBKDF2PasswordProtection.cs | 59 ++++++------
Projects/UOContent/UOContent.csproj | 8 +-
azure-pipelines.yml | 2 +-
global.json | 2 +-
version.json | 2 +-
16 files changed, 124 insertions(+), 154 deletions(-)
delete mode 100644 Projects/Server/Random/BuiltInSecureRng.cs
diff --git a/.config/dotnet-tools.json b/.config/dotnet-tools.json
index 08b49e256..5e8c8ad9b 100644
--- a/.config/dotnet-tools.json
+++ b/.config/dotnet-tools.json
@@ -3,7 +3,7 @@
"isRoot": true,
"tools": {
"modernuoschemagenerator": {
- "version": "2.12.20",
+ "version": "2.13.0",
"commands": [
"ModernUOSchemaGenerator"
]
diff --git a/.github/workflows/build-test.yml b/.github/workflows/build-test.yml
index c2f1c3b2f..0a906a743 100644
--- a/.github/workflows/build-test.yml
+++ b/.github/workflows/build-test.yml
@@ -23,7 +23,7 @@ jobs:
- uses: actions/checkout@v4
with:
fetch-depth: 0 # avoid shallow clone so nbgv can do its work.
- - name: Install .NET 9
+ - name: Install .NET
uses: actions/setup-dotnet@v4
with:
global-json-file: global.json
@@ -86,7 +86,7 @@ jobs:
- uses: actions/checkout@v4
with:
fetch-depth: 0 # avoid shallow clone so nbgv can do its work.
- - name: Install .NET 9
+ - name: Install .NET
uses: actions/setup-dotnet@v4
with:
global-json-file: global.json
diff --git a/.github/workflows/create-release.yml b/.github/workflows/create-release.yml
index 0e8334e59..aa02e4489 100644
--- a/.github/workflows/create-release.yml
+++ b/.github/workflows/create-release.yml
@@ -14,7 +14,7 @@ jobs:
with:
fetch-depth: 0 # avoid shallow clone so nbgv can do its work.
token: ${{ secrets.PERSONAL_ACCESS_TOKEN }}
- - name: Install .NET 9
+ - name: Install .NET
uses: actions/setup-dotnet@v4
with:
global-json-file: global.json
diff --git a/Directory.Build.props b/Directory.Build.props
index ae71e92d4..57f39a926 100644
--- a/Directory.Build.props
+++ b/Directory.Build.props
@@ -3,9 +3,9 @@
Kamron Batman
ModernUO
- 2019-2024
- net9.0
- 13
+ 2019-2025
+ net10.0
+ 14
true
true
NU1603
@@ -64,9 +64,9 @@
-
+
- 3.7.115
+ 3.9.50
all
diff --git a/Projects/Server.Tests/Server.Tests.csproj b/Projects/Server.Tests/Server.Tests.csproj
index 038575c90..0f913dd48 100644
--- a/Projects/Server.Tests/Server.Tests.csproj
+++ b/Projects/Server.Tests/Server.Tests.csproj
@@ -5,9 +5,9 @@
Server.Tests
-
+
-
+
all
runtime; build; native; contentfiles; analyzers; buildtransitive
diff --git a/Projects/Server/Random/BuiltInSecureRng.cs b/Projects/Server/Random/BuiltInSecureRng.cs
deleted file mode 100644
index dec408338..000000000
--- a/Projects/Server/Random/BuiltInSecureRng.cs
+++ /dev/null
@@ -1,28 +0,0 @@
-/*************************************************************************
- * ModernUO *
- * Copyright 2019-2023 - ModernUO Development Team *
- * Email: hi@modernuo.com *
- * File: SecureRandom.cs *
- * *
- * This program is free software: you can redistribute it and/or modify *
- * it under the terms of the GNU General Public License as published by *
- * the Free Software Foundation, either version 3 of the License, or *
- * (at your option) any later version. *
- * *
- * You should have received a copy of the GNU General Public License *
- * along with this program. If not, see . *
- *************************************************************************/
-
-using System;
-using System.Runtime.CompilerServices;
-using System.Security.Cryptography;
-
-namespace Server;
-
-public static class BuiltInSecureRng
-{
- public static RandomNumberGenerator Generator { get; } = RandomNumberGenerator.Create();
-
- [MethodImpl(MethodImplOptions.AggressiveInlining)]
- public static void NextBytes(Span buffer) => Generator.GetBytes(buffer);
-}
diff --git a/Projects/Server/Server.csproj b/Projects/Server/Server.csproj
index 5e16cde20..d9da8166d 100644
--- a/Projects/Server/Server.csproj
+++ b/Projects/Server/Server.csproj
@@ -37,10 +37,10 @@
-
+
-
+
diff --git a/Projects/UOContent.Tests/UOContent.Tests.csproj b/Projects/UOContent.Tests/UOContent.Tests.csproj
index 77064faa1..dca5343eb 100644
--- a/Projects/UOContent.Tests/UOContent.Tests.csproj
+++ b/Projects/UOContent.Tests/UOContent.Tests.csproj
@@ -4,9 +4,9 @@
Debug;Release;Analyze
-
+
-
+
all
runtime; build; native; contentfiles; analyzers; buildtransitive
diff --git a/Projects/UOContent/Accounting/Security/AccountSecurity.cs b/Projects/UOContent/Accounting/Security/AccountSecurity.cs
index 22860d370..355137ab7 100644
--- a/Projects/UOContent/Accounting/Security/AccountSecurity.cs
+++ b/Projects/UOContent/Accounting/Security/AccountSecurity.cs
@@ -1,6 +1,6 @@
/*************************************************************************
* ModernUO *
- * Copyright 2019-2023 - ModernUO Development Team *
+ * Copyright 2019-2025 - ModernUO Development Team *
* Email: hi@modernuo.com *
* File: AccountSecurity.cs *
* *
@@ -15,56 +15,55 @@
using System;
-namespace Server.Accounting.Security
-{
- public enum PasswordProtectionAlgorithm
- {
- // Obsolete algorithms from RunUO. These are not secure!
- // They are included for password upgrades only.
- None,
- MD5,
- SHA1,
+namespace Server.Accounting.Security;
- // Supported algorithms
- SHA2, // ServUO compatibility
- PBKDF2,
- Argon2 // Recommended algorithm for real security.
+public enum PasswordProtectionAlgorithm
+{
+ // Obsolete algorithms from RunUO. These are not secure!
+ // They are included for password upgrades only.
+ None,
+ MD5,
+ SHA1,
+
+ // Supported algorithms
+ SHA2, // ServUO compatibility
+ PBKDF2,
+ Argon2 // Recommended algorithm for real security.
+}
+
+public static class AccountSecurity
+{
+ public static PasswordProtectionAlgorithm CurrentAlgorithm { get; set; }
+
+ public static IPasswordProtection CurrentPasswordProtection => GetPasswordProtection(CurrentAlgorithm);
+
+ public static void Configure()
+ {
+ CurrentAlgorithm =
+ ServerConfiguration.GetOrUpdateSetting(
+ "accountSecurity.encryptionAlgorithm",
+ PasswordProtectionAlgorithm.Argon2
+ );
+
+ if (CurrentAlgorithm < PasswordProtectionAlgorithm.SHA2)
+ {
+ throw new Exception($"Security: {CurrentAlgorithm} is obsolete and not secure. Do not use it.");
+ }
}
- public static class AccountSecurity
+ public static IPasswordProtection GetPasswordProtection(PasswordProtectionAlgorithm algorithm)
{
- public static PasswordProtectionAlgorithm CurrentAlgorithm { get; set; }
-
- public static IPasswordProtection CurrentPasswordProtection => GetPasswordProtection(CurrentAlgorithm);
-
- public static void Configure()
+ var passwordProtection = algorithm switch
{
- CurrentAlgorithm =
- ServerConfiguration.GetOrUpdateSetting(
- "accountSecurity.encryptionAlgorithm",
- PasswordProtectionAlgorithm.Argon2
- );
+ PasswordProtectionAlgorithm.MD5 => HashAlgorithmPasswordProtection.MD5Instance,
+ PasswordProtectionAlgorithm.SHA1 => HashAlgorithmPasswordProtection.SHA1Instance,
+ PasswordProtectionAlgorithm.SHA2 => HashAlgorithmPasswordProtection.SHA2Instance,
+ PasswordProtectionAlgorithm.PBKDF2 => PBKDF2PasswordProtection.Instance,
+ PasswordProtectionAlgorithm.Argon2 => Argon2PasswordProtection.Instance,
+ PasswordProtectionAlgorithm.None => throw new Exception("Do not use PasswordProtectionAlgorithm.None"),
+ _ => throw new Exception("No algorithm")
+ };
- if (CurrentAlgorithm < PasswordProtectionAlgorithm.SHA2)
- {
- throw new Exception($"Security: {CurrentAlgorithm} is obsolete and not secure. Do not use it.");
- }
- }
-
- public static IPasswordProtection GetPasswordProtection(PasswordProtectionAlgorithm algorithm)
- {
- var passwordProtection = algorithm switch
- {
- PasswordProtectionAlgorithm.MD5 => HashAlgorithmPasswordProtection.MD5Instance,
- PasswordProtectionAlgorithm.SHA1 => HashAlgorithmPasswordProtection.SHA1Instance,
- PasswordProtectionAlgorithm.SHA2 => HashAlgorithmPasswordProtection.SHA2Instance,
- PasswordProtectionAlgorithm.PBKDF2 => PBKDF2PasswordProtection.Instance,
- PasswordProtectionAlgorithm.Argon2 => Argon2PasswordProtection.Instance,
- PasswordProtectionAlgorithm.None => throw new Exception("Do not use PasswordProtectionAlgorithm.None"),
- _ => throw new Exception("No algorithm")
- };
-
- return passwordProtection;
- }
+ return passwordProtection;
}
}
diff --git a/Projects/UOContent/Accounting/Security/Argon2PasswordProtection.cs b/Projects/UOContent/Accounting/Security/Argon2PasswordProtection.cs
index f49d2a040..6d3214a48 100644
--- a/Projects/UOContent/Accounting/Security/Argon2PasswordProtection.cs
+++ b/Projects/UOContent/Accounting/Security/Argon2PasswordProtection.cs
@@ -1,6 +1,6 @@
/*************************************************************************
* ModernUO *
- * Copyright 2019-2023 - ModernUO Development Team *
+ * Copyright 2019-2025 - ModernUO Development Team *
* Email: hi@modernuo.com *
* File: Argon2PasswordProtection.cs *
* *
@@ -15,18 +15,17 @@
using System.Security.Cryptography;
-namespace Server.Accounting.Security
+namespace Server.Accounting.Security;
+
+public class Argon2PasswordProtection : IPasswordProtection
{
- public class Argon2PasswordProtection : IPasswordProtection
- {
- public static IPasswordProtection Instance = new Argon2PasswordProtection();
+ public static IPasswordProtection Instance = new Argon2PasswordProtection();
- private readonly Argon2PasswordHasher m_PasswordHasher = new(rng: BuiltInSecureRng.Generator);
+ private readonly Argon2PasswordHasher m_PasswordHasher = new(rng: RandomNumberGenerator.Create());
- public string EncryptPassword(string plainPassword) =>
- m_PasswordHasher.Hash(plainPassword);
+ public string EncryptPassword(string plainPassword) =>
+ m_PasswordHasher.Hash(plainPassword);
- public bool ValidatePassword(string encryptedPassword, string plainPassword) =>
- m_PasswordHasher.Verify(encryptedPassword, plainPassword);
- }
+ public bool ValidatePassword(string encryptedPassword, string plainPassword) =>
+ m_PasswordHasher.Verify(encryptedPassword, plainPassword);
}
diff --git a/Projects/UOContent/Accounting/Security/HashAlgorithmPasswordProtection.cs b/Projects/UOContent/Accounting/Security/HashAlgorithmPasswordProtection.cs
index 978adbbf8..f50888675 100644
--- a/Projects/UOContent/Accounting/Security/HashAlgorithmPasswordProtection.cs
+++ b/Projects/UOContent/Accounting/Security/HashAlgorithmPasswordProtection.cs
@@ -1,6 +1,6 @@
/*************************************************************************
* ModernUO *
- * Copyright 2019-2023 - ModernUO Development Team *
+ * Copyright 2019-2025 - ModernUO Development Team *
* Email: hi@modernuo.com *
* File: HashAlgorithmPasswordProtection.cs *
* *
@@ -17,24 +17,23 @@ using System;
using System.Security.Cryptography;
using Server.Text;
-namespace Server.Accounting.Security
+namespace Server.Accounting.Security;
+
+public class HashAlgorithmPasswordProtection : IPasswordProtection
{
- public class HashAlgorithmPasswordProtection : IPasswordProtection
+ public static IPasswordProtection MD5Instance = new HashAlgorithmPasswordProtection(MD5.Create());
+ public static IPasswordProtection SHA1Instance = new HashAlgorithmPasswordProtection(SHA1.Create());
+ public static IPasswordProtection SHA2Instance = new HashAlgorithmPasswordProtection(SHA512.Create());
+ private readonly HashAlgorithm _hashAlgorithm;
+
+ public HashAlgorithmPasswordProtection(HashAlgorithm hashAlgorithm) => _hashAlgorithm = hashAlgorithm;
+
+ public string EncryptPassword(string plainPassword)
{
- public static IPasswordProtection MD5Instance = new HashAlgorithmPasswordProtection(MD5.Create());
- public static IPasswordProtection SHA1Instance = new HashAlgorithmPasswordProtection(SHA1.Create());
- public static IPasswordProtection SHA2Instance = new HashAlgorithmPasswordProtection(SHA512.Create());
- private readonly HashAlgorithm _hashAlgorithm;
-
- public HashAlgorithmPasswordProtection(HashAlgorithm hashAlgorithm) => _hashAlgorithm = hashAlgorithm;
-
- public string EncryptPassword(string plainPassword)
- {
- byte[] bytes = plainPassword.AsSpan(0, Math.Min(256, plainPassword.Length)).GetBytesAscii();
- return _hashAlgorithm.ComputeHash(bytes).ToHexString();
- }
-
- public bool ValidatePassword(string encryptedPassword, string plainPassword) =>
- EncryptPassword(plainPassword) == encryptedPassword;
+ byte[] bytes = plainPassword.AsSpan(0, Math.Min(256, plainPassword.Length)).GetBytesAscii();
+ return _hashAlgorithm.ComputeHash(bytes).ToHexString();
}
+
+ public bool ValidatePassword(string encryptedPassword, string plainPassword) =>
+ EncryptPassword(plainPassword) == encryptedPassword;
}
diff --git a/Projects/UOContent/Accounting/Security/PBKDF2PasswordProtection.cs b/Projects/UOContent/Accounting/Security/PBKDF2PasswordProtection.cs
index e9fd8b05f..74f999a3f 100644
--- a/Projects/UOContent/Accounting/Security/PBKDF2PasswordProtection.cs
+++ b/Projects/UOContent/Accounting/Security/PBKDF2PasswordProtection.cs
@@ -1,6 +1,6 @@
/*************************************************************************
* ModernUO *
- * Copyright 2019-2023 - ModernUO Development Team *
+ * Copyright 2019-2025 - ModernUO Development Team *
* Email: hi@modernuo.com *
* File: PBKDF2PasswordProtection.cs *
* *
@@ -18,42 +18,43 @@ using System.Buffers.Binary;
using System.Security.Cryptography;
using Server.Text;
-namespace Server.Accounting.Security
+namespace Server.Accounting.Security;
+
+public class PBKDF2PasswordProtection : IPasswordProtection
{
- public class PBKDF2PasswordProtection : IPasswordProtection
+ private const ushort m_MinIterations = 1024;
+ private const ushort m_MaxIterations = 1536;
+ private const int m_SaltSize = 8;
+ private const int m_HashSize = 32;
+ private const int m_OutputSize = 2 + m_SaltSize + m_HashSize;
+ public static readonly IPasswordProtection Instance = new PBKDF2PasswordProtection();
+
+ public string EncryptPassword(string plainPassword)
{
- private const ushort m_MinIterations = 1024;
- private const ushort m_MaxIterations = 1536;
- private const int m_SaltSize = 8;
- private const int m_HashSize = 32;
- private const int m_OutputSize = 2 + m_SaltSize + m_HashSize;
- public static readonly IPasswordProtection Instance = new PBKDF2PasswordProtection();
+ Span output = stackalloc byte[m_OutputSize];
+ var iterations = Utility.RandomMinMax(m_MinIterations, m_MaxIterations);
+ BinaryPrimitives.WriteUInt16LittleEndian(output[..2], (ushort)iterations);
- public string EncryptPassword(string plainPassword)
- {
- Span output = stackalloc byte[m_OutputSize];
- var iterations = Utility.RandomMinMax(m_MinIterations, m_MaxIterations);
- BinaryPrimitives.WriteUInt16LittleEndian(output[..2], (ushort)iterations);
+ var salt = output.Slice(2, m_SaltSize);
+ RandomNumberGenerator.Fill(salt);
- var rfc2898 = new Rfc2898DeriveBytes(plainPassword, m_SaltSize, iterations, HashAlgorithmName.SHA256);
- rfc2898.Salt.CopyTo(output.Slice(2, m_SaltSize));
- rfc2898.GetBytes(m_HashSize).CopyTo(output[(m_SaltSize + 2)..]);
+ var hash = output.Slice(2 + m_SaltSize, m_HashSize);
+ Rfc2898DeriveBytes.Pbkdf2(plainPassword, salt, hash, iterations, HashAlgorithmName.SHA256);
- return output.ToHexString();
- }
+ return output.ToHexString();
+ }
- public bool ValidatePassword(string encryptedPassword, string plainPassword)
- {
- Span encryptedBytes = stackalloc byte[m_OutputSize];
- encryptedPassword.GetBytes(encryptedBytes);
+ public bool ValidatePassword(string encryptedPassword, string plainPassword)
+ {
+ Span encryptedBytes = stackalloc byte[m_OutputSize];
+ encryptedPassword.GetBytes(encryptedBytes);
- var iterations = BinaryPrimitives.ReadUInt16LittleEndian(encryptedBytes[..2]);
- var salt = encryptedBytes.Slice(2, m_SaltSize);
+ var iterations = BinaryPrimitives.ReadUInt16LittleEndian(encryptedBytes[..2]);
+ var salt = encryptedBytes.Slice(2, m_SaltSize);
- ReadOnlySpan hash =
- new Rfc2898DeriveBytes(plainPassword, salt.ToArray(), iterations, HashAlgorithmName.SHA256).GetBytes(m_HashSize);
+ Span hash = stackalloc byte[m_HashSize];
+ Rfc2898DeriveBytes.Pbkdf2(plainPassword, salt, hash, iterations, HashAlgorithmName.SHA256);
- return hash.SequenceEqual(encryptedBytes[(m_SaltSize + 2)..]);
- }
+ return hash.SequenceEqual(encryptedBytes[(m_SaltSize + 2)..]);
}
}
diff --git a/Projects/UOContent/UOContent.csproj b/Projects/UOContent/UOContent.csproj
index bea34e002..4ca0561f8 100644
--- a/Projects/UOContent/UOContent.csproj
+++ b/Projects/UOContent/UOContent.csproj
@@ -39,16 +39,16 @@
false
-
-
+
+
-
-
+
+
diff --git a/azure-pipelines.yml b/azure-pipelines.yml
index 798907fc1..0a0fb6a25 100644
--- a/azure-pipelines.yml
+++ b/azure-pipelines.yml
@@ -14,7 +14,7 @@ jobs:
steps:
- task: UseDotNet@2
- displayName: 'Install .NET 9'
+ displayName: 'Install .NET'
inputs:
useGlobalJson: true
- task: NuGetAuthenticate@1
diff --git a/global.json b/global.json
index 733b653c1..6a288505a 100644
--- a/global.json
+++ b/global.json
@@ -1,6 +1,6 @@
{
"sdk": {
- "version": "9.0.100",
+ "version": "10.0.100",
"rollForward": "latestMajor",
"allowPrerelease": false
}
diff --git a/version.json b/version.json
index d36382b04..de4574c38 100644
--- a/version.json
+++ b/version.json
@@ -1,4 +1,4 @@
{
"$schema": "https://raw.githubusercontent.com/dotnet/Nerdbank.GitVersioning/master/src/NerdBank.GitVersioning/version.schema.json",
- "version": "0.15.1"
+ "version": "0.15.2"
}
From c308a3baadb7c20ffe254917141a33cf2e759ef5 Mon Sep 17 00:00:00 2001
From: Bohica <53943479+Bohicatv@users.noreply.github.com>
Date: Wed, 12 Nov 2025 09:49:35 -0800
Subject: [PATCH 27/47] fix: Fixes timer leak in BaseCamp (#2259)
---
Projects/UOContent/Multis/Camps/BaseCamp.cs | 29 +++++++++++++++++----
1 file changed, 24 insertions(+), 5 deletions(-)
diff --git a/Projects/UOContent/Multis/Camps/BaseCamp.cs b/Projects/UOContent/Multis/Camps/BaseCamp.cs
index 3b9cece2a..e3d10dd0b 100644
--- a/Projects/UOContent/Multis/Camps/BaseCamp.cs
+++ b/Projects/UOContent/Multis/Camps/BaseCamp.cs
@@ -23,6 +23,7 @@ public abstract partial class BaseCamp : BaseMulti
private TimeSpan _decayDelay;
private Timer _decayTimer;
+ private Timer _initTimer;
public BaseCamp(int multiID) : base(multiID)
{
@@ -31,7 +32,7 @@ public abstract partial class BaseCamp : BaseMulti
_decayDelay = TimeSpan.FromMinutes(30.0);
RefreshDecay(true);
- Timer.StartTimer(CheckAddComponents);
+ _initTimer = Timer.DelayCall(TimeSpan.Zero, CheckAddComponents);
}
public virtual int EventRange => 10;
@@ -50,6 +51,8 @@ public abstract partial class BaseCamp : BaseMulti
public void CheckAddComponents()
{
+ _initTimer = null;
+
if (Deleted)
{
return;
@@ -138,16 +141,16 @@ public abstract partial class BaseCamp : BaseMulti
for (var i = 0; i < _items.Count; ++i)
{
- _items[i].Delete();
+ _items[i]?.Delete();
}
for (var i = 0; i < _mobiles.Count; ++i)
{
var mob = _mobiles[i];
- if (mob.CantWalk || (mob as BaseCreature)?.IsPrisoner == false)
+ if (mob != null && (mob.CantWalk || (mob as BaseCreature)?.IsPrisoner == false))
{
- _mobiles[i].Delete();
+ mob.Delete();
}
}
@@ -156,6 +159,9 @@ public abstract partial class BaseCamp : BaseMulti
_decayTimer?.Stop();
_decayTimer = null;
+
+ _initTimer?.Stop();
+ _initTimer = null;
}
private void Deserialize(IGenericReader reader, int version)
@@ -168,7 +174,20 @@ public abstract partial class BaseCamp : BaseMulti
[AfterDeserialization]
private void AfterDeserialization()
{
- RefreshDecay(false);
+ var remaining = _decayTime - Core.Now;
+
+ if (remaining > TimeSpan.Zero)
+ {
+ _decayDelay = remaining;
+ RefreshDecay(false);
+ }
+ else
+ {
+ Timer.DelayCall(TimeSpan.Zero, Delete);
+ return;
+ }
+
+ _initTimer = Timer.DelayCall(TimeSpan.Zero, CheckAddComponents);
}
}
From 5d920a25b1adaac85f7f3c40249d846815f6fbde Mon Sep 17 00:00:00 2001
From: Kamron Batman <3953314+kamronbatman@users.noreply.github.com>
Date: Wed, 12 Nov 2025 23:03:14 -0800
Subject: [PATCH 28/47] chore: Updates .NET reference material to 10 (#2261)
---
.github/workflows/build-test.yml | 13 ++--------
README.md | 44 ++++++++++++++++----------------
docs/installation.md | 6 ++---
3 files changed, 27 insertions(+), 36 deletions(-)
diff --git a/.github/workflows/build-test.yml b/.github/workflows/build-test.yml
index 0a906a743..1c9ea98a9 100644
--- a/.github/workflows/build-test.yml
+++ b/.github/workflows/build-test.yml
@@ -54,20 +54,11 @@ jobs:
- container: ubuntu:jammy
name: Ubuntu 22
packageManager: apt
- - container: ubuntu:focal
- name: Ubuntu 20
- packageManager: apt
- container: debian:bookworm
name: Debian 12
packageManager: apt
- - container: debian:bullseye
- name: Debian 11
- packageManager: apt
- - container: fedora:39
- name: Fedora 39
- packageManager: dnf
- - container: fedora:40
- name: Fedora 40
+ - container: fedora:42
+ name: Fedora 42
packageManager: dnf
- container: quay.io/centos/centos:stream9
name: CentOS 9 Stream
diff --git a/README.md b/README.md
index 4108f12c9..385219433 100644
--- a/README.md
+++ b/README.md
@@ -15,38 +15,38 @@ ModernUO [](https://www.microsoft.com/en-US/evalcenter/evaluate-windows-server-2022)
-
+[](https://www.microsoft.com/en-US/evalcenter/evaluate-windows-server-2022)
+
[](https://www.debian.org/distrib/)
[](https://ubuntu.com/download/server)
-[](https://alpinelinux.org/downloads/)
-[](https://getfedora.org/en/server/download/)
-[](https://access.redhat.com/downloads)
-[](https://www.centos.org/download/)
-[](https://get.opensuse.org/)
-[](https://www.suse.com/download/sles/)
-[](https://linuxmint.com/download.php)
+[](https://alpinelinux.org/downloads/)
+[](https://getfedora.org/en/server/download/)
+[](https://access.redhat.com/downloads)
+[](https://www.centos.org/download/)
+[](https://get.opensuse.org/)
+[](https://www.suse.com/download/sles/)
+[](https://linuxmint.com/download.php)
[](https://archlinux.org/download/)
#### Required Frameworks
##### All Operating Systems
-[](https://dotnet.microsoft.com/download/dotnet/9.0)
+[](https://dotnet.microsoft.com/download/dotnet/10.0)
##### Windows
-[](https://learn.microsoft.com/en-us/cpp/windows/latest-supported-vc-redist?view=msvc-170#visual-studio-2015-2017-2019-and-2022)
+[](https://aka.ms/vc14/vc_redist.x64.exe)
#### Development
[](https://git-scm.com/downloads)
-[](https://dotnet.microsoft.com/download/dotnet/9.0)
+[](https://dotnet.microsoft.com/download/dotnet/10.0)
#### Supported IDEs
-
+
-
+
## Getting Started
@@ -57,13 +57,13 @@ ModernUO [] [os] [arch (default: x64)]`
- - `os` - [Supported operating systems](https://github.com/dotnet/core/blob/main/release-notes/9.0/supported-os.md)
- - `win` - Windows 10/11/2019/2022/2025
- - `osx` - MacOS 13/14/15 (Sequoia, Sonoma, Big Sur)
- - `linux` - Linux
+ - `os` - [Supported operating systems](https://github.com/dotnet/core/blob/main/release-notes/10.0/supported-os.md)
+ - `win` - [Windows](https://learn.microsoft.com/en-us/dotnet/core/install/windows)
+ - `osx` - [MacOS](https://learn.microsoft.com/en-us/dotnet/core/install/macos)
+ - `linux` - [Linux](https://learn.microsoft.com/en-us/dotnet/core/install/linux)
- `arch`
- - `x64` - Intel 64-bit
- - `arm64` - ARM 64-bit (Windows Arm64 not supported)
+ - `x64` - Intel/AMD 64-bit
+ - `arm64` - ARM 64-bit (Windows not supported)
## Linux Prerequisites
### Fedora, CentOS, RHEL, etc
@@ -86,8 +86,8 @@ brew install icu4c libdeflate zstd argon2
```
## Running the Server
-- Follow the [publish](https://github.com/modernuo/ModernUO#publishing-builds) instructions
-- Run `ModernUO.exe` or `dotnet ModernUO.dll` from the `Distribution` directory on the
+- Follow the [publish](https://github.com/modernuo/ModernUO#buildingpublishing) instructions
+- Run `ModernUO.exe` or `dotnet ModernUO.dll` from the `Distribution` directory
## Troubleshooting / FAQ
- See [FAQ](./FAQ.md)
diff --git a/docs/installation.md b/docs/installation.md
index ff6d90c8b..2fefc1d58 100644
--- a/docs/installation.md
+++ b/docs/installation.md
@@ -6,7 +6,7 @@ title: Installation
=== "Windows"
### Prerequisites
- 1. Download and install the latest [.NET 8 SDK](https://dotnet.microsoft.com/download/dotnet/8.0)
+ 1. Download and install the latest [.NET 10 SDK](https://dotnet.microsoft.com/download/dotnet/10.0)
1. Download and install from [here](https://git-scm.com/download/win)
!!! Tip
@@ -22,7 +22,7 @@ title: Installation
=== "OSX"
### Prerequisites
- 1. Download and install the latest [.NET 8 SDK](https://dotnet.microsoft.com/download/dotnet/8.0).
+ 1. Download and install the latest [.NET 10 SDK](https://dotnet.microsoft.com/download/dotnet/10.0).
1. Using _terminal_, install [homebrew](https://brew.sh) and git:
```bash
/bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/master/install.sh)"
@@ -37,7 +37,7 @@ title: Installation
=== "Linux"
### Prerequisites
- 1. Download and install the latest [.NET 8 SDK](https://docs.microsoft.com/en-us/dotnet/core/install/linux).
+ 1. Download and install the latest [.NET 10 SDK](https://docs.microsoft.com/en-us/dotnet/core/install/linux).
1. Using _bash_, install git:
```bash
sudo apt update && sudo apt install git
From 68caaab92caa0bc0bb600ca7ba3189a5e8ad1de1 Mon Sep 17 00:00:00 2001
From: Kamron Batman <3953314+kamronbatman@users.noreply.github.com>
Date: Wed, 12 Nov 2025 23:34:13 -0800
Subject: [PATCH 29/47] fix: Makes SpanWriter/SpanReader exceptions clearer
(#2262)
---
Projects/Server/Buffers/SpanReader.cs | 75 ++++++++++----------------
Projects/Server/Buffers/SpanWriter.cs | 76 ++++++++++-----------------
2 files changed, 55 insertions(+), 96 deletions(-)
diff --git a/Projects/Server/Buffers/SpanReader.cs b/Projects/Server/Buffers/SpanReader.cs
index 31c5fc51d..290ba5a94 100644
--- a/Projects/Server/Buffers/SpanReader.cs
+++ b/Projects/Server/Buffers/SpanReader.cs
@@ -1,6 +1,6 @@
/*************************************************************************
* ModernUO *
- * Copyright 2019-2023 - ModernUO Development Team *
+ * Copyright 2019-2025 - ModernUO Development Team *
* Email: hi@modernuo.com *
* File: SpanReader.cs *
* *
@@ -14,7 +14,6 @@
*************************************************************************/
using System.Buffers.Binary;
-using System.Diagnostics;
using System.IO;
using System.Runtime.CompilerServices;
using System.Text;
@@ -45,7 +44,7 @@ public ref struct SpanReader
{
if (Position >= Length)
{
- throw new OutOfMemoryException();
+ throw new EndOfStreamException("Cannot read past the end of the buffer.");
}
return _buffer[Position++];
@@ -62,7 +61,7 @@ public ref struct SpanReader
{
if (!BinaryPrimitives.TryReadInt16BigEndian(_buffer[Position..], out var value))
{
- throw new OutOfMemoryException();
+ throw new EndOfStreamException("Cannot read past the end of the buffer.");
}
Position += 2;
@@ -74,7 +73,7 @@ public ref struct SpanReader
{
if (!BinaryPrimitives.TryReadInt16LittleEndian(_buffer[Position..], out var value))
{
- throw new OutOfMemoryException();
+ throw new EndOfStreamException("Cannot read past the end of the buffer.");
}
Position += 2;
@@ -86,7 +85,7 @@ public ref struct SpanReader
{
if (!BinaryPrimitives.TryReadUInt16BigEndian(_buffer[Position..], out var value))
{
- throw new OutOfMemoryException();
+ throw new EndOfStreamException("Cannot read past the end of the buffer.");
}
Position += 2;
@@ -98,7 +97,7 @@ public ref struct SpanReader
{
if (!BinaryPrimitives.TryReadUInt16LittleEndian(_buffer[Position..], out var value))
{
- throw new OutOfMemoryException();
+ throw new EndOfStreamException("Cannot read past the end of the buffer.");
}
Position += 2;
@@ -110,7 +109,7 @@ public ref struct SpanReader
{
if (!BinaryPrimitives.TryReadInt32BigEndian(_buffer[Position..], out var value))
{
- throw new OutOfMemoryException();
+ throw new EndOfStreamException("Cannot read past the end of the buffer.");
}
Position += 4;
@@ -122,7 +121,7 @@ public ref struct SpanReader
{
if (!BinaryPrimitives.TryReadUInt32BigEndian(_buffer[Position..], out var value))
{
- throw new OutOfMemoryException();
+ throw new EndOfStreamException("Cannot read past the end of the buffer.");
}
Position += 4;
@@ -134,7 +133,7 @@ public ref struct SpanReader
{
if (!BinaryPrimitives.TryReadUInt32LittleEndian(_buffer[Position..], out var value))
{
- throw new OutOfMemoryException();
+ throw new EndOfStreamException("Cannot read past the end of the buffer.");
}
Position += 4;
@@ -146,7 +145,7 @@ public ref struct SpanReader
{
if (!BinaryPrimitives.TryReadInt64BigEndian(_buffer[Position..], out var value))
{
- throw new OutOfMemoryException();
+ throw new EndOfStreamException("Cannot read past the end of the buffer.");
}
Position += 8;
@@ -158,7 +157,7 @@ public ref struct SpanReader
{
if (!BinaryPrimitives.TryReadUInt64BigEndian(_buffer[Position..], out var value))
{
- throw new OutOfMemoryException();
+ throw new EndOfStreamException("Cannot read past the end of the buffer.");
}
Position += 8;
@@ -173,9 +172,9 @@ public ref struct SpanReader
return "";
}
- int byteLength = encoding.GetByteLengthForEncoding();
+ var byteLength = encoding.GetByteLengthForEncoding();
- bool isFixedLength = fixedLength > -1;
+ var isFixedLength = fixedLength > -1;
var remaining = Remaining;
int size;
@@ -184,7 +183,7 @@ public ref struct SpanReader
size = fixedLength * byteLength;
if (size > Remaining)
{
- throw new OutOfMemoryException();
+ throw new EndOfStreamException("Cannot read past the end of the buffer.");
}
}
else
@@ -255,42 +254,24 @@ public ref struct SpanReader
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public int Seek(int offset, SeekOrigin origin)
{
- Debug.Assert(
- origin != SeekOrigin.End || offset <= 0,
- "Attempting to seek to a position beyond capacity using SeekOrigin.End"
- );
-
- Debug.Assert(
- origin != SeekOrigin.End || offset >= -_buffer.Length,
- "Attempting to seek to a negative position using SeekOrigin.End"
- );
-
- Debug.Assert(
- origin != SeekOrigin.Begin || offset >= 0,
- "Attempting to seek to a negative position using SeekOrigin.Begin"
- );
-
- Debug.Assert(
- origin != SeekOrigin.Begin || offset <= _buffer.Length,
- "Attempting to seek to a position beyond the capacity using SeekOrigin.Begin"
- );
-
- Debug.Assert(
- origin != SeekOrigin.Current || Position + offset >= 0,
- "Attempting to seek to a negative position using SeekOrigin.Current"
- );
-
- Debug.Assert(
- origin != SeekOrigin.Current || Position + offset <= _buffer.Length,
- "Attempting to seek to a position beyond the capacity using SeekOrigin.Current"
- );
-
- return Position = Math.Max(0, origin switch
+ var newPosition = origin switch
{
SeekOrigin.Current => Position + offset,
SeekOrigin.End => _buffer.Length + offset,
_ => offset // Begin
- });
+ };
+
+ if (newPosition < 0)
+ {
+ throw new ArgumentOutOfRangeException(nameof(offset), "Seek operation would result in a negative position.");
+ }
+
+ if (newPosition > _buffer.Length)
+ {
+ throw new ArgumentOutOfRangeException(nameof(offset), $"Cannot seek to position {newPosition} beyond buffer length {_buffer.Length}.");
+ }
+
+ return Position = newPosition;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
diff --git a/Projects/Server/Buffers/SpanWriter.cs b/Projects/Server/Buffers/SpanWriter.cs
index 4ce423c5b..ccdaddd38 100644
--- a/Projects/Server/Buffers/SpanWriter.cs
+++ b/Projects/Server/Buffers/SpanWriter.cs
@@ -1,6 +1,6 @@
/*************************************************************************
* ModernUO *
- * Copyright 2019-2023 - ModernUO Development Team *
+ * Copyright 2019-2025 - ModernUO Development Team *
* Email: hi@modernuo.com *
* File: SpanWriter.cs *
* *
@@ -14,7 +14,6 @@
*************************************************************************/
using System.Buffers.Binary;
-using System.Diagnostics;
using System.IO;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
@@ -56,14 +55,14 @@ public ref struct SpanWriter
public Span RawBuffer => _buffer;
/**
- * Converts the writer to a Span using a SpanOwner.
- * If the buffer was stackalloc, it will be copied to a rented buffer.
- * Otherwise the existing rented buffer is used.
- *
- * Note:
- * Do not use the SpanWriter after calling this method.
- * This method will effectively dispose of the SpanWriter and is therefore considered terminal.
- */
+ * Converts the writer to a Span using a SpanOwner.
+ * If the buffer was stackalloc, it will be copied to a rented buffer.
+ * Otherwise the existing rented buffer is used.
+ *
+ * Note:
+ * Do not use the SpanWriter after calling this method.
+ * This method will effectively dispose of the SpanWriter and is therefore considered terminal.
+ */
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public SpanOwner ToSpan()
{
@@ -117,11 +116,11 @@ public ref struct SpanWriter
private void Grow(int additionalCapacity)
{
var newSize = Math.Max(BytesWritten + additionalCapacity, _buffer.Length * 2);
- byte[] poolArray = STArrayPool.Shared.Rent(newSize);
+ var poolArray = STArrayPool.Shared.Rent(newSize);
_buffer[..BytesWritten].CopyTo(poolArray);
- byte[] toReturn = _arrayToReturnToPool;
+ var toReturn = _arrayToReturnToPool;
_buffer = _arrayToReturnToPool = poolArray;
if (toReturn != null)
{
@@ -136,7 +135,7 @@ public ref struct SpanWriter
{
if (!_resize)
{
- throw new OutOfMemoryException();
+ throw new InvalidOperationException("Buffer is full and resizing is disabled.");
}
Grow(count);
@@ -151,7 +150,7 @@ public ref struct SpanWriter
{
if (!_resize)
{
- throw new OutOfMemoryException();
+ throw new InvalidOperationException("Buffer is full and resizing is disabled.");
}
Grow(capacity - BytesWritten);
@@ -400,46 +399,25 @@ public ref struct SpanWriter
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public int Seek(int offset, SeekOrigin origin)
{
- Debug.Assert(
- origin != SeekOrigin.End || _resize || offset <= 0,
- "Attempting to seek to a position beyond capacity using SeekOrigin.End without resize"
- );
-
- Debug.Assert(
- origin != SeekOrigin.End || offset >= -_buffer.Length,
-
- "Attempting to seek to a negative position using SeekOrigin.End"
- );
-
- Debug.Assert(
- origin != SeekOrigin.Begin || offset >= 0,
- "Attempting to seek to a negative position using SeekOrigin.Begin"
- );
-
- Debug.Assert(
- origin != SeekOrigin.Begin || _resize || offset <= _buffer.Length,
- "Attempting to seek to a position beyond the capacity using SeekOrigin.Begin without resize"
- );
-
- Debug.Assert(
- origin != SeekOrigin.Current || _position + offset >= 0,
- "Attempting to seek to a negative position using SeekOrigin.Current"
- );
-
- Debug.Assert(
- origin != SeekOrigin.Current || _resize || _position + offset <= _buffer.Length,
- "Attempting to seek to a position beyond the capacity using SeekOrigin.Current without resize"
- );
-
- var newPosition = Math.Max(0, origin switch
+ var newPosition = origin switch
{
SeekOrigin.Current => _position + offset,
SeekOrigin.End => BytesWritten + offset,
_ => offset // Begin
- });
+ };
+
+ if (newPosition < 0)
+ {
+ throw new ArgumentOutOfRangeException(nameof(offset), "Seek operation would result in a negative position.");
+ }
if (newPosition > _buffer.Length)
{
+ if (!_resize)
+ {
+ throw new InvalidOperationException($"Cannot seek to position {newPosition} beyond buffer capacity {_buffer.Length} when resizing is disabled.");
+ }
+
Grow(newPosition - _buffer.Length + 1);
}
@@ -449,7 +427,7 @@ public ref struct SpanWriter
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void Dispose()
{
- byte[] toReturn = _arrayToReturnToPool;
+ var toReturn = _arrayToReturnToPool;
this = default; // for safety, to avoid using pooled array if this instance is erroneously appended to again
if (toReturn != null)
{
@@ -478,7 +456,7 @@ public ref struct SpanWriter
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void Dispose()
{
- byte[] toReturn = _arrayToReturnToPool;
+ var toReturn = _arrayToReturnToPool;
this = default;
if (_length > 0)
{
From 6f5b7f7a6b3e4265232663c7609acbe6301bbb42 Mon Sep 17 00:00:00 2001
From: Kamron Batman <3953314+kamronbatman@users.noreply.github.com>
Date: Thu, 13 Nov 2025 00:00:19 -0800
Subject: [PATCH 30/47] fix: Fixes SpanWriter/SpanReader tests (#2263)
---
.../Tests/Buffers/SpanReaderTests.cs | 597 +++++++++++++++++
.../Tests/Buffers/SpanWriterTests.cs | 618 ++++++++++++++++--
Projects/Server/Buffers/SpanReader.cs | 2 +-
3 files changed, 1171 insertions(+), 46 deletions(-)
create mode 100644 Projects/Server.Tests/Tests/Buffers/SpanReaderTests.cs
diff --git a/Projects/Server.Tests/Tests/Buffers/SpanReaderTests.cs b/Projects/Server.Tests/Tests/Buffers/SpanReaderTests.cs
new file mode 100644
index 000000000..9a68576c8
--- /dev/null
+++ b/Projects/Server.Tests/Tests/Buffers/SpanReaderTests.cs
@@ -0,0 +1,597 @@
+using System;
+using System.Buffers;
+using System.IO;
+using Xunit;
+
+namespace Server.Tests;
+
+public class SpanReaderTests
+{
+ [Fact]
+ public void TestReadByte()
+ {
+ ReadOnlySpan buffer = [0x12, 0x34];
+ var reader = new SpanReader(buffer);
+
+ Assert.Equal(0x12, reader.ReadByte());
+ Assert.Equal(0x34, reader.ReadByte());
+ Assert.Equal(2, reader.Position);
+ }
+
+ [Fact]
+ public void TestReadByteAtEnd()
+ {
+ Assert.Throws(
+ () =>
+ {
+ ReadOnlySpan buffer = [0x12];
+ var reader = new SpanReader(buffer);
+ reader.ReadByte();
+ reader.ReadByte();
+ }
+ );
+ }
+
+ [Fact]
+ public void TestReadBoolean()
+ {
+ ReadOnlySpan buffer = [0, 1, 2, 255];
+ var reader = new SpanReader(buffer);
+
+ Assert.False(reader.ReadBoolean());
+ Assert.True(reader.ReadBoolean());
+ Assert.True(reader.ReadBoolean());
+ Assert.True(reader.ReadBoolean());
+ Assert.Equal(4, reader.Position);
+ }
+
+ [Fact]
+ public void TestReadSByte()
+ {
+ ReadOnlySpan buffer = [0xFF, 0x7F];
+ var reader = new SpanReader(buffer);
+
+ Assert.Equal(-1, reader.ReadSByte());
+ Assert.Equal(127, reader.ReadSByte());
+ Assert.Equal(2, reader.Position);
+ }
+
+ [Fact]
+ public void TestReadInt16BigEndian()
+ {
+ ReadOnlySpan buffer = [0x12, 0x34];
+ var reader = new SpanReader(buffer);
+
+ Assert.Equal(0x1234, reader.ReadInt16());
+ Assert.Equal(2, reader.Position);
+ }
+
+ [Fact]
+ public void TestReadInt16LittleEndian()
+ {
+ ReadOnlySpan buffer = [0x34, 0x12];
+ var reader = new SpanReader(buffer);
+
+ Assert.Equal(0x1234, reader.ReadInt16LE());
+ Assert.Equal(2, reader.Position);
+ }
+
+ [Fact]
+ public void TestReadInt16AtEnd()
+ {
+ Assert.Throws(
+ () =>
+ {
+ ReadOnlySpan buffer = [0x12];
+ var reader = new SpanReader(buffer);
+ reader.ReadInt16();
+ }
+ );
+ }
+
+ [Fact]
+ public void TestReadUInt16BigEndian()
+ {
+ ReadOnlySpan buffer = [0x12, 0x34];
+ var reader = new SpanReader(buffer);
+
+ Assert.Equal((ushort)0x1234, reader.ReadUInt16());
+ Assert.Equal(2, reader.Position);
+ }
+
+ [Fact]
+ public void TestReadUInt16LittleEndian()
+ {
+ ReadOnlySpan buffer = [0x34, 0x12];
+ var reader = new SpanReader(buffer);
+
+ Assert.Equal((ushort)0x1234, reader.ReadUInt16LE());
+ Assert.Equal(2, reader.Position);
+ }
+
+ [Fact]
+ public void TestReadInt32BigEndian()
+ {
+ ReadOnlySpan buffer = [0x12, 0x34, 0x56, 0x78];
+ var reader = new SpanReader(buffer);
+
+ Assert.Equal(0x12345678, reader.ReadInt32());
+ Assert.Equal(4, reader.Position);
+ }
+
+ [Fact]
+ public void TestReadInt32AtEnd()
+ {
+ Assert.Throws