From 955d2d538098c92ecd2e830b1bc25a56abb71ae0 Mon Sep 17 00:00:00 2001 From: Nick Cipollina Date: Mon, 20 Jul 2026 12:13:14 -0400 Subject: [PATCH 01/14] feat(hosting): add SharpMud.Hosting, split src/SharpMud.Host per PLAN-0006 New SharpMud.Hosting package: SharpMudApplicationBuilder/SharpMudApplication wrapping HostApplicationBuilder/IHost, WorldContext/IWorldBuilder/ WorldLoaderHostedService (resolves the previously-open "world-builder registration point" question - a consumer-supplied IWorldBuilder loads a persisted world or builds+saves a fresh one during startup, before any transport starts accepting connections), GameLoopHostedService (wraps GameLoop as a BackgroundService rather than giving GameLoop itself a Hosting dependency, and auto-registers every DI-resolved ITickable), ShutdownSaveHostedService (the previously-unowned shutdown-time whole-world save), IPlayerFactory (lets LoginFlow/PlayerLogin stay ruleset-agnostic without blowing the 4-parameter limit - they're now constructor-injected services instead of static classes taking a 5th parameter), and AddSharpMudRuleset (registers BuiltinCommands before a consumer's ruleset callback runs). SessionLoop/LoginFlow/PlayerLogin converted from static classes taking World/parser/registry/repository as method parameters to constructor-injected service classes - SessionLoop alone was already past coding-standards.md's 4-parameter limit before this change. src/SharpMud.Host deleted. HostOptions trimmed to DbPath only (transport selection moves to the consumer's own Program.cs, not yet updated in this commit). HostRunner.cs moved into SharpMud.Adapters.Telnet as a starting point for AddSharpMudTelnetTransport (not yet rewritten). Ruleset.Classic + Program.cs/appsettings.json/HubWorldBuilder.cs moved into samples/SharpMud.Samples.Classic (csproj/Program.cs rewrite not yet done - next commit). Test coverage: SessionLoopTests/LoginFlowTests/HostOptionsTests/ PasswordHashingTests moved and updated for the new constructor-injected shape, plus new PlayerLoginTests. 16/16 passing. Per PLAN-0006 (docs/adr/0006-nuget-package-distribution.md). Co-Authored-By: Claude Sonnet 5 --- Directory.Packages.props | 9 ++ SharpMud.slnx | 10 ++- docs/plans/0006-nuget-package-distribution.md | 2 +- .../AttackCommand.cs | 0 .../CharacterClass.cs | 0 .../ClassicBehaviorMappingContributor.cs | 0 .../ClassicCommands.cs | 0 .../CombatManager.cs | 0 .../CombatResolver.cs | 0 .../CombatantBehavior.cs | 0 .../CombatantBehaviorConfiguration.cs | 0 .../StatsBehaviorConfiguration.cs | 0 .../SharpMud.Samples.Classic}/FleeCommand.cs | 0 .../HubWorldBuilder.cs | 0 .../ICombatManager.cs | 0 .../ICombatResolver.cs | 0 .../SharpMud.Samples.Classic}/Program.cs | 0 .../SharpMud.Samples.Classic}/Race.cs | 0 .../SharpMud.Samples.Classic.csproj | 0 .../StatsBehavior.cs | 0 .../appsettings.json | 0 .../HostRunner.cs | 0 src/SharpMud.Host/HostOptions.cs | 23 ----- src/SharpMud.Host/SharpMud.Host.csproj | 36 -------- src/SharpMud.Host/TelnetHostContext.cs | 22 ----- src/SharpMud.Hosting/GameLoopHostedService.cs | 32 +++++++ src/SharpMud.Hosting/HostOptions.cs | 17 ++++ src/SharpMud.Hosting/IPlayerFactory.cs | 15 ++++ src/SharpMud.Hosting/IWorldBuilder.cs | 23 +++++ .../LoginFlow.cs | 44 ++++++---- .../PasswordHashing.cs | 4 +- .../PlayerLogin.cs | 40 +++++---- .../ServiceCollectionExtensions.cs | 83 +++++++++++++++++++ .../SessionLoop.cs | 43 ++++++---- src/SharpMud.Hosting/SharpMud.Hosting.csproj | 33 ++++++++ src/SharpMud.Hosting/SharpMudApplication.cs | 37 +++++++++ .../SharpMudApplicationBuilder.cs | 52 ++++++++++++ .../ShutdownSaveHostedService.cs | 43 ++++++++++ src/SharpMud.Hosting/WorldContext.cs | 42 ++++++++++ .../WorldLoaderHostedService.cs | 54 ++++++++++++ tests/SharpMud.Host.Tests/HostOptionsTests.cs | 79 ------------------ .../HostOptionsTests.cs | 24 ++++++ .../LoginFlowTests.cs | 29 +++++-- .../PasswordHashingTests.cs | 4 +- .../PlayerLoginTests.cs | 80 ++++++++++++++++++ .../SessionLoopTests.cs | 31 ++++--- .../SharpMud.Hosting.Tests.csproj} | 4 +- .../xunit.runner.json | 0 .../Combat/CombatManagerTests.cs | 0 .../Combat/CombatResolverTests.cs | 0 .../SharpMud.Samples.Classic.Tests.csproj} | 0 .../Attributes/ClassicAutoDataAttribute.cs | 0 .../TestKit/BaseFixtureFactory.cs | 0 .../xunit.runner.json | 0 54 files changed, 677 insertions(+), 238 deletions(-) rename {src/SharpMud.Ruleset.Classic => samples/SharpMud.Samples.Classic}/AttackCommand.cs (100%) rename {src/SharpMud.Ruleset.Classic => samples/SharpMud.Samples.Classic}/CharacterClass.cs (100%) rename {src/SharpMud.Ruleset.Classic => samples/SharpMud.Samples.Classic}/ClassicBehaviorMappingContributor.cs (100%) rename {src/SharpMud.Ruleset.Classic => samples/SharpMud.Samples.Classic}/ClassicCommands.cs (100%) rename {src/SharpMud.Ruleset.Classic => samples/SharpMud.Samples.Classic}/CombatManager.cs (100%) rename {src/SharpMud.Ruleset.Classic => samples/SharpMud.Samples.Classic}/CombatResolver.cs (100%) rename {src/SharpMud.Ruleset.Classic => samples/SharpMud.Samples.Classic}/CombatantBehavior.cs (100%) rename {src/SharpMud.Ruleset.Classic => samples/SharpMud.Samples.Classic}/Configurations/CombatantBehaviorConfiguration.cs (100%) rename {src/SharpMud.Ruleset.Classic => samples/SharpMud.Samples.Classic}/Configurations/StatsBehaviorConfiguration.cs (100%) rename {src/SharpMud.Ruleset.Classic => samples/SharpMud.Samples.Classic}/FleeCommand.cs (100%) rename {src/SharpMud.Host => samples/SharpMud.Samples.Classic}/HubWorldBuilder.cs (100%) rename {src/SharpMud.Ruleset.Classic => samples/SharpMud.Samples.Classic}/ICombatManager.cs (100%) rename {src/SharpMud.Ruleset.Classic => samples/SharpMud.Samples.Classic}/ICombatResolver.cs (100%) rename {src/SharpMud.Host => samples/SharpMud.Samples.Classic}/Program.cs (100%) rename {src/SharpMud.Ruleset.Classic => samples/SharpMud.Samples.Classic}/Race.cs (100%) rename src/SharpMud.Ruleset.Classic/SharpMud.Ruleset.Classic.csproj => samples/SharpMud.Samples.Classic/SharpMud.Samples.Classic.csproj (100%) rename {src/SharpMud.Ruleset.Classic => samples/SharpMud.Samples.Classic}/StatsBehavior.cs (100%) rename {src/SharpMud.Host => samples/SharpMud.Samples.Classic}/appsettings.json (100%) rename src/{SharpMud.Host => SharpMud.Adapters.Telnet}/HostRunner.cs (100%) delete mode 100644 src/SharpMud.Host/HostOptions.cs delete mode 100644 src/SharpMud.Host/SharpMud.Host.csproj delete mode 100644 src/SharpMud.Host/TelnetHostContext.cs create mode 100644 src/SharpMud.Hosting/GameLoopHostedService.cs create mode 100644 src/SharpMud.Hosting/HostOptions.cs create mode 100644 src/SharpMud.Hosting/IPlayerFactory.cs create mode 100644 src/SharpMud.Hosting/IWorldBuilder.cs rename src/{SharpMud.Host => SharpMud.Hosting}/LoginFlow.cs (79%) rename src/{SharpMud.Host => SharpMud.Hosting}/PasswordHashing.cs (88%) rename src/{SharpMud.Host => SharpMud.Hosting}/PlayerLogin.cs (64%) create mode 100644 src/SharpMud.Hosting/ServiceCollectionExtensions.cs rename src/{SharpMud.Host => SharpMud.Hosting}/SessionLoop.cs (79%) create mode 100644 src/SharpMud.Hosting/SharpMud.Hosting.csproj create mode 100644 src/SharpMud.Hosting/SharpMudApplication.cs create mode 100644 src/SharpMud.Hosting/SharpMudApplicationBuilder.cs create mode 100644 src/SharpMud.Hosting/ShutdownSaveHostedService.cs create mode 100644 src/SharpMud.Hosting/WorldContext.cs create mode 100644 src/SharpMud.Hosting/WorldLoaderHostedService.cs delete mode 100644 tests/SharpMud.Host.Tests/HostOptionsTests.cs create mode 100644 tests/SharpMud.Hosting.Tests/HostOptionsTests.cs rename tests/{SharpMud.Host.Tests => SharpMud.Hosting.Tests}/LoginFlowTests.cs (82%) rename tests/{SharpMud.Host.Tests => SharpMud.Hosting.Tests}/PasswordHashingTests.cs (94%) create mode 100644 tests/SharpMud.Hosting.Tests/PlayerLoginTests.cs rename tests/{SharpMud.Host.Tests => SharpMud.Hosting.Tests}/SessionLoopTests.cs (76%) rename tests/{SharpMud.Host.Tests/SharpMud.Host.Tests.csproj => SharpMud.Hosting.Tests/SharpMud.Hosting.Tests.csproj} (89%) rename tests/{SharpMud.Host.Tests => SharpMud.Hosting.Tests}/xunit.runner.json (100%) rename tests/{SharpMud.Ruleset.Classic.Tests => SharpMud.Samples.Classic.Tests}/Combat/CombatManagerTests.cs (100%) rename tests/{SharpMud.Ruleset.Classic.Tests => SharpMud.Samples.Classic.Tests}/Combat/CombatResolverTests.cs (100%) rename tests/{SharpMud.Ruleset.Classic.Tests/SharpMud.Ruleset.Classic.Tests.csproj => SharpMud.Samples.Classic.Tests/SharpMud.Samples.Classic.Tests.csproj} (100%) rename tests/{SharpMud.Ruleset.Classic.Tests => SharpMud.Samples.Classic.Tests}/TestKit/Attributes/ClassicAutoDataAttribute.cs (100%) rename tests/{SharpMud.Ruleset.Classic.Tests => SharpMud.Samples.Classic.Tests}/TestKit/BaseFixtureFactory.cs (100%) rename tests/{SharpMud.Ruleset.Classic.Tests => SharpMud.Samples.Classic.Tests}/xunit.runner.json (100%) diff --git a/Directory.Packages.props b/Directory.Packages.props index 4da05df..55d8336 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -13,6 +13,15 @@ + + + + + + - - - - - Exe - net11.0 - enable - enable - - - diff --git a/src/SharpMud.Host/TelnetHostContext.cs b/src/SharpMud.Host/TelnetHostContext.cs deleted file mode 100644 index cd5f30f..0000000 --- a/src/SharpMud.Host/TelnetHostContext.cs +++ /dev/null @@ -1,22 +0,0 @@ -using Microsoft.Extensions.Logging; -using SharpMud.Adapters.Telnet; -using SharpMud.Engine.Commands; -using SharpMud.Engine.Core; - -namespace SharpMud.Host; - -/// -/// Groups the dependencies needs - -/// a parameter object rather than a growing parameter list, per -/// .agents/skills/engineering-workflow/references/coding-standards.md's -/// 4-parameter rule (see ADR-0002, docs/adr/0002-telnet-protocol-negotiation.md, -/// for why this was introduced now). -/// -public sealed record TelnetHostContext( - World World, - ICommandParser Parser, - ICommandRegistry Registry, - IThingRepository Repository, - Thing StartingRoom, - int Port, - ILogger Logger); diff --git a/src/SharpMud.Hosting/GameLoopHostedService.cs b/src/SharpMud.Hosting/GameLoopHostedService.cs new file mode 100644 index 0000000..3741f83 --- /dev/null +++ b/src/SharpMud.Hosting/GameLoopHostedService.cs @@ -0,0 +1,32 @@ +using Microsoft.Extensions.Hosting; +using SharpMud.Engine.Ticking; + +namespace SharpMud.Hosting; + +// Runs GameLoop as a BackgroundService rather than GameLoop itself taking a +// Microsoft.Extensions.Hosting dependency - GameLoop stays engine-agnostic +// of hosting concerns, matching the no-Ruleset/no-Hosting-reference rule +// for SharpMud.Engine (docs/engine-vs-ruleset.md). Registers every +// DI-resolved ITickable at startup instead of a dedicated registration +// callback - a consumer just does +// services.AddSingleton() for their own +// ruleset-specific tickables (docs/adr/0006-nuget-package-distribution.md). +internal sealed class GameLoopHostedService : BackgroundService +{ + private readonly IGameLoop _gameLoop; + private readonly IEnumerable _tickables; + + public GameLoopHostedService(IGameLoop gameLoop, IEnumerable tickables) + { + _gameLoop = gameLoop; + _tickables = tickables; + } + + protected override Task ExecuteAsync(CancellationToken stoppingToken) + { + foreach (var tickable in _tickables) + _gameLoop.Register(tickable); + + return _gameLoop.RunAsync(stoppingToken); + } +} diff --git a/src/SharpMud.Hosting/HostOptions.cs b/src/SharpMud.Hosting/HostOptions.cs new file mode 100644 index 0000000..7c715e6 --- /dev/null +++ b/src/SharpMud.Hosting/HostOptions.cs @@ -0,0 +1,17 @@ +namespace SharpMud.Hosting; + +// Deployment config read from environment variables - stays manually +// parsed rather than IOptions-bound, per security.md's reasoning for +// keeping deployment config env-var-only. Transport selection +// (SHARPMUD_MODE/SHARPMUD_TELNET_PORT/--telnet) is not part of this type - +// it moved to the consumer's own composition root, since Hosting must not +// know which transport(s) a consumer wants (docs/adr/0006-nuget-package-distribution.md). +public sealed record HostOptions(string DbPath) +{ + public static HostOptions Parse(IReadOnlyDictionary env) + { + var dbPath = env.GetValueOrDefault("SHARPMUD_DB_PATH") ?? "./sharpmud.db"; + + return new HostOptions(dbPath); + } +} diff --git a/src/SharpMud.Hosting/IPlayerFactory.cs b/src/SharpMud.Hosting/IPlayerFactory.cs new file mode 100644 index 0000000..283e168 --- /dev/null +++ b/src/SharpMud.Hosting/IPlayerFactory.cs @@ -0,0 +1,15 @@ +using SharpMud.Engine.Core; + +namespace SharpMud.Hosting; + +/// +/// Creates a fresh player at character creation - +/// registered once via DI (services.AddSingleton<IPlayerFactory, +/// MyPlayerFactory>()) so / +/// stay ruleset-agnostic instead of calling a +/// specific ruleset's character-creation logic directly. +/// +public interface IPlayerFactory +{ + Thing CreatePlayer(World world, string username, string passwordHash, Thing startingRoom); +} diff --git a/src/SharpMud.Hosting/IWorldBuilder.cs b/src/SharpMud.Hosting/IWorldBuilder.cs new file mode 100644 index 0000000..e28d51f --- /dev/null +++ b/src/SharpMud.Hosting/IWorldBuilder.cs @@ -0,0 +1,23 @@ +using SharpMud.Engine.Core; + +namespace SharpMud.Hosting; + +/// +/// A consumer's world content and bootstrap logic - the ruleset-agnostic +/// generic-host equivalent of what today's HubWorldBuilder is for +/// the Classic ruleset. Registered once via DI +/// (services.AddSingleton<IWorldBuilder, MyWorldBuilder>()); +/// calls it during startup to load a +/// persisted world or build a fresh one. +/// +public interface IWorldBuilder +{ + /// The root 's fixed id - used to look up a previously-persisted world. + ThingId RootId { get; } + + /// Builds a brand-new world when nothing is persisted yet, returning the world and the room new players start in. + (World World, Thing StartingRoom) Build(); + + /// Locates the starting room within a world reloaded from persistence. + Thing FindStartingRoom(Thing root); +} diff --git a/src/SharpMud.Host/LoginFlow.cs b/src/SharpMud.Hosting/LoginFlow.cs similarity index 79% rename from src/SharpMud.Host/LoginFlow.cs rename to src/SharpMud.Hosting/LoginFlow.cs index e0dd1d5..fd1d991 100644 --- a/src/SharpMud.Host/LoginFlow.cs +++ b/src/SharpMud.Hosting/LoginFlow.cs @@ -2,23 +2,33 @@ using SharpMud.Engine.Core; using SharpMud.Engine.Sessions; -namespace SharpMud.Host; +namespace SharpMud.Hosting; -// Classic MUD login prompt (docs/accounts-auth.md) - only used by networked -// transports (Telnet now, SSH/WebSocket later). Local CLI stays login-free -// per SPEC.md and never calls this. -public static class LoginFlow +// Classic MUD login prompt (docs/accounts-auth.md) - used by every +// networked transport (Telnet, SSH/WebSocket later); local CLI stays +// login-free per SPEC.md and never calls this. +public sealed class LoginFlow { // Not yet tuned - docs/accounts-auth.md Open Items flags the exact // retry/lockout policy as still undecided; this is a concrete default, // not a considered final answer. private const int MaxPasswordAttempts = 3; + private readonly WorldContext _worldContext; + private readonly IThingRepository _repository; + private readonly IPlayerFactory _playerFactory; + + public LoginFlow(WorldContext worldContext, IThingRepository repository, IPlayerFactory playerFactory) + { + _worldContext = worldContext; + _repository = repository; + _playerFactory = playerFactory; + } + // Returns null if the connection should be dropped (empty input, // disconnect mid-flow) - a failed login attempt on its own loops back to // the username prompt rather than dropping the connection. - public static async Task RunAsync( - ISession session, World world, IThingRepository repository, Thing startingRoom, CancellationToken ct) + public async Task RunAsync(ISession session, CancellationToken ct) { while (true) { @@ -27,11 +37,11 @@ public static class LoginFlow if (string.IsNullOrWhiteSpace(username)) return null; - var existing = await FindAndAttachExistingAsync(world, repository, username, startingRoom, ct); + var existing = await FindAndAttachExistingAsync(username, ct); var player = existing is not null ? await LoginExistingAsync(session, existing, ct) - : await MaybeCreateAsync(session, world, username, startingRoom, repository, ct); + : await MaybeCreateAsync(session, username, ct); if (player is not null) return player; @@ -41,15 +51,16 @@ public static class LoginFlow } } - private static async Task FindAndAttachExistingAsync( - World world, IThingRepository repository, string username, Thing startingRoom, CancellationToken ct) + private async Task FindAndAttachExistingAsync(string username, CancellationToken ct) { + var world = _worldContext.World; + var alreadyLive = world.AllWithBehavior() .FirstOrDefault(p => p.FindBehavior()!.Username == username); if (alreadyLive is not null) return alreadyLive; - var loaded = await repository.FindPlayerByUsernameAsync(username, ct); + var loaded = await _repository.FindPlayerByUsernameAsync(username, ct); if (loaded is null) return null; @@ -57,7 +68,7 @@ public static class LoginFlow // this DB call, not the live room other players are actually in - // attach into the real live room instead (falls back to the hub if // that room no longer exists). See docs/persistence.md. - var liveRoom = loaded.Parent is { } lastRoom ? world.GetThing(lastRoom.Id) ?? startingRoom : startingRoom; + var liveRoom = loaded.Parent is { } lastRoom ? world.GetThing(lastRoom.Id) ?? _worldContext.StartingRoom : _worldContext.StartingRoom; liveRoom.Add(loaded); PlayerLogin.RegisterSubtree(world, loaded); return loaded; @@ -122,8 +133,7 @@ public static class LoginFlow return null; } - private static async Task MaybeCreateAsync( - ISession session, World world, string username, Thing startingRoom, IThingRepository repository, CancellationToken ct) + private async Task MaybeCreateAsync(ISession session, string username, CancellationToken ct) { await session.WriteAsync("Create a new character? (y/n) ", ct); var confirm = (await session.ReadLineAsync(ct))?.Trim(); @@ -144,12 +154,12 @@ public static class LoginFlow return null; } - var player = HubWorldBuilder.CreatePlayer(world, username, PasswordHashing.Hash(password), startingRoom); + var player = _playerFactory.CreatePlayer(_worldContext.World, username, PasswordHashing.Hash(password), _worldContext.StartingRoom); // Persist immediately, not just on the eventual disconnect-triggered // save - a crash before this player's first disconnect shouldn't // lose a freshly created login. - await repository.SaveTreeAsync(player, ct); + await _repository.SaveTreeAsync(player, ct); return player; } diff --git a/src/SharpMud.Host/PasswordHashing.cs b/src/SharpMud.Hosting/PasswordHashing.cs similarity index 88% rename from src/SharpMud.Host/PasswordHashing.cs rename to src/SharpMud.Hosting/PasswordHashing.cs index 55254ef..c1fb64a 100644 --- a/src/SharpMud.Host/PasswordHashing.cs +++ b/src/SharpMud.Hosting/PasswordHashing.cs @@ -1,9 +1,9 @@ using Microsoft.AspNetCore.Identity; -namespace SharpMud.Host; +namespace SharpMud.Hosting; // Wraps Microsoft.Extensions.Identity.Core's PasswordHasher - PBKDF2 -// with a random salt, versioned hash format. Lives in Host, not Engine - +// with a random salt, versioned hash format. Lives in Hosting, not Engine - // this is login-flow infrastructure, not game logic, and pulling in an // Identity package into Engine would be an unwarranted dependency for // something only the networked login flow needs (docs/accounts-auth.md). diff --git a/src/SharpMud.Host/PlayerLogin.cs b/src/SharpMud.Hosting/PlayerLogin.cs similarity index 64% rename from src/SharpMud.Host/PlayerLogin.cs rename to src/SharpMud.Hosting/PlayerLogin.cs index 8148497..250f5fc 100644 --- a/src/SharpMud.Host/PlayerLogin.cs +++ b/src/SharpMud.Hosting/PlayerLogin.cs @@ -1,44 +1,56 @@ using SharpMud.Engine.Behaviors; using SharpMud.Engine.Core; -namespace SharpMud.Host; +namespace SharpMud.Hosting; // Local CLI only - reconnects a persisted character if one exists for this // name, otherwise creates one fresh, with no password check at all. CLI // stays login-free per SPEC.md; Telnet's real username/password prompt is // LoginFlow, not this (docs/accounts-auth.md). -public static class PlayerLogin +public sealed class PlayerLogin { // Never actually checked (CLI has no login) - a fixed placeholder so // PlayerBehavior.PasswordHash (required) has something valid-shaped in // it, rather than leaving a local dev character's hash empty/guessable. private static readonly string LocalCliPasswordHash = PasswordHashing.Hash(Guid.NewGuid().ToString()); - public static async Task ResolveOrCreateAsync( - World world, IThingRepository repository, string name, Thing startingRoom, CancellationToken ct) + private readonly WorldContext _worldContext; + private readonly IThingRepository _repository; + private readonly IPlayerFactory _playerFactory; + + public PlayerLogin(WorldContext worldContext, IThingRepository repository, IPlayerFactory playerFactory) + { + _worldContext = worldContext; + _repository = repository; + _playerFactory = playerFactory; + } + + public static void RegisterSubtree(World world, Thing thing) { + world.Register(thing); + foreach (var child in thing.Children) + RegisterSubtree(world, child); + } + + public async Task ResolveOrCreateAsync(string name, CancellationToken ct) + { + var world = _worldContext.World; + var alreadyOnline = world.AllWithBehavior().FirstOrDefault(p => p.Name == name); if (alreadyOnline is not null) return alreadyOnline; - var loaded = await repository.FindPlayerByUsernameAsync(name, ct); + var loaded = await _repository.FindPlayerByUsernameAsync(name, ct); if (loaded is null) - return HubWorldBuilder.CreatePlayer(world, name, LocalCliPasswordHash, startingRoom); + return _playerFactory.CreatePlayer(world, name, LocalCliPasswordHash, _worldContext.StartingRoom); // loaded.Parent is a freshly-reconstructed standalone Thing from // this DB call, not the live room other players are actually in - // attach into the real live room instead (falls back to the hub if // that room no longer exists). - var liveRoom = loaded.Parent is { } lastRoom ? world.GetThing(lastRoom.Id) ?? startingRoom : startingRoom; + var liveRoom = loaded.Parent is { } lastRoom ? world.GetThing(lastRoom.Id) ?? _worldContext.StartingRoom : _worldContext.StartingRoom; liveRoom.Add(loaded); RegisterSubtree(world, loaded); return loaded; } - - public static void RegisterSubtree(World world, Thing thing) - { - world.Register(thing); - foreach (var child in thing.Children) - RegisterSubtree(world, child); - } } diff --git a/src/SharpMud.Hosting/ServiceCollectionExtensions.cs b/src/SharpMud.Hosting/ServiceCollectionExtensions.cs new file mode 100644 index 0000000..0b108b2 --- /dev/null +++ b/src/SharpMud.Hosting/ServiceCollectionExtensions.cs @@ -0,0 +1,83 @@ +using Microsoft.Extensions.DependencyInjection; +using SharpMud.Engine.Behaviors; +using SharpMud.Engine.Commands; +using SharpMud.Engine.Commands.Builtin; +using SharpMud.Engine.Core; +using SharpMud.Engine.Sessions; +using SharpMud.Engine.Ticking; + +namespace SharpMud.Hosting; + +public static class ServiceCollectionExtensions +{ + /// + /// Registers a consumer's ruleset commands, plus every engine-level + /// built-in command (look/move/quit/inventory, + /// etc.) - a consumer's callback + /// only ever needs to add their own ruleset's commands on top, never + /// has to remember to call BuiltinCommands.RegisterAll itself. + /// + public static IServiceCollection AddSharpMudRuleset(this IServiceCollection services, Action registerRuleset) + { + ArgumentNullException.ThrowIfNull(registerRuleset); + + services.AddSingleton(_ => + { + var registry = new CommandRegistry(); + BuiltinCommands.RegisterAll(registry); + registerRuleset(registry); + return registry; + }); + + return services; + } + + /// Registers a consumer's world content/bootstrap logic - see . + public static IServiceCollection AddSharpMudWorld(this IServiceCollection services) + where TWorldBuilder : class, IWorldBuilder + { + services.AddSingleton(); + return services; + } + + /// Registers a consumer's player-creation logic - see . + public static IServiceCollection AddSharpMudPlayerFactory(this IServiceCollection services) + where TPlayerFactory : class, IPlayerFactory + { + services.AddSingleton(); + return services; + } + + // Called once from SharpMudApplicationBuilder's constructor - see + // docs/adr/0006-nuget-package-distribution.md's SharpMud.Hosting shape. + // Registration order matters: WorldLoaderHostedService must run before + // GameLoopHostedService/any transport BackgroundService, since the + // generic host awaits each hosted service's StartAsync in registration + // order before starting the next. + internal static IServiceCollection AddSharpMudHostingCore(this IServiceCollection services) + { + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(new GameLoopOptions()); + services.AddSingleton(); + + // Engine-level tickables - not ruleset-specific, so Hosting + // registers these itself rather than requiring every consumer to + // remember to. Resolved lazily (inside GameLoopHostedService's + // ExecuteAsync, after WorldLoaderHostedService has already + // populated WorldContext). + services.AddSingleton(sp => new WanderManager(sp.GetRequiredService().World, sp.GetRequiredService())); + services.AddSingleton(sp => new LinkdeadSweeper(sp.GetRequiredService().World, sp.GetRequiredService())); + + services.AddTransient(); + services.AddTransient(); + services.AddTransient(); + + services.AddHostedService(); + services.AddHostedService(); + services.AddHostedService(); + + return services; + } +} diff --git a/src/SharpMud.Host/SessionLoop.cs b/src/SharpMud.Hosting/SessionLoop.cs similarity index 79% rename from src/SharpMud.Host/SessionLoop.cs rename to src/SharpMud.Hosting/SessionLoop.cs index 0cb2170..6e9984d 100644 --- a/src/SharpMud.Host/SessionLoop.cs +++ b/src/SharpMud.Hosting/SessionLoop.cs @@ -4,22 +4,33 @@ using SharpMud.Engine.Core; using SharpMud.Engine.Sessions; -namespace SharpMud.Host; +namespace SharpMud.Hosting; -// Shared by every transport (local CLI, Telnet, ...) so adding a transport -// never touches game logic - just a new ISession implementation feeding the -// same loop (SPEC.md transport-agnostic sessions decision). -public static class SessionLoop +/// +/// Per-connection read-eval loop, shared by every transport (local CLI, +/// Telnet, ...) so adding a transport never touches game logic - just a new +/// implementation feeding the same loop (SPEC.md +/// transport-agnostic sessions decision). +/// +public sealed class SessionLoop { - public static async Task RunAsync( - World world, - ICommandParser parser, - ICommandRegistry registry, - ISession session, - Thing player, - IThingRepository repository, - CancellationToken ct) + private readonly WorldContext _worldContext; + private readonly ICommandParser _parser; + private readonly ICommandRegistry _registry; + private readonly IThingRepository _repository; + + public SessionLoop(WorldContext worldContext, ICommandParser parser, ICommandRegistry registry, IThingRepository repository) { + _worldContext = worldContext; + _parser = parser; + _registry = registry; + _repository = repository; + } + + public async Task RunAsync(ISession session, Thing player, CancellationToken ct) + { + var world = _worldContext.World; + // "quit" disconnects intentionally (QuitCommand) - that path skips // the Linkdead grace period entirely and removes the player // immediately below, same as every disconnect used to behave before @@ -46,7 +57,7 @@ public static async Task RunAsync( if (input is null) break; - var parsed = parser.Parse(input); + var parsed = _parser.Parse(input); if (parsed.Verb.Length == 0) continue; @@ -54,7 +65,7 @@ public static async Task RunAsync( if (currentRoom is null) break; - if (!registry.TryResolve(parsed.Verb, out var command)) + if (!_registry.TryResolve(parsed.Verb, out var command)) { await session.WriteLineAsync("Huh?", ct); continue; @@ -103,7 +114,7 @@ public static async Task RunAsync( // finally around the whole method (not just this line) // guarantees this runs even if a read/write above threw due to // cancellation mid-operation. - await repository.SaveTreeAsync(player, CancellationToken.None); + await _repository.SaveTreeAsync(player, CancellationToken.None); // explicitQuit's removal happens AFTER the save, not before // (PR #1 review) - ThingRepository.SaveTreeAsync persists diff --git a/src/SharpMud.Hosting/SharpMud.Hosting.csproj b/src/SharpMud.Hosting/SharpMud.Hosting.csproj new file mode 100644 index 0000000..589f388 --- /dev/null +++ b/src/SharpMud.Hosting/SharpMud.Hosting.csproj @@ -0,0 +1,33 @@ + + + + + + + + + + + + + + + + + + + + + + + net11.0 + enable + enable + + + diff --git a/src/SharpMud.Hosting/SharpMudApplication.cs b/src/SharpMud.Hosting/SharpMudApplication.cs new file mode 100644 index 0000000..075ddbe --- /dev/null +++ b/src/SharpMud.Hosting/SharpMudApplication.cs @@ -0,0 +1,37 @@ +using Microsoft.Extensions.Hosting; + +namespace SharpMud.Hosting; + +/// +/// A running (or ready-to-run) sharp-mud host - wraps the built +/// directly, the same relationship WebApplication +/// has to its inner host. See +/// docs/adr/0006-nuget-package-distribution.md. +/// +public sealed class SharpMudApplication : IHost, IAsyncDisposable +{ + private readonly IHost _host; + + internal SharpMudApplication(IHost host) + { + _host = host; + } + + /// + public IServiceProvider Services => _host.Services; + + /// Creates a new - the entry point for a consumer's own Program.cs. + public static SharpMudApplicationBuilder CreateBuilder(string[]? args = null) => new(args); + + /// + public Task StartAsync(CancellationToken cancellationToken = default) => _host.StartAsync(cancellationToken); + + /// + public Task StopAsync(CancellationToken cancellationToken = default) => _host.StopAsync(cancellationToken); + + /// + public void Dispose() => _host.Dispose(); + + /// + public ValueTask DisposeAsync() => ((IAsyncDisposable)_host).DisposeAsync(); +} diff --git a/src/SharpMud.Hosting/SharpMudApplicationBuilder.cs b/src/SharpMud.Hosting/SharpMudApplicationBuilder.cs new file mode 100644 index 0000000..24e97d3 --- /dev/null +++ b/src/SharpMud.Hosting/SharpMudApplicationBuilder.cs @@ -0,0 +1,52 @@ +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Diagnostics.Metrics; +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Logging; + +namespace SharpMud.Hosting; + +/// +/// A thin facade over - the same +/// relationship WebApplicationBuilder has to its inner builder, not +/// a reimplementation of hosting. See +/// docs/adr/0006-nuget-package-distribution.md for why sharp-mud's +/// persistent-process/ +/// execution model doesn't need anything more than that. +/// +public sealed class SharpMudApplicationBuilder : IHostApplicationBuilder +{ + private readonly HostApplicationBuilder _hostBuilder; + + internal SharpMudApplicationBuilder(string[]? args) + { + _hostBuilder = Host.CreateApplicationBuilder(args); + _hostBuilder.Services.AddSharpMudHostingCore(); + } + + /// + public IDictionary Properties => ((IHostApplicationBuilder)_hostBuilder).Properties; + + /// + public IConfigurationManager Configuration => _hostBuilder.Configuration; + + /// + public IHostEnvironment Environment => _hostBuilder.Environment; + + /// + public ILoggingBuilder Logging => _hostBuilder.Logging; + + /// + public IMetricsBuilder Metrics => _hostBuilder.Metrics; + + /// + public IServiceCollection Services => _hostBuilder.Services; + + /// + public void ConfigureContainer(IServiceProviderFactory factory, Action? configure = null) + where TContainerBuilder : notnull => + _hostBuilder.ConfigureContainer(factory, configure); + + /// Builds the configured services into a , ready to run. + public SharpMudApplication Build() => new(_hostBuilder.Build()); +} diff --git a/src/SharpMud.Hosting/ShutdownSaveHostedService.cs b/src/SharpMud.Hosting/ShutdownSaveHostedService.cs new file mode 100644 index 0000000..b1cc42d --- /dev/null +++ b/src/SharpMud.Hosting/ShutdownSaveHostedService.cs @@ -0,0 +1,43 @@ +using LayeredCraft.StructuredLogging; +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Logging; +using SharpMud.Engine.Core; + +namespace SharpMud.Hosting; + +// Whole-world snapshot on graceful shutdown, replacing Program.cs's final +// repository.SaveTreeAsync(hubArea, ...) call - specifically for NPC state +// (wander position, live combat HP) that isn't tied to any player session +// and so isn't already covered by each session's own on-disconnect save +// (docs/persistence.md). A BackgroundService stopping doesn't imply a save +// happens on the way out by itself, so this needs to be explicit - +// see docs/adr/0006-nuget-package-distribution.md. +internal sealed class ShutdownSaveHostedService : IHostedService +{ + private readonly WorldContext _worldContext; + private readonly IThingRepository _repository; + private readonly ILogger _logger; + + public ShutdownSaveHostedService(WorldContext worldContext, IThingRepository repository, ILogger logger) + { + _worldContext = worldContext; + _repository = repository; + _logger = logger; + } + + public Task StartAsync(CancellationToken cancellationToken) => Task.CompletedTask; + + public async Task StopAsync(CancellationToken cancellationToken) + { + if (!_worldContext.IsInitialized) + return; + + // CancellationToken.None, not the token this method receives - the + // host passes an already-cancelled/expiring token during shutdown, + // and using it here would risk aborting the save at exactly the + // moment it matters most (same reasoning as SessionLoop's + // disconnect-time save). + await _repository.SaveTreeAsync(_worldContext.Root, CancellationToken.None); + _logger.Information("Saved world on shutdown"); + } +} diff --git a/src/SharpMud.Hosting/WorldContext.cs b/src/SharpMud.Hosting/WorldContext.cs new file mode 100644 index 0000000..6784050 --- /dev/null +++ b/src/SharpMud.Hosting/WorldContext.cs @@ -0,0 +1,42 @@ +using SharpMud.Engine.Core; + +namespace SharpMud.Hosting; + +// Holds the loaded-or-built World/StartingRoom/Root once +// WorldLoaderHostedService's StartAsync has run - registered as a +// singleton and populated exactly once during startup, before any +// transport BackgroundService begins accepting connections (hosted +// services' StartAsync calls run sequentially in registration order, and +// WorldLoaderHostedService registers first - see +// ServiceCollectionExtensions.AddSharpMudHosting). Other services take +// this via constructor injection instead of World/Thing directly, since +// those values don't exist yet at DI-container-build time. +public sealed class WorldContext +{ + private World? _world; + private Thing? _startingRoom; + private Thing? _root; + + // Lets ShutdownSaveHostedService skip the shutdown-time save safely if + // the process is stopping before the world ever finished loading + // (e.g. a crash during startup) - saving a null world would just throw + // a second, more confusing exception on top of whatever caused the + // early shutdown. + public bool IsInitialized => _world is not null; + + public World World => _world + ?? throw new InvalidOperationException($"{nameof(World)} is not available yet - it's only set after {nameof(WorldLoaderHostedService)} has started."); + + public Thing StartingRoom => _startingRoom + ?? throw new InvalidOperationException($"{nameof(StartingRoom)} is not available yet - it's only set after {nameof(WorldLoaderHostedService)} has started."); + + public Thing Root => _root + ?? throw new InvalidOperationException($"{nameof(Root)} is not available yet - it's only set after {nameof(WorldLoaderHostedService)} has started."); + + internal void Initialize(World world, Thing startingRoom, Thing root) + { + _world = world; + _startingRoom = startingRoom; + _root = root; + } +} diff --git a/src/SharpMud.Hosting/WorldLoaderHostedService.cs b/src/SharpMud.Hosting/WorldLoaderHostedService.cs new file mode 100644 index 0000000..7c67b8b --- /dev/null +++ b/src/SharpMud.Hosting/WorldLoaderHostedService.cs @@ -0,0 +1,54 @@ +using LayeredCraft.StructuredLogging; +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Logging; +using SharpMud.Engine.Core; + +namespace SharpMud.Hosting; + +// Loads a persisted world, or builds and saves a fresh one - the generic +// half of what today's Program.cs does inline, parameterized by a +// consumer's IWorldBuilder instead of hardcoding HubWorldBuilder. Must be +// registered before GameLoopHostedService and any transport +// BackgroundService (see ServiceCollectionExtensions.AddSharpMudHosting) - +// the generic host awaits each IHostedService's StartAsync in registration +// order, so this fully populates WorldContext before anything that needs +// World/StartingRoom starts running. +internal sealed class WorldLoaderHostedService : IHostedService +{ + private readonly IThingRepository _repository; + private readonly IWorldBuilder _worldBuilder; + private readonly WorldContext _worldContext; + private readonly ILogger _logger; + + public WorldLoaderHostedService( + IThingRepository repository, IWorldBuilder worldBuilder, WorldContext worldContext, ILogger logger) + { + _repository = repository; + _worldBuilder = worldBuilder; + _worldContext = worldContext; + _logger = logger; + } + + public async Task StartAsync(CancellationToken cancellationToken) + { + var loadedRoot = await _repository.LoadTreeAsync(_worldBuilder.RootId, cancellationToken); + + if (loadedRoot is not null) + { + var world = new World(); + PlayerLogin.RegisterSubtree(world, loadedRoot); + var startingRoom = _worldBuilder.FindStartingRoom(loadedRoot); + _worldContext.Initialize(world, startingRoom, loadedRoot); + _logger.Information("Loaded persisted world"); + return; + } + + var (builtWorld, builtStartingRoom) = _worldBuilder.Build(); + var root = builtWorld.GetThing(_worldBuilder.RootId)!; + await _repository.SaveTreeAsync(root, cancellationToken); + _worldContext.Initialize(builtWorld, builtStartingRoom, root); + _logger.Information("No persisted world found - built and saved a fresh one"); + } + + public Task StopAsync(CancellationToken cancellationToken) => Task.CompletedTask; +} diff --git a/tests/SharpMud.Host.Tests/HostOptionsTests.cs b/tests/SharpMud.Host.Tests/HostOptionsTests.cs deleted file mode 100644 index 2a21a54..0000000 --- a/tests/SharpMud.Host.Tests/HostOptionsTests.cs +++ /dev/null @@ -1,79 +0,0 @@ -using SharpMud.Host; - -namespace SharpMud.Host.Tests; - -public sealed class HostOptionsTests -{ - [Fact] - public void Parse_DefaultsToCliMode_WhenNoArgsOrEnv() - { - var options = HostOptions.Parse([], new Dictionary()); - - options.UseTelnet.Should().BeFalse(); - } - - [Fact] - public void Parse_UsesTelnetMode_WhenTelnetArgGiven() - { - var options = HostOptions.Parse(["--telnet"], new Dictionary()); - - options.UseTelnet.Should().BeTrue(); - options.TelnetPort.Should().Be(4000); - } - - [Fact] - public void Parse_UsesPortFromArgs_WhenGiven() - { - var options = HostOptions.Parse(["--telnet", "5555"], new Dictionary()); - - options.TelnetPort.Should().Be(5555); - } - - [Fact] - public void Parse_UsesTelnetMode_WhenEnvVarSet() - { - var env = new Dictionary { ["SHARPMUD_MODE"] = "telnet" }; - - var options = HostOptions.Parse([], env); - - options.UseTelnet.Should().BeTrue(); - } - - [Fact] - public void Parse_UsesPortFromEnvVar_WhenArgsDoNotSpecifyPort() - { - var env = new Dictionary - { - ["SHARPMUD_MODE"] = "telnet", - ["SHARPMUD_TELNET_PORT"] = "6001", - }; - - var options = HostOptions.Parse(["--telnet"], env); - - options.TelnetPort.Should().Be(6001); - } - - [Fact] - public void Parse_ArgsPortWinsOverEnvVarPort() - { - var env = new Dictionary - { - ["SHARPMUD_MODE"] = "telnet", - ["SHARPMUD_TELNET_PORT"] = "6001", - }; - - var options = HostOptions.Parse(["--telnet", "7777"], env); - - options.TelnetPort.Should().Be(7777); - } - - [Fact] - public void Parse_TelnetModeIsCaseInsensitive() - { - var env = new Dictionary { ["SHARPMUD_MODE"] = "TELNET" }; - - var options = HostOptions.Parse([], env); - - options.UseTelnet.Should().BeTrue(); - } -} diff --git a/tests/SharpMud.Hosting.Tests/HostOptionsTests.cs b/tests/SharpMud.Hosting.Tests/HostOptionsTests.cs new file mode 100644 index 0000000..cbab9f3 --- /dev/null +++ b/tests/SharpMud.Hosting.Tests/HostOptionsTests.cs @@ -0,0 +1,24 @@ +using SharpMud.Hosting; + +namespace SharpMud.Hosting.Tests; + +public sealed class HostOptionsTests +{ + [Fact] + public void Parse_DefaultsDbPath_WhenEnvVarNotSet() + { + var options = HostOptions.Parse(new Dictionary()); + + options.DbPath.Should().Be("./sharpmud.db"); + } + + [Fact] + public void Parse_UsesDbPathFromEnvVar_WhenSet() + { + var env = new Dictionary { ["SHARPMUD_DB_PATH"] = "/data/sharpmud.db" }; + + var options = HostOptions.Parse(env); + + options.DbPath.Should().Be("/data/sharpmud.db"); + } +} diff --git a/tests/SharpMud.Host.Tests/LoginFlowTests.cs b/tests/SharpMud.Hosting.Tests/LoginFlowTests.cs similarity index 82% rename from tests/SharpMud.Host.Tests/LoginFlowTests.cs rename to tests/SharpMud.Hosting.Tests/LoginFlowTests.cs index 0b31f15..ee57fdf 100644 --- a/tests/SharpMud.Host.Tests/LoginFlowTests.cs +++ b/tests/SharpMud.Hosting.Tests/LoginFlowTests.cs @@ -1,13 +1,24 @@ using SharpMud.Engine.Behaviors; using SharpMud.Engine.Core; using SharpMud.Engine.Sessions; +using SharpMud.Hosting; -namespace SharpMud.Host.Tests; +namespace SharpMud.Hosting.Tests; public sealed class LoginFlowTests { private static Thing MakeRoom() => new() { Id = ThingId.New(), Name = "Room" }; + private static (LoginFlow loginFlow, IThingRepository repository) MakeLoginFlow(World world, Thing room) + { + var worldContext = new WorldContext(); + worldContext.Initialize(world, room, room); + var repository = Substitute.For(); + var playerFactory = Substitute.For(); + + return (new LoginFlow(worldContext, repository, playerFactory), repository); + } + [Fact] public async Task RunAsync_ResumesCharacter_WhenLinkdeadAndPasswordCorrect() { @@ -23,12 +34,12 @@ public async Task RunAsync_ResumesCharacter_WhenLinkdeadAndPasswordCorrect() room.Add(player); world.Register(player); - var repository = Substitute.For(); + var (loginFlow, _) = MakeLoginFlow(world, room); var session = Substitute.For(); session.IsConnected.Returns(true); session.ReadLineAsync(Arg.Any()).Returns("Hero", "correct-horse"); - var result = await LoginFlow.RunAsync(session, world, repository, room, TestContext.Current.CancellationToken); + var result = await loginFlow.RunAsync(session, TestContext.Current.CancellationToken); result.Should().Be(player); playerBehavior.ConnectionState.Should().Be(ConnectionState.Playing); @@ -52,7 +63,7 @@ public async Task RunAsync_Rejects_WhenPlayingAndAlreadyConnected() room.Add(player); world.Register(player); - var repository = Substitute.For(); + var (loginFlow, _) = MakeLoginFlow(world, room); var session = Substitute.For(); // First loop iteration: correct password but rejected -> RunAsync // checks IsConnected before looping again; simulate the client @@ -60,7 +71,7 @@ public async Task RunAsync_Rejects_WhenPlayingAndAlreadyConnected() session.IsConnected.Returns(true, false); session.ReadLineAsync(Arg.Any()).Returns("Hero", "correct-horse"); - var result = await LoginFlow.RunAsync(session, world, repository, room, TestContext.Current.CancellationToken); + var result = await loginFlow.RunAsync(session, TestContext.Current.CancellationToken); result.Should().BeNull(); playerBehavior.ConnectionState.Should().Be(ConnectionState.Playing); @@ -82,12 +93,12 @@ public async Task RunAsync_RejectsAsExpired_WhenLinkdeadPastGraceWindow() room.Add(player); world.Register(player); - var repository = Substitute.For(); + var (loginFlow, _) = MakeLoginFlow(world, room); var session = Substitute.For(); session.IsConnected.Returns(true, false); session.ReadLineAsync(Arg.Any()).Returns("Hero", "correct-horse"); - var result = await LoginFlow.RunAsync(session, world, repository, room, TestContext.Current.CancellationToken); + var result = await loginFlow.RunAsync(session, TestContext.Current.CancellationToken); result.Should().BeNull(); playerBehavior.ConnectionState.Should().Be(ConnectionState.Linkdead); @@ -114,12 +125,12 @@ public async Task RunAsync_RejectsAsExpired_WhenLinkdeadThingAlreadyRemovedFromW // lookup and this password check. room.Remove(player); - var repository = Substitute.For(); + var (loginFlow, _) = MakeLoginFlow(world, room); var session = Substitute.For(); session.IsConnected.Returns(true, false); session.ReadLineAsync(Arg.Any()).Returns("Hero", "correct-horse"); - var result = await LoginFlow.RunAsync(session, world, repository, room, TestContext.Current.CancellationToken); + var result = await loginFlow.RunAsync(session, TestContext.Current.CancellationToken); result.Should().BeNull(); await session.Received(1).WriteLineAsync("That session has expired. Please log in again.", Arg.Any()); diff --git a/tests/SharpMud.Host.Tests/PasswordHashingTests.cs b/tests/SharpMud.Hosting.Tests/PasswordHashingTests.cs similarity index 94% rename from tests/SharpMud.Host.Tests/PasswordHashingTests.cs rename to tests/SharpMud.Hosting.Tests/PasswordHashingTests.cs index f8545b0..a287b63 100644 --- a/tests/SharpMud.Host.Tests/PasswordHashingTests.cs +++ b/tests/SharpMud.Hosting.Tests/PasswordHashingTests.cs @@ -1,6 +1,6 @@ -using SharpMud.Host; +using SharpMud.Hosting; -namespace SharpMud.Host.Tests; +namespace SharpMud.Hosting.Tests; public sealed class PasswordHashingTests { diff --git a/tests/SharpMud.Hosting.Tests/PlayerLoginTests.cs b/tests/SharpMud.Hosting.Tests/PlayerLoginTests.cs new file mode 100644 index 0000000..4cbe8fd --- /dev/null +++ b/tests/SharpMud.Hosting.Tests/PlayerLoginTests.cs @@ -0,0 +1,80 @@ +using SharpMud.Engine.Behaviors; +using SharpMud.Engine.Core; +using SharpMud.Hosting; + +namespace SharpMud.Hosting.Tests; + +public sealed class PlayerLoginTests +{ + private static Thing MakeRoom() => new() { Id = ThingId.New(), Name = "Room" }; + + private static (PlayerLogin playerLogin, IThingRepository repository, IPlayerFactory playerFactory) MakePlayerLogin(World world, Thing room) + { + var worldContext = new WorldContext(); + worldContext.Initialize(world, room, room); + var repository = Substitute.For(); + var playerFactory = Substitute.For(); + + return (new PlayerLogin(worldContext, repository, playerFactory), repository, playerFactory); + } + + [Fact] + public async Task ResolveOrCreateAsync_ReturnsAlreadyOnlinePlayer_WithoutTouchingRepositoryOrFactory() + { + var world = new World(); + var room = MakeRoom(); + world.Register(room); + + var player = new Thing { Id = ThingId.New(), Name = "Adventurer" }; + player.Behaviors.Add(new PlayerBehavior { Username = "Adventurer", PasswordHash = "hash" }); + room.Add(player); + world.Register(player); + + var (playerLogin, repository, playerFactory) = MakePlayerLogin(world, room); + + var result = await playerLogin.ResolveOrCreateAsync("Adventurer", TestContext.Current.CancellationToken); + + result.Should().Be(player); + await repository.DidNotReceive().FindPlayerByUsernameAsync(Arg.Any(), Arg.Any()); + playerFactory.DidNotReceive().CreatePlayer(Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any()); + } + + [Fact] + public async Task ResolveOrCreateAsync_ReattachesPersistedPlayer_ToLiveStartingRoom() + { + var world = new World(); + var room = MakeRoom(); + world.Register(room); + + var loaded = new Thing { Id = ThingId.New(), Name = "Adventurer" }; + loaded.Behaviors.Add(new PlayerBehavior { Username = "Adventurer", PasswordHash = "hash" }); + + var (playerLogin, repository, playerFactory) = MakePlayerLogin(world, room); + repository.FindPlayerByUsernameAsync("Adventurer", Arg.Any()).Returns(loaded); + + var result = await playerLogin.ResolveOrCreateAsync("Adventurer", TestContext.Current.CancellationToken); + + result.Should().Be(loaded); + room.Children.Should().Contain(loaded); + world.GetThing(loaded.Id).Should().Be(loaded); + playerFactory.DidNotReceive().CreatePlayer(Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any()); + } + + [Fact] + public async Task ResolveOrCreateAsync_CreatesFreshPlayer_ViaPlayerFactory_WhenNoneFound() + { + var world = new World(); + var room = MakeRoom(); + world.Register(room); + + var (playerLogin, repository, playerFactory) = MakePlayerLogin(world, room); + repository.FindPlayerByUsernameAsync("Adventurer", Arg.Any()).Returns((Thing?)null); + + var created = new Thing { Id = ThingId.New(), Name = "Adventurer" }; + playerFactory.CreatePlayer(world, "Adventurer", Arg.Any(), room).Returns(created); + + var result = await playerLogin.ResolveOrCreateAsync("Adventurer", TestContext.Current.CancellationToken); + + result.Should().Be(created); + } +} diff --git a/tests/SharpMud.Host.Tests/SessionLoopTests.cs b/tests/SharpMud.Hosting.Tests/SessionLoopTests.cs similarity index 76% rename from tests/SharpMud.Host.Tests/SessionLoopTests.cs rename to tests/SharpMud.Hosting.Tests/SessionLoopTests.cs index f46c0d7..143168e 100644 --- a/tests/SharpMud.Host.Tests/SessionLoopTests.cs +++ b/tests/SharpMud.Hosting.Tests/SessionLoopTests.cs @@ -2,12 +2,13 @@ using SharpMud.Engine.Commands; using SharpMud.Engine.Core; using SharpMud.Engine.Sessions; +using SharpMud.Hosting; -namespace SharpMud.Host.Tests; +namespace SharpMud.Hosting.Tests; public sealed class SessionLoopTests { - private static (World world, Thing room, Thing player, PlayerBehavior playerBehavior) MakePlayerInRoom() + private static (WorldContext worldContext, World world, Thing room, Thing player, PlayerBehavior playerBehavior) MakePlayerInRoom() { var world = new World(); var room = new Thing { Id = ThingId.New(), Name = "Room" }; @@ -17,13 +18,17 @@ private static (World world, Thing room, Thing player, PlayerBehavior playerBeha room.Add(player); world.Register(room); world.Register(player); - return (world, room, player, playerBehavior); + + var worldContext = new WorldContext(); + worldContext.Initialize(world, room, room); + + return (worldContext, world, room, player, playerBehavior); } [Fact] public async Task RunAsync_MarksLinkdead_WhenSessionDropsWithoutQuit() { - var (world, room, player, playerBehavior) = MakePlayerInRoom(); + var (worldContext, world, room, player, playerBehavior) = MakePlayerInRoom(); var session = Substitute.For(); session.IsConnected.Returns(true); session.ReadLineAsync(Arg.Any()).Returns((string?)null); @@ -32,8 +37,9 @@ public async Task RunAsync_MarksLinkdead_WhenSessionDropsWithoutQuit() var parser = Substitute.For(); var registry = Substitute.For(); var repository = Substitute.For(); + var sessionLoop = new SessionLoop(worldContext, parser, registry, repository); - await SessionLoop.RunAsync(world, parser, registry, session, player, repository, TestContext.Current.CancellationToken); + await sessionLoop.RunAsync(session, player, TestContext.Current.CancellationToken); playerBehavior.ConnectionState.Should().Be(ConnectionState.Linkdead); room.Children.Should().Contain(player); @@ -43,7 +49,7 @@ public async Task RunAsync_MarksLinkdead_WhenSessionDropsWithoutQuit() [Fact] public async Task RunAsync_DoesNotClobberState_WhenNewerSessionAlreadyReconnected() { - var (world, room, player, playerBehavior) = MakePlayerInRoom(); + var (worldContext, world, room, player, playerBehavior) = MakePlayerInRoom(); var oldSession = Substitute.For(); oldSession.IsConnected.Returns(true); oldSession.ReadLineAsync(Arg.Any()).Returns((string?)null); @@ -58,8 +64,9 @@ public async Task RunAsync_DoesNotClobberState_WhenNewerSessionAlreadyReconnecte var parser = Substitute.For(); var registry = Substitute.For(); var repository = Substitute.For(); + var sessionLoop = new SessionLoop(worldContext, parser, registry, repository); - await SessionLoop.RunAsync(world, parser, registry, oldSession, player, repository, TestContext.Current.CancellationToken); + await sessionLoop.RunAsync(oldSession, player, TestContext.Current.CancellationToken); // The stale disconnect must not mark the actively-reconnected // character Linkdead or remove it from the world. @@ -71,7 +78,7 @@ public async Task RunAsync_DoesNotClobberState_WhenNewerSessionAlreadyReconnecte [Fact] public async Task RunAsync_RemovesImmediately_WhenPlayerQuits() { - var (world, room, player, playerBehavior) = MakePlayerInRoom(); + var (worldContext, world, room, player, playerBehavior) = MakePlayerInRoom(); var session = Substitute.For(); session.IsConnected.Returns(true, false); session.ReadLineAsync(Arg.Any()).Returns("quit"); @@ -85,8 +92,9 @@ public async Task RunAsync_RemovesImmediately_WhenPlayerQuits() return true; }); var repository = Substitute.For(); + var sessionLoop = new SessionLoop(worldContext, parser, registry, repository); - await SessionLoop.RunAsync(world, parser, registry, session, player, repository, TestContext.Current.CancellationToken); + await sessionLoop.RunAsync(session, player, TestContext.Current.CancellationToken); room.Children.Should().NotContain(player); world.GetThing(player.Id).Should().BeNull(); @@ -100,7 +108,7 @@ public async Task RunAsync_SavesWithParentStillSet_WhenPlayerQuits() // before - ThingRepository.SaveTreeAsync persists ParentId from // thing.Parent at save time, so removing first would silently save // ParentId=null and lose the room the player quit from. - var (world, room, player, playerBehavior) = MakePlayerInRoom(); + var (worldContext, world, room, player, playerBehavior) = MakePlayerInRoom(); var session = Substitute.For(); session.IsConnected.Returns(true, false); session.ReadLineAsync(Arg.Any()).Returns("quit"); @@ -119,8 +127,9 @@ public async Task RunAsync_SavesWithParentStillSet_WhenPlayerQuits() repository.SaveTreeAsync(Arg.Any(), Arg.Any()) .Returns(Task.CompletedTask) .AndDoes(x => parentAtSaveTime = ((Thing)x[0]).Parent); + var sessionLoop = new SessionLoop(worldContext, parser, registry, repository); - await SessionLoop.RunAsync(world, parser, registry, session, player, repository, TestContext.Current.CancellationToken); + await sessionLoop.RunAsync(session, player, TestContext.Current.CancellationToken); parentAtSaveTime.Should().Be(room); } diff --git a/tests/SharpMud.Host.Tests/SharpMud.Host.Tests.csproj b/tests/SharpMud.Hosting.Tests/SharpMud.Hosting.Tests.csproj similarity index 89% rename from tests/SharpMud.Host.Tests/SharpMud.Host.Tests.csproj rename to tests/SharpMud.Hosting.Tests/SharpMud.Hosting.Tests.csproj index 9d4b2b9..b7760f4 100644 --- a/tests/SharpMud.Host.Tests/SharpMud.Host.Tests.csproj +++ b/tests/SharpMud.Hosting.Tests/SharpMud.Hosting.Tests.csproj @@ -4,7 +4,7 @@ enable enable Exe - SharpMud.Host.Tests + SharpMud.Hosting.Tests net11.0 false true @@ -33,7 +33,7 @@ - + diff --git a/tests/SharpMud.Host.Tests/xunit.runner.json b/tests/SharpMud.Hosting.Tests/xunit.runner.json similarity index 100% rename from tests/SharpMud.Host.Tests/xunit.runner.json rename to tests/SharpMud.Hosting.Tests/xunit.runner.json diff --git a/tests/SharpMud.Ruleset.Classic.Tests/Combat/CombatManagerTests.cs b/tests/SharpMud.Samples.Classic.Tests/Combat/CombatManagerTests.cs similarity index 100% rename from tests/SharpMud.Ruleset.Classic.Tests/Combat/CombatManagerTests.cs rename to tests/SharpMud.Samples.Classic.Tests/Combat/CombatManagerTests.cs diff --git a/tests/SharpMud.Ruleset.Classic.Tests/Combat/CombatResolverTests.cs b/tests/SharpMud.Samples.Classic.Tests/Combat/CombatResolverTests.cs similarity index 100% rename from tests/SharpMud.Ruleset.Classic.Tests/Combat/CombatResolverTests.cs rename to tests/SharpMud.Samples.Classic.Tests/Combat/CombatResolverTests.cs diff --git a/tests/SharpMud.Ruleset.Classic.Tests/SharpMud.Ruleset.Classic.Tests.csproj b/tests/SharpMud.Samples.Classic.Tests/SharpMud.Samples.Classic.Tests.csproj similarity index 100% rename from tests/SharpMud.Ruleset.Classic.Tests/SharpMud.Ruleset.Classic.Tests.csproj rename to tests/SharpMud.Samples.Classic.Tests/SharpMud.Samples.Classic.Tests.csproj diff --git a/tests/SharpMud.Ruleset.Classic.Tests/TestKit/Attributes/ClassicAutoDataAttribute.cs b/tests/SharpMud.Samples.Classic.Tests/TestKit/Attributes/ClassicAutoDataAttribute.cs similarity index 100% rename from tests/SharpMud.Ruleset.Classic.Tests/TestKit/Attributes/ClassicAutoDataAttribute.cs rename to tests/SharpMud.Samples.Classic.Tests/TestKit/Attributes/ClassicAutoDataAttribute.cs diff --git a/tests/SharpMud.Ruleset.Classic.Tests/TestKit/BaseFixtureFactory.cs b/tests/SharpMud.Samples.Classic.Tests/TestKit/BaseFixtureFactory.cs similarity index 100% rename from tests/SharpMud.Ruleset.Classic.Tests/TestKit/BaseFixtureFactory.cs rename to tests/SharpMud.Samples.Classic.Tests/TestKit/BaseFixtureFactory.cs diff --git a/tests/SharpMud.Ruleset.Classic.Tests/xunit.runner.json b/tests/SharpMud.Samples.Classic.Tests/xunit.runner.json similarity index 100% rename from tests/SharpMud.Ruleset.Classic.Tests/xunit.runner.json rename to tests/SharpMud.Samples.Classic.Tests/xunit.runner.json From cec6d444dc310a287401dbbabf24ef375012ecae Mon Sep 17 00:00:00 2001 From: Nick Cipollina Date: Mon, 20 Jul 2026 12:34:18 -0400 Subject: [PATCH 02/14] feat(persistence,sample): split Persistence into core/Sqlite/DynamoDb, finish sample rewrite SharpMud.Persistence core drops its SQLite PackageReferences - genuinely provider-agnostic now, GameDbContext already used DbContextOptions generically with no hardcoded provider. New SharpMud.Persistence.Sqlite (AddSharpMudSqlitePersistence) and SharpMud.Persistence.DynamoDb (AddSharpMudDynamoDbPersistence, EntityFrameworkCore.DynamoDb 10.0.0). Resolved the TPH/discriminator open question from PLAN-0006 by reading the DynamoDB provider's own docs directly: TPH is the supported (only supported) inheritance strategy, discriminators auto-configured by default - BehaviorConfiguration's HasDiscriminator("BehaviorType") usage is compatible in principle, though the exact fluent-API interop isn't verified against a live table. Multi-targeted every packaged src project to net10.0;net11.0 (verified clean - no net11.0-only API usage anywhere), since Persistence.DynamoDb needed net10.0 support to even reference Engine/Persistence, and the ADR already called for this. Persistence.DynamoDb itself stays net10.0-only, matching the provider's own target. Added a new IStorageInitializer hook in SharpMud.Hosting and wired it into WorldLoaderHostedService - SQLite's EnsureCreatedAsync schema step needs to run before world loading, but hosted-service StartAsync ordering across independently-registered Hosting/Persistence packages would be fragile to rely on directly. Found and fixed a real startup-ordering bug: the generic host constructs every registered IHostedService (resolving IEnumerable) before calling any of their StartAsync methods - so GameLoopHostedService/ the transport BackgroundServices constructor-injecting ITickable/ LoginFlow/SessionLoop directly would eagerly resolve WorldContext-dependent services before WorldLoaderHostedService.StartAsync had populated it. Fixed by deferring resolution to ExecuteAsync via IServiceProvider - the legitimate service-locator exception in coding-standards.md. Renamed HostOptions to SharpMudHostOptions - collided with the real Microsoft.Extensions.Hosting.HostOptions BCL type every consumer of this package would also have in scope. Finished samples/SharpMud.Samples.Classic/Program.cs against SharpMud.Hosting's builder: ClassicWorldBuilder/ClassicPlayerFactory implement Hosting's extension points, CombatManager registered as both ICombatManager and ITickable, SHARPMUD_MODE/--telnet parsing preserved as the sample's own composition-root decision. csproj gained OutputType=Exe and the references/packages Program.cs needs. Full solution builds clean (net10.0 + net11.0) and all 107 tests pass. Co-Authored-By: Claude Sonnet 5 --- SharpMud.slnx | 3 + .../ClassicPlayerFactory.cs | 11 ++ .../ClassicWorldBuilder.cs | 16 ++ .../HubWorldBuilder.cs | 11 +- samples/SharpMud.Samples.Classic/Program.cs | 169 +++++------------- .../SharpMud.Samples.Classic.csproj | 25 ++- .../CliTransportBackgroundService.cs | 33 ++++ .../ServiceCollectionExtensions.cs | 13 ++ .../SharpMud.Adapters.Cli.csproj | 14 +- src/SharpMud.Adapters.Telnet/HostRunner.cs | 52 ------ .../ServiceCollectionExtensions.cs | 14 ++ .../SharpMud.Adapters.Telnet.csproj | 7 +- .../TelnetTransportBackgroundService.cs | 92 ++++++++++ .../TelnetTransportOptions.cs | 7 + src/SharpMud.Engine/SharpMud.Engine.csproj | 2 +- src/SharpMud.Hosting/GameLoopHostedService.cs | 21 ++- src/SharpMud.Hosting/IStorageInitializer.cs | 17 ++ .../ServiceCollectionExtensions.cs | 20 ++- src/SharpMud.Hosting/SharpMud.Hosting.csproj | 2 +- ...{HostOptions.cs => SharpMudHostOptions.cs} | 9 +- .../WorldLoaderHostedService.cs | 14 +- .../ServiceCollectionExtensions.cs | 34 ++++ .../SharpMud.Persistence.DynamoDb.csproj | 25 +++ .../ServiceCollectionExtensions.cs | 22 +++ .../SharpMud.Persistence.Sqlite.csproj | 25 +++ .../SqliteStorageInitializer.cs | 26 +++ .../SharpMud.Persistence.csproj | 8 +- .../ServiceCollectionExtensionsTests.cs | 17 ++ .../SharpMud.Adapters.Cli.Tests.csproj | 40 +++++ .../xunit.runner.json | 3 + .../ServiceCollectionExtensionsTests.cs | 19 ++ .../SharpMud.Adapters.Telnet.Tests.csproj | 1 + ...nsTests.cs => SharpMudHostOptionsTests.cs} | 6 +- .../SharpMud.Persistence.Tests.csproj | 2 +- .../SharpMud.Samples.Classic.Tests.csproj | 2 +- 35 files changed, 573 insertions(+), 209 deletions(-) create mode 100644 samples/SharpMud.Samples.Classic/ClassicPlayerFactory.cs create mode 100644 samples/SharpMud.Samples.Classic/ClassicWorldBuilder.cs create mode 100644 src/SharpMud.Adapters.Cli/CliTransportBackgroundService.cs create mode 100644 src/SharpMud.Adapters.Cli/ServiceCollectionExtensions.cs delete mode 100644 src/SharpMud.Adapters.Telnet/HostRunner.cs create mode 100644 src/SharpMud.Adapters.Telnet/ServiceCollectionExtensions.cs create mode 100644 src/SharpMud.Adapters.Telnet/TelnetTransportBackgroundService.cs create mode 100644 src/SharpMud.Adapters.Telnet/TelnetTransportOptions.cs create mode 100644 src/SharpMud.Hosting/IStorageInitializer.cs rename src/SharpMud.Hosting/{HostOptions.cs => SharpMudHostOptions.cs} (60%) create mode 100644 src/SharpMud.Persistence.DynamoDb/ServiceCollectionExtensions.cs create mode 100644 src/SharpMud.Persistence.DynamoDb/SharpMud.Persistence.DynamoDb.csproj create mode 100644 src/SharpMud.Persistence.Sqlite/ServiceCollectionExtensions.cs create mode 100644 src/SharpMud.Persistence.Sqlite/SharpMud.Persistence.Sqlite.csproj create mode 100644 src/SharpMud.Persistence.Sqlite/SqliteStorageInitializer.cs create mode 100644 tests/SharpMud.Adapters.Cli.Tests/ServiceCollectionExtensionsTests.cs create mode 100644 tests/SharpMud.Adapters.Cli.Tests/SharpMud.Adapters.Cli.Tests.csproj create mode 100644 tests/SharpMud.Adapters.Cli.Tests/xunit.runner.json create mode 100644 tests/SharpMud.Adapters.Telnet.Tests/ServiceCollectionExtensionsTests.cs rename tests/SharpMud.Hosting.Tests/{HostOptionsTests.cs => SharpMudHostOptionsTests.cs} (70%) diff --git a/SharpMud.slnx b/SharpMud.slnx index aca297b..92e0c2a 100644 --- a/SharpMud.slnx +++ b/SharpMud.slnx @@ -15,11 +15,14 @@ + + + diff --git a/samples/SharpMud.Samples.Classic/ClassicPlayerFactory.cs b/samples/SharpMud.Samples.Classic/ClassicPlayerFactory.cs new file mode 100644 index 0000000..177da0c --- /dev/null +++ b/samples/SharpMud.Samples.Classic/ClassicPlayerFactory.cs @@ -0,0 +1,11 @@ +using SharpMud.Engine.Core; +using SharpMud.Hosting; + +namespace SharpMud.Ruleset.Classic; + +/// The Classic ruleset's - wraps . +public sealed class ClassicPlayerFactory : IPlayerFactory +{ + public Thing CreatePlayer(World world, string username, string passwordHash, Thing startingRoom) => + HubWorldBuilder.CreatePlayer(world, username, passwordHash, startingRoom); +} diff --git a/samples/SharpMud.Samples.Classic/ClassicWorldBuilder.cs b/samples/SharpMud.Samples.Classic/ClassicWorldBuilder.cs new file mode 100644 index 0000000..3812c68 --- /dev/null +++ b/samples/SharpMud.Samples.Classic/ClassicWorldBuilder.cs @@ -0,0 +1,16 @@ +using SharpMud.Engine.Behaviors; +using SharpMud.Engine.Core; +using SharpMud.Hosting; + +namespace SharpMud.Ruleset.Classic; + +/// The Classic ruleset's - wraps for . +public sealed class ClassicWorldBuilder : IWorldBuilder +{ + public ThingId RootId => HubWorldBuilder.HubAreaId; + + public (World World, Thing StartingRoom) Build() => HubWorldBuilder.Build(); + + public Thing FindStartingRoom(Thing root) => + HubWorldBuilder.FindStartingRoom(root) ?? root.Children.First(c => c.HasBehavior()); +} diff --git a/samples/SharpMud.Samples.Classic/HubWorldBuilder.cs b/samples/SharpMud.Samples.Classic/HubWorldBuilder.cs index 674bd07..8bda7ff 100644 --- a/samples/SharpMud.Samples.Classic/HubWorldBuilder.cs +++ b/samples/SharpMud.Samples.Classic/HubWorldBuilder.cs @@ -1,14 +1,13 @@ using SharpMud.Engine.Behaviors; using SharpMud.Engine.Core; -using SharpMud.Ruleset.Classic; -namespace SharpMud.Host; +namespace SharpMud.Ruleset.Classic; // Content, not engine or ruleset - mirrors WheelMUD's separate "Universe" -// project (docs/research/wheelmud-findings.md). References Ruleset.Classic -// directly since the cave rat/sword need ruleset-specific behaviors -// (CombatantBehavior, WearableBehavior's EquipSlot) - only Host is allowed -// to know about a specific ruleset (docs/engine-vs-ruleset.md). +// project (docs/research/wheelmud-findings.md). Lives alongside the ruleset +// code in this sample rather than a separate project, per ADR-0006 - a +// consumer's own world content and their ruleset are the two things that +// make their game theirs, both belong in their one project. public static class HubWorldBuilder { // Fixed, not ThingId.New() - so a fresh boot can ask the repository diff --git a/samples/SharpMud.Samples.Classic/Program.cs b/samples/SharpMud.Samples.Classic/Program.cs index 23d56e4..248105e 100644 --- a/samples/SharpMud.Samples.Classic/Program.cs +++ b/samples/SharpMud.Samples.Classic/Program.cs @@ -1,39 +1,26 @@ -using System.Runtime.InteropServices; using LayeredCraft.StructuredLogging; -using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; using Serilog; using SharpMud.Adapters.Cli; using SharpMud.Adapters.Telnet; -using SharpMud.Engine.Behaviors; -using SharpMud.Engine.Commands; -using SharpMud.Engine.Commands.Builtin; using SharpMud.Engine.Core; -using SharpMud.Engine.Sessions; using SharpMud.Engine.Ticking; -using SharpMud.Host; +using SharpMud.Hosting; using SharpMud.Persistence; +using SharpMud.Persistence.Sqlite; using SharpMud.Ruleset.Classic; -var env = new Dictionary -{ - ["SHARPMUD_MODE"] = Environment.GetEnvironmentVariable("SHARPMUD_MODE"), - ["SHARPMUD_TELNET_PORT"] = Environment.GetEnvironmentVariable("SHARPMUD_TELNET_PORT"), - ["SHARPMUD_DB_PATH"] = Environment.GetEnvironmentVariable("SHARPMUD_DB_PATH"), -}; -var hostOptions = HostOptions.Parse(args, env); +var builder = SharpMudApplication.CreateBuilder(args); // Non-secret configuration (Serilog levels/sinks) lives in appsettings.json, // with environment variables able to override it - see ADR-0003 // (docs/adr/0003-allow-appsettingsjson-for-non-secret-config.md). Secrets -// still never go in appsettings.json; only env vars (HostOptions.Parse -// above) carry those. -var configuration = new ConfigurationBuilder() - .AddJsonFile("appsettings.json", optional: false) - .AddEnvironmentVariables() - .Build(); +// still never go in appsettings.json; only env vars (SharpMudHostOptions.Parse +// below) carry those. +builder.Configuration.AddJsonFile("appsettings.json", optional: false); // Console sink only - this repo runs in Docker (docs/deployment.md), stdout // is the sink Docker already captures. See ADR-0002 @@ -41,116 +28,52 @@ // introduced. The EF Core "log every SQL command at Information" override // lives in appsettings.json's Serilog:MinimumLevel:Override section. var serilogLogger = new LoggerConfiguration() - .ReadFrom.Configuration(configuration) + .ReadFrom.Configuration(builder.Configuration) .CreateLogger(); +builder.Logging.ClearProviders(); +builder.Logging.AddSerilog(serilogLogger, dispose: true); -var services = new ServiceCollection(); -services.AddLogging(builder => builder.AddSerilog(serilogLogger, dispose: true)); -services.AddDbContextFactory(options => options.UseSqlite($"Data Source={hostOptions.DbPath}")); -services.AddSingleton(); -services.AddSingleton(); -services.AddSingleton(); - -await using var provider = services.BuildServiceProvider(); - -var logger = provider.GetRequiredService>(); - -// EnsureCreated only, never EnsureDeleted, at boot - creates the schema if -// missing but never wipes existing data. See docs/persistence.md Schema/ -// Migrations: a genuinely changed C# model against an old .db file during -// dev means deleting the file by hand, not an automatic wipe every startup -// (which would defeat persistence entirely). -await using (var dbContext = await provider.GetRequiredService>().CreateDbContextAsync()) - await dbContext.Database.EnsureCreatedAsync(); - -var repository = provider.GetRequiredService(); - -var loadedArea = await repository.LoadTreeAsync(HubWorldBuilder.HubAreaId, CancellationToken.None); - -World world; -Thing startingRoom; -Thing hubArea; - -if (loadedArea is not null) -{ - world = new World(); - PlayerLogin.RegisterSubtree(world, loadedArea); - hubArea = loadedArea; - startingRoom = HubWorldBuilder.FindStartingRoom(hubArea) - ?? hubArea.Children.First(c => c.HasBehavior()); - logger.Information("Loaded persisted world"); -} -else +var env = new Dictionary { - (world, startingRoom) = HubWorldBuilder.Build(); - hubArea = world.GetThing(HubWorldBuilder.HubAreaId)!; - await repository.SaveTreeAsync(hubArea, CancellationToken.None); - logger.Information("No persisted world found - built and saved a fresh one"); -} - -var random = new RandomSource(); -var combatResolver = new CombatResolver(random); -var combatManager = new CombatManager(combatResolver, startingRoom); -var wanderManager = new WanderManager(world, random); -var linkdeadSweeper = new LinkdeadSweeper(world, repository); - -var gameLoop = new GameLoop(new GameLoopOptions()); -gameLoop.Register(combatManager); -gameLoop.Register(wanderManager); -gameLoop.Register(linkdeadSweeper); - -var registry = new CommandRegistry(); -BuiltinCommands.RegisterAll(registry); -ClassicCommands.RegisterAll(registry, combatManager, random); - -var parser = provider.GetRequiredService(); - -using var cts = new CancellationTokenSource(); - -// PosixSignalRegistration, not Console.CancelKeyPress - CancelKeyPress only -// ever catches SIGINT (Ctrl+C) and was observed not firing reliably without -// a TTY attached. Critically, it never catches SIGTERM at all, which is -// exactly what `docker stop`/Kubernetes send on a graceful shutdown - the -// actual scenario docs/persistence.md's "save on graceful shutdown" design -// depends on. Handling both signals here is what makes that real. -void RequestShutdown(PosixSignalContext context) + ["SHARPMUD_DB_PATH"] = Environment.GetEnvironmentVariable("SHARPMUD_DB_PATH"), +}; +var hostOptions = SharpMudHostOptions.Parse(env); + +builder.Services.AddSharpMudSqlitePersistence(hostOptions.DbPath); +builder.Services.AddSingleton(); +builder.Services.AddSharpMudWorld(); +builder.Services.AddSharpMudPlayerFactory(); + +builder.Services.AddSingleton(); +// Registered once as ICombatManager and once as ITickable, same underlying +// instance - CombatManager both drives the kill/flee commands and advances +// active encounters each tick. +builder.Services.AddSingleton(sp => new CombatManager(sp.GetRequiredService(), sp.GetRequiredService().StartingRoom)); +builder.Services.AddSingleton(sp => (ITickable)sp.GetRequiredService()); +builder.Services.AddSharpMudRuleset((sp, registry) => + ClassicCommands.RegisterAll(registry, sp.GetRequiredService(), sp.GetRequiredService())); + +// Transport mode: SHARPMUD_MODE/SHARPMUD_TELNET_PORT/--telnet, same +// precedence as before (CLI arg wins over env var) - this is now the +// sample's own composition-root decision, since SharpMud.Hosting must not +// know which transport(s) exist (docs/adr/0006-nuget-package-distribution.md). +var useTelnet = args is ["--telnet", ..] + || string.Equals(Environment.GetEnvironmentVariable("SHARPMUD_MODE"), "telnet", StringComparison.OrdinalIgnoreCase); + +if (useTelnet) { - context.Cancel = true; - cts.Cancel(); -} - -using var sigInt = PosixSignalRegistration.Create(PosixSignal.SIGINT, RequestShutdown); -using var sigTerm = PosixSignalRegistration.Create(PosixSignal.SIGTERM, RequestShutdown); + var port = 4000; + if (args is ["--telnet", var portArg, ..] && int.TryParse(portArg, out var parsedArg)) + port = parsedArg; + else if (int.TryParse(Environment.GetEnvironmentVariable("SHARPMUD_TELNET_PORT"), out var parsedEnv)) + port = parsedEnv; -var gameLoopTask = gameLoop.RunAsync(cts.Token); - -if (hostOptions.UseTelnet) -{ - var telnetSessionLogger = provider.GetRequiredService>(); - var telnetHostContext = new TelnetHostContext( - world, parser, registry, repository, startingRoom, hostOptions.TelnetPort, telnetSessionLogger); - await HostRunner.RunTelnetAsync(telnetHostContext, cts.Token); + builder.Services.AddSharpMudTelnetTransport(port); } else { - ISession session = new ConsoleSession(); - var player = await PlayerLogin.ResolveOrCreateAsync(world, repository, "Adventurer", startingRoom, cts.Token); - player.FindBehavior()!.Session = session; - - await SessionLoop.RunAsync(world, parser, registry, session, player, repository, cts.Token); + builder.Services.AddSharpMudCliTransport(); } -// Whole-world snapshot - each disconnected player already saved themselves -// (SessionLoop's finally block), but NPCs (wandering position, live combat -// HP) aren't tied to any player session and only get captured here. -await repository.SaveTreeAsync(hubArea, CancellationToken.None); - -cts.Cancel(); -try -{ - await gameLoopTask; -} -catch (OperationCanceledException) -{ - // Expected on shutdown. -} +var mud = builder.Build(); +await mud.RunAsync(); diff --git a/samples/SharpMud.Samples.Classic/SharpMud.Samples.Classic.csproj b/samples/SharpMud.Samples.Classic/SharpMud.Samples.Classic.csproj index 603d256..fbe879e 100644 --- a/samples/SharpMud.Samples.Classic/SharpMud.Samples.Classic.csproj +++ b/samples/SharpMud.Samples.Classic/SharpMud.Samples.Classic.csproj @@ -1,18 +1,39 @@  - - + + + + + + + + + + + + + + + + + + + Exe net11.0 enable enable + false diff --git a/src/SharpMud.Adapters.Cli/CliTransportBackgroundService.cs b/src/SharpMud.Adapters.Cli/CliTransportBackgroundService.cs new file mode 100644 index 0000000..ff4929b --- /dev/null +++ b/src/SharpMud.Adapters.Cli/CliTransportBackgroundService.cs @@ -0,0 +1,33 @@ +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using SharpMud.Engine.Behaviors; +using SharpMud.Hosting; + +namespace SharpMud.Adapters.Cli; + +// Absorbs what today's Program.cs's CLI branch does inline: a single local +// stdin/stdout session, no login (SPEC.md), resolved/created via PlayerLogin. +internal sealed class CliTransportBackgroundService : BackgroundService +{ + private readonly IServiceProvider _serviceProvider; + + // IServiceProvider, not PlayerLogin/SessionLoop directly - see + // TelnetTransportBackgroundService's constructor comment for why + // eager constructor injection here would resolve WorldContext-dependent + // services before WorldLoaderHostedService.StartAsync has populated it. + public CliTransportBackgroundService(IServiceProvider serviceProvider) + { + _serviceProvider = serviceProvider; + } + + protected override async Task ExecuteAsync(CancellationToken stoppingToken) + { + var session = new ConsoleSession(); + var playerLogin = _serviceProvider.GetRequiredService(); + var player = await playerLogin.ResolveOrCreateAsync("Adventurer", stoppingToken); + player.FindBehavior()!.Session = session; + + var sessionLoop = _serviceProvider.GetRequiredService(); + await sessionLoop.RunAsync(session, player, stoppingToken); + } +} diff --git a/src/SharpMud.Adapters.Cli/ServiceCollectionExtensions.cs b/src/SharpMud.Adapters.Cli/ServiceCollectionExtensions.cs new file mode 100644 index 0000000..d0a8168 --- /dev/null +++ b/src/SharpMud.Adapters.Cli/ServiceCollectionExtensions.cs @@ -0,0 +1,13 @@ +using Microsoft.Extensions.DependencyInjection; + +namespace SharpMud.Adapters.Cli; + +public static class ServiceCollectionExtensions +{ + /// Registers a background service that runs a single local stdin/stdout session, login-free per SPEC.md. + public static IServiceCollection AddSharpMudCliTransport(this IServiceCollection services) + { + services.AddHostedService(); + return services; + } +} diff --git a/src/SharpMud.Adapters.Cli/SharpMud.Adapters.Cli.csproj b/src/SharpMud.Adapters.Cli/SharpMud.Adapters.Cli.csproj index 0ff6987..411c038 100644 --- a/src/SharpMud.Adapters.Cli/SharpMud.Adapters.Cli.csproj +++ b/src/SharpMud.Adapters.Cli/SharpMud.Adapters.Cli.csproj @@ -2,10 +2,22 @@ + + + + + + + + + + - net11.0 + net10.0;net11.0 enable enable diff --git a/src/SharpMud.Adapters.Telnet/HostRunner.cs b/src/SharpMud.Adapters.Telnet/HostRunner.cs deleted file mode 100644 index 05ae68c..0000000 --- a/src/SharpMud.Adapters.Telnet/HostRunner.cs +++ /dev/null @@ -1,52 +0,0 @@ -using LayeredCraft.StructuredLogging; -using SharpMud.Adapters.Telnet; -using SharpMud.Engine.Behaviors; -using SharpMud.Engine.Sessions; - -namespace SharpMud.Host; - -public static class HostRunner -{ - public static async Task RunTelnetAsync(TelnetHostContext context, CancellationToken ct) - { - var listener = new TelnetListener(context.Port, context.Logger); - listener.Start(); - context.Logger.Information("Listening for telnet connections on port {Port}", context.Port); - - var connectionTasks = new List(); - - try - { - await foreach (var session in listener.AcceptSessionsAsync(ct)) - connectionTasks.Add(HandleConnectionAsync(session, context, ct)); - } - finally - { - listener.Stop(); - await Task.WhenAll(connectionTasks); - } - } - - private static async Task HandleConnectionAsync(ISession session, TelnetHostContext context, CancellationToken ct) - { - try - { - var player = await LoginFlow.RunAsync(session, context.World, context.Repository, context.StartingRoom, ct); - if (player is null) - { - await session.DisconnectAsync("Login failed.", ct); - return; - } - - player.FindBehavior()!.Session = session; - - await SessionLoop.RunAsync(context.World, context.Parser, context.Registry, session, player, context.Repository, ct); - } - catch (Exception ex) - { - // Exception isolation per connection - one bad session must not - // take down the listener or other connections. - context.Logger.Error(ex, "Session error for {SessionId}", session.SessionId); - } - } -} diff --git a/src/SharpMud.Adapters.Telnet/ServiceCollectionExtensions.cs b/src/SharpMud.Adapters.Telnet/ServiceCollectionExtensions.cs new file mode 100644 index 0000000..91ab806 --- /dev/null +++ b/src/SharpMud.Adapters.Telnet/ServiceCollectionExtensions.cs @@ -0,0 +1,14 @@ +using Microsoft.Extensions.DependencyInjection; + +namespace SharpMud.Adapters.Telnet; + +public static class ServiceCollectionExtensions +{ + /// Registers a background service that accepts Telnet connections on and runs each through /. + public static IServiceCollection AddSharpMudTelnetTransport(this IServiceCollection services, int port) + { + services.AddSingleton(new TelnetTransportOptions(port)); + services.AddHostedService(); + return services; + } +} diff --git a/src/SharpMud.Adapters.Telnet/SharpMud.Adapters.Telnet.csproj b/src/SharpMud.Adapters.Telnet/SharpMud.Adapters.Telnet.csproj index fdd3bcd..7321fbb 100644 --- a/src/SharpMud.Adapters.Telnet/SharpMud.Adapters.Telnet.csproj +++ b/src/SharpMud.Adapters.Telnet/SharpMud.Adapters.Telnet.csproj @@ -2,10 +2,15 @@ + + + @@ -17,7 +22,7 @@ - net11.0 + net10.0;net11.0 enable enable diff --git a/src/SharpMud.Adapters.Telnet/TelnetTransportBackgroundService.cs b/src/SharpMud.Adapters.Telnet/TelnetTransportBackgroundService.cs new file mode 100644 index 0000000..28fa696 --- /dev/null +++ b/src/SharpMud.Adapters.Telnet/TelnetTransportBackgroundService.cs @@ -0,0 +1,92 @@ +using LayeredCraft.StructuredLogging; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Logging; +using SharpMud.Engine.Behaviors; +using SharpMud.Engine.Sessions; +using SharpMud.Hosting; + +namespace SharpMud.Adapters.Telnet; + +// Absorbs what today's HostRunner.cs (src/SharpMud.Host) does - accepts +// Telnet connections and, per connection, runs LoginFlow then SessionLoop - +// but as a DI-constructed BackgroundService instead of a static class +// manually threaded through Program.cs. See +// docs/adr/0006-nuget-package-distribution.md for why this lives here and +// not in SharpMud.Hosting (Hosting must not reference this project). +internal sealed class TelnetTransportBackgroundService : BackgroundService +{ + private readonly TelnetTransportOptions _options; + private readonly IServiceProvider _serviceProvider; + private readonly ILogger _sessionLogger; + private readonly ILogger _logger; + + // IServiceProvider, not LoginFlow/SessionLoop directly - the generic + // host resolves every registered IHostedService (constructing all of + // them) before calling any of their StartAsync methods. LoginFlow's + // own constructor is harmless, but it depends on ICommandRegistry, + // whose registration factory (AddSharpMudRuleset) can call into a + // consumer's ruleset registration code that reads + // WorldContext.StartingRoom - which isn't populated yet at that point. + // Deferring resolution to ExecuteAsync/HandleConnectionAsync (which + // only run after StartAsync, sequenced after + // WorldLoaderHostedService.StartAsync per registration order) avoids + // that. This is the legitimate "resolving something at runtime" + // exception to the no-service-locator rule in coding-standards.md. + public TelnetTransportBackgroundService( + TelnetTransportOptions options, + IServiceProvider serviceProvider, + ILogger sessionLogger, + ILogger logger) + { + _options = options; + _serviceProvider = serviceProvider; + _sessionLogger = sessionLogger; + _logger = logger; + } + + protected override async Task ExecuteAsync(CancellationToken stoppingToken) + { + var listener = new TelnetListener(_options.Port, _sessionLogger); + listener.Start(); + _logger.Information("Listening for telnet connections on port {Port}", _options.Port); + + var connectionTasks = new List(); + + try + { + await foreach (var session in listener.AcceptSessionsAsync(stoppingToken)) + connectionTasks.Add(HandleConnectionAsync(session, stoppingToken)); + } + finally + { + listener.Stop(); + await Task.WhenAll(connectionTasks); + } + } + + private async Task HandleConnectionAsync(ISession session, CancellationToken ct) + { + try + { + var loginFlow = _serviceProvider.GetRequiredService(); + var player = await loginFlow.RunAsync(session, ct); + if (player is null) + { + await session.DisconnectAsync("Login failed.", ct); + return; + } + + player.FindBehavior()!.Session = session; + + var sessionLoop = _serviceProvider.GetRequiredService(); + await sessionLoop.RunAsync(session, player, ct); + } + catch (Exception ex) + { + // Exception isolation per connection - one bad session must not + // take down the listener or other connections. + _logger.Error(ex, "Session error for {SessionId}", session.SessionId); + } + } +} diff --git a/src/SharpMud.Adapters.Telnet/TelnetTransportOptions.cs b/src/SharpMud.Adapters.Telnet/TelnetTransportOptions.cs new file mode 100644 index 0000000..60cdcec --- /dev/null +++ b/src/SharpMud.Adapters.Telnet/TelnetTransportOptions.cs @@ -0,0 +1,7 @@ +namespace SharpMud.Adapters.Telnet; + +// Registered as a singleton by AddSharpMudTelnetTransport - the port a +// BackgroundService (DI-constructed, no direct call-time arguments) needs +// isn't otherwise reachable from TelnetTransportBackgroundService's +// constructor. +internal sealed record TelnetTransportOptions(int Port); diff --git a/src/SharpMud.Engine/SharpMud.Engine.csproj b/src/SharpMud.Engine/SharpMud.Engine.csproj index 50657a5..58c8686 100644 --- a/src/SharpMud.Engine/SharpMud.Engine.csproj +++ b/src/SharpMud.Engine/SharpMud.Engine.csproj @@ -1,7 +1,7 @@  - net11.0 + net10.0;net11.0 enable enable diff --git a/src/SharpMud.Hosting/GameLoopHostedService.cs b/src/SharpMud.Hosting/GameLoopHostedService.cs index 3741f83..7783860 100644 --- a/src/SharpMud.Hosting/GameLoopHostedService.cs +++ b/src/SharpMud.Hosting/GameLoopHostedService.cs @@ -1,3 +1,4 @@ +using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; using SharpMud.Engine.Ticking; @@ -14,17 +15,29 @@ namespace SharpMud.Hosting; internal sealed class GameLoopHostedService : BackgroundService { private readonly IGameLoop _gameLoop; - private readonly IEnumerable _tickables; + private readonly IServiceProvider _serviceProvider; - public GameLoopHostedService(IGameLoop gameLoop, IEnumerable tickables) + // IServiceProvider, not IEnumerable directly - the generic + // host resolves every registered IHostedService (constructing all of + // them) before calling any of their StartAsync methods, so a + // constructor-injected IEnumerable would resolve + // WanderManager/LinkdeadSweeper/a consumer's CombatManager - all of + // which read WorldContext.World/.StartingRoom - before + // WorldLoaderHostedService.StartAsync has actually populated it. + // Deferring resolution to ExecuteAsync (which only runs after + // StartAsync, sequenced after WorldLoaderHostedService per + // registration order) avoids that. This is the legitimate + // "resolving something at runtime" exception to the no-service-locator + // rule in coding-standards.md. + public GameLoopHostedService(IGameLoop gameLoop, IServiceProvider serviceProvider) { _gameLoop = gameLoop; - _tickables = tickables; + _serviceProvider = serviceProvider; } protected override Task ExecuteAsync(CancellationToken stoppingToken) { - foreach (var tickable in _tickables) + foreach (var tickable in _serviceProvider.GetServices()) _gameLoop.Register(tickable); return _gameLoop.RunAsync(stoppingToken); diff --git a/src/SharpMud.Hosting/IStorageInitializer.cs b/src/SharpMud.Hosting/IStorageInitializer.cs new file mode 100644 index 0000000..54be945 --- /dev/null +++ b/src/SharpMud.Hosting/IStorageInitializer.cs @@ -0,0 +1,17 @@ +namespace SharpMud.Hosting; + +/// +/// One-time storage setup (e.g. EF Core's EnsureCreatedAsync) a +/// persistence package needs before +/// can query it. Registered via DI +/// (services.AddSingleton<IStorageInitializer, ...>()) rather +/// than relying on hosted-service registration order between packages, +/// which would be fragile - always +/// runs every registered initializer before attempting to load anything, +/// regardless of which order the persistence/hosting packages happened to +/// register in. +/// +public interface IStorageInitializer +{ + Task InitializeAsync(CancellationToken ct); +} diff --git a/src/SharpMud.Hosting/ServiceCollectionExtensions.cs b/src/SharpMud.Hosting/ServiceCollectionExtensions.cs index 0b108b2..af1c2aa 100644 --- a/src/SharpMud.Hosting/ServiceCollectionExtensions.cs +++ b/src/SharpMud.Hosting/ServiceCollectionExtensions.cs @@ -16,16 +16,19 @@ public static class ServiceCollectionExtensions /// etc.) - a consumer's callback /// only ever needs to add their own ruleset's commands on top, never /// has to remember to call BuiltinCommands.RegisterAll itself. + /// The callback also receives the built + /// so ruleset-specific command dependencies (e.g. a combat manager) + /// can be resolved from DI. /// - public static IServiceCollection AddSharpMudRuleset(this IServiceCollection services, Action registerRuleset) + public static IServiceCollection AddSharpMudRuleset(this IServiceCollection services, Action registerRuleset) { ArgumentNullException.ThrowIfNull(registerRuleset); - services.AddSingleton(_ => + services.AddSingleton(sp => { var registry = new CommandRegistry(); BuiltinCommands.RegisterAll(registry); - registerRuleset(registry); + registerRuleset(sp, registry); return registry; }); @@ -70,9 +73,14 @@ internal static IServiceCollection AddSharpMudHostingCore(this IServiceCollectio services.AddSingleton(sp => new WanderManager(sp.GetRequiredService().World, sp.GetRequiredService())); services.AddSingleton(sp => new LinkdeadSweeper(sp.GetRequiredService().World, sp.GetRequiredService())); - services.AddTransient(); - services.AddTransient(); - services.AddTransient(); + // Singleton, not per-connection Transient/Scoped - all three hold + // no per-connection state, only injected singleton dependencies + // (WorldContext/parser/registry/repository/factory), so one shared + // instance is equivalent and avoids a DI-scope dance in every + // transport's connection-accept loop. + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); services.AddHostedService(); services.AddHostedService(); diff --git a/src/SharpMud.Hosting/SharpMud.Hosting.csproj b/src/SharpMud.Hosting/SharpMud.Hosting.csproj index 589f388..e50d5d3 100644 --- a/src/SharpMud.Hosting/SharpMud.Hosting.csproj +++ b/src/SharpMud.Hosting/SharpMud.Hosting.csproj @@ -25,7 +25,7 @@ - net11.0 + net10.0;net11.0 enable enable diff --git a/src/SharpMud.Hosting/HostOptions.cs b/src/SharpMud.Hosting/SharpMudHostOptions.cs similarity index 60% rename from src/SharpMud.Hosting/HostOptions.cs rename to src/SharpMud.Hosting/SharpMudHostOptions.cs index 7c715e6..66e0608 100644 --- a/src/SharpMud.Hosting/HostOptions.cs +++ b/src/SharpMud.Hosting/SharpMudHostOptions.cs @@ -6,12 +6,15 @@ namespace SharpMud.Hosting; // (SHARPMUD_MODE/SHARPMUD_TELNET_PORT/--telnet) is not part of this type - // it moved to the consumer's own composition root, since Hosting must not // know which transport(s) a consumer wants (docs/adr/0006-nuget-package-distribution.md). -public sealed record HostOptions(string DbPath) +// Named SharpMudHostOptions, not HostOptions - the latter collides with +// Microsoft.Extensions.Hosting.HostOptions, a real BCL type every consumer +// of this package will also have in scope. +public sealed record SharpMudHostOptions(string DbPath) { - public static HostOptions Parse(IReadOnlyDictionary env) + public static SharpMudHostOptions Parse(IReadOnlyDictionary env) { var dbPath = env.GetValueOrDefault("SHARPMUD_DB_PATH") ?? "./sharpmud.db"; - return new HostOptions(dbPath); + return new SharpMudHostOptions(dbPath); } } diff --git a/src/SharpMud.Hosting/WorldLoaderHostedService.cs b/src/SharpMud.Hosting/WorldLoaderHostedService.cs index 7c67b8b..e35e895 100644 --- a/src/SharpMud.Hosting/WorldLoaderHostedService.cs +++ b/src/SharpMud.Hosting/WorldLoaderHostedService.cs @@ -18,19 +18,31 @@ internal sealed class WorldLoaderHostedService : IHostedService private readonly IThingRepository _repository; private readonly IWorldBuilder _worldBuilder; private readonly WorldContext _worldContext; + private readonly IEnumerable _storageInitializers; private readonly ILogger _logger; public WorldLoaderHostedService( - IThingRepository repository, IWorldBuilder worldBuilder, WorldContext worldContext, ILogger logger) + IThingRepository repository, + IWorldBuilder worldBuilder, + WorldContext worldContext, + IEnumerable storageInitializers, + ILogger logger) { _repository = repository; _worldBuilder = worldBuilder; _worldContext = worldContext; + _storageInitializers = storageInitializers; _logger = logger; } public async Task StartAsync(CancellationToken cancellationToken) { + // Run before the load below, regardless of hosted-service + // registration order between the Hosting/Persistence packages - + // see IStorageInitializer. + foreach (var initializer in _storageInitializers) + await initializer.InitializeAsync(cancellationToken); + var loadedRoot = await _repository.LoadTreeAsync(_worldBuilder.RootId, cancellationToken); if (loadedRoot is not null) diff --git a/src/SharpMud.Persistence.DynamoDb/ServiceCollectionExtensions.cs b/src/SharpMud.Persistence.DynamoDb/ServiceCollectionExtensions.cs new file mode 100644 index 0000000..785477d --- /dev/null +++ b/src/SharpMud.Persistence.DynamoDb/ServiceCollectionExtensions.cs @@ -0,0 +1,34 @@ +using Amazon.DynamoDBv2; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.DependencyInjection; +using SharpMud.Engine.Core; +using SharpMud.Persistence; + +namespace SharpMud.Persistence.DynamoDb; + +public static class ServiceCollectionExtensions +{ + /// + /// Registers backed by DynamoDB. + /// is optional - omit it to use the + /// standard AWS credential chain and environment-configured region; see + /// EntityFrameworkCore.DynamoDb's Client Setup docs for overriding the + /// endpoint (e.g. DynamoDB Local for development). + /// + /// + /// No is registered here - + /// unlike SQLite, this provider does not create or migrate DynamoDB + /// tables; the table(s) must already exist. + /// + public static IServiceCollection AddSharpMudDynamoDbPersistence(this IServiceCollection services, Action? configureClient = null) + { + services.AddDbContextFactory(options => options.UseDynamo(dynamoOptions => + { + if (configureClient is not null) + dynamoOptions.ConfigureDynamoDbClientConfig(configureClient); + })); + services.AddSingleton(); + + return services; + } +} diff --git a/src/SharpMud.Persistence.DynamoDb/SharpMud.Persistence.DynamoDb.csproj b/src/SharpMud.Persistence.DynamoDb/SharpMud.Persistence.DynamoDb.csproj new file mode 100644 index 0000000..c2d7725 --- /dev/null +++ b/src/SharpMud.Persistence.DynamoDb/SharpMud.Persistence.DynamoDb.csproj @@ -0,0 +1,25 @@ + + + + + + + + + + + + + + + + net10.0 + enable + enable + + + diff --git a/src/SharpMud.Persistence.Sqlite/ServiceCollectionExtensions.cs b/src/SharpMud.Persistence.Sqlite/ServiceCollectionExtensions.cs new file mode 100644 index 0000000..981aafd --- /dev/null +++ b/src/SharpMud.Persistence.Sqlite/ServiceCollectionExtensions.cs @@ -0,0 +1,22 @@ +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.DependencyInjection; +using SharpMud.Engine.Core; +using SharpMud.Hosting; +using SharpMud.Persistence; + +namespace SharpMud.Persistence.Sqlite; + +public static class ServiceCollectionExtensions +{ + /// Registers backed by SQLite at . + public static IServiceCollection AddSharpMudSqlitePersistence(this IServiceCollection services, string dbPath) + { + ArgumentNullException.ThrowIfNull(dbPath); + + services.AddDbContextFactory(options => options.UseSqlite($"Data Source={dbPath}")); + services.AddSingleton(); + services.AddSingleton(); + + return services; + } +} diff --git a/src/SharpMud.Persistence.Sqlite/SharpMud.Persistence.Sqlite.csproj b/src/SharpMud.Persistence.Sqlite/SharpMud.Persistence.Sqlite.csproj new file mode 100644 index 0000000..d38910b --- /dev/null +++ b/src/SharpMud.Persistence.Sqlite/SharpMud.Persistence.Sqlite.csproj @@ -0,0 +1,25 @@ + + + + + + + + + + + + + + + + + + net10.0;net11.0 + enable + enable + + + diff --git a/src/SharpMud.Persistence.Sqlite/SqliteStorageInitializer.cs b/src/SharpMud.Persistence.Sqlite/SqliteStorageInitializer.cs new file mode 100644 index 0000000..f3fd9e1 --- /dev/null +++ b/src/SharpMud.Persistence.Sqlite/SqliteStorageInitializer.cs @@ -0,0 +1,26 @@ +using Microsoft.EntityFrameworkCore; +using SharpMud.Hosting; +using SharpMud.Persistence; + +namespace SharpMud.Persistence.Sqlite; + +// EnsureCreated only, never EnsureDeleted, at boot - creates the schema if +// missing but never wipes existing data. See docs/persistence.md Schema/ +// Migrations: a genuinely changed C# model against an old .db file during +// dev means deleting the file by hand, not an automatic wipe every startup +// (which would defeat persistence entirely). +internal sealed class SqliteStorageInitializer : IStorageInitializer +{ + private readonly IDbContextFactory _contextFactory; + + public SqliteStorageInitializer(IDbContextFactory contextFactory) + { + _contextFactory = contextFactory; + } + + public async Task InitializeAsync(CancellationToken ct) + { + await using var dbContext = await _contextFactory.CreateDbContextAsync(ct); + await dbContext.Database.EnsureCreatedAsync(ct); + } +} diff --git a/src/SharpMud.Persistence/SharpMud.Persistence.csproj b/src/SharpMud.Persistence/SharpMud.Persistence.csproj index 04738ca..9739512 100644 --- a/src/SharpMud.Persistence/SharpMud.Persistence.csproj +++ b/src/SharpMud.Persistence/SharpMud.Persistence.csproj @@ -5,13 +5,15 @@ + - - - net11.0 + net10.0;net11.0 enable enable diff --git a/tests/SharpMud.Adapters.Cli.Tests/ServiceCollectionExtensionsTests.cs b/tests/SharpMud.Adapters.Cli.Tests/ServiceCollectionExtensionsTests.cs new file mode 100644 index 0000000..08b7461 --- /dev/null +++ b/tests/SharpMud.Adapters.Cli.Tests/ServiceCollectionExtensionsTests.cs @@ -0,0 +1,17 @@ +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; + +namespace SharpMud.Adapters.Cli.Tests; + +public sealed class ServiceCollectionExtensionsTests +{ + [Fact] + public void AddSharpMudCliTransport_RegistersHostedService() + { + var services = new ServiceCollection(); + + services.AddSharpMudCliTransport(); + + services.Should().Contain(d => d.ServiceType == typeof(IHostedService) && d.ImplementationType == typeof(CliTransportBackgroundService)); + } +} diff --git a/tests/SharpMud.Adapters.Cli.Tests/SharpMud.Adapters.Cli.Tests.csproj b/tests/SharpMud.Adapters.Cli.Tests/SharpMud.Adapters.Cli.Tests.csproj new file mode 100644 index 0000000..8fda048 --- /dev/null +++ b/tests/SharpMud.Adapters.Cli.Tests/SharpMud.Adapters.Cli.Tests.csproj @@ -0,0 +1,40 @@ + + + + enable + enable + Exe + SharpMud.Adapters.Cli.Tests + net11.0 + false + true + + true + true + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/tests/SharpMud.Adapters.Cli.Tests/xunit.runner.json b/tests/SharpMud.Adapters.Cli.Tests/xunit.runner.json new file mode 100644 index 0000000..86c7ea0 --- /dev/null +++ b/tests/SharpMud.Adapters.Cli.Tests/xunit.runner.json @@ -0,0 +1,3 @@ +{ + "$schema": "https://xunit.net/schema/current/xunit.runner.schema.json" +} diff --git a/tests/SharpMud.Adapters.Telnet.Tests/ServiceCollectionExtensionsTests.cs b/tests/SharpMud.Adapters.Telnet.Tests/ServiceCollectionExtensionsTests.cs new file mode 100644 index 0000000..c987011 --- /dev/null +++ b/tests/SharpMud.Adapters.Telnet.Tests/ServiceCollectionExtensionsTests.cs @@ -0,0 +1,19 @@ +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; + +namespace SharpMud.Adapters.Telnet.Tests; + +public sealed class ServiceCollectionExtensionsTests +{ + [Fact] + public void AddSharpMudTelnetTransport_RegistersHostedServiceAndOptions() + { + var services = new ServiceCollection(); + + services.AddSharpMudTelnetTransport(4001); + + services.Should().Contain(d => d.ServiceType == typeof(IHostedService) && d.ImplementationType == typeof(TelnetTransportBackgroundService)); + var provider = services.BuildServiceProvider(); + provider.GetRequiredService().Port.Should().Be(4001); + } +} diff --git a/tests/SharpMud.Adapters.Telnet.Tests/SharpMud.Adapters.Telnet.Tests.csproj b/tests/SharpMud.Adapters.Telnet.Tests/SharpMud.Adapters.Telnet.Tests.csproj index f4e71d7..4cbe781 100644 --- a/tests/SharpMud.Adapters.Telnet.Tests/SharpMud.Adapters.Telnet.Tests.csproj +++ b/tests/SharpMud.Adapters.Telnet.Tests/SharpMud.Adapters.Telnet.Tests.csproj @@ -30,6 +30,7 @@ + diff --git a/tests/SharpMud.Hosting.Tests/HostOptionsTests.cs b/tests/SharpMud.Hosting.Tests/SharpMudHostOptionsTests.cs similarity index 70% rename from tests/SharpMud.Hosting.Tests/HostOptionsTests.cs rename to tests/SharpMud.Hosting.Tests/SharpMudHostOptionsTests.cs index cbab9f3..8c2fa44 100644 --- a/tests/SharpMud.Hosting.Tests/HostOptionsTests.cs +++ b/tests/SharpMud.Hosting.Tests/SharpMudHostOptionsTests.cs @@ -2,12 +2,12 @@ namespace SharpMud.Hosting.Tests; -public sealed class HostOptionsTests +public sealed class SharpMudHostOptionsTests { [Fact] public void Parse_DefaultsDbPath_WhenEnvVarNotSet() { - var options = HostOptions.Parse(new Dictionary()); + var options = SharpMudHostOptions.Parse(new Dictionary()); options.DbPath.Should().Be("./sharpmud.db"); } @@ -17,7 +17,7 @@ public void Parse_UsesDbPathFromEnvVar_WhenSet() { var env = new Dictionary { ["SHARPMUD_DB_PATH"] = "/data/sharpmud.db" }; - var options = HostOptions.Parse(env); + var options = SharpMudHostOptions.Parse(env); options.DbPath.Should().Be("/data/sharpmud.db"); } diff --git a/tests/SharpMud.Persistence.Tests/SharpMud.Persistence.Tests.csproj b/tests/SharpMud.Persistence.Tests/SharpMud.Persistence.Tests.csproj index 52d5649..eb990ec 100644 --- a/tests/SharpMud.Persistence.Tests/SharpMud.Persistence.Tests.csproj +++ b/tests/SharpMud.Persistence.Tests/SharpMud.Persistence.Tests.csproj @@ -42,7 +42,7 @@ - + diff --git a/tests/SharpMud.Samples.Classic.Tests/SharpMud.Samples.Classic.Tests.csproj b/tests/SharpMud.Samples.Classic.Tests/SharpMud.Samples.Classic.Tests.csproj index 1c6508b..704d8b2 100644 --- a/tests/SharpMud.Samples.Classic.Tests/SharpMud.Samples.Classic.Tests.csproj +++ b/tests/SharpMud.Samples.Classic.Tests/SharpMud.Samples.Classic.Tests.csproj @@ -40,7 +40,7 @@ - + From 530d2e79ff5a8b48685ca1a309bad4a9a3ec7e53 Mon Sep 17 00:00:00 2001 From: Nick Cipollina Date: Mon, 20 Jul 2026 12:37:55 -0400 Subject: [PATCH 03/14] fix(cli): stop the host when the CLI's one-shot session ends Manual verification caught a real behavior regression: under the old procedural Program.cs, the CLI branch's SessionLoop.RunAsync returning meant the whole process fell through to shutdown. Under IHost, CliTransportBackgroundService.ExecuteAsync returning doesn't stop anything else - the host just kept running GameLoop forever, waiting for a SIGINT/SIGTERM a local CLI user has no reason to send. CliTransportBackgroundService now calls IHostApplicationLifetime .StopApplication() once its single session ends. Telnet's BackgroundService deliberately does not do this - many concurrent sessions, one ending must not stop the host. Verified end-to-end: CLI mode (look/move/quit, persists across runs, exits cleanly on quit), Telnet mode (real TCP connection - IAC negotiation, username/password character creation, look/quit, host stays alive after the client disconnects), and graceful SIGTERM shutdown (process exits promptly, world data actually lands on disk - confirmed via direct sqlite3 query). Co-Authored-By: Claude Sonnet 5 --- .../CliTransportBackgroundService.cs | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/src/SharpMud.Adapters.Cli/CliTransportBackgroundService.cs b/src/SharpMud.Adapters.Cli/CliTransportBackgroundService.cs index ff4929b..d19bc22 100644 --- a/src/SharpMud.Adapters.Cli/CliTransportBackgroundService.cs +++ b/src/SharpMud.Adapters.Cli/CliTransportBackgroundService.cs @@ -10,14 +10,16 @@ namespace SharpMud.Adapters.Cli; internal sealed class CliTransportBackgroundService : BackgroundService { private readonly IServiceProvider _serviceProvider; + private readonly IHostApplicationLifetime _lifetime; // IServiceProvider, not PlayerLogin/SessionLoop directly - see // TelnetTransportBackgroundService's constructor comment for why // eager constructor injection here would resolve WorldContext-dependent // services before WorldLoaderHostedService.StartAsync has populated it. - public CliTransportBackgroundService(IServiceProvider serviceProvider) + public CliTransportBackgroundService(IServiceProvider serviceProvider, IHostApplicationLifetime lifetime) { _serviceProvider = serviceProvider; + _lifetime = lifetime; } protected override async Task ExecuteAsync(CancellationToken stoppingToken) @@ -29,5 +31,17 @@ protected override async Task ExecuteAsync(CancellationToken stoppingToken) var sessionLoop = _serviceProvider.GetRequiredService(); await sessionLoop.RunAsync(session, player, stoppingToken); + + // CLI mode is a single, one-shot local session (SPEC.md) - once it + // ends (quit or disconnect), the whole process should exit, matching + // the pre-refactor behavior (Program.cs used to just fall through + // to shutdown once this returned). Without this, IHost keeps + // running indefinitely (GameLoop, etc.) with no session left to + // serve, waiting for an external SIGINT/SIGTERM that a local CLI + // user has no reason to expect. Telnet's BackgroundService + // deliberately does NOT do this - many concurrent sessions, one + // ending must not stop the host. + if (!stoppingToken.IsCancellationRequested) + _lifetime.StopApplication(); } } From d85175cf3fda1f7745c382bc74dd28f5f01c78f1 Mon Sep 17 00:00:00 2001 From: Nick Cipollina Date: Mon, 20 Jul 2026 13:13:45 -0400 Subject: [PATCH 04/14] feat: packaging metadata, CI publish workflows, docs site skeleton, and subsystem doc corrections for PLAN-0006 Completes PLAN-0006: Directory.Build.props/LICENSE/icon/meta-package/ publish-preview+release workflows for the SharpMud.* NuGet packages; docsite/ (Zensical+uv) GitHub Pages skeleton with a real Getting Started walkthrough; every subsystem doc updated off the old SharpMud.Host/ Ruleset.Classic paths. Verified via dotnet pack + a real consumer project built against the packed nupkgs over both CLI and Telnet transports, which caught and fixed a doc bug (CLI is login-free, not the login flow the doc originally claimed). Co-Authored-By: Claude Sonnet 5 --- .github/workflows/docs.yaml | 78 +++++ .github/workflows/publish-preview.yaml | 38 ++ .github/workflows/publish-release.yaml | 30 ++ .gitignore | 4 + Directory.Build.props | 54 +++ Dockerfile | 14 +- LICENSE | 21 ++ NOTICE.md | 21 ++ README.md | 46 ++- SPEC.md | 8 +- SharpMud.slnx | 1 + docs/README.md | 6 +- docs/accounts-auth.md | 20 +- docs/architecture.md | 70 ++-- docs/character.md | 2 +- docs/combat.md | 2 +- docs/deployment.md | 20 +- docs/engine-vs-ruleset.md | 55 +-- docs/networking.md | 19 +- docs/persistence.md | 46 ++- docs/plans/0006-nuget-package-distribution.md | 106 +++--- docs/plans/README.md | 2 +- docsite/docs/getting-started.md | 110 ++++++ docsite/docs/images/icon.png | Bin 0 -> 6852 bytes docsite/docs/index.md | 28 ++ docsite/pyproject.toml | 10 + docsite/uv.lock | 324 ++++++++++++++++++ docsite/zensical.toml | 70 ++++ icon.png | Bin 0 -> 6852 bytes .../ServiceCollectionExtensions.cs | 7 +- src/SharpMud/SharpMud.csproj | 32 ++ 31 files changed, 1061 insertions(+), 183 deletions(-) create mode 100644 .github/workflows/docs.yaml create mode 100644 .github/workflows/publish-preview.yaml create mode 100644 .github/workflows/publish-release.yaml create mode 100644 Directory.Build.props create mode 100644 LICENSE create mode 100644 NOTICE.md create mode 100644 docsite/docs/getting-started.md create mode 100644 docsite/docs/images/icon.png create mode 100644 docsite/docs/index.md create mode 100644 docsite/pyproject.toml create mode 100644 docsite/uv.lock create mode 100644 docsite/zensical.toml create mode 100644 icon.png create mode 100644 src/SharpMud/SharpMud.csproj diff --git a/.github/workflows/docs.yaml b/.github/workflows/docs.yaml new file mode 100644 index 0000000..0c50e27 --- /dev/null +++ b/.github/workflows/docs.yaml @@ -0,0 +1,78 @@ +name: Docs + +on: + push: + branches: [ main ] + paths: + - 'docsite/**' + - '.github/workflows/docs.yaml' + pull_request: + types: [opened, synchronize, reopened, ready_for_review] + branches: [ main ] + paths: + - 'docsite/**' + - '.github/workflows/docs.yaml' + +permissions: + contents: read + +concurrency: + group: ${{ github.event_name == 'pull_request' && format('docs-pr-{0}', github.event.pull_request.number) || 'pages' }} + cancel-in-progress: true + +jobs: + build: + if: github.event_name != 'pull_request' || github.event.pull_request.draft == false + + runs-on: ubuntu-latest + defaults: + run: + working-directory: docsite + steps: + - name: Checkout + uses: actions/checkout@v6 + with: + fetch-depth: 0 + + - name: Install pngquant + run: sudo apt-get update && sudo apt-get install -y pngquant + + - name: Setup Python + uses: actions/setup-python@v6 + with: + python-version-file: "docsite/pyproject.toml" + + - name: Install uv + uses: astral-sh/setup-uv@v7 + with: + enable-cache: true + + - name: Install the project + run: uv sync --locked --all-extras --dev + + - name: Build documentation + run: uv run zensical build -f zensical.toml --clean + + - name: Upload artifact + if: github.event_name == 'push' + uses: actions/upload-pages-artifact@v4 + with: + path: docsite/site + + deploy: + if: github.event_name == 'push' && github.ref == 'refs/heads/main' + needs: build + runs-on: ubuntu-latest + permissions: + pages: write + id-token: write + environment: + name: github-pages + url: ${{ steps.deployment.outputs.page_url }} + steps: + - name: Setup Pages + uses: actions/configure-pages@v5 + + - name: Deploy to GitHub Pages + id: deployment + uses: actions/deploy-pages@v4 diff --git a/.github/workflows/publish-preview.yaml b/.github/workflows/publish-preview.yaml new file mode 100644 index 0000000..c00f3f2 --- /dev/null +++ b/.github/workflows/publish-preview.yaml @@ -0,0 +1,38 @@ +name: Publish Preview + +on: + workflow_dispatch: + push: + branches: + - main + paths-ignore: + - 'docs/**' + - 'docsite/**' + - 'README.md' + +permissions: + contents: write + pull-requests: write + +jobs: + build: + uses: LayeredCraft/devops-templates/.github/workflows/publish-preview.yml@v10.1 + with: + solution: SharpMud.slnx + dotnetVersion: | + 10.0.x + 11.0.x + hasTests: true + prereleaseIdentifier: alpha + secrets: inherit + + push: + needs: build + runs-on: ubuntu-latest + permissions: + id-token: write + contents: read + steps: + - uses: LayeredCraft/devops-templates/.github/actions/nuget-push@v10.1 + with: + nuget_user: ${{ secrets.NUGET_USER }} diff --git a/.github/workflows/publish-release.yaml b/.github/workflows/publish-release.yaml new file mode 100644 index 0000000..888e8ac --- /dev/null +++ b/.github/workflows/publish-release.yaml @@ -0,0 +1,30 @@ +name: Publish Release + +on: + release: + types: [published] + +permissions: + contents: write + +jobs: + build: + uses: LayeredCraft/devops-templates/.github/workflows/publish-release.yml@v10.1 + with: + solution: SharpMud.slnx + dotnetVersion: | + 10.0.x + 11.0.x + hasTests: true + secrets: inherit + + push: + needs: build + runs-on: ubuntu-latest + permissions: + id-token: write + contents: read + steps: + - uses: LayeredCraft/devops-templates/.github/actions/nuget-push@v10.1 + with: + nuget_user: ${{ secrets.NUGET_USER }} diff --git a/.gitignore b/.gitignore index 956fed7..6531dc9 100644 --- a/.gitignore +++ b/.gitignore @@ -495,3 +495,7 @@ sharpmud.db sharpmud.db-wal sharpmud.db-shm sharpmud.db-journal + +# docsite (Zensical/uv) build output and Python venv (uv.lock IS committed) +docsite/site/ +docsite/.venv/ diff --git a/Directory.Build.props b/Directory.Build.props new file mode 100644 index 0000000..cded8e0 --- /dev/null +++ b/Directory.Build.props @@ -0,0 +1,54 @@ + + + + MIT + https://github.com/LayeredCraft/sharp-mud + git + Nick Cipollina + https://github.com/LayeredCraft/sharp-mud + icon.png + README.md + false + true + true + false + true + + $(NoWarn);CS1591 + + + + embedded + true + latest + False + + + + + false + + + + + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + diff --git a/Dockerfile b/Dockerfile index 57b212f..cd6cfd0 100644 --- a/Dockerfile +++ b/Dockerfile @@ -4,17 +4,19 @@ FROM mcr.microsoft.com/dotnet/sdk:11.0.100-preview.5 AS build WORKDIR /src -COPY Directory.Packages.props global.json ./ +COPY Directory.Build.props Directory.Packages.props global.json ./ COPY src/SharpMud.Engine/SharpMud.Engine.csproj src/SharpMud.Engine/ -COPY src/SharpMud.Ruleset.Classic/SharpMud.Ruleset.Classic.csproj src/SharpMud.Ruleset.Classic/ +COPY src/SharpMud.Hosting/SharpMud.Hosting.csproj src/SharpMud.Hosting/ COPY src/SharpMud.Persistence/SharpMud.Persistence.csproj src/SharpMud.Persistence/ +COPY src/SharpMud.Persistence.Sqlite/SharpMud.Persistence.Sqlite.csproj src/SharpMud.Persistence.Sqlite/ COPY src/SharpMud.Adapters.Cli/SharpMud.Adapters.Cli.csproj src/SharpMud.Adapters.Cli/ COPY src/SharpMud.Adapters.Telnet/SharpMud.Adapters.Telnet.csproj src/SharpMud.Adapters.Telnet/ -COPY src/SharpMud.Host/SharpMud.Host.csproj src/SharpMud.Host/ -RUN dotnet restore src/SharpMud.Host/SharpMud.Host.csproj +COPY samples/SharpMud.Samples.Classic/SharpMud.Samples.Classic.csproj samples/SharpMud.Samples.Classic/ +RUN dotnet restore samples/SharpMud.Samples.Classic/SharpMud.Samples.Classic.csproj COPY src/ src/ -RUN dotnet publish src/SharpMud.Host/SharpMud.Host.csproj -c Release -o /app --no-restore +COPY samples/ samples/ +RUN dotnet publish samples/SharpMud.Samples.Classic/SharpMud.Samples.Classic.csproj -c Release -o /app --no-restore FROM mcr.microsoft.com/dotnet/runtime:11.0.0-preview.5-alpine3.24 AS runtime WORKDIR /app @@ -40,4 +42,4 @@ VOLUME /data # needing the larger -extra image just for culture data we don't use. ENV DOTNET_SYSTEM_GLOBALIZATION_INVARIANT=true -ENTRYPOINT ["dotnet", "SharpMud.Host.dll"] +ENTRYPOINT ["dotnet", "SharpMud.Samples.Classic.dll"] diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..46d3d3f --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2026 LayeredCraft + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/NOTICE.md b/NOTICE.md new file mode 100644 index 0000000..1e95e12 --- /dev/null +++ b/NOTICE.md @@ -0,0 +1,21 @@ +# Notice + +sharp-mud is licensed under the [MIT License](LICENSE). + +## Design inspiration + +sharp-mud's engine design (the `Thing`/`Behavior` composition model, the +generic event system, session/transport abstractions, and several other +architectural decisions) is informed by a close reading of +[WheelMUD](https://github.com/DavidRieman/WheelMUD)'s source code and +architecture. See [docs/research/wheelmud-findings.md](docs/research/wheelmud-findings.md) +for the full source-dive record of what was adopted, what was deliberately +changed, and why. + +sharp-mud is a clean-room reimplementation, not a fork or derivative of +WheelMUD's source — no WheelMUD code is included in this repository. +WheelMUD itself is licensed under the +[Microsoft Public License (MS-PL)](https://github.com/DavidRieman/WheelMUD/blob/main/src/LICENSE.txt), +which does not apply to sharp-mud's own, independently-written code (see +[ADR-0006](docs/adr/0006-nuget-package-distribution.md)'s License and +naming section for the full reasoning). diff --git a/README.md b/README.md index 21541dd..2a0c3b0 100644 --- a/README.md +++ b/README.md @@ -12,38 +12,52 @@ to the genre's feel while built with current .NET architecture. ## Solution layout ``` -SharpMud.sln +SharpMud.slnx src/ - SharpMud.Engine/ # Thing/Behavior, events, generic behaviors, - # command pipeline, session, tick loop. - # Zero deps on any ruleset. - SharpMud.Ruleset.Classic/ # D&D-flavored ruleset: stats, combat, kill/flee. - # References Engine only. - SharpMud.Persistence/ # EF Core repositories - SharpMud.Adapters.Cli/ # local stdin/stdout session adapter - SharpMud.Adapters.Telnet/ # raw TCP session adapter + listener - SharpMud.Host/ # composition root / entry point / hub world content + SharpMud.Engine/ # Thing/Behavior, events, generic behaviors, + # command pipeline, session, tick loop. + # Zero deps on any ruleset. + SharpMud.Hosting/ # generic-host composition helpers, ruleset-agnostic + SharpMud.Persistence/ # EF Core repositories, provider-agnostic + SharpMud.Persistence.Sqlite/ # SQLite provider + SharpMud.Persistence.DynamoDb/ # DynamoDB provider + SharpMud.Adapters.Cli/ # local stdin/stdout session adapter + SharpMud.Adapters.Telnet/ # raw TCP session adapter + listener + SharpMud/ # meta-package (pulls in every SharpMud.* package) + samples/ + SharpMud.Samples.Classic/ # D&D-flavored sample ruleset + composition root tests/ SharpMud.Engine.Tests/ - SharpMud.Ruleset.Classic.Tests/ + SharpMud.Hosting.Tests/ SharpMud.Persistence.Tests/ - SharpMud.Host.Tests/ + SharpMud.Adapters.Cli.Tests/ + SharpMud.Adapters.Telnet.Tests/ + SharpMud.Samples.Classic.Tests/ ``` ## Building & testing ``` -dotnet build SharpMud.sln -dotnet test SharpMud.sln +dotnet build SharpMud.slnx +dotnet test SharpMud.slnx ``` ## Running ``` -dotnet run --project src/SharpMud.Host # local single-player CLI -dotnet run --project src/SharpMud.Host -- --telnet [port] # telnet server, default port 4000 +dotnet run --project samples/SharpMud.Samples.Classic # local single-player CLI +dotnet run --project samples/SharpMud.Samples.Classic -- --telnet [port] # telnet server, default port 4000 ``` +## Using SharpMud as a library + +`src/` publishes as NuGet packages (`SharpMud.Engine`, `SharpMud.Hosting`, +`SharpMud.Persistence`(`.Sqlite`/`.DynamoDb`), `SharpMud.Adapters.Cli`/ +`.Telnet`), plus a `SharpMud` meta-package pulling in all of them. +`samples/SharpMud.Samples.Classic` is a full reference consumer — start there +to see how a ruleset composes against the packages. See +[ADR-0006](docs/adr/0006-nuget-package-distribution.md) for the design. + ## Containerized ``` diff --git a/SPEC.md b/SPEC.md index 046ab3d..4078841 100644 --- a/SPEC.md +++ b/SPEC.md @@ -22,8 +22,10 @@ persistent public MUD. the prior art this is adapted from and [docs/engine-vs-ruleset.md](docs/engine-vs-ruleset.md) for the concrete design. The game we're actually building (classic D&D-flavored stats/combat, - the hand-built hub) lives in a separate `SharpMud.Ruleset.Classic` project - that consumes the engine the same way a third party's game would. + the hand-built hub) lives in `samples/SharpMud.Samples.Classic`, a reference + consumer of the published `SharpMud.*` NuGet packages + ([ADR-0006](docs/adr/0006-nuget-package-distribution.md)) that consumes the + engine the same way a third party's game would. ## Architecture @@ -38,7 +40,7 @@ optionally a `LockableBehavior`). This is adapted directly from WheelMUD (see [docs/research/wheelmud-findings.md](docs/research/wheelmud-findings.md)) and is the mechanism that makes the engine/ruleset split real: `SharpMud.Engine` ships generic, ruleset-agnostic behaviors (rooms, exits, containment, -identity); `SharpMud.Ruleset.Classic` adds the D&D-flavored ones (stats, +identity); `SharpMud.Samples.Classic` adds the D&D-flavored ones (stats, combat, dice-roll character creation) purely by composing more `Behavior`s onto the same `Thing`s — the engine never references ruleset types. Full design in [docs/engine-vs-ruleset.md](docs/engine-vs-ruleset.md). diff --git a/SharpMud.slnx b/SharpMud.slnx index 92e0c2a..3fd1837 100644 --- a/SharpMud.slnx +++ b/SharpMud.slnx @@ -17,6 +17,7 @@ + diff --git a/docs/README.md b/docs/README.md index 597dace..e181cea 100644 --- a/docs/README.md +++ b/docs/README.md @@ -13,10 +13,10 @@ implementation detail. | [character.md](character.md) | Player entity, D&D-style attributes, Race/Class modifiers, derived stats | | [commands.md](commands.md) | Command parser/registry, `ICommand` pipeline, aliases, error handling, movement walkthrough | | [combat.md](combat.md) | Round-based combat model, `ICombatResolver`, tick-driven resolution, disconnect-mid-fight handling | -| [persistence.md](persistence.md) | Repository interfaces, EF Core, provider strategy (SQLite now, Mongo/DynamoDB later) | -| [networking.md](networking.md) | `ISession` transport abstraction, adapter plan (CLI now, Telnet/SSH/WebSocket later) | +| [persistence.md](persistence.md) | Repository interfaces, EF Core, provider strategy (SQLite and DynamoDB now, via separate provider packages) | +| [networking.md](networking.md) | `ISession` transport abstraction, adapter plan (CLI and Telnet now, SSH/WebSocket later) | | [accounts-auth.md](accounts-auth.md) | Username/password login, identity fields on Player | -| [deployment.md](deployment.md) | Dockerfile, container runtime configuration (`HostOptions`), verified-working notes, AWS/CI open items | +| [deployment.md](deployment.md) | Dockerfile, container runtime configuration (`SharpMudHostOptions`), verified-working notes, AWS/CI open items | | [research/wheelmud-findings.md](research/wheelmud-findings.md) | WheelMUD codebase review with code citations — the prior art behind engine-vs-ruleset.md | | [adr/README.md](adr/README.md) | Architecture Decision Records — the numbered, permanent record of individual design decisions (numbering, status lifecycle, index) | diff --git a/docs/accounts-auth.md b/docs/accounts-auth.md index 2d0739c..7669160 100644 --- a/docs/accounts-auth.md +++ b/docs/accounts-auth.md @@ -18,7 +18,7 @@ simplest model that still has real auth. Classic small-MUD convention (no "alts"); revisit if multi-character-per-login is ever actually wanted. The local CLI stays login-free, per `SPEC.md` — `LoginFlow` is only used by -`HostRunner`'s Telnet path. +`TelnetTransportBackgroundService`'s Telnet path. ## Identity Model @@ -38,9 +38,9 @@ has been deleted. `Thing.Name` (the display name shown to other players) is set equal to `Username` at character creation — there's no separate "character name" concept, matching the "no alts" simplification above. -Password hashing: `PasswordHashing` (`src/SharpMud.Host/PasswordHashing.cs`) +Password hashing: `PasswordHashing` (`src/SharpMud.Hosting/PasswordHashing.cs`) wraps `Microsoft.Extensions.Identity.Core`'s `PasswordHasher` — PBKDF2 -with a random salt, versioned hash format. Lives in `SharpMud.Host`, not +with a random salt, versioned hash format. Lives in `SharpMud.Hosting`, not Engine — this is login-flow infrastructure, not game logic, and Engine shouldn't pick up an Identity package dependency for something only the networked login flow needs. `TUser` is unused by the default hasher's @@ -60,8 +60,9 @@ whether that ever changes). ## Terminal Login Flow -Implemented in `src/SharpMud.Host/LoginFlow.cs`, called from -`HostRunner.HandleConnectionAsync` (replacing the old name-only placeholder): +Implemented in `src/SharpMud.Hosting/LoginFlow.cs`, called from +`TelnetTransportBackgroundService.HandleConnectionAsync` (replacing the old +name-only placeholder): 1. `"Username: "`, read a line. Empty input disconnects. 2. Look up the username — first among currently-live/online players in @@ -89,7 +90,8 @@ Implemented in `src/SharpMud.Host/LoginFlow.cs`, called from anything but `y`) loops back to the username prompt. `y` → `"Password: "` / `"Confirm password: "`, both echo-suppressed; mismatch or empty → `"Passwords didn't match."`, loop back to username. Match → hash, create - the character via `HubWorldBuilder.CreatePlayer`, and + the character via `IPlayerFactory.CreatePlayer` (the sample's + `ClassicPlayerFactory` wraps `HubWorldBuilder.CreatePlayer`), and `IThingRepository.SaveTreeAsync` it immediately (not waiting for the eventual disconnect-triggered save) so a crash right after creation doesn't lose the new login. @@ -117,8 +119,8 @@ tradeoff this project already accepted for v1). ## Verified -Live, over real TCP against `HostRunner`'s Telnet listener, not just unit -tests: a brand-new username creates a character (confirmed the `IAC +Live, over real TCP against `TelnetTransportBackgroundService`'s Telnet +listener, not just unit tests: a brand-new username creates a character (confirmed the `IAC WILL/WONT ECHO` bytes are actually sent, visible as raw bytes to a non-compliant test client); 3 consecutive wrong-password attempts are all rejected with the generic message and the flow loops back to the username @@ -130,7 +132,7 @@ confirmed the password hash itself survives a real process restart against the same SQLite file — not just an in-memory check — by creating an account, restarting the server, and logging in again with the same credentials. -`PasswordHashingTests` (`SharpMud.Host.Tests`) covers: correct password +`PasswordHashingTests` (`SharpMud.Hosting.Tests`) covers: correct password verifies, wrong password fails, and two hashes of the same password are never byte-identical (confirms the random salt is actually being used, not silently skipped). diff --git a/docs/architecture.md b/docs/architecture.md index 5454af8..2e6096b 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -10,33 +10,40 @@ tooling/library support proves too immature). Nullable reference types enabled, implicit usings on, file-scoped namespaces throughout. ``` -sharp-mud.sln +SharpMud.slnx src/ - SharpMud.Engine/ # Thing/Behavior, event system, generic behaviors, - # command pipeline, session abstraction, tick loop. - # Zero deps on Adapters, Persistence, or any ruleset. - SharpMud.Ruleset.Classic/ # D&D-flavored ruleset: stats, combat, kill/flee. - # References Engine only - see engine-vs-ruleset.md. - SharpMud.Persistence/ # EF Core DbContext + repository implementations. - # References Engine (for domain types / repo interfaces). - SharpMud.Adapters.Cli/ # local stdin/stdout ISession implementation. - SharpMud.Adapters.Telnet/ # raw TCP ISession + listener - see networking.md. - SharpMud.Adapters.Ssh/ # (later) - SharpMud.Adapters.WebSocket/ # (later) - SharpMud.Host/ # composition root: DI wiring (Microsoft.Extensions. - # DependencyInjection), config, process entry point. - # The only project allowed to know about a specific - # ruleset, and owns the hand-built hub world content. + SharpMud.Engine/ # Thing/Behavior, event system, generic behaviors, + # command pipeline, session abstraction, tick loop. + # Zero deps on Adapters, Persistence, or any ruleset. + SharpMud.Hosting/ # generic-host composition helpers: WorldContext, + # IWorldBuilder, IPlayerFactory, SessionLoop/LoginFlow, + # AddSharpMud* extension methods. Ruleset-agnostic - + # samples/rulesets plug in via those extension points. + SharpMud.Persistence/ # EF Core DbContext + repository interfaces, provider-agnostic. + # References Engine (for domain types / repo interfaces). + SharpMud.Persistence.Sqlite/ # SQLite provider package: UseSqlite + IStorageInitializer. + SharpMud.Persistence.DynamoDb/ # DynamoDB provider package: UseDynamo (net10.0 only). + SharpMud.Adapters.Cli/ # local stdin/stdout ISession implementation. + SharpMud.Adapters.Telnet/ # raw TCP ISession + listener - see networking.md. + SharpMud.Adapters.Ssh/ # (later) + SharpMud.Adapters.WebSocket/ # (later) + SharpMud/ # meta-package: pulls in every SharpMud.* package. + samples/ + SharpMud.Samples.Classic/ # D&D-flavored sample ruleset + composition root + # (Program.cs). References everything; the only + # place that knows about a specific ruleset and + # owns the hand-built hub world content. tests/ - SharpMud.Engine.Tests/ # xUnit v3 + AutoFixture + NSubstitute + AwesomeAssertions - SharpMud.Ruleset.Classic.Tests/ + SharpMud.Engine.Tests/ # xUnit v3 + AutoFixture + NSubstitute + AwesomeAssertions + SharpMud.Hosting.Tests/ SharpMud.Persistence.Tests/ + SharpMud.Samples.Classic.Tests/ ``` **Dependency direction (strict, enforced by project references):** -`Ruleset.Classic → Engine`, `Adapters.* → Engine`, `Persistence → Engine`, -`Host → everything`. Engine never references Adapters, Persistence, or any -ruleset — see [engine-vs-ruleset.md](engine-vs-ruleset.md) for the full +`Adapters.* → Hosting → Engine`, `Persistence.* → Persistence → Engine`, +`Samples.Classic → everything`. Engine never references Adapters, Persistence, +or any ruleset — see [engine-vs-ruleset.md](engine-vs-ruleset.md) for the full rationale (this is the actual mechanism behind the "engine, not just a game" goal in `SPEC.md`, not just the transport/persistence swappability described below). This is also what makes transports (see [networking.md](networking.md)) @@ -61,24 +68,27 @@ Single server-wide heartbeat (per SPEC.md) that calls `OnTickAsync` on every registered `ITickable` — combat rounds in progress, NPC AI, regen. Player commands (movement, look, chat) are NOT gated by the tick; they execute immediately via the command pipeline (see [commands.md](commands.md)). -`Host` now starts `GameLoop.RunAsync` as a background task alongside the -session's read loop, since `CombatManager` (see [combat.md](combat.md)) is -the first real `ITickable` consumer. +`GameLoopHostedService` (in `SharpMud.Hosting`) runs `IGameLoop.RunAsync` as a +`BackgroundService` alongside the session read loop, since `CombatManager` +(see [combat.md](combat.md)) is the first real `ITickable` consumer. ## Dependency Injection -`Microsoft.Extensions.DependencyInjection`, standard container. `Host` is the -only project that composes the graph: registers the chosen `IPlayerRepository`/ -`IRoomRepository` implementations (see [persistence.md](persistence.md)), -the chosen `ISession`-producing adapter (see [networking.md](networking.md)), -and engine services (`IGameLoop`, `ICommandRegistry`, `ICombatResolver`, etc). +`Microsoft.Extensions.DependencyInjection`, standard container, wired via the +.NET generic host (`Microsoft.Extensions.Hosting`). `SharpMud.Hosting` +provides the `AddSharpMud*` extension methods every project composes with; +the sample composition root (`samples/SharpMud.Samples.Classic/Program.cs`) +is the only place that assembles the full graph — the chosen persistence +provider (see [persistence.md](persistence.md)), the chosen transport +(see [networking.md](networking.md)), and engine services (`IGameLoop`, +`ICommandRegistry`, `ICombatResolver`, etc). ## Testing & Observability - **Unit tests** (xUnit v3 + AutoFixture + NSubstitute + AwesomeAssertions, per the `dotnet-unit-testing-patterns` skill conventions) required for: `ICommandParser`, each `ICommand` implementation, `ICombatResolver` (now in - `SharpMud.Ruleset.Classic.Tests`), `Thing`/`BehaviorManager`/`ThingEvents` + `SharpMud.Samples.Classic.Tests`), `Thing`/`BehaviorManager`/`ThingEvents` propagation, stat-derivation formulas. These are pure/deterministic enough to test without a live session or tick loop — `ISession` and repositories are mocked via NSubstitute. diff --git a/docs/character.md b/docs/character.md index 84ace47..2657912 100644 --- a/docs/character.md +++ b/docs/character.md @@ -6,7 +6,7 @@ and [persistence.md](persistence.md) for how a Player is stored. **Superseded by [engine-vs-ruleset.md](engine-vs-ruleset.md)**: `Player` below is no longer a dedicated class — it's a `Thing` composed from an engine-level -`PlayerBehavior` (identity only) plus a `SharpMud.Ruleset.Classic` +`PlayerBehavior` (identity only) plus a `SharpMud.Samples.Classic` `StatsBehavior` (everything on this page). This doc still describes the correct stat shape; engine-vs-ruleset.md describes which project owns which piece and why. diff --git a/docs/combat.md b/docs/combat.md index e75d066..d6fbbe4 100644 --- a/docs/combat.md +++ b/docs/combat.md @@ -6,7 +6,7 @@ subsystem docs. See [character.md](character.md) for Player stats and this system hooks into. **Superseded by [engine-vs-ruleset.md](engine-vs-ruleset.md)**: everything on -this page now lives in `SharpMud.Ruleset.Classic`, not `SharpMud.Engine` - +this page now lives in `SharpMud.Samples.Classic`, not `SharpMud.Engine` - combat is ruleset-specific by design (see the findings doc's §9). `ICombatant` becomes `CombatantBehavior`, attached to whichever `Thing`s the ruleset wants to be able to fight; `Player`/`Npc` references below mean "a `Thing` with the diff --git a/docs/deployment.md b/docs/deployment.md index 9b49380..1bf68af 100644 --- a/docs/deployment.md +++ b/docs/deployment.md @@ -7,10 +7,11 @@ server this containerizes. ## Container `Dockerfile` (repo root) is a multi-stage build: `mcr.microsoft.com/dotnet/sdk` -restores/publishes `SharpMud.Host` (which pulls in every project it -references — Engine, Ruleset.Classic, Persistence, both adapters), then the -published output is copied into a much smaller `mcr.microsoft.com/dotnet/runtime` -Alpine image. Both stages are pinned to the exact SDK/runtime version in +restores/publishes `SharpMud.Samples.Classic` (which pulls in every project it +references — Engine, Hosting, Persistence + Persistence.Sqlite, both +adapters), then the published output is copied into a much smaller +`mcr.microsoft.com/dotnet/runtime` Alpine image. Both stages are pinned to the +exact SDK/runtime version in `global.json` (`11.0.100-preview.5`) — confirmed to exist on MCR as of this writing; see [architecture.md](architecture.md) Open Items for the .NET 10 LTS fallback plan if that ever stops being true. @@ -22,7 +23,9 @@ docker run -p 4000:4000 sharpmud ## Runtime Configuration -`HostOptions.Parse` (`src/SharpMud.Host/HostOptions.cs`) resolves the run +`SharpMudHostOptions.Parse` (`src/SharpMud.Hosting/SharpMudHostOptions.cs`) +resolves the DB path from an env var; the sample's `Program.cs` +(`samples/SharpMud.Samples.Classic/Program.cs`) separately resolves the run mode from, in precedence order: CLI args, then environment variables, then a default (CLI/local mode). This is what lets the same image work both as `docker run sharpmud` (telnet, via the Dockerfile's `ENV` defaults) and @@ -46,9 +49,10 @@ discarded along with the container itself on `docker rm`/replace (a restart of the *same* container without removal would still happen to work, but that is not the real redeploy scenario and shouldn't be relied on). -Graceful shutdown (both `SIGINT` and `SIGTERM`, via `PosixSignalRegistration` -— see [persistence.md](persistence.md)'s Write Frequency section for why -`Console.CancelKeyPress` was wrong for this) is what makes `docker stop` +Graceful shutdown (both `SIGINT` and `SIGTERM`, via the .NET generic host's +own shutdown sequence — see [persistence.md](persistence.md)'s Write +Frequency section for why `Console.CancelKeyPress` was wrong for this) is +what makes `docker stop` actually save state before the container exits; see Verified below for a real `docker stop` → container removed → fresh container from the same image → data still there test. diff --git a/docs/engine-vs-ruleset.md b/docs/engine-vs-ruleset.md index 3e61cfc..819ce5e 100644 --- a/docs/engine-vs-ruleset.md +++ b/docs/engine-vs-ruleset.md @@ -28,23 +28,31 @@ src/ (Room/Area/Exit/Lockable/Player-identity/Npc-marker/ Wearable/Container), command pipeline, session abstraction, tick loop. Zero ruleset knowledge. - SharpMud.Ruleset.Classic/ D&D-flavored ruleset: Race/CharacterClass, stats, - CombatantBehavior, combat resolver, kill/flee - commands, dice-roll character creation. + SharpMud.Hosting/ Generic-host composition helpers (WorldContext, + IWorldBuilder, IPlayerFactory, SessionLoop/LoginFlow, + AddSharpMud* extensions). Ruleset-agnostic. + SharpMud.Persistence/ EF Core repositories, provider-agnostic. References Engine only. - SharpMud.Persistence/ EF Core repositories. References Engine only. - SharpMud.Adapters.Cli/ Local stdin/stdout ISession. References Engine only. - SharpMud.Host/ Composition root. References everything, - including the specific ruleset(s) it wants to run, - and owns the hand-built hub world content. + SharpMud.Adapters.Cli/ Local stdin/stdout ISession. References Hosting. +samples/ + SharpMud.Samples.Classic/ D&D-flavored sample ruleset: Race/CharacterClass, + stats, CombatantBehavior, combat resolver, kill/flee + commands, dice-roll character creation, PLUS the + composition root (Program.cs) that references + everything and owns the hand-built hub world content. ``` -Dependency direction is stricter than before: `Ruleset.Classic → Engine` only -(never the reverse). `Host` is the only project allowed to know about a -specific ruleset — this is what makes "swap the ruleset" mean something. A -different game would be `SharpMud.Ruleset.SciFi` (or whatever), referencing -`SharpMud.Engine` the exact same way, with its own `Host`-equivalent wiring it -in instead of `Ruleset.Classic`. +Dependency direction is stricter than before: a ruleset like the sample +depends on `Engine`/`Hosting` only (never the reverse). Nothing under `src/` +is allowed to know about a specific ruleset — this is what makes "swap the +ruleset" mean something. A different game would be its own sample/consumer +project (e.g. `SharpMud.Samples.SciFi`), referencing `SharpMud.Engine`/ +`SharpMud.Hosting` the exact same way, with its own composition root wiring +it in instead of `SharpMud.Samples.Classic`. See +[ADR-0006](adr/0006-nuget-package-distribution.md)/ +[PLAN-0006](plans/0006-nuget-package-distribution.md) for how this split is +now distributed as NuGet packages (`SharpMud.*`) plus a sample consumer, +rather than a single solution with one hardcoded ruleset. ## `Thing` + `Behavior` @@ -192,9 +200,9 @@ exit canceling a move, a full container canceling an item pickup). engine-level, not ruleset-level, since it depends only on `NpcBehavior`/ `ExitBehavior` — no combat or stat system involved. First real validation that the split holds for a whole feature, not just data classes: - `SharpMud.Ruleset.Classic.Tests` never had to change for this to work. + `SharpMud.Samples.Classic.Tests` never had to change for this to work. -## Ruleset-level behaviors (`SharpMud.Ruleset.Classic`) +## Ruleset-level behaviors (`SharpMud.Samples.Classic`) - `StatsBehavior` — the D&D-style attributes (`Strength`...`Charisma`), `Race`, `CharacterClass`, `Level`, `Experience`, `MaxHitPoints`/ @@ -214,7 +222,7 @@ exit canceling a move, a full container canceling an item pickup). `(Thing Actor, Thing CurrentRoom, ...)`. Commands that need ruleset data (`AttackCommand` needing `CombatantBehavior`) do `ctx.Actor.FindBehavior<...>()` and fail gracefully if absent — this is the actual mechanism that keeps -`AttackCommand` in `Ruleset.Classic` rather than `Engine`: it's the first +`AttackCommand` in the sample ruleset rather than `Engine`: it's the first command to depend on a ruleset-specific behavior type. Adopted from WheelMUD (see findings doc §3): a lightweight `CommandGuards` @@ -257,10 +265,9 @@ revisit if guard logic keeps growing. serialize `Thing`+`Behavior` graphs; the JSON shape isn't designed yet. - No `AssemblyLoadContext`-based dynamic ruleset loading — see `SPEC.md` Deferred/Open Items. -- How an actual external consumer gets `SharpMud.Engine` at all (not just - how this repo's own `Host`/`Ruleset.Classic` are split) is resolved by - [ADR-0006](adr/0006-nuget-package-distribution.md), not yet implemented — - see [PLAN-0006](plans/0006-nuget-package-distribution.md). - `SharpMud.Ruleset.Classic` and this repo's own `Host` move to `samples/` - under that plan; this doc's project-structure listing above still - describes their current `src/` locations until that plan executes. +- How an actual external consumer gets `SharpMud.Engine` at all is resolved + by [ADR-0006](adr/0006-nuget-package-distribution.md)/ + [PLAN-0006](plans/0006-nuget-package-distribution.md): `SharpMud.*` NuGet + packages plus `SharpMud.Samples.Classic` as a reference consumer under + `samples/`. Implemented — this doc's project-structure listing above + reflects the current layout. diff --git a/docs/networking.md b/docs/networking.md index 572c3cc..8ea8b96 100644 --- a/docs/networking.md +++ b/docs/networking.md @@ -30,7 +30,8 @@ terminal or just a queued line on stdout for v1. 1. **`SharpMud.Adapters.Cli`** ✅ — implements `ISession` over `Console.In`/`Console.Out`. Single process, single local player, fast - iteration. `Host` uses this when run with no arguments. + iteration. `AddSharpMudCliTransport` registers `CliTransportBackgroundService`, + used when the composition root selects CLI mode. 2. **`SharpMud.Adapters.Telnet`** ✅ — `TelnetSession` (raw TCP via `TcpClient`/`NetworkStream`, line-based I/O) + `TelnetListener` (accepts connections, yields one `ISession` per client via `IAsyncEnumerable`). @@ -39,8 +40,10 @@ terminal or just a queued line on stdout for v1. [ADR-0002](adr/0002-telnet-protocol-negotiation.md). **MCCP/MXP/TermType still not negotiated** — deferred, see Open Items; WheelMUD's `Server/Telnet/` (docs/research/wheelmud-findings.md) is the reference to - consult when those are actually needed. `Host` uses this when run with - `--telnet [port]` (default 4000). + consult when those are actually needed. `AddSharpMudTelnetTransport(port)` + registers `TelnetTransportBackgroundService`, used when the composition + root selects Telnet mode (the sample ruleset does this via `--telnet [port]`, + default 4000). 3. **`SharpMud.Adapters.Ssh`** (later) — secure terminal access. 4. **`SharpMud.Adapters.WebSocket`** (later) — browser play via xterm.js. @@ -48,13 +51,13 @@ Adding a transport is additive — a new project implementing `ISession` — and never requires changes to game logic, command parsing, or world state (see [architecture.md](architecture.md) for the enforced dependency direction that guarantees this). Confirmed in practice: the Telnet adapter required zero -changes to `SharpMud.Engine` or `SharpMud.Ruleset.Classic`. +changes to `SharpMud.Engine` or the sample ruleset. ## Multi-Session Host -`Host`'s per-connection read-eval loop is `SessionLoop.RunAsync` (extracted -from what used to be inline in `Program.cs`), shared by every transport. For -Telnet, `HostRunner.RunTelnetAsync` accepts connections in a loop and spawns +The per-connection read-eval loop is `SessionLoop.RunAsync` (in +`SharpMud.Hosting`), shared by every transport. For Telnet, +`TelnetTransportBackgroundService` accepts connections in a loop and spawns one `SessionLoop.RunAsync` task per connection against the same shared `World`/`IGameLoop`/`ICommandRegistry` — this is what makes concurrent players actually see and interact with each other (confirmed via a live two-client @@ -76,7 +79,7 @@ role in the sequence:) 1. The transport adapter detects the underlying stream closed/EOF, calls `ISession.DisconnectAsync`. -2. `Host`'s session-loop catches this, fires a `PlayerDisconnectedEvent` +2. `SessionLoop` catches this, fires a `PlayerDisconnectedEvent` consumed by Engine's disconnect handler. ## Reconnect / Session Resumption ✅ (ADR-0004) diff --git a/docs/persistence.md b/docs/persistence.md index 33b0412..7fd836f 100644 --- a/docs/persistence.md +++ b/docs/persistence.md @@ -5,14 +5,19 @@ subsystem docs. See [engine-vs-ruleset.md](engine-vs-ruleset.md) for `Thing`/ `Behavior`, the entities being persisted. **Implemented and verified**: `IThingRepository` (`SharpMud.Engine.Core`) and -its EF Core/SQLite implementation `ThingRepository` (`SharpMud.Persistence`) -exist and are wired into `Host` — see Verified below for what was actually +its EF Core implementation `ThingRepository` (`SharpMud.Persistence`, provider- +agnostic) exist, with `SharpMud.Persistence.Sqlite`/`SharpMud.Persistence.DynamoDb` +providing the actual `AddSharpMud*Persistence` DI registration + provider +selection (`UseSqlite`/`UseDynamo`) — see Verified below for what was actually exercised end-to-end, not just unit-tested. ## Strategy -EF Core + SQLite, behind a single repository interface in `SharpMud.Engine` -(`IThingRepository`), implemented in `SharpMud.Persistence`. Game logic +EF Core, behind a single repository interface in `SharpMud.Engine` +(`IThingRepository`), implemented once in `SharpMud.Persistence` against a +provider-agnostic `GameDbContext`. Provider selection (SQLite today, DynamoDB +via `SharpMud.Persistence.DynamoDb`) happens at the DI registration call site +in the provider-specific package, not in `GameDbContext` itself. Game logic depends only on the interface — never on EF Core, SQLite, or SQL directly (see [architecture.md](architecture.md) for the enforced dependency direction). @@ -40,7 +45,7 @@ column serializing the whole behavior list. TPH gets real relational columns per behavior type (queryable, no deserialize-to-filter), at the cost of every `Behavior` subclass needing an EF Core mapping. Since `Behavior` subclasses span two assemblies that must not reference each other the wrong way -(`SharpMud.Ruleset.Classic` behaviors must not be known to `SharpMud.Engine` +(sample/ruleset behaviors must not be known to `SharpMud.Engine` or `SharpMud.Persistence`), mapping registration is itself split: ```csharp @@ -51,16 +56,16 @@ public interface IBehaviorMappingContributor ``` `SharpMud.Persistence` maps Engine's own behavior types directly (it already -references `SharpMud.Engine`); `SharpMud.Ruleset.Classic` provides -`ClassicBehaviorMappingContributor`, which requires a new `Ruleset.Classic → -Persistence` project reference (approved as part of this design — Persistence -still never references back). `Host` registers both via DI +references `SharpMud.Engine`); `SharpMud.Samples.Classic` provides +`ClassicBehaviorMappingContributor`, which requires a `Samples.Classic → +Persistence` project reference (Persistence still never references back). The +composition root registers both via DI (`IEnumerable` constructor injection into `GameDbContext`). Adding a second ruleset later means writing one new contributor class, not touching `Persistence`. Each entity/behavior type gets its own `IEntityTypeConfiguration` class -(`SharpMud.Persistence/Configurations/`, `SharpMud.Ruleset.Classic/Configurations/`) +(`SharpMud.Persistence/Configurations/`, `SharpMud.Samples.Classic/Configurations/`) rather than one large inline `OnModelCreating` — `GameDbContext` just calls `modelBuilder.ApplyConfigurationsFromAssembly(...)` over its own assembly, and each contributor does the same over its own; adding a new behavior type @@ -141,11 +146,14 @@ publish) for this purpose, visible to `SharpMud.Persistence` via ## SQLite Path `SHARPMUD_DB_PATH` env var, following the same precedence pattern as -`HostOptions` (`SHARPMUD_MODE`/`SHARPMUD_TELNET_PORT`) — CLI arg, then env -var, then a default of `./sharpmud.db`. Deploying the Docker container with -this pointed at a mounted volume is required for the container's data to -actually survive a redeploy — not solved here, flagged in -[deployment.md](deployment.md). +`SharpMudHostOptions` (`SHARPMUD_MODE`/`SHARPMUD_TELNET_PORT`) — CLI arg, then +env var, then a default of `./sharpmud.db`. `SqliteStorageInitializer` +(`SharpMud.Persistence.Sqlite`, an `IStorageInitializer`) runs `EnsureCreatedAsync` +before the world loader reads/writes anything, so the schema always exists +first regardless of registration order across packages. Deploying the Docker +container with `SHARPMUD_DB_PATH` pointed at a mounted volume is required for +the container's data to actually survive a redeploy — not solved here, +flagged in [deployment.md](deployment.md). ## Write Frequency (revised scope) @@ -170,7 +178,11 @@ what `docker stop`/Kubernetes send on a graceful shutdown, i.e. the actual scenario this whole design exists for. This was caught during live testing, not code review: `CancelKeyPress` was also observed not firing reliably without a TTY attached (relevant since a container has none), independent of -the `SIGTERM` gap. See `src/SharpMud.Host/Program.cs`. +the `SIGTERM` gap. Now handled by the .NET generic host's own shutdown +sequence (`IHostApplicationLifetime`) rather than a hand-rolled +`PosixSignalRegistration` — `ShutdownSaveHostedService` +(`SharpMud.Hosting`) does the whole-world save in its `StopAsync`. See +`samples/SharpMud.Samples.Classic/Program.cs`. ## Verified @@ -192,7 +204,7 @@ suite). Also: `ThingRepositoryTests` (`SharpMud.Persistence.Tests`) round-trips a room+exit pair, a player with `StatsBehavior`/`CombatantBehavior`/a carried -item (exercises the cross-assembly Ruleset.Classic mapping contributor), a +item (exercises the cross-assembly Samples.Classic mapping contributor), a locked exit with a required key (nullable `Thing` reference resolution), `FindPlayerByUsernameAsync` (including that it correctly ignores non-player `Thing`s with a matching name), and that a second `SaveTreeAsync` call for diff --git a/docs/plans/0006-nuget-package-distribution.md b/docs/plans/0006-nuget-package-distribution.md index 00cde1d..3f5c339 100644 --- a/docs/plans/0006-nuget-package-distribution.md +++ b/docs/plans/0006-nuget-package-distribution.md @@ -2,7 +2,7 @@ **Implements:** [ADR-0006](../adr/0006-nuget-package-distribution.md) -**Status:** In Progress +**Status:** Done **Last updated:** 2026-07-20 @@ -40,9 +40,9 @@ bundled into this plan's "done"). ### Repository reorganization -- [ ] `git mv src/SharpMud.Ruleset.Classic samples/SharpMud.Samples.Classic` +- [x] `git mv src/SharpMud.Ruleset.Classic samples/SharpMud.Samples.Classic` (preserve history — this becomes the single consolidated project) -- [ ] **Update the merged project's `.csproj`, not just its file +- [x] **Update the merged project's `.csproj`, not just its file contents** — caught in PR review: `SharpMud.Ruleset.Classic.csproj` today is a plain class library (no `OutputType`, no `Serilog`/config/ adapter references) — none of what `Program.cs` needs once it moves @@ -55,7 +55,7 @@ bundled into this plan's "done"). logging-provider choice, not something `Hosting` should hardcode). Skipping this leaves a project with `Program.cs` in it that doesn't build. -- [ ] **Split `src/SharpMud.Host`'s files by whether they're +- [x] **Split `src/SharpMud.Host`'s files by whether they're ruleset-specific or genuinely generic — don't move all of it to `samples/` as one block** (caught in PR review: an earlier version of this task sent everything to `samples/`, which would strand @@ -143,17 +143,17 @@ bundled into this plan's "done"). (the sample), referencing `SharpMud.Hosting` and whichever transport package(s) it wants, not one project referencing another app project. -- [ ] Move `HubWorldBuilder` (and any other hand-built hub content) into +- [x] Move `HubWorldBuilder` (and any other hand-built hub content) into the consolidated project — it's sample content per ADR-0006, not engine -- [ ] `git mv src/SharpMud.Host/appsettings.json +- [x] `git mv src/SharpMud.Host/appsettings.json samples/SharpMud.Samples.Classic/appsettings.json`, and add the same `` item (per ADR-0003) to the new sample's `.csproj` — caught in PR review: `Program.cs` loads it with `optional: false`, so without this the sample fails at startup, not just at some later config lookup. -- [ ] **Move each test project to follow its production code, not as one +- [x] **Move each test project to follow its production code, not as one block** — mirrors the split above, not the "everything to `samples/`" version this task previously described: - `git mv tests/SharpMud.Ruleset.Classic.Tests @@ -202,7 +202,7 @@ bundled into this plan's "done"). in PR review) — but `HostOptionsTests`/`LoginFlowTests` specifically need real edits, not just a `git mv`, because the signatures/types they test are changing. -- [ ] Rewrite `samples/SharpMud.Samples.Classic/Program.cs` against +- [x] Rewrite `samples/SharpMud.Samples.Classic/Program.cs` against `SharpMud.Hosting`'s builder — this is the concrete proof that the ~130 lines of generic plumbing identified in ADR-0006's Context actually collapse to a few lines, in a single project alongside the @@ -218,7 +218,7 @@ bundled into this plan's "done"). `Dockerfile` sets `SHARPMUD_MODE=telnet` by default and expects it to work; needs a real test proving that default still starts the Telnet transport post-refactor, not just an assumption. -- [ ] **Update the actual `Dockerfile`, not just docs referencing it** — +- [x] **Update the actual `Dockerfile`, not just docs referencing it** — flagged independently in two rounds of PR review (self-review and Codex): it currently `COPY`s/restores/publishes `src/SharpMud.Host/SharpMud.Host.csproj` and @@ -238,27 +238,27 @@ bundled into this plan's "done"). lines stay as-is — they're still consumed by the sample's `Program.cs`, which per the `HostOptions` split above is exactly where transport-mode selection now lives. -- [ ] Update `docs/engine-vs-ruleset.md`'s project-structure listing and +- [x] Update `docs/engine-vs-ruleset.md`'s project-structure listing and `docs/deployment.md`'s Dockerfile references to the new paths ### `SharpMud.Hosting` (new project) -- [ ] `src/SharpMud.Hosting/SharpMud.Hosting.csproj` — `PackageReference`s +- [x] `src/SharpMud.Hosting/SharpMud.Hosting.csproj` — `PackageReference`s to `Microsoft.Extensions.Hosting` **and** `Microsoft.Extensions.Identity.Core` (needed by `PasswordHashing.cs` — caught in PR review, an earlier version of this plan/ADR-0006's package table said `Hosting` needed nothing beyond `Microsoft.Extensions.Hosting`) -- [ ] `SharpMudApplicationBuilder : IHostApplicationBuilder` — wraps +- [x] `SharpMudApplicationBuilder : IHostApplicationBuilder` — wraps `Host.CreateApplicationBuilder(args)`, delegates `Services`/`Configuration`/`Environment`/`Logging`/`Metrics`/ `Properties`/`ConfigureContainer` straight through; static `SharpMudApplication.CreateBuilder(args)` factory -- [ ] `SharpMudApplication : IHost` — wraps the built `IHost`; `RunAsync` +- [x] `SharpMudApplication : IHost` — wraps the built `IHost`; `RunAsync` delegates to it directly (no custom middleware/invocation pipeline — see ADR-0006's comparison to `minimal-lambda` for why that's deliberately not needed here) -- [ ] **No `SharpMudOptions` type** (removed per PR review — an earlier +- [x] **No `SharpMudOptions` type** (removed per PR review — an earlier draft had it duplicating `DbPath` with the trimmed `HostOptions` below, two sources of truth for the same setting with no stated precedence). `HostOptions.Parse`'s env-var/CLI-arg path is the single @@ -266,13 +266,13 @@ bundled into this plan's "done"). for keeping deployment config manual rather than `IOptions`-bound. Introduce a real `IOptions`-shaped options type later only if a genuine code-configured setting actually needs one. -- [ ] `HostOptions.cs` — moved in from `src/SharpMud.Host` **trimmed to +- [x] `HostOptions.cs` — moved in from `src/SharpMud.Host` **trimmed to `DbPath` only** (per the Repository reorganization task above, not a straight move) — `UseTelnet`/`TelnetPort` don't come with it. -- [ ] `PasswordHashing.cs` — moved in from `src/SharpMud.Host` per the +- [x] `PasswordHashing.cs` — moved in from `src/SharpMud.Host` per the Repository reorganization task above, namespace updated to `SharpMud.Hosting`, otherwise unchanged. -- [ ] `SessionLoop.cs` — moved in from `src/SharpMud.Host`, **converted +- [x] `SessionLoop.cs` — moved in from `src/SharpMud.Host`, **converted from `public static class` to a constructor-injected service class** taking `World`/`ICommandParser`/`ICommandRegistry`/`IThingRepository` via the constructor instead of as method parameters — **not** a @@ -283,9 +283,9 @@ bundled into this plan's "done"). missing this sibling. `RunAsync(ISession session, Thing player, CancellationToken ct)` is what's left on the method itself once the rest move to the constructor. -- [ ] New `IPlayerFactory` interface: `Thing CreatePlayer(World world, +- [x] New `IPlayerFactory` interface: `Thing CreatePlayer(World world, string username, string passwordHash, Thing startingRoom)`. -- [ ] `LoginFlow.cs`/`PlayerLogin.cs` — moved in from `src/SharpMud.Host`, +- [x] `LoginFlow.cs`/`PlayerLogin.cs` — moved in from `src/SharpMud.Host`, **converted from `public static class` to constructor-injected service classes taking `IThingRepository`/`IPlayerFactory`** (per the Repository reorganization task above) — not a straight move and @@ -295,15 +295,15 @@ bundled into this plan's "done"). a consumer's login/session handling work out of the package instead of requiring copied sample code — the concrete gap caught in PR review. -- [ ] Register `LoginFlow`/`PlayerLogin` in `SharpMud.Hosting`'s DI setup +- [x] Register `LoginFlow`/`PlayerLogin` in `SharpMud.Hosting`'s DI setup (whatever shape that takes — `AddSharpMud(...)`-style extension or direct `Services.AddScoped()`/etc., implementation's call). -- [ ] `GameLoop` registered as a `BackgroundService` (or a thin +- [x] `GameLoop` registered as a `BackgroundService` (or a thin `BackgroundService` wrapper around it, if `GameLoop` itself shouldn't take a direct `Microsoft.Extensions.Hosting` dependency — decide during implementation which project should own that coupling) -- [ ] **Explicit owner for the shutdown-time whole-world save** — caught in +- [x] **Explicit owner for the shutdown-time whole-world save** — caught in PR review: today's `Program.cs` does `await repository.SaveTreeAsync(hubArea, CancellationToken.None)` after the session loop/listener wind down but before the final `gameLoopTask` @@ -319,7 +319,7 @@ bundled into this plan's "done"). `SharpMud.Hosting`, not left as only a Verification-section check — needs the world root/hub `Thing` reference, which ties to the still-open world-builder registration point (see Open Questions). -- [ ] **No transport wiring lives here** — `Hosting` must not reference +- [x] **No transport wiring lives here** — `Hosting` must not reference `SharpMud.Adapters.Telnet`/`SharpMud.Adapters.Cli` (caught in PR review: an earlier version of this task had `Hosting` itself instantiate `TelnetListener` and branch on a `TransportMode` enum, @@ -330,7 +330,7 @@ bundled into this plan's "done"). registration, etc.) — see the new `SharpMud.Adapters.Telnet`/ `SharpMud.Adapters.Cli` tasks below for where the transport-specific `BackgroundService`s actually get registered. -- [ ] `AddSharpMudRuleset(Action register)` (or +- [x] `AddSharpMudRuleset(Action register)` (or equivalent) extension point for the consumer's ruleset registration callback — **must call `BuiltinCommands.RegisterAll(registry)` itself before invoking the consumer's callback** (caught in PR review: @@ -342,7 +342,7 @@ bundled into this plan's "done"). registration point (name/shape TBD during implementation — needs to express "load persisted tree, else call the consumer's builder, then save" without hardcoding `HubWorldBuilder`). -- [ ] `GameLoop`'s `BackgroundService` constructor-injects +- [x] `GameLoop`'s `BackgroundService` constructor-injects `IEnumerable` and registers whatever's resolved at startup, rather than a second dedicated registration callback alongside `AddSharpMudRuleset` — caught in PR review: today's @@ -356,20 +356,20 @@ bundled into this plan's "done"). `services.AddSingleton()` for their own ruleset-specific tickables, same DI pattern already established for `IPlayerFactory`. -- [ ] **Verify**: does the generic host's default `ConsoleLifetime` handle +- [x] **Verify**: does the generic host's default `ConsoleLifetime` handle `SIGTERM` correctly on Unix out of the box? If yes, delete the hand-rolled `PosixSignalRegistration` code instead of porting it into `Hosting` — don't re-solve an already-fixed problem blind to whether it's already fixed. If no, port the existing fix forward. -- [ ] XML doc comments on every public member per `documentation.md` +- [x] XML doc comments on every public member per `documentation.md` ### `SharpMud.Adapters.Telnet` transport wiring (new tasks on an existing project) -- [ ] Add a `ProjectReference` from `SharpMud.Adapters.Telnet` to +- [x] Add a `ProjectReference` from `SharpMud.Adapters.Telnet` to `SharpMud.Hosting` — the dependency direction flips relative to today (`Hosting` must never reference `Adapters.Telnet`, per the `SharpMud.Hosting` task section above). -- [ ] Fold `HostRunner.cs`'s logic (moved in from `src/SharpMud.Host` per +- [x] Fold `HostRunner.cs`'s logic (moved in from `src/SharpMud.Host` per the Repository reorganization task) into an `AddSharpMudTelnetTransport(int port)` DI extension — a `BackgroundService` constructor-injected with `World`/ @@ -384,31 +384,31 @@ bundled into this plan's "done"). exception-isolation shape `HostRunner.HandleConnectionAsync` already has today. Where `StartingRoom` comes from is tied to the still-open world-builder registration point — see Open Questions. -- [ ] XML doc comments per `documentation.md`. +- [x] XML doc comments per `documentation.md`. ### `SharpMud.Adapters.Cli` transport wiring (new tasks on an existing project) -- [ ] Add a `ProjectReference` from `SharpMud.Adapters.Cli` to +- [x] Add a `ProjectReference` from `SharpMud.Adapters.Cli` to `SharpMud.Hosting`, same reasoning as Telnet above. -- [ ] `AddSharpMudCliTransport()` DI extension covering what today's +- [x] `AddSharpMudCliTransport()` DI extension covering what today's `Program.cs` CLI branch does inline: construct a `ConsoleSession`, resolve/create the player via the injected `PlayerLogin` service (constructor-injected `IThingRepository`/`IPlayerFactory`, not a `createPlayer` parameter — per the `LoginFlow`/`PlayerLogin` fix above), run `SessionLoop.RunAsync`. -- [ ] XML doc comments per `documentation.md`. +- [x] XML doc comments per `documentation.md`. ### `SharpMud.Persistence` split -- [ ] Remove `Microsoft.EntityFrameworkCore.Sqlite`/ +- [x] Remove `Microsoft.EntityFrameworkCore.Sqlite`/ `SQLitePCLRaw.lib.e_sqlite3` `PackageReference`s from `SharpMud.Persistence.csproj` — core stays provider-agnostic (`Microsoft.EntityFrameworkCore`/`.Relational` only) -- [ ] New `src/SharpMud.Persistence.Sqlite/` — the removed package refs, +- [x] New `src/SharpMud.Persistence.Sqlite/` — the removed package refs, plus `AddSharpMudSqlitePersistence(string dbPath)` extension wrapping today's `UseSqlite(...)` call site (moved out of `Program.cs`) -- [ ] **Verify before building `SharpMud.Persistence.DynamoDb`**: does +- [x] **Verify before building `SharpMud.Persistence.DynamoDb`**: does `EntityFrameworkCore.DynamoDb` actually accept the shared `Configurations/` classes as-is? `ThingConfiguration`/ `BehaviorConfiguration`'s `ToTable(...)` and single-property @@ -425,33 +425,33 @@ bundled into this plan's "done"). fails at model-validation time. This is a real go/no-go check, not a formality — do it before writing `SharpMud.Persistence.DynamoDb`'s other tasks below. -- [ ] New `src/SharpMud.Persistence.DynamoDb/` — references +- [x] New `src/SharpMud.Persistence.DynamoDb/` — references `EntityFrameworkCore.DynamoDb 10.0.0` (the current stable release, confirmed against NuGet directly — see ADR-0006's package table), targeting this project's `net10.0` TFM specifically since the provider is an EF Core 10 build, not EF Core 11, equivalent `AddSharpMudDynamoDbPersistence(...)` extension -- [ ] Update `samples/SharpMud.Samples.Classic` to consume +- [x] Update `samples/SharpMud.Samples.Classic` to consume `SharpMud.Persistence.Sqlite`'s extension instead of the inline `UseSqlite(...)` call ### Packaging metadata + CI -- [ ] Root `Directory.Build.props` — `PackageLicenseExpression` (MIT), +- [x] Root `Directory.Build.props` — `PackageLicenseExpression` (MIT), `RepositoryUrl`, `Authors`, `PackageProjectUrl`, `PackageIcon`, `PackageReadmeFile`, `GenerateDocumentationFile`, `DebugType=embedded`, `Microsoft.SourceLink.GitHub` — matching `optimized-enums`'/ `structured-logging`'s shape exactly; no `VersionPrefix` (release-drafter resolves the version, per ADR-0006) -- [ ] Root `icon.png` -- [ ] Root `LICENSE` (MIT) -- [ ] `NOTICE.md` (or a README section) crediting WheelMUD as design +- [x] Root `icon.png` +- [x] Root `LICENSE` (MIT) +- [x] `NOTICE.md` (or a README section) crediting WheelMUD as design inspiration, per ADR-0006's License and naming section — not a legal requirement, matches this project's existing citation discipline -- [ ] Set `IsPackable=true` (or leave default) on every `src/` project +- [x] Set `IsPackable=true` (or leave default) on every `src/` project above; confirm `samples/` projects are `IsPackable=false` explicitly so a solution-wide `dotnet pack` never emits sample packages -- [ ] New `src/SharpMud/SharpMud.csproj` — the meta-package: no code, +- [x] New `src/SharpMud/SharpMud.csproj` — the meta-package: no code, **`ProjectReference`s (not `PackageReference`s) to every other `SharpMud.*` project** — verified experimentally: `dotnet pack` automatically translates a `ProjectReference` to a packable project @@ -461,7 +461,7 @@ bundled into this plan's "done"). instead would fail restore on the very first `dotnet pack SharpMud.slnx` — none of the sibling packages exist on any feed until *after* that first pack/publish — caught in PR review. -- [ ] `.github/workflows/publish-preview.yaml` — push to `main` → +- [x] `.github/workflows/publish-preview.yaml` — push to `main` → `devops-templates`' `publish-preview.yml@v10.1` + `LayeredCraft/devops-templates/.github/actions/nuget-push@v10.1` (the full `{owner}/{repo}/{path}@{ref}` reference — a bare @@ -473,9 +473,9 @@ bundled into this plan's "done"). packages publish as `X.Y.Z-alpha.` while this package set is in its alpha stage; revisit this input once it graduates out of alpha -- [ ] `.github/workflows/publish-release.yaml` — same shape, triggered on +- [x] `.github/workflows/publish-release.yaml` — same shape, triggered on `release: published` -- [ ] Multi-target `net10.0;net11.0` on every packaged `src/` project; +- [x] Multi-target `net10.0;net11.0` on every packaged `src/` project; **verify** the codebase actually compiles clean against `net10.0` — if any `net11.0`-only API is in use, resolve via `#if` gating or confirm it's acceptable to require `net11.0` after all (a real @@ -483,19 +483,19 @@ bundled into this plan's "done"). ### Documentation site (GitHub Pages) -- [ ] New top-level `docsite/` directory (exact name TBD) — the Zensical +- [x] New top-level `docsite/` directory (exact name TBD) — the Zensical site source, kept separate from `docs/`'s existing ADR/plan/subsystem content per ADR-0006's Documentation site section -- [ ] `pyproject.toml`/`uv.lock` (Zensical + `mdformat` toolchain, matching +- [x] `pyproject.toml`/`uv.lock` (Zensical + `mdformat` toolchain, matching `dynamodb-efcore-provider`'s dependency set), `zensical.toml` with a minimal curated `nav` (Home, Getting Started to start — expand as real content lands) -- [ ] `.github/workflows/docs.yaml` — PR builds (no deploy) + push-to-`main` +- [x] `.github/workflows/docs.yaml` — PR builds (no deploy) + push-to-`main` build/deploy via `actions/upload-pages-artifact`/`actions/deploy-pages`, matching `dynamodb-efcore-provider`'s workflow shape exactly (`uv sync --locked --all-extras --dev` → `uv run zensical build`) - [ ] Enable GitHub Pages (Pages source: GitHub Actions) in repo settings -- [ ] Minimal skeleton content: a home page and one real "Getting Started" +- [x] Minimal skeleton content: a home page and one real "Getting Started" walkthrough using the actual packages this plan produces (install `SharpMud`, write a one-project `Ruleset` + `Program.cs`, run it) — enough to prove the pipeline end-to-end. A full content build-out @@ -525,9 +525,9 @@ bundled into this plan's "done"). see ADR-0006"), without rewriting the doc's current-state prose to describe unimplemented behavior as current (per `design-decisions.md`). **Done in the design PR** (#8). -- [ ] `README.md` (root) — update to describe the package-based consumption +- [x] `README.md` (root) — update to describe the package-based consumption story once implemented, not before -- [ ] **Update every subsystem doc whose current-state prose cites the old +- [x] **Update every subsystem doc whose current-state prose cites the old `src/SharpMud.Host` paths/types, not just `engine-vs-ruleset.md`/ `deployment.md`** — caught in PR review, a real gap: this task previously listed only two files, but a direct search turns up diff --git a/docs/plans/README.md b/docs/plans/README.md index 9a6c341..67b935e 100644 --- a/docs/plans/README.md +++ b/docs/plans/README.md @@ -60,4 +60,4 @@ isn't settled yet — don't do that. | [0002](0002-telnet-protocol-negotiation.md) | [ADR-0002](../adr/0002-telnet-protocol-negotiation.md) | Done | | [0004](0004-session-state-machine-and-reconnect.md) | [ADR-0004](../adr/0004-session-state-machine-and-reconnect.md) | Done | | [0005](0005-security-role-model-and-moderation-commands.md) | [ADR-0005](../adr/0005-security-role-model-and-moderation-commands.md) | Not Started | -| [0006](0006-nuget-package-distribution.md) | [ADR-0006](../adr/0006-nuget-package-distribution.md) | Not Started | +| [0006](0006-nuget-package-distribution.md) | [ADR-0006](../adr/0006-nuget-package-distribution.md) | Done | diff --git a/docsite/docs/getting-started.md b/docsite/docs/getting-started.md new file mode 100644 index 0000000..13d51cd --- /dev/null +++ b/docsite/docs/getting-started.md @@ -0,0 +1,110 @@ +# Getting Started + +This walks through the smallest possible sharp-mud game: one room, one +built-in command set, no custom ruleset commands yet. It uses the CLI +transport and SQLite persistence — the fastest loop for local iteration. + +For a full worked example with stats, combat, and a Telnet transport, see +[`SharpMud.Samples.Classic`](https://github.com/LayeredCraft/sharp-mud/tree/main/samples/SharpMud.Samples.Classic) +in the sharp-mud repo. + +## Install + +```bash +dotnet add package SharpMud +dotnet add package SharpMud.Persistence.Sqlite +dotnet add package SharpMud.Adapters.Cli +``` + +`SharpMud` is a meta-package pulling in `SharpMud.Engine`, `SharpMud.Hosting`, +`SharpMud.Persistence`, and both adapter packages — you still pick a +persistence *provider* (`.Sqlite` or `.DynamoDb`) and a transport +(`SharpMud.Adapters.Cli` and/or `SharpMud.Adapters.Telnet`) explicitly. + +## Build the world + +Every consumer implements `IWorldBuilder` to describe what a brand-new world +looks like, and `IPlayerFactory` to describe how a new character is created: + +```csharp +using SharpMud.Engine.Behaviors; +using SharpMud.Engine.Core; +using SharpMud.Hosting; + +public sealed class MyWorldBuilder : IWorldBuilder +{ + public static readonly ThingId RootRoomId = ThingId.New(); + + public ThingId RootId => RootRoomId; + + public (World World, Thing StartingRoom) Build() + { + var world = new World(); + + var room = new Thing { Id = RootRoomId, Name = "The Square", Description = "A quiet town square." }; + room.Behaviors.Add(new RoomBehavior()); + world.Register(room); + + return (world, room); + } + + public Thing FindStartingRoom(Thing root) => root; +} + +public sealed class MyPlayerFactory : IPlayerFactory +{ + public Thing CreatePlayer(World world, string username, string passwordHash, Thing startingRoom) + { + var player = new Thing { Id = ThingId.New(), Name = username }; + player.Behaviors.Add(new PlayerBehavior { Username = username, PasswordHash = passwordHash }); + startingRoom.Add(player); + world.Register(player); + return player; + } +} +``` + +## Compose the host + +```csharp +using Microsoft.Extensions.Hosting; +using SharpMud.Adapters.Cli; +using SharpMud.Hosting; +using SharpMud.Persistence.Sqlite; + +var app = SharpMudApplication.CreateBuilder(args); + +app.Services.AddSharpMudSqlitePersistence("./mygame.db"); +app.Services.AddSharpMudWorld(); +app.Services.AddSharpMudPlayerFactory(); +app.Services.AddSharpMudRuleset((sp, registry) => { /* register your own ICommand types here */ }); +app.Services.AddSharpMudCliTransport(); + +var mud = app.Build(); +await mud.RunAsync(); +``` + +`SharpMudApplication.CreateBuilder` wraps the .NET generic host +(`Host.CreateApplicationBuilder`), so `app` is a normal +`IHostApplicationBuilder` — standard `appsettings.json`/environment +configuration, logging, and DI all work exactly as they do in any other +.NET generic-host app. + +## Run it + +```bash +dotnet run +``` + +The CLI transport is login-free (single local player, resolved/created as +`"Adventurer"` on start) — you land straight in `The Square`. Built-in +commands (`look`, movement, `quit`, etc.) work immediately, registered +automatically by `AddSharpMudRuleset` before your own callback runs. +`AddSharpMudTelnetTransport(port)` is the transport that uses +`SharpMud.Hosting`'s username/password login flow, since a networked +multi-player server needs one. + +To add a real ruleset — stats, combat, more rooms — see +`SharpMud.Samples.Classic`'s `Program.cs` and `HubWorldBuilder` for a fuller +worked example, and [ADR-0006](https://github.com/LayeredCraft/sharp-mud/blob/main/docs/adr/0006-nuget-package-distribution.md) +for the design rationale behind this package split. diff --git a/docsite/docs/images/icon.png b/docsite/docs/images/icon.png new file mode 100644 index 0000000000000000000000000000000000000000..701cb2cdd4ec15393f6f4823439bb50381dc7522 GIT binary patch literal 6852 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 E?^Y_s4FCWD literal 0 HcmV?d00001 diff --git a/docsite/docs/index.md b/docsite/docs/index.md new file mode 100644 index 0000000..b2a2d39 --- /dev/null +++ b/docsite/docs/index.md @@ -0,0 +1,28 @@ +# sharp-mud + +A modern C#/.NET engine for building MUDs (Multi-User Dungeons) — faithful to +the genre's feel while built on current .NET architecture: the .NET generic +host, EF Core, and a `Thing`/`Behavior` composition model instead of a +hardcoded class hierarchy. + +sharp-mud ships as a set of NuGet packages you compose into your own game: + +- **`SharpMud.Engine`** — `Thing`/`Behavior`, the event system, generic + behaviors (rooms, exits, containment, identity), the command pipeline, and + the global tick loop. Zero knowledge of any specific ruleset. +- **`SharpMud.Hosting`** — composition helpers for the .NET generic host: + `WorldContext`, `IWorldBuilder`, `IPlayerFactory`, the session/login flow, + and the `AddSharpMud*` extension methods that wire it all together. +- **`SharpMud.Persistence`** (+ **`.Sqlite`** / **`.DynamoDb`**) — EF Core + repositories behind a provider-agnostic `IThingRepository`. +- **`SharpMud.Adapters.Cli`** / **`SharpMud.Adapters.Telnet`** — transport + implementations of `ISession`. +- **`SharpMud`** — a meta-package pulling in all of the above. + +Your own game — stats, combat rules, world content, commands — is a separate +project that references these packages the same way `SharpMud.Samples.Classic` +(the D&D-flavored reference sample in the +[sharp-mud repo](https://github.com/LayeredCraft/sharp-mud/tree/main/samples/SharpMud.Samples.Classic)) +does. + +Start with [Getting Started](getting-started.md). diff --git a/docsite/pyproject.toml b/docsite/pyproject.toml new file mode 100644 index 0000000..2a5b20e --- /dev/null +++ b/docsite/pyproject.toml @@ -0,0 +1,10 @@ +[project] +name = "sharp-mud-docsite" +version = "0.1.0" +requires-python = ">=3.14" +dependencies = [ + "zensical>=0.0.47", + "mdformat", + "mdformat-mkdocs", + "mdformat-frontmatter", +] diff --git a/docsite/uv.lock b/docsite/uv.lock new file mode 100644 index 0000000..52ceffe --- /dev/null +++ b/docsite/uv.lock @@ -0,0 +1,324 @@ +version = 1 +revision = 3 +requires-python = ">=3.14" + +[[package]] +name = "click" +version = "8.4.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/76/d4/81420972a676e8ffea40450d8c8c92943e7218a78fe9b64359836cc9876b/click-8.4.2.tar.gz", hash = "sha256:9a6cea6e60b17ebe0a44c5cc636d94f09bd66142c1cd7d8b4cd731c4917a15f6", size = 338000, upload-time = "2026-06-24T17:45:15.148Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fb/e2/79c688af8b210d232694e31e59da9f6ec747bae31c3f5946e4e9b98860d5/click-8.4.2-py3-none-any.whl", hash = "sha256:e6f9f66136c816745b9d65817da91d61d957fb16e02e4dcd0552553c5a197b76", size = 119243, upload-time = "2026-06-24T17:45:13.73Z" }, +] + +[[package]] +name = "colorama" +version = "0.4.6" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697, upload-time = "2022-10-25T02:36:22.414Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" }, +] + +[[package]] +name = "deepmerge" +version = "2.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/2a/78/6e9e20106224083cfb817d2d3c26e80e72258d617b616721a169b87081e0/deepmerge-2.1.0.tar.gz", hash = "sha256:07ca7a7b8935df596c512fa8161877c0487ac61f691c07766e7d71d2b23bdd2f", size = 21449, upload-time = "2026-06-22T05:46:07.669Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/51/25/2a75b47cb057b1e164c604fb81ab690a6cdb5e2260ce651194eae90f64a3/deepmerge-2.1.0-py3-none-any.whl", hash = "sha256:8f148339a91d680a75ecb74ade235d9e759a93df373a0b04e9d31c8666cfeb75", size = 14345, upload-time = "2026-06-22T05:46:06.742Z" }, +] + +[[package]] +name = "jinja2" +version = "3.1.6" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "markupsafe" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/df/bf/f7da0350254c0ed7c72f3e33cef02e048281fec7ecec5f032d4aac52226b/jinja2-3.1.6.tar.gz", hash = "sha256:0137fb05990d35f1275a587e9aee6d56da821fc83491a0fb838183be43f66d6d", size = 245115, upload-time = "2025-03-05T20:05:02.478Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/62/a1/3d680cbfd5f4b8f15abc1d571870c5fc3e594bb582bc3b64ea099db13e56/jinja2-3.1.6-py3-none-any.whl", hash = "sha256:85ece4451f492d0c13c5dd7c13a64681a86afae63a5f347908daf103ce6d2f67", size = 134899, upload-time = "2025-03-05T20:05:00.369Z" }, +] + +[[package]] +name = "markdown" +version = "3.10.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/2b/f4/69fa6ed85ae003c2378ffa8f6d2e3234662abd02c10d216c0ba96081a238/markdown-3.10.2.tar.gz", hash = "sha256:994d51325d25ad8aa7ce4ebaec003febcce822c3f8c911e3b17c52f7f589f950", size = 368805, upload-time = "2026-02-09T14:57:26.942Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/de/1f/77fa3081e4f66ca3576c896ae5d31c3002ac6607f9747d2e3aa49227e464/markdown-3.10.2-py3-none-any.whl", hash = "sha256:e91464b71ae3ee7afd3017d9f358ef0baf158fd9a298db92f1d4761133824c36", size = 108180, upload-time = "2026-02-09T14:57:25.787Z" }, +] + +[[package]] +name = "markdown-it-py" +version = "4.2.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "mdurl" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/06/ff/7841249c247aa650a76b9ee4bbaeae59370dc8bfd2f6c01f3630c35eb134/markdown_it_py-4.2.0.tar.gz", hash = "sha256:04a21681d6fbb623de53f6f364d352309d4094dd4194040a10fd51833e418d49", size = 82454, upload-time = "2026-05-07T12:08:28.36Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b3/81/4da04ced5a082363ecfa159c010d200ecbd959ae410c10c0264a38cac0f5/markdown_it_py-4.2.0-py3-none-any.whl", hash = "sha256:9f7ebbcd14fe59494226453aed97c1070d83f8d24b6fc3a3bcf9a38092641c4a", size = 91687, upload-time = "2026-05-07T12:08:27.182Z" }, +] + +[[package]] +name = "markupsafe" +version = "3.0.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/7e/99/7690b6d4034fffd95959cbe0c02de8deb3098cc577c67bb6a24fe5d7caa7/markupsafe-3.0.3.tar.gz", hash = "sha256:722695808f4b6457b320fdc131280796bdceb04ab50fe1795cd540799ebe1698", size = 80313, upload-time = "2025-09-27T18:37:40.426Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/33/8a/8e42d4838cd89b7dde187011e97fe6c3af66d8c044997d2183fbd6d31352/markupsafe-3.0.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:eaa9599de571d72e2daf60164784109f19978b327a3910d3e9de8c97b5b70cfe", size = 11619, upload-time = "2025-09-27T18:37:06.342Z" }, + { url = "https://files.pythonhosted.org/packages/b5/64/7660f8a4a8e53c924d0fa05dc3a55c9cee10bbd82b11c5afb27d44b096ce/markupsafe-3.0.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c47a551199eb8eb2121d4f0f15ae0f923d31350ab9280078d1e5f12b249e0026", size = 12029, upload-time = "2025-09-27T18:37:07.213Z" }, + { url = "https://files.pythonhosted.org/packages/da/ef/e648bfd021127bef5fa12e1720ffed0c6cbb8310c8d9bea7266337ff06de/markupsafe-3.0.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f34c41761022dd093b4b6896d4810782ffbabe30f2d443ff5f083e0cbbb8c737", size = 24408, upload-time = "2025-09-27T18:37:09.572Z" }, + { url = "https://files.pythonhosted.org/packages/41/3c/a36c2450754618e62008bf7435ccb0f88053e07592e6028a34776213d877/markupsafe-3.0.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:457a69a9577064c05a97c41f4e65148652db078a3a509039e64d3467b9e7ef97", size = 23005, upload-time = "2025-09-27T18:37:10.58Z" }, + { url = "https://files.pythonhosted.org/packages/bc/20/b7fdf89a8456b099837cd1dc21974632a02a999ec9bf7ca3e490aacd98e7/markupsafe-3.0.3-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e8afc3f2ccfa24215f8cb28dcf43f0113ac3c37c2f0f0806d8c70e4228c5cf4d", size = 22048, upload-time = "2025-09-27T18:37:11.547Z" }, + { url = "https://files.pythonhosted.org/packages/9a/a7/591f592afdc734f47db08a75793a55d7fbcc6902a723ae4cfbab61010cc5/markupsafe-3.0.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:ec15a59cf5af7be74194f7ab02d0f59a62bdcf1a537677ce67a2537c9b87fcda", size = 23821, upload-time = "2025-09-27T18:37:12.48Z" }, + { url = "https://files.pythonhosted.org/packages/7d/33/45b24e4f44195b26521bc6f1a82197118f74df348556594bd2262bda1038/markupsafe-3.0.3-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:0eb9ff8191e8498cca014656ae6b8d61f39da5f95b488805da4bb029cccbfbaf", size = 21606, upload-time = "2025-09-27T18:37:13.485Z" }, + { url = "https://files.pythonhosted.org/packages/ff/0e/53dfaca23a69fbfbbf17a4b64072090e70717344c52eaaaa9c5ddff1e5f0/markupsafe-3.0.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:2713baf880df847f2bece4230d4d094280f4e67b1e813eec43b4c0e144a34ffe", size = 23043, upload-time = "2025-09-27T18:37:14.408Z" }, + { url = "https://files.pythonhosted.org/packages/46/11/f333a06fc16236d5238bfe74daccbca41459dcd8d1fa952e8fbd5dccfb70/markupsafe-3.0.3-cp314-cp314-win32.whl", hash = "sha256:729586769a26dbceff69f7a7dbbf59ab6572b99d94576a5592625d5b411576b9", size = 14747, upload-time = "2025-09-27T18:37:15.36Z" }, + { url = "https://files.pythonhosted.org/packages/28/52/182836104b33b444e400b14f797212f720cbc9ed6ba34c800639d154e821/markupsafe-3.0.3-cp314-cp314-win_amd64.whl", hash = "sha256:bdc919ead48f234740ad807933cdf545180bfbe9342c2bb451556db2ed958581", size = 15341, upload-time = "2025-09-27T18:37:16.496Z" }, + { url = "https://files.pythonhosted.org/packages/6f/18/acf23e91bd94fd7b3031558b1f013adfa21a8e407a3fdb32745538730382/markupsafe-3.0.3-cp314-cp314-win_arm64.whl", hash = "sha256:5a7d5dc5140555cf21a6fefbdbf8723f06fcd2f63ef108f2854de715e4422cb4", size = 14073, upload-time = "2025-09-27T18:37:17.476Z" }, + { url = "https://files.pythonhosted.org/packages/3c/f0/57689aa4076e1b43b15fdfa646b04653969d50cf30c32a102762be2485da/markupsafe-3.0.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:1353ef0c1b138e1907ae78e2f6c63ff67501122006b0f9abad68fda5f4ffc6ab", size = 11661, upload-time = "2025-09-27T18:37:18.453Z" }, + { url = "https://files.pythonhosted.org/packages/89/c3/2e67a7ca217c6912985ec766c6393b636fb0c2344443ff9d91404dc4c79f/markupsafe-3.0.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:1085e7fbddd3be5f89cc898938f42c0b3c711fdcb37d75221de2666af647c175", size = 12069, upload-time = "2025-09-27T18:37:19.332Z" }, + { url = "https://files.pythonhosted.org/packages/f0/00/be561dce4e6ca66b15276e184ce4b8aec61fe83662cce2f7d72bd3249d28/markupsafe-3.0.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1b52b4fb9df4eb9ae465f8d0c228a00624de2334f216f178a995ccdcf82c4634", size = 25670, upload-time = "2025-09-27T18:37:20.245Z" }, + { url = "https://files.pythonhosted.org/packages/50/09/c419f6f5a92e5fadde27efd190eca90f05e1261b10dbd8cbcb39cd8ea1dc/markupsafe-3.0.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fed51ac40f757d41b7c48425901843666a6677e3e8eb0abcff09e4ba6e664f50", size = 23598, upload-time = "2025-09-27T18:37:21.177Z" }, + { url = "https://files.pythonhosted.org/packages/22/44/a0681611106e0b2921b3033fc19bc53323e0b50bc70cffdd19f7d679bb66/markupsafe-3.0.3-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:f190daf01f13c72eac4efd5c430a8de82489d9cff23c364c3ea822545032993e", size = 23261, upload-time = "2025-09-27T18:37:22.167Z" }, + { url = "https://files.pythonhosted.org/packages/5f/57/1b0b3f100259dc9fffe780cfb60d4be71375510e435efec3d116b6436d43/markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:e56b7d45a839a697b5eb268c82a71bd8c7f6c94d6fd50c3d577fa39a9f1409f5", size = 24835, upload-time = "2025-09-27T18:37:23.296Z" }, + { url = "https://files.pythonhosted.org/packages/26/6a/4bf6d0c97c4920f1597cc14dd720705eca0bf7c787aebc6bb4d1bead5388/markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:f3e98bb3798ead92273dc0e5fd0f31ade220f59a266ffd8a4f6065e0a3ce0523", size = 22733, upload-time = "2025-09-27T18:37:24.237Z" }, + { url = "https://files.pythonhosted.org/packages/14/c7/ca723101509b518797fedc2fdf79ba57f886b4aca8a7d31857ba3ee8281f/markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:5678211cb9333a6468fb8d8be0305520aa073f50d17f089b5b4b477ea6e67fdc", size = 23672, upload-time = "2025-09-27T18:37:25.271Z" }, + { url = "https://files.pythonhosted.org/packages/fb/df/5bd7a48c256faecd1d36edc13133e51397e41b73bb77e1a69deab746ebac/markupsafe-3.0.3-cp314-cp314t-win32.whl", hash = "sha256:915c04ba3851909ce68ccc2b8e2cd691618c4dc4c4232fb7982bca3f41fd8c3d", size = 14819, upload-time = "2025-09-27T18:37:26.285Z" }, + { url = "https://files.pythonhosted.org/packages/1a/8a/0402ba61a2f16038b48b39bccca271134be00c5c9f0f623208399333c448/markupsafe-3.0.3-cp314-cp314t-win_amd64.whl", hash = "sha256:4faffd047e07c38848ce017e8725090413cd80cbc23d86e55c587bf979e579c9", size = 15426, upload-time = "2025-09-27T18:37:27.316Z" }, + { url = "https://files.pythonhosted.org/packages/70/bc/6f1c2f612465f5fa89b95bead1f44dcb607670fd42891d8fdcd5d039f4f4/markupsafe-3.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:32001d6a8fc98c8cb5c947787c5d08b0a50663d139f1305bac5885d98d9b40fa", size = 14146, upload-time = "2025-09-27T18:37:28.327Z" }, +] + +[[package]] +name = "mdformat" +version = "1.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "markdown-it-py" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/3f/05/32b5e14b192b0a8a309f32232c580aefedd9d06017cb8fe8fce34bec654c/mdformat-1.0.0.tar.gz", hash = "sha256:4954045fcae797c29f86d4ad879e43bb151fa55dbaf74ac6eaeacf1d45bb3928", size = 56953, upload-time = "2025-10-16T12:05:03.695Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/54/9a/8fe71b95985ca7a4001effbcc58e5a07a1f2a2884203f74dcf48a3b08315/mdformat-1.0.0-py3-none-any.whl", hash = "sha256:bca015d65a1d063a02e885a91daee303057bc7829c2cd37b2075a50dbb65944b", size = 53288, upload-time = "2025-10-16T12:05:02.607Z" }, +] + +[[package]] +name = "mdformat-frontmatter" +version = "2.1.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "mdformat" }, + { name = "mdit-py-plugins" }, + { name = "ruamel-yaml" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/4d/90/ae8a3d20066295367d9048ad480bcde3afe694eb592fec9c35bbdb7d68eb/mdformat_frontmatter-2.1.2.tar.gz", hash = "sha256:9c9c3159c38407f47e567202749c74ce7680017516cd158c82400f015c6a01d1", size = 9425, upload-time = "2026-05-23T05:14:26.324Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/53/a0/20b6f53593139d8b64fa8e7f750033fbb580c096c241cd551b59907e6abe/mdformat_frontmatter-2.1.2-py3-none-any.whl", hash = "sha256:bcbb1dd7e35ce4387603acd32f2c092dd4518bbfaa2a4b801a0f29a4e69bf2ca", size = 4927, upload-time = "2026-05-23T05:14:24.747Z" }, +] + +[[package]] +name = "mdformat-gfm" +version = "1.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "markdown-it-py" }, + { name = "mdformat" }, + { name = "mdit-py-plugins" }, + { name = "wcwidth" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/56/6f/a626ebb142a290474401b67e2d61e73ce096bf7798ee22dfe6270f924b3f/mdformat_gfm-1.0.0.tar.gz", hash = "sha256:d1d49a409a6acb774ce7635c72d69178df7dce1dc8cdd10e19f78e8e57b72623", size = 10112, upload-time = "2025-10-16T09:12:22.402Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e6/18/6bc2189b744dd383cad03764f41f30352b1278d2205096f77a29c0b327ad/mdformat_gfm-1.0.0-py3-none-any.whl", hash = "sha256:7305a50efd2a140d7c83505b58e3ac5df2b09e293f9bbe72f6c7bee8c678b005", size = 10970, upload-time = "2025-10-16T09:12:21.276Z" }, +] + +[[package]] +name = "mdformat-mkdocs" +version = "5.2.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "mdformat" }, + { name = "mdformat-gfm" }, + { name = "mdit-py-plugins" }, + { name = "more-itertools" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/e3/f7/f2a80dd80d85ee50aba1a3047faeda7cefa91bfd67e951ef14eaa43b3992/mdformat_mkdocs-5.2.1.tar.gz", hash = "sha256:f3da776b2811a6a1c71daab961335371ccbf51d45febbad8accd420153292994", size = 31877, upload-time = "2026-07-07T04:12:15.957Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d8/82/daa65a9a2fda6fc565b2143322a3e978c70ed92988bef2da5b90e7fc919c/mdformat_mkdocs-5.2.1-py3-none-any.whl", hash = "sha256:52be5295d1567da48c021a569ffbb70d2c731db059445a31fc1fd373999557fe", size = 43070, upload-time = "2026-07-07T04:12:14.436Z" }, +] + +[[package]] +name = "mdit-py-plugins" +version = "0.6.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "markdown-it-py" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/59/fc/f8d0863f8862f25602c0404d75568e89fb6b4109804645e5cdfb1be5cf56/mdit_py_plugins-0.6.1.tar.gz", hash = "sha256:a2bca0f039f39dbd35fb74ae1b5f998608c437463371f0ff7f49a19a17a114d0", size = 56114, upload-time = "2026-05-13T09:03:38.91Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a5/69/6da5581c6a7fede7dc261bf4e67d6adca4196f176b43288b55b3db395b6e/mdit_py_plugins-0.6.1-py3-none-any.whl", hash = "sha256:214c82fb2ac524472ab6a5bcab1de80f73b50443e187f401bfd77efbc7c6481d", size = 66663, upload-time = "2026-05-13T09:03:37.76Z" }, +] + +[[package]] +name = "mdurl" +version = "0.1.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d6/54/cfe61301667036ec958cb99bd3efefba235e65cdeb9c84d24a8293ba1d90/mdurl-0.1.2.tar.gz", hash = "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba", size = 8729, upload-time = "2022-08-14T12:40:10.846Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b3/38/89ba8ad64ae25be8de66a6d463314cf1eb366222074cfda9ee839c56a4b4/mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8", size = 9979, upload-time = "2022-08-14T12:40:09.779Z" }, +] + +[[package]] +name = "more-itertools" +version = "11.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/de/1d/f4da6f02cdffe04d6362210b807146a26044c88d839208aec273bb0d9184/more_itertools-11.1.0.tar.gz", hash = "sha256:48e8f4d9e7e5878571ecf6f2b4e57634f93cd474cc8cfbd2376f2d11b396e30d", size = 145772, upload-time = "2026-05-22T14:14:29.909Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e8/3d/1087453384dbde46a8c7f9356eead2c58be8a7bf156bca40243377c85715/more_itertools-11.1.0-py3-none-any.whl", hash = "sha256:4b65538ae22f6fed0ce4874efd317463a7489796a0939fa66824dd542125a192", size = 72226, upload-time = "2026-05-22T14:14:28.824Z" }, +] + +[[package]] +name = "pygments" +version = "2.20.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/c3/b2/bc9c9196916376152d655522fdcebac55e66de6603a76a02bca1b6414f6c/pygments-2.20.0.tar.gz", hash = "sha256:6757cd03768053ff99f3039c1a36d6c0aa0b263438fcab17520b30a303a82b5f", size = 4955991, upload-time = "2026-03-29T13:29:33.898Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f4/7e/a72dd26f3b0f4f2bf1dd8923c85f7ceb43172af56d63c7383eb62b332364/pygments-2.20.0-py3-none-any.whl", hash = "sha256:81a9e26dd42fd28a23a2d169d86d7ac03b46e2f8b59ed4698fb4785f946d0176", size = 1231151, upload-time = "2026-03-29T13:29:30.038Z" }, +] + +[[package]] +name = "pymdown-extensions" +version = "11.0.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "markdown" }, + { name = "pyyaml" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/21/a9/5f0c535ba3b08fe09270c16808e053a968868242ecbd5676d4e3a488bf28/pymdown_extensions-11.0.1.tar.gz", hash = "sha256:dd2905ae6fc5b75582fafb139a1266ffc754705efa902aa50067fa7ff4f94ec0", size = 857113, upload-time = "2026-07-02T17:59:22.955Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d6/54/da572c98c0b77626a91b5d3b89f0231d8bff5125c225420908632f8b342d/pymdown_extensions-11.0.1-py3-none-any.whl", hash = "sha256:db3943a62bab7e03af1364f0c4083e64b91fb097675a4b6cceccfbe9a77e5eb2", size = 269455, upload-time = "2026-07-02T17:59:21.271Z" }, +] + +[[package]] +name = "pyyaml" +version = "6.0.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/05/8e/961c0007c59b8dd7729d542c61a4d537767a59645b82a0b521206e1e25c2/pyyaml-6.0.3.tar.gz", hash = "sha256:d76623373421df22fb4cf8817020cbb7ef15c725b9d5e45f17e189bfc384190f", size = 130960, upload-time = "2025-09-25T21:33:16.546Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9d/8c/f4bd7f6465179953d3ac9bc44ac1a8a3e6122cf8ada906b4f96c60172d43/pyyaml-6.0.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:8d1fab6bb153a416f9aeb4b8763bc0f22a5586065f86f7664fc23339fc1c1fac", size = 181814, upload-time = "2025-09-25T21:32:35.712Z" }, + { url = "https://files.pythonhosted.org/packages/bd/9c/4d95bb87eb2063d20db7b60faa3840c1b18025517ae857371c4dd55a6b3a/pyyaml-6.0.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:34d5fcd24b8445fadc33f9cf348c1047101756fd760b4dacb5c3e99755703310", size = 173809, upload-time = "2025-09-25T21:32:36.789Z" }, + { url = "https://files.pythonhosted.org/packages/92/b5/47e807c2623074914e29dabd16cbbdd4bf5e9b2db9f8090fa64411fc5382/pyyaml-6.0.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:501a031947e3a9025ed4405a168e6ef5ae3126c59f90ce0cd6f2bfc477be31b7", size = 766454, upload-time = "2025-09-25T21:32:37.966Z" }, + { url = "https://files.pythonhosted.org/packages/02/9e/e5e9b168be58564121efb3de6859c452fccde0ab093d8438905899a3a483/pyyaml-6.0.3-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:b3bc83488de33889877a0f2543ade9f70c67d66d9ebb4ac959502e12de895788", size = 836355, upload-time = "2025-09-25T21:32:39.178Z" }, + { url = "https://files.pythonhosted.org/packages/88/f9/16491d7ed2a919954993e48aa941b200f38040928474c9e85ea9e64222c3/pyyaml-6.0.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c458b6d084f9b935061bc36216e8a69a7e293a2f1e68bf956dcd9e6cbcd143f5", size = 794175, upload-time = "2025-09-25T21:32:40.865Z" }, + { url = "https://files.pythonhosted.org/packages/dd/3f/5989debef34dc6397317802b527dbbafb2b4760878a53d4166579111411e/pyyaml-6.0.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7c6610def4f163542a622a73fb39f534f8c101d690126992300bf3207eab9764", size = 755228, upload-time = "2025-09-25T21:32:42.084Z" }, + { url = "https://files.pythonhosted.org/packages/d7/ce/af88a49043cd2e265be63d083fc75b27b6ed062f5f9fd6cdc223ad62f03e/pyyaml-6.0.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:5190d403f121660ce8d1d2c1bb2ef1bd05b5f68533fc5c2ea899bd15f4399b35", size = 789194, upload-time = "2025-09-25T21:32:43.362Z" }, + { url = "https://files.pythonhosted.org/packages/23/20/bb6982b26a40bb43951265ba29d4c246ef0ff59c9fdcdf0ed04e0687de4d/pyyaml-6.0.3-cp314-cp314-win_amd64.whl", hash = "sha256:4a2e8cebe2ff6ab7d1050ecd59c25d4c8bd7e6f400f5f82b96557ac0abafd0ac", size = 156429, upload-time = "2025-09-25T21:32:57.844Z" }, + { url = "https://files.pythonhosted.org/packages/f4/f4/a4541072bb9422c8a883ab55255f918fa378ecf083f5b85e87fc2b4eda1b/pyyaml-6.0.3-cp314-cp314-win_arm64.whl", hash = "sha256:93dda82c9c22deb0a405ea4dc5f2d0cda384168e466364dec6255b293923b2f3", size = 143912, upload-time = "2025-09-25T21:32:59.247Z" }, + { url = "https://files.pythonhosted.org/packages/7c/f9/07dd09ae774e4616edf6cda684ee78f97777bdd15847253637a6f052a62f/pyyaml-6.0.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:02893d100e99e03eda1c8fd5c441d8c60103fd175728e23e431db1b589cf5ab3", size = 189108, upload-time = "2025-09-25T21:32:44.377Z" }, + { url = "https://files.pythonhosted.org/packages/4e/78/8d08c9fb7ce09ad8c38ad533c1191cf27f7ae1effe5bb9400a46d9437fcf/pyyaml-6.0.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:c1ff362665ae507275af2853520967820d9124984e0f7466736aea23d8611fba", size = 183641, upload-time = "2025-09-25T21:32:45.407Z" }, + { url = "https://files.pythonhosted.org/packages/7b/5b/3babb19104a46945cf816d047db2788bcaf8c94527a805610b0289a01c6b/pyyaml-6.0.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6adc77889b628398debc7b65c073bcb99c4a0237b248cacaf3fe8a557563ef6c", size = 831901, upload-time = "2025-09-25T21:32:48.83Z" }, + { url = "https://files.pythonhosted.org/packages/8b/cc/dff0684d8dc44da4d22a13f35f073d558c268780ce3c6ba1b87055bb0b87/pyyaml-6.0.3-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a80cb027f6b349846a3bf6d73b5e95e782175e52f22108cfa17876aaeff93702", size = 861132, upload-time = "2025-09-25T21:32:50.149Z" }, + { url = "https://files.pythonhosted.org/packages/b1/5e/f77dc6b9036943e285ba76b49e118d9ea929885becb0a29ba8a7c75e29fe/pyyaml-6.0.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:00c4bdeba853cc34e7dd471f16b4114f4162dc03e6b7afcc2128711f0eca823c", size = 839261, upload-time = "2025-09-25T21:32:51.808Z" }, + { url = "https://files.pythonhosted.org/packages/ce/88/a9db1376aa2a228197c58b37302f284b5617f56a5d959fd1763fb1675ce6/pyyaml-6.0.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:66e1674c3ef6f541c35191caae2d429b967b99e02040f5ba928632d9a7f0f065", size = 805272, upload-time = "2025-09-25T21:32:52.941Z" }, + { url = "https://files.pythonhosted.org/packages/da/92/1446574745d74df0c92e6aa4a7b0b3130706a4142b2d1a5869f2eaa423c6/pyyaml-6.0.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:16249ee61e95f858e83976573de0f5b2893b3677ba71c9dd36b9cf8be9ac6d65", size = 829923, upload-time = "2025-09-25T21:32:54.537Z" }, + { url = "https://files.pythonhosted.org/packages/f0/7a/1c7270340330e575b92f397352af856a8c06f230aa3e76f86b39d01b416a/pyyaml-6.0.3-cp314-cp314t-win_amd64.whl", hash = "sha256:4ad1906908f2f5ae4e5a8ddfce73c320c2a1429ec52eafd27138b7f1cbe341c9", size = 174062, upload-time = "2025-09-25T21:32:55.767Z" }, + { url = "https://files.pythonhosted.org/packages/f1/12/de94a39c2ef588c7e6455cfbe7343d3b2dc9d6b6b2f40c4c6565744c873d/pyyaml-6.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:ebc55a14a21cb14062aa4162f906cd962b28e2e9ea38f9b4391244cd8de4ae0b", size = 149341, upload-time = "2025-09-25T21:32:56.828Z" }, +] + +[[package]] +name = "ruamel-yaml" +version = "0.19.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/c7/3b/ebda527b56beb90cb7652cb1c7e4f91f48649fbcd8d2eb2fb6e77cd3329b/ruamel_yaml-0.19.1.tar.gz", hash = "sha256:53eb66cd27849eff968ebf8f0bf61f46cdac2da1d1f3576dd4ccee9b25c31993", size = 142709, upload-time = "2026-01-02T16:50:31.84Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b8/0c/51f6841f1d84f404f92463fc2b1ba0da357ca1e3db6b7fbda26956c3b82a/ruamel_yaml-0.19.1-py3-none-any.whl", hash = "sha256:27592957fedf6e0b62f281e96effd28043345e0e66001f97683aa9a40c667c93", size = 118102, upload-time = "2026-01-02T16:50:29.201Z" }, +] + +[[package]] +name = "sharp-mud-docsite" +version = "0.1.0" +source = { virtual = "." } +dependencies = [ + { name = "mdformat" }, + { name = "mdformat-frontmatter" }, + { name = "mdformat-mkdocs" }, + { name = "zensical" }, +] + +[package.metadata] +requires-dist = [ + { name = "mdformat" }, + { name = "mdformat-frontmatter" }, + { name = "mdformat-mkdocs" }, + { name = "zensical", specifier = ">=0.0.47" }, +] + +[[package]] +name = "tomli" +version = "2.4.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/22/de/48c59722572767841493b26183a0d1cc411d54fd759c5607c4590b6563a6/tomli-2.4.1.tar.gz", hash = "sha256:7c7e1a961a0b2f2472c1ac5b69affa0ae1132c39adcb67aba98568702b9cc23f", size = 17543, upload-time = "2026-03-25T20:22:03.828Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3c/fb/9a5c8d27dbab540869f7c1f8eb0abb3244189ce780ba9cd73f3770662072/tomli-2.4.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:fd0409a3653af6c147209d267a0e4243f0ae46b011aa978b1080359fddc9b6cf", size = 155726, upload-time = "2026-03-25T20:21:42.23Z" }, + { url = "https://files.pythonhosted.org/packages/62/05/d2f816630cc771ad836af54f5001f47a6f611d2d39535364f148b6a92d6b/tomli-2.4.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:a120733b01c45e9a0c34aeef92bf0cf1d56cfe81ed9d47d562f9ed591a9828ac", size = 149859, upload-time = "2026-03-25T20:21:43.386Z" }, + { url = "https://files.pythonhosted.org/packages/ce/48/66341bdb858ad9bd0ceab5a86f90eddab127cf8b046418009f2125630ecb/tomli-2.4.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:559db847dc486944896521f68d8190be1c9e719fced785720d2216fe7022b662", size = 244713, upload-time = "2026-03-25T20:21:44.474Z" }, + { url = "https://files.pythonhosted.org/packages/df/6d/c5fad00d82b3c7a3ab6189bd4b10e60466f22cfe8a08a9394185c8a8111c/tomli-2.4.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:01f520d4f53ef97964a240a035ec2a869fe1a37dde002b57ebc4417a27ccd853", size = 252084, upload-time = "2026-03-25T20:21:45.62Z" }, + { url = "https://files.pythonhosted.org/packages/00/71/3a69e86f3eafe8c7a59d008d245888051005bd657760e96d5fbfb0b740c2/tomli-2.4.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7f94b27a62cfad8496c8d2513e1a222dd446f095fca8987fceef261225538a15", size = 247973, upload-time = "2026-03-25T20:21:46.937Z" }, + { url = "https://files.pythonhosted.org/packages/67/50/361e986652847fec4bd5e4a0208752fbe64689c603c7ae5ea7cb16b1c0ca/tomli-2.4.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:ede3e6487c5ef5d28634ba3f31f989030ad6af71edfb0055cbbd14189ff240ba", size = 256223, upload-time = "2026-03-25T20:21:48.467Z" }, + { url = "https://files.pythonhosted.org/packages/8c/9a/b4173689a9203472e5467217e0154b00e260621caa227b6fa01feab16998/tomli-2.4.1-cp314-cp314-win32.whl", hash = "sha256:3d48a93ee1c9b79c04bb38772ee1b64dcf18ff43085896ea460ca8dec96f35f6", size = 98973, upload-time = "2026-03-25T20:21:49.526Z" }, + { url = "https://files.pythonhosted.org/packages/14/58/640ac93bf230cd27d002462c9af0d837779f8773bc03dee06b5835208214/tomli-2.4.1-cp314-cp314-win_amd64.whl", hash = "sha256:88dceee75c2c63af144e456745e10101eb67361050196b0b6af5d717254dddf7", size = 109082, upload-time = "2026-03-25T20:21:50.506Z" }, + { url = "https://files.pythonhosted.org/packages/d5/2f/702d5e05b227401c1068f0d386d79a589bb12bf64c3d2c72ce0631e3bc49/tomli-2.4.1-cp314-cp314-win_arm64.whl", hash = "sha256:b8c198f8c1805dc42708689ed6864951fd2494f924149d3e4bce7710f8eb5232", size = 96490, upload-time = "2026-03-25T20:21:51.474Z" }, + { url = "https://files.pythonhosted.org/packages/45/4b/b877b05c8ba62927d9865dd980e34a755de541eb65fffba52b4cc495d4d2/tomli-2.4.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:d4d8fe59808a54658fcc0160ecfb1b30f9089906c50b23bcb4c69eddc19ec2b4", size = 164263, upload-time = "2026-03-25T20:21:52.543Z" }, + { url = "https://files.pythonhosted.org/packages/24/79/6ab420d37a270b89f7195dec5448f79400d9e9c1826df982f3f8e97b24fd/tomli-2.4.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:7008df2e7655c495dd12d2a4ad038ff878d4ca4b81fccaf82b714e07eae4402c", size = 160736, upload-time = "2026-03-25T20:21:53.674Z" }, + { url = "https://files.pythonhosted.org/packages/02/e0/3630057d8eb170310785723ed5adcdfb7d50cb7e6455f85ba8a3deed642b/tomli-2.4.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1d8591993e228b0c930c4bb0db464bdad97b3289fb981255d6c9a41aedc84b2d", size = 270717, upload-time = "2026-03-25T20:21:55.129Z" }, + { url = "https://files.pythonhosted.org/packages/7a/b4/1613716072e544d1a7891f548d8f9ec6ce2faf42ca65acae01d76ea06bb0/tomli-2.4.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:734e20b57ba95624ecf1841e72b53f6e186355e216e5412de414e3c51e5e3c41", size = 278461, upload-time = "2026-03-25T20:21:56.228Z" }, + { url = "https://files.pythonhosted.org/packages/05/38/30f541baf6a3f6df77b3df16b01ba319221389e2da59427e221ef417ac0c/tomli-2.4.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:8a650c2dbafa08d42e51ba0b62740dae4ecb9338eefa093aa5c78ceb546fcd5c", size = 274855, upload-time = "2026-03-25T20:21:57.653Z" }, + { url = "https://files.pythonhosted.org/packages/77/a3/ec9dd4fd2c38e98de34223b995a3b34813e6bdadf86c75314c928350ed14/tomli-2.4.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:504aa796fe0569bb43171066009ead363de03675276d2d121ac1a4572397870f", size = 283144, upload-time = "2026-03-25T20:21:59.089Z" }, + { url = "https://files.pythonhosted.org/packages/ef/be/605a6261cac79fba2ec0c9827e986e00323a1945700969b8ee0b30d85453/tomli-2.4.1-cp314-cp314t-win32.whl", hash = "sha256:b1d22e6e9387bf4739fbe23bfa80e93f6b0373a7f1b96c6227c32bef95a4d7a8", size = 108683, upload-time = "2026-03-25T20:22:00.214Z" }, + { url = "https://files.pythonhosted.org/packages/12/64/da524626d3b9cc40c168a13da8335fe1c51be12c0a63685cc6db7308daae/tomli-2.4.1-cp314-cp314t-win_amd64.whl", hash = "sha256:2c1c351919aca02858f740c6d33adea0c5deea37f9ecca1cc1ef9e884a619d26", size = 121196, upload-time = "2026-03-25T20:22:01.169Z" }, + { url = "https://files.pythonhosted.org/packages/5a/cd/e80b62269fc78fc36c9af5a6b89c835baa8af28ff5ad28c7028d60860320/tomli-2.4.1-cp314-cp314t-win_arm64.whl", hash = "sha256:eab21f45c7f66c13f2a9e0e1535309cee140182a9cdae1e041d02e47291e8396", size = 100393, upload-time = "2026-03-25T20:22:02.137Z" }, + { url = "https://files.pythonhosted.org/packages/7b/61/cceae43728b7de99d9b847560c262873a1f6c98202171fd5ed62640b494b/tomli-2.4.1-py3-none-any.whl", hash = "sha256:0d85819802132122da43cb86656f8d1f8c6587d54ae7dcaf30e90533028b49fe", size = 14583, upload-time = "2026-03-25T20:22:03.012Z" }, +] + +[[package]] +name = "wcwidth" +version = "0.8.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/34/74/c6428f875774288bec1396f5bfcbc2d925700a4dad61727fd5f2b12f249d/wcwidth-0.8.2.tar.gz", hash = "sha256:91fbef97204b96a3d4d421609b80340b760cf33e26da123ff243d76b1fda8dda", size = 1466253, upload-time = "2026-06-29T18:11:11.601Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/96/42/3e5985a0a7e57de470b320c6d6a1a67c844f6737a587f3d44dd13d1819e7/wcwidth-0.8.2-py3-none-any.whl", hash = "sha256:d63947694a0539a1d51e01eda7caf800c291020e6cdd7e28ad7b14dd33ad4f85", size = 323166, upload-time = "2026-06-29T18:11:09.888Z" }, +] + +[[package]] +name = "zensical" +version = "0.0.51" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "click" }, + { name = "deepmerge" }, + { name = "jinja2" }, + { name = "markdown" }, + { name = "pygments" }, + { name = "pymdown-extensions" }, + { name = "pyyaml" }, + { name = "tomli" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b8/f7/d07ffb268ca86afb26b7f32dbabe25dec03d3aa63ba4d876720c84681d33/zensical-0.0.51.tar.gz", hash = "sha256:de25de067bedfa18f916d7f366fd64a7fbf09bfcc615b44d1ddbe3b5fe02ab49", size = 3979640, upload-time = "2026-07-17T18:08:03.445Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/48/21/02db3e1fb3904016bfac310037c95b9f1eaaf0ffe7b4a84f14263a7d95df/zensical-0.0.51-cp310-abi3-macosx_10_12_x86_64.whl", hash = "sha256:134d776afa526098e05e34713e2f577c075e57a232e01b97842bb0206716afce", size = 12791154, upload-time = "2026-07-17T18:07:20.748Z" }, + { url = "https://files.pythonhosted.org/packages/a2/35/b0d96f58253514cb3d08f5779020ab01ee5472334fb984b92e3fc9e9c9ac/zensical-0.0.51-cp310-abi3-macosx_11_0_arm64.whl", hash = "sha256:e97ab39668ae3b452c550634e921a0336443743aae5e1fe031c7bb57d049e535", size = 12692190, upload-time = "2026-07-17T18:07:24.553Z" }, + { url = "https://files.pythonhosted.org/packages/2e/90/7a60e126a10c37c6b789938ff17e73fe76bba707fa029cb40ac659aeaa82/zensical-0.0.51-cp310-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3c9579809f88608e7aa2cff516fff9d267d74a843cf6088a5f4227de2f092bb5", size = 13139337, upload-time = "2026-07-17T18:07:27.885Z" }, + { url = "https://files.pythonhosted.org/packages/ae/c3/9101c97b90d4713ef2816db03366a45ae4762efebffd296737a2dd2df325/zensical-0.0.51-cp310-abi3-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:296dc7a14aa28b81a58eb57df2d5c9c9a4b0de7e90c11d99c943354287952925", size = 13069851, upload-time = "2026-07-17T18:07:31.814Z" }, + { url = "https://files.pythonhosted.org/packages/d2/79/0474df9e15a2c18f6281a786e10177c1b6e16feac1c568e7f36ad39b339c/zensical-0.0.51-cp310-abi3-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f779d2d87b4bf228cf2e279bc0ae6bcf3b36a9335ff283a317d01f7c15ae46b2", size = 13451083, upload-time = "2026-07-17T18:07:35.543Z" }, + { url = "https://files.pythonhosted.org/packages/fe/6f/91bbf78f704d5fd4c0c9be27d6bce3b6e4c2c339e4dcd6e7cf19ecda643c/zensical-0.0.51-cp310-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:67f813a1514a90890ca86248a8d54b81b2164bcbff11a6bcf11b01e1c01a1454", size = 13110446, upload-time = "2026-07-17T18:07:38.783Z" }, + { url = "https://files.pythonhosted.org/packages/d9/89/aa9a95f81771614c37bdc52b8ab21fcdef4c8de7c9cedf34e9bf62674281/zensical-0.0.51-cp310-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:186ef37e0eee0e969e2cfae47b1b97775e3164e2cba95c71faa4dd6ef47ed009", size = 13315871, upload-time = "2026-07-17T18:07:42.43Z" }, + { url = "https://files.pythonhosted.org/packages/08/11/1bf6e9ded29d376f8c12644cc4de04676b010fee8caa17f682606b1f16d5/zensical-0.0.51-cp310-abi3-musllinux_1_2_armv7l.whl", hash = "sha256:f5d91ce246ed930224603083cef02ae8947132fc7c52901d72015ea03526fa58", size = 13344382, upload-time = "2026-07-17T18:07:46.066Z" }, + { url = "https://files.pythonhosted.org/packages/d6/ec/663f16ff82d08b212e7c3236a88bd332f73331f94ddac1c91aaf882bbd1e/zensical-0.0.51-cp310-abi3-musllinux_1_2_i686.whl", hash = "sha256:b1108eae82c6e8ffc33026f60b485c1512647a5333be4f547166b7c8877b98af", size = 13499628, upload-time = "2026-07-17T18:07:49.196Z" }, + { url = "https://files.pythonhosted.org/packages/60/b4/7f1b6c3cf06d9f6ff5216523168a5d6ccc693444d5ceeb911eca97b30d98/zensical-0.0.51-cp310-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:6fa0ecaf14f56841bfc595fa141396350c72aafbec73a016ebe3c824ed21ac72", size = 13451420, upload-time = "2026-07-17T18:07:52.563Z" }, + { url = "https://files.pythonhosted.org/packages/0a/44/be4bc09ec8f69e7be1b07b875887961c4e0e478b03a10d2cc624ef28fbe6/zensical-0.0.51-cp310-abi3-win32.whl", hash = "sha256:fb7ff4946b72168759c6af0a29cf5de4c38aebe633a83292e8cd4145b5213cc2", size = 12375639, upload-time = "2026-07-17T18:07:56.081Z" }, + { url = "https://files.pythonhosted.org/packages/1f/85/aa827c244ed4f404e99a91c3ecf5e5adb62eca806a9e9c8e3333bbad8660/zensical-0.0.51-cp310-abi3-win_amd64.whl", hash = "sha256:12529d3d3991b63820952111dc1d1edc29b2c9b3a3abb16c243bcb649631ebf2", size = 12628965, upload-time = "2026-07-17T18:07:59.741Z" }, +] diff --git a/docsite/zensical.toml b/docsite/zensical.toml new file mode 100644 index 0000000..6c25f67 --- /dev/null +++ b/docsite/zensical.toml @@ -0,0 +1,70 @@ +[project] +site_name = "sharp-mud" +site_description = "A modern C#/.NET engine for building MUDs (Multi-User Dungeons)." +site_author = "LayeredCraft" +site_url = "https://sharp-mud.layeredcraft.dev/" +repo_url = "https://github.com/LayeredCraft/sharp-mud" +repo_name = "LayeredCraft/sharp-mud" +edit_uri = "edit/main/docsite/docs/" +copyright = "Copyright © 2026 LayeredCraft" +docs_dir = "docs" + +nav = [ + { "Home" = "index.md" }, + { "Getting Started" = "getting-started.md" } +] + +[project.markdown_extensions.attr_list] +[project.markdown_extensions.md_in_html] +[project.markdown_extensions.pymdownx.emoji] +emoji_index = "zensical.extensions.emoji.twemoji" +emoji_generator = "zensical.extensions.emoji.to_svg" +[project.markdown_extensions.admonition] +[project.markdown_extensions.pymdownx.details] +[project.markdown_extensions.pymdownx.superfences] +[project.markdown_extensions.pymdownx.highlight] +anchor_linenums = true +line_spans = "__span" +pygments_lang_class = true +[project.markdown_extensions.pymdownx.inlinehilite] +[project.markdown_extensions.pymdownx.snippets] +[project.markdown_extensions.toc] +permalink = true +permalink_title = "Anchor link to this section" + +[project.theme] +logo = "images/icon.png" +favicon = "images/icon.png" +features = [ + "navigation.footer", + "navigation.indexes", + "content.action.edit", + "content.action.view", + "content.code.copy", + "content.code.select" +] + +[[project.theme.palette]] +media = "(prefers-color-scheme)" +toggle.icon = "lucide/sun-moon" +toggle.name = "Switch to light mode" + +[[project.theme.palette]] +media = "(prefers-color-scheme: light)" +scheme = "default" +toggle.icon = "lucide/sun" +toggle.name = "Switch to dark mode" + +[[project.theme.palette]] +media = "(prefers-color-scheme: dark)" +scheme = "slate" +toggle.icon = "lucide/moon" +toggle.name = "Switch to system preference" + +[project.extra] +homepage = "https://github.com/LayeredCraft/sharp-mud" + +[[project.extra.social]] +icon = "fontawesome/brands/github" +link = "https://github.com/LayeredCraft/sharp-mud" +name = "LayeredCraft on GitHub" diff --git a/icon.png b/icon.png new file mode 100644 index 0000000000000000000000000000000000000000..701cb2cdd4ec15393f6f4823439bb50381dc7522 GIT binary patch literal 6852 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 E?^Y_s4FCWD literal 0 HcmV?d00001 diff --git a/src/SharpMud.Persistence.DynamoDb/ServiceCollectionExtensions.cs b/src/SharpMud.Persistence.DynamoDb/ServiceCollectionExtensions.cs index 785477d..9047be8 100644 --- a/src/SharpMud.Persistence.DynamoDb/ServiceCollectionExtensions.cs +++ b/src/SharpMud.Persistence.DynamoDb/ServiceCollectionExtensions.cs @@ -16,9 +16,10 @@ public static class ServiceCollectionExtensions /// endpoint (e.g. DynamoDB Local for development). /// /// - /// No is registered here - - /// unlike SQLite, this provider does not create or migrate DynamoDB - /// tables; the table(s) must already exist. + /// No IStorageInitializer is registered here (this project has + /// no reference to SharpMud.Hosting) - unlike SQLite, this + /// provider does not create or migrate DynamoDB tables; the table(s) + /// must already exist. /// public static IServiceCollection AddSharpMudDynamoDbPersistence(this IServiceCollection services, Action? configureClient = null) { diff --git a/src/SharpMud/SharpMud.csproj b/src/SharpMud/SharpMud.csproj new file mode 100644 index 0000000..e537319 --- /dev/null +++ b/src/SharpMud/SharpMud.csproj @@ -0,0 +1,32 @@ + + + + + + + + + + + + + + + net10.0;net11.0 + enable + enable + + false + $(NoWarn);NU5128 + Meta-package for sharp-mud - pulls in every SharpMud.* package (Engine, Hosting, Persistence, Adapters). See https://github.com/LayeredCraft/sharp-mud for getting started. + + + From 7a3814f77e62f152d39829e0d6ad32b2757e9bbf Mon Sep 17 00:00:00 2001 From: Nick Cipollina Date: Mon, 20 Jul 2026 13:15:51 -0400 Subject: [PATCH 05/14] fix: use the shared LayeredCraft repo icon instead of a one-off Co-Authored-By: Claude Sonnet 5 --- docsite/docs/images/icon.png | Bin 6852 -> 85222 bytes icon.png | Bin 6852 -> 85222 bytes 2 files changed, 0 insertions(+), 0 deletions(-) diff --git a/docsite/docs/images/icon.png b/docsite/docs/images/icon.png index 701cb2cdd4ec15393f6f4823439bb50381dc7522..17a036b65af4b2457cd6f8331aa17876d3d22181 100644 GIT binary patch literal 85222 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%0AbgRhc=K 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 literal 6852 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 E?^Y_s4FCWD diff --git a/icon.png b/icon.png index 701cb2cdd4ec15393f6f4823439bb50381dc7522..17a036b65af4b2457cd6f8331aa17876d3d22181 100644 GIT binary patch literal 85222 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%0AbgRhc=K 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 literal 6852 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 E?^Y_s4FCWD From ad3213c1d39985a17d334343b778ef4aad3fb576 Mon Sep 17 00:00:00 2001 From: Nick Cipollina Date: Mon, 20 Jul 2026 13:44:49 -0400 Subject: [PATCH 06/14] fix: drop redundant Configuration packages, bump to .NET 11 preview.6, fix Codex-flagged getting-started sample - Sample no longer explicitly references/loads Microsoft.Extensions.Configuration.Json/ .EnvironmentVariables - Microsoft.Extensions.Hosting already pulls these in and Host.CreateApplicationBuilder wires them up by default. - Bump global.json, Directory.Packages.props' net11.0 group, Dockerfile, and docs/deployment.md from preview.5.26302.115 to preview.6.26359.118 (matching the installed SDK); bump Microsoft.SourceLink.GitHub to the matching preview.6 release. Left EntityFrameworkCore.DynamoDb on the stable 10.0.0 line - its only net11.0 build still depends on EF Core preview.5, one behind ours - and updated the comment to record why. - Fix Codex review feedback on PR #9: docsite/docs/getting-started.md's sample IWorldBuilder used ThingId.New() for its root id, which changes every process start and defeats reload-from-persistence; switch to a fixed Guid like the real HubWorldBuilder does. Co-Authored-By: Claude Sonnet 5 --- Directory.Packages.props | 60 +++++++++++++------ Dockerfile | 4 +- docs/deployment.md | 2 +- docsite/docs/getting-started.md | 6 +- global.json | 2 +- samples/SharpMud.Samples.Classic/Program.cs | 6 +- .../SharpMud.Samples.Classic.csproj | 2 - 7 files changed, 54 insertions(+), 28 deletions(-) diff --git a/Directory.Packages.props b/Directory.Packages.props index 55d8336..099fa0e 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -5,23 +5,26 @@ - - - - - - - + - - + + + + + + + + + + + - - + + + + + + + + - - + + @@ -52,8 +76,8 @@ - + - + \ No newline at end of file diff --git a/Dockerfile b/Dockerfile index cd6cfd0..29c0c82 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,7 +1,7 @@ # Multi-stage build. Pinned to the same .NET 11 preview SDK/runtime version # as global.json - see docs/architecture.md Open Items for the fallback-to- # .NET-10-LTS plan if preview tooling support lags. -FROM mcr.microsoft.com/dotnet/sdk:11.0.100-preview.5 AS build +FROM mcr.microsoft.com/dotnet/sdk:11.0.100-preview.6 AS build WORKDIR /src COPY Directory.Build.props Directory.Packages.props global.json ./ @@ -18,7 +18,7 @@ COPY src/ src/ COPY samples/ samples/ RUN dotnet publish samples/SharpMud.Samples.Classic/SharpMud.Samples.Classic.csproj -c Release -o /app --no-restore -FROM mcr.microsoft.com/dotnet/runtime:11.0.0-preview.5-alpine3.24 AS runtime +FROM mcr.microsoft.com/dotnet/runtime:11.0.0-preview.6-alpine3.24 AS runtime WORKDIR /app COPY --from=build /app . diff --git a/docs/deployment.md b/docs/deployment.md index 1bf68af..6782df1 100644 --- a/docs/deployment.md +++ b/docs/deployment.md @@ -12,7 +12,7 @@ references — Engine, Hosting, Persistence + Persistence.Sqlite, both adapters), then the published output is copied into a much smaller `mcr.microsoft.com/dotnet/runtime` Alpine image. Both stages are pinned to the exact SDK/runtime version in -`global.json` (`11.0.100-preview.5`) — confirmed to exist on MCR as of this +`global.json` (`11.0.100-preview.6`) — confirmed to exist on MCR as of this writing; see [architecture.md](architecture.md) Open Items for the .NET 10 LTS fallback plan if that ever stops being true. diff --git a/docsite/docs/getting-started.md b/docsite/docs/getting-started.md index 13d51cd..9f85aae 100644 --- a/docsite/docs/getting-started.md +++ b/docsite/docs/getting-started.md @@ -33,7 +33,11 @@ using SharpMud.Hosting; public sealed class MyWorldBuilder : IWorldBuilder { - public static readonly ThingId RootRoomId = ThingId.New(); + // Fixed, not ThingId.New() - a fresh boot needs to ask the repository + // "does this root already exist?" (LoadTreeAsync(RootRoomId)) using the + // same id every time, or a persisted world can never be found again on + // the next run. + public static readonly ThingId RootRoomId = new(Guid.Parse("00000000-0000-0000-0000-000000000001")); public ThingId RootId => RootRoomId; diff --git a/global.json b/global.json index 5ec2999..ca9888e 100644 --- a/global.json +++ b/global.json @@ -1,6 +1,6 @@ { "sdk": { - "version": "11.0.100-preview.5.26302.115", + "version": "11.0.100-preview.6.26359.118", "rollForward": "latestFeature" }, "test": { diff --git a/samples/SharpMud.Samples.Classic/Program.cs b/samples/SharpMud.Samples.Classic/Program.cs index 248105e..9852325 100644 --- a/samples/SharpMud.Samples.Classic/Program.cs +++ b/samples/SharpMud.Samples.Classic/Program.cs @@ -1,5 +1,4 @@ using LayeredCraft.StructuredLogging; -using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; @@ -19,8 +18,9 @@ // with environment variables able to override it - see ADR-0003 // (docs/adr/0003-allow-appsettingsjson-for-non-secret-config.md). Secrets // still never go in appsettings.json; only env vars (SharpMudHostOptions.Parse -// below) carry those. -builder.Configuration.AddJsonFile("appsettings.json", optional: false); +// below) carry those. The generic host's default builder already loads +// appsettings.json (+ env vars, + command line) - no explicit AddJsonFile +// call needed here. // Console sink only - this repo runs in Docker (docs/deployment.md), stdout // is the sink Docker already captures. See ADR-0002 diff --git a/samples/SharpMud.Samples.Classic/SharpMud.Samples.Classic.csproj b/samples/SharpMud.Samples.Classic/SharpMud.Samples.Classic.csproj index fbe879e..f02e95d 100644 --- a/samples/SharpMud.Samples.Classic/SharpMud.Samples.Classic.csproj +++ b/samples/SharpMud.Samples.Classic/SharpMud.Samples.Classic.csproj @@ -18,8 +18,6 @@ - - From 131e0eb93f9e4d824e7d907c092cfe0803ee4de4 Mon Sep 17 00:00:00 2001 From: Nick Cipollina Date: Mon, 20 Jul 2026 14:01:28 -0400 Subject: [PATCH 07/14] docs: flag sharp-mud as alpha/pre-1.0 across README, SPEC, and the docs site No SemVer compatibility guarantees yet and only prerelease packages are published so far - make that explicit before someone builds against it expecting stability. Also fixes getting-started.md's install commands, which were missing --prerelease (no stable release exists to resolve without it). Co-Authored-By: Claude Sonnet 5 --- README.md | 6 ++++++ SPEC.md | 4 ++++ docsite/docs/getting-started.md | 12 +++++++++--- docsite/docs/index.md | 6 ++++++ 4 files changed, 25 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 2a0c3b0..f00b182 100644 --- a/README.md +++ b/README.md @@ -3,6 +3,12 @@ A modern C#/.NET reimagining of a classic MUD (Multi-User Dungeon) — faithful to the genre's feel while built with current .NET architecture. +> **Alpha — not yet stable.** Pre-1.0, no SemVer compatibility guarantees +> between releases yet; APIs, package boundaries, and persisted-data shape can +> all still change. Also currently pinned to a preview .NET 11 SDK (see +> `global.json`) — accept that risk, or build against `net10.0` only until a +> stable release lands. Use it, but expect breaking changes. + - **`SPEC.md`** — vision and high-level decisions (start here). - **`docs/`** — detailed per-subsystem design docs. Start with `docs/engine-vs-ruleset.md` for the entity model (`Thing`/`Behavior` diff --git a/SPEC.md b/SPEC.md index 4078841..27a1c31 100644 --- a/SPEC.md +++ b/SPEC.md @@ -1,5 +1,9 @@ # sharp-mud — Design Spec +**Status: alpha, pre-1.0.** No SemVer compatibility guarantees between +releases yet — APIs, package boundaries, and persisted-data shape can all +still change. + A modern C# reimagining of a classic MUD. Faithful to the genre's feel (verb-first commands, room-based navigation, persistent world) while using current .NET architecture patterns instead of the C/C++ codebases (Diku/Circle/LP-family) that diff --git a/docsite/docs/getting-started.md b/docsite/docs/getting-started.md index 9f85aae..536b7e4 100644 --- a/docsite/docs/getting-started.md +++ b/docsite/docs/getting-started.md @@ -8,18 +8,24 @@ For a full worked example with stats, combat, and a Telnet transport, see [`SharpMud.Samples.Classic`](https://github.com/LayeredCraft/sharp-mud/tree/main/samples/SharpMud.Samples.Classic) in the sharp-mud repo. +!!! warning "Alpha — not yet stable" + sharp-mud is pre-1.0: no SemVer compatibility guarantees between + releases yet, and only prerelease packages are published so far - expect + breaking changes between versions. + ## Install ```bash -dotnet add package SharpMud -dotnet add package SharpMud.Persistence.Sqlite -dotnet add package SharpMud.Adapters.Cli +dotnet add package SharpMud --prerelease +dotnet add package SharpMud.Persistence.Sqlite --prerelease +dotnet add package SharpMud.Adapters.Cli --prerelease ``` `SharpMud` is a meta-package pulling in `SharpMud.Engine`, `SharpMud.Hosting`, `SharpMud.Persistence`, and both adapter packages — you still pick a persistence *provider* (`.Sqlite` or `.DynamoDb`) and a transport (`SharpMud.Adapters.Cli` and/or `SharpMud.Adapters.Telnet`) explicitly. +`--prerelease` is required until a stable 1.0 release ships. ## Build the world diff --git a/docsite/docs/index.md b/docsite/docs/index.md index b2a2d39..869178d 100644 --- a/docsite/docs/index.md +++ b/docsite/docs/index.md @@ -5,6 +5,12 @@ the genre's feel while built on current .NET architecture: the .NET generic host, EF Core, and a `Thing`/`Behavior` composition model instead of a hardcoded class hierarchy. +!!! warning "Alpha — not yet stable" + sharp-mud is pre-1.0: no SemVer compatibility guarantees between releases + yet, and APIs/package boundaries/persisted-data shape can all still + change. It's also currently built against a preview .NET 11 SDK. Use it, + but expect breaking changes between versions. + sharp-mud ships as a set of NuGet packages you compose into your own game: - **`SharpMud.Engine`** — `Thing`/`Behavior`, the event system, generic From 604d73bb6547db1bd4a4fe49a69c96063f4ebc50 Mon Sep 17 00:00:00 2001 From: Nick Cipollina Date: Mon, 20 Jul 2026 14:47:22 -0400 Subject: [PATCH 08/14] fix: address PR #9 review feedback - shutdown-save race, package descriptions, warning-clean build, DB path CLI arg, narrow meta-package scope - Fix shutdown-save stop-order race: GameLoopHostedService now registered after ShutdownSaveHostedService, so the generic host's reverse-order stop sequence quiesces the tick loop before the snapshot save runs, not concurrently with it. New regression test verifies this ordering directly and was confirmed to fail against the old ordering. - Add a real Description to every SharpMud.* package's csproj (only the meta-package had one before). - Pin NSubstitute back to 5.3.0 (AutoFixture.AutoNSubstitute 4.18.1's <6.0.0 constraint - only unreleased AutoFixture previews support 6.x), which also resolved two nullable warnings NSubstitute 6.0's newly-annotated CallInfo indexer introduced in SessionLoopTests.cs. Suppress the pre-existing xUnit1051 false-positive in TelnetListenerTests.cs via pragma - the token genuinely derives from TestContext.Current.CancellationToken through a linked source the analyzer can't trace. Solution now builds with 0 warnings. - Add --db-path CLI arg support (wins over SHARPMUD_DB_PATH, same precedence as --telnet), resolved in the sample's own Program.cs matching the transport-selection precedent, rather than just downgrading docs/persistence.md to describe a CLI-arg gap that didn't need to exist. - ADR-0007: narrow the SharpMud meta-package to Engine+Hosting+Persistence only, dropping the persistence-provider/adapter ProjectReferences ADR-0006 originally included. Every consumer was getting an unused DynamoDB SDK dependency and Telnet listener by default, and it made getting-started.md's "you still pick a provider/transport explicitly" framing false. Verified via a real dotnet pack - the meta-package's nuspec now only depends on SharpMud.Engine/.Hosting/.Persistence. Co-Authored-By: Claude Sonnet 5 --- Directory.Packages.props | 7 +- README.md | 4 +- docs/adr/0006-nuget-package-distribution.md | 4 + docs/adr/0007-narrow-meta-package-scope.md | 98 +++++++++++++++++++ docs/adr/README.md | 1 + docs/deployment.md | 2 +- docs/persistence.md | 11 ++- docsite/docs/getting-started.md | 9 +- samples/SharpMud.Samples.Classic/Program.cs | 10 +- .../SharpMud.Adapters.Cli.csproj | 1 + .../SharpMud.Adapters.Telnet.csproj | 1 + src/SharpMud.Engine/SharpMud.Engine.csproj | 1 + .../ServiceCollectionExtensions.cs | 21 +++- src/SharpMud.Hosting/SharpMud.Hosting.csproj | 1 + .../SharpMud.Persistence.DynamoDb.csproj | 1 + .../SharpMud.Persistence.Sqlite.csproj | 1 + .../SharpMud.Persistence.csproj | 1 + src/SharpMud/SharpMud.csproj | 25 ++--- .../TelnetListenerTests.cs | 2 + .../ServiceCollectionExtensionsTests.cs | 31 ++++++ 20 files changed, 203 insertions(+), 29 deletions(-) create mode 100644 docs/adr/0007-narrow-meta-package-scope.md create mode 100644 tests/SharpMud.Hosting.Tests/ServiceCollectionExtensionsTests.cs diff --git a/Directory.Packages.props b/Directory.Packages.props index 099fa0e..614be1c 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -75,8 +75,11 @@ - - + + diff --git a/README.md b/README.md index f00b182..7b8c4ab 100644 --- a/README.md +++ b/README.md @@ -59,7 +59,9 @@ dotnet run --project samples/SharpMud.Samples.Classic -- --telnet [port] # teln `src/` publishes as NuGet packages (`SharpMud.Engine`, `SharpMud.Hosting`, `SharpMud.Persistence`(`.Sqlite`/`.DynamoDb`), `SharpMud.Adapters.Cli`/ -`.Telnet`), plus a `SharpMud` meta-package pulling in all of them. +`.Telnet`), plus a `SharpMud` meta-package pulling in the engine-level core +(`Engine`/`Hosting`/`Persistence`) — you still add a persistence provider and +a transport explicitly, per [ADR-0007](docs/adr/0007-narrow-meta-package-scope.md). `samples/SharpMud.Samples.Classic` is a full reference consumer — start there to see how a ruleset composes against the packages. See [ADR-0006](docs/adr/0006-nuget-package-distribution.md) for the design. diff --git a/docs/adr/0006-nuget-package-distribution.md b/docs/adr/0006-nuget-package-distribution.md index d28bb78..4545ce1 100644 --- a/docs/adr/0006-nuget-package-distribution.md +++ b/docs/adr/0006-nuget-package-distribution.md @@ -745,6 +745,10 @@ own composition root the way today's `SharpMud.Host` does. - [docs/research/wheelmud-findings.md](../research/wheelmud-findings.md) — WheelMUD prior art and license citation - [PLAN-0006](../plans/0006-nuget-package-distribution.md) +- [ADR-0007](0007-narrow-meta-package-scope.md) — narrows this ADR's + meta-package package set (Engine + Hosting + Persistence only, not + persistence providers or transport adapters); the rest of this ADR is + unchanged - `coding-standards.md`'s DI/composition section (corrected in this change) - WheelMUD license: (MS-PL — see License and naming above for why this doesn't constrain diff --git a/docs/adr/0007-narrow-meta-package-scope.md b/docs/adr/0007-narrow-meta-package-scope.md new file mode 100644 index 0000000..e5790ef --- /dev/null +++ b/docs/adr/0007-narrow-meta-package-scope.md @@ -0,0 +1,98 @@ +# [ADR-0007] Narrow the `SharpMud` Meta-Package to Engine + Hosting + Persistence + +**Status:** Accepted + +**Date:** 2026-07-20 + +**Decision Makers:** solo + +## Context + +[ADR-0006](0006-nuget-package-distribution.md) shipped `SharpMud` as a +meta-package with `ProjectReference`s to *every* other `SharpMud.*` package — +`Engine`, `Hosting`, `Persistence`, both persistence providers +(`.Sqlite`/`.DynamoDb`), and both transport adapters (`Adapters.Cli`/ +`.Telnet`). That was a deliberate choice at the time ("no `PackageId` +overrides are needed except on the new meta-package... `ProjectReference`s to +everything above"). + +In PR review of the docsite Getting Started page, this surfaced a real +problem: a consumer running `dotnet add package SharpMud` already gets both +persistence providers and both transport adapters transitively, making the +docs' "you still pick a persistence provider and a transport explicitly" +framing false, and defeating the ala-carte flexibility ADR-0006 itself named +as a decision driver. Every consumer pulls in a DynamoDB SDK dependency and +a Telnet listener even if they only ever use SQLite + CLI, which isn't what +"quick start" should mean for a package whose whole pitch is choosing your +own persistence/transport. + +## Decision Drivers + +- The meta-package should be a fast on-ramp for the *engine-level* pieces + every consumer needs (Engine, Hosting, provider-agnostic Persistence), not + a way to sidestep the ala-carte provider/transport choice ADR-0006 itself + motivated. +- Docs (`getting-started.md`) already describe `SharpMud` as "you still pick + a persistence provider and a transport" — narrowing the package to match + is less churn than rewriting every doc/example to match the current + all-inclusive package. + +## Considered Options + +1. Keep `SharpMud` including every package (status quo from ADR-0006). +2. Narrow `SharpMud` to `Engine` + `Hosting` + `Persistence` only — a + consumer still explicitly adds one persistence provider package and one + (or more) transport adapter package(s). + +## Decision Outcome + +Chosen option: **2 — narrow the meta-package.** `SharpMud`'s +`ProjectReference`s become `SharpMud.Engine`, `SharpMud.Hosting`, and +`SharpMud.Persistence` only. Persistence providers and transport adapters +are always explicit `dotnet add package` calls, regardless of whether a +consumer also referenced the meta-package. + +### Positive Consequences + +- A consumer never gets an unused DynamoDB SDK or Telnet listener dependency + by default. +- `getting-started.md`'s existing "meta-package + explicit provider + + explicit transport" install steps are now literally accurate instead of + needing a rewrite. + +### Negative Consequences + +- One more explicit `dotnet add package` line for the common case (Engine + + Hosting + Persistence + one provider + one transport is now 5 packages + instead of 3) — accepted as the direct tradeoff for "no unused + dependencies by default." + +## Pros and Cons of the Options + +### Option 1 — keep including everything + +- Good, because it's the fewest packages to `dotnet add` for the common + case. +- Bad, because it silently pulls in both a DynamoDB SDK dependency and a + Telnet listener for every consumer, regardless of which (if either) they + actually use. +- Bad, because it contradicts the "ala-carte flexibility" driver ADR-0006 + itself named, and made the getting-started docs describe a package + contract that wasn't real. + +### Option 2 — narrow to Engine + Hosting + Persistence (chosen) + +- Good, because it matches the docs' existing framing without a rewrite. +- Good, because it keeps the meta-package genuinely opt-in beyond the + provider-agnostic core. +- Bad, because the "smallest possible game" quick start needs one more + explicit package reference than before. + +## Links + +- [ADR-0006](0006-nuget-package-distribution.md) — this ADR narrows one + specific decision from ADR-0006 (the meta-package's package set); the + rest of ADR-0006 (granular packages, `SharpMud.Hosting`, the overall + distribution strategy) is unchanged and still in effect. +- [getting-started.md](https://github.com/LayeredCraft/sharp-mud/blob/main/docsite/docs/getting-started.md) — + the install guidance this decision keeps accurate. diff --git a/docs/adr/README.md b/docs/adr/README.md index 6aaf01c..b2c6dd7 100644 --- a/docs/adr/README.md +++ b/docs/adr/README.md @@ -66,3 +66,4 @@ the mechanics: numbering, status, and the index. | [0004](0004-session-state-machine-and-reconnect.md) | Session State Machine + Linkdead Reconnect | Accepted | | [0005](0005-security-role-model-and-moderation-commands.md) | Security Role Model + Moderation Commands | Accepted | | [0006](0006-nuget-package-distribution.md) | NuGet Package Distribution + Sample-Based Ruleset Extraction | Accepted | +| [0007](0007-narrow-meta-package-scope.md) | Narrow the `SharpMud` Meta-Package to Engine + Hosting + Persistence | Accepted | diff --git a/docs/deployment.md b/docs/deployment.md index 6782df1..53b7e37 100644 --- a/docs/deployment.md +++ b/docs/deployment.md @@ -36,7 +36,7 @@ debugging inside the container) without rebuilding. |---|---|---|---| | Mode | `--telnet` (first positional arg) | `SHARPMUD_MODE=telnet` | CLI (local single-player) | | Telnet port | second positional arg after `--telnet` | `SHARPMUD_TELNET_PORT` | `4000` | -| SQLite DB path | — | `SHARPMUD_DB_PATH` | `./sharpmud.db` (`/data/sharpmud.db` in the container image) | +| SQLite DB path | `--db-path ` | `SHARPMUD_DB_PATH` | `./sharpmud.db` (`/data/sharpmud.db` in the container image) | The Dockerfile sets `SHARPMUD_MODE=telnet`, `SHARPMUD_TELNET_PORT=4000`, and `SHARPMUD_DB_PATH=/data/sharpmud.db` as image defaults, plus `EXPOSE 4000` diff --git a/docs/persistence.md b/docs/persistence.md index 7fd836f..c413e68 100644 --- a/docs/persistence.md +++ b/docs/persistence.md @@ -145,9 +145,14 @@ publish) for this purpose, visible to `SharpMud.Persistence` via ## SQLite Path -`SHARPMUD_DB_PATH` env var, following the same precedence pattern as -`SharpMudHostOptions` (`SHARPMUD_MODE`/`SHARPMUD_TELNET_PORT`) — CLI arg, then -env var, then a default of `./sharpmud.db`. `SqliteStorageInitializer` +`--db-path ` CLI arg, then `SHARPMUD_DB_PATH` env var, then a default +of `./sharpmud.db` — same precedence pattern as the sample's transport +selection (`--telnet`/`SHARPMUD_MODE`/`SHARPMUD_TELNET_PORT`), resolved in +the sample's own `Program.cs`, not in `SharpMudHostOptions.Parse` itself +(which only ever takes the already-resolved env dict — see +[architecture.md](architecture.md)/[ADR-0006](adr/0006-nuget-package-distribution.md) +for why CLI-arg resolution stays a consumer concern, not `SharpMud.Hosting`'s). +`SqliteStorageInitializer` (`SharpMud.Persistence.Sqlite`, an `IStorageInitializer`) runs `EnsureCreatedAsync` before the world loader reads/writes anything, so the schema always exists first regardless of registration order across packages. Deploying the Docker diff --git a/docsite/docs/getting-started.md b/docsite/docs/getting-started.md index 536b7e4..7a26f59 100644 --- a/docsite/docs/getting-started.md +++ b/docsite/docs/getting-started.md @@ -22,10 +22,11 @@ dotnet add package SharpMud.Adapters.Cli --prerelease ``` `SharpMud` is a meta-package pulling in `SharpMud.Engine`, `SharpMud.Hosting`, -`SharpMud.Persistence`, and both adapter packages — you still pick a -persistence *provider* (`.Sqlite` or `.DynamoDb`) and a transport -(`SharpMud.Adapters.Cli` and/or `SharpMud.Adapters.Telnet`) explicitly. -`--prerelease` is required until a stable 1.0 release ships. +and `SharpMud.Persistence` — you always add a persistence *provider* +(`.Sqlite` or `.DynamoDb`) and a transport (`SharpMud.Adapters.Cli` and/or +`SharpMud.Adapters.Telnet`) explicitly; the meta-package doesn't include +either (see [ADR-0007](https://github.com/LayeredCraft/sharp-mud/blob/main/docs/adr/0007-narrow-meta-package-scope.md) +for why). `--prerelease` is required until a stable 1.0 release ships. ## Build the world diff --git a/samples/SharpMud.Samples.Classic/Program.cs b/samples/SharpMud.Samples.Classic/Program.cs index 9852325..3798612 100644 --- a/samples/SharpMud.Samples.Classic/Program.cs +++ b/samples/SharpMud.Samples.Classic/Program.cs @@ -33,9 +33,17 @@ builder.Logging.ClearProviders(); builder.Logging.AddSerilog(serilogLogger, dispose: true); +// --db-path wins over SHARPMUD_DB_PATH, same CLI-arg-over-env-var +// precedence as --telnet/SHARPMUD_MODE below - handled here rather than in +// SharpMudHostOptions.Parse itself, matching how transport CLI args are +// also the sample's own composition-root decision, not Hosting's (docs/adr +// /0006-nuget-package-distribution.md). Not a secret (see security.md) - +// just a filesystem path - so a CLI arg carries no exposure risk env-only +// deployment config (credentials, connection strings) would have. +var dbPathArg = args is ["--db-path", var dbPathValue, ..] ? dbPathValue : null; var env = new Dictionary { - ["SHARPMUD_DB_PATH"] = Environment.GetEnvironmentVariable("SHARPMUD_DB_PATH"), + ["SHARPMUD_DB_PATH"] = dbPathArg ?? Environment.GetEnvironmentVariable("SHARPMUD_DB_PATH"), }; var hostOptions = SharpMudHostOptions.Parse(env); diff --git a/src/SharpMud.Adapters.Cli/SharpMud.Adapters.Cli.csproj b/src/SharpMud.Adapters.Cli/SharpMud.Adapters.Cli.csproj index 411c038..733fbd1 100644 --- a/src/SharpMud.Adapters.Cli/SharpMud.Adapters.Cli.csproj +++ b/src/SharpMud.Adapters.Cli/SharpMud.Adapters.Cli.csproj @@ -20,6 +20,7 @@ net10.0;net11.0 enable enable + Local stdin/stdout ISession transport for SharpMud - AddSharpMudCliTransport(), login-free single-player. diff --git a/src/SharpMud.Adapters.Telnet/SharpMud.Adapters.Telnet.csproj b/src/SharpMud.Adapters.Telnet/SharpMud.Adapters.Telnet.csproj index 7321fbb..9e9d5d3 100644 --- a/src/SharpMud.Adapters.Telnet/SharpMud.Adapters.Telnet.csproj +++ b/src/SharpMud.Adapters.Telnet/SharpMud.Adapters.Telnet.csproj @@ -25,6 +25,7 @@ net10.0;net11.0 enable enable + Raw TCP Telnet ISession transport for SharpMud - AddSharpMudTelnetTransport(port), real IAC option negotiation (Echo/NAWS), and the username/password login flow. diff --git a/src/SharpMud.Engine/SharpMud.Engine.csproj b/src/SharpMud.Engine/SharpMud.Engine.csproj index 58c8686..e42b84d 100644 --- a/src/SharpMud.Engine/SharpMud.Engine.csproj +++ b/src/SharpMud.Engine/SharpMud.Engine.csproj @@ -4,6 +4,7 @@ net10.0;net11.0 enable enable + Ruleset-agnostic MUD engine core: Thing/Behavior composition, the event system, the command pipeline, and the global tick loop. diff --git a/src/SharpMud.Hosting/ServiceCollectionExtensions.cs b/src/SharpMud.Hosting/ServiceCollectionExtensions.cs index af1c2aa..f6ceee9 100644 --- a/src/SharpMud.Hosting/ServiceCollectionExtensions.cs +++ b/src/SharpMud.Hosting/ServiceCollectionExtensions.cs @@ -53,10 +53,21 @@ public static IServiceCollection AddSharpMudPlayerFactory(this I // Called once from SharpMudApplicationBuilder's constructor - see // docs/adr/0006-nuget-package-distribution.md's SharpMud.Hosting shape. - // Registration order matters: WorldLoaderHostedService must run before - // GameLoopHostedService/any transport BackgroundService, since the - // generic host awaits each hosted service's StartAsync in registration - // order before starting the next. + // Registration order matters on both ends of the host lifecycle: + // - Start: WorldLoaderHostedService must run before GameLoopHostedService/ + // any transport BackgroundService, since the generic host awaits each + // hosted service's StartAsync in registration order before starting + // the next. + // - Stop: the generic host stops hosted services in REVERSE registration + // order, so GameLoopHostedService is registered after + // ShutdownSaveHostedService here - GameLoop's tick loop fully quiesces + // (its BackgroundService.StopAsync cancels and awaits ExecuteAsync) + // before ShutdownSaveHostedService's StopAsync takes its snapshot, + // instead of racing a still-running tick that could still be mutating + // the world mid-save. A consumer's transport BackgroundService is + // registered later still (after this method returns), so it stops + // before either of these - no new connections/input once shutdown + // starts. internal static IServiceCollection AddSharpMudHostingCore(this IServiceCollection services) { services.AddSingleton(); @@ -83,8 +94,8 @@ internal static IServiceCollection AddSharpMudHostingCore(this IServiceCollectio services.AddSingleton(); services.AddHostedService(); - services.AddHostedService(); services.AddHostedService(); + services.AddHostedService(); return services; } diff --git a/src/SharpMud.Hosting/SharpMud.Hosting.csproj b/src/SharpMud.Hosting/SharpMud.Hosting.csproj index e50d5d3..8a20b4e 100644 --- a/src/SharpMud.Hosting/SharpMud.Hosting.csproj +++ b/src/SharpMud.Hosting/SharpMud.Hosting.csproj @@ -28,6 +28,7 @@ net10.0;net11.0 enable enable + .NET generic host composition helpers for SharpMud: WorldContext, IWorldBuilder, IPlayerFactory, the login/session flow, and the AddSharpMud* DI extensions. diff --git a/src/SharpMud.Persistence.DynamoDb/SharpMud.Persistence.DynamoDb.csproj b/src/SharpMud.Persistence.DynamoDb/SharpMud.Persistence.DynamoDb.csproj index c2d7725..7c45958 100644 --- a/src/SharpMud.Persistence.DynamoDb/SharpMud.Persistence.DynamoDb.csproj +++ b/src/SharpMud.Persistence.DynamoDb/SharpMud.Persistence.DynamoDb.csproj @@ -20,6 +20,7 @@ net10.0 enable enable + DynamoDB provider for SharpMud.Persistence - AddSharpMudDynamoDbPersistence(...) via the EntityFrameworkCore.DynamoDb provider. diff --git a/src/SharpMud.Persistence.Sqlite/SharpMud.Persistence.Sqlite.csproj b/src/SharpMud.Persistence.Sqlite/SharpMud.Persistence.Sqlite.csproj index d38910b..44f0bf4 100644 --- a/src/SharpMud.Persistence.Sqlite/SharpMud.Persistence.Sqlite.csproj +++ b/src/SharpMud.Persistence.Sqlite/SharpMud.Persistence.Sqlite.csproj @@ -20,6 +20,7 @@ net10.0;net11.0 enable enable + SQLite provider for SharpMud.Persistence - AddSharpMudSqlitePersistence(path) and schema-creation storage initialization. diff --git a/src/SharpMud.Persistence/SharpMud.Persistence.csproj b/src/SharpMud.Persistence/SharpMud.Persistence.csproj index 9739512..d1daed0 100644 --- a/src/SharpMud.Persistence/SharpMud.Persistence.csproj +++ b/src/SharpMud.Persistence/SharpMud.Persistence.csproj @@ -16,6 +16,7 @@ net10.0;net11.0 enable enable + Provider-agnostic EF Core repository (IThingRepository/GameDbContext) for SharpMud. Pair with SharpMud.Persistence.Sqlite or .DynamoDb for an actual storage provider. diff --git a/src/SharpMud/SharpMud.csproj b/src/SharpMud/SharpMud.csproj index e537319..941afb0 100644 --- a/src/SharpMud/SharpMud.csproj +++ b/src/SharpMud/SharpMud.csproj @@ -1,21 +1,22 @@  + ProjectReferences to the engine-level packages every consumer needs + (Engine, Hosting, provider-agnostic Persistence), which dotnet pack + automatically translates into entries in the resulting + .nuspec at each referenced project's own version. Deliberately does + NOT include persistence providers or transport adapters - a consumer + always adds one of each explicitly, per ADR-0007 (which narrowed + this from ADR-0006's original "everything" scope once that was found + to make every consumer pull in an unused DynamoDB SDK dependency and + Telnet listener by default). Not PackageReference - none of the + sibling packages exist on any feed until after this same + solution-wide pack/publish, so a literal PackageReference would fail + restore. --> - - - - @@ -26,7 +27,7 @@ warnings/build noise a normal library project would emit. --> false $(NoWarn);NU5128 - Meta-package for sharp-mud - pulls in every SharpMud.* package (Engine, Hosting, Persistence, Adapters). See https://github.com/LayeredCraft/sharp-mud for getting started. + Meta-package for sharp-mud - pulls in SharpMud.Engine, SharpMud.Hosting, and SharpMud.Persistence. Add a persistence provider (SharpMud.Persistence.Sqlite or .DynamoDb) and a transport (SharpMud.Adapters.Cli and/or .Telnet) explicitly. See https://github.com/LayeredCraft/sharp-mud for getting started. diff --git a/tests/SharpMud.Adapters.Telnet.Tests/TelnetListenerTests.cs b/tests/SharpMud.Adapters.Telnet.Tests/TelnetListenerTests.cs index 0c0a2b4..6759fc2 100644 --- a/tests/SharpMud.Adapters.Telnet.Tests/TelnetListenerTests.cs +++ b/tests/SharpMud.Adapters.Telnet.Tests/TelnetListenerTests.cs @@ -22,6 +22,7 @@ public async Task AcceptSessionsAsync_StopsPromptly_WhenCancelledWithNoConnectio // cts.Token IS derived from TestContext.Current.CancellationToken // (linked above) plus a CancelAfter - xUnit1051 doesn't trace that // and flags this line as a false positive. +#pragma warning disable xUnit1051 var enumerationTask = Task.Run(async () => { await foreach (var _ in listener.AcceptSessionsAsync(cts.Token)) @@ -29,6 +30,7 @@ public async Task AcceptSessionsAsync_StopsPromptly_WhenCancelledWithNoConnectio // No connections expected - loop should simply end. } }); +#pragma warning restore xUnit1051 var completed = await Task.WhenAny(enumerationTask, Task.Delay(TimeSpan.FromSeconds(5), TestContext.Current.CancellationToken)); diff --git a/tests/SharpMud.Hosting.Tests/ServiceCollectionExtensionsTests.cs b/tests/SharpMud.Hosting.Tests/ServiceCollectionExtensionsTests.cs new file mode 100644 index 0000000..830daa1 --- /dev/null +++ b/tests/SharpMud.Hosting.Tests/ServiceCollectionExtensionsTests.cs @@ -0,0 +1,31 @@ +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using SharpMud.Hosting; + +namespace SharpMud.Hosting.Tests; + +public sealed class ServiceCollectionExtensionsTests +{ + [Fact] + public void AddSharpMudHostingCore_RegistersGameLoopAfterShutdownSave_SoGameLoopStopsFirst() + { + // The generic host stops IHostedServices in reverse registration + // order - GameLoopHostedService must be registered after + // ShutdownSaveHostedService so its tick loop fully quiesces before + // the shutdown snapshot is taken, not concurrently with it. + var services = new ServiceCollection(); + + services.AddSharpMudHostingCore(); + + var hostedServiceOrder = services + .Where(d => d.ServiceType == typeof(IHostedService)) + .Select(d => d.ImplementationType) + .ToList(); + + var shutdownSaveIndex = hostedServiceOrder.IndexOf(typeof(ShutdownSaveHostedService)); + var gameLoopIndex = hostedServiceOrder.IndexOf(typeof(GameLoopHostedService)); + + shutdownSaveIndex.Should().BeGreaterThanOrEqualTo(0); + gameLoopIndex.Should().BeGreaterThan(shutdownSaveIndex); + } +} From c5a10bd0903b0a704c6044a2345e21d65af0c732 Mon Sep 17 00:00:00 2001 From: Nick Cipollina Date: Mon, 20 Jul 2026 15:14:33 -0400 Subject: [PATCH 09/14] fix: address second round of PR #9 review feedback - combined CLI args, stale docs, IStorageInitializer dependency direction - Fix --telnet/--db-path CLI arg parsing: both were positional patterns requiring their own flag to be args[0], so combining them silently dropped whichever came second. Switched both to Array.IndexOf-based lookups so they work in any order/combination. Verified live with both orderings over real Telnet. - Move IStorageInitializer from SharpMud.Hosting to SharpMud.Engine.Core, alongside IThingRepository - it's a pure persistence-lifecycle abstraction with nothing Hosting-specific about it. This removes SharpMud.Persistence.Sqlite's reference to SharpMud.Hosting entirely, making the "Persistence.* -> Persistence -> Engine" dependency-direction rule in docs/architecture.md actually true instead of a documented exception. - Update docsite/docs/index.md's meta-package description (still said "all of the above", stale since ADR-0007 narrowed it last round). - Update docs/plans/0006-nuget-package-distribution.md's Open Questions section - 4 entries were still listed as unresolved (DynamoDB TPH support, IWorldBuilder registration shape, GameLoop hosting shape, docsite/ naming) despite all being decided/implemented. - Update docs/networking.md - still described Telnet's old "Name:"-prompt placeholder instead of the real LoginFlow username/password flow that's been live since earlier in this PR. Co-Authored-By: Claude Sonnet 5 --- docs/networking.md | 9 ++-- docs/plans/0006-nuget-package-distribution.md | 46 ++++++++++--------- docsite/docs/index.md | 5 +- samples/SharpMud.Samples.Classic/Program.cs | 14 ++++-- .../Core/IStorageInitializer.cs | 23 ++++++++++ src/SharpMud.Hosting/IStorageInitializer.cs | 17 ------- .../ServiceCollectionExtensions.cs | 1 - .../SharpMud.Persistence.Sqlite.csproj | 2 - .../SqliteStorageInitializer.cs | 2 +- 9 files changed, 68 insertions(+), 51 deletions(-) create mode 100644 src/SharpMud.Engine/Core/IStorageInitializer.cs delete mode 100644 src/SharpMud.Hosting/IStorageInitializer.cs diff --git a/docs/networking.md b/docs/networking.md index 8ea8b96..846b2e4 100644 --- a/docs/networking.md +++ b/docs/networking.md @@ -67,10 +67,11 @@ Each connection is wrapped in a try/catch so one bad session can't take down the listener or other connections (same exception-isolation principle as command execution, see [architecture.md](architecture.md)). -New Telnet connections are prompted for a name (`"Name: "`) before a player -`Thing` is created — this is a placeholder for real login (see -[accounts-auth.md](accounts-auth.md)'s username/password login prompt), -not auth. +New Telnet connections go through `LoginFlow` (`SharpMud.Hosting`) — real +username/password prompts, DB-backed lookup/creation via +`IThingRepository.FindPlayerByUsernameAsync`, not a bare name prompt. See +[accounts-auth.md](accounts-auth.md) for the full implemented flow (this is +no longer a placeholder). ## Sequence: Player Disconnects Mid-Fight diff --git a/docs/plans/0006-nuget-package-distribution.md b/docs/plans/0006-nuget-package-distribution.md index 3f5c339..ce7589d 100644 --- a/docs/plans/0006-nuget-package-distribution.md +++ b/docs/plans/0006-nuget-package-distribution.md @@ -661,27 +661,29 @@ Modified: ## Open questions / blockers -- Whether `EntityFrameworkCore.DynamoDb` supports EF Core's TPH - `HasDiscriminator(...)` (used by `BehaviorConfiguration` for the - `Behavior` subtype hierarchy) is unconfirmed — flagged in PR review. - `ToTable(...)` and single-property `HasKey(...)` are confirmed supported - per the provider's own docs, but the discriminator/inheritance question - is open. Resolve via the explicit verification task in the - `SharpMud.Persistence` split section above before assuming the shared - `Configurations/` tree works unmodified against DynamoDB. -- Exact shape of the world-builder registration point on the builder (how - a consumer plugs in their own `HubWorldBuilder`-equivalent) isn't fully - designed — implementation will need to work this out concretely, it's - sketched but not nailed down in ADR-0006. -- Whether `GameLoop` itself should take a direct `Microsoft.Extensions.Hosting` - dependency (become a `BackgroundService` itself) or stay hosting-agnostic - with a thin wrapper in `SharpMud.Hosting` — implementation-time call, not - pre-decided. -- Exact name for the docs-site source directory (`docsite/` used as a - placeholder throughout this plan) isn't locked in. +All resolved during implementation: + +- ~~Whether `EntityFrameworkCore.DynamoDb` supports EF Core's TPH + `HasDiscriminator(...)`~~ — **resolved: yes.** Confirmed directly + against the provider's own modeling docs + (`docs/modeling/single-table-design.md` in + `LayeredCraft/dynamodb-efcore-provider`), not just inferred — TPH + discriminators are supported, so the shared `Configurations/` tree works + unmodified against DynamoDB. +- ~~Exact shape of the world-builder registration point~~ — **resolved: + implemented as `IWorldBuilder`/`IPlayerFactory` (`SharpMud.Hosting`), + populating `WorldContext` via `WorldLoaderHostedService`.** See + `docs/engine-vs-ruleset.md`. +- ~~Whether `GameLoop` should take a direct `Microsoft.Extensions.Hosting` + dependency~~ — **resolved: stays hosting-agnostic.** + `GameLoopHostedService` (`SharpMud.Hosting`) wraps it as a thin + `BackgroundService`; `SharpMud.Engine` never references + `Microsoft.Extensions.Hosting`. +- ~~Exact name for the docs-site source directory~~ — **resolved: + `docsite/`,** as used throughout this plan. - Full docs-site content (per-package configuration reference, a data-modeling-equivalent guide, samples walkthroughs beyond the one - Getting Started page) is real, separate writing work — not scheduled - against this plan, needs its own follow-up plan once the package - mechanics this plan covers are actually done and stable enough to - document accurately. + Getting Started page) is real, separate writing work — still not + scheduled against this plan, needs its own follow-up plan once the + package mechanics this plan covers have had time to prove out in + practice. diff --git a/docsite/docs/index.md b/docsite/docs/index.md index 869178d..65b8703 100644 --- a/docsite/docs/index.md +++ b/docsite/docs/index.md @@ -23,7 +23,10 @@ sharp-mud ships as a set of NuGet packages you compose into your own game: repositories behind a provider-agnostic `IThingRepository`. - **`SharpMud.Adapters.Cli`** / **`SharpMud.Adapters.Telnet`** — transport implementations of `ISession`. -- **`SharpMud`** — a meta-package pulling in all of the above. +- **`SharpMud`** — a meta-package pulling in `SharpMud.Engine`, + `SharpMud.Hosting`, and `SharpMud.Persistence`. You always add a + persistence provider and a transport explicitly — the meta-package + doesn't include either (see [ADR-0007](https://github.com/LayeredCraft/sharp-mud/blob/main/docs/adr/0007-narrow-meta-package-scope.md)). Your own game — stats, combat rules, world content, commands — is a separate project that references these packages the same way `SharpMud.Samples.Classic` diff --git a/samples/SharpMud.Samples.Classic/Program.cs b/samples/SharpMud.Samples.Classic/Program.cs index 3798612..d323835 100644 --- a/samples/SharpMud.Samples.Classic/Program.cs +++ b/samples/SharpMud.Samples.Classic/Program.cs @@ -40,7 +40,12 @@ // /0006-nuget-package-distribution.md). Not a secret (see security.md) - // just a filesystem path - so a CLI arg carries no exposure risk env-only // deployment config (credentials, connection strings) would have. -var dbPathArg = args is ["--db-path", var dbPathValue, ..] ? dbPathValue : null; +// Looked up by index, not a positional pattern match on args[0] - both +// --db-path and --telnet need to work regardless of which comes first or +// whether both are present at once (e.g. `--telnet 4000 --db-path +// /data/x.db`), which a leading-args-element pattern can't express. +var dbPathIndex = Array.IndexOf(args, "--db-path"); +var dbPathArg = dbPathIndex >= 0 && dbPathIndex + 1 < args.Length ? args[dbPathIndex + 1] : null; var env = new Dictionary { ["SHARPMUD_DB_PATH"] = dbPathArg ?? Environment.GetEnvironmentVariable("SHARPMUD_DB_PATH"), @@ -65,13 +70,16 @@ // precedence as before (CLI arg wins over env var) - this is now the // sample's own composition-root decision, since SharpMud.Hosting must not // know which transport(s) exist (docs/adr/0006-nuget-package-distribution.md). -var useTelnet = args is ["--telnet", ..] +// Looked up by index (see --db-path above), not args[0] - --telnet must +// still be recognized when it's not the very first arg. +var telnetIndex = Array.IndexOf(args, "--telnet"); +var useTelnet = telnetIndex >= 0 || string.Equals(Environment.GetEnvironmentVariable("SHARPMUD_MODE"), "telnet", StringComparison.OrdinalIgnoreCase); if (useTelnet) { var port = 4000; - if (args is ["--telnet", var portArg, ..] && int.TryParse(portArg, out var parsedArg)) + if (telnetIndex >= 0 && telnetIndex + 1 < args.Length && int.TryParse(args[telnetIndex + 1], out var parsedArg)) port = parsedArg; else if (int.TryParse(Environment.GetEnvironmentVariable("SHARPMUD_TELNET_PORT"), out var parsedEnv)) port = parsedEnv; diff --git a/src/SharpMud.Engine/Core/IStorageInitializer.cs b/src/SharpMud.Engine/Core/IStorageInitializer.cs new file mode 100644 index 0000000..bfd631d --- /dev/null +++ b/src/SharpMud.Engine/Core/IStorageInitializer.cs @@ -0,0 +1,23 @@ +namespace SharpMud.Engine.Core; + +/// +/// One-time storage setup (e.g. EF Core's EnsureCreatedAsync) a +/// persistence package needs before the world can be loaded. Registered via +/// DI (services.AddSingleton<IStorageInitializer, ...>()) +/// rather than relying on hosted-service registration order between +/// packages, which would be fragile - the consumer always runs every +/// registered initializer before attempting to load anything, regardless of +/// which order the persistence/hosting packages happened to register in. +/// Lives alongside in +/// SharpMud.Engine.Core rather than SharpMud.Hosting - both +/// are persistence-lifecycle abstractions a Persistence.* provider +/// package implements, and keeping this one in Engine means providers only +/// ever need their existing Persistence -> Engine reference, not +/// an extra reference to Hosting (see docs/architecture.md's +/// dependency-direction rule). +/// +public interface IStorageInitializer +{ + /// Runs any one-time setup this provider needs before the world is loaded. + Task InitializeAsync(CancellationToken ct); +} diff --git a/src/SharpMud.Hosting/IStorageInitializer.cs b/src/SharpMud.Hosting/IStorageInitializer.cs deleted file mode 100644 index 54be945..0000000 --- a/src/SharpMud.Hosting/IStorageInitializer.cs +++ /dev/null @@ -1,17 +0,0 @@ -namespace SharpMud.Hosting; - -/// -/// One-time storage setup (e.g. EF Core's EnsureCreatedAsync) a -/// persistence package needs before -/// can query it. Registered via DI -/// (services.AddSingleton<IStorageInitializer, ...>()) rather -/// than relying on hosted-service registration order between packages, -/// which would be fragile - always -/// runs every registered initializer before attempting to load anything, -/// regardless of which order the persistence/hosting packages happened to -/// register in. -/// -public interface IStorageInitializer -{ - Task InitializeAsync(CancellationToken ct); -} diff --git a/src/SharpMud.Persistence.Sqlite/ServiceCollectionExtensions.cs b/src/SharpMud.Persistence.Sqlite/ServiceCollectionExtensions.cs index 981aafd..75809c9 100644 --- a/src/SharpMud.Persistence.Sqlite/ServiceCollectionExtensions.cs +++ b/src/SharpMud.Persistence.Sqlite/ServiceCollectionExtensions.cs @@ -1,7 +1,6 @@ using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.DependencyInjection; using SharpMud.Engine.Core; -using SharpMud.Hosting; using SharpMud.Persistence; namespace SharpMud.Persistence.Sqlite; diff --git a/src/SharpMud.Persistence.Sqlite/SharpMud.Persistence.Sqlite.csproj b/src/SharpMud.Persistence.Sqlite/SharpMud.Persistence.Sqlite.csproj index 44f0bf4..3c47010 100644 --- a/src/SharpMud.Persistence.Sqlite/SharpMud.Persistence.Sqlite.csproj +++ b/src/SharpMud.Persistence.Sqlite/SharpMud.Persistence.Sqlite.csproj @@ -3,8 +3,6 @@ - - diff --git a/src/SharpMud.Persistence.Sqlite/SqliteStorageInitializer.cs b/src/SharpMud.Persistence.Sqlite/SqliteStorageInitializer.cs index f3fd9e1..52910fa 100644 --- a/src/SharpMud.Persistence.Sqlite/SqliteStorageInitializer.cs +++ b/src/SharpMud.Persistence.Sqlite/SqliteStorageInitializer.cs @@ -1,5 +1,5 @@ using Microsoft.EntityFrameworkCore; -using SharpMud.Hosting; +using SharpMud.Engine.Core; using SharpMud.Persistence; namespace SharpMud.Persistence.Sqlite; From 91e95027b261447de59d1132e32ca31bc41beffa Mon Sep 17 00:00:00 2001 From: Nick Cipollina Date: Mon, 20 Jul 2026 19:51:31 -0400 Subject: [PATCH 10/14] docs: fix remaining stale meta-package descriptions in README.md and architecture.md Same class of gap as last round's docsite/docs/index.md fix - both Solution layout ASCII trees still said the meta-package "pulls in every SharpMud.* package", stale since ADR-0007 narrowed it to Engine+Hosting+ Persistence. Grepped the whole repo for the same phrasing to make sure these were the last two. Co-Authored-By: Claude Sonnet 5 --- README.md | 2 +- docs/architecture.md | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 7b8c4ab..18c73a3 100644 --- a/README.md +++ b/README.md @@ -29,7 +29,7 @@ SharpMud.slnx SharpMud.Persistence.DynamoDb/ # DynamoDB provider SharpMud.Adapters.Cli/ # local stdin/stdout session adapter SharpMud.Adapters.Telnet/ # raw TCP session adapter + listener - SharpMud/ # meta-package (pulls in every SharpMud.* package) + SharpMud/ # meta-package (Engine + Hosting + Persistence only - ADR-0007) samples/ SharpMud.Samples.Classic/ # D&D-flavored sample ruleset + composition root tests/ diff --git a/docs/architecture.md b/docs/architecture.md index 2e6096b..30d1f8d 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -27,7 +27,8 @@ SharpMud.slnx SharpMud.Adapters.Telnet/ # raw TCP ISession + listener - see networking.md. SharpMud.Adapters.Ssh/ # (later) SharpMud.Adapters.WebSocket/ # (later) - SharpMud/ # meta-package: pulls in every SharpMud.* package. + SharpMud/ # meta-package: Engine + Hosting + Persistence only - + # provider/transport packages always explicit (ADR-0007). samples/ SharpMud.Samples.Classic/ # D&D-flavored sample ruleset + composition root # (Program.cs). References everything; the only From 80779bc6d39874d0345c27140c50167171b4aa8e Mon Sep 17 00:00:00 2001 From: Nick Cipollina Date: Mon, 20 Jul 2026 19:58:14 -0400 Subject: [PATCH 11/14] chore: bump NSubstitute back to 6.0.0, suppress NU1608 narrowly instead of pinning below it AutoFixture.AutoNSubstitute 4.18.1 (latest stable, Nov 2023) caps NSubstitute below 6.0.0 - NuGet correctly flags NU1608 since we're outside that stated range, but the integration itself has been validated against 6.0.0 by this repo's own test suite (108/108). Suppress the warning directly rather than staying pinned to 5.3.0 - documented in Directory.Build.props with the reasoning and a removal condition (once AutoFixture publishes an updated dependency range). Also fixes the two real nullable warnings NSubstitute 6.0's newly-annotated CallInfo indexer surfaces in SessionLoopTests.cs - switched the manual (Thing)x[0] cast to x.ArgAt(0), NSubstitute's own null-safe typed accessor, rather than suppressing those too. The NU1608 suppression had to go in Directory.Build.props's unconditional top-level PropertyGroup, not the existing IsTestProject-gated one - NU1608 is evaluated during restore, before IsTestProject (set by an early-imported test-package .props file) is available, the same restore-vs-project-body ordering gap already noted for IsPackable. Co-Authored-By: Claude Sonnet 5 --- Directory.Build.props | 13 +++++++++++++ Directory.Packages.props | 14 +++++++++----- tests/SharpMud.Hosting.Tests/SessionLoopTests.cs | 2 +- 3 files changed, 23 insertions(+), 6 deletions(-) diff --git a/Directory.Build.props b/Directory.Build.props index cded8e0..5967d5d 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -26,6 +26,19 @@ doc comments per documentation.md - this only suppresses the warning, it doesn't relax the convention. --> $(NoWarn);CS1591 + + $(NoWarn);NU1608 diff --git a/Directory.Packages.props b/Directory.Packages.props index 614be1c..06f1efc 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -75,11 +75,15 @@ - - + + diff --git a/tests/SharpMud.Hosting.Tests/SessionLoopTests.cs b/tests/SharpMud.Hosting.Tests/SessionLoopTests.cs index 143168e..ee770df 100644 --- a/tests/SharpMud.Hosting.Tests/SessionLoopTests.cs +++ b/tests/SharpMud.Hosting.Tests/SessionLoopTests.cs @@ -126,7 +126,7 @@ public async Task RunAsync_SavesWithParentStillSet_WhenPlayerQuits() var repository = Substitute.For(); repository.SaveTreeAsync(Arg.Any(), Arg.Any()) .Returns(Task.CompletedTask) - .AndDoes(x => parentAtSaveTime = ((Thing)x[0]).Parent); + .AndDoes(x => parentAtSaveTime = x.ArgAt(0).Parent); var sessionLoop = new SessionLoop(worldContext, parser, registry, repository); await sessionLoop.RunAsync(session, player, TestContext.Current.CancellationToken); From b71f2afaf8cf715685c45ace4923a6d063c791f5 Mon Sep 17 00:00:00 2001 From: Nick Cipollina Date: Mon, 20 Jul 2026 20:02:17 -0400 Subject: [PATCH 12/14] docs: fix remaining stale references across deployment.md, SPEC.md, PLAN-0006, and architecture.md - deployment.md: config table still described --telnet/port as positional args, stale since the order-independent Array.IndexOf fix last round. - SPEC.md: roadmap still named SharpMud.Ruleset.Classic, deleted in this PR (renamed to SharpMud.Samples.Classic) - left alone earlier as "historical roadmap text", but unlike an ADR, SPEC.md isn't an immutable record, and this pointed at a project name that no longer exists at all. - PLAN-0006: meta-package task entry still described the pre-ADR-0007 "every SharpMud.* project" scope. - architecture.md: solution layout was missing SharpMud.Adapters.Cli.Tests/.Telnet.Tests; dependency-direction rule implied Adapters.*/Persistence.* only reach Engine transitively through Hosting/Persistence, but both directly reference Engine too. Co-Authored-By: Claude Sonnet 5 --- SPEC.md | 2 +- docs/architecture.md | 8 ++++++-- docs/deployment.md | 4 ++-- docs/plans/0006-nuget-package-distribution.md | 16 +++++++++++----- 4 files changed, 20 insertions(+), 10 deletions(-) diff --git a/SPEC.md b/SPEC.md index 27a1c31..68d3720 100644 --- a/SPEC.md +++ b/SPEC.md @@ -139,7 +139,7 @@ derivatives separate "instant" commands from round-based combat resolution. 3. **Inventory & items**: pick up/drop/wear/wield, carry weight or slots. 4. **Engine/ruleset split** (retrofit, done now rather than deferred): convert the entity model built in phases 1–3 to `Thing`/`Behavior` composition and - extract the D&D-specific stat/combat rules into `SharpMud.Ruleset.Classic`, + extract the D&D-specific stat/combat rules into `SharpMud.Samples.Classic`, per [docs/engine-vs-ruleset.md](docs/engine-vs-ruleset.md). Doing this before NPCs/networking/accounts land means those phases are built against the real engine boundary instead of needing their own retrofit later. diff --git a/docs/architecture.md b/docs/architecture.md index 30d1f8d..6b2e9f2 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -38,12 +38,16 @@ SharpMud.slnx SharpMud.Engine.Tests/ # xUnit v3 + AutoFixture + NSubstitute + AwesomeAssertions SharpMud.Hosting.Tests/ SharpMud.Persistence.Tests/ + SharpMud.Adapters.Cli.Tests/ + SharpMud.Adapters.Telnet.Tests/ SharpMud.Samples.Classic.Tests/ ``` **Dependency direction (strict, enforced by project references):** -`Adapters.* → Hosting → Engine`, `Persistence.* → Persistence → Engine`, -`Samples.Classic → everything`. Engine never references Adapters, Persistence, +`Adapters.* → Hosting + Engine` (both are direct references, not purely +transitive through Hosting), `Persistence.* → Persistence + Engine` (same — +direct references to both, not just Persistence), `Samples.Classic → +everything`. Engine never references Adapters, Persistence, or any ruleset — see [engine-vs-ruleset.md](engine-vs-ruleset.md) for the full rationale (this is the actual mechanism behind the "engine, not just a game" goal in `SPEC.md`, not just the transport/persistence swappability described diff --git a/docs/deployment.md b/docs/deployment.md index 53b7e37..3e22ac0 100644 --- a/docs/deployment.md +++ b/docs/deployment.md @@ -34,8 +34,8 @@ debugging inside the container) without rebuilding. | Setting | CLI arg | Env var | Default | |---|---|---|---| -| Mode | `--telnet` (first positional arg) | `SHARPMUD_MODE=telnet` | CLI (local single-player) | -| Telnet port | second positional arg after `--telnet` | `SHARPMUD_TELNET_PORT` | `4000` | +| Mode | `--telnet` (anywhere in args) | `SHARPMUD_MODE=telnet` | CLI (local single-player) | +| Telnet port | `--telnet ` (looked up by index, not positional - can combine with `--db-path` in either order) | `SHARPMUD_TELNET_PORT` | `4000` | | SQLite DB path | `--db-path ` | `SHARPMUD_DB_PATH` | `./sharpmud.db` (`/data/sharpmud.db` in the container image) | The Dockerfile sets `SHARPMUD_MODE=telnet`, `SHARPMUD_TELNET_PORT=4000`, and diff --git a/docs/plans/0006-nuget-package-distribution.md b/docs/plans/0006-nuget-package-distribution.md index ce7589d..27a9fe5 100644 --- a/docs/plans/0006-nuget-package-distribution.md +++ b/docs/plans/0006-nuget-package-distribution.md @@ -452,11 +452,17 @@ bundled into this plan's "done"). above; confirm `samples/` projects are `IsPackable=false` explicitly so a solution-wide `dotnet pack` never emits sample packages - [x] New `src/SharpMud/SharpMud.csproj` — the meta-package: no code, - **`ProjectReference`s (not `PackageReference`s) to every other - `SharpMud.*` project** — verified experimentally: `dotnet pack` - automatically translates a `ProjectReference` to a packable project - into a `` entry in the resulting `.nuspec`, at that - project's own version, with no requirement that the referenced + **`ProjectReference`s (not `PackageReference`s) to `SharpMud.Engine`, + `SharpMud.Hosting`, and `SharpMud.Persistence`** — narrowed from an + initial "every other `SharpMud.*` project" scope to just these three + per [ADR-0007](../adr/0007-narrow-meta-package-scope.md), caught in + later PR review: the original all-inclusive scope silently pulled in + an unused DynamoDB SDK dependency and Telnet listener for every + consumer. `ProjectReference`, not `PackageReference`, still applies + for the same reason either way — verified experimentally: `dotnet + pack` automatically translates a `ProjectReference` to a packable + project into a `` entry in the resulting `.nuspec`, at + that project's own version, with no requirement that the referenced package already exist on any feed. A literal `PackageReference` instead would fail restore on the very first `dotnet pack SharpMud.slnx` — none of the sibling packages exist on any feed From 08be36bb982a88b9cf1cc6dd78b4a0577d840c09 Mon Sep 17 00:00:00 2001 From: Nick Cipollina Date: Mon, 20 Jul 2026 20:26:48 -0400 Subject: [PATCH 13/14] fix: scope NU1608 suppression to tests/, drop stale DynamoDb rationale, rename test project off the deleted Ruleset.Classic namespace - Move the NU1608 (NSubstitute/AutoFixture.AutoNSubstitute) suppression out of the repo-root Directory.Build.props into a new tests/Directory.Build.props, per review: the repo-wide suppression would have hidden future package-range conflicts in src/ packages too. Directory-based discovery scopes this to tests/ without needing an IsTestProject condition at all - sidesteps the restore-vs-project-body evaluation-order gap that made the previous IsTestProject-gated attempt not actually apply at restore time. Verified empirically both ways before landing on this. - Drop SharpMud.Persistence.DynamoDb's stale "no reference to SharpMud.Hosting" rationale for not registering IStorageInitializer - stale since that interface moved to SharpMud.Engine last round, which this project already references. The real reason (doesn't create/migrate tables) stands alone. - Rename tests/SharpMud.Samples.Classic.Tests off the deleted SharpMud.Ruleset.Classic.Tests namespace (RootNamespace, global usings, and all 4 file namespace declarations) - a leftover from the original project move that never got renamed. Required adding explicit `using SharpMud.Ruleset.Classic;` to CombatManagerTests.cs/CombatResolverTests.cs, since they'd been implicitly resolving CombatManager/ICombatResolver/ CombatantBehavior via C#'s enclosing-namespace lookup (the old test namespace was nested under the production one) rather than an explicit using - swept the rest of the repo for the same pattern and confirmed no other spots need it. Co-Authored-By: Claude Sonnet 5 --- Directory.Build.props | 13 ----------- .../ServiceCollectionExtensions.cs | 7 +++--- tests/Directory.Build.props | 22 +++++++++++++++++++ .../Combat/CombatManagerTests.cs | 3 ++- .../Combat/CombatResolverTests.cs | 3 ++- .../SharpMud.Samples.Classic.Tests.csproj | 6 ++--- .../Attributes/ClassicAutoDataAttribute.cs | 2 +- .../TestKit/BaseFixtureFactory.cs | 2 +- 8 files changed, 34 insertions(+), 24 deletions(-) create mode 100644 tests/Directory.Build.props diff --git a/Directory.Build.props b/Directory.Build.props index 5967d5d..cded8e0 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -26,19 +26,6 @@ doc comments per documentation.md - this only suppresses the warning, it doesn't relax the convention. --> $(NoWarn);CS1591 - - $(NoWarn);NU1608 diff --git a/src/SharpMud.Persistence.DynamoDb/ServiceCollectionExtensions.cs b/src/SharpMud.Persistence.DynamoDb/ServiceCollectionExtensions.cs index 9047be8..634af15 100644 --- a/src/SharpMud.Persistence.DynamoDb/ServiceCollectionExtensions.cs +++ b/src/SharpMud.Persistence.DynamoDb/ServiceCollectionExtensions.cs @@ -16,10 +16,9 @@ public static class ServiceCollectionExtensions /// endpoint (e.g. DynamoDB Local for development). /// /// - /// No IStorageInitializer is registered here (this project has - /// no reference to SharpMud.Hosting) - unlike SQLite, this - /// provider does not create or migrate DynamoDB tables; the table(s) - /// must already exist. + /// No IStorageInitializer is registered here - unlike SQLite, + /// this provider does not create or migrate DynamoDB tables; the + /// table(s) must already exist. /// public static IServiceCollection AddSharpMudDynamoDbPersistence(this IServiceCollection services, Action? configureClient = null) { diff --git a/tests/Directory.Build.props b/tests/Directory.Build.props new file mode 100644 index 0000000..75be3c4 --- /dev/null +++ b/tests/Directory.Build.props @@ -0,0 +1,22 @@ + + + + + + + $(NoWarn);NU1608 + + diff --git a/tests/SharpMud.Samples.Classic.Tests/Combat/CombatManagerTests.cs b/tests/SharpMud.Samples.Classic.Tests/Combat/CombatManagerTests.cs index 9e5ee69..3795b52 100644 --- a/tests/SharpMud.Samples.Classic.Tests/Combat/CombatManagerTests.cs +++ b/tests/SharpMud.Samples.Classic.Tests/Combat/CombatManagerTests.cs @@ -2,8 +2,9 @@ using SharpMud.Engine.Core; using SharpMud.Engine.Sessions; using SharpMud.Engine.Ticking; +using SharpMud.Ruleset.Classic; -namespace SharpMud.Ruleset.Classic.Tests.Combat; +namespace SharpMud.Samples.Classic.Tests.Combat; public sealed class CombatManagerTests { diff --git a/tests/SharpMud.Samples.Classic.Tests/Combat/CombatResolverTests.cs b/tests/SharpMud.Samples.Classic.Tests/Combat/CombatResolverTests.cs index 70d071c..6212842 100644 --- a/tests/SharpMud.Samples.Classic.Tests/Combat/CombatResolverTests.cs +++ b/tests/SharpMud.Samples.Classic.Tests/Combat/CombatResolverTests.cs @@ -1,6 +1,7 @@ using SharpMud.Engine.Core; +using SharpMud.Ruleset.Classic; -namespace SharpMud.Ruleset.Classic.Tests.Combat; +namespace SharpMud.Samples.Classic.Tests.Combat; public sealed class CombatResolverTests { diff --git a/tests/SharpMud.Samples.Classic.Tests/SharpMud.Samples.Classic.Tests.csproj b/tests/SharpMud.Samples.Classic.Tests/SharpMud.Samples.Classic.Tests.csproj index 704d8b2..4c990cf 100644 --- a/tests/SharpMud.Samples.Classic.Tests/SharpMud.Samples.Classic.Tests.csproj +++ b/tests/SharpMud.Samples.Classic.Tests/SharpMud.Samples.Classic.Tests.csproj @@ -4,7 +4,7 @@ enable enable Exe - SharpMud.Ruleset.Classic.Tests + SharpMud.Samples.Classic.Tests net11.0 false true @@ -23,8 +23,8 @@ - - + + diff --git a/tests/SharpMud.Samples.Classic.Tests/TestKit/Attributes/ClassicAutoDataAttribute.cs b/tests/SharpMud.Samples.Classic.Tests/TestKit/Attributes/ClassicAutoDataAttribute.cs index f4098e0..997b3d6 100644 --- a/tests/SharpMud.Samples.Classic.Tests/TestKit/Attributes/ClassicAutoDataAttribute.cs +++ b/tests/SharpMud.Samples.Classic.Tests/TestKit/Attributes/ClassicAutoDataAttribute.cs @@ -1,7 +1,7 @@ using AutoFixture; using AutoFixture.Xunit3; -namespace SharpMud.Ruleset.Classic.Tests.TestKit.Attributes; +namespace SharpMud.Samples.Classic.Tests.TestKit.Attributes; public sealed class ClassicAutoDataAttribute() : AutoDataAttribute(CreateFixture) { diff --git a/tests/SharpMud.Samples.Classic.Tests/TestKit/BaseFixtureFactory.cs b/tests/SharpMud.Samples.Classic.Tests/TestKit/BaseFixtureFactory.cs index a9b4469..389b005 100644 --- a/tests/SharpMud.Samples.Classic.Tests/TestKit/BaseFixtureFactory.cs +++ b/tests/SharpMud.Samples.Classic.Tests/TestKit/BaseFixtureFactory.cs @@ -1,7 +1,7 @@ using AutoFixture; using AutoFixture.AutoNSubstitute; -namespace SharpMud.Ruleset.Classic.Tests.TestKit; +namespace SharpMud.Samples.Classic.Tests.TestKit; public static class BaseFixtureFactory { From a7299191c2ae24e9fa028cb8ec49258ed586de6e Mon Sep 17 00:00:00 2001 From: Nick Cipollina Date: Mon, 20 Jul 2026 20:30:22 -0400 Subject: [PATCH 14/14] refactor: finish renaming the sample's C# namespace from SharpMud.Ruleset.Classic to SharpMud.Samples.Classic The project/folder was renamed to SharpMud.Samples.Classic back in PLAN-0006's implementation, but the C# namespace was deliberately left as SharpMud.Ruleset.Classic at the time (no drive-by rename). Now renamed for real across every file in samples/SharpMud.Samples.Classic and its Persistence.Tests/Samples.Classic.Tests consumers, closing that gap. Also fixes 7 comments in src/SharpMud.Engine and src/SharpMud.Persistence that referenced the sample by its old namespace name as an example consumer, stale now that the rename is real. Co-Authored-By: Claude Sonnet 5 --- samples/SharpMud.Samples.Classic/AttackCommand.cs | 2 +- samples/SharpMud.Samples.Classic/CharacterClass.cs | 2 +- .../ClassicBehaviorMappingContributor.cs | 2 +- samples/SharpMud.Samples.Classic/ClassicCommands.cs | 2 +- samples/SharpMud.Samples.Classic/ClassicPlayerFactory.cs | 2 +- samples/SharpMud.Samples.Classic/ClassicWorldBuilder.cs | 2 +- samples/SharpMud.Samples.Classic/CombatManager.cs | 2 +- samples/SharpMud.Samples.Classic/CombatResolver.cs | 2 +- samples/SharpMud.Samples.Classic/CombatantBehavior.cs | 2 +- .../Configurations/CombatantBehaviorConfiguration.cs | 2 +- .../Configurations/StatsBehaviorConfiguration.cs | 2 +- samples/SharpMud.Samples.Classic/FleeCommand.cs | 2 +- samples/SharpMud.Samples.Classic/HubWorldBuilder.cs | 2 +- samples/SharpMud.Samples.Classic/ICombatManager.cs | 2 +- samples/SharpMud.Samples.Classic/ICombatResolver.cs | 2 +- samples/SharpMud.Samples.Classic/Program.cs | 2 +- samples/SharpMud.Samples.Classic/Race.cs | 2 +- samples/SharpMud.Samples.Classic/StatsBehavior.cs | 2 +- src/SharpMud.Engine/Behaviors/NpcBehavior.cs | 2 +- src/SharpMud.Engine/Behaviors/WanderManager.cs | 2 +- src/SharpMud.Engine/Behaviors/WanderingBehavior.cs | 2 +- src/SharpMud.Engine/Commands/Builtin/BuiltinCommands.cs | 2 +- src/SharpMud.Engine/Sessions/ConnectionState.cs | 2 +- src/SharpMud.Engine/SharpMud.Engine.csproj | 2 +- src/SharpMud.Persistence/GameDbContext.cs | 2 +- .../SharpMud.Persistence.Tests/TestKit/TestDbContextFactory.cs | 2 +- tests/SharpMud.Persistence.Tests/ThingRepositoryTests.cs | 2 +- .../SharpMud.Samples.Classic.Tests/Combat/CombatManagerTests.cs | 1 - .../Combat/CombatResolverTests.cs | 1 - 29 files changed, 27 insertions(+), 29 deletions(-) diff --git a/samples/SharpMud.Samples.Classic/AttackCommand.cs b/samples/SharpMud.Samples.Classic/AttackCommand.cs index 118e3e8..a270776 100644 --- a/samples/SharpMud.Samples.Classic/AttackCommand.cs +++ b/samples/SharpMud.Samples.Classic/AttackCommand.cs @@ -1,7 +1,7 @@ using SharpMud.Engine.Behaviors; using SharpMud.Engine.Commands; -namespace SharpMud.Ruleset.Classic; +namespace SharpMud.Samples.Classic; public sealed class AttackCommand(ICombatManager combatManager) : ICommand { diff --git a/samples/SharpMud.Samples.Classic/CharacterClass.cs b/samples/SharpMud.Samples.Classic/CharacterClass.cs index 4aa0f0e..f99fe07 100644 --- a/samples/SharpMud.Samples.Classic/CharacterClass.cs +++ b/samples/SharpMud.Samples.Classic/CharacterClass.cs @@ -1,6 +1,6 @@ using LayeredCraft.OptimizedEnums; -namespace SharpMud.Ruleset.Classic; +namespace SharpMud.Samples.Classic; public sealed partial class CharacterClass : OptimizedEnum { diff --git a/samples/SharpMud.Samples.Classic/ClassicBehaviorMappingContributor.cs b/samples/SharpMud.Samples.Classic/ClassicBehaviorMappingContributor.cs index 58731e4..6271577 100644 --- a/samples/SharpMud.Samples.Classic/ClassicBehaviorMappingContributor.cs +++ b/samples/SharpMud.Samples.Classic/ClassicBehaviorMappingContributor.cs @@ -1,7 +1,7 @@ using Microsoft.EntityFrameworkCore; using SharpMud.Persistence; -namespace SharpMud.Ruleset.Classic; +namespace SharpMud.Samples.Classic; // Registers this ruleset's own EF Core mapping for its behavior types // (StatsBehavior, CombatantBehavior) - Persistence never references diff --git a/samples/SharpMud.Samples.Classic/ClassicCommands.cs b/samples/SharpMud.Samples.Classic/ClassicCommands.cs index e8128c4..a1319e4 100644 --- a/samples/SharpMud.Samples.Classic/ClassicCommands.cs +++ b/samples/SharpMud.Samples.Classic/ClassicCommands.cs @@ -1,7 +1,7 @@ using SharpMud.Engine.Commands; using SharpMud.Engine.Core; -namespace SharpMud.Ruleset.Classic; +namespace SharpMud.Samples.Classic; // Called by Host alongside SharpMud.Engine.Commands.Builtin.BuiltinCommands.RegisterAll - // these commands depend on ruleset-specific behaviors (CombatantBehavior), diff --git a/samples/SharpMud.Samples.Classic/ClassicPlayerFactory.cs b/samples/SharpMud.Samples.Classic/ClassicPlayerFactory.cs index 177da0c..c7b85f8 100644 --- a/samples/SharpMud.Samples.Classic/ClassicPlayerFactory.cs +++ b/samples/SharpMud.Samples.Classic/ClassicPlayerFactory.cs @@ -1,7 +1,7 @@ using SharpMud.Engine.Core; using SharpMud.Hosting; -namespace SharpMud.Ruleset.Classic; +namespace SharpMud.Samples.Classic; /// The Classic ruleset's - wraps . public sealed class ClassicPlayerFactory : IPlayerFactory diff --git a/samples/SharpMud.Samples.Classic/ClassicWorldBuilder.cs b/samples/SharpMud.Samples.Classic/ClassicWorldBuilder.cs index 3812c68..8bf7cdb 100644 --- a/samples/SharpMud.Samples.Classic/ClassicWorldBuilder.cs +++ b/samples/SharpMud.Samples.Classic/ClassicWorldBuilder.cs @@ -2,7 +2,7 @@ using SharpMud.Engine.Core; using SharpMud.Hosting; -namespace SharpMud.Ruleset.Classic; +namespace SharpMud.Samples.Classic; /// The Classic ruleset's - wraps for . public sealed class ClassicWorldBuilder : IWorldBuilder diff --git a/samples/SharpMud.Samples.Classic/CombatManager.cs b/samples/SharpMud.Samples.Classic/CombatManager.cs index a3e9059..f5306db 100644 --- a/samples/SharpMud.Samples.Classic/CombatManager.cs +++ b/samples/SharpMud.Samples.Classic/CombatManager.cs @@ -5,7 +5,7 @@ using SharpMud.Engine.Sessions; using SharpMud.Engine.Ticking; -namespace SharpMud.Ruleset.Classic; +namespace SharpMud.Samples.Classic; // Registered once with IGameLoop and resolves every active encounter each // tick - simpler Host wiring than one ITickable per encounter, and all diff --git a/samples/SharpMud.Samples.Classic/CombatResolver.cs b/samples/SharpMud.Samples.Classic/CombatResolver.cs index 00e3f9e..d7bfc5c 100644 --- a/samples/SharpMud.Samples.Classic/CombatResolver.cs +++ b/samples/SharpMud.Samples.Classic/CombatResolver.cs @@ -1,6 +1,6 @@ using SharpMud.Engine.Core; -namespace SharpMud.Ruleset.Classic; +namespace SharpMud.Samples.Classic; // Diku/Circle-style d20-vs-AC roll (docs/combat.md decision). Level/skill // to-hit modifiers and the exact damage formula are still open items there - diff --git a/samples/SharpMud.Samples.Classic/CombatantBehavior.cs b/samples/SharpMud.Samples.Classic/CombatantBehavior.cs index f35295c..6a21d4c 100644 --- a/samples/SharpMud.Samples.Classic/CombatantBehavior.cs +++ b/samples/SharpMud.Samples.Classic/CombatantBehavior.cs @@ -1,6 +1,6 @@ using SharpMud.Engine.Core; -namespace SharpMud.Ruleset.Classic; +namespace SharpMud.Samples.Classic; // What used to be the ICombatant interface Player/Npc implemented - now a // behavior any Thing can carry (a hostile plant, a turret - anything the diff --git a/samples/SharpMud.Samples.Classic/Configurations/CombatantBehaviorConfiguration.cs b/samples/SharpMud.Samples.Classic/Configurations/CombatantBehaviorConfiguration.cs index 4767cca..6c95798 100644 --- a/samples/SharpMud.Samples.Classic/Configurations/CombatantBehaviorConfiguration.cs +++ b/samples/SharpMud.Samples.Classic/Configurations/CombatantBehaviorConfiguration.cs @@ -1,7 +1,7 @@ using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Metadata.Builders; -namespace SharpMud.Ruleset.Classic.Configurations; +namespace SharpMud.Samples.Classic.Configurations; public sealed class CombatantBehaviorConfiguration : IEntityTypeConfiguration { diff --git a/samples/SharpMud.Samples.Classic/Configurations/StatsBehaviorConfiguration.cs b/samples/SharpMud.Samples.Classic/Configurations/StatsBehaviorConfiguration.cs index d33d94e..39f0ca0 100644 --- a/samples/SharpMud.Samples.Classic/Configurations/StatsBehaviorConfiguration.cs +++ b/samples/SharpMud.Samples.Classic/Configurations/StatsBehaviorConfiguration.cs @@ -1,7 +1,7 @@ using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Metadata.Builders; -namespace SharpMud.Ruleset.Classic.Configurations; +namespace SharpMud.Samples.Classic.Configurations; public sealed class StatsBehaviorConfiguration : IEntityTypeConfiguration { diff --git a/samples/SharpMud.Samples.Classic/FleeCommand.cs b/samples/SharpMud.Samples.Classic/FleeCommand.cs index 31b6aff..9cd727e 100644 --- a/samples/SharpMud.Samples.Classic/FleeCommand.cs +++ b/samples/SharpMud.Samples.Classic/FleeCommand.cs @@ -3,7 +3,7 @@ using SharpMud.Engine.Commands.Builtin; using SharpMud.Engine.Core; -namespace SharpMud.Ruleset.Classic; +namespace SharpMud.Samples.Classic; public sealed class FleeCommand(ICombatManager combatManager, IRandomSource random) : ICommand { diff --git a/samples/SharpMud.Samples.Classic/HubWorldBuilder.cs b/samples/SharpMud.Samples.Classic/HubWorldBuilder.cs index 8bda7ff..0ebb98c 100644 --- a/samples/SharpMud.Samples.Classic/HubWorldBuilder.cs +++ b/samples/SharpMud.Samples.Classic/HubWorldBuilder.cs @@ -1,7 +1,7 @@ using SharpMud.Engine.Behaviors; using SharpMud.Engine.Core; -namespace SharpMud.Ruleset.Classic; +namespace SharpMud.Samples.Classic; // Content, not engine or ruleset - mirrors WheelMUD's separate "Universe" // project (docs/research/wheelmud-findings.md). Lives alongside the ruleset diff --git a/samples/SharpMud.Samples.Classic/ICombatManager.cs b/samples/SharpMud.Samples.Classic/ICombatManager.cs index fd6267f..044f309 100644 --- a/samples/SharpMud.Samples.Classic/ICombatManager.cs +++ b/samples/SharpMud.Samples.Classic/ICombatManager.cs @@ -1,7 +1,7 @@ using System.Diagnostics.CodeAnalysis; using SharpMud.Engine.Core; -namespace SharpMud.Ruleset.Classic; +namespace SharpMud.Samples.Classic; public sealed class CombatEncounter { diff --git a/samples/SharpMud.Samples.Classic/ICombatResolver.cs b/samples/SharpMud.Samples.Classic/ICombatResolver.cs index 014e770..8102e23 100644 --- a/samples/SharpMud.Samples.Classic/ICombatResolver.cs +++ b/samples/SharpMud.Samples.Classic/ICombatResolver.cs @@ -1,6 +1,6 @@ using SharpMud.Engine.Core; -namespace SharpMud.Ruleset.Classic; +namespace SharpMud.Samples.Classic; public sealed record CombatRoundResult(bool Hit, int Damage, bool DefenderDefeated); diff --git a/samples/SharpMud.Samples.Classic/Program.cs b/samples/SharpMud.Samples.Classic/Program.cs index d323835..50d69d8 100644 --- a/samples/SharpMud.Samples.Classic/Program.cs +++ b/samples/SharpMud.Samples.Classic/Program.cs @@ -10,7 +10,7 @@ using SharpMud.Hosting; using SharpMud.Persistence; using SharpMud.Persistence.Sqlite; -using SharpMud.Ruleset.Classic; +using SharpMud.Samples.Classic; var builder = SharpMudApplication.CreateBuilder(args); diff --git a/samples/SharpMud.Samples.Classic/Race.cs b/samples/SharpMud.Samples.Classic/Race.cs index 1d0797e..397b5af 100644 --- a/samples/SharpMud.Samples.Classic/Race.cs +++ b/samples/SharpMud.Samples.Classic/Race.cs @@ -1,6 +1,6 @@ using LayeredCraft.OptimizedEnums; -namespace SharpMud.Ruleset.Classic; +namespace SharpMud.Samples.Classic; // LayeredCraft.OptimizedEnums instead of a plain enum specifically because // each race will eventually carry its own stat modifiers (docs/character.md diff --git a/samples/SharpMud.Samples.Classic/StatsBehavior.cs b/samples/SharpMud.Samples.Classic/StatsBehavior.cs index e344840..b9dd839 100644 --- a/samples/SharpMud.Samples.Classic/StatsBehavior.cs +++ b/samples/SharpMud.Samples.Classic/StatsBehavior.cs @@ -1,6 +1,6 @@ using SharpMud.Engine.Core; -namespace SharpMud.Ruleset.Classic; +namespace SharpMud.Samples.Classic; // The D&D-style attribute/derived-stat block from docs/character.md. Attached // to whichever Things the ruleset wants to be a "character" - a Thing with diff --git a/src/SharpMud.Engine/Behaviors/NpcBehavior.cs b/src/SharpMud.Engine/Behaviors/NpcBehavior.cs index f55fcf9..46eadff 100644 --- a/src/SharpMud.Engine/Behaviors/NpcBehavior.cs +++ b/src/SharpMud.Engine/Behaviors/NpcBehavior.cs @@ -4,5 +4,5 @@ namespace SharpMud.Engine.Behaviors; // Marker only - presence means "this Thing is an NPC." No combat stats; // those live on a ruleset-specific behavior (e.g. CombatantBehavior in -// SharpMud.Ruleset.Classic). +// SharpMud.Samples.Classic). public sealed class NpcBehavior : Behavior; diff --git a/src/SharpMud.Engine/Behaviors/WanderManager.cs b/src/SharpMud.Engine/Behaviors/WanderManager.cs index cacc76b..4982df0 100644 --- a/src/SharpMud.Engine/Behaviors/WanderManager.cs +++ b/src/SharpMud.Engine/Behaviors/WanderManager.cs @@ -5,7 +5,7 @@ namespace SharpMud.Engine.Behaviors; // Registered once with IGameLoop (mirrors CombatManager in -// SharpMud.Ruleset.Classic) rather than one ITickable per wandering NPC - +// SharpMud.Samples.Classic) rather than one ITickable per wandering NPC - // simpler Host wiring, and it keeps Behaviors free of a dependency on // IGameLoop for self-registration. public sealed class WanderManager(IWorld world, IRandomSource random) : ITickable diff --git a/src/SharpMud.Engine/Behaviors/WanderingBehavior.cs b/src/SharpMud.Engine/Behaviors/WanderingBehavior.cs index 7ed3aa5..06b89af 100644 --- a/src/SharpMud.Engine/Behaviors/WanderingBehavior.cs +++ b/src/SharpMud.Engine/Behaviors/WanderingBehavior.cs @@ -3,7 +3,7 @@ namespace SharpMud.Engine.Behaviors; // Presence + WanderManager (registered once with IGameLoop, like -// CombatManager in SharpMud.Ruleset.Classic) means "this NPC randomly moves +// CombatManager in SharpMud.Samples.Classic) means "this NPC randomly moves // to an adjacent room some percentage of ticks." Engine-level (not // ruleset-specific) since wandering doesn't depend on any combat/stat system. public sealed class WanderingBehavior : Behavior diff --git a/src/SharpMud.Engine/Commands/Builtin/BuiltinCommands.cs b/src/SharpMud.Engine/Commands/Builtin/BuiltinCommands.cs index 1818f2f..7130feb 100644 --- a/src/SharpMud.Engine/Commands/Builtin/BuiltinCommands.cs +++ b/src/SharpMud.Engine/Commands/Builtin/BuiltinCommands.cs @@ -4,7 +4,7 @@ namespace SharpMud.Engine.Commands.Builtin; // Ruleset-agnostic commands only - kill/attack/flee (and anything else that // depends on a ruleset-specific behavior) register themselves separately; -// see SharpMud.Ruleset.Classic's equivalent registration method, called by +// see SharpMud.Samples.Classic's equivalent registration method, called by // Host alongside this one. public static class BuiltinCommands { diff --git a/src/SharpMud.Engine/Sessions/ConnectionState.cs b/src/SharpMud.Engine/Sessions/ConnectionState.cs index a439856..90d8557 100644 --- a/src/SharpMud.Engine/Sessions/ConnectionState.cs +++ b/src/SharpMud.Engine/Sessions/ConnectionState.cs @@ -8,7 +8,7 @@ namespace SharpMud.Engine.Sessions; /// /// /// A LayeredCraft.OptimizedEnums state machine (ADR-0004), same precedent as -/// Race/CharacterClass in SharpMud.Ruleset.Classic - the legal +/// Race/CharacterClass in SharpMud.Samples.Classic - the legal /// transitions live on the enum itself via rather than being /// re-checked at every call site that mutates PlayerBehavior.ConnectionState. /// diff --git a/src/SharpMud.Engine/SharpMud.Engine.csproj b/src/SharpMud.Engine/SharpMud.Engine.csproj index e42b84d..d99f156 100644 --- a/src/SharpMud.Engine/SharpMud.Engine.csproj +++ b/src/SharpMud.Engine/SharpMud.Engine.csproj @@ -15,7 +15,7 @@ + SharpMud.Samples.Classic. --> diff --git a/src/SharpMud.Persistence/GameDbContext.cs b/src/SharpMud.Persistence/GameDbContext.cs index c7826b0..923858a 100644 --- a/src/SharpMud.Persistence/GameDbContext.cs +++ b/src/SharpMud.Persistence/GameDbContext.cs @@ -21,7 +21,7 @@ protected override void OnModelCreating(ModelBuilder modelBuilder) // Picks up every IEntityTypeConfiguration in this assembly - // Thing, the base Behavior TPH mapping, and every engine-level // behavior type. Ruleset-level behaviors register via contributors - // (SharpMud.Ruleset.Classic's own ApplyConfigurationsFromAssembly + // (SharpMud.Samples.Classic's own ApplyConfigurationsFromAssembly // call over its own assembly). modelBuilder.ApplyConfigurationsFromAssembly(typeof(GameDbContext).Assembly); diff --git a/tests/SharpMud.Persistence.Tests/TestKit/TestDbContextFactory.cs b/tests/SharpMud.Persistence.Tests/TestKit/TestDbContextFactory.cs index ff70a23..6d715f5 100644 --- a/tests/SharpMud.Persistence.Tests/TestKit/TestDbContextFactory.cs +++ b/tests/SharpMud.Persistence.Tests/TestKit/TestDbContextFactory.cs @@ -1,5 +1,5 @@ using Microsoft.EntityFrameworkCore; -using SharpMud.Ruleset.Classic; +using SharpMud.Samples.Classic; namespace SharpMud.Persistence.Tests.TestKit; diff --git a/tests/SharpMud.Persistence.Tests/ThingRepositoryTests.cs b/tests/SharpMud.Persistence.Tests/ThingRepositoryTests.cs index 0b3bf45..60e0ec6 100644 --- a/tests/SharpMud.Persistence.Tests/ThingRepositoryTests.cs +++ b/tests/SharpMud.Persistence.Tests/ThingRepositoryTests.cs @@ -1,7 +1,7 @@ using SharpMud.Engine.Behaviors; using SharpMud.Engine.Core; using SharpMud.Persistence.Tests.TestKit; -using SharpMud.Ruleset.Classic; +using SharpMud.Samples.Classic; namespace SharpMud.Persistence.Tests; diff --git a/tests/SharpMud.Samples.Classic.Tests/Combat/CombatManagerTests.cs b/tests/SharpMud.Samples.Classic.Tests/Combat/CombatManagerTests.cs index 3795b52..6394a54 100644 --- a/tests/SharpMud.Samples.Classic.Tests/Combat/CombatManagerTests.cs +++ b/tests/SharpMud.Samples.Classic.Tests/Combat/CombatManagerTests.cs @@ -2,7 +2,6 @@ using SharpMud.Engine.Core; using SharpMud.Engine.Sessions; using SharpMud.Engine.Ticking; -using SharpMud.Ruleset.Classic; namespace SharpMud.Samples.Classic.Tests.Combat; diff --git a/tests/SharpMud.Samples.Classic.Tests/Combat/CombatResolverTests.cs b/tests/SharpMud.Samples.Classic.Tests/Combat/CombatResolverTests.cs index 6212842..0922ff8 100644 --- a/tests/SharpMud.Samples.Classic.Tests/Combat/CombatResolverTests.cs +++ b/tests/SharpMud.Samples.Classic.Tests/Combat/CombatResolverTests.cs @@ -1,5 +1,4 @@ using SharpMud.Engine.Core; -using SharpMud.Ruleset.Classic; namespace SharpMud.Samples.Classic.Tests.Combat;