Stress Test
tools/Moongate.Stress is a black-box load testing tool that spawns real UO TCP clients against a live Moongate server. It validates login flow, enters the game world, runs a continuous movement loop, and evaluates Service Level Objectives (SLOs).
Test Flow
The test runs in five phases:
Phase 1: User Bootstrap
The tool ensures test accounts exist before connecting. It calls the HTTP API to create accounts with format {prefix}_{index:0000} (e.g. stress_0001, stress_0002, ...).
- Fetches existing users via
GET /api/users/ - Creates missing accounts via
POST /api/users/ - If JWT is enabled, authenticates first via
POST /auth/loginwith admin credentials
Phase 2: Ramp-up
Clients spawn gradually at the configured rate (--ramp-up-per-second). The delay between each client spawn is 1.0 / ramp-up-per-second seconds. The duration timer starts after all clients are launched.
Phase 3: Login
Each client executes the full UO login handshake:
- Connects to the login server (
host:port) - Sends login seed packet (
0xEF) - Sends account login packet (
0x80) with username/password - Receives server list (
0xA8), sends server select (0xA0, index 0) - Receives redirect packet (
0x8C) with game server IP, port, and session key
Phase 4: Game Entry
After redirect, the client connects to the game server:
- Sends seed packet with session key
- Sends game login packet (
0x91) with session key, username, password - Sends login character packet (
0x5D) - If no login confirmation within 7 seconds, falls back to character creation (
0xF8) - Waits for login confirmation (
0x1B) within a 10-second deadline - On failure, marks login as failed
Phase 5: Movement Loop
Each logged-in client enters a continuous movement loop until the test duration expires:
- Sends a random directional movement packet (
0x02) with a running flag and incrementing sequence number - Waits up to 1500ms for the movement ACK packet (
0x22) matching the sequence - Records the ACK latency using
Stopwatch.GetTimestamp()precision timing - Waits
--move-interval-msthen sends the next move
CLI Options
| Flag | Default | Description |
|---|---|---|
--host |
127.0.0.1 |
Server hostname |
--port |
2593 |
Login server port (1-65535) |
--http |
http://localhost:8088 |
HTTP API base URL for user bootstrap |
--clients |
100 |
Number of concurrent stress clients (1-10,000) |
--duration |
300 |
Test duration in seconds (10-86,400) |
--ramp-up-per-second |
10 |
Clients to spawn per second (1-10,000) |
--move-interval-ms |
300 |
Delay between moves per client in ms (50-10,000) |
--user-prefix |
stress |
Username prefix for test accounts |
--user-password |
StressPwd#123 |
Password for all test accounts |
--user-role |
Regular |
Account role (Regular, Counselor, GameMaster, Administrator) |
--admin-username |
(none) | Admin username for JWT authentication |
--admin-password |
(none) | Admin password for JWT authentication |
--verbose |
false |
Enable verbose per-client logging |
Run
Quick local test with 10 clients for 30 seconds:
dotnet run --project tools/Moongate.Stress -- \
--host 127.0.0.1 --port 2593 \
--http http://localhost:8088 \
--clients 10 --duration 30 --ramp-up-per-second 5
Full 100-client run:
dotnet run --project tools/Moongate.Stress -- \
--host 127.0.0.1 --port 2593 \
--http http://localhost:8088 \
--clients 100 --duration 300 --ramp-up-per-second 10
With JWT-enabled HTTP API:
dotnet run --project tools/Moongate.Stress -- \
--host 127.0.0.1 --port 2593 \
--http http://localhost:8088 \
--clients 100 --duration 300 \
--admin-username admin --admin-password your_password
Service Level Objectives (SLOs)
The tool evaluates these SLOs at the end of the run. The process exits with code 0 on pass, 1 on failure.
| SLO | Threshold |
|---|---|
| Login success rate | >= 99% |
| Unexpected disconnects | 0 |
| Movement ACK p95 latency | < 200ms |
| Minimum ACKs received | >= 1 |
All conditions must pass for the test to succeed.
Metrics Collected
| Metric | Description |
|---|---|
totalClients |
Number of configured clients |
loginSucceeded |
Successful login count |
loginFailed |
Failed login count |
unexpectedDisconnects |
Premature disconnection count |
movesSent |
Total movement packets sent |
movesAcked |
Total movement ACK packets received |
ackLatencyP50Ms |
50th percentile ACK latency (ms) |
ackLatencyP95Ms |
95th percentile ACK latency (ms) |
ackLatencyP99Ms |
99th percentile ACK latency (ms) |
durationSeconds |
Actual test duration |
Latency is measured per-packet using Stopwatch.GetTimestamp() for high-precision timing. Percentiles are computed from the full sorted latency distribution at the end of the run.
Output
The tool produces two outputs:
Console summary: prints all metrics, SLO evaluation results, and pass/fail status.
JSON artifact: written to artifacts/stress/latest.json:
{
"metrics": {
"totalClients": 100,
"loginSucceeded": 100,
"loginFailed": 0,
"unexpectedDisconnects": 0,
"movesSent": 98200,
"movesAcked": 98195,
"ackLatencyP50Ms": 2.1,
"ackLatencyP95Ms": 8.4,
"ackLatencyP99Ms": 15.2,
"durationSeconds": 300
},
"passed": true,
"failedConditions": []
}
Implementation Details
The stress client speaks real UO protocol:
- Packet writers for all login/movement opcodes (
0x80,0x91,0xEF,0x5D,0xF8,0x02,0xA0) - Huffman decompression for server responses (256-entry encoding table with variable-length codes)
- Character creation fallback: if no existing character is found, the client creates a default male human on Trammel with base stats (STR 60, DEX 50, INT 40)
- Thread-safe metrics: uses
Interlockedoperations,ConcurrentDictionary, andConcurrentQueuefor lock-free collection
Notes
- This is a practical operational stress test, not a micro-benchmark.
- Run on the same network profile you want to validate (localhost, LAN, or staging).
- Start with short runs (
--clients 10 --duration 30) before scaling to 100+ clients. - The tool does not test game logic beyond movement. It validates server stability under concurrent connection and packet load.
Previous: Configuration | Next: Console Commands