An open API service indexing awesome lists of open source software.

https://github.com/moongate-community/moongatev2

Moongate is modern Ultima Online server emulator built from scratch in C# with AOT compilation for high performance and nostalgic gameplay experience.
https://github.com/moongate-community/moongatev2

high-performance mmo mmorpg modernuo retrogaming rpg runuo servuo ultimaonline uox3

Last synced: 3 months ago
JSON representation

Moongate is modern Ultima Online server emulator built from scratch in C# with AOT compilation for high performance and nostalgic gameplay experience.

Awesome Lists containing this project

README

          

# Moongate v2


Moongate logo


.NET 10
AOT Enabled
Lua Scripting
GPL-3.0 License
Development Status

[![CI](https://github.com/moongate-community/moongatev2/actions/workflows/ci.yml/badge.svg)](https://github.com/moongate-community/moongatev2/actions/workflows/ci.yml)
[![Tests](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/moongate-community/moongatev2/gh-pages/badges/tests.json)](https://github.com/moongate-community/moongatev2/actions/workflows/ci.yml)
[![Coverage](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/moongate-community/moongatev2/gh-pages/badges/coverage.json)](https://github.com/moongate-community/moongatev2/actions/workflows/coverage.yml)
[![Code Quality](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/moongate-community/moongatev2/gh-pages/badges/quality.json)](https://github.com/moongate-community/moongatev2/actions/workflows/quality.yml)
[![Quality Gate](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/moongate-community/moongatev2/gh-pages/badges/quality-gate.json)](https://github.com/moongate-community/moongatev2/actions/workflows/quality.yml)
[![Security](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/moongate-community/moongatev2/gh-pages/badges/security.json)](https://github.com/moongate-community/moongatev2/actions/workflows/security.yml)
[![Latest Release](https://img.shields.io/github/v/release/moongate-community/moongatev2)](https://github.com/moongate-community/moongatev2/releases)
[![Latest Pre-release](https://img.shields.io/github/v/release/moongate-community/moongatev2?include_prereleases&label=pre-release)](https://github.com/moongate-community/moongatev2/releases)
[![Docs](https://github.com/moongate-community/moongatev2/actions/workflows/docs.yml/badge.svg)](https://github.com/moongate-community/moongatev2/actions/workflows/docs.yml)
[![Release](https://github.com/moongate-community/moongatev2/actions/workflows/release.yml/badge.svg)](https://github.com/moongate-community/moongatev2/actions/workflows/release.yml)
[![Docker Image](https://img.shields.io/docker/v/tgiachi/moongate?sort=semver)](https://hub.docker.com/r/tgiachi/moongate)
[![Docker Pulls](https://img.shields.io/docker/pulls/tgiachi/moongate)](https://hub.docker.com/r/tgiachi/moongate)
[![Docker Image Size](https://img.shields.io/docker/image-size/tgiachi/moongate/latest)](https://hub.docker.com/r/tgiachi/moongate)

Moongate v2 is a modern Ultima Online server project built with .NET 10.
It targets a clean, modular architecture with strong packet tooling, deterministic game-loop processing, and practical test coverage.

> Looking for collaborators: I am actively seeking contributors to help build Moongate v2, and I would especially appreciate support with technical/code reviews.
> Want to help? Open an issue/discussion on GitHub or join Discord:
> - Issues: https://github.com/moongate-community/moongatev2/issues
> - Discussions: https://github.com/moongate-community/moongatev2/discussions
> - Matrix room: https://matrix.to/#/#moongate:matrix.org

> Moongate is not a clone of ModernUO, RunUO, ServUO or any other server, and it does not aim to be. In fact, we owe a great deal of inspiration to these projects. Their legacy and technical achievements are invaluable, and this project would not exist without them. Thank you.

## Acknowledgements

Special thanks to the teams and contributors behind these projects, which strongly inspired Moongate:

- POLServer:
- ModernUO:

Data credits:

- World decoration datasets (`Assets/data/decoration/**`) are imported from the ModernUO Distribution data pack.
- World location datasets (`Assets/data/locations/**`) are imported/adapted from the ModernUO Distribution data pack.
- Sign datasets (`Assets/data/signs/signs.cfg`) are imported/adapted from ModernUO data format and content.

Thanks to the ModernUO team for making these resources available.

## Index

- [Project Goals](#project-goals)
- [Project Story](#project-story)
- [Frontend Preview](#frontend-preview)
- [Current Status](#current-status)
- [Spatial Chunk Strategy](#spatial-chunk-strategy)
- [World Generation Pipeline](#world-generation-pipeline)
- [UO Feature Support (Current)](#uo-feature-support-current)
- [Persistence](#persistence)
- [Email Delivery (Minimal SMTP)](#email-delivery-minimal-smtp)
- [Templates](#templates)
- [Solution Structure](#solution-structure)
- [Source Generators (AOT)](#source-generators-aot)
- [Event And Packet Separation](#event-and-packet-separation)
- [Game Loop Scheduling](#game-loop-scheduling)
- [Requirements](#requirements)
- [Server Startup Tutorial](#server-startup-tutorial)
- [Quick Start](#quick-start)
- [Command System](#command-system)
- [Scripting](#scripting)
- [Item ScriptId Dispatch](#item-scriptid-dispatch)
- [Scripts](#scripts)
- [Benchmarks](#benchmarks)
- [Docker](#docker)
- [Docker Monitoring Stack](#docker-monitoring-stack)
- [Documentation](#documentation)
- [Development Notes](#development-notes)
- [Contributing](#contributing)
- [License](#license)

## Project Goals

- Build a maintainable UO server foundation focused on correctness and iteration speed.
- Keep networking and game-loop boundaries explicit and thread-safe.
- Model protocol packets with typed definitions and source-generated registration.
- Stay AOT-aware while preserving a smooth local development workflow.

## Project Story

You can read the background and motivation behind Moongate v2 here:

-

## Frontend Preview

I hate building frontend myself, so thanks to Codex I started adding a UI layer in `ui/`.

![UI Screen 1](images/ui/ui_screen1.png)
![UI Screen 2](images/ui/ui_screen2.png)
![UI Screen 3](images/ui/ui_screen_3.png)

The UI now also includes Item Templates search with image previews.

## Current Status

The project is actively in development and already includes:

- TCP server startup and connection lifecycle handling.
- Packet framing/parsing for fixed and variable packet sizes.
- Attribute-based packet mapping (`[PacketHandler(...)]`) with source generation.
- Inbound message bus (`IMessageBusService`) for network thread -> game-loop crossing.
- Domain event bus (`IGameEventBusService`) with initial events (`PlayerConnectedEvent`, `PlayerDisconnectedEvent`).
- Outbound event listener abstraction (`IOutboundEventListener`) for domain-event -> network side effects.
- Session split between transport (`GameNetworkSession`) and gameplay/protocol context (`GameSession`).
- Unit tests for core server behaviors and packet infrastructure.
- Lua scripting runtime with module/function binding and `.luarc` generation support.
- Lua metadata files (`definitions.lua`, `.luarc.json`) generated in configured `LuaEngineConfig.LuarcDirectory` during engine startup.
- Embedded HTTP host (`Moongate.Server/Http`) for health/admin endpoints and OpenAPI/Scalar docs.
- Dedicated HTTP rolling logs in the shared logs directory (`moongate_http-*.log`).
- Snapshot+journal persistence module (`Moongate.Persistence`) integrated in server lifecycle.
- ID-based persistence references for character equipment/container ownership.
- Interactive console UI with fixed prompt (`moongate>`) and Spectre-based colored log rendering.
- Timer wheel runtime metrics integrated in the metrics pipeline (`timer.*`).
- Timestamp-driven game loop scheduling with timer delta updates and optional idle CPU throttling.
- Region system adopted from ModernUO (chosen as the most robust baseline), including polymorphic JSON loading via `$type`.
- Spatial region resolution indexed by sector with deterministic ordering:
- higher `Priority` first
- then deeper parent/child hierarchy (`ChildLevel`) when priority ties.
- Region music mapped as typed `MusicName` and resolved by `MapId` + position.
- Minimal email stack with Scriban templates and SMTP sender (`Moongate.Email`), wired through `IEmailService`.
- Basic/timid A* pathfinding service is available (`IPathfindingService` / `AStarPathfindingService`) and already used by Lua mobile movement primitives (`MoveTowards`).
- Light cycle is now isolated in `ILightService`/`LightService` (separate from weather), including global override commands exposed to Lua.
- Lua command scripts are organized under `moongate_data/scripts/commands/gm` (one command per file, imported from `init.lua`).

## Recent Development Highlights

- Persistence serialization was migrated to MessagePack-CSharp source-generated contracts to resolve NativeAOT runtime instability.
- Outbound packet sending was split into a dedicated networking thread path to reduce game-loop contention.
- Spatial/game-loop hot paths received allocation-focused optimizations across login, packet dispatch, event bus, and persistence mapping.
- Light cycle logic was extracted from `WeatherService` into dedicated `ILightService`/`LightService`.
- New Lua GM command scripts were added under `moongate_data/scripts/commands/gm` (`.eclipse`, `.set_world_light`, `.teleports`).

## Spatial Chunk Strategy

Moongate uses a sector/chunk-based world streaming strategy instead of a pure range-view scan model.

- World data is indexed by sectors (`16x16`) and loaded lazily.
- When a sector is touched, Moongate loads entities (items + mobiles) around it in a configurable sector radius.
- Around player login and sector changes, snapshots are sent using sector radius windows.
- Sectors are created, populated, and reused in memory; inactive areas stay unloaded until requested.

Why this choice:

- Predictable memory growth and lower steady-state CPU usage on large worlds.
- Better cache locality for entity queries and network snapshot generation.
- Simpler scalability path for high-concurrency shards.

Compared to classic server approaches that rely mainly on repeated range-view scans, this model is intentionally closer to chunk-streaming systems (Minecraft-style): load/unload by sector boundaries with configurable warmup and sync radii.

For a detailed internal status snapshot, see `docs/plans/status-2026-02-19.md`.

## World Generation Pipeline

Moongate uses a world-generation pipeline based on `IWorldGenerator`.

- Each generator is a named unit (`Name`), orchestrated by `IWorldGeneratorBuilderService`.
- The builder supports:
- full execution (`GenerateAsync()`),
- targeted execution by name (`GenerateAsync("doors")`),
- optional progress callback (`Action`) for logs/progress output.
- Door generation is implemented as `DoorGeneratorBuilder` (`Name = "doors"`), with hardcoded scan regions (ModernUO-style) and `CanFit` filtering before accepting candidate placements.
- Generated doors are persisted as world items and include facing/link metadata for runtime behavior.
- Doors now support live open/close behavior on double-click through Lua + `DoorService`.
- ORA LE PORTE SI APRONO!! :D :D

Manual trigger:

- Command: `.spawn_doors`
- Scope: console + in-game admin command
- Behavior: runs only the `doors` generator and streams progress lines to command output.

## UO Feature Support (Current)

This section reflects the current server-side implementation status.

### Supported now

- Active inbound packet handlers:
- Login/auth: `0xEF`, `0x80`, `0xA0`, `0x91`, `0x5D`, `0xBD`
- Character: `0x00`
- Movement: `0x02`, `0xC8`
- Item interaction: `0x07`, `0x08`, `0x09`, `0x13`, `0x06`
- Speech/chat: `0xAD`, `0xB5`
- Targeting: `0x6C`
- General info multiplexer: `0xBF`
- Player status: `0x34`
- Ping: `0x73`
- Tooltip: `0xD6`
- `0xBF` subcommands currently wired in runtime:
- `0x06` Party System
- `0x1A` Stat Lock Change
- `0x2C` Use Targeted Item
- `0x2D` Cast Targeted Spell
- `0x2E` Use Targeted Skill
- Active outbound gameplay packets include:
- Login/session: `0x8C`, `0xA8`, `0xA9`, `0x1B`, `0x55`, `0x82`, `0xB9`
- World/entity sync: `0x78`, `0x20`, `0x2E`, `0x24`, `0x3C`, `0x11`, `0x88`, `0xF3`, `0x23`, `0x76`
- Movement/time: `0x22`, `0x21`, `0x5B`, `0xF2`
- Environment/effects: `0xBC`, `0x4F`, `0x4E`, `0x6D`, `0x65`, `0x54`, `0x70`, `0xC0`, `0xC7`
- UI/speech: `0xAE`, `0xB0`, `0xDD`

### Partially implemented

- Protocol model coverage is broader than runtime gameplay wiring:
- many packet contracts exist in `Moongate.Network.Packets`,
- only the opcodes listed above are currently connected to live handlers/flows.
- Item pipeline is functional for pickup/drop/equip/container refresh, but advanced cases (full trade/vendor/economy semantics) are still expanding.
- Lua runtime is integrated (commands, speech, targeting, gump builder), but high-level game systems are still script-surface growth areas.

### Not yet implemented (major areas)

- Full combat loop (swing/spell damage pipeline, notoriety-driven combat rules).
- Skill system execution and progression.
- NPC AI, vendors, loot systems, and spawn regions are still evolving; pathfinding currently exists in a basic form and is not yet a full navigation stack.
- World simulation breadth (housing, boats, advanced map interactions, seasons/weather effects gameplay-side).
- Economy systems and complete trading/vendor behavior.
- Full UO protocol listener coverage (many opcodes intentionally unhandled yet).

## Persistence

Moongate uses a lightweight file-based persistence model implemented in `src/Moongate.Persistence`:

- Snapshot file (`world.snapshot.bin`) for full world state checkpoints.
- Append-only journal (`world.journal.bin`) for incremental operations between snapshots.
- MessagePack-CSharp (source-generated) binary serialization for compact and fast read/write.
- Per-operation checksums in journal entries to detect truncated/corrupted tails.
- Runtime file-lock mode for snapshot/journal handles (`PersistenceOptions.EnableFileLock`, default: enabled).
- Thread-safe repositories for accounts, mobiles, and items.
- Mobile/item relations are persisted by serial references:
- `UOMobileEntity.BackpackId`
- `UOMobileEntity.EquippedItemIds`
- `UOItemEntity.ParentContainerId` + `ContainerPosition`
- `UOItemEntity.EquippedMobileId` + `EquippedLayer`

Runtime behavior:

- On startup, `IPersistenceService.StartAsync()` loads snapshot (if present) and replays journal.
- During runtime, repositories append operations to journal.
- On save/stop, `SaveSnapshotAsync()` writes a new snapshot and resets the journal.
- With file-lock mode enabled, snapshot/journal handles remain open for process lifetime and prevent concurrent writers.

NativeAOT note (post-mortem):

- We hit an insidious NativeAOT crash (`Segmentation fault: 11`) during persistence save.
- Root cause: the previous MemoryPack-based snapshot/journal path crashed under AOT in our runtime scenario.
- Resolution: full persistence serializer migration from MemoryPack to MessagePack-CSharp source-generated contracts (`MessagePackObject`), covering both snapshot and journal payloads.
- Result: AOT startup + first admin account creation + save cycle now complete without crash.

Storage location:

- Files are written under the server `save` directory (`DirectoriesConfig[DirectoryType.Save]`).

Query support:

- `IAccountRepository`, `IMobileRepository`, and `IItemRepository` expose `QueryAsync(...)`.
- Queries are evaluated on immutable snapshots with ZLinq-backed projection/filtering.

## Email Delivery (Minimal SMTP)

Moongate includes a minimal email pipeline:

- `IEmailService`: orchestration entrypoint.
- `IEmailTemplateService`: template rendering via Scriban (`Moongate.Email`).
- `IEmailSender`: transport abstraction with SMTP implementation (`SmtpEmailSender`).
- `NoOpEmailSender`: selected automatically when email is disabled.
- `websiteUrl`: global Scriban variable injected from `Http.WebsiteUrl`.

Default templates are loaded from:

- `moongate_data/email/templates/registration_ok/*`
- `moongate_data/email/templates/recover_password/*`

Runtime directory mapping uses `DirectoryType.EmailTemplates`.

Minimal config shape:

```json
{
"email": {
"isEnabled": false,
"fromAddress": "noreply@localhost",
"fallbackLocale": "en",
"smtp": {
"host": "localhost",
"port": 25,
"useSsl": false,
"username": null,
"password": null
}
}
}
```

## Templates

Moongate loads gameplay templates from `DirectoriesConfig[DirectoryType.Templates]`:

- `templates/items/**/*.json` -> loaded by `ItemTemplateLoader` into `IItemTemplateService`
- `templates/mobiles/**/*.json` -> loaded by `MobileTemplateLoader` into `IMobileTemplateService`

Template values are data-driven and resolved at runtime using spec objects:

- `HueSpec`: supports fixed values (`"4375"`, `"0x1117"`) and ranges (`"hue(5:55)"`)
- `GoldValueSpec`: supports fixed values (`"0"`) and dice notation (`"dice(1d8+8)"`)

Example item template:

```json
{
"type": "item",
"id": "leather_backpack",
"name": "Leather Backpack",
"category": "Container",
"itemId": "0x0E76",
"hue": "hue(10:80)",
"goldValue": "dice(2d8+12)",
"lootType": "Regular",
"stackable": false,
"isMovable": true
}
```

Example startup item template:

```json
{
"type": "item",
"id": "inner_torso",
"category": "Start Clothes",
"itemId": "0x1F7B",
"hue": "4375",
"goldValue": "dice(1d4+1)",
"weight": 1
}
```

Example mobile template:

```json
{
"type": "mobile",
"id": "orione",
"name": "Orione",
"category": "animals",
"body": "0xC9",
"skinHue": 779,
"hairStyle": 0,
"brain": "orion"
}
```

Resolution model:

- JSON loading parses to typed specs (`HueSpec`, `GoldValueSpec`)
- final random values are resolved when creating runtime entities (not at JSON load time)

## Solution Structure

- `src/Moongate.Server`: host/bootstrap, game loop, network orchestration, session/event services.
- `src/Moongate.Network.Packets`: packet contracts, descriptors, registry, packet definitions.
- `src/Moongate.Generators`: unified source generators for packets, handlers, metrics, script-module registry, and version metadata.
- `src/Moongate.UO.Data`: UO domain data types and utility models.
- `src/Moongate.Core`: shared low-level utilities.
- `src/Moongate.Network`: TCP/network primitives.
- `src/Moongate.Scripting`: Lua engine service, script modules, script loaders, and scripting helpers.
- `src/Moongate.Server/Http`: embedded ASP.NET Core host service used by the server bootstrap.
- `tests/Moongate.Tests`: unit tests.
- `benchmarks/Moongate.Benchmarks`: BenchmarkDotNet performance suite.
- `docs/`: documentation and project notes (plans, sprints, protocol notes, journal).

## Source Generators (AOT)

Moongate uses source generators to reduce runtime reflection/discovery work and improve Native AOT compatibility and startup performance.

Current generator project:

- `Moongate.Generators`
- Generates packet table/registry wiring and `PacketDefinition` constants from packet metadata.
- Generates bootstrap packet-listener registrations from `[RegisterPacketHandler(...)]`.
- Generates bootstrap game-event-listener subscriptions from `[RegisterGameEventListener]`.
- Generates bootstrap file-loader registrations from `[RegisterFileLoader(order)]`.
- Generates metric snapshot mappers from metric-decorated models.
- Generates script module registries from `[ScriptModule(...)]` in `Moongate.Scripting` and `Moongate.Server`.
- Generates `VersionUtils` metadata for server version/codename.

Why this helps for AOT:

- Moves dynamic mapping logic from runtime to compile time.
- Reduces dependency on reflection-based registration paths.
- Improves deterministic startup behavior.

## Event And Packet Separation

Moongate uses a strict separation between inbound protocol parsing and outbound event projections:

- `IPacketListener` handles inbound packets only (`Client -> Server`) and applies domain use-cases.
- Domain services publish `IGameEvent` messages through `IGameEventBusService`.
- Game event listeners are declared with `IGameEventListener` and auto-subscribed at bootstrap via `[RegisterGameEventListener]`.
- `IOutboundEventListener` handles outbound side-effects from domain events (for example enqueueing packets).
- `RegisterOutboundEventListener()` is the bootstrap helper to register outbound listeners as hosted services with priority.
- `IOutgoingPacketQueue` and `IOutboundPacketSender` deliver outbound packets on the game-loop/network boundary.

## Game Loop Scheduling

The server loop is timestamp-driven (monotonic `Stopwatch`) rather than fixed-sleep tick stepping:

- `GameLoopService` computes current loop timestamp and calls `ITimerService.UpdateTicksDelta(...)`.
- `TimerWheelService` accumulates elapsed milliseconds and advances only the required number of wheel ticks.
- This keeps timer semantics stable while adapting to real runtime load.
- Optional idle throttling (`Game.IdleCpuEnabled`, `Game.IdleSleepMilliseconds`) sleeps briefly when no work was processed.

### Background Jobs And Main-Thread Dispatch

Moongate provides `IBackgroundJobService` to run non-gameplay work in parallel and safely marshal results back to the game loop thread.

Use it for:

- file parsing/import tasks
- image generation and offline processors
- CPU/I/O work that does not directly mutate world state

Do not mutate gameplay state directly inside background workers.
Post results back to game loop callbacks instead.

Example:

```csharp
public sealed class SeedImportService
{
private readonly IBackgroundJobService _backgroundJobService;

public SeedImportService(IBackgroundJobService backgroundJobService)
{
_backgroundJobService = backgroundJobService;
}

public void ImportAsync()
{
_backgroundJobService.RunBackgroundAndPostResultAsync(
async () => await LoadSeedStatsAsync(),
result =>
{
// This callback executes on game-loop thread.
ApplyStatsToRuntime(result);
},
ex =>
{
// Also marshaled on game-loop thread.
Log.Error(ex, "Seed import failed.");
}
);
}
}
```

## Requirements

- .NET SDK 10.0.x

## Server Startup Tutorial

This is the recommended first-time setup to run the server locally.

1. Prepare directories:
- `MOONGATE_ROOT_DIRECTORY`: server root (config, save, logs, scripts, templates).
- `MOONGATE_UO_DIRECTORY`: Ultima Online client data directory.
2. Export env vars:

```bash
export MOONGATE_ROOT_DIRECTORY="$HOME/moongate"
export MOONGATE_UO_DIRECTORY="/path/to/uo-client"
```

3. Restore/build/test:

```bash
dotnet restore
dotnet build
dotnet test
```

4. Start server:

```bash
dotnet run --project src/Moongate.Server
```

5. First startup behavior:
- If `moongate.json` is missing, it is created in `MOONGATE_ROOT_DIRECTORY`.
- Asset/data files are copied only when missing.
- If no accounts exist, a default admin is created.

6. Optional admin credentials override:

```bash
export MOONGATE_ADMIN_USERNAME="admin"
export MOONGATE_ADMIN_PASSWORD="change-me-now"
```

7. Verify runtime:
- Game TCP server: port `2593`
- HTTP endpoints (default): `http://localhost:8088/`, `http://localhost:8088/health`, `http://localhost:8088/metrics`, `http://localhost:8088/scalar`
- Logs: `MOONGATE_ROOT_DIRECTORY/logs`

## Environment Configuration

Moongate now supports full configuration override through environment variables.

- Prefix: `MOONGATE_`
- Nested properties: use `__` (double underscore)
- Precedence: `MOONGATE_*` env vars override `moongate.json`

Example:

- `MOONGATE_HTTP__PORT=8088`
- `MOONGATE_HTTP__JWT__ISSUER=moongate-http`
- `MOONGATE_SPATIAL__SECTOR_ENTER_SYNC_RADIUS=3`

Supported config env variables:

- Core:
- `MOONGATE_ROOT_DIRECTORY`
- `MOONGATE_UO_DIRECTORY`
- `MOONGATE_LOG_LEVEL`
- `MOONGATE_LOG_PACKET_DATA`
- `MOONGATE_IS_DEVELOPER_MODE`
- HTTP:
- `MOONGATE_HTTP__IS_ENABLED`
- `MOONGATE_HTTP__PORT`
- `MOONGATE_HTTP__WEBSITE_URL`
- `MOONGATE_HTTP__IS_OPEN_API_ENABLED`
- `MOONGATE_HTTP__JWT__IS_ENABLED`
- `MOONGATE_HTTP__JWT__SIGNING_KEY`
- `MOONGATE_HTTP__JWT__ISSUER`
- `MOONGATE_HTTP__JWT__AUDIENCE`
- `MOONGATE_HTTP__JWT__EXPIRATION_MINUTES`
- Game:
- `MOONGATE_GAME__SHARD_NAME`
- `MOONGATE_GAME__TIMER_TICK_MILLISECONDS`
- `MOONGATE_GAME__TIMER_WHEEL_SIZE`
- `MOONGATE_GAME__IDLE_CPU_ENABLED`
- `MOONGATE_GAME__IDLE_SLEEP_MILLISECONDS`
- Metrics:
- `MOONGATE_METRICS__ENABLED`
- `MOONGATE_METRICS__INTERVAL_MILLISECONDS`
- `MOONGATE_METRICS__LOG_ENABLED`
- `MOONGATE_METRICS__LOG_TO_CONSOLE`
- `MOONGATE_METRICS__LOG_LEVEL`
- Persistence:
- `MOONGATE_PERSISTENCE__SAVE_INTERVAL_SECONDS`
- Spatial:
- `MOONGATE_SPATIAL__LAZY_SECTOR_ITEM_LOAD_ENABLED`
- `MOONGATE_SPATIAL__SECTOR_WARMUP_RADIUS`
- `MOONGATE_SPATIAL__SECTOR_ENTER_SYNC_RADIUS`
- `MOONGATE_SPATIAL__LAZY_SECTOR_ENTITY_LOAD_RADIUS`
- `MOONGATE_SPATIAL__SECTOR_UPDATE_BROADCAST_RADIUS`
- `MOONGATE_SPATIAL__LIGHT_WORLD_START_UTC`
- `MOONGATE_SPATIAL__LIGHT_SECONDS_PER_UO_MINUTE`
- Scripting:
- `MOONGATE_SCRIPTING__ENABLE_FILE_WATCHER`
- Email:
- `MOONGATE_EMAIL__IS_ENABLED`
- `MOONGATE_EMAIL__FROM_ADDRESS`
- `MOONGATE_EMAIL__FALLBACK_LOCALE`
- `MOONGATE_EMAIL__SMTP__HOST`
- `MOONGATE_EMAIL__SMTP__PORT`
- `MOONGATE_EMAIL__SMTP__USE_SSL`
- `MOONGATE_EMAIL__SMTP__USERNAME`
- `MOONGATE_EMAIL__SMTP__PASSWORD`

Additional runtime env variables (not part of `MoongateConfig`):

- `MOONGATE_ADMIN_USERNAME`
- `MOONGATE_ADMIN_PASSWORD`
- `MOONGATE_UI_DIST`
- `MOONGATE_HTTP_JWT_SIGNING_KEY` (legacy explicit fallback; `MOONGATE_HTTP__JWT__SIGNING_KEY` is preferred)

### Docker Compose Example

```yaml
services:
moongate:
image: tgiachi/moongate:latest
environment:
MOONGATE_ROOT_DIRECTORY: /data/moongate
MOONGATE_UO_DIRECTORY: /data/uo
MOONGATE_HTTP__PORT: "8088"
MOONGATE_HTTP__IS_OPEN_API_ENABLED: "true"
MOONGATE_HTTP__JWT__SIGNING_KEY: "change-me"
MOONGATE_SPATIAL__SECTOR_ENTER_SYNC_RADIUS: "3"
MOONGATE_SPATIAL__SECTOR_UPDATE_BROADCAST_RADIUS: "3"
MOONGATE_SPATIAL__LIGHT_WORLD_START_UTC: "1997-09-01T00:00:00Z"
MOONGATE_SPATIAL__LIGHT_SECONDS_PER_UO_MINUTE: "5"
MOONGATE_PERSISTENCE__SAVE_INTERVAL_SECONDS: "60"
MOONGATE_EMAIL__IS_ENABLED: "true"
MOONGATE_EMAIL__SMTP__HOST: "smtp.example.com"
MOONGATE_EMAIL__SMTP__PORT: "587"
MOONGATE_EMAIL__SMTP__USE_SSL: "true"
MOONGATE_EMAIL__SMTP__USERNAME: "smtp-user"
MOONGATE_EMAIL__SMTP__PASSWORD: "smtp-pass"
volumes:
- ./moongate_data:/data/moongate
- ./uo:/data/uo:ro
ports:
- "2593:2593"
- "8088:8088"
```

## Quick Start

```bash
dotnet restore
dotnet build
dotnet test
dotnet run --project src/Moongate.Server
```

By default, the server starts with packet data logging enabled in `Program.cs`.

Console logging:

- Custom Serilog console sink with output template compatible formatting.
- Level-based colored output in terminal (Spectre.Console).
- Placeholder values (message properties) highlighted with dedicated styling.
- Fixed bottom prompt row (`moongate>`) when running in an interactive terminal.

HTTP service defaults:

- `Http.IsEnabled = true`
- `Http.Port = 8088`
- `Http.WebsiteUrl = "http://localhost"`
- `Http.IsOpenApiEnabled = true`
- Base endpoint: `/`
- Health endpoint: `/health`
- OpenAPI JSON: `/openapi/v1.json`
- Scalar UI: `/scalar`
- Users API:
- `GET /api/users`
- `GET /api/users/{accountId}`
- `POST /api/users`
- `PUT /api/users/{accountId}`
- `DELETE /api/users/{accountId}`

## Command System

Commands now use a hybrid model:

- **Primary path (C# built-ins)**: `ICommandExecutor` + `[RegisterConsoleCommand(...)]`
- Discovered and registered at compile-time by `ConsoleCommandRegistrationGenerator`
- Executors are registered as DryIoc singletons
- **Secondary path (dynamic/Lua/future)**: manual `ICommandSystemService.RegisterCommand(...)`
- Kept intentionally for runtime registration scenarios

Authorization behavior:

- Console source is always evaluated as `AccountType.Administrator`.
- In-game source is evaluated using `GameSession.AccountType` (set during login).
- If source is valid but role is too low, command execution is rejected with warning output.

Example C# command registration (source-generated):

```csharp
using Moongate.Server.Attributes;
using Moongate.Server.Data.Internal.Commands;
using Moongate.Server.Interfaces.Services.Console;
using Moongate.Server.Types.Commands;
using Moongate.UO.Data.Types;

[RegisterConsoleCommand(
"whoami|me",
"Shows basic identity information.",
CommandSourceType.Console | CommandSourceType.InGame,
AccountType.Regular
)]
public sealed class WhoAmICommand : ICommandExecutor
{
public Task ExecuteCommandAsync(CommandSystemContext context)
{
context.Print("You are connected.");
return Task.CompletedTask;
}
}
```

Example dynamic/manual registration (runtime, e.g. Lua bridge):

```csharp
commandSystemService.RegisterCommand(
"lua_ping",
context =>
{
context.Print("pong");
return Task.CompletedTask;
},
source: CommandSourceType.Console | CommandSourceType.InGame,
minimumAccountType: AccountType.Regular
);
```

Usage:

- Console: type command directly, for example `help`.
- In-game: prefix with `.` in Unicode chat, for example `.help`.

Built-in commands:

- `help|?` -> Console + InGame, `Regular`
- `lock|*` -> Console only, `Administrator`
- `exit|shutdown` -> Console only, `Administrator`
- `add_user` -> Console + InGame, `Administrator`
- `send_target` -> InGame only, `Regular`
- `orion` -> InGame only, `Regular` (opens target cursor and spawns Orion on selected location)
- `teleport|tp` -> InGame only, `GameMaster` (usage: `.teleport `)
- `add_item_backpack|.add_item_backpack` -> InGame only, `GameMaster` (usage: `.add_item_backpack `)

## Scripting

Moongate includes a Lua scripting subsystem in `src/Moongate.Scripting`, based on MoonSharp.

- `LuaScriptEngineService` handles script execution, callbacks, constants, and function invocation.
- Script modules are exposed with attributes (`[ScriptModule]`, `[ScriptFunction]`).
- Script module registration is compile-time generated (`ScriptModuleRegistry`) and invoked from bootstrap.
- `LuaScriptLoader` resolves scripts from configured script directories.
- `.luarc` metadata generation is included to improve editor tooling.

Current automated coverage includes:

- `LuaScriptLoader` file resolution and load behavior.
- `LuaScriptEngineService` constants, callbacks, module calls, error path, and naming conversions.
- `ScriptResultBuilder` success/error contract behavior.

Example script callback (for example in `/scripts/init.lua`):

```lua
function on_player_connected(p)
log.info("Toh! un player s'e' connesso")
end
```

### NPC Brain Example (`brain_loop` + `on_event`)

Mobile template:

```json
{
"type": "mobile",
"id": "orc_warrior",
"name": "an orc warrior",
"body": "0x11",
"brain": "orc_warrior"
}
```

Lua script (`/scripts/ai/orc_warrior.lua`):

```lua
function brain_loop(npc_id)
while true do
-- tactical tick sleep in milliseconds
coroutine.yield(250)
end
end

function on_event(event_type, from_serial, event_obj)
if event_type ~= "speech_heard" or event_obj == nil then
return
end

local listener_npc_id = event_obj.listener_npc_id
local text = event_obj.text
if listener_npc_id == nil or text == nil then
return
end

if string.find(string.lower(text), "hello", 1, true) then
log.info("NPC " .. tostring(listener_npc_id) .. " heard hello from " .. tostring(from_serial))
end
end
```

Notes:

- `brain` in mobile templates is treated as a brain id.
- Scripts are loaded from `moongate_data/scripts/**` (usually via `require(...)` in `init.lua`).
- `brain_loop` is resumed by the runner and can control next wake time via `coroutine.yield(ms)`.
- `on_event` is invoked with `(eventType, fromSerial, eventObject)`.
- Current event type emitted by the brain runner: `speech_heard`.
- `eventObject` contains: `listener_npc_id`, `speaker_id`, `text`, `speech_type`, `map_id`, and `location` (`x`, `y`, `z`).

### Visual Effects From Lua

Moongate now exposes visual effect helpers both on mobile proxies and as a global module:

```lua
local npc = mobile.get(0x00000030)
if npc then
npc:SetEffect(0x3728, 10, 10, 0, 0, 2023)
end

-- broadcast location effect
effect.send(1, 3613, 2585, 0, 0x3728, 10, 10, 0, 0, 2023)

-- single target effect
effect.send_to_player(0x00000022, 3613, 2585, 0, 0x3728, 10, 10, 0, 0, 5023)
```

Related runtime events:

- `MobilePlayEffectEvent` (broadcast in range)
- `PlayEffectToPlayerEvent` (single session via character id)

### Item `ScriptId` Dispatch

Items can define `scriptId` in templates and runtime entities (`UOItemEntity.ScriptId`).
`IItemScriptDispatcher` resolves `scriptId` as a Lua table and invokes hook functions on that table.

Dispatch convention:

- If `scriptId` is set and not `none`: table name is normalized `scriptId` (non-alphanumeric -> `_`, lowercase)
- If `scriptId == "none"`: fallback table resolution from item name
- First candidate: ``
- Second candidate: `items_`
- Hook names:
- `single_click` -> `on_click`
- `double_click` -> `on_double_click`

GM Lua command examples shipped today:

- `moongate_data/scripts/commands/gm/eclipse.lua` -> `.eclipse`
- `moongate_data/scripts/commands/gm/set_world_light.lua` -> `.set_world_light <0-255>`
- `moongate_data/scripts/commands/gm/teleports.lua` -> `.teleports`

Example:

- `scriptId = "items.healing-potion"`
- Lua table resolved: `items_healing_potion`
- On single click dispatcher tries: `items_healing_potion.on_click` (and aliases)

Example template:

```json
{
"type": "item",
"id": "healing_potion",
"name": "a healing potion",
"itemId": "0x0F0C",
"scriptId": "items.healing_potion"
}
```

Example Lua:

```lua
items_healing_potion = {
on_click = function(ctx)
log.info("Potion clicked, serial=" .. tostring(ctx.item.serial))
end,
on_double_click = function(ctx)
log.info("Potion double clicked by mobile=" .. tostring(ctx.mobile_id))
end
}
```

Fallback example (`scriptId = "none"` and item name `Brick`):

```lua
brick = {
on_double_click = function(ctx)
log.info("Brick double-click from session " .. tostring(ctx.session_id))
end
}
```

`ctx` payload keys:

- `hook`
- `session_id`
- `mobile_id`
- `metadata`
- `item`:
- `serial`, `script_id`, `name`, `map_id`, `item_id`, `amount`, `hue`, `location.{x,y,z}`

### Lua Gump Example

Moongate now supports two complementary gump flows:

- file-based layout table (recommended) with `gump.send_layout(...)`
- runtime fluent builder with `gump.create()` / `gump.send(...)`

File-based layout conventions:

- store gump files in `moongate_data/scripts/gumps/**.lua`
- each file returns a table with `ui` and optional `handlers`
- button click wiring is declarative: `onclick = "handler_name"`
- optional `ctx` can be passed to `gump.send_layout(...)` for text placeholders (`$ctx.name`, `$ctx.level`, ...)

Example file (`moongate_data/scripts/gumps/test_shop.lua`):

```lua
return {
ui = {
{ type = "page", index = 0 },
{ type = "background", x = 0, y = 0, gump_id = 9200, width = 320, height = 180 },
{ type = "label", x = 20, y = 20, hue = 1152, text = "Hello $ctx.name" },
{ type = "button", id = 1, x = 20, y = 130, normal_id = 4005, pressed_id = 4007, onclick = "open_next" }
},
handlers = {
open_next = function(cb_ctx)
log.info("Button clicked: " .. tostring(cb_ctx.button_id))
end
}
}
```

Usage:

```lua
local layout = require("gumps/test_shop")
local ui_ctx = { name = "Orion", level = 42 }
gump.send_layout(session_id, layout, character_id, 0xB300, 120, 80, ui_ctx)
```

Runtime builder mode remains available for dynamic/UI-generated-at-runtime scenarios.

## Scripts

Repository helper scripts in `scripts/`:

- `scripts/build_image.sh`: builds the Docker image using `docker buildx`, with options for tag, platform, push, and no-cache.
- `scripts/run_aot.sh`: publishes and runs the server with NativeAOT settings for local AOT verification.
- `scripts/run_benchmarks.sh`: runs BenchmarkDotNet benchmarks (`markdown` + `csv` exporters).
- `scripts/run_benchmarks_compare.sh`: runs side-by-side `JIT vs NativeAOT` micro-benchmark comparison and writes `BenchmarkDotNet.Artifacts/results/aot-vs-jit.md`.
- `scripts/run_benchmarks_lua.sh`: runs Lua script engine benchmarks only (JIT, MoonSharp is NativeAOT-incompatible). Accepts extra BenchmarkDotNet args.

## Benchmarks

Run locally:

```bash
./scripts/run_benchmarks.sh --filter '*'
```

Latest local snapshot (`2026-02-23`, `BenchmarkDotNet 0.14.0`, macOS `Darwin 25.3.0`, Apple `M4 Max`, `.NET 10.0.3`):

| Benchmark | Mean | Allocated |
|---|---:|---:|
| `PacketParsingBenchmark.ParseLoginSeedPacket` | `94.82 ns` | `664 B` |
| `PacketSerializationBenchmark.WriteServerListPacket` | `64.19 ns` | `128 B` |
| `PacketStreamParsingBenchmark.ParseMixedPacketStreamInChunks` | `24.25 us` | `56 KB` |
| `PacketDispatchBenchmark.DispatchToThreeListeners` | `68.21 ns` | `296 B` |
| `PacketDispatchBenchmark.DispatchWithoutListeners` | `8.99 ns` | `64 B` |
| `NetworkCompressionBenchmark.Compress256Bytes` | `220.76 ns` | `-` |
| `NetworkCompressionBenchmark.CompressAndDecompress1024Bytes` | `60.03 us` | `48.10 KB` |
| `NetworkCompressionBenchmark.CompressionMiddlewareProcessSend1024Bytes` | `908.72 ns` | `1.48 KB` |
| `QueueThroughputBenchmark.OutgoingQueueEnqueueThenDrain` | `24.309 us` | `-` |
| `QueueThroughputBenchmark.MessageBusPublishThenDrain` | `9.725 us` | `-` |
| `TimerWheelBenchmark.UpdateTicksDelta` | `2.893 us` | `4.05 KB` |

### Gameplay Hot-Path Benchmarks

Run only the new gameplay-focused suites:

```bash
dotnet run -c Release --project benchmarks/Moongate.Benchmarks/Moongate.Benchmarks.csproj -- \
--filter '*SpatialWorldServiceBenchmark*' '*ItemServiceBenchmark*' '*PacketGameplayHotPathBenchmark*'
```

Latest quick snapshot (`2026-03-02`, `BenchmarkDotNet 0.15.8`, macOS `Darwin 25.3.0`, Apple `M4 Max`, `.NET 10.0.3`, quick config `Launch=1/Warmup=1/Iteration=1`):

| Benchmark | Mean | Allocated |
|---|---:|---:|
| `SpatialWorldServiceBenchmark.AddOrUpdateMobiles (500)` | `75.939 us` | `74.56 KB` |
| `SpatialWorldServiceBenchmark.MoveMobilesAcrossSectors (500)` | `27.548 us` | `117.53 KB` |
| `SpatialWorldServiceBenchmark.GetPlayersInHotSector (500)` | `1.769 us` | `6.16 KB` |
| `SpatialWorldServiceBenchmark.AddOrUpdateMobiles (2000)` | `325.353 us` | `297.27 KB` |
| `SpatialWorldServiceBenchmark.MoveMobilesAcrossSectors (2000)` | `105.423 us` | `469.15 KB` |
| `SpatialWorldServiceBenchmark.GetPlayersInHotSector (2000)` | `1.745 us` | `6.16 KB` |
| `ItemServiceBenchmark.MoveItemBetweenContainers` | `359.772 ns` | `1.85 KB` |
| `ItemServiceBenchmark.DropItemToGroundFromContainer` | `489.566 ns` | `2.25 KB` |
| `PacketGameplayHotPathBenchmark.ParseMoveRequestPacket` | `8.930 ns` | `32 B` |
| `PacketGameplayHotPathBenchmark.ParsePickUpItemPacket` | `8.620 ns` | `32 B` |
| `PacketGameplayHotPathBenchmark.ParseDropItemPacket` | `11.192 ns` | `48 B` |
| `PacketGameplayHotPathBenchmark.ParseDropWearItemPacket` | `8.955 ns` | `32 B` |
| `PacketGameplayHotPathBenchmark.ParseMixedGameplayPacketBurst` | `10.956 ns` | `36 B` |
| `PacketGameplayHotPathBenchmark.WriteObjectInformationPacket` | `63.047 ns` | `-` |
| `PacketGameplayHotPathBenchmark.WriteDraggingOfItemPacket` | `51.664 ns` | `-` |

Notes:

- This snapshot is intended for fast regression checks, not for publication-grade comparisons.
- Use default/full BenchmarkDotNet settings for release notes and long-term trend baselines.

### Lua Script Engine

Run locally:

```bash
./scripts/run_benchmarks_lua.sh
```

> Note: MoonSharp relies on reflection and dynamic code generation — NativeAOT is not supported for this suite.

Latest local snapshot (`2026-02-25`, `BenchmarkDotNet 0.15.8`, macOS `Darwin 25.3.0`, Apple `M4 Max`, `.NET 10.0`):

| Benchmark | Mean | Allocated |
|---|---:|---:|
| `LuaScriptEngineBenchmark.ExecuteSimpleScriptCached` | `328.87 ns` | `800 B` |
| `LuaScriptEngineBenchmark.ExecuteLoopScriptCached` | `5.68 us` | `19.67 KB` |
| `LuaScriptEngineBenchmark.ExecuteSimpleScriptUncached` | `6.28 us` | `6.12 KB` |
| `LuaScriptEngineBenchmark.CallFunctionNoArgs` | `49.22 ns` | `256 B` |
| `LuaScriptEngineBenchmark.CallFunctionWithArgs` | `135.40 ns` | `864 B` |

Generated reports are stored in:

- `BenchmarkDotNet.Artifacts/results/*.md`
- `BenchmarkDotNet.Artifacts/results/*.csv`

### AOT vs JIT

Run side-by-side comparison:

```bash
./scripts/run_benchmarks_compare.sh
```

Latest comparison snapshot (`2026-02-23`, `net10.0`, Apple `M4 Max`, `osx-arm64`):

| Benchmark | JIT Mean | AOT Mean | Speedup (JIT/AOT) |
|---|---:|---:|---:|
| `Compress256Bytes` | `934.48 ns` | `319.04 ns` | `2.93x` |
| `CompressAndDecompress1024Bytes` | `59.60 us` | `102.20 us` | `0.58x` |
| `CompressionMiddlewareProcessSend1024Bytes` | `974.86 ns` | `1.34 us` | `0.73x` |
| `ParseLoginSeedPacket` | `360.97 ns` | `71.66 ns` | `5.04x` |
| `ParseMixedPacketStreamInChunks` | `26.10 us` | `37.71 us` | `0.69x` |
| `WriteServerListPacket` | `585.93 ns` | `98.31 ns` | `5.96x` |

Detailed report:

- `BenchmarkDotNet.Artifacts/results/aot-vs-jit.md`

## Stress Test (Socket UO, Black-Box)

Use the dedicated stress runner to validate server stability with real UO socket clients.

Scenario target (default):

- `100` concurrent clients
- `300s` duration
- account bootstrap via HTTP users API
- login + enter world + continuous movement loop
- SLO checks:
- login success rate `>= 99%`
- unexpected disconnects `= 0`
- movement ACK p95 `< 200ms`

Run:

```bash
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
```

When JWT protection is enabled on `/api/users`, provide admin credentials:

```bash
dotnet run --project tools/Moongate.Stress -- \
--admin-username admin --admin-password your_password
```

Output:

- console summary with pass/fail and SLO violations
- JSON report at `artifacts/stress/latest.json`

## Docker

Build the image:

```bash
./scripts/build_image.sh -t moongate-server:local
```

Run the container:

```bash
docker run --rm -it \
-p 2593:2593 \
-p 8088:8088 \
-v /path/host/moongate-root:/app \
-v /path/host/uo-client:/uo:ro \
--name moongate \
moongate-server:local
```

The Docker image publishes a NativeAOT binary and runs it on Alpine (`linux-musl` runtime).
It also builds the frontend in `ui/` and serves it from `/` via the HTTP service.
Container defaults:

- `MOONGATE_ROOT_DIRECTORY=/app`
- `MOONGATE_UO_DIRECTORY=/uo`
- `MOONGATE_UI_DIST=/opt/moongate/ui/dist`

`/path/host/uo-client` must contain required UO client files (e.g. `client.exe`).

Console behavior in Docker:

- Run with `-it` to enable the interactive prompt UI (`moongate>`).
- Without TTY (`-it` omitted), logs still work but prompt interaction is disabled.

## Docker Monitoring Stack

The repository includes a complete monitoring stack under `stack/`:

- Moongate server container
- Prometheus scraping `http://moongate:8088/metrics`
- Grafana with pre-provisioned datasource and dashboard

Quick start:

```bash
cd stack
docker compose up -d --build
```

Useful endpoints:

- Grafana: `http://localhost:3000`
- Prometheus: `http://localhost:9090`
- Moongate metrics: `http://localhost:8088/metrics`

For full setup details, volumes, troubleshooting, and dashboard notes, see `stack/README.md`.

## Documentation

Project documentation is in `docs/`.
Published documentation is available at:

- https://moongate-community.github.io/moongatev2/

- Docs home: `docs/Home.md`
- Development plan: `docs/plans/moongate-v2-development-plan.md`
- Current status snapshot: `docs/plans/status-2026-02-19.md`
- Sprint tracking: `docs/sprints/sprint-001.md`
- Sprint closeout: `docs/sprints/sprint-001-closeout-2026-02-18.md`
- Protocol notes index: `docs/protocol/README.md`

## Development Notes

- Shared build/analyzer/version settings are centralized in `Directory.Build.props`.
- Current global version baseline: `0.17.0`.
- CI validates build/tests/coverage/quality/security; release and Docker image publishing run through dedicated workflows.

## Contributing

We welcome contributions. Please fork the repository and submit pull requests with your changes.
Make sure code follows the project coding standards and includes appropriate tests.

## License

This project is licensed under the GNU General Public License v3.0 (GPL-3.0).
See `LICENSE` for details.