Skip to content

Add client/server CLI on shared Plugin Manager infrastructure - #150

Open
Jacoby6000 wants to merge 29 commits into
mainfrom
server-cli
Open

Jacoby6000 wants to merge 29 commits into
mainfrom
server-cli

Conversation

@Jacoby6000

@Jacoby6000 Jacoby6000 commented Oct 11, 2026 •

Copy link
Copy Markdown
Contributor

Add a client/server CLI with YAML hosting, shared verified mod caches, and safer DLL injection.

  • CLI launches and registry queries
    • Use System.CommandLine and Core services without WPF; avoid a separate launch pipeline.
    • Support client/server options and mod/DLL list, get, and versions queries.
  • Authoritative YAML and multi-server hosting
    • YAML replaces other launch arguments for reproducible configuration.
    • Each servers entry owns mod-setup, ini, and an optional saved-dir-suffix (default: its key). Startup is sequential; shutdown is grouped; natural exits remain independent.
  • Typed per-server INI overrides
    • Infer known fields from Core definitions; unknown fields require {type, value}. Support scalar, enum, record, and list values.
    • Patch only specified profile INI keys; replace selected arrays without touching other fields. raw passes an exact string through a CLI option instead. Validate before preparation.
  • Profile-local paks and plugins
    • CLI assets use %LOCALAPPDATA%/Chivalry 2/Saved_<suffix>/unchained/{paks,plugins}; client suffix defaults to unchained.
    • Separate inventories, signatures, and profile locks preserve independent selections. Forward --additional-pak-dir; leave installation-wide files untouched and mounted.
  • Shared pak/DLL caches with hard links
    • SHA-512 pak and SHA-256 DLL caches avoid repeated downloads and duplicate storage.
    • Validate bytes, lock cache fills across processes, and publish atomically. Use hard links or copy fallback; never overwrite linked bytes or unknown destination files.
  • Suspended startup and server initialization
    • Inject before resuming the game's main thread; terminate the child on failure.
    • Initialize server first-run settings in the game's hyphen-normalized saved profile to avoid unattended startup prompts.
  • Terminal monitoring
    • Reuse the running-server GUI's telemetry with bounded history, scrolling, keyboard/mouse tabs, and shutdown controls.
    • Retain noninteractive hosting.
  • Shared validation and injection safety
    • Resolve explicit DLL pins before dependencies; revalidate paks after DLL preparation adds dependencies.
    • Reuse protected injection handles and ownership checks. YAML cannot grant unrestricted DLL loading; preserve the GUI's one-time -67 retry.
  • GUI selections and remaining routing work
    • GUI servers retain separate saved suffixes, ports, and configuration while sharing selections.
    • Per-server GUI asset materialization is still pending; the current GUI uses one shared asset directory.

Examples

Run on Windows with the CLI executable and an existing game installation. Replace the installation path; it must contain TBL.
These examples pin registered releases. Replace the core plugin pin with a registered build supporting --additional-pak-dir before launching.

Client:

.\UnchainedLauncher.CLI.exe client `
  --chivalry2-path "C:\Games\Chivalry 2" --platform Steam `
  --dll Chiv2-Community/UnchainedPlugin:v1.0.0-RC3 `
  --mod Chiv2-Community/Unchained-Mods:v0.1.0 `
  --mod Chiv2-Community/Chiv2Turbo:v0.0.4

Headless server:

.\UnchainedLauncher.CLI.exe server `
  --chivalry2-path "C:\Games\Chivalry 2" --platform Steam `
  --saved-dir-suffix MyServer --name "My Server" --starting-map FFA_Courtyard `
  --dll Chiv2-Community/UnchainedPlugin:v1.0.0-RC3 `
  --mod Chiv2-Community/Unchained-Mods:v0.1.0 `
  --mod Chiv2-Community/Chiv2Turbo:v0.0.4 `
  --server-mod /Game/Mods/AgMods/Chiv2Turbo/Chiv2Turbo.Chiv2Turbo_C

Two servers, saved as server.yaml:

chiv2-path: 'C:\Games\Chivalry 2'
platform: Steam
servers:
  first:
    # saved-dir-suffix defaults to first; set it here to override.
    mod-setup:
      mods:
        - Chiv2-Community/Unchained-Mods:v0.1.0
        - Chiv2-Community/Chiv2Turbo:v0.0.4
      dlls:
        - Chiv2-Community/UnchainedPlugin:v1.0.0-RC3
    ini:
      Game:
        /Script/TBL.TBLGameMode:
          MapList: [FFA_Courtyard, FFA_Wardenglade]
          BotBackfillEnabled: true
          ClassLimits:
            - Class: Poleman
              ClassLimitPercent: 10
            - Class: Archer
              ClassLimitPercent: 0
    name: First Server
    starting-map: FFA_Courtyard
    mod-actors:
      - /Game/Mods/AgMods/Chiv2Turbo/Chiv2Turbo.Chiv2Turbo_C
    game-port: 7777
    ping-port: 3075
    query-port: 7071
    rcon-port: 9001
  second:
    mod-setup:
      mods:
        - Chiv2-Community/Unchained-Mods:v0.1.0
        - Chiv2-Community/Chiv2Turbo:v0.0.4
      dlls:
        - Chiv2-Community/UnchainedPlugin:v1.0.0-RC3
    name: Second Server
    starting-map: FFA_Courtyard
    mod-actors:
      - /Game/Mods/AgMods/Chiv2Turbo/Chiv2Turbo.Chiv2Turbo_C
    game-port: 7778
    ping-port: 3076
    query-port: 7072
    rcon-port: 9002
.\UnchainedLauncher.CLI.exe server --config-path .\server.yaml

Profiles default to first and second. Each selects its own paks/DLLs and enables the Chiv2Turbo actor; matching bytes share the verified cache. Installing a pak does not enable its server actors; mod-actors / --server-mod does. The official registry is included automatically. Server ports must be available and reachable by players.

Validation and limits

  • 443 CLI tests pass in Debug/Release; Windows x64 CLI publication and Windows-targeted GUI build pass.
  • Proton checks cover injection cleanup, cache/link identity, copy fallback, cancellation, version switching, and collision/archival safety. Two live servers answered A2S and shared cached pak bytes.
  • Requires an UnchainedPlugin build supporting --additional-pak-dir; live checks used a local build. This PR does not release the plugin change.
  • Native Windows game execution and a fresh independent review of the current head remain pending. Earlier clean review/Windows CI applied to 2377958.
  • Windows may require stopping peers before changing linked assets. Existing dependency/compiler warnings remain.

Related: #149, #152

@mergify

mergify Bot commented Oct 11, 2026 •

Copy link
Copy Markdown
Contributor

Tick the box to add this pull request to the merge queue (same as @mergifyio queue).

  • Queue this pull request

@Jacoby6000 Jacoby6000 left a comment •

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reviewed the full diff at dd57412. One Windows blocker: leaked registry input streams (inline). CLI 41/41 passes in Debug/Release; Windows CI fails cleanup. No live game/injection test.

Assert.False((await manager.UpdateModsList()).HasErrors);
var options = ServerOptions.Parse(["server", "--chivalry2-path", _directory, "--platform", "Steam"]).ToLaunchOptions();
var dialogue = new ConsoleDialogue();
Assert.True((await new Chivalry2ModsInstaller(manager, dialogue).PrepareLaunch(options)).IsSome);

@Jacoby6000 Jacoby6000 Oct 11, 2026 •

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P1] Dispose registry input streams

FileWriter.WriteAsync closes only its output, leaving local registry paks locked. Windows CI fails test cleanup and blocks packaging. Dispose owned inputs on success, failure, and cancellation; test exclusive source reopening without forced GC.

@Jacoby6000 Jacoby6000 Oct 11, 2026 •

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 97cbc03. FileWriter disposes owned inputs on success, failure, cancellation, and abandoned acquisitions. Regressions check disposal and exclusive reopening. Also locked concurrent progress aggregation. CLI 51/51 passes in Debug/Release; Windows CI passes.

@Jacoby6000 Jacoby6000 left a comment •

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reviewed the full diff at 3034861. Two parsing defects below; stream ownership is fixed. CLI 121/121 passes in Debug/Release; Windows CI passes. No live game/injection test.

Comment on lines +26 to +28
if (probe.GetResult(config) is { } configResult) {
ThrowErrors(probe);
return YamlConfiguration.Load(probe.GetValue(config)!, probe.CommandResult.Command.Name == "client");

@Jacoby6000 Jacoby6000 Oct 11, 2026 •

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P2] Ignore unrelated arguments before loading authoritative YAML

Valid YAML plus --name @missing.rsp fails response-file expansion, although YAML must replace that option. Disable expansion/validation of ignored input; add client/server regressions.

@Jacoby6000 Jacoby6000 Oct 11, 2026 •

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in bd6e079. YAML selection checks only selector errors; unrelated CLI input stays ignored. Launcher response-file expansion is disabled and documented. Client/server regressions pass; CLI 129/129 in Debug/Release and Windows CI pass.

root.Subcommands.Add(server.Command);
root.Subcommands.Add(client.Command);
var parsed = root.Parse(launcherArgs);
var definition = parsed.CommandResult.Command == server.Command ? server : client;

@Jacoby6000 Jacoby6000 Oct 11, 2026 •

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P2] Parse launcher arguments once

Ordinary launches run both selector and root parsers; tracing shows response files opened twice. Reuse one System.CommandLine grammar and ParseResult for selection and binding.

@Jacoby6000 Jacoby6000 Oct 11, 2026 •

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in bd6e079. One System.CommandLine grammar and ParseResult now handle selection and binding. Parse-count regressions confirm one call; response files are not opened. CLI 129/129 in Debug/Release and Windows CI pass.

@Jacoby6000 Jacoby6000 left a comment •

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reviewed the full diff at bd6e079. Prior fixes hold; inactive YAML still validates ignored content (inline). CLI 129/129 passes in Debug/Release; Windows CI passes. No live game/injection test.

var deserializer = new DeserializerBuilder()
.WithNamingConvention(HyphenatedNamingConvention.Instance)
.WithTypeConverter(new InstallationTypeYamlConverter())
.WithDuplicateKeyChecking()

@Jacoby6000 Jacoby6000 Oct 11, 2026 •

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P2] Skip inactive YAML blocks

The inactive object? property still deserializes dictionaries: duplicate keys under server reject a valid client launch, and vice versa. Skip the inactive node without materializing it; keep root/shared/active validation strict. Cover nested duplicates for both commands.

@Jacoby6000 Jacoby6000 Oct 11, 2026 •

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in bf3a996. A property-scoped YamlDotNet converter skips inactive nodes; root/shared/active duplicate checks remain strict. Regressions cover duplicates, tags, aliases, and malformed YAML. CLI 161/161 passes in Debug/Release; Windows CI passes.

@Jacoby6000 Jacoby6000 left a comment •

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reviewed the full diff at bf3a996. Prior fixes hold; CLI bypasses the GUI's headless/desync guard (inline). CLI 161/161 passes in Debug/Release; Windows CI passes. No live game/injection test.

: base(shared, SharedLaunchOptions.Entries(settings.GameArgs, allowEmpty: true)) {
Settings = settings;
NoTui = noTui;
if (new[] { settings.GamePort, settings.PingPort, settings.QueryPort, settings.RconPort }.Distinct().Count() != 4)

@Jacoby6000 Jacoby6000 Oct 11, 2026 •

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P2] Enforce the headless/desync guard

--headless false leaves desync enabled, unlike the GUI's compatibility guard. YAML has the same gap. Reject !Headless && DesyncPatch; require --no-desync-patch or desync-patch: false for rendering. Update examples and tests. No live crash is claimed.

@Jacoby6000 Jacoby6000 Oct 11, 2026 •

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 16c5192. Shared server validation rejects rendered launches with desync enabled and explains how to disable it. Defaults remain unchanged; clients are unaffected. CLI/YAML regressions pass: CLI 167/167 in Debug/Release and Windows CI.

@Jacoby6000 Jacoby6000 left a comment •

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reviewed the full diff at 16c5192. One argument-quoting defect reproduced with subprocess argv capture. CLI 167/167 passes in Debug/Release; Windows CI passes. No live game/injection test.


private protected LaunchOptions BuildLaunchOptions(IEnumerable<string> prefix, LanguageExt.Option<ServerLaunchOptions> server) => new(
EnabledReleases: Shared.Mods.Distinct(), ServerBrowserBackend: Some(Shared.Backend),
LaunchArgs: string.Join(" ", prefix.Concat(GameArguments.Select(ArgumentEscaper.Escape))),

@Jacoby6000 Jacoby6000 Oct 11, 2026 •

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P2] Preserve forwarded argument boundaries

A trailing backslash in -UserDir=C:\Server Profiles\ escapes the closing quote, absorbing subsequent launcher flags. Tabs and backslash/quote combinations also break. Reproduced through client/server CLI and YAML subprocess launches. Use vetted Windows quoting or ArgumentList; test exact argv, including independent -unchained.

@Jacoby6000 Jacoby6000 Oct 11, 2026 •

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 33f977c; fd8d2b2 only ignores probe build outputs. Shared escaping now uses pinned CliWrap ArgumentsBuilder.Escape. Added 66 subprocess argv regressions across client/server and CLI/YAML; 38 fail without the fix. CLI 233/233 passes in Debug/Release; Windows CI passes.

@Jacoby6000 Jacoby6000 left a comment •

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reviewed the full diff at fd8d2b2. One managed-inventory handoff defect reproduced through Core installation/signing. CLI 233/233 passes in Debug/Release; Windows CI passes. No live game/injection test.

Comment thread UnchainedLauncher.CLI/ServerLauncher.cs Outdated
var cachePath = Path.Combine(options.Chivalry2Path, FilePaths.ModCachePath, "cli_mod_manager.json");
var codec = new ModManagerCodec(registry);
ModManager? cached = null;
var existingCache = File.Exists(cachePath) ? cachePath : FilePaths.ModManagerConfigPath;

@Jacoby6000 Jacoby6000 Oct 11, 2026 •

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P2] Share physical managed-pak ownership

After GUI → CLI → GUI → CLI, the stale CLI inventory misses later GUI paks and treats them as unmanaged. Excluded mods remain signed and installed; reordering can leave duplicates. Reconcile directory ownership independently of host selections. Test sequential handoffs while preserving unknown paks and GUI selections.

@Jacoby6000 Jacoby6000 Oct 11, 2026 •

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in e05766b. Core reconciles physical ownership through .unchained_managed_paks.json; legacy caches seed it once. Host selections stay separate. Six regressions cover handoffs, duplicates, unknown files, and partial-failure retries. CLI 239/239 passes in Debug/Release; Windows CI passes. Concurrent preparation remains unsupported at this commit.

@Jacoby6000 Jacoby6000 changed the title Add Core-backed server CLI and live TUI dashboard Add client/server CLI on shared Plugin Manager infrastructure Oct 13, 2026
@Jacoby6000
Jacoby6000 changed the base branch from main to feature/dll-validation October 13, 2026 20:21
@Jacoby6000
Jacoby6000 changed the base branch from feature/dll-validation to main October 15, 2026 02:00

@Jacoby6000 Jacoby6000 left a comment •

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Full review: one DLL-pin defect.

if (!roots.Any(root => root.Org.Equals("Chiv2-Community", StringComparison.OrdinalIgnoreCase)
&& root.ModuleName.Equals("UnchainedPlugin", StringComparison.OrdinalIgnoreCase)))
roots.Insert(0, new ReleaseCoordinates("Chiv2-Community", "UnchainedPlugin", "latest"));
foreach (var root in roots) Visit(root.Org + "/" + root.ModuleName, root.Version, exact: true);

@Jacoby6000 Jacoby6000 Oct 15, 2026 •

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P2] Index explicit DLL pins before dependency traversal

Requesting a dependent before its explicitly pinned dependency can greedily select a newer version and report a false conflict; reversing the order succeeds. Resolve against all explicit pins and test both orders.

@Jacoby6000 Jacoby6000 Oct 15, 2026 •

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 9283596: index explicit pins before traversal. Ordering, conflict, target, prerelease, and cycle regressions pass; CLI 359/359 in Debug/Release.

@Jacoby6000 Jacoby6000 left a comment •

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Full review: one pak-validation defect.

Comment thread UnchainedLauncher.CLI/ServerLauncher.cs Outdated
cancellationToken.ThrowIfCancellationRequested();
if (updated.IsNone) return LanguageExt.Option<LaunchOptions>.None;
try {
updated = await modInstaller.PrepareLaunch(updated.IfNone(launch));

@Jacoby6000 Jacoby6000 Oct 15, 2026 •

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P2] Revalidate paks after DLL preparation

DLL preparation can select an older pak whose transitive dependencies were not checked earlier. Installation/signing then accepts an incomplete set. Rerun ValidateReleases after DLL preparation and before pak installation.

@Jacoby6000 Jacoby6000 Oct 15, 2026 •

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 2377958: revalidate after DLL preparation, before installation. Client/server regressions cover missing dependencies and valid older releases; CLI 370/370 passes in Debug/Release.

Jacoby6000 and others added 6 commits October 15, 2026 10:05
Cache verified paks and DLLs with per-object download leases, atomic publication, hard links, and copy fallback. Route CLI assets by saved profile and keep GUI selections shared. Start modded processes suspended for validated injection and initialize server profiles using game-normalized suffixes.
@Jacoby6000 Jacoby6000 mentioned this pull request Oct 16, 2026
8 tasks
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Status: Backlog

Development

Successfully merging this pull request may close these issues.

1 participant