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. · GitHub

✨ Check out this trending post from Hacker News 📖

📂 **Category**:

✅ **What You’ll Learn**:

Moongate logo

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

CI
Tests
Coverage
Code Quality
Quality Gate
Security
Latest Release
Latest Pre-release
Docs
Release
Docker Image
Docker Pulls
Docker Image Size

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:

Moongate is not a clone of ModernUO, RunUO, ServUO or any other emulator, 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.

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

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.

  • 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.

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

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

UI Screen 1
UI Screen 2
UI Screen 3

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

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).

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 emulator 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!! 😀 😀

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.

  • 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
  • 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).

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:

{
  "email": {
    "isEnabled": false,
    "fromAddress": "noreply@localhost",
    "fallbackLocale": "en",
    "smtp": 🔥
  }
}

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:

{
  "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:

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

Example mobile template:

{
  "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)
  • 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/: Obsidian knowledge base (plans, sprints, protocol notes, journal).

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.

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:

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.");
            }
        );
    }
}

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:
export MOONGATE_ROOT_DIRECTORY="$HOME/moongate"
export MOONGATE_UO_DIRECTORY="/path/to/uo-client"
  1. Restore/build/test:
dotnet restore
dotnet build
dotnet test
  1. Start server:
dotnet run --project src/Moongate.Server
  1. 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.
  2. Optional admin credentials override:

export MOONGATE_ADMIN_USERNAME="admin"
export MOONGATE_ADMIN_PASSWORD="change-me-now"
  1. 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)
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"
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}

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):

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):

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 )

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):

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

NPC Brain Example (brain_loop + on_event)

Mobile template:

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

Lua script (/scripts/ai/orc_warrior.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).

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

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)

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:

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

Example 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):

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}

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):

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:

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.

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.

Run locally:

./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:

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.

Run locally:

./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

Run side-by-side comparison:

./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:

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:

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

Build the image:

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

Run the container:

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.

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:

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.

Project documentation (Obsidian vault) 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

  • 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.

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.

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

{💬|⚡|🔥} **What’s your take?**
Share your thoughts in the comments below!

#️⃣ **#moongatecommunitymoongatev2 #Moongate #modern #Ultima #Online #server #emulator #built #scratch #AOT #compilation #high #performance #nostalgic #gameplay #experience #GitHub**

🕒 **Posted on**: 1772811864

🌟 **Want more?** Click here for more info! 🌟

By

Leave a Reply

Your email address will not be published. Required fields are marked *