Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
39 commits
Select commit Hold shift + click to select a range
c8bd895
Add registry-validated DLL loading and Plugin Manager
Jacoby6000 Oct 13, 2026
3f19f94
[GHA] Format Source
Jacoby6000 Oct 13, 2026
e57be08
Record DLL validation submission
Jacoby6000 Oct 13, 2026
14383aa
Record PR 152 metadata update
Jacoby6000 Oct 13, 2026
3999a7e
List mod plugin requirements and shorten Plugin Manager docs
Jacoby6000 Oct 13, 2026
74db8a5
Merge remote-tracking branch 'origin/feature/dll-validation' into fea…
Jacoby6000 Oct 13, 2026
6725161
[GHA] Format Source
Jacoby6000 Oct 13, 2026
9366db5
Rename InjectDlLs to InjectDLLs
Jacoby6000 Oct 13, 2026
1fee21a
Record mod plugin dependency warning decision
Jacoby6000 Oct 13, 2026
1537e7c
Merge remote-tracking branch 'origin/feature/dll-validation' into fea…
Jacoby6000 Oct 13, 2026
97c9c1f
[GHA] Format Source
Jacoby6000 Oct 13, 2026
f9f6657
Remove obsolete Clientside launch guard and check enabled plugins
Jacoby6000 Oct 13, 2026
9e19a54
Merge remote-tracking branch 'origin/feature/dll-validation' into fea…
Jacoby6000 Oct 13, 2026
7976bdb
[GHA] Format Source
Jacoby6000 Oct 13, 2026
ebbaa40
Record removal of Clientside launch support
Jacoby6000 Oct 13, 2026
f7ba38b
Merge remote-tracking branch 'origin/feature/dll-validation' into fea…
Jacoby6000 Oct 13, 2026
6d6e181
fix: recover revoked plugin pins and preserve local plugin requirements
Jacoby6000 Oct 13, 2026
9e5a111
[GHA] Format Source
Jacoby6000 Oct 13, 2026
515e155
Record PR 152 review fixes and outcome
Jacoby6000 Oct 13, 2026
ce7cefd
Cache inactive managed DLLs outside Plugins without moving unknown files
Jacoby6000 Oct 13, 2026
ee0c326
[GHA] Format Source
Jacoby6000 Oct 13, 2026
17ab687
Record managed DLL cache boundary
Jacoby6000 Oct 13, 2026
ad3ec37
Merge remote-tracking branch 'origin/feature/dll-validation' into fea…
Jacoby6000 Oct 13, 2026
9bcfd9a
Bind managed DLL archival deletion to validated Windows handle
Jacoby6000 Oct 13, 2026
a583c37
[GHA] Format Source
Jacoby6000 Oct 13, 2026
8483c16
Accept Windows directory publication failure in archival regression
Jacoby6000 Oct 13, 2026
ab80b43
fix: preserve active DLL publication collisions
Jacoby6000 Oct 13, 2026
afb1747
[GHA] Format Source
Jacoby6000 Oct 13, 2026
980af1c
Record DLL cache race hardening
Jacoby6000 Oct 14, 2026
eac8659
Record Steam-install GUI startup smoke test
Jacoby6000 Oct 14, 2026
f932306
Fix launch preparation failures and refreshed plugin selections
Jacoby6000 Oct 14, 2026
e04b340
Record pak preparation and plugin refresh decisions
Jacoby6000 Oct 14, 2026
2f68f8c
[GHA] Format Source
Jacoby6000 Oct 14, 2026
8ba0b24
Record Plugin Manager release-notes card decision
Jacoby6000 Oct 14, 2026
89aed4c
Fix pak collision relocation load-order prefixes
Jacoby6000 Oct 14, 2026
c039b0a
Record pak collision load-order decision
Jacoby6000 Oct 14, 2026
17289f5
Refactor plugin validation and shared UI helpers; ignore annals
Jacoby6000 Oct 14, 2026
47dea2f
Migrate registry config to shared roots and address review annotations
Jacoby6000 Oct 15, 2026
3776dba
[GHA] Format Source
Jacoby6000 Oct 15, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -9,4 +9,5 @@ packages
.idea

out
nul
nul
/annals/
3 changes: 2 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,9 @@ Check out [the wiki](https://unchained.wiki) for more information about modding
## Features

- **Launch Chivalry 2**: Easily launch Chivalry 2 with custom configurations.
- **Switch Between Vanilla, Client Side Modded, Unchained**: Seamlessly switch between vanilla and modded installs without manually moving any files around. Use unchained mode to connect to community hosted servers.
- **Switch Between Vanilla and Unchained**: Switch without moving files manually. Use Unchained for mods, plugins, and community servers.
- **Mod Management**: Download and manage mods for Chivalry 2.
- **DLL Management**: Enable registry-approved native plugins with SHA-256 and size validation.
- **Server Hosting**: Configure and host a server for your friends to join in Unchained Mode

## Community
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
using System.Text.Json;
using UnchainedLauncher.Core.JsonModels.ModMetadata;
using UnchainedLauncher.Core.Services.Dlls;

namespace UnchainedLauncher.Core.Tests.Unit.Services.Dlls {
public sealed class DependencyTypesTests {
private static Release Pak(string name, params Dependency[] plugins) => new("v1.0.0", "hash", name + ".pak",
DateTime.UtcNow, new ModInfo("https://github.com/Test/" + name, name, "Description", null, null, [], [],
PluginDependencies: plugins.ToList()), null!, null);

[Fact]
public void ModPluginWarningListsOneModAndDeduplicatesItsPluginRequirements() {
var dependency = new Dependency("https://github.com/Test/Native", ">=1.0.0,<2.0.0");
var error = Assert.Throws<InvalidOperationException>(() =>
DependencyValidator.Validate([Pak("Arena", dependency, dependency), Pak("NoPlugins")], []));
Assert.Equal("One mod requires a DLL plugin. Enable them in the Plugin Manager.\n\nArena:\n\t- Native >=1.0.0,<2.0.0",
error.Message);
}

[Fact]
public void ModPluginWarningGroupsAllModsIncludingPakDependencies() {
var alpha = Pak("Alpha", new Dependency("https://github.com/Test/Native", "^1.0.0"));
var beta = Pak("Beta", new Dependency("https://github.com/Test/ZPlugin", "2.0.0"),
new Dependency("https://github.com/Test/APlugin", "~1.2.0"));
alpha = alpha with { Info = alpha.Info with { ModDependencies = [new Dependency(beta.Info.RepoUrl, "1.0.0")] } };
var error = Assert.Throws<InvalidOperationException>(() => DependencyValidator.Validate([beta, alpha, Pak("NoPlugins")], []));
Assert.Equal("Some mods require DLL plugins. Enable them in the Plugin Manager.\n\nAlpha:\n\t- Native ^1.0.0\n\nBeta:\n\t- APlugin ~1.2.0\n\t- ZPlugin 2.0.0",
error.Message);
}

[Fact]
public void ModPluginWarningMergesCaseInsensitiveRepositoriesAndKeepsFirstDisplayName() {
var dependency = new Dependency("https://github.com/Test/Native", "1.0.0");
var first = Pak("Arena", dependency);
var second = first with { Info = first.Info with { RepoUrl = first.Info.RepoUrl.ToLowerInvariant(), Name = "Other name" } };
var error = Assert.Throws<InvalidOperationException>(() => DependencyValidator.Validate([first, second], []));
Assert.Equal("One mod requires a DLL plugin. Enable them in the Plugin Manager.\n\nArena:\n\t- Native 1.0.0", error.Message);
}

[Fact]
public void ModPluginWarningUsesPluralPluginsForOneMod() {
var error = Assert.Throws<InvalidOperationException>(() => DependencyValidator.Validate([
Pak("Arena", new Dependency("https://github.com/Test/Native", "1.0.0"),
new Dependency("https://github.com/Test/Other", "2.0.0"))], []));
Assert.StartsWith("One mod requires DLL plugins.", error.Message);
}

[Fact]
public void ModPluginWarningKeepsSharedPluginUnderEachMod() {
var dependency = new Dependency("https://github.com/Test/Native", "1.0.0");
var error = Assert.Throws<InvalidOperationException>(() => DependencyValidator.Validate([Pak("Beta", dependency), Pak("Alpha", dependency)], []));
Assert.Equal("Some mods require a DLL plugin. Enable them in the Plugin Manager.\n\nAlpha:\n\t- Native 1.0.0\n\nBeta:\n\t- Native 1.0.0",
error.Message);
}

[Fact]
public void ManuallyEnabledCompatiblePluginsPassAndDoNotAppearInWarning() {
var native = new DllRelease("Test", "Native", "1.5.0", "https://example.com/Native.dll", new string('a', 64), 1);
var satisfied = Pak("Satisfied", new Dependency("https://github.com/Test/Native", "^1.0.0"));
DependencyValidator.Validate([satisfied], [native]);
var missing = new Dependency("https://github.com/Test/Other", "2.0.0");
var partial = Pak("Partial", satisfied.Info.EffectivePluginDependencies[0], missing);
var error = Assert.Throws<InvalidOperationException>(() => DependencyValidator.Validate([satisfied, partial], [native]));
Assert.Equal("One mod requires a DLL plugin. Enable them in the Plugin Manager.\n\nPartial:\n\t- Other 2.0.0", error.Message);
Assert.Throws<InvalidOperationException>(() => DependencyValidator.Validate([satisfied], [native with { Version = "2.0.0" }]));
DependencyValidator.Validate([Pak("NoPlugins")], []);
}

[Fact]
public void LegacyModDependenciesAreReadAsModDependencies() {
var json = """
{"repo_url":"https://github.com/Test/Pak","name":"Pak","description":"Description",
"authors":["Author"],"image_urls":[],
"dependencies":[{"repo_url":"https://github.com/Test/OldPak","version":"1.0.0"}],
"mod_dependencies":[{"repo_url":"https://github.com/Test/NewPak","version":"2.0.0"}],
"plugin_dependencies":[{"repo_url":"https://github.com/Test/Native","version":"^1.0.0"}]}
""";
var info = JsonSerializer.Deserialize<ModInfo>(json)!;
Assert.Equal(2, info.EffectiveModDependencies.Count);
Assert.Equal("https://github.com/Test/Native", Assert.Single(info.EffectivePluginDependencies).RepoUrl);
var restored = JsonSerializer.Deserialize<ModInfo>(JsonSerializer.Serialize(info))!;
Assert.Equal(info.EffectiveModDependencies, restored.EffectiveModDependencies);
}

[Fact]
public void ModAndPluginCrossDependenciesRequireCorrectEnabledTypesAndVersions() {
var nativeDependency = new Dependency("https://github.com/Test/Native", ">=1.0.0,<2.0.0");
var pakInfo = new ModInfo("https://github.com/Test/Pak", "Pak", "Description", null, null, [], ["Author"],
ModDependencies: [], PluginDependencies: [nativeDependency]);
var pak = new Release("v1.0.0", "hash", "Pak.pak", DateTime.UtcNow, pakInfo, null!, null);
var nativeInfo = new PluginInfo("Native", "Description", ["Author"], [],
ModDependencies: [new Dependency("https://github.com/Test/Pak", "1.0.0")]);
var native = new DllRelease("Test", "Native", "1.0.0", "https://example.com/Native.dll", new string('a', 64), 1, nativeInfo);
DependencyValidator.Validate([pak], [native]);
Assert.Throws<InvalidOperationException>(() => DependencyValidator.Validate([pak], []));
Assert.Throws<InvalidOperationException>(() => DependencyValidator.Validate([], [native]));
Assert.Throws<InvalidOperationException>(() => DependencyValidator.Validate([pak], [native with { Version = "2.0.0" }]));
Assert.Throws<InvalidOperationException>(() => DependencyValidator.Validate([pak with { Tag = "v2.0.0" }], [native]));

var wrongType = native with { Info = nativeInfo with { ModDependencies = [nativeDependency] } };
Assert.Throws<InvalidOperationException>(() => DependencyValidator.Validate([pak], [wrongType]));
}

[Fact]
public void MissingModQueryReportsFirstRequirementInPluginAndDependencyOrder() {
var first = new DllRelease("Test", "First", "1.0.0", "https://example.com/First.dll", new string('a', 64), 1,
new PluginInfo("First", "Description", ["Author"], ModDependencies: [
new Dependency("https://github.com/Test/Existing", "1.0.0"),
new Dependency("https://github.com/Test/Missing", "2.0.0"),
new Dependency("https://github.com/Test/Other", "3.0.0")]));
var second = first with { Name = "Second" };
var error = Assert.Throws<InvalidOperationException>(() => DependencyValidator.Validate([Pak("Existing")], [first, second]));
Assert.Equal("Enable a compatible mod for Test/First: https://github.com/Test/Missing 2.0.0", error.Message);
DependencyValidator.Validate([], [first with { Info = null }]);
}
}
}
Loading
Loading