ModernUO/dev-docs/claude-skills/modernuo-threading.md
Kamron Batman 4f9bc1d9f6
feat: Adds AI skills to migrate from RunUO (#2366)
## Summary

Adds comprehensive RunUO → ModernUO migration documentation and Claude AI skills to help shard owners and script authors convert RunUO 2.7 code to ModernUO.

- **10 migration skills** (`dev-docs/claude-skills/migrate-from-runuo/`) — system-by-system conversion guides (foundation, serialization, timers, gumps, packets, property lists, commands/events, persistence, items/mobiles, systems/engines)
- **12 reference docs** (`dev-docs/runuo-migration-docs/`) — deep-reference with before/after examples, API mapping tables, edge cases, and gotchas
- **Updated existing skills** — `modernuo-timers`, `modernuo-serialization`, and `modernuo-threading` now document that `Serialize()` runs on background threads and timers are not thread-safe
- **Updated `CLAUDE.md`** — added migration skill lookup table

### Key migration patterns covered
- Manual `Serialize()`/`Deserialize()` → source-generated `[SerializableField]`
- `Packet` class hierarchy → static `SpanWriter`/`SpanReader` methods
- `Timer` subclasses → `TimerExecutionToken` fire-and-forget
- `Gump` → `StaticGump<T>`/`DynamicGump` with builders
- `EventSink.WorldSave` → `GenericPersistence`
- `ObjectPropertyList` → `IPropertyList` with string hole rules
- Universal changes: naming (`m_` → `_`), `[Constructable]` → `[Constructible]`, logging, spatial queries
2026-03-13 00:33:45 -07:00

175 lines
6.8 KiB
Markdown

---
name: modernuo-threading
description: >
Trigger when discussing async patterns, world saves, game loop, or reviewing code for threading issues. When using await, Task, or any concurrency-related code in game logic.
---
# ModernUO Threading & Event Loop
## When This Activates
- Reviewing code for threading issues
- Discussing async/await patterns
- Working with world saves
- Any mention of `Task.Run`, `Thread`, `lock`, `ConcurrentDictionary`
- Understanding the game loop
## CRITICAL RULE: Single-Threaded Game Logic
ModernUO uses a **single-threaded game loop**. All game logic runs on one thread. There are NO exceptions for game code.
## Forbidden in Game Code
```csharp
// ALL of these are WRONG in Projects/UOContent/ code:
Task.Run(() => ProcessItems()); // Background thread
new Thread(BackgroundWork).Start(); // Manual thread
ThreadPool.QueueUserWorkItem(Work); // Thread pool
lock (_syncObj) { ... } // Locking
Monitor.Enter(obj); // Monitor
volatile int _counter; // Volatile
ConcurrentDictionary<int, Item> _items; // Concurrent collections
ConcurrentQueue<T> _queue; // Concurrent collections
Interlocked.Increment(ref _count); // Atomics
Mutex mutex; // OS mutex
Semaphore sem; // Semaphore
ReaderWriterLockSlim rwl; // RW lock
```
**Why**: The game loop is single-threaded. Concurrency primitives add overhead for no benefit, and background threads would cause data races with game state.
## Why await Is Safe
`EventLoopContext` implements `SynchronizationContext` and routes all `await` continuations back to the main thread:
```csharp
// This is SAFE in game code:
await Timer.Pause(TimeSpan.FromMilliseconds(100));
// Continuation runs on the game thread, not a thread pool thread
```
The flow:
1. `await` captures `EventLoopContext` as the synchronization context
2. When the awaited task completes, the continuation is posted to `EventLoopContext._queue`
3. `LoopContext.ExecuteTasks()` runs those continuations on the main thread during the next game loop tick
## Game Loop Structure
```csharp
// Simplified from Projects/Server/Main.cs
while (!Closing)
{
_tickCount = GetTimestamp();
_now = DateTime.UtcNow;
Mobile.ProcessDeltaQueue(); // Send mobile state changes to clients
Item.ProcessDeltaQueue(); // Send item state changes to clients
Timer.Slice(_tickCount); // Execute due timers
NetState.Slice(); // Process network I/O
LoopContext.ExecuteTasks(); // Run async continuations (Timer.Pause, etc.)
Timer.CheckTimerPool(); // Refill timer pool if needed
}
```
## EventLoopContext Details
```csharp
public sealed class EventLoopContext : SynchronizationContext
{
private readonly ConcurrentQueue<Action> _queue; // Normal tasks
private readonly ConcurrentQueue<Action> _priorityQueue; // High priority
private readonly int _maxPerFrame; // Default: 128
// Posts run on next ExecuteTasks() call
public void Post(Action d, Priority priority = Priority.Normal);
// Send blocks if called from another thread, immediate if on game thread
public override void Send(SendOrPostCallback d, object state);
// Called once per game loop tick
public void ExecuteTasks();
}
```
## Memory: STArrayPool vs ArrayPool
In game code, use `STArrayPool<T>.Shared` (single-threaded, no locks):
```csharp
// GOOD - no locking overhead
var buffer = STArrayPool<byte>.Shared.Rent(1024);
try { /* use buffer */ }
finally { STArrayPool<byte>.Shared.Return(buffer); }
```
`ArrayPool<T>.Shared` uses locks for thread safety -- unnecessary overhead in single-threaded context.
## Memory: PooledRefList
```csharp
// Stack-allocated list using pooled arrays
using var list = PooledRefList<Mobile>.Create();
list.Add(mobile);
// Automatically returns array to pool on Dispose
// For multi-threaded contexts (rare):
using var list = PooledRefList<Mobile>.CreateMT();
```
## World Saves
World saves use **parallel serialization threads**. This is critical to understand:
1. **Preserialize**: Allocates heaps and wakes serialization thread workers (background)
2. **Snapshot**: Main thread calls `Persistence.SerializeAll()` which pushes entities into `SerializationThreadWorker` queues (round-robin). Workers call `Serialize(writer)` on **their own background threads** in parallel.
3. **Write snapshot**: Disk I/O on background threads after serialization completes
```csharp
// From World.cs -- save flow:
World.Save();
→ Preserialize() on thread pool (allocate heaps, wake serialization workers)
→ Snapshot() on main thread (queues entities to workers, workers serialize in parallel)
→ SerializationThreadWorker.Execute() calls e.Serialize(writer) on background thread
→ PauseSerializationThreads() (wait for workers to finish)
→ WriteSnapshot() on thread pool (disk I/O only)
```
### Serialize() runs on background threads
Because `SerializationThreadWorker` calls `Serialize()` on its own thread, **`Serialize()` must be pure**:
- **NO** creating/destroying Items or Mobiles
- **NO** starting/stopping timers (not thread-safe)
- **NO** sending packets or modifying NetState
- **NO** mutating shared game state
- **ONLY** read fields and write to `IGenericWriter`
See `modernuo-serialization.md` for full purity rules.
## Exceptions: Server Infrastructure
These files MAY use threading (they're server infrastructure, not game logic):
- `Projects/Server/Main.cs` - Event loop, thread setup
- `Projects/Server/World/World.cs` - World save I/O
- `Projects/Server/Network/` - Network I/O
- `Projects/Server/Timer/Timer.Pool.cs` - Pool refill
## Anti-Patterns
| Pattern | Problem | Solution |
|---|---|---|
| `Task.Run(...)` | Runs on thread pool, races with game state | Use `Timer.StartTimer()` |
| `new Thread(...)` | Same as above | Use `Timer.StartTimer()` |
| `lock(obj)` | Unnecessary overhead, no contention exists | Remove lock, use plain code |
| `ConcurrentDictionary` | Lock-free but still overhead | Use `Dictionary<K,V>` |
| `volatile` | Memory barriers not needed on single thread | Use plain field |
| `Thread.Sleep()` | Blocks entire game loop | Use `await Timer.Pause()` |
| `ArrayPool<T>.Shared` | Uses locks | Use `STArrayPool<T>.Shared` |
## Real Examples
- Game loop: `Projects/Server/Main.cs` (RunEventLoop)
- EventLoopContext: `Projects/Server/EventLoopTasks.cs`
- STArrayPool: `Projects/Server/Buffers/STArrayPool.cs`
- PooledRefList: `Projects/Server/Collections/PooledRefList.cs`
- World save: `Projects/Server/World/World.cs`
## See Also
- `dev-docs/threading-model.md` - Complete threading documentation
- `dev-docs/claude-skills/modernuo-code-audit.md` - Threading audit rules
- `dev-docs/claude-skills/modernuo-timers.md` - Timer-based scheduling