Highly Performant & Scalable Ultima Online Server Emulator https://www.modernuo.com
Find a file
Kamron Batman d16166591c
fix(network): stop sending once a disconnect is handed to the socket (#2637)
## Problem

When a client's send buffer fills, the `send buffer exhausted` warning repeats for every packet, every tick, until the NetState is finally disposed. Before the io_uring transport an overflow produced one message plus the disconnect line.

## Root cause

Since #2315, `NetState.Disconnect()` only queues. The NetState keeps running, the Mobile stays attached, and in `Slice()` the queued disconnect becomes `RingSocket.Disconnect()`, which sees buffered or in-flight sends and merely sets `DisconnectPending` while the transport drains. Nothing stopped game logic from writing into that buffer afterwards, so:

- every broadcast to that player still reached `Send()`, hit the full buffer, and re-reported exhaustion (made visible by #2551);
- refills kept `ReadableBytes` above zero, so the graceful drain could never finish, and `DataSent` completions kept pushing the alive check out. A slow-but-acking client could keep a "disconnected" session attached indefinitely.

Two more sources of the same warning surfaced during the analysis: `Dispose()` sets `_running = false` before nulling `Mobile.NetState`, and the setter's bank-close / target-cancel packets then reported `0 writable`; and when the socket takes the immediate-close branch (nothing in flight) `Connected` drops without `DisconnectPending`, leaving a one-tick window that also re-reported.

## Changes

- `CannotSendPackets()` refuses once the socket is `DisconnectPending` or no longer `Connected`. Sends between `Disconnect()` and the `Slice()` handoff are still delivered (kicks with a message, the play-server ack).
- `SendBufferExhausted()` reports once per disconnect and keeps the first reason.
- `Send()` is silent while closing instead of reporting exhaustion for a socket that is going away.
- `_nextAliveCheck` is seeded from a real tick (the zero default suppressed the alive sweep on hosts whose counter starts negative).
- `CancelAllTrades()` had its null guard inverted since 547c2ea0f (#2603) and never cancelled anything.

## Does the gate cut off the graceful flush?

No. The gate only refuses writes made after `Slice()` has handed the disconnect to the socket. Everything committed before that point is drained by the transport, which then sends FIN. This matches the pre-io_uring lifecycle: `Disconnect()` cleared `_running` at once, and the next `Slice()` made its final `Flush()` and then closed the socket in `Dispose()`. In both worlds the send window after `Disconnect()` ends at the next network slice; the old one made a single flush attempt, the new one drains everything buffered.

Checked flows:

- **Login gateway.** `PlayServer` sends the 0x8C ack, and the parser queues `Disconnect()` in the same `HandleReceive` call. Both happen before the handoff, so the ack is delivered and FIN follows. ClassicUO's `HandleRelayServerPacket` disconnects and opens a fresh connection before sending the seed and second login, so the game login is a new NetState. The `LoginServer_ServerSelectAck` "CUO/Orion do not reconnect" fallback (#489, 2021) resets the parser state and returns without parsing or replying, and no further receive is posted once the disconnect is pending, so it sends nothing either way. Orion likewise opens a separate game socket before closing the login one.
- **Login rejections, character create/select/delete errors, duplicate-packet guards.** Each sends its rejection first and calls `Disconnect()` in the same handler.
- **Kicks and bans** (`[kick`, `[ban`, AdminGump, ClientGump, ClientVerification, AssistantHandler, lockdown). The message is sent first; delayed variants fire `Disconnect()` from a timer with nothing sent afterwards.
- **Main loop order.** Timers run before `NetState.Slice()`, packet handlers run inside it before the flush, and `LoopContext` tasks run after it; in every case a send that precedes `Disconnect()` reaches the buffer before the next handoff.

`Send_BeforeDisconnect_IsDeliveredThenPeerSeesEof` and `Send_LargeBeforeDisconnect_IsFullyDrainedThenPeerSeesEof` read the bytes back from the peer socket after the handoff (the latter 192 KB across several send completions, past the loopback kernel buffers) and then wait for the FIN, proving delivery and clean close with the gate in place.

## Tests

Seven new tests in `NetStateDisconnectTests` over real loopback sockets: force-close after the drain deadline, delivery then EOF for small and multi-completion sends before `Disconnect()`, send dropped after the handoff, send dropped after an immediate close, exhaustion reported once with the first reason kept, and trades cancelled on disconnect. `MockAccount` promoted to a shared test helper. Server.Tests 876 passed, UOContent.Tests 1048 passed against the published 1.0.11.

## Drain deadline and IORingGroup 1.0.11

A socket handed a disconnect drains what is buffered and closes once the peer has acknowledged it. Send completions keep `NextActivityCheck` moving, so a slow but acking peer could hold a closing socket open indefinitely. A deadline (`DrainTimeoutMs`, 10 s) is now armed at the handoff, or on first sight of a transport-initiated drain in `CheckAlive`, and force-closes when it passes, independent of the inactivity check.

That force-close is only safe with IORingGroup 1.0.11 (modernuo/IORingGroup#12), which this PR bumps to. Before it, `DisconnectImmediate` released pooled buffers while recv/send operations could still be in flight, a failed send stranded the socket forever, and a recv completion could be delivered after its buffer was released. 1.0.11 retires every outstanding operation before release, aborts on a failed send, and holds buffers until the pass after the `Disconnected` event.
2026-09-11 23:18:37 -07:00
.config fix: Fixes dirty-tracking gaps in generated content: setters, sub-object owners, BaseVendor (#2609) 2026-09-06 09:43:44 -07:00
.github fix: Require only runtime packages on Linux, and check ICU and tzdata the way the runtime does (#2561) 2026-08-07 15:03:08 -07:00
branding feat(build-tool): add application icon and refresh MUO.ico (#2487) 2026-06-14 11:59:54 -07:00
dev-docs feat(skills): SkillEvents.SkillUsed for cross-assembly subscribers; InternalsVisibleTo ModernSpawner.Tests (#2636) 2026-09-11 22:58:44 -07:00
Distribution/Data feat: Adds independent think/move clocks for creature AI to fix speed (#2591) 2026-08-23 10:19:59 -07:00
Projects fix(network): stop sending once a disconnect is handed to the socket (#2637) 2026-09-11 23:18:37 -07:00
tools feat(network): allowlist false-positive IPs, escalate on behavior (#2556) 2026-07-30 23:12:17 -07:00
.cursorrules chore: Adds AI instructions and SKILLs for ModernUO codebase (#2347) 2026-03-01 11:42:19 -08:00
.editorconfig chore(docs): Adds UnmanagedDataReader & BinaryFileReader documentation (#1968) 2024-10-01 17:07:51 -07:00
.gitattributes fix: Adds migration checks to CICD, updates dependencies, adds Fedora 37, alpine 3.17 support (#1309) 2022-12-22 12:12:47 -08:00
.gitignore perf: Sleep the event loop when idle. Fixes networking micro-stalls. Adds event loop instrumentation. (#2559) 2026-08-09 13:24:59 -07:00
AGENTS.md chore: Adds AI instructions and SKILLs for ModernUO codebase (#2347) 2026-03-01 11:42:19 -08:00
azure-pipelines.yml chore: Cleans workflows for NodeJS 24 (#2394) 2026-03-28 21:55:29 -07:00
CLAUDE.md fix: TextDefinition was uneditable in the props gump, and where parsed constants its own way (#2624) 2026-09-10 19:36:30 -07:00
CODE_OF_CONDUCT.md Create CODE_OF_CONDUCT.md 2020-05-05 08:51:39 -07:00
CONTRIBUTING.md chore(license): Removes CLA requirement. All contributors moving forward will retain copyright. (#2038) 2024-12-31 02:52:58 -08:00
Directory.Build.props fix: Bumps dependencies (#2638) 2026-09-11 22:55:29 -07:00
FAQ.md docs: Updates FAQ for time zone issues. (#1368) 2023-03-10 00:18:36 -08:00
GEMINI.md chore: Adds AI instructions and SKILLs for ModernUO codebase (#2347) 2026-03-01 11:42:19 -08:00
global.json feat: Adds Build Tool for Publishing/Setup (#2392) 2026-03-28 21:21:50 -07:00
LICENSE Fixes more line ending issues (#267) 2020-09-27 13:54:56 -07:00
ModernUO.slnx feat: Adds Build Tool for Publishing/Setup (#2392) 2026-03-28 21:21:50 -07:00
publish.cmd feat: Adds Build Tool for Publishing/Setup (#2392) 2026-03-28 21:21:50 -07:00
publish.ps1 fix: Adds stamp check for build tool versioning (#2400) 2026-04-06 16:18:02 -06:00
publish.sh fix: Adds stamp check for build tool versioning (#2400) 2026-04-06 16:18:02 -06:00
README.md chore: Simplify server requirements section in README (#2582) 2026-08-21 19:22:04 -07:00
rider-settings.zip Cleanup/Housekeeping (#242) 2020-09-12 15:31:21 -07:00
Rules.ruleset chore(docs): Adds UnmanagedDataReader & BinaryFileReader documentation (#1968) 2024-10-01 17:07:51 -07:00
SPONSORS.md chore: Updates readme files (#2247) 2025-07-27 11:04:32 -07:00
stylecop.json Updates formatting rules (#199) 2020-08-25 18:00:51 -07:00
THIRD-PARTY-NOTICES docs: Adds RunUO-Encryption notice attribution to DarkStorm (#2369) 2026-03-12 23:35:47 -07:00
version.json feat: Adds Build Tool for Publishing/Setup (#2392) 2026-03-28 21:21:50 -07:00

ModernUO - Ultima Online Server Emulator for the modern era!

ModernUO Discord Subreddit subscribers Twitter Follow

Ultima Online Server Emulator for the modern era!

GitHub license GitHub stars GitHub issues
GitHub build Azure Pipelines build

Requirements

Supported Operating Systems

Windows 10/11/2012 R2/2016/2019/2022/2025 MacOS 14+ Debian 12+ Ubuntu 22+ LTS
Alpine 3.22+ Fedora 42+ RedHat 9+ CentOS Stream 9+ AlmaLinux 10 openSUSE 15.6+ SUSE Enterprise 15 SP6 Linux Mint 21+ Arch

Required Frameworks

All Operating Systems

.NET

Windows

VC++ Redistributable v14

Hardware

Use vCPU RAM Storage
Development / test 2 dedicated 2 GB SSD
Small live shard (< 50 concurrent) 4 dedicated 4 GB NVMe
Medium (50200) 48 8 GB NVMe
Large (200+) 8+, high clock 16 GB+ NVMe

See dev-docs/server-requirements.md for more information.

Development

git .NET

Supported IDEs

Jetbrains Rider 2025.3+ space VSCode space Visual Studio 2026

Getting Started

  • Install prerequisite requirements
  • Clone this repository (or download the latest):
    • git clone https://github.com/modernuo/ModernUO.git
  • Open ModernUO.sln to start developing

Building/Publishing

Run ./publish.cmd (Windows) or ./publish.sh (Linux/macOS) with no arguments to launch the guided build tool. It will:

  • Check prerequisites (.NET SDK, native libraries)
  • Walk you through configuration and platform selection
  • Build and publish the server to the Distribution directory
  • Show deployment instructions for cross-compiled builds

Command Line

./publish.cmd [release|debug] [os] [arch]

Linux Prerequisites

Fedora, CentOS, RHEL, AlmaLinux, Rocky, etc

dnf upgrade --refresh -y
# RHEL-family distributions (CentOS Stream, AlmaLinux, Rocky, RHEL) need CRB and EPEL enabled.
# Fedora skips this block.
dnf install -y dnf-plugins-core
dnf config-manager --set-enabled crb
dnf install -y epel-release
# Prerequisites
dnf install -y findutils libicu libdeflate libargon2 tzdata

Ubuntu, Debian, etc

apt-get update -y
# The ICU runtime package carries the ABI version in its name (libicu74, libicu76, …) and has no
# stable alias, so match it by pattern rather than pinning a release-specific name.
apt-get install -y '^libicu[0-9]+$' libdeflate0 libargon2-1 tzdata

Only the runtime libraries are needed — the -dev/-devel packages are not. Run ./build-tool --check-prereqs to check the current machine and print the exact packages your release needs.

zstd is not listed because ZstdNet bundles libzstd for every platform, and liburing is not listed because IORingGroup issues io_uring syscalls directly.

If the shard's configured time zone is a legacy alias such as US/Eastern, Debian 12 and Ubuntu 24.04 also need tzdata-legacy. See Platform Prerequisites for what each dependency is for and what breaks without it.

OSX Requirements

brew install icu4c libdeflate argon2

Running the Server

  • Follow the publish instructions
  • The Distribution directory is portable — copy it to your production server for deployment
  • Run ModernUO.exe or dotnet ModernUO.dll from the Distribution directory
  • On first run, the server will prompt you to configure game data file locations

Troubleshooting / FAQ

Want to sponsor?

Thank you for supporting us! You can find out how by visiting the sponsors page.

Collaborators

Kamron Batman Mark1145

Thanks

  • RunUO Team & Community
  • Voxpire, the ServUO Team & Community
  • Karasho, Jaedan and the ClassicUO Community



Development Tools & Plugins provided with ♥ by

JetBrains
Material Theme

Code Signing Policy

Free code signing provided by SignPath.io, certificate by SignPath Foundation.

This program will not transfer any information to other networked systems unless specifically requested by the user or the person installing or operating it

Teams & Roles

Approvers & Committers: Development Team