ShipModCatalog.RemovePackage: the one uninstall moves a mod aside to GameData/RemovedMods
Presence under Mods is the only enablement; removal is a move, never a delete, and moving a package back reinstalls it. Refuses names that are not a package directory directly under the mods root. Not yet built or run; verify runs at the end of the cut. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
3f29d803eb
commit
e04e247820
3 changed files with 70 additions and 0 deletions
1
.gitignore
vendored
1
.gitignore
vendored
|
|
@ -94,6 +94,7 @@ crashlytics-build.properties
|
|||
Packages/packages-lock.json
|
||||
GameData/run.cc
|
||||
GameData/player.cc
|
||||
GameData/RemovedMods/
|
||||
Vault/Aetheria/.obsidian/plugins/copilot/data.json
|
||||
|
||||
# Agent scratch, sandboxed toolchain caches, and throwaway checkouts. These had
|
||||
|
|
|
|||
|
|
@ -189,6 +189,28 @@ public static class ShipModCatalog
|
|||
return (composition.Included.Length == 0 ? shippedCatalog : derivedCatalog, composition.Excluded);
|
||||
}
|
||||
|
||||
// The one uninstall: presence under Mods is the only enablement (MR1), so a package leaves by moving, never by deletion,
|
||||
// to <parent of modsRoot>/RemovedMods/<package> ('<package> (2)' and on when taken). Moving it back reinstalls it.
|
||||
// Returns the destination. Compose and ResolveCatalog never read RemovedMods.
|
||||
public static string RemovePackage(string modsRoot, string package)
|
||||
{
|
||||
if (string.IsNullOrEmpty(package) || package == "." || package == ".." || package == "(all mods)" ||
|
||||
package != Path.GetFileName(package) || package.IndexOfAny(Path.GetInvalidFileNameChars()) >= 0)
|
||||
throw new ArgumentException("Not a mod package name.", nameof(package));
|
||||
var source = Path.Combine(Path.GetFullPath(modsRoot), package);
|
||||
if (!Directory.Exists(source)) throw new DirectoryNotFoundException("The package is not a directory under the mods folder.");
|
||||
var removed = RemovedRoot(modsRoot);
|
||||
Directory.CreateDirectory(removed);
|
||||
var destination = Path.Combine(removed, package);
|
||||
for (var copy = 2; Directory.Exists(destination) || File.Exists(destination); copy++)
|
||||
destination = Path.Combine(removed, $"{package} ({copy})");
|
||||
Directory.Move(source, destination);
|
||||
return destination;
|
||||
}
|
||||
|
||||
private static string RemovedRoot(string modsRoot) =>
|
||||
Path.Combine(Path.GetDirectoryName(Path.GetFullPath(modsRoot)), "RemovedMods");
|
||||
|
||||
private static Package Bind(HullData hull, ShipAuthoring ship, FactionProductData[] products, string directory, string path)
|
||||
{
|
||||
if (!string.Equals(Path.GetFileName(directory), ship.Id, StringComparison.Ordinal))
|
||||
|
|
|
|||
|
|
@ -582,6 +582,53 @@ public sealed class ShipModCatalogTests : IDisposable
|
|||
Assert.Contains(ShipModCatalog.HullKey("mod.c").Value, Records(Derived).Keys);
|
||||
}
|
||||
|
||||
private string RemovedMods => Path.Combine(_directory.Path, "RemovedMods");
|
||||
|
||||
[Fact]
|
||||
public void RemovePackageMovesItAsideAndTheNextBootDoesNotLoadIt()
|
||||
{
|
||||
WritePackage("mod.skiff");
|
||||
var before = (Ship: Hash(Path.Combine(Mods, "mod.skiff", "ship.cc")), Glb: Hash(Path.Combine(Mods, "mod.skiff", "skiff.glb")));
|
||||
Assert.Equal(Derived, ShipModCatalog.ResolveCatalog(Shipped, Derived, Mods).Catalog);
|
||||
|
||||
var destination = ShipModCatalog.RemovePackage(Mods, "mod.skiff");
|
||||
|
||||
Assert.Equal(Path.Combine(RemovedMods, "mod.skiff"), destination);
|
||||
Assert.False(Directory.Exists(Path.Combine(Mods, "mod.skiff")));
|
||||
Assert.Equal(before, (Hash(Path.Combine(destination, "ship.cc")), Hash(Path.Combine(destination, "skiff.glb"))));
|
||||
Assert.Equal(Shipped, ShipModCatalog.ResolveCatalog(Shipped, Derived, Mods).Catalog);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void RemovingAPackageTwiceKeepsBothCopies()
|
||||
{
|
||||
WritePackage("mod.skiff");
|
||||
ShipModCatalog.RemovePackage(Mods, "mod.skiff");
|
||||
File.WriteAllText(Path.Combine(RemovedMods, "mod.skiff", "marker.txt"), "first");
|
||||
WritePackage("mod.skiff");
|
||||
var second = ShipModCatalog.RemovePackage(Mods, "mod.skiff");
|
||||
|
||||
Assert.Equal(Path.Combine(RemovedMods, "mod.skiff (2)"), second);
|
||||
Assert.Equal("first", File.ReadAllText(Path.Combine(RemovedMods, "mod.skiff", "marker.txt")));
|
||||
Assert.False(File.Exists(Path.Combine(second, "marker.txt")));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void RemovePackageRefusesWhatIsNotAPackageDirectory()
|
||||
{
|
||||
WritePackage("mod.skiff");
|
||||
File.WriteAllText(Path.Combine(_directory.Path, "outside.txt"), "x");
|
||||
Directory.CreateDirectory(Path.Combine(_directory.Path, "Mods2"));
|
||||
foreach (var name in new[] { "", ".", "..", "(all mods)", "a/b", "a\b", "../Mods", "missing" })
|
||||
Assert.ThrowsAny<Exception>(() => ShipModCatalog.RemovePackage(Mods, name));
|
||||
Assert.True(Directory.Exists(Mods));
|
||||
Assert.True(Directory.Exists(Path.Combine(Mods, "mod.skiff")));
|
||||
Assert.True(Directory.Exists(Path.Combine(_directory.Path, "Mods2")));
|
||||
Assert.True(File.Exists(Path.Combine(_directory.Path, "outside.txt")));
|
||||
Assert.False(Directory.Exists(RemovedMods));
|
||||
Assert.Equal(new[] { "mod.skiff" }, Directory.GetDirectories(Mods).Select(Path.GetFileName));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TheBootCatalogIsTheShippedOneUntilAModIsInstalled()
|
||||
{
|
||||
|
|
|
|||
Loading…
Reference in a new issue