diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml
index 53b53ad..9267718 100644
--- a/.github/workflows/ci.yaml
+++ b/.github/workflows/ci.yaml
@@ -8,27 +8,49 @@ on:
- main
jobs:
build:
+ strategy:
+ matrix:
+ configuration: [Release]
+ platform: [x64]
runs-on: windows-latest
permissions:
packages: read
steps:
- - uses: actions/checkout@v3
- - uses: actions/setup-dotnet@v2
- with:
- dotnet-version: '6.0.x'
- - if: ${{ github.ref == 'refs/heads/main' }}
- run: |
- dotnet nuget add source --username USERNAME --password ${{ secrets.GITHUB_TOKEN }} --store-password-in-clear-text --name github "https://nuget.pkg.github.com/OpenSimTools/index.json"
- dotnet publish -c Release
- - if: ${{ github.ref == 'refs/heads/main' }}
- uses: actions/upload-artifact@v3
- with:
- name: AMS2CM
- path: src/CLI/bin/Release/net6.0-windows/publish/**
- if-no-files-found: error
- - if: ${{ github.ref == 'refs/heads/main' }}
- uses: actions/upload-artifact@v3
- with:
- name: AMS2CM
- path: LICENSE
- if-no-files-found: error
+ - name: Checkout
+ uses: actions/checkout@v2
+ with:
+ fetch-depth: 0
+
+ - name: Install .NET Core
+ uses: actions/setup-dotnet@v1
+ with:
+ dotnet-version: 6.0.x
+ - name: Setup MSBuild
+ uses: microsoft/setup-msbuild@v1.0.2
+ - name: Configure NuGet repository
+ run: dotnet nuget add source --username USERNAME --password ${{ secrets.GITHUB_TOKEN }} --store-password-in-clear-text --name github "https://nuget.pkg.github.com/OpenSimTools/index.json"
+
+ - name: Build
+ run: |
+ msbuild /t:Restore /p:Configuration=${{ matrix.configuration }}
+ msbuild /t:Publish /p:Configuration=${{ matrix.configuration }} /p:Platform=${{ matrix.platform }}
+
+ - name: Upload license
+ uses: actions/upload-artifact@v3
+ with:
+ name: AMS2CM
+ path: LICENSE
+ if-no-files-found: error
+ - name: Upload CLI
+ uses: actions/upload-artifact@v3
+ with:
+ name: AMS2CM
+ path: src/CLI/bin/${{ matrix.configuration }}/net6.0-windows/publish/**
+ if-no-files-found: error
+ # Separate package until stable, to be able to release CLI independently
+ - name: Upload GUI
+ uses: actions/upload-artifact@v3
+ with:
+ name: AMS2CM GUI
+ path: src/GUI/bin/${{ matrix.platform }}/${{ matrix.configuration }}/net6.0-windows10.0.19041.0/**
+ if-no-files-found: error
diff --git a/AMS2CM.sln b/AMS2CM.sln
index d72fddf..8397dc8 100644
--- a/AMS2CM.sln
+++ b/AMS2CM.sln
@@ -1,22 +1,87 @@
Microsoft Visual Studio Solution File, Format Version 12.00
-Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CLI", "src\CLI\CLI.csproj", "{77400FE1-383E-4D92-9A7F-D4AEA8A1AE0C}"
+# Visual Studio Version 17
+VisualStudioVersion = 17.5.33530.505
+MinimumVisualStudioVersion = 10.0.40219.1
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CLI", "src\CLI\CLI.csproj", "{77400FE1-383E-4D92-9A7F-D4AEA8A1AE0C}"
EndProject
-Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Core", "src\Core\Core.csproj", "{A20048F0-D212-499D-8CCF-5E0B989E21F7}"
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Core", "src\Core\Core.csproj", "{A20048F0-D212-499D-8CCF-5E0B989E21F7}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "GUI", "src\GUI\GUI.csproj", "{2C96062E-2EDF-4BBE-8BC2-968B7A1F4EFE}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
+ Debug|ARM64 = Debug|ARM64
+ Debug|x64 = Debug|x64
+ Debug|x86 = Debug|x86
Release|Any CPU = Release|Any CPU
+ Release|ARM64 = Release|ARM64
+ Release|x64 = Release|x64
+ Release|x86 = Release|x86
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{77400FE1-383E-4D92-9A7F-D4AEA8A1AE0C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{77400FE1-383E-4D92-9A7F-D4AEA8A1AE0C}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {77400FE1-383E-4D92-9A7F-D4AEA8A1AE0C}.Debug|ARM64.ActiveCfg = Debug|Any CPU
+ {77400FE1-383E-4D92-9A7F-D4AEA8A1AE0C}.Debug|ARM64.Build.0 = Debug|Any CPU
+ {77400FE1-383E-4D92-9A7F-D4AEA8A1AE0C}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {77400FE1-383E-4D92-9A7F-D4AEA8A1AE0C}.Debug|x64.Build.0 = Debug|Any CPU
+ {77400FE1-383E-4D92-9A7F-D4AEA8A1AE0C}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {77400FE1-383E-4D92-9A7F-D4AEA8A1AE0C}.Debug|x86.Build.0 = Debug|Any CPU
{77400FE1-383E-4D92-9A7F-D4AEA8A1AE0C}.Release|Any CPU.ActiveCfg = Release|Any CPU
{77400FE1-383E-4D92-9A7F-D4AEA8A1AE0C}.Release|Any CPU.Build.0 = Release|Any CPU
+ {77400FE1-383E-4D92-9A7F-D4AEA8A1AE0C}.Release|ARM64.ActiveCfg = Release|Any CPU
+ {77400FE1-383E-4D92-9A7F-D4AEA8A1AE0C}.Release|ARM64.Build.0 = Release|Any CPU
+ {77400FE1-383E-4D92-9A7F-D4AEA8A1AE0C}.Release|x64.ActiveCfg = Release|Any CPU
+ {77400FE1-383E-4D92-9A7F-D4AEA8A1AE0C}.Release|x64.Build.0 = Release|Any CPU
+ {77400FE1-383E-4D92-9A7F-D4AEA8A1AE0C}.Release|x86.ActiveCfg = Release|Any CPU
+ {77400FE1-383E-4D92-9A7F-D4AEA8A1AE0C}.Release|x86.Build.0 = Release|Any CPU
{A20048F0-D212-499D-8CCF-5E0B989E21F7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{A20048F0-D212-499D-8CCF-5E0B989E21F7}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {A20048F0-D212-499D-8CCF-5E0B989E21F7}.Debug|ARM64.ActiveCfg = Debug|Any CPU
+ {A20048F0-D212-499D-8CCF-5E0B989E21F7}.Debug|ARM64.Build.0 = Debug|Any CPU
+ {A20048F0-D212-499D-8CCF-5E0B989E21F7}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {A20048F0-D212-499D-8CCF-5E0B989E21F7}.Debug|x64.Build.0 = Debug|Any CPU
+ {A20048F0-D212-499D-8CCF-5E0B989E21F7}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {A20048F0-D212-499D-8CCF-5E0B989E21F7}.Debug|x86.Build.0 = Debug|Any CPU
{A20048F0-D212-499D-8CCF-5E0B989E21F7}.Release|Any CPU.ActiveCfg = Release|Any CPU
{A20048F0-D212-499D-8CCF-5E0B989E21F7}.Release|Any CPU.Build.0 = Release|Any CPU
+ {A20048F0-D212-499D-8CCF-5E0B989E21F7}.Release|ARM64.ActiveCfg = Release|Any CPU
+ {A20048F0-D212-499D-8CCF-5E0B989E21F7}.Release|ARM64.Build.0 = Release|Any CPU
+ {A20048F0-D212-499D-8CCF-5E0B989E21F7}.Release|x64.ActiveCfg = Release|Any CPU
+ {A20048F0-D212-499D-8CCF-5E0B989E21F7}.Release|x64.Build.0 = Release|Any CPU
+ {A20048F0-D212-499D-8CCF-5E0B989E21F7}.Release|x86.ActiveCfg = Release|Any CPU
+ {A20048F0-D212-499D-8CCF-5E0B989E21F7}.Release|x86.Build.0 = Release|Any CPU
+ {2C96062E-2EDF-4BBE-8BC2-968B7A1F4EFE}.Debug|Any CPU.ActiveCfg = Debug|x64
+ {2C96062E-2EDF-4BBE-8BC2-968B7A1F4EFE}.Debug|Any CPU.Build.0 = Debug|x64
+ {2C96062E-2EDF-4BBE-8BC2-968B7A1F4EFE}.Debug|Any CPU.Deploy.0 = Debug|x64
+ {2C96062E-2EDF-4BBE-8BC2-968B7A1F4EFE}.Debug|ARM64.ActiveCfg = Debug|ARM64
+ {2C96062E-2EDF-4BBE-8BC2-968B7A1F4EFE}.Debug|ARM64.Build.0 = Debug|ARM64
+ {2C96062E-2EDF-4BBE-8BC2-968B7A1F4EFE}.Debug|ARM64.Deploy.0 = Debug|ARM64
+ {2C96062E-2EDF-4BBE-8BC2-968B7A1F4EFE}.Debug|x64.ActiveCfg = Debug|x64
+ {2C96062E-2EDF-4BBE-8BC2-968B7A1F4EFE}.Debug|x64.Build.0 = Debug|x64
+ {2C96062E-2EDF-4BBE-8BC2-968B7A1F4EFE}.Debug|x64.Deploy.0 = Debug|x64
+ {2C96062E-2EDF-4BBE-8BC2-968B7A1F4EFE}.Debug|x86.ActiveCfg = Debug|x86
+ {2C96062E-2EDF-4BBE-8BC2-968B7A1F4EFE}.Debug|x86.Build.0 = Debug|x86
+ {2C96062E-2EDF-4BBE-8BC2-968B7A1F4EFE}.Debug|x86.Deploy.0 = Debug|x86
+ {2C96062E-2EDF-4BBE-8BC2-968B7A1F4EFE}.Release|Any CPU.ActiveCfg = Release|x64
+ {2C96062E-2EDF-4BBE-8BC2-968B7A1F4EFE}.Release|Any CPU.Build.0 = Release|x64
+ {2C96062E-2EDF-4BBE-8BC2-968B7A1F4EFE}.Release|Any CPU.Deploy.0 = Release|x64
+ {2C96062E-2EDF-4BBE-8BC2-968B7A1F4EFE}.Release|ARM64.ActiveCfg = Release|ARM64
+ {2C96062E-2EDF-4BBE-8BC2-968B7A1F4EFE}.Release|ARM64.Build.0 = Release|ARM64
+ {2C96062E-2EDF-4BBE-8BC2-968B7A1F4EFE}.Release|ARM64.Deploy.0 = Release|ARM64
+ {2C96062E-2EDF-4BBE-8BC2-968B7A1F4EFE}.Release|x64.ActiveCfg = Release|x64
+ {2C96062E-2EDF-4BBE-8BC2-968B7A1F4EFE}.Release|x64.Build.0 = Release|x64
+ {2C96062E-2EDF-4BBE-8BC2-968B7A1F4EFE}.Release|x64.Deploy.0 = Release|x64
+ {2C96062E-2EDF-4BBE-8BC2-968B7A1F4EFE}.Release|x86.ActiveCfg = Release|x86
+ {2C96062E-2EDF-4BBE-8BC2-968B7A1F4EFE}.Release|x86.Build.0 = Release|x86
+ {2C96062E-2EDF-4BBE-8BC2-968B7A1F4EFE}.Release|x86.Deploy.0 = Release|x86
+ EndGlobalSection
+ GlobalSection(SolutionProperties) = preSolution
+ HideSolutionNode = FALSE
+ EndGlobalSection
+ GlobalSection(ExtensibilityGlobals) = postSolution
+ SolutionGuid = {6C69475D-BCFD-4B42-A90D-845A72BFB1F8}
EndGlobalSection
EndGlobal
diff --git a/LICENSE b/LICENSE
index 653deb4..7eb47e5 100644
--- a/LICENSE
+++ b/LICENSE
@@ -1,4 +1,4 @@
-Copyright (c) 2023 OpenSimTools Contributors
+Copyright (c) 2023 Open Sim Tools
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
diff --git a/src/CLI/CLI.csproj b/src/CLI/CLI.csproj
index e70b46c..6c0c32a 100644
--- a/src/CLI/CLI.csproj
+++ b/src/CLI/CLI.csproj
@@ -1,31 +1,23 @@
+
+ Exe
+ net6.0-windows
+ enable
+ enable
+ OpenSimTools
+ OpenSimTools Contributors
+ AMS2CM
+ AMS2CM.CLI
+ ..\Shared\AMS2CM.ico
+
-
- Exe
- net6.0-windows
- enable
- enable
- OpenSimTools
- OpenSimTools Contributors
- AMS2CM
- AMS2CM.CLI
- AMS2CM.ico
-
-
-
-
-
-
-
-
-
+
-
+
-
+
Always
-
diff --git a/src/CLI/Config.cs b/src/Core/Config.cs
similarity index 100%
rename from src/CLI/Config.cs
rename to src/Core/Config.cs
diff --git a/src/Core/Core.csproj b/src/Core/Core.csproj
index fb34204..754be5b 100644
--- a/src/Core/Core.csproj
+++ b/src/Core/Core.csproj
@@ -1,5 +1,4 @@
-
net6.0-windows
enable
@@ -17,4 +16,9 @@
+
+
+
+
+
diff --git a/src/Core/ModManager.cs b/src/Core/ModManager.cs
index 4e6eadf..e0f15d4 100644
--- a/src/Core/ModManager.cs
+++ b/src/Core/ModManager.cs
@@ -1,4 +1,4 @@
-using Core.Games;
+using Core.Games;
using Core.Mods;
using Newtonsoft.Json;
using SevenZipExtractor;
@@ -8,7 +8,8 @@ namespace Core;
public class ModManager
{
private record WorkPaths(
- string ModArchivesDir,
+ string EnabledModArchivesDir,
+ string DisabledModArchivesDir,
string TempDir,
string CurrentStateFile
);
@@ -20,6 +21,7 @@ string CurrentStateFile
private const string ModsDirName = "Mods";
private const string EnabledModsDirName = "Enabled";
+ private const string DisabledModsSubdir = "Disabled";
private const string TempDirName = "Temp";
private const string CurrentStateFileName = "installed.json";
@@ -33,11 +35,13 @@ string CurrentStateFile
public ModManager(IGame game, IModFactory modFactory)
{
+
this.game = game;
this.modFactory = modFactory;
var modsDir = Path.Combine(game.InstallationDirectory, ModsDirName);
workPaths = new WorkPaths(
- ModArchivesDir: Path.Combine(modsDir, EnabledModsDirName),
+ EnabledModArchivesDir: Path.Combine(modsDir, EnabledModsDirName),
+ DisabledModArchivesDir: Path.Combine(modsDir, DisabledModsSubdir),
TempDir: Path.Combine(modsDir, TempDirName),
CurrentStateFile: Path.Combine(modsDir, CurrentStateFileName)
);
@@ -54,6 +58,75 @@ private static void AddToEnvionmentPath(string additionalPath)
Environment.SetEnvironmentVariable(pathEnvVar, $"{env};{additionalPath}");
}
+ public List FetchState()
+ {
+ var installedPackageNames = ReadPreviouslyInstalledFiles().Keys.Where(_ => !IsBootFiles(_)).ToHashSet();
+ var enabledTuples = ListEnabledModPackages().Select(_ => (_, true));
+ var disabledTuples = ListDisabledModPackages().Select(_ => (_, false));
+ var availableTuples = enabledTuples.Concat(disabledTuples);
+ var availableModsState = availableTuples.Select(_ =>
+ {
+ var packagePath = _._;
+ var isEnabled = _.Item2;
+ var packageName = PackageName(packagePath);
+ return new ModState(
+ PackageName: packageName,
+ PackagePath: packagePath,
+ IsEnabled: isEnabled,
+ IsInstalled: installedPackageNames.Contains(packageName)
+ );
+ });
+ var availablePackageNames = availableModsState.Select(_ => _.PackageName);
+ var unavailablePackageNames = installedPackageNames.Except(availablePackageNames);
+ var unavailableModsState = unavailablePackageNames.Select(packageName => new ModState(
+ PackageName: packageName,
+ PackagePath: null,
+ IsEnabled: null,
+ IsInstalled: true
+ ));
+ return unavailableModsState.Concat(availableModsState).ToList();
+ }
+
+ public ModState EnableNewMod(string packagePath)
+ {
+ var destinationDirectoryPath = workPaths.EnabledModArchivesDir;
+ ExistingDirectoryOrCreate(destinationDirectoryPath);
+ var destinationFilePath = Path.Combine(destinationDirectoryPath, Path.GetFileName(packagePath));
+ File.Copy(packagePath, destinationFilePath);
+ return new ModState(
+ PackageName: PackageName(destinationFilePath),
+ PackagePath: destinationFilePath,
+ IsEnabled: true,
+ IsInstalled: false
+ );
+ }
+
+ public string EnableMod(string packagePath)
+ {
+ return MoveMod(packagePath, workPaths.EnabledModArchivesDir);
+ }
+
+ public string DisableMod(string packagePath)
+ {
+ return MoveMod(packagePath, workPaths.DisabledModArchivesDir);
+ }
+
+ private string MoveMod(string packagePath, string destinationDirectoryPath)
+ {
+ ExistingDirectoryOrCreate(destinationDirectoryPath);
+ var destinationFilePath = Path.Combine(destinationDirectoryPath, Path.GetFileName(packagePath));
+ File.Move(packagePath, destinationFilePath);
+ return destinationFilePath;
+ }
+
+ private static void ExistingDirectoryOrCreate(string directoryPath)
+ {
+ if (!Directory.Exists(directoryPath))
+ {
+ Directory.CreateDirectory(directoryPath);
+ }
+ }
+
public void InstallEnabledMods()
{
// It shoulnd't be needed, but some systems seem to want to load oo2core
@@ -112,27 +185,18 @@ private void CheckNoBootfilesInstalled()
private void InstallAllModFiles()
{
- if (!Directory.Exists(workPaths.ModArchivesDir))
- {
- Console.WriteLine($"No mod archives found in {workPaths.ModArchivesDir}");
- return;
- }
+ var modPackages = ListEnabledModPackages();
var modConfigs = new List();
- var modArchives = Directory.EnumerateFiles(workPaths.ModArchivesDir).ToList();
var installedFilesByMod = new Dictionary>();
try
{
- if (!modArchives.Any())
- {
- Console.WriteLine($"No mod archives found in {workPaths.ModArchivesDir}");
- }
- else
+ if (modPackages.Any())
{
Console.WriteLine("Installing mods:");
- foreach (var archivePath in modArchives)
+ foreach (var packagePath in modPackages)
{
- var packageName = Path.GetFileNameWithoutExtension(archivePath);
- if (packageName.StartsWith(BootfilesPrefix))
+ var packageName = Path.GetFileNameWithoutExtension(packagePath);
+ if (IsBootFiles(packageName))
{
Console.WriteLine($"- {packageName} (skipped)");
continue;
@@ -140,7 +204,7 @@ private void InstallAllModFiles()
Console.WriteLine($"- {packageName}");
- var mod = ExtractMod(packageName, archivePath);
+ var mod = ExtractMod(packageName, packagePath);
try
{
mod.Install(game.InstallationDirectory);
@@ -173,6 +237,10 @@ private void InstallAllModFiles()
Console.WriteLine("Post-processing not required");
}
}
+ else
+ {
+ Console.WriteLine($"No mod archives found in {workPaths.EnabledModArchivesDir}");
+ }
}
finally
{
@@ -180,6 +248,8 @@ private void InstallAllModFiles()
}
}
+ private bool IsBootFiles(string packageName) => packageName.StartsWith(BootfilesPrefix);
+
private IMod ExtractMod(string packageName, string archivePath)
{
var extractionDir = Path.Combine(workPaths.TempDir, packageName);
@@ -191,7 +261,7 @@ private IMod ExtractMod(string packageName, string archivePath)
private IMod BootfilesMod()
{
- var bootfilesArchives = Directory.EnumerateFiles(workPaths.ModArchivesDir, $"{BootfilesPrefix}*.*");
+ var bootfilesArchives = Directory.EnumerateFiles(workPaths.EnabledModArchivesDir, $"{BootfilesPrefix}*.*");
switch (bootfilesArchives.Count())
{
case 0:
@@ -212,6 +282,24 @@ private IMod BootfilesMod()
}
}
+ private IReadOnlyCollection ListEnabledModPackages() => ListModPackages(workPaths.EnabledModArchivesDir);
+
+ private IReadOnlyCollection ListDisabledModPackages() => ListModPackages(workPaths.DisabledModArchivesDir);
+
+ private IReadOnlyCollection ListModPackages(string path)
+ {
+ if (Directory.Exists(path))
+ {
+ return Directory.EnumerateFiles(path).ToList();
+ }
+ else
+ {
+ return Array.Empty();
+ }
+ }
+
+ private string PackageName(string archivePath) => Path.GetFileNameWithoutExtension(archivePath);
+
private Dictionary> ReadPreviouslyInstalledFiles() {
if (!File.Exists(workPaths.CurrentStateFile))
{
@@ -224,6 +312,9 @@ private Dictionary> ReadPreviouslyInstalledF
private void WriteInstalledFiles(Dictionary> filesByMod)
{
+ if (!filesByMod.Any() && !File.Exists(workPaths.CurrentStateFile)) {
+ return;
+ }
File.WriteAllText(workPaths.CurrentStateFile, JsonConvert.SerializeObject(filesByMod, JsonSerializerSettings));
}
}
\ No newline at end of file
diff --git a/src/Core/ModState.cs b/src/Core/ModState.cs
new file mode 100644
index 0000000..ead7eb6
--- /dev/null
+++ b/src/Core/ModState.cs
@@ -0,0 +1,8 @@
+namespace Core;
+
+public record ModState(
+ string PackageName,
+ string? PackagePath,
+ bool IsInstalled,
+ bool? IsEnabled
+);
diff --git a/src/GUI/App.xaml b/src/GUI/App.xaml
new file mode 100644
index 0000000..5f4c61b
--- /dev/null
+++ b/src/GUI/App.xaml
@@ -0,0 +1,13 @@
+
+
+
+
+
+
+
+
+
diff --git a/src/GUI/App.xaml.cs b/src/GUI/App.xaml.cs
new file mode 100644
index 0000000..9657da3
--- /dev/null
+++ b/src/GUI/App.xaml.cs
@@ -0,0 +1,19 @@
+using Microsoft.UI.Xaml;
+
+namespace AMS2CM.GUI;
+
+public partial class App : Application
+{
+ public App()
+ {
+ InitializeComponent();
+ }
+
+ protected override void OnLaunched(LaunchActivatedEventArgs args)
+ {
+ window = new MainWindow();
+ window.Activate();
+ }
+
+ private Window window;
+}
diff --git a/src/GUI/GUI.csproj b/src/GUI/GUI.csproj
new file mode 100644
index 0000000..136d05a
--- /dev/null
+++ b/src/GUI/GUI.csproj
@@ -0,0 +1,34 @@
+
+
+ WinExe
+ net6.0-windows10.0.19041.0
+ 10.0.17763.0
+ AMS2CM.GUI
+ AMS2CM.GUI
+ app.manifest
+ x86;x64
+ win10-x86;win10-x64
+ true
+ true
+ ..\Shared\AMS2CM.ico
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Always
+
+
+ Always
+
+
+
diff --git a/src/GUI/MainWindow.xaml b/src/GUI/MainWindow.xaml
new file mode 100644
index 0000000..d7b0994
--- /dev/null
+++ b/src/GUI/MainWindow.xaml
@@ -0,0 +1,87 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/GUI/MainWindow.xaml.cs b/src/GUI/MainWindow.xaml.cs
new file mode 100644
index 0000000..d32720e
--- /dev/null
+++ b/src/GUI/MainWindow.xaml.cs
@@ -0,0 +1,130 @@
+using System;
+using System.Collections.Generic;
+using System.Collections.ObjectModel;
+using Microsoft.VisualBasic.FileIO;
+using System.Linq;
+using Core;
+using Core.Games;
+using Microsoft.UI.Xaml;
+using Windows.ApplicationModel.DataTransfer;
+using Windows.Storage;
+using WinUIEx;
+
+namespace AMS2CM.GUI;
+
+public sealed partial class MainWindow : WindowEx
+{
+ private readonly ObservableCollection modList;
+ private readonly ModManager modManager;
+
+ public MainWindow()
+ {
+ InitializeComponent();
+ modManager = CreateModManager();
+ modList = new ObservableCollection();
+ ModListView.ItemsSource = modList;
+ SyncModListView();
+ }
+
+ private static ModManager CreateModManager()
+ {
+ var args = Environment.GetCommandLineArgs();
+ var config = Config.Load(args);
+ var game = new Game(config.Game);
+ var modFactory = new ModFactory(config.ModInstall, game);
+ return new ModManager(game, modFactory);
+ }
+
+ private void SyncButton_Click(object sender, RoutedEventArgs e)
+ {
+ SyncButton.IsEnabled = false;
+ modManager.InstallEnabledMods();
+ SyncModListView();
+ SyncButton.IsEnabled = true;
+ }
+
+ private void SyncModListView()
+ {
+ modList.Clear();
+ foreach (var modState in modManager.FetchState().OrderBy(_ => _.PackageName))
+ {
+ modList.Add(new ModVM(modState, modManager));
+ }
+ }
+
+ private void ModListView_DragOver(object sender, DragEventArgs e)
+ {
+ e.AcceptedOperation = DataPackageOperation.Copy;
+ }
+
+ private async void ModListView_Drop(object sender, DragEventArgs e)
+ {
+ if (e.DataView.Contains(StandardDataFormats.StorageItems))
+ {
+ var items = await e.DataView.GetStorageItemsAsync();
+ if (items.Count > 0)
+ {
+ foreach (var storageFile in items.OfType())
+ {
+ var filePath = storageFile.Path;
+ modManager.EnableNewMod(filePath);
+ }
+ // Refresh list after adding mods with drag and drop
+ SyncModListView();
+ }
+ }
+ }
+
+ private async void ModListView_DragItemsStarting(object sender, Microsoft.UI.Xaml.Controls.DragItemsStartingEventArgs e)
+ {
+ var storageItems = new List();
+ foreach (var o in e.Items)
+ {
+ var mvm = (ModVM)o;
+ var si = await StorageFile.GetFileFromPathAsync(mvm.PackagePath);
+ storageItems.Add(si);
+ }
+ e.Data.SetStorageItems(storageItems);
+
+ e.Data.RequestedOperation = DataPackageOperation.Move;
+ }
+
+ private void ModListView_DragItemsCompleted(Microsoft.UI.Xaml.Controls.ListViewBase sender, Microsoft.UI.Xaml.Controls.DragItemsCompletedEventArgs args)
+ {
+ // Refresh list after removing mods with drag and drop
+ SyncModListView();
+ }
+
+ private void ModListMenuToInstall_Click(object sender, RoutedEventArgs e)
+ {
+ foreach (var o in ModListView.SelectedItems)
+ {
+ var mvm = (ModVM)o;
+ mvm.IsEnabled = true;
+ }
+ }
+
+ private void ModListMenuDelete_Click(object sender, RoutedEventArgs e)
+ {
+ foreach (var o in ModListView.SelectedItems)
+ {
+ var mvm = (ModVM)o;
+ if (mvm.IsAvailable)
+ {
+ FileSystem.DeleteFile(mvm.PackagePath, UIOption.AllDialogs, RecycleOption.SendToRecycleBin);
+ }
+ }
+ // Refresh list after removing mods with context menu
+ SyncModListView();
+ }
+
+ private void ModListView_RightTapped(object sender, Microsoft.UI.Xaml.Input.RightTappedRoutedEventArgs e)
+ {
+ // Select the mod if right click outside of selection
+ var mvm = (e.OriginalSource as FrameworkElement).DataContext as ModVM;
+ if (!ModListView.SelectedItems.Contains(mvm))
+ {
+ ModListView.SelectedItem = mvm;
+ }
+ }
+}
diff --git a/src/GUI/ModVM.cs b/src/GUI/ModVM.cs
new file mode 100644
index 0000000..1ea0256
--- /dev/null
+++ b/src/GUI/ModVM.cs
@@ -0,0 +1,61 @@
+using System.ComponentModel;
+using Core;
+
+namespace AMS2CM.GUI;
+
+internal class ModVM : INotifyPropertyChanged
+{
+ private readonly ModState modState;
+ private readonly ModManager modManager;
+ private bool isEnabled;
+ private string currentPackagePath;
+
+ public event PropertyChangedEventHandler PropertyChanged;
+
+ public ModVM(ModState modState, ModManager modManager)
+ {
+ this.modState = modState;
+ this.modManager = modManager;
+ isEnabled = modState.IsEnabled ?? false;
+ currentPackagePath = modState.PackagePath;
+ }
+
+ public string PackageName => modState.PackageName;
+
+ public string PackagePath => currentPackagePath;
+
+ public bool IsInstalled => modState.IsInstalled;
+
+ public bool IsEnabled
+ {
+ get => isEnabled;
+ set => EnableOrDisable(value);
+ }
+
+ public bool IsAvailable
+ {
+ get => modState.IsEnabled is not null;
+ set => DoNothing();
+ }
+
+ private void EnableOrDisable(bool shouldEnable)
+ {
+ if (!IsAvailable || shouldEnable == isEnabled)
+ return;
+
+ if (shouldEnable)
+ {
+ currentPackagePath = modManager.EnableMod(currentPackagePath);
+ }
+ else
+ {
+ currentPackagePath = modManager.DisableMod(currentPackagePath);
+ }
+ isEnabled = shouldEnable;
+ PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(IsEnabled)));
+ }
+
+ private void DoNothing()
+ {
+ }
+}
diff --git a/src/GUI/app.manifest b/src/GUI/app.manifest
new file mode 100644
index 0000000..51325a1
--- /dev/null
+++ b/src/GUI/app.manifest
@@ -0,0 +1,17 @@
+
+
+
+
+
+
+
+
+
+
+
+
+ true/PM
+ PerMonitorV2, PerMonitor
+
+
+
\ No newline at end of file
diff --git a/src/CLI/AMS2CM.ico b/src/Shared/AMS2CM.ico
similarity index 100%
rename from src/CLI/AMS2CM.ico
rename to src/Shared/AMS2CM.ico
diff --git a/src/CLI/Config.yaml b/src/Shared/Config.yaml
similarity index 100%
rename from src/CLI/Config.yaml
rename to src/Shared/Config.yaml