diff --git a/AM2R Atomic.sln b/AM2R Atomic.sln index e14fde4..b1773d3 100644 --- a/AM2R Atomic.sln +++ b/AM2R Atomic.sln @@ -13,6 +13,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Atomic.Gtk", "Atomic.Gtk\At EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Atomic.Mac", "Atomic.Mac\Atomic.Mac.csproj", "{A0481858-CA50-4DB8-A547-193BD1D57D21}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AtomicLibTests", "AtomicLibTests\AtomicLibTests.csproj", "{705598A0-F06A-432C-869E-A98D480161FD}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -39,6 +41,10 @@ Global {A0481858-CA50-4DB8-A547-193BD1D57D21}.Debug|Any CPU.Build.0 = Debug|Any CPU {A0481858-CA50-4DB8-A547-193BD1D57D21}.Release|Any CPU.ActiveCfg = Release|Any CPU {A0481858-CA50-4DB8-A547-193BD1D57D21}.Release|Any CPU.Build.0 = Release|Any CPU + {705598A0-F06A-432C-869E-A98D480161FD}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {705598A0-F06A-432C-869E-A98D480161FD}.Debug|Any CPU.Build.0 = Debug|Any CPU + {705598A0-F06A-432C-869E-A98D480161FD}.Release|Any CPU.ActiveCfg = Release|Any CPU + {705598A0-F06A-432C-869E-A98D480161FD}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/AtomicLib/Config.cs b/AtomicLib/Config.cs index 2c3081c..a39aeda 100644 --- a/AtomicLib/Config.cs +++ b/AtomicLib/Config.cs @@ -10,7 +10,7 @@ namespace AtomicLib; [XmlRoot("config")] public class Config { - public static string ConfigFilePath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), "Atomic", "config.xml"); + public static readonly string ConfigFilePath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData, Environment.SpecialFolderOption.Create), "Atomic", "config.xml"); /// /// Language used for the modpacker. diff --git a/AtomicLib/Core.cs b/AtomicLib/Core.cs index bba2420..12a8c54 100644 --- a/AtomicLib/Core.cs +++ b/AtomicLib/Core.cs @@ -35,6 +35,15 @@ public static class Core public static void CreateModPack(ModCreationInfo modInfo, string output) { + if (modInfo is null) + throw new NullReferenceException(nameof(modInfo)); + if (modInfo.Profile is null) + throw new NullReferenceException(nameof(modInfo.Profile)); + if (!File.Exists(modInfo.AM2R11Path)) + throw new FileNotFoundException("AM2R_11 file path could not be found!"); + if (modInfo.Profile.SupportsAndroid && !File.Exists(modInfo.ApkModPath)) + throw new FileNotFoundException("Android is marked as supported, but the APK path (" + modInfo.ApkModPath + ") could not be found!"); + ProfileOperatingSystems profileOS = Enum.Parse(modInfo.Profile.OperatingSystem); string modZipPath = profileOS switch { @@ -43,7 +52,10 @@ public static class Core ProfileOperatingSystems.Mac => modInfo.MacModPath, _ => throw new NotSupportedException("The current operating system is not supported!") }; - + + if (!File.Exists(modZipPath)) + throw new FileNotFoundException("The file path (" + modZipPath + ") for the OS (" + modInfo.Profile.OperatingSystem + ") could not be found!"); + // Cleanup in case of previous errors if (Directory.Exists($"{Path.GetTempPath()}/Atomic")) Directory.Delete($"{Path.GetTempPath()}/Atomic", true); @@ -55,6 +67,7 @@ public static class Core string tempProfilePath = Directory.CreateDirectory($"{tempPath}/profile").FullName; // Extract 1.1 and modded AM2R to their own directories in temp work + // We *probably* should check for 1.1 validity before extracting, *HOWEVER* that makes it kinda difficult to test against. ZipFile.ExtractToDirectory(modInfo.AM2R11Path, tempOriginalPath); ZipFile.ExtractToDirectory(modZipPath, tempModPath); @@ -65,7 +78,9 @@ public static class Core { case ProfileOperatingSystems.Windows: if (modInfo.Profile.UsesYYC) + { CreatePatch($"{tempOriginalPath}/data.win", $"{tempModPath}/AM2R.exe", $"{tempProfilePath}/AM2R.xdelta"); + } else { CreatePatch($"{tempOriginalPath}/data.win", $"{tempModPath}/data.win", $"{tempProfilePath}/data.xdelta"); @@ -95,7 +110,7 @@ public static class Core // Extract APK first in order to create patch from the data.win // - java -jar apktool.jar d "AM2RWrapper_old.apk" - RunJavaJar($"\"{localPath}/utilities/android/apktool.jar\" d -f -o \"{tempAndroid}\" \"{modInfo.ApkModPath}\"", tempAndroid); + RunJavaJar($"\"{localPath}/utilities/android/apktool.jar\" d -f -o \"{tempAndroid}\" \"{modInfo.ApkModPath}\""); // Create game.droid patch CreatePatch($"{tempOriginalPath}/data.win", $"{tempAndroid}/assets/game.droid", $"{tempProfilePath}/droid.xdelta"); @@ -200,7 +215,7 @@ public static class Core public static void RunJavaJar(string arguments = null, string workingDirectory = null) { - workingDirectory ??= Environment.GetFolderPath(Environment.SpecialFolder.UserProfile); + workingDirectory ??= Directory.GetCurrentDirectory(); string proc = "", javaArgs = ""; diff --git a/AtomicLib/OS.cs b/AtomicLib/OS.cs index 3e15205..de6ec89 100644 --- a/AtomicLib/OS.cs +++ b/AtomicLib/OS.cs @@ -74,7 +74,7 @@ public static class OS /// /// Checks if the Launcher is ran from a Flatpak. /// - /// see langword="true"/> if run from a Flatpak, if not. + /// if run from a Flatpak, if not. private static bool CheckIfRunFromFlatpak() { if (!IsLinux) return false; diff --git a/AtomicLibTests/AtomicLibTests.csproj b/AtomicLibTests/AtomicLibTests.csproj new file mode 100644 index 0000000..c43837f --- /dev/null +++ b/AtomicLibTests/AtomicLibTests.csproj @@ -0,0 +1,47 @@ + + + + net6.0 + enable + enable + + false + + + + + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + + + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + + + + + + diff --git a/AtomicLibTests/CoreTests.cs b/AtomicLibTests/CoreTests.cs new file mode 100644 index 0000000..db6aefc --- /dev/null +++ b/AtomicLibTests/CoreTests.cs @@ -0,0 +1,211 @@ +using System.IO.Compression; +using System.Xml.Serialization; +using AtomicLib; +using AtomicLib.XML; +using Xunit; +using Xunit.Abstractions; +using Xunit.Sdk; + +namespace AtomicLibTests; + +public class CoreTests : IDisposable +{ + private string am2r_11Path; + private ITestOutputHelper console; + private string testTempDir; + + public CoreTests(ITestOutputHelper output) + { + console = output; + testTempDir = Path.GetTempPath() + Guid.NewGuid() + "/"; + // TODO: use a custom "AM2R_11" which is just a modified am2r_sever. the backend doesnt check for 1.1 validity, so we can do that. + Directory.CreateDirectory(testTempDir); + var am2rEnvVar = Environment.GetEnvironmentVariable("AM2R_11PATH"); + if (am2rEnvVar is not null && File.Exists(am2rEnvVar)) + { + am2r_11Path = am2rEnvVar; + return; + } + + if (File.Exists("AM2R_11.zip")) + { + am2r_11Path = "AM2R_11.zip"; + return; + } + + Assert.Fail("AM2R 1.1 file could not be found! Please place it into PWD or provide it via the AM2R_11PATH environment variable."); + } + + public void Dispose() + { + Directory.Delete(testTempDir, true); + } + + [Fact] + public void CreateModPack_ShouldThrowWithNullModInfo() + { + Assert.Throws(() => Core.CreateModPack(null, testTempDir + "foo.zip")); + } + + [Fact] + public void CreateModPack_ShouldThrowWithNullModInfoProfile() + { + var modInfo = new ModCreationInfo(); + modInfo.Profile = null; + Assert.Throws(() => Core.CreateModPack(modInfo, testTempDir + "foo.zip")); + } + + [Fact] + public void CreateModPack_ShouldThrowWithAM2R11PathNotSet() + { + var modInfo = new ModCreationInfo(); + modInfo.Profile = new ModProfileXML(); + modInfo.Profile.OperatingSystem = "Windows"; + + Assert.Throws(() => Core.CreateModPack(modInfo, testTempDir + "foo.zip")); + } + + [Fact] + public void CreateModPack_ShouldThrowWithUnknownProfileOS() + { + var modInfo = new ModCreationInfo(); + modInfo.Profile = new ModProfileXML(); + modInfo.Profile.OperatingSystem = "asdfasdf"; + modInfo.AM2R11Path = am2r_11Path; + + Assert.Throws(() => Core.CreateModPack(modInfo, testTempDir + "foo.zip")); + } + + [Fact] + public void CreateModPack_ShouldThrowWhenProfileSetButNotOSpath() + { + var modInfo = new ModCreationInfo(); + modInfo.Profile = new ModProfileXML(); + modInfo.Profile.OperatingSystem = "Windows"; + modInfo.AM2R11Path = am2r_11Path; + + Assert.Throws(() => Core.CreateModPack(modInfo, testTempDir + "foo.zip")); + } + + [Fact] + public void CreateModPack_ShouldThrowWhenAndroidIsMarkedAsSupportedButPathDoesNotExist() + { + var modInfo = new ModCreationInfo(); + modInfo.Profile = new ModProfileXML(); + modInfo.Profile.OperatingSystem = "Windows"; + modInfo.Profile.SupportsAndroid = true; + modInfo.AM2R11Path = am2r_11Path; + + Assert.Throws(() => Core.CreateModPack(modInfo, testTempDir + "foo.zip")); + } + + [Theory] + [InlineData("Windows", false, false, false)] + [InlineData("Windows", false, false, true)] + [InlineData("Windows", false, true, false)] + [InlineData("Windows", false, true, true)] + [InlineData("Windows", true, false, false)] + [InlineData("Windows", true, false, true)] + [InlineData("Windows", true, true, false)] + [InlineData("Windows", true, true, true)] + [InlineData("Linux", false, false, false)] + [InlineData("Linux", false, false, true)] + [InlineData("Linux", false, true, false)] + [InlineData("Linux", false, true, true)] + [InlineData("Linux", true, false, false)] + [InlineData("Linux", true, false, true)] + [InlineData("Linux", true, true, false)] + [InlineData("Linux", true, true, true)] + [InlineData("Mac", false, false, false)] + [InlineData("Mac", false, false, true)] + [InlineData("Mac", false, true, false)] + [InlineData("Mac", false, true, true)] + [InlineData("Mac", true, false, false)] + [InlineData("Mac", true, false, true)] + [InlineData("Mac", true, true, false)] + [InlineData("Mac", true, true, true)] + public void CreateModPack_AllOptionsShouldCauseValidModpacks(string operatingSystem, bool usesCustomMusic, bool supportsAndroid, bool isYYC) + { + var modInfo = new ModCreationInfo(); + modInfo.Profile = new ModProfileXML(); + modInfo.Profile.OperatingSystem = operatingSystem; + modInfo.Profile.UsesCustomMusic = usesCustomMusic; + modInfo.Profile.SupportsAndroid = supportsAndroid; + if (supportsAndroid) + modInfo.ApkModPath = "GameAndroid.apk"; + modInfo.Profile.UsesYYC = isYYC; + modInfo.Profile.Name = "Cool Mod"; + modInfo.Profile.Version = "cool version"; + modInfo.Profile.ProfileNotes = "This is my very own cool mod"; + modInfo.AM2R11Path = am2r_11Path; + switch (operatingSystem) + { + case "Windows": modInfo.WindowsModPath = "GameWin.zip"; break; + case "Linux": modInfo.LinuxModPath = "GameLin.zip"; break; + case "Mac": modInfo.MacModPath = "GameMac.zip"; break; + default: Assert.Fail(nameof(CreateModPack_AllOptionsShouldCauseValidModpacks) + " was called with improper parameters?"); break; + } + + Core.CreateModPack(modInfo, testTempDir + "foo.zip"); + + // TODO: assert on proper packaging, by investigating contents of zip + ZipArchive archive = ZipFile.OpenRead(testTempDir + "foo.zip"); + Assert.True(archive.Entries.FirstOrDefault(f => f.FullName == "profile.xml") is not null); + // TODO: try to deserialize xml to check it has what we put in + if (isYYC) + { + if (operatingSystem == "Windows") + { + Assert.True(archive.Entries.FirstOrDefault(f => f.FullName == "AM2R.xdelta") is not null); + Assert.True(archive.Entries.FirstOrDefault(f => f.FullName == "game.xdelta") is null); + } + } + else + { + // Unix has both in YYC and non-YYC + Assert.True(archive.Entries.FirstOrDefault(f => f.FullName == "AM2R.xdelta") is not null); + if (operatingSystem == "Windows") + Assert.True(archive.Entries.FirstOrDefault(f => f.FullName == "data.xdelta") is not null); + else + Assert.True(archive.Entries.FirstOrDefault(f => f.FullName == "game.xdelta") is not null); + } + + if (supportsAndroid) + { + Assert.True(archive.Entries.FirstOrDefault(f => f.FullName == "droid.xdelta") is not null); + Assert.True(archive.Entries.FirstOrDefault(f => f.FullName == "android/AM2RWrapper.apk") is not null); + if (isYYC) + Assert.True(archive.Entries.FirstOrDefault(f => f.FullName == "android/AM2R.ini") is not null); + // TODO: atomic currently always copies the file if it exists. Should it only copy it if we're dealing with yyc? + //else + //Assert.True(archive.Entries.FirstOrDefault(f => f.FullName == "android/AM2R.ini") is null); + } + + if (operatingSystem is "Linux" or "Mac") + { + Assert.True(archive.Entries.FirstOrDefault(f => f.FullName == "files_to_copy/icon.png") is not null); + Assert.True(archive.Entries.FirstOrDefault(f => f.FullName == "files_to_copy/splash.png") is not null); + if (operatingSystem == "Mac") + { + Assert.True(archive.Entries.FirstOrDefault(f => f.FullName == "Info.plist") is not null); + Assert.True(archive.Entries.FirstOrDefault(f => f.FullName == "files_to_copy/yoyorunner.config") is not null); + Assert.True(archive.Entries.FirstOrDefault(f => f.FullName == "files_to_copy/gamecontrollerdb.txt") is not null); + Assert.True(archive.Entries.FirstOrDefault(f => f.FullName == "files_to_copy/english.lproj/MainMenu.nib") is not null); + } + } + else + { + Assert.True(archive.Entries.FirstOrDefault(f => f.FullName == "files_to_copy/icon.png") is null); + Assert.True(archive.Entries.FirstOrDefault(f => f.FullName == "files_to_copy/splash.png") is null); + } + + if (usesCustomMusic) + { + // TODO: check for custom music. do that after we provided a fake am2r_11. + } + else + { + // TODO: make sure that if we're providing lowercase songs, that they'll be also blacklisted. + } + } +} \ No newline at end of file diff --git a/AtomicLibTests/GameAndroid.apk b/AtomicLibTests/GameAndroid.apk new file mode 100644 index 0000000..34317b9 Binary files /dev/null and b/AtomicLibTests/GameAndroid.apk differ diff --git a/AtomicLibTests/GameLin.zip b/AtomicLibTests/GameLin.zip new file mode 100644 index 0000000..aa174d3 Binary files /dev/null and b/AtomicLibTests/GameLin.zip differ diff --git a/AtomicLibTests/GameMac.zip b/AtomicLibTests/GameMac.zip new file mode 100644 index 0000000..687235e Binary files /dev/null and b/AtomicLibTests/GameMac.zip differ diff --git a/AtomicLibTests/GameWin.zip b/AtomicLibTests/GameWin.zip new file mode 100644 index 0000000..d3b79a9 Binary files /dev/null and b/AtomicLibTests/GameWin.zip differ diff --git a/AtomicLibTests/LICENSE-AM2RServer.txt b/AtomicLibTests/LICENSE-AM2RServer.txt new file mode 100644 index 0000000..49043c9 --- /dev/null +++ b/AtomicLibTests/LICENSE-AM2RServer.txt @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2022 milesthenerd + +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. \ No newline at end of file