├── .gitignore ├── CLI ├── CLI.csproj ├── Program.cs └── Properties │ ├── PublishProfiles │ ├── FrameworkDependent.pubxml │ └── SelfContained.pubxml │ └── launchSettings.json ├── Core ├── BondSchema │ ├── ApiManifest.cs │ ├── BondAsset.cs │ ├── BondGuid.cs │ ├── CacheFile.cs │ ├── CacheMap.cs │ ├── CustomsManifest.cs │ ├── EngineGameVariant.cs │ ├── GameManifest.cs │ ├── MapVariant.cs │ └── UgcGameVariant.cs ├── Cache │ ├── CacheManager.cs │ └── UserCacheManager.cs ├── Constants.cs ├── Core.csproj ├── GameApi.cs ├── LuaBundle.cs ├── Oodle.cs ├── Serialization │ ├── BondReader.cs │ ├── BondWriter.cs │ ├── MyBinaryReader.cs │ ├── MyBinaryWriter.cs │ └── SchemaSerializer.cs ├── Settings │ ├── SettingsBase.cs │ └── UserSettings.cs ├── Updater.cs ├── UrlHasher.cs ├── Utils │ ├── Exceptions.cs │ ├── ExtensionMethods.cs │ ├── FileUtil.cs │ ├── OrderedDictionary.cs │ └── Util.cs ├── VariantManager.cs ├── Variants │ └── VariantAsset.cs ├── lib │ └── oo2core_9_win64.dll └── resources │ ├── endpoints-lan.json │ ├── endpoints-offline.json │ └── endpoints-online.json ├── Directory.Build.props ├── GUI ├── App.xaml ├── App.xaml.cs ├── AssemblyInfo.cs ├── GUI.csproj ├── Models │ ├── LanguageModel.cs │ └── VariantModel.cs ├── Properties │ └── PublishProfiles │ │ ├── FrameworkDependent.pubxml │ │ └── SelfContained.pubxml ├── Utils │ ├── Converters.cs │ ├── HyperlinkExtensions.cs │ ├── IOService.cs │ ├── ListViewExtensions.cs │ ├── NavigationService.cs │ ├── NotifyPropertyChanged.cs │ ├── RelayCommand.cs │ ├── WindowExtensions.cs │ └── WindowManager.cs ├── ViewModels │ ├── AboutViewModel.cs │ ├── ExtractViewModel.cs │ ├── FileActionViewModel.cs │ ├── HashUrlViewModel.cs │ ├── MainViewModel.cs │ ├── ResultViewModel.cs │ ├── SettingsViewModel.cs │ ├── UpdaterViewModel.cs │ ├── VariantViewModel.cs │ ├── VariantViewModelContext.cs │ └── ViewModelBase.cs └── Views │ ├── AboutWindow.xaml │ ├── AboutWindow.xaml.cs │ ├── ErrorWindow.xaml │ ├── ErrorWindow.xaml.cs │ ├── ExtractWindow.xaml │ ├── ExtractWindow.xaml.cs │ ├── FileActionWindow.xaml │ ├── FileActionWindow.xaml.cs │ ├── HashUrlWindow.xaml │ ├── HashUrlWindow.xaml.cs │ ├── MainWindow.xaml │ ├── MainWindow.xaml.cs │ ├── ResultWindow.xaml │ ├── ResultWindow.xaml.cs │ ├── SettingsWindow.xaml │ ├── SettingsWindow.xaml.cs │ ├── UpdateWindow.xaml │ ├── UpdateWindow.xaml.cs │ ├── VariantListView.xaml │ └── VariantListView.xaml.cs ├── InfiniteVariantTool.sln ├── LICENSE.md ├── README.md ├── Tests ├── BondFileTests.cs ├── BondSchemaTests.cs ├── CacheFileTests.cs ├── CacheManagerTests.cs ├── CacheMapTests.cs ├── HashUrlTests.cs ├── LuaBundleTests.cs ├── OodleTests.cs ├── Secrets.cs ├── TestUtil.cs ├── Tests.csproj └── VariantManagerTests.cs ├── icon.ico ├── publish.bat └── publish.ps1 /CLI/CLI.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | Exe 5 | net6.0 6 | enable 7 | InfiniteVariantToolCLI 8 | ..\icon.ico 9 | InfiniteVariantTool.$(MSBuildProjectName.Replace(" ", "_")) 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /CLI/Properties/PublishProfiles/FrameworkDependent.pubxml: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | Release 5 | Any CPU 6 | ..\publish\framework_dependent 7 | FileSystem 8 | net6.0 9 | win-x64 10 | false 11 | true 12 | true 13 | true 14 | 15 | -------------------------------------------------------------------------------- /CLI/Properties/PublishProfiles/SelfContained.pubxml: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | Release 5 | Any CPU 6 | ..\publish\self_contained 7 | FileSystem 8 | net6.0 9 | win-x64 10 | true 11 | true 12 | true 13 | true 14 | true 15 | 16 | -------------------------------------------------------------------------------- /CLI/Properties/launchSettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "profiles": { 3 | "InfiniteVariantTool.CLI": { 4 | "commandName": "Project", 5 | "commandLineArgs": "variants list Map" 6 | } 7 | } 8 | } -------------------------------------------------------------------------------- /Core/BondSchema/ApiManifest.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Diagnostics.CodeAnalysis; 4 | using System.Linq; 5 | using System.Text; 6 | using System.Text.Json.Serialization; 7 | using System.Text.RegularExpressions; 8 | using System.Threading.Tasks; 9 | 10 | namespace InfiniteVariantTool.Core.BondSchema 11 | { 12 | [Bond.Schema] 13 | public class ApiManifest 14 | { 15 | #region Schema 16 | 17 | [Bond.Id(0)] 18 | public Dictionary Authorities { get; set; } 19 | [Bond.Id(1)] 20 | public Dictionary RetryPolicies { get; set; } 21 | [Bond.Id(2)] 22 | public Dictionary Settings { get; set; } 23 | [Bond.Id(3)] 24 | public Dictionary Endpoints { get; set; } 25 | 26 | public ApiManifest() 27 | { 28 | Authorities = new(); 29 | RetryPolicies = new(); 30 | Settings = new(); 31 | Endpoints = new(); 32 | } 33 | 34 | [Bond.Schema] 35 | public class Authority 36 | { 37 | [Bond.Id(0)] 38 | public string AuthorityId { get; set; } 39 | [Bond.Id(1)] 40 | public int Scheme { get; set; } 41 | [Bond.Id(2), Bond.Type(typeof(Bond.Tag.nullable))] 42 | public string? Hostname { get; set; } 43 | [Bond.Id(3), Bond.Type(typeof(Bond.Tag.nullable))] 44 | public ushort? Port { get; set; } 45 | [Bond.Id(4)] 46 | public HashSet AuthenticationMethods { get; set; } 47 | 48 | public Authority() 49 | { 50 | AuthorityId = ""; 51 | AuthenticationMethods = new(); 52 | } 53 | 54 | // copy constructor 55 | public Authority(Authority other) 56 | { 57 | AuthorityId = other.AuthorityId; 58 | Scheme = other.Scheme; 59 | Hostname = other.Hostname; 60 | Port = other.Port; 61 | AuthenticationMethods = new(other.AuthenticationMethods); 62 | } 63 | } 64 | 65 | [Bond.Schema] 66 | public class RetryPolicy 67 | { 68 | [Bond.Id(0)] 69 | public string RetryPolicyId { get; set; } 70 | [Bond.Id(1)] 71 | public uint TimeoutMs { get; set; } 72 | [Bond.Id(2), Bond.Type(typeof(Bond.Tag.nullable))] 73 | public RetryOptions? RetryOptions { get; set; } 74 | 75 | public RetryPolicy() 76 | { 77 | RetryPolicyId = ""; 78 | } 79 | } 80 | 81 | [Bond.Schema] 82 | public class RetryOptions 83 | { 84 | [Bond.Id(0)] 85 | public byte MaxRetryCount { get; set; } 86 | [Bond.Id(1)] 87 | public uint RetryDelayMs { get; set; } 88 | [Bond.Id(2)] 89 | public float RetryGrowth { get; set; } 90 | [Bond.Id(3)] 91 | public uint RetryJitterMs { get; set; } 92 | [Bond.Id(4)] 93 | public bool RetryIfNotFound { get; set; } 94 | 95 | public RetryOptions() 96 | { 97 | MaxRetryCount = 3; 98 | RetryDelayMs = 2000; 99 | RetryGrowth = 2; 100 | RetryJitterMs = 150; 101 | } 102 | } 103 | 104 | [Bond.Schema] 105 | public class Endpoint 106 | { 107 | [Bond.Id(0)] 108 | public string AuthorityId { get; set; } 109 | [Bond.Id(1)] 110 | public string Path { get; set; } 111 | [Bond.Id(2), Bond.Type(typeof(Bond.Tag.nullable))] 112 | public string? QueryString { get; set; } 113 | [Bond.Id(3)] 114 | public string RetryPolicyId { get; set; } 115 | [Bond.Id(4)] 116 | public string TopicName { get; set; } 117 | [Bond.Id(5)] 118 | public int AcknowledgementTypeId { get; set; } 119 | [Bond.Id(6)] 120 | public bool AuthenticationLifetimeExtensionSupported { get; set; } 121 | [Bond.Id(7)] 122 | public bool ClearanceAware { get; set; } 123 | 124 | public Endpoint() 125 | { 126 | AuthorityId = ""; 127 | Path = ""; 128 | RetryPolicyId = ""; 129 | TopicName = ""; 130 | EndpointId = ""; 131 | } 132 | 133 | // copy constructor 134 | public Endpoint(Endpoint other) 135 | { 136 | AuthorityId = other.AuthorityId; 137 | Path = other.Path; 138 | QueryString = other.QueryString; 139 | RetryPolicyId = other.RetryPolicyId; 140 | TopicName = other.TopicName; 141 | AcknowledgementTypeId = other.AcknowledgementTypeId; 142 | AuthenticationLifetimeExtensionSupported = other.AuthenticationLifetimeExtensionSupported; 143 | ClearanceAware = other.ClearanceAware; 144 | EndpointId = other.EndpointId; 145 | } 146 | 147 | #region MyExtensions 148 | 149 | [JsonIgnore] 150 | public string EndpointId { get; set; } 151 | 152 | #endregion 153 | } 154 | #endregion 155 | } 156 | } 157 | -------------------------------------------------------------------------------- /Core/BondSchema/BondGuid.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Buffers.Binary; 3 | using System.Collections.Generic; 4 | using System.Linq; 5 | using System.Text; 6 | using System.Text.Json; 7 | using System.Text.Json.Serialization; 8 | using System.Threading.Tasks; 9 | 10 | namespace InfiniteVariantTool.Core.BondSchema 11 | { 12 | [Bond.Schema] 13 | public class BondGuid 14 | { 15 | #region Schema 16 | 17 | [Bond.Id(0)] 18 | public uint Data1 { get; set; } 19 | 20 | [Bond.Id(1)] 21 | public ushort Data2 { get; set; } 22 | 23 | [Bond.Id(2)] 24 | public ushort Data3 { get; set; } 25 | 26 | [Bond.Id(3)] 27 | public ulong Data4 { get; set; } 28 | 29 | public BondGuid() 30 | { 31 | 32 | } 33 | 34 | // copy constructor 35 | public BondGuid(BondGuid other) 36 | { 37 | Data1 = other.Data1; 38 | Data2 = other.Data2; 39 | Data3 = other.Data3; 40 | Data4 = other.Data4; 41 | } 42 | 43 | #endregion 44 | 45 | #region MyExtensions 46 | 47 | public BondGuid(Guid guid) 48 | { 49 | byte[] bytes = guid.ToByteArray(); 50 | Data1 = BitConverter.ToUInt32(bytes, 0); 51 | Data2 = BitConverter.ToUInt16(bytes, 4); 52 | Data3 = BitConverter.ToUInt16(bytes, 6); 53 | Data4 = BitConverter.ToUInt64(bytes, 8); 54 | } 55 | 56 | public BondGuid(string guid) : this(Guid.Parse(guid)) 57 | { 58 | 59 | } 60 | 61 | public override string ToString() 62 | { 63 | byte[] bytes = new byte[8]; 64 | BinaryPrimitives.WriteUInt64LittleEndian(bytes, Data4); 65 | ulong data5 = BinaryPrimitives.ReadUInt64BigEndian(bytes); 66 | ushort data4 = (ushort)(data5 >> 48); 67 | data5 &= 0xffffffffffff; 68 | return $"{Data1:x08}-{Data2:x04}-{Data3:x04}-{data4:x04}-{data5:x012}"; 69 | } 70 | 71 | public override bool Equals(object? obj) 72 | { 73 | return obj is BondGuid other 74 | && Data1 == other.Data1 75 | && Data2 == other.Data2 76 | && Data3 == other.Data3 77 | && Data4 == other.Data4; 78 | } 79 | 80 | public override int GetHashCode() 81 | { 82 | return (int)Data1 ^ Data2 ^ (Data3 << 16) ^ (int)(Data4 & 0xffffffff) ^ (int)(Data4 >> 32); 83 | } 84 | 85 | public static explicit operator Guid(BondGuid bondGuid) => Guid.Parse(bondGuid.ToString()); 86 | public static explicit operator BondGuid(Guid guid) => new(guid); 87 | 88 | #endregion 89 | } 90 | 91 | public class BondGuidJsonConverter : JsonConverter 92 | { 93 | public override BondGuid Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) 94 | { 95 | return new BondGuid(reader.GetString()!); 96 | } 97 | 98 | public override void Write(Utf8JsonWriter writer, BondGuid bondGuid, JsonSerializerOptions options) 99 | { 100 | writer.WriteStringValue(bondGuid.ToString()); 101 | } 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /Core/BondSchema/CacheFile.cs: -------------------------------------------------------------------------------- 1 | using Bond.IO.Safe; 2 | using Bond.Protocols; 3 | using System; 4 | using System.Collections.Generic; 5 | using System.IO; 6 | using System.Linq; 7 | using System.Text; 8 | using System.Threading.Tasks; 9 | 10 | namespace InfiniteVariantTool.Core.BondSchema 11 | { 12 | [Bond.Schema] 13 | public class CacheFile 14 | { 15 | #region Schema 16 | 17 | [Bond.Id(1), Bond.Type(typeof(Bond.Tag.nullable))] 18 | public CacheMap.Metadata? Metadata { get; set; } 19 | [Bond.Id(2)] 20 | public sbyte[] Data { get; set; } 21 | 22 | public CacheFile() 23 | { 24 | Data = Array.Empty(); 25 | } 26 | 27 | #endregion 28 | 29 | #region MyExtensions 30 | 31 | public byte[] UData => (byte[])(Array)Data; 32 | 33 | #endregion 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /Core/BondSchema/CacheMap.cs: -------------------------------------------------------------------------------- 1 | using Bond.IO.Safe; 2 | using Bond.Protocols; 3 | using System; 4 | using System.Collections.Generic; 5 | using System.IO; 6 | using System.Linq; 7 | using System.Text; 8 | using System.Threading.Tasks; 9 | 10 | namespace InfiniteVariantTool.Core.BondSchema 11 | { 12 | [Bond.Schema] 13 | public class CacheMap 14 | { 15 | #region Schema 16 | 17 | [Bond.Id(0)] 18 | public Dictionary Entries { get; set; } 19 | [Bond.Id(1)] 20 | public sbyte Unk { get; set; } 21 | [Bond.Id(2)] 22 | public string Language { get; set; } 23 | 24 | public CacheMap() 25 | { 26 | Entries = new(); 27 | Language = ""; 28 | } 29 | 30 | [Bond.Schema] 31 | public class Entry 32 | { 33 | [Bond.Id(0)] 34 | public long CreateTime { get; set; } 35 | [Bond.Id(1)] 36 | public long AccessTime { get; set; } 37 | [Bond.Id(2)] 38 | public long WriteTime { get; set; } 39 | [Bond.Id(3)] 40 | public Metadata Metadata { get; set; } 41 | [Bond.Id(4)] 42 | public ulong Size { get; set; } 43 | 44 | public Entry() 45 | { 46 | Metadata = new(); 47 | } 48 | } 49 | 50 | [Bond.Schema] 51 | public class Metadata 52 | { 53 | [Bond.Id(0)] 54 | public string Etag { get; set; } 55 | [Bond.Id(1)] 56 | public ulong Timestamp { get; set; } 57 | [Bond.Id(2)] 58 | public Dictionary Headers { get; set; } 59 | [Bond.Id(3)] 60 | public BondGuid Guid { get; set; } 61 | [Bond.Id(4)] 62 | public string Url { get; set; } 63 | 64 | public Metadata() 65 | { 66 | Etag = ""; 67 | Headers = new(); 68 | Guid = new(); 69 | Url = ""; 70 | } 71 | } 72 | 73 | #endregion 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /Core/BondSchema/CustomsManifest.cs: -------------------------------------------------------------------------------- 1 | using InfiniteVariantTool.Core.Variants; 2 | using System; 3 | using System.Collections.Generic; 4 | using System.Linq; 5 | using System.Text; 6 | using System.Threading.Tasks; 7 | 8 | namespace InfiniteVariantTool.Core.BondSchema 9 | { 10 | [Bond.Schema] 11 | public class CustomsManifest : BondAsset 12 | { 13 | #region Schema 14 | 15 | [Bond.Id(0)] 16 | public CustomData_ CustomData { get; set; } 17 | [Bond.Id(1)] 18 | public List MapLinks { get; set; } 19 | [Bond.Id(2)] 20 | public List PlaylistLinks { get; set; } 21 | [Bond.Id(3)] 22 | public List PrefabLinks { get; set; } 23 | [Bond.Id(4)] 24 | public List UgcGameVariantLinks { get; set; } 25 | [Bond.Id(5)] 26 | public List MapModePairLinks { get; set; } 27 | [Bond.Id(6), Bond.Type(typeof(List))] 28 | public List Tags { get; set; } 29 | 30 | public CustomsManifest() 31 | { 32 | CustomData = new(); 33 | MapLinks = new(); 34 | PlaylistLinks = new(); 35 | PrefabLinks = new(); 36 | UgcGameVariantLinks = new(); 37 | MapModePairLinks = new(); 38 | Tags = new(); 39 | } 40 | 41 | [Bond.Schema] 42 | public class CustomData_ 43 | { 44 | [Bond.Id(0)] 45 | public int Intent { get; set; } 46 | } 47 | 48 | #endregion 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /Core/BondSchema/EngineGameVariant.cs: -------------------------------------------------------------------------------- 1 | using InfiniteVariantTool.Core.Variants; 2 | using System; 3 | using System.Collections.Generic; 4 | using System.Linq; 5 | using System.Text; 6 | using System.Threading.Tasks; 7 | 8 | namespace InfiniteVariantTool.Core.BondSchema 9 | { 10 | [Bond.Schema, Variant] 11 | public class EngineGameVariant : BondAsset 12 | { 13 | #region Schema 14 | 15 | [Bond.Id(0)] 16 | public CustomData_ CustomData { get; set; } 17 | [Bond.Id(1), Bond.Type(typeof(List))] 18 | public List Tags { get; set; } 19 | 20 | public EngineGameVariant() 21 | { 22 | CustomData = new(); 23 | Tags = new(); 24 | } 25 | 26 | // partial copy constructor 27 | public EngineGameVariant(BondAsset other) : base(other) 28 | { 29 | CustomData = new(); 30 | Tags = new(); 31 | } 32 | 33 | [Bond.Schema] 34 | public class CustomData_ 35 | { 36 | [Bond.Id(0)] 37 | public SubsetData_ SubsetData { get; set; } 38 | [Bond.Id(1)] 39 | public Dictionary LocalizedData { get; set; } 40 | 41 | public CustomData_() 42 | { 43 | SubsetData = new(); 44 | LocalizedData = new(); 45 | } 46 | } 47 | 48 | [Bond.Schema] 49 | public class SubsetData_ 50 | { 51 | [Bond.Id(0)] 52 | public int StatBucketGameType { get; set; } 53 | [Bond.Id(1)] 54 | public string EngineName { get; set; } 55 | [Bond.Id(2)] 56 | public string VariantName { get; set; } 57 | 58 | public SubsetData_() 59 | { 60 | EngineName = ""; 61 | VariantName = ""; 62 | } 63 | } 64 | 65 | #endregion 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /Core/BondSchema/GameManifest.cs: -------------------------------------------------------------------------------- 1 | using InfiniteVariantTool.Core.Cache; 2 | using InfiniteVariantTool.Core.Variants; 3 | using System; 4 | using System.Collections.Generic; 5 | using System.Linq; 6 | using System.Text; 7 | using System.Threading.Tasks; 8 | 9 | namespace InfiniteVariantTool.Core.BondSchema 10 | { 11 | [Bond.Schema] 12 | public class GameManifest : BondAsset 13 | { 14 | #region Schema 15 | 16 | [Bond.Id(0)] 17 | public CustomData_ CustomData { get; set; } 18 | [Bond.Id(1), Bond.Type(typeof(List))] 19 | public List Tags { get; set; } 20 | [Bond.Id(2)] 21 | public List MapLinks { get; set; } 22 | [Bond.Id(3)] 23 | public List UgcGameVariantLinks { get; set; } 24 | [Bond.Id(4)] 25 | public List PlaylistLinks { get; set; } 26 | [Bond.Id(5)] 27 | public List EngineGameVariantLinks { get; set; } 28 | 29 | public GameManifest() 30 | { 31 | CustomData = new(); 32 | Tags = new(); 33 | MapLinks = new(); 34 | UgcGameVariantLinks = new(); 35 | PlaylistLinks = new(); 36 | EngineGameVariantLinks = new(); 37 | } 38 | 39 | [Bond.Schema] 40 | public class CustomData_ 41 | { 42 | [Bond.Id(0)] 43 | public string BranchName { get; set; } 44 | [Bond.Id(1)] 45 | public string BuildNumber { get; set; } 46 | [Bond.Id(2)] 47 | public int Kind { get; set; } 48 | [Bond.Id(3)] 49 | public string ContentVersion { get; set; } 50 | [Bond.Id(4)] 51 | public BondGuid BuildGuid { get; set; } 52 | [Bond.Id(5)] 53 | public int Visibility { get; set; } 54 | 55 | public CustomData_() 56 | { 57 | BranchName = ""; 58 | BuildNumber = ""; 59 | ContentVersion = ""; 60 | BuildGuid = new(); 61 | } 62 | } 63 | 64 | #endregion 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /Core/BondSchema/MapVariant.cs: -------------------------------------------------------------------------------- 1 | using InfiniteVariantTool.Core.Variants; 2 | using System; 3 | using System.Collections.Generic; 4 | using System.Linq; 5 | using System.Text; 6 | using System.Threading.Tasks; 7 | 8 | namespace InfiniteVariantTool.Core.BondSchema 9 | { 10 | [Bond.Schema, Variant] 11 | public class MapVariant : BondAsset 12 | { 13 | [Bond.Id(0)] 14 | public CustomData_ CustomData { get; set; } 15 | [Bond.Id(1), Bond.Type(typeof(List))] 16 | public List Tags { get; set; } 17 | 18 | public MapVariant() 19 | { 20 | CustomData = new(); 21 | Tags = new(); 22 | } 23 | 24 | // partial copy constructor 25 | public MapVariant(BondAsset other) : base(other) 26 | { 27 | CustomData = new(); 28 | Tags = new(); 29 | } 30 | 31 | [Bond.Schema] 32 | public class CustomData_ 33 | { 34 | [Bond.Id(0)] 35 | public int NumOfObjectsOnMap { get; set; } 36 | [Bond.Id(1)] 37 | public int TagLevelId { get; set; } 38 | [Bond.Id(2)] 39 | public bool IsBaked { get; set; } 40 | [Bond.Id(3)] 41 | public bool HasNodeGraph { get; set; } 42 | } 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /Core/BondSchema/UgcGameVariant.cs: -------------------------------------------------------------------------------- 1 | using InfiniteVariantTool.Core.Variants; 2 | using System; 3 | using System.Collections.Generic; 4 | using System.Linq; 5 | using System.Text; 6 | using System.Threading.Tasks; 7 | 8 | namespace InfiniteVariantTool.Core.BondSchema 9 | { 10 | [Bond.Schema, Variant] 11 | public class UgcGameVariant : BondAsset 12 | { 13 | #region schema 14 | 15 | [Bond.Id(0)] 16 | public CustomData_ CustomData { get; set; } 17 | [Bond.Id(1), Bond.Type(typeof(List))] 18 | public List Tags { get; set; } 19 | [Bond.Id(2), Bond.Type(typeof(Bond.Tag.nullable))] 20 | public BondAsset? EngineGameVariantLink { get; set; } 21 | 22 | public UgcGameVariant() 23 | { 24 | CustomData = new(); 25 | Tags = new(); 26 | } 27 | 28 | // partial copy constructor 29 | public UgcGameVariant(BondAsset other) : base(other) 30 | { 31 | CustomData = new(); 32 | Tags = new(); 33 | } 34 | 35 | [Bond.Schema] 36 | public class CustomData_ 37 | { 38 | [Bond.Id(0)] 39 | public Dictionary KeyValues { get; set; } 40 | 41 | public CustomData_() 42 | { 43 | KeyValues = new(); 44 | } 45 | } 46 | 47 | #endregion 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /Core/Cache/UserCacheManager.cs: -------------------------------------------------------------------------------- 1 | using InfiniteVariantTool.Core.Variants; 2 | using System; 3 | using System.Collections.Generic; 4 | using System.IO; 5 | using System.Linq; 6 | using System.Text; 7 | using System.Threading.Tasks; 8 | using System.Xml.Linq; 9 | 10 | namespace InfiniteVariantTool.Core.Cache 11 | { 12 | public class UserCacheManager 13 | { 14 | public List Entries { get; private set; } 15 | private string cacheDirectory; 16 | 17 | public UserCacheManager(string cacheDirectory) 18 | { 19 | this.cacheDirectory = cacheDirectory; 20 | Entries = new(); 21 | } 22 | 23 | public async Task LoadEntries() 24 | { 25 | Entries.Clear(); 26 | foreach (string filePath in VariantAsset.FindVariants(cacheDirectory)) 27 | { 28 | Entries.Add(await VariantAsset.Load(filePath, false)); 29 | } 30 | } 31 | 32 | public void RemoveVariant(string variantFilePath) 33 | { 34 | Entries.RemoveAll(entry => entry.FilePath == variantFilePath); 35 | 36 | string variantDirectory = Path.GetDirectoryName(variantFilePath)!; 37 | if (Directory.Exists(variantDirectory)) 38 | { 39 | Directory.Delete(variantDirectory, true); 40 | } 41 | 42 | // remove empty parent directories 43 | string currentPath = Path.GetFullPath(Path.GetDirectoryName(variantDirectory)!); 44 | string basePath = Path.GetFullPath(cacheDirectory); 45 | while (!currentPath.Equals(basePath, StringComparison.InvariantCultureIgnoreCase) && Directory.Exists(currentPath) 46 | && !Directory.EnumerateFileSystemEntries(currentPath).Any()) 47 | { 48 | Directory.Delete(currentPath); 49 | currentPath = Path.GetDirectoryName(currentPath)!; 50 | } 51 | } 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /Core/Constants.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.IO; 3 | 4 | namespace InfiniteVariantTool.Core 5 | { 6 | public static class Constants 7 | { 8 | public static readonly string GameExeName = "HaloInfinite.exe"; 9 | public static readonly string OfflineCacheDirectory = Path.Combine("package", "pc"); 10 | public static readonly string OnlineCacheDirectory = "disk_cache"; 11 | public static readonly string LanCacheDirectory = "server_disk_cache"; 12 | public static readonly string CmsDirectory = "__cms__"; 13 | public static readonly byte[] pngSignature = new byte[] { 0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A }; 14 | public static readonly byte[] jpgSignature = new byte[] { 0xff, 0xd8, 0xff }; 15 | public static readonly string AppName = "InfiniteVariantTool"; 16 | } 17 | 18 | public enum Game 19 | { 20 | HaloInfinite, 21 | Halo5 22 | } 23 | 24 | public class MimeType 25 | { 26 | public readonly string Value; 27 | private MimeType(string value) 28 | { 29 | Value = value; 30 | } 31 | 32 | public static readonly MimeType OctetStream = new("application/octet-stream"); 33 | public static readonly MimeType Bond = new("application/x-bond-compact-binary"); 34 | public static readonly MimeType Json = new("application/json"); 35 | } 36 | 37 | public class Language 38 | { 39 | public readonly string ShortCode; 40 | public readonly string Code; 41 | public readonly string Name; 42 | private Language(string shortCode, string code, string name) 43 | { 44 | ShortCode = shortCode; 45 | Code = code; 46 | Name = name; 47 | } 48 | 49 | private Language(string shortCode) 50 | { 51 | ShortCode = shortCode; 52 | Code = ""; 53 | Name = ""; 54 | } 55 | 56 | public static Language? TryFromCode(string code) 57 | { 58 | if (code == "") 59 | { 60 | return null; 61 | } 62 | return Languages.Find(lang => lang.Code == code); 63 | } 64 | 65 | public static Language FromCode(string code) 66 | { 67 | return TryFromCode(code) ?? throw new KeyNotFoundException(); 68 | } 69 | 70 | public static Language FromShortCode(string shortCode) 71 | { 72 | return Languages.Find(lang => lang.ShortCode == shortCode) ?? throw new KeyNotFoundException(); 73 | } 74 | 75 | public static readonly Language En = new("en", "en-US", "English"); 76 | public static readonly Language Jpn = new("jpn", "ja-JP", "Japanese"); 77 | public static readonly Language De = new("de", "de-DE", "German"); 78 | public static readonly Language Fr = new("fr", "fr-FR", "French"); 79 | public static readonly Language Sp = new("sp", "es-ES", "Spanish"); 80 | public static readonly Language Mx = new("mx", "es-MX", "Spanish (Mexico)"); 81 | public static readonly Language It = new("it", "it-IT", "Italian"); 82 | public static readonly Language Kor = new("kor", "ko-KR", "Korean"); 83 | public static readonly Language Cht = new("cht", "zh-TW", "Chinese (Traditional)"); 84 | public static readonly Language Chs = new("chs", "zh-CN", "Chinese (Simplified)"); 85 | public static readonly Language Pl = new("pl", "pl-PL", "Polish"); 86 | public static readonly Language Ru = new("ru", "ru-RU", "Russian"); 87 | public static readonly Language Nl = new("nl", "nl-NL", "Dutch"); 88 | public static readonly Language Br = new("br", "pt-BR", "Portuguese (Brazil)"); 89 | public static readonly Language Dk = new("dk"); 90 | public static readonly Language Fi = new("fi"); 91 | public static readonly Language No = new("no"); 92 | public static readonly Language Pt = new("pt"); 93 | public static readonly List Languages = new() { En, Jpn, De, Fr, Sp, Mx, It, Kor, Cht, Chs, Pl, Ru, Nl, Br, Dk, Fi, No, Pt }; 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /Core/Core.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | net6.0 5 | enable 6 | InfiniteVariantTool.$(MSBuildProjectName.Replace(" ", "_")) 7 | InfiniteVariantToolCore 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | PreserveNewest 20 | %(Filename)%(Extension) 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | -------------------------------------------------------------------------------- /Core/Oodle.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Diagnostics.CodeAnalysis; 3 | using System.Runtime.InteropServices; 4 | 5 | namespace InfiniteVariantTool.Core 6 | { 7 | public static class Oodle 8 | { 9 | public const string DllName = "oo2core_9_win64.dll"; 10 | 11 | [DllImport(DllName)] 12 | public static extern long OodleLZ_Decompress( 13 | byte[] compBuf, 14 | long compBufSize, 15 | byte[] rawBuf, 16 | long rawLen, 17 | bool fuzzSafe = true, 18 | bool checkCRC = false, 19 | OodleLZ_Verbosity verbosity = OodleLZ_Verbosity.None, 20 | IntPtr decBufBase = default, 21 | long decBufSize = 0, 22 | OodleDecompressCallback? fpCallback = null, 23 | object? callbackUserData = null, 24 | byte[]? decoderMemory = null, 25 | long decoderMemorySize = 0, 26 | OodleLZ_Decode_ThreadPhase threadPhase = OodleLZ_Decode_ThreadPhase.Unthreaded); 27 | 28 | [DllImport(DllName)] 29 | public static extern long OodleLZ_Compress( 30 | OodleLZ_Compressor compressor, 31 | byte[] rawBuf, 32 | long rawLen, 33 | byte[] compBuf, 34 | OodleLZ_CompressionLevel level, 35 | IntPtr pOptions = default, 36 | IntPtr dictionaryBase = default, 37 | IntPtr lrm = default, 38 | byte[]? scratchMem = null, 39 | long scratchSize = 0); 40 | 41 | [DllImport(DllName)] 42 | public static extern long OodleLZ_GetCompressedBufferSizeNeeded(OodleLZ_Compressor compressor, long rawLen); 43 | 44 | public static bool TryDecompress(byte[] compressedData, long decompressedSize, [MaybeNullWhen(false)] out byte[] decompressedData) 45 | { 46 | byte[] decompressBuffer = new byte[decompressedSize]; 47 | if (0 != OodleLZ_Decompress(compressedData, compressedData.Length, decompressBuffer, decompressedSize)) 48 | { 49 | decompressedData = decompressBuffer; 50 | return true; 51 | } 52 | decompressedData = null; 53 | return false; 54 | } 55 | 56 | public static byte[] Decompress(byte[] compressedData, long decompressedSize) 57 | { 58 | if (TryDecompress(compressedData, decompressedSize, out byte[]? decompressedData)) 59 | { 60 | return decompressedData; 61 | } 62 | else 63 | { 64 | throw new OodleException("Failed to decompress data"); 65 | } 66 | } 67 | 68 | public static byte[] Compress(byte[] data, OodleLZ_Compressor compressor, OodleLZ_CompressionLevel level) 69 | { 70 | long compressionBufferSize = OodleLZ_GetCompressedBufferSizeNeeded(compressor, data.Length); 71 | byte[] compressionBuffer = new byte[compressionBufferSize]; 72 | long compressedSize = OodleLZ_Compress(compressor, data, data.Length, compressionBuffer, level); 73 | if (compressedSize != 0) 74 | { 75 | return compressionBuffer[..(int)compressedSize]; 76 | } 77 | else 78 | { 79 | throw new OodleException("Failed to compress data"); 80 | } 81 | } 82 | } 83 | 84 | public delegate OodleDecompressCallbackRet OodleDecompressCallback(object userdata, IntPtr rawBuf, long rawLen, IntPtr compBuf, long compBufferSize , long rawDone, long compUsed); 85 | 86 | public enum OodleLZ_Compressor 87 | { 88 | Invalid = -1, 89 | None = 3, 90 | Kraken = 8, 91 | Leviathan = 13, 92 | Mermaid = 9, 93 | Selkie = 11, 94 | Hydra = 12 95 | } 96 | 97 | public enum OodleLZ_Verbosity 98 | { 99 | None = 0, 100 | Minimal = 1, 101 | Some = 2, 102 | Lots = 3 103 | } 104 | 105 | public enum OodleDecompressCallbackRet 106 | { 107 | Continue = 0, 108 | Cancel = 1, 109 | Invalid = 2 110 | } 111 | 112 | public enum OodleLZ_Decode_ThreadPhase 113 | { 114 | ThreadPhase1 = 1, 115 | ThreadPhase2 = 2, 116 | ThreadPhaseAll = 3, 117 | Unthreaded = ThreadPhaseAll 118 | } 119 | 120 | public enum OodleLZ_CompressionLevel 121 | { 122 | None = 0, 123 | SuperFast = 1, 124 | VeryFast = 2, 125 | Fast = 3, 126 | Normal = 4, 127 | Optimal1 = 5, 128 | Optimal2 = 6, 129 | Optimal3 = 7, 130 | Optimal4 = 8, 131 | Optimal5 = 9, 132 | HyperFast1 = -1, 133 | HyperFast2 = -2, 134 | HyperFast3 = -3, 135 | HyperFast4 = -4, 136 | HyperFast = HyperFast1, 137 | Optimal = Optimal2, 138 | Max = Optimal5, 139 | Min = HyperFast4 140 | } 141 | 142 | public class OodleException : Exception 143 | { 144 | public OodleException() { } 145 | public OodleException(string message) : base(message) { } 146 | public OodleException(string message, Exception innerException) : base(message, innerException) { } 147 | } 148 | } 149 | -------------------------------------------------------------------------------- /Core/Serialization/MyBinaryReader.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Buffers.Binary; 4 | 5 | namespace InfiniteVariantTool.Core.Serialization 6 | { 7 | public class MyBinaryReader 8 | { 9 | private readonly byte[] data; 10 | private int cursor; 11 | private bool littleEndian; 12 | private readonly Stack cursorStack; 13 | public MyBinaryReader(byte[] data, bool littleEndian = true) 14 | { 15 | this.data = data; 16 | this.littleEndian = littleEndian; 17 | cursor = 0; 18 | cursorStack = new Stack(); 19 | } 20 | 21 | public bool EOF => cursor == data.Length; 22 | 23 | public void SetLittleEndian(bool isLittleEndian) 24 | { 25 | littleEndian = isLittleEndian; 26 | } 27 | 28 | public bool ReadBool() 29 | { 30 | return ReadUInt8() != 0; 31 | } 32 | 33 | public sbyte ReadInt8() 34 | { 35 | return (sbyte)ReadUInt8(); 36 | } 37 | 38 | public byte ReadUInt8() 39 | { 40 | 41 | byte ret = data[cursor]; 42 | cursor += sizeof(byte); 43 | return ret; 44 | } 45 | 46 | public short ReadInt16() 47 | { 48 | Span span = new Span(data)[cursor..]; 49 | short ret = littleEndian ? BinaryPrimitives.ReadInt16LittleEndian(span) : BinaryPrimitives.ReadInt16BigEndian(span); 50 | cursor += sizeof(short); 51 | return ret; 52 | } 53 | 54 | public ushort ReadUInt16() 55 | { 56 | return (ushort)ReadInt16(); 57 | } 58 | 59 | public int ReadInt32() 60 | { 61 | Span span = new Span(data)[cursor..]; 62 | int ret = littleEndian ? BinaryPrimitives.ReadInt32LittleEndian(span) : BinaryPrimitives.ReadInt32BigEndian(span); 63 | cursor += sizeof(int); 64 | return ret; 65 | } 66 | 67 | public uint ReadUInt32() 68 | { 69 | return (uint)ReadInt32(); 70 | } 71 | 72 | public long ReadInt64() 73 | { 74 | Span span = new Span(data)[cursor..]; 75 | long ret = littleEndian ? BinaryPrimitives.ReadInt64LittleEndian(span) : BinaryPrimitives.ReadInt64BigEndian(span); 76 | cursor += sizeof(long); 77 | return ret; 78 | } 79 | 80 | public ulong ReadUInt64() 81 | { 82 | return (ulong)ReadInt64(); 83 | } 84 | 85 | public float ReadFloat() 86 | { 87 | Span span = new Span(data)[cursor..]; 88 | float ret = littleEndian ? BinaryPrimitives.ReadSingleLittleEndian(span) : BinaryPrimitives.ReadSingleBigEndian(span); 89 | cursor += sizeof(float); 90 | return ret; 91 | } 92 | 93 | public double ReadDouble() 94 | { 95 | Span span = new Span(data)[cursor..]; 96 | double ret = littleEndian ? BinaryPrimitives.ReadDoubleLittleEndian(span) : BinaryPrimitives.ReadDoubleBigEndian(span); 97 | cursor += sizeof(double); 98 | return ret; 99 | } 100 | 101 | public string ReadString(int length) 102 | { 103 | string ret = System.Text.Encoding.UTF8.GetString(data, cursor, length); 104 | cursor += length; 105 | return ret; 106 | } 107 | 108 | public string ReadCString(int maxLength) 109 | { 110 | int length = 0; 111 | while (length < maxLength && Data[cursor + length] != 0) 112 | { 113 | length++; 114 | } 115 | return ReadString(length); 116 | } 117 | 118 | public string ReadStringBuffer(int size) 119 | { 120 | string ret = ReadCString(size); 121 | cursor += size - ret.Length; 122 | return ret; 123 | } 124 | 125 | public string ReadWString(int length) 126 | { 127 | string ret = System.Text.Encoding.Unicode.GetString(data, cursor, length * 2); 128 | cursor += length * 2; 129 | return ret; 130 | } 131 | 132 | public byte[] ReadBytes(int length) 133 | { 134 | byte[] ret = data[cursor..(cursor + length)]; 135 | cursor += length; 136 | return ret; 137 | } 138 | 139 | public long ReadLEB128() 140 | { 141 | ulong unsigned = ReadLEB128U(); 142 | return (long)(unsigned >> 1) ^ -(long)(unsigned & 1); 143 | } 144 | 145 | public ulong ReadLEB128U() 146 | { 147 | ulong result = 0; 148 | int shift = 0; 149 | ulong b; 150 | do 151 | { 152 | b = ReadUInt8(); 153 | result |= (b & 0x7f) << shift; 154 | shift += 7; 155 | } while ((b & 0x80) != 0); 156 | return result; 157 | } 158 | 159 | public void Skip(int offset) 160 | { 161 | cursor += offset; 162 | } 163 | 164 | public void Seek(int position) 165 | { 166 | cursor = position; 167 | } 168 | 169 | public void PushCursor() 170 | { 171 | cursorStack.Push(cursor); 172 | } 173 | 174 | public int PopCursor() 175 | { 176 | cursor = cursorStack.Pop(); 177 | return cursor; 178 | } 179 | 180 | public void Pad(int padding) 181 | { 182 | if (cursor % padding != 0) 183 | { 184 | Skip(padding - (cursor % padding)); 185 | } 186 | } 187 | 188 | public int Position => cursor; 189 | public int Size => data.Length; 190 | public byte[] Data => data; 191 | } 192 | } 193 | -------------------------------------------------------------------------------- /Core/Serialization/SchemaSerializer.cs: -------------------------------------------------------------------------------- 1 | using Bond.IO.Safe; 2 | using Bond.Protocols; 3 | using InfiniteVariantTool.Core.BondSchema; 4 | using System; 5 | using System.Collections.Generic; 6 | using System.IO; 7 | using System.Linq; 8 | using System.Text; 9 | using System.Text.Json; 10 | using System.Threading; 11 | using System.Threading.Tasks; 12 | using System.Xml.Linq; 13 | 14 | namespace InfiniteVariantTool.Core.Serialization 15 | { 16 | public class SchemaSerializer 17 | { 18 | private static readonly JsonSerializerOptions jsonOptions = new() 19 | { 20 | WriteIndented = true, 21 | IgnoreReadOnlyFields = true, 22 | IgnoreReadOnlyProperties = true, 23 | Converters = 24 | { 25 | new BondGuidJsonConverter() 26 | } 27 | }; 28 | 29 | 30 | public static T DeserializeBond(byte[] data) 31 | { 32 | InputBuffer input = new(data); 33 | CompactBinaryReader reader = new(input, 2); 34 | return Bond.Deserialize.From(reader); 35 | } 36 | 37 | public static object DeserializeBond(byte[] data, Type type) 38 | { 39 | InputBuffer input = new(data); 40 | CompactBinaryReader reader = new(input, 2); 41 | Bond.Deserializer> deserializer = new(type); 42 | return deserializer.Deserialize(reader); 43 | } 44 | 45 | public static async Task DeserializeBondAsync(string filePath, Type type) 46 | { 47 | var input = new InputBuffer(await File.ReadAllBytesAsync(filePath)); 48 | var reader = new CompactBinaryReader(input, 2); 49 | var deserializer = new Bond.Deserializer>(type); 50 | return deserializer.Deserialize(reader); 51 | } 52 | 53 | public static byte[] SerializeBond(object src) 54 | { 55 | OutputBuffer output = new(); 56 | CompactBinaryWriter writer = new(output, 2); 57 | Bond.Serializer> serializer = new(src.GetType()); 58 | serializer.Serialize(src, writer); 59 | return output.Data.ToArray(); 60 | } 61 | 62 | public static T DeserializeJson(byte[] data) 63 | { 64 | return JsonSerializer.Deserialize(data, jsonOptions)!; 65 | } 66 | 67 | public static async Task DeserializeJsonAsync(string filePath, Type type) 68 | { 69 | using var stream = File.OpenRead(filePath); 70 | return (await JsonSerializer.DeserializeAsync(stream, type, jsonOptions))!; 71 | } 72 | 73 | public static string SerializeJson(object src) 74 | { 75 | return JsonSerializer.Serialize(src, src.GetType(), jsonOptions); 76 | } 77 | 78 | public static string SerializeJson(object src, Type type) 79 | { 80 | return JsonSerializer.Serialize(src, type, jsonOptions); 81 | } 82 | 83 | public static async Task SerializeJsonAsync(Stream stream, object src, Type type) 84 | { 85 | await JsonSerializer.SerializeAsync(stream, src, type, jsonOptions); 86 | } 87 | 88 | public static T SerializeXml(XElement doc) 89 | { 90 | BondWriter bw = new(doc); 91 | byte[] packed = bw.Write(); 92 | return DeserializeBond(packed); 93 | } 94 | 95 | public static async Task SerializeXmlAsync(string filePath, Type type) 96 | { 97 | using var stream = File.OpenRead(filePath); 98 | XDocument doc = await XDocument.LoadAsync(stream, LoadOptions.None, new CancellationTokenSource().Token); 99 | BondWriter br = new(doc.Root!); 100 | var input = new InputBuffer(br.Write()); 101 | var reader = new CompactBinaryReader(input, 2); 102 | var deserializer = new Bond.Deserializer>(type); 103 | return deserializer.Deserialize(reader); 104 | } 105 | 106 | public static BondReadResult DeserializeXml(byte[] data) 107 | { 108 | BondReader br = new(data); 109 | return br.Read(); 110 | } 111 | } 112 | } 113 | -------------------------------------------------------------------------------- /Core/Settings/UserSettings.cs: -------------------------------------------------------------------------------- 1 |  2 | using System.IO; 3 | 4 | namespace InfiniteVariantTool.Core.Settings 5 | { 6 | // Properties.Settings can't share settings beetween different executables so implement my own 7 | 8 | public class UserSettings : SettingsBase 9 | { 10 | public UserSettings(string name) 11 | : base(name) 12 | { 13 | } 14 | 15 | private static UserSettings? instance = null; 16 | public static UserSettings Instance 17 | { 18 | get 19 | { 20 | if (instance == null) 21 | { 22 | instance = new("InfiniteVariantTool"); 23 | } 24 | return instance; 25 | } 26 | } 27 | 28 | [Setting("Directory of your Halo Infinite installation")] 29 | public string GameDirectory { get; set; } = @"C:\Program Files (x86)\Steam\steamapps\common\Halo Infinite"; 30 | 31 | [Setting("Directory where your installed variants are stored")] 32 | public string VariantDirectory { get; set; } = Path.Combine(SettingsDirectoryVar, "variants"); 33 | 34 | [Setting("Language that Halo Infinite is currently set to display")] 35 | public string Language { get; set; } = "auto"; 36 | 37 | [Setting("Check for updates on program start and prompt to download if available")] 38 | public bool CheckForUpdates { get; set; } = true; 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /Core/Updater.cs: -------------------------------------------------------------------------------- 1 | using Octokit; 2 | using System; 3 | using System.Collections.Generic; 4 | using System.Diagnostics.CodeAnalysis; 5 | using System.Linq; 6 | using System.Reflection; 7 | using System.Text; 8 | using System.Text.RegularExpressions; 9 | using System.Threading.Tasks; 10 | 11 | namespace InfiniteVariantTool.Core 12 | { 13 | public class Updater 14 | { 15 | private const string gitHubUser = "soupstream"; 16 | private const string gitHubRepo = "InfiniteVariantTool"; 17 | public static string GitHubRepoUrl => $"https://github.com/{gitHubUser}/{gitHubRepo}"; 18 | public static string GitHubReleaseUrl => GitHubRepoUrl + "/releases/latest"; 19 | private GitHubClient client; 20 | 21 | public Updater() 22 | { 23 | client = new GitHubClient(new ProductHeaderValue("infinite-variant-tool")); 24 | } 25 | 26 | public static Version CurrentVersion => Assembly.GetExecutingAssembly().GetName().Version!; 27 | 28 | public async Task GetLatestVersion() 29 | { 30 | var releases = await client.Repository.Release.GetAll(gitHubUser, gitHubRepo); 31 | if (releases.Any()) 32 | { 33 | return ParseVersion(releases[0].TagName); 34 | } 35 | return null; 36 | } 37 | 38 | // looser version parsing rules 39 | private Version? ParseVersion(string versionStr) 40 | { 41 | // look for version number with regex and pick largest match 42 | string? match = Regex.Matches(versionStr, "[0-9.]+") 43 | .Aggregate((string?)null, (max, cur) => (max?.Length ?? 0) > cur.Value.Length ? max : cur.Value); 44 | if (match != null) 45 | { 46 | return new Version(match).ZeroMissingFields(); 47 | } 48 | return null; 49 | } 50 | } 51 | 52 | public static class VersionExtensions 53 | { 54 | public static Version ZeroMissingFields(this Version version) 55 | { 56 | return new Version( 57 | Math.Max(0, version.Major), 58 | Math.Max(0, version.Minor), 59 | Math.Max(0, version.Build), 60 | Math.Max(0, version.Revision)); 61 | } 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /Core/UrlHasher.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Text; 5 | using System.Text.RegularExpressions; 6 | using System.IO; 7 | using Newtonsoft.Json; 8 | using System.Buffers.Binary; 9 | using System.Diagnostics.CodeAnalysis; 10 | using InfiniteVariantTool.Core.Utils; 11 | using InfiniteVariantTool.Core.BondSchema; 12 | 13 | namespace InfiniteVariantTool.Core 14 | { 15 | public class UrlHasher 16 | { 17 | public static ulong HashUrl(string url) 18 | { 19 | return Hash(GetDataToHash(new Uri(url))); 20 | } 21 | 22 | public static ulong HashUrl(string path, string query, ApiManifest.Endpoint endpoint, ApiManifest.Authority authority, bool forceGeneric = false) 23 | { 24 | return Hash(GetDataToHash(path, query, endpoint, authority, forceGeneric)); 25 | } 26 | 27 | // FNV-1a 28 | private static ulong Hash(byte[] data) 29 | { 30 | if (data.Length == 0) 31 | { 32 | return 0; 33 | } 34 | ulong offsetBasis = 0xcbf29ce484222325; 35 | ulong prime = 0x100000001b3; 36 | ulong hash = offsetBasis; 37 | foreach (byte b in data) 38 | { 39 | hash ^= b; 40 | hash *= prime; 41 | } 42 | return hash; 43 | } 44 | 45 | private static ulong Hash(List data) 46 | { 47 | ulong hash = 0; 48 | foreach (byte[] b in data) 49 | { 50 | hash ^= Hash(b); 51 | } 52 | return hash; 53 | } 54 | 55 | private static List GetDataToHash(Uri uri, ApiManifest.Endpoint endpoint, ApiManifest.Authority authority) 56 | { 57 | return GetDataToHash(uri.AbsolutePath, uri.Query, endpoint, authority, false); 58 | } 59 | private static List GetDataToHash(string path, string query, ApiManifest.Endpoint endpoint, ApiManifest.Authority authority, bool forceGeneric) 60 | { 61 | if (!forceGeneric && authority.AuthenticationMethods.Contains(0)) 62 | { 63 | return new List() 64 | { 65 | Encoding.UTF8.GetBytes(endpoint.EndpointId), 66 | Encoding.UTF8.GetBytes(path), 67 | Encoding.UTF8.GetBytes(query) 68 | }; 69 | } 70 | else 71 | { 72 | return GetDataToHash(authority.Hostname!, path, query, authority.Scheme, authority.Port ?? throw new InvalidOperationException("expected port number, got null")); 73 | } 74 | } 75 | 76 | private static List GetDataToHash(Uri uri, int scheme = 2, int port = 443) 77 | { 78 | return GetDataToHash(uri.Host, uri.AbsolutePath, uri.Query, scheme, port); 79 | } 80 | 81 | private static List GetDataToHash(string host, string path, string query, int scheme = 2, int port = 443) 82 | { 83 | byte[] schemeBytes = new byte[sizeof(int)]; 84 | byte[] portBytes = new byte[sizeof(int)]; 85 | BinaryPrimitives.WriteInt32LittleEndian(schemeBytes, scheme); 86 | BinaryPrimitives.WriteInt32LittleEndian(portBytes, port); 87 | return new List() 88 | { 89 | Encoding.UTF8.GetBytes(host), 90 | Encoding.UTF8.GetBytes(path), 91 | Encoding.UTF8.GetBytes(query), 92 | schemeBytes, 93 | portBytes 94 | }; 95 | } 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /Core/Utils/Exceptions.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Text; 5 | using System.Threading.Tasks; 6 | 7 | namespace InfiniteVariantTool.Core.Utils 8 | { 9 | public class LanguageNotFoundException : Exception 10 | { 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /Core/Utils/ExtensionMethods.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Xml.Linq; 5 | using System.Diagnostics.CodeAnalysis; 6 | using System.Text; 7 | using System.Text.RegularExpressions; 8 | using System.Reflection; 9 | 10 | namespace InfiniteVariantTool.Core.Utils 11 | { 12 | public static class MyExtensions 13 | { 14 | public static string GetText(this XElement xel) 15 | { 16 | XNode? node = xel.Nodes().FirstOrDefault(child => child is XText); 17 | if (node is XText textNode) 18 | { 19 | return textNode.Value; 20 | } 21 | else 22 | { 23 | return ""; 24 | } 25 | } 26 | 27 | public static void SetText(this XElement xel, string text) 28 | { 29 | XNode? node = xel.Nodes().FirstOrDefault(child => child is XText); 30 | if (node is XText textNode) 31 | { 32 | textNode.Value = text; 33 | } 34 | else 35 | { 36 | xel.Add(new XText(text)); 37 | } 38 | } 39 | 40 | public static TEnum? ToEnum(this string value) where TEnum : struct 41 | { 42 | return Enum.TryParse(value, out TEnum result) ? result : null; 43 | } 44 | 45 | public static bool TryFirst(this IEnumerable source, [MaybeNullWhen(false)] out TSource value, Func predicate) 46 | { 47 | foreach (TSource item in source) 48 | { 49 | if (predicate(item)) 50 | { 51 | value = item; 52 | return true; 53 | } 54 | } 55 | value = default; 56 | return false; 57 | } 58 | 59 | public static bool TryFirst(this IEnumerable source, [MaybeNullWhen(false)] out TSource value) 60 | { 61 | if (source.Any()) 62 | { 63 | value = source.First(); 64 | return true; 65 | } 66 | value = default; 67 | return false; 68 | } 69 | 70 | public static XProcessingInstruction? GetProcessingInstruction(this XDocument doc) 71 | { 72 | return doc.Nodes() 73 | .OfType() 74 | .FirstOrDefault(node => node.Target == Constants.AppName); 75 | } 76 | 77 | public static Dictionary GetAttributes(this XProcessingInstruction xpi) 78 | { 79 | // not 100% robust but good enough 80 | var matches = Regex.Matches(xpi.Data, "([a-zA-Z0-9_]*) *= *\"([^\"]*)"); 81 | Dictionary attributes = new(); 82 | foreach (Match match in matches) 83 | { 84 | attributes[match.Groups[1].Value] = match.Groups[2].Value; 85 | } 86 | return attributes; 87 | } 88 | 89 | public static void SetAttributes(this XProcessingInstruction xpi, Dictionary attributes) 90 | { 91 | xpi.Data = string.Join(' ', attributes.Select(entry => $"{entry.Key}=\"{entry.Value}\"")); 92 | } 93 | 94 | public static string? GetAttribute(this XProcessingInstruction xpi, string key) 95 | { 96 | var attributes = xpi.GetAttributes(); 97 | return attributes.GetValueOrDefault(key); 98 | } 99 | 100 | public static void SetAttribute(this XProcessingInstruction xpi, string key, string value) 101 | { 102 | var attributes = xpi.GetAttributes(); 103 | attributes[key] = value; 104 | xpi.SetAttributes(attributes); 105 | } 106 | 107 | public static string? GetVersion(this XDocument doc) 108 | { 109 | return doc.GetProcessingInstruction()?.GetAttribute("version"); 110 | } 111 | 112 | public static void SetProcessingInstructionAttribute(this XDocument doc, string key, string value) 113 | { 114 | XProcessingInstruction? xpi = doc.GetProcessingInstruction(); 115 | if (xpi == null) 116 | { 117 | xpi = new XProcessingInstruction(Constants.AppName, ""); 118 | doc.AddFirst(xpi); 119 | } 120 | xpi.SetAttribute(key, value); 121 | } 122 | 123 | public static void SetVersion(this XDocument doc, string version) 124 | { 125 | doc.SetProcessingInstructionAttribute("version", version); 126 | } 127 | 128 | public static void SetVersion(this XDocument doc) 129 | { 130 | string? version = Assembly.GetExecutingAssembly().GetName().Version?.ToString(); 131 | if (version != null) 132 | { 133 | doc.SetVersion(version); 134 | } 135 | } 136 | 137 | public static void SaveVersioned(this XDocument doc, string filename) 138 | { 139 | doc.SetVersion(); 140 | doc.Save(filename); 141 | } 142 | 143 | public static void SaveVersioned(this XElement xel, string filename) 144 | { 145 | XDocument doc = new(xel); 146 | doc.SetVersion(); 147 | doc.Save(filename); 148 | } 149 | } 150 | } 151 | -------------------------------------------------------------------------------- /Core/Utils/OrderedDictionary.cs: -------------------------------------------------------------------------------- 1 | using System.Collections; 2 | using System.Collections.Generic; 3 | 4 | namespace InfiniteVariantTool.Core.Utils 5 | { 6 | // https://referencesource.microsoft.com/#System.Web.Extensions/Util/OrderedDictionary.cs 7 | 8 | public class OrderedDictionary : IDictionary where TKey : notnull 9 | { 10 | private Dictionary _dictionary; 11 | private List _keys; 12 | private List _values; 13 | 14 | // when true: inserting an existing key moves the entry to the end of the list 15 | public bool MoveExistingEntries { get; set; } = false; 16 | 17 | // Cannot easily support ctor that takes IEqualityComparer, since List doesn't have an easy 18 | // way to use the IEqualityComparer. 19 | public OrderedDictionary() 20 | : this(0) 21 | { 22 | } 23 | 24 | public OrderedDictionary(int capacity) 25 | { 26 | _dictionary = new Dictionary(capacity); 27 | _keys = new List(capacity); 28 | _values = new List(capacity); 29 | } 30 | 31 | public int Count 32 | { 33 | get 34 | { 35 | return _dictionary.Count; 36 | } 37 | } 38 | 39 | public ICollection Keys 40 | { 41 | get 42 | { 43 | return _keys.AsReadOnly(); 44 | } 45 | } 46 | 47 | public TValue this[TKey key] 48 | { 49 | get 50 | { 51 | return _dictionary[key]; 52 | } 53 | set 54 | { 55 | int index = _keys.IndexOf(key); 56 | if (index == -1) 57 | { 58 | _keys.Add(key); 59 | _values.Add(value); 60 | _dictionary[key] = value; 61 | } 62 | else 63 | { 64 | if (MoveExistingEntries) 65 | { 66 | // If key has already been added, we must first remove it from the lists so it is not 67 | // in the lists multiple times. 68 | RemoveFromListsByIndex(index); 69 | _keys.Add(key); 70 | _values.Add(value); 71 | } 72 | 73 | _dictionary[key] = value; 74 | } 75 | } 76 | } 77 | 78 | public ICollection Values 79 | { 80 | get 81 | { 82 | return _values.AsReadOnly(); 83 | } 84 | } 85 | 86 | public void Add(TKey key, TValue value) 87 | { 88 | // Dictionary.Add() will throw if it already contains key 89 | _dictionary.Add(key, value); 90 | _keys.Add(key); 91 | _values.Add(value); 92 | } 93 | 94 | public void Clear() 95 | { 96 | _dictionary.Clear(); 97 | _keys.Clear(); 98 | _values.Clear(); 99 | } 100 | 101 | public bool ContainsKey(TKey key) 102 | { 103 | return _dictionary.ContainsKey(key); 104 | } 105 | 106 | public bool ContainsValue(TValue value) 107 | { 108 | return _dictionary.ContainsValue(value); 109 | } 110 | 111 | public IEnumerator> GetEnumerator() 112 | { 113 | int i = 0; 114 | // Must use foreach instead of a for loop, since we want the underlying List enumerator to 115 | // throw an exception if the list is modified during enumeration. 116 | foreach (TKey key in _keys) 117 | { 118 | yield return new KeyValuePair(key, _values[i]); 119 | i++; 120 | } 121 | } 122 | 123 | private void RemoveFromLists(TKey key) 124 | { 125 | int index = _keys.IndexOf(key); 126 | if (index != -1) 127 | { 128 | RemoveFromListsByIndex(index); 129 | } 130 | } 131 | 132 | private void RemoveFromListsByIndex(int index) 133 | { 134 | _keys.RemoveAt(index); 135 | _values.RemoveAt(index); 136 | } 137 | 138 | public bool Remove(TKey key) 139 | { 140 | RemoveFromLists(key); 141 | return _dictionary.Remove(key); 142 | } 143 | 144 | public bool TryGetValue(TKey key, out TValue value) 145 | { 146 | return _dictionary.TryGetValue(key, out value!); 147 | } 148 | 149 | #region ICollection> Members 150 | bool ICollection>.IsReadOnly 151 | { 152 | get 153 | { 154 | return ((ICollection>)_dictionary).IsReadOnly; 155 | } 156 | } 157 | 158 | void ICollection>.Add(KeyValuePair item) 159 | { 160 | Add(item.Key, item.Value); 161 | } 162 | 163 | bool ICollection>.Contains(KeyValuePair item) 164 | { 165 | return ((ICollection>)_dictionary).Contains(item); 166 | } 167 | 168 | void ICollection>.CopyTo(KeyValuePair[] array, int arrayIndex) 169 | { 170 | ((ICollection>)_dictionary).CopyTo(array, arrayIndex); 171 | } 172 | 173 | bool ICollection>.Remove(KeyValuePair item) 174 | { 175 | bool removed = ((ICollection>)_dictionary).Remove(item); 176 | 177 | // Only remove from lists if it was removed from the dictionary, since the dictionary may contain 178 | // the key but not the value. 179 | if (removed) 180 | { 181 | RemoveFromLists(item.Key); 182 | } 183 | 184 | return removed; 185 | } 186 | #endregion 187 | 188 | #region IEnumerable Members 189 | IEnumerator IEnumerable.GetEnumerator() 190 | { 191 | return GetEnumerator(); 192 | } 193 | #endregion 194 | } 195 | } 196 | -------------------------------------------------------------------------------- /Core/Utils/Util.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.IO; 5 | using System.Xml.Linq; 6 | using System.Globalization; 7 | using System.Threading; 8 | using Newtonsoft.Json; 9 | using System.Text.RegularExpressions; 10 | using InfiniteVariantTool.Core.Serialization; 11 | using InfiniteVariantTool.Core.Cache; 12 | using InfiniteVariantTool.Core.Settings; 13 | using System.Reflection; 14 | using System.Diagnostics; 15 | 16 | namespace InfiniteVariantTool.Core.Utils 17 | { 18 | public class Util 19 | { 20 | public static byte[] ListToBlob(XElement list) 21 | { 22 | List blob = new(); 23 | foreach (var item in list.Elements()) 24 | { 25 | blob.Add(byte.Parse(item.GetText())); 26 | } 27 | return blob.ToArray(); 28 | } 29 | 30 | public static bool IsNodeGUID(XElement node) 31 | { 32 | if (node.Elements().Count() == 4) 33 | { 34 | BondType[] schema = { BondType.uint32, BondType.uint16, BondType.uint16, BondType.uint64 }; 35 | for (int i = 0; i < 4; i++) 36 | { 37 | if (node.Elements().ElementAt(i).Name != schema[i].ToString()) 38 | { 39 | return false; 40 | } 41 | } 42 | return true; 43 | } 44 | return false; 45 | } 46 | 47 | public static bool ArrayStartsWith(T[] target, T[] pattern) where T : IEquatable 48 | { 49 | if (target.Length < pattern.Length) 50 | { 51 | return false; 52 | } 53 | 54 | for (int i = 0; i < pattern.Length; i++) 55 | { 56 | if (!EqualityComparer.Default.Equals(target[i], pattern[i])) 57 | { 58 | return false; 59 | } 60 | } 61 | 62 | return true; 63 | } 64 | 65 | public static bool ArrayContains(T[] target, T[] pattern) where T : IEquatable 66 | { 67 | for (int i = 0; i < target.Length - pattern.Length; i++) 68 | { 69 | for (int j = 0; j < pattern.Length; j++) 70 | { 71 | if (!EqualityComparer.Default.Equals(target[i + j], pattern[j])) 72 | { 73 | break; 74 | } 75 | else if (j == pattern.Length - 1) 76 | { 77 | return true; 78 | } 79 | } 80 | } 81 | return false; 82 | } 83 | 84 | public static string NullTerminate(string str) 85 | { 86 | int idx = str.IndexOf('\0'); 87 | if (idx == -1) 88 | { 89 | return str; 90 | } 91 | else 92 | { 93 | return str[..idx]; 94 | } 95 | } 96 | 97 | public static bool NullableSequenceEqual(IEnumerable? first, IEnumerable? second) 98 | { 99 | if (first != null && second != null) 100 | { 101 | return first.SequenceEqual(second); 102 | } 103 | else 104 | { 105 | return first == second; 106 | } 107 | } 108 | } 109 | 110 | public class Culture : IDisposable 111 | { 112 | private readonly CultureInfo originalCulture = Thread.CurrentThread.CurrentCulture; 113 | 114 | public Culture(string cultureName) 115 | : this(CultureInfo.GetCultureInfo(cultureName)) 116 | { 117 | } 118 | 119 | public Culture(CultureInfo culture) 120 | { 121 | Thread.CurrentThread.CurrentCulture = culture; 122 | } 123 | 124 | public void Dispose() 125 | { 126 | Thread.CurrentThread.CurrentCulture = originalCulture; 127 | } 128 | } 129 | } 130 | -------------------------------------------------------------------------------- /Core/lib/oo2core_9_win64.dll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/soupstream/InfiniteVariantTool/17e407f6939e875a0dd0a6c182eef542f4079012/Core/lib/oo2core_9_win64.dll -------------------------------------------------------------------------------- /Directory.Build.props: -------------------------------------------------------------------------------- 1 | 2 | 3 | 0.6.0.0 4 | 5 | 6 | -------------------------------------------------------------------------------- /GUI/App.xaml: -------------------------------------------------------------------------------- 1 |  8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 19 | 23 | 27 | 31 | 35 | 36 | 37 | 38 | -------------------------------------------------------------------------------- /GUI/App.xaml.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Configuration; 4 | using System.Data; 5 | using System.Globalization; 6 | using System.Linq; 7 | using System.Threading; 8 | using System.Threading.Tasks; 9 | using System.Windows; 10 | using System.Windows.Data; 11 | using System.Windows.Markup; 12 | using System.Windows.Threading; 13 | 14 | namespace InfiniteVariantTool.GUI 15 | { 16 | /// 17 | /// Interaction logic for App.xaml 18 | /// 19 | public partial class App : Application 20 | { 21 | private void Application_DispatcherUnhandledException(object sender, DispatcherUnhandledExceptionEventArgs e) 22 | { 23 | ErrorWindow errorWindow = new(e.Exception.ToString(), !IsExceptionRecoverable(e.Exception)); 24 | errorWindow.Owner = Current.Windows.OfType().FirstOrDefault(window => window.IsActive); 25 | errorWindow.Show(); 26 | e.Handled = true; 27 | } 28 | 29 | private bool IsExceptionRecoverable(Exception e) 30 | { 31 | Exception? currentException = e; 32 | while (currentException != null) 33 | { 34 | if (currentException is XamlParseException) 35 | { 36 | return false; 37 | } 38 | currentException = currentException.InnerException; 39 | } 40 | return true; 41 | } 42 | 43 | 44 | private const string UniqueEventName = "7d939ec9-437e-4438-8b96-2e1fb897e52a"; 45 | private const string UniqueMutexName = "a56f78b5-6734-430d-b4c2-227e8e2d38b0"; 46 | private EventWaitHandle? eventWaitHandle; 47 | private Mutex? mutex; 48 | private void Application_Startup(object sender, StartupEventArgs e) 49 | { 50 | mutex = new Mutex(true, UniqueMutexName, out bool isOwned); 51 | eventWaitHandle = new EventWaitHandle(false, EventResetMode.AutoReset, UniqueEventName); 52 | 53 | // So, R# would not give a warning that this variable is not used. 54 | GC.KeepAlive(mutex); 55 | 56 | if (isOwned) 57 | { 58 | // Spawn a thread which will be waiting for our event 59 | Thread thread = new(() => 60 | { 61 | while (eventWaitHandle.WaitOne()) 62 | { 63 | Current.Dispatcher.BeginInvoke(() => ((MainWindow)Current.MainWindow).BringToForeground()); 64 | } 65 | }); 66 | 67 | // It is important mark it as background otherwise it will prevent app from exiting. 68 | thread.IsBackground = true; 69 | 70 | thread.Start(); 71 | return; 72 | } 73 | 74 | // Notify other instance so it could bring itself to foreground. 75 | eventWaitHandle.Set(); 76 | 77 | // Terminate this instance. 78 | Shutdown(); 79 | } 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /GUI/AssemblyInfo.cs: -------------------------------------------------------------------------------- 1 | using System.Windows; 2 | 3 | [assembly: ThemeInfo( 4 | ResourceDictionaryLocation.None, //where theme specific resource dictionaries are located 5 | //(used if a resource is not found in the page, 6 | // or application resource dictionaries) 7 | ResourceDictionaryLocation.SourceAssembly //where the generic resource dictionary is located 8 | //(used if a resource is not found in the page, 9 | // app, or any theme specific resource dictionaries) 10 | )] 11 | -------------------------------------------------------------------------------- /GUI/GUI.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | WinExe 5 | net6.0-windows 6 | true 7 | enable 8 | InfiniteVariantTool.$(MSBuildProjectName.Replace(" ", "_")) 9 | InfiniteVariantTool 10 | ..\icon.ico 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /GUI/Models/LanguageModel.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Text; 5 | using System.Threading.Tasks; 6 | 7 | namespace InfiniteVariantTool.GUI 8 | { 9 | public class LanguageModel 10 | { 11 | public string Name { get; set; } 12 | public string Code { get; set; } 13 | public LanguageModel(string name, string code) 14 | { 15 | Name = name; 16 | Code = code; 17 | } 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /GUI/Models/VariantModel.cs: -------------------------------------------------------------------------------- 1 | using InfiniteVariantTool.Core.Cache; 2 | using InfiniteVariantTool.Core.Variants; 3 | using System; 4 | using System.Collections.Generic; 5 | using System.ComponentModel; 6 | using System.Linq; 7 | using System.Text; 8 | using System.Threading.Tasks; 9 | 10 | namespace InfiniteVariantTool.GUI 11 | { 12 | public class VariantModel : NotifyPropertyChanged 13 | { 14 | public VariantModel() 15 | { 16 | name = ""; 17 | description = ""; 18 | } 19 | 20 | public VariantModel(VariantAsset entry, bool isUserVariant) 21 | { 22 | name = entry.Variant.PublicName; 23 | description = entry.Variant.Description; 24 | enabled = entry.Enabled; 25 | assetId = (Guid)entry.Variant.AssetId; 26 | versionId = (Guid)entry.Variant.VersionId; 27 | Type = entry.Type.EnumValue; 28 | Filename = entry.FilePath; 29 | IsUserVariant = isUserVariant; 30 | } 31 | 32 | public VariantModel(VariantModel variant) 33 | { 34 | name = variant.name; 35 | description = variant.description; 36 | enabled = variant.enabled; 37 | assetId = variant.assetId; 38 | versionId = variant.versionId; 39 | Type = variant.Type; 40 | Filename = variant.Filename; 41 | IsUserVariant = variant.IsUserVariant; 42 | } 43 | 44 | protected string name; 45 | public string Name 46 | { 47 | get => name; 48 | set 49 | { 50 | if (name != value) 51 | { 52 | OnBeforePropertyChange(); 53 | name = value; 54 | OnPropertyChange(); 55 | } 56 | } 57 | } 58 | 59 | protected string description; 60 | public string Description 61 | { 62 | get => description; 63 | set 64 | { 65 | if (description != value) 66 | { 67 | OnBeforePropertyChange(); 68 | description = value; 69 | OnPropertyChange(); 70 | } 71 | } 72 | } 73 | 74 | protected bool? enabled; 75 | public bool? Enabled 76 | { 77 | get => enabled; 78 | set 79 | { 80 | if (enabled != value) 81 | { 82 | OnBeforePropertyChange(); 83 | enabled = value; 84 | OnPropertyChange(); 85 | } 86 | } 87 | } 88 | 89 | protected Guid assetId; 90 | public Guid AssetId 91 | { 92 | get => assetId; 93 | set 94 | { 95 | if (assetId != value) 96 | { 97 | OnBeforePropertyChange(); 98 | assetId = value; 99 | OnPropertyChange(); 100 | } 101 | } 102 | } 103 | 104 | protected Guid versionId; 105 | public Guid VersionId 106 | { 107 | get => versionId; 108 | set 109 | { 110 | if (versionId != value) 111 | { 112 | OnBeforePropertyChange(); 113 | versionId = value; 114 | OnPropertyChange(); 115 | } 116 | } 117 | } 118 | 119 | public VariantTypeEnum Type { get; set; } 120 | public string? Filename { get; set; } 121 | public bool IsUserVariant { get; set; } 122 | } 123 | } 124 | -------------------------------------------------------------------------------- /GUI/Properties/PublishProfiles/FrameworkDependent.pubxml: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | Release 5 | Any CPU 6 | ..\publish\framework_dependent 7 | FileSystem 8 | net6.0-windows 9 | win-x64 10 | false 11 | true 12 | true 13 | true 14 | 15 | -------------------------------------------------------------------------------- /GUI/Properties/PublishProfiles/SelfContained.pubxml: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | Release 5 | Any CPU 6 | ..\publish\self_contained 7 | FileSystem 8 | net6.0-windows 9 | win-x64 10 | true 11 | true 12 | true 13 | true 14 | true 15 | 16 | -------------------------------------------------------------------------------- /GUI/Utils/Converters.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Globalization; 4 | using System.Linq; 5 | using System.Text; 6 | using System.Threading.Tasks; 7 | using System.Windows; 8 | using System.Windows.Data; 9 | 10 | namespace InfiniteVariantTool.GUI 11 | { 12 | 13 | public class NullConverter : IValueConverter 14 | { 15 | public NullConverter(T nullValue, T notNullValue) 16 | { 17 | Null = nullValue; 18 | NotNull = notNullValue; 19 | } 20 | 21 | public T Null { get; set; } 22 | public T NotNull { get; set; } 23 | 24 | public object? Convert(object value, Type targetType, object parameter, CultureInfo culture) 25 | { 26 | return value == null ? Null : NotNull; 27 | } 28 | 29 | public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) 30 | { 31 | throw new NotImplementedException(); 32 | } 33 | } 34 | 35 | public sealed class NullToVisibilityConverter : NullConverter 36 | { 37 | public NullToVisibilityConverter() : base(Visibility.Visible, Visibility.Collapsed) 38 | { 39 | 40 | } 41 | } 42 | 43 | public sealed class NullToBooleanConverter : NullConverter 44 | { 45 | public NullToBooleanConverter() : base(false, true) 46 | { 47 | 48 | } 49 | } 50 | 51 | public class BooleanConverter : IValueConverter 52 | { 53 | public BooleanConverter(T trueValue, T falseValue) 54 | { 55 | True = trueValue; 56 | False = falseValue; 57 | } 58 | 59 | public T True { get; set; } 60 | public T False { get; set; } 61 | 62 | public virtual object? Convert(object value, Type targetType, object parameter, CultureInfo culture) 63 | { 64 | return value is bool && ((bool)value) ? True : False; 65 | } 66 | 67 | public virtual object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) 68 | { 69 | return value is T && EqualityComparer.Default.Equals((T)value, True); 70 | } 71 | } 72 | 73 | public sealed class BooleanToVisibilityConverter : BooleanConverter 74 | { 75 | public BooleanToVisibilityConverter() : base(Visibility.Visible, Visibility.Collapsed) 76 | { 77 | 78 | } 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /GUI/Utils/HyperlinkExtensions.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.Xaml.Behaviors; 2 | using System; 3 | using System.Collections.Generic; 4 | using System.Diagnostics; 5 | using System.Linq; 6 | using System.Text; 7 | using System.Threading.Tasks; 8 | using System.Windows; 9 | using System.Windows.Documents; 10 | 11 | namespace InfiniteVariantTool.GUI 12 | { 13 | public class OpenHyperlinkBehavior : Behavior 14 | { 15 | protected override void OnAttached() 16 | { 17 | base.OnAttached(); 18 | AssociatedObject.RequestNavigate += Hyperlink_RequestNavigate; 19 | } 20 | 21 | protected override void OnDetaching() 22 | { 23 | AssociatedObject.RequestNavigate -= Hyperlink_RequestNavigate; 24 | base.OnDetaching(); 25 | } 26 | 27 | private void Hyperlink_RequestNavigate(object sender, System.Windows.Navigation.RequestNavigateEventArgs e) 28 | { 29 | var ps = new ProcessStartInfo(e.Uri.AbsoluteUri) 30 | { 31 | UseShellExecute = true, 32 | Verb = "open" 33 | }; 34 | Process.Start(ps); 35 | e.Handled = true; 36 | } 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /GUI/Utils/IOService.cs: -------------------------------------------------------------------------------- 1 | using Ookii.Dialogs.Wpf; 2 | using System; 3 | using System.Collections.Generic; 4 | using System.Diagnostics; 5 | using System.Diagnostics.CodeAnalysis; 6 | using System.IO; 7 | using System.Linq; 8 | using System.Text; 9 | using System.Threading.Tasks; 10 | using System.Windows; 11 | 12 | namespace InfiniteVariantTool.GUI 13 | { 14 | 15 | public abstract class IOService 16 | { 17 | public abstract void ShowInExplorer(string path); 18 | public abstract string? SelectPath(PathSelectType selectType, string? title = null, string? filter = null, string? initialPath = null); 19 | public bool TrySelectPath([NotNullWhen(true)] out string? path, PathSelectType selectType, string? title = null, string? filter = null, string? initialPath = null) 20 | { 21 | string? result = SelectPath(selectType, title, filter, initialPath); 22 | if (result == null) 23 | { 24 | path = null; 25 | return false; 26 | } 27 | else 28 | { 29 | path = result; 30 | return true; 31 | } 32 | } 33 | } 34 | 35 | public enum PathSelectType 36 | { 37 | Open, 38 | Save, 39 | } 40 | 41 | public class IOServiceImpl : IOService 42 | { 43 | private Window window; 44 | public IOServiceImpl(Window window) 45 | { 46 | this.window = window; 47 | } 48 | 49 | public override void ShowInExplorer(string path) 50 | { 51 | if (File.Exists(path)) 52 | { 53 | Process.Start("explorer.exe", $"/select,\"{path}\""); 54 | } 55 | else 56 | { 57 | Process.Start("explorer.exe", $"\"{path}\""); 58 | } 59 | } 60 | 61 | public override string? SelectPath(PathSelectType selectType, string? title = null, string? filter = null, string? initialPath = null) 62 | { 63 | if (filter != null && filter.StartsWith("Directory")) 64 | { 65 | VistaFolderBrowserDialog dialog = new() 66 | { 67 | UseDescriptionForTitle = true, 68 | ShowNewFolderButton = true, 69 | }; 70 | if (title != null) 71 | { 72 | dialog.Description = title; 73 | } 74 | if (initialPath != null) 75 | { 76 | dialog.SelectedPath = initialPath; 77 | } 78 | if (dialog.ShowDialog(window) == true) 79 | { 80 | return dialog.SelectedPath; 81 | } 82 | } 83 | else 84 | { 85 | VistaFileDialog dialog = selectType switch 86 | { 87 | PathSelectType.Open => new VistaOpenFileDialog(), 88 | PathSelectType.Save => new VistaSaveFileDialog() 89 | { 90 | OverwritePrompt = false 91 | }, 92 | _ => throw new ArgumentException() 93 | }; 94 | if (filter != null) 95 | { 96 | dialog.Filter = filter; 97 | } 98 | if (title != null) 99 | { 100 | dialog.Title = title; 101 | } 102 | if (initialPath != null) 103 | { 104 | dialog.InitialDirectory = initialPath; 105 | } 106 | if (dialog.ShowDialog(window) == true) 107 | { 108 | return dialog.FileName; 109 | } 110 | } 111 | return null; 112 | } 113 | } 114 | } 115 | -------------------------------------------------------------------------------- /GUI/Utils/ListViewExtensions.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.Xaml.Behaviors; 2 | using System; 3 | using System.Collections; 4 | using System.Collections.Generic; 5 | using System.Collections.ObjectModel; 6 | using System.Collections.Specialized; 7 | using System.Linq; 8 | using System.Text; 9 | using System.Threading.Tasks; 10 | using System.Windows; 11 | using System.Windows.Controls; 12 | 13 | namespace InfiniteVariantTool.GUI 14 | { 15 | public class MultiSelectionBehavior : Behavior 16 | { 17 | private bool _isUpdatingTarget; 18 | private bool _isUpdatingSource; 19 | 20 | public static readonly DependencyProperty SelectedItemsProperty = 21 | DependencyProperty.Register(nameof(SelectedItems), typeof(IList), typeof(MultiSelectionBehavior), new UIPropertyMetadata(null, SelectedItemsChanged)); 22 | 23 | public IList SelectedItems 24 | { 25 | get { return (IList)GetValue(SelectedItemsProperty); } 26 | set { SetValue(SelectedItemsProperty, value); } 27 | } 28 | 29 | protected override void OnAttached() 30 | { 31 | base.OnAttached(); 32 | if (SelectedItems != null) 33 | { 34 | AssociatedObject.SelectedItems.Clear(); 35 | foreach (var item in SelectedItems) 36 | { 37 | AssociatedObject.SelectedItems.Add(item); 38 | } 39 | } 40 | } 41 | 42 | private static void SelectedItemsChanged(DependencyObject o, DependencyPropertyChangedEventArgs e) 43 | { 44 | var behavior = o as MultiSelectionBehavior; 45 | if (behavior?.AssociatedObject == null) 46 | { 47 | return; 48 | } 49 | 50 | if (e.OldValue is INotifyCollectionChanged oldValue) 51 | { 52 | oldValue.CollectionChanged -= behavior.SourceCollectionChanged; 53 | behavior.AssociatedObject.SelectionChanged -= behavior.ListBoxSelectionChanged; 54 | } 55 | if (e.NewValue is INotifyCollectionChanged newValue) 56 | { 57 | behavior.AssociatedObject.SelectedItems.Clear(); 58 | foreach (var item in (IEnumerable)newValue) 59 | { 60 | behavior.AssociatedObject.SelectedItems.Add(item); 61 | } 62 | 63 | behavior.AssociatedObject.SelectionChanged += behavior.ListBoxSelectionChanged; 64 | newValue.CollectionChanged += behavior.SourceCollectionChanged; 65 | } 66 | } 67 | 68 | void SourceCollectionChanged(object? sender, NotifyCollectionChangedEventArgs e) 69 | { 70 | if (_isUpdatingSource) 71 | { 72 | return; 73 | } 74 | 75 | try 76 | { 77 | _isUpdatingTarget = true; 78 | 79 | if (e.OldItems != null) 80 | { 81 | foreach (var item in e.OldItems) 82 | { 83 | AssociatedObject.SelectedItems.Remove(item); 84 | } 85 | } 86 | 87 | if (e.NewItems != null) 88 | { 89 | foreach (var item in e.NewItems) 90 | { 91 | AssociatedObject.SelectedItems.Add(item); 92 | } 93 | } 94 | 95 | if (e.Action == NotifyCollectionChangedAction.Reset) 96 | { 97 | AssociatedObject.SelectedItems.Clear(); 98 | } 99 | } 100 | finally 101 | { 102 | _isUpdatingTarget = false; 103 | } 104 | } 105 | 106 | private void ListBoxSelectionChanged(object sender, SelectionChangedEventArgs e) 107 | { 108 | if (_isUpdatingTarget) 109 | { 110 | return; 111 | } 112 | 113 | var selectedItems = SelectedItems; 114 | if (selectedItems == null) 115 | { 116 | return; 117 | } 118 | 119 | try 120 | { 121 | _isUpdatingSource = true; 122 | 123 | foreach (var item in e.RemovedItems) 124 | { 125 | selectedItems.Remove(item); 126 | } 127 | 128 | foreach (var item in e.AddedItems) 129 | { 130 | selectedItems.Add(item); 131 | } 132 | } 133 | finally 134 | { 135 | _isUpdatingSource = false; 136 | } 137 | } 138 | } 139 | 140 | 141 | public class ScrollToSelectedItemBehaviour : Behavior 142 | { 143 | protected override void OnAttached() 144 | { 145 | base.OnAttached(); 146 | AssociatedObject.SelectionChanged += AssociatedObjectOnSelectionChanged; 147 | AssociatedObject.IsVisibleChanged += AssociatedObjectOnIsVisibleChanged; 148 | } 149 | 150 | protected override void OnDetaching() 151 | { 152 | AssociatedObject.SelectionChanged -= AssociatedObjectOnSelectionChanged; 153 | AssociatedObject.IsVisibleChanged -= AssociatedObjectOnIsVisibleChanged; 154 | base.OnDetaching(); 155 | } 156 | 157 | private static void AssociatedObjectOnIsVisibleChanged(object sender, DependencyPropertyChangedEventArgs e) 158 | { 159 | ScrollIntoFirstSelectedItem(sender); 160 | } 161 | 162 | private static void AssociatedObjectOnSelectionChanged(object sender, SelectionChangedEventArgs e) 163 | { 164 | ScrollIntoFirstSelectedItem(sender); 165 | } 166 | 167 | private static void ScrollIntoFirstSelectedItem(object sender) 168 | { 169 | if (sender is not ListView listBox) 170 | { 171 | return; 172 | } 173 | var selectedItems = listBox.SelectedItems; 174 | if (selectedItems.Count > 0) 175 | { 176 | listBox.ScrollIntoView(selectedItems[0]); 177 | } 178 | } 179 | } 180 | } 181 | -------------------------------------------------------------------------------- /GUI/Utils/NavigationService.cs: -------------------------------------------------------------------------------- 1 | using InfiniteVariantTool.Core; 2 | using System; 3 | using System.Collections.Generic; 4 | using System.Linq; 5 | using System.Text; 6 | using System.Threading.Tasks; 7 | using System.Windows; 8 | 9 | namespace InfiniteVariantTool.GUI 10 | { 11 | public abstract class NavigationService 12 | { 13 | public abstract void Close(); 14 | public abstract void OpenSettingsWindow(); 15 | public abstract void OpenUnpackCacheFileWindow(); 16 | public abstract void OpenPackCacheFileWindow(); 17 | public abstract void OpenUnpackLuaBundleWindow(); 18 | public abstract void OpenPackLuaBundleWindow(); 19 | public abstract void OpenHashUrlWindow(VariantManager variantManager); 20 | public abstract void OpenResultWindow(string output, string? path); 21 | public abstract void OpenExtractWindow(VariantManager variantManager, VariantModel variant); 22 | public abstract void OpenAboutWindow(); 23 | public abstract void OpenUpdateWindow(); 24 | public abstract void OpenUpdateWindow(Version newVersion); 25 | } 26 | 27 | public class NavigationServiceImpl : NavigationService 28 | { 29 | private Window window; 30 | public WindowManager WindowManager { get; private set; } 31 | public NavigationServiceImpl(Window window, WindowManager? windowManager = null) 32 | { 33 | this.window = window; 34 | this.WindowManager = windowManager ?? new WindowManager(window); 35 | } 36 | 37 | public override void Close() 38 | { 39 | window.Close(); 40 | } 41 | 42 | public override void OpenExtractWindow(VariantManager variantManager, VariantModel variant) 43 | { 44 | WindowManager.Show(variantManager, variant); 45 | } 46 | 47 | public override void OpenSettingsWindow() 48 | { 49 | WindowManager.ShowOrActivate(); 50 | } 51 | 52 | public override void OpenUnpackCacheFileWindow() 53 | { 54 | WindowManager.ShowOrActivate(); 55 | } 56 | 57 | public override void OpenPackCacheFileWindow() 58 | { 59 | WindowManager.ShowOrActivate(); 60 | } 61 | 62 | public override void OpenUnpackLuaBundleWindow() 63 | { 64 | WindowManager.ShowOrActivate(); 65 | } 66 | 67 | public override void OpenPackLuaBundleWindow() 68 | { 69 | WindowManager.ShowOrActivate(); 70 | } 71 | 72 | public override void OpenHashUrlWindow(VariantManager variantManager) 73 | { 74 | WindowManager.ShowOrActivate(variantManager); 75 | } 76 | 77 | public override void OpenResultWindow(string output, string? path = null) 78 | { 79 | WindowManager.Show(output, path); 80 | } 81 | 82 | public override void OpenAboutWindow() 83 | { 84 | WindowManager.ShowOrActivate(); 85 | } 86 | 87 | public override void OpenUpdateWindow() 88 | { 89 | WindowManager.ShowOrActivate(); 90 | } 91 | 92 | public override void OpenUpdateWindow(Version newVersion) 93 | { 94 | WindowManager.Show(newVersion); 95 | } 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /GUI/Utils/NotifyPropertyChanged.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.ComponentModel; 4 | using System.Linq; 5 | using System.Runtime.CompilerServices; 6 | using System.Text; 7 | using System.Threading.Tasks; 8 | 9 | namespace InfiniteVariantTool.GUI 10 | { 11 | public class NotifyPropertyChanged : INotifyPropertyChanged 12 | { 13 | public event PropertyChangedEventHandler? PropertyChanged; 14 | protected void OnPropertyChange([CallerMemberName] string propertyName = "ayy lmao") 15 | { 16 | PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)); 17 | } 18 | 19 | public event PropertyChangedEventHandler? BeforePropertyChanged; 20 | protected void OnBeforePropertyChange([CallerMemberName] string propertyName = "ayy lmao") 21 | { 22 | BeforePropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)); 23 | } 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /GUI/Utils/RelayCommand.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Text; 5 | using System.Threading.Tasks; 6 | using System.Windows.Input; 7 | 8 | namespace InfiniteVariantTool.GUI 9 | { 10 | public abstract class RelayCommand : ICommand 11 | { 12 | public event EventHandler? CanExecuteChanged; 13 | 14 | public void RaiseCanExecuteChanged() 15 | { 16 | CanExecuteChanged?.Invoke(this, EventArgs.Empty); 17 | } 18 | 19 | public abstract bool CanExecute(object? parameter); 20 | 21 | public abstract void Execute(object? parameter); 22 | 23 | public bool IsExecuting { get; protected set; } 24 | } 25 | 26 | public class SyncRelayCommand : RelayCommand 27 | { 28 | private Action execute; 29 | private Func? canExecute; 30 | private Action? onError; 31 | 32 | public SyncRelayCommand(Action execute, Func? canExecute = null, Action? onError = null) 33 | { 34 | this.execute = execute; 35 | this.canExecute = canExecute; 36 | this.onError = onError; 37 | } 38 | 39 | public override bool CanExecute(object? parameter) 40 | { 41 | return canExecute?.Invoke(parameter) ?? true; 42 | } 43 | 44 | public override void Execute(object? parameter) 45 | { 46 | try 47 | { 48 | IsExecuting = true; 49 | execute(parameter); 50 | } 51 | catch (Exception ex) 52 | { 53 | onError?.Invoke(ex); 54 | throw; 55 | } 56 | finally 57 | { 58 | IsExecuting = false; 59 | } 60 | } 61 | } 62 | 63 | public class AsyncRelayCommand : RelayCommand 64 | { 65 | private Func execute; 66 | private Func? canExecute; 67 | private Action? onError; 68 | 69 | public AsyncRelayCommand(Func execute, Func? canExecute = null, Action? onError = null) 70 | { 71 | this.execute = execute; 72 | this.canExecute = canExecute; 73 | this.onError = onError; 74 | } 75 | 76 | public override bool CanExecute(object? parameter) 77 | { 78 | return !IsExecuting && (canExecute?.Invoke(parameter) ?? true); 79 | } 80 | 81 | public override async void Execute(object? parameter) 82 | { 83 | await ExecuteAsync(parameter); 84 | } 85 | 86 | public async Task ExecuteAsync(object? parameter) 87 | { 88 | IsExecuting = true; 89 | RaiseCanExecuteChanged(); 90 | try 91 | { 92 | await execute(parameter); 93 | } 94 | catch (Exception ex) 95 | { 96 | onError?.Invoke(ex); 97 | throw; 98 | } 99 | finally 100 | { 101 | IsExecuting = false; 102 | RaiseCanExecuteChanged(); 103 | } 104 | } 105 | } 106 | } 107 | -------------------------------------------------------------------------------- /GUI/Utils/WindowExtensions.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.Xaml.Behaviors; 2 | using System; 3 | using System.Collections.Generic; 4 | using System.Linq; 5 | using System.Text; 6 | using System.Threading.Tasks; 7 | using System.Windows; 8 | 9 | namespace InfiniteVariantTool.GUI 10 | { 11 | // WindowStartupLocation="CenterOwner" doesn't work when using SizeToContent="WidthAndHeight" 12 | // Adding this behavior fixes the issue 13 | public class FixCenterWindowBehavior : Behavior 14 | { 15 | protected override void OnAttached() 16 | { 17 | base.OnAttached(); 18 | AssociatedObject.SizeChanged += AssociatedObject_SizeChanged; 19 | } 20 | 21 | protected override void OnDetaching() 22 | { 23 | AssociatedObject.SizeChanged -= AssociatedObject_SizeChanged; 24 | base.OnDetaching(); 25 | } 26 | 27 | private void AssociatedObject_SizeChanged(object sender, SizeChangedEventArgs e) 28 | { 29 | AssociatedObject.Top = AssociatedObject.Owner.Top + (AssociatedObject.Owner.Height - e.NewSize.Height) / 2; 30 | AssociatedObject.Left = AssociatedObject.Owner.Left + (AssociatedObject.Owner.Width - e.NewSize.Width) / 2; 31 | AssociatedObject.SizeChanged -= AssociatedObject_SizeChanged; 32 | } 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /GUI/Utils/WindowManager.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Text; 5 | using System.Threading.Tasks; 6 | using System.Windows; 7 | 8 | namespace InfiniteVariantTool.GUI 9 | { 10 | public class WindowManager 11 | { 12 | private Window parent; 13 | private List windows = new(); 14 | private Dictionary singleWindows = new(); 15 | private Dictionary<(Type, Type), Window> singleWindowsByViewModel = new(); 16 | 17 | public WindowManager(Window parent) 18 | { 19 | this.parent = parent; 20 | parent.Closed += (_, _) => CloseAll(); 21 | } 22 | 23 | private void CloseAll() 24 | { 25 | foreach (var window in singleWindows.Values 26 | .Concat(singleWindowsByViewModel.Values) 27 | .Concat(windows)) 28 | { 29 | // not needed if setting owner 30 | // window.Close(); 31 | } 32 | singleWindows.Clear(); 33 | singleWindowsByViewModel.Clear(); 34 | windows.Clear(); 35 | } 36 | 37 | private void OnChildWindowClose(object? sender, EventArgs e) 38 | { 39 | var window = (Window)sender!; 40 | window.Owner.Focus(); 41 | } 42 | 43 | public void Show() where T : Window 44 | { 45 | T window = (T)(Activator.CreateInstance(typeof(T))!); 46 | window.Closed += (_, _) => windows.Remove(window); 47 | windows.Add(window); 48 | if (window.DataContext is ViewModel viewModel) 49 | { 50 | viewModel.IOService = new IOServiceImpl(window); 51 | viewModel.NavigationService = new NavigationServiceImpl(window); 52 | } 53 | window.Owner = parent; 54 | window.Closed += OnChildWindowClose; 55 | window.Show(); 56 | } 57 | 58 | public void Show(params object?[]? args) 59 | where TWindow : Window 60 | where TViewModel : ViewModel 61 | { 62 | TWindow window = (TWindow)(Activator.CreateInstance(typeof(TWindow))!); 63 | TViewModel viewModel = (TViewModel)(Activator.CreateInstance(typeof(TViewModel), args)!); 64 | viewModel.IOService = new IOServiceImpl(window); 65 | viewModel.NavigationService = new NavigationServiceImpl(window); 66 | window.DataContext = viewModel; 67 | window.Closed += (_, _) => windows.Remove(window); 68 | windows.Add(window); 69 | window.Owner = parent; 70 | window.Closed += OnChildWindowClose; 71 | window.Show(); 72 | } 73 | 74 | public void ShowOrActivate() where T : Window 75 | { 76 | if (singleWindows.ContainsKey(typeof(T))) 77 | { 78 | singleWindows[typeof(T)].Activate(); 79 | } 80 | else 81 | { 82 | T window = (T)(Activator.CreateInstance(typeof(T))!); 83 | window.Closed += (_, _) => singleWindows.Remove(typeof(T)); 84 | singleWindows[typeof(T)] = window; 85 | if (window.DataContext is ViewModel viewModel) 86 | { 87 | viewModel.IOService = new IOServiceImpl(window); 88 | viewModel.NavigationService = new NavigationServiceImpl(window); 89 | } 90 | window.Owner = parent; 91 | window.Closed += OnChildWindowClose; 92 | window.Show(); 93 | } 94 | } 95 | 96 | public void ShowOrActivate(params object?[]? args) 97 | where TWindow : Window 98 | where TViewModel : ViewModel 99 | { 100 | var key = (typeof(TWindow), typeof(TViewModel)); 101 | if (singleWindowsByViewModel.ContainsKey(key)) 102 | { 103 | singleWindowsByViewModel[key].Activate(); 104 | } 105 | else 106 | { 107 | TWindow window = (TWindow)(Activator.CreateInstance(typeof(TWindow))!); 108 | TViewModel viewModel = (TViewModel)(Activator.CreateInstance(typeof(TViewModel), args)!); 109 | viewModel.IOService = new IOServiceImpl(window); 110 | viewModel.NavigationService = new NavigationServiceImpl(window); 111 | window.DataContext = viewModel; 112 | window.Closed += (_, _) => singleWindowsByViewModel.Remove(key); 113 | singleWindowsByViewModel[key] = window; 114 | window.Owner = parent; 115 | window.Closed += OnChildWindowClose; 116 | window.Show(); 117 | } 118 | } 119 | 120 | private void CenterWindowOnParent(Window window) 121 | { 122 | window.Left = parent.Left + (parent.Width - window.ActualWidth) / 2; 123 | window.Top = parent.Top + (parent.Height - window.ActualHeight) / 2; 124 | } 125 | } 126 | } 127 | -------------------------------------------------------------------------------- /GUI/ViewModels/AboutViewModel.cs: -------------------------------------------------------------------------------- 1 | using InfiniteVariantTool.Core; 2 | using System; 3 | using System.Collections.Generic; 4 | using System.Linq; 5 | using System.Text; 6 | using System.Threading.Tasks; 7 | 8 | namespace InfiniteVariantTool.GUI 9 | { 10 | public class AboutViewModel : ViewModel 11 | { 12 | public string Version => Updater.CurrentVersion.ToString(3); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /GUI/ViewModels/ExtractViewModel.cs: -------------------------------------------------------------------------------- 1 | using InfiniteVariantTool.Core; 2 | using InfiniteVariantTool.Core.Settings; 3 | using InfiniteVariantTool.Core.Utils; 4 | using InfiniteVariantTool.Core.Variants; 5 | using System; 6 | using System.Collections.Generic; 7 | using System.IO; 8 | using System.Linq; 9 | using System.Text; 10 | using System.Threading.Tasks; 11 | 12 | namespace InfiniteVariantTool.GUI 13 | { 14 | public class ExtractViewModel : ViewModel 15 | { 16 | private VariantManager variantManager; 17 | private VariantModel variant; 18 | public ExtractViewModel(VariantManager variantManager, VariantModel variant) 19 | { 20 | this.variantManager = variantManager; 21 | this.variant = variant; 22 | } 23 | 24 | public string DefaultOutputDirectory 25 | { 26 | get 27 | { 28 | DirectoryFileNameDeduper deduper = new(UserSettings.Instance.VariantDirectory); 29 | string filename = FileUtil.MakeValidFilename(variant.Name); 30 | return deduper.Dedupe(Path.Combine(UserSettings.Instance.VariantDirectory, filename)); 31 | } 32 | } 33 | 34 | public bool IsUgcGameVariant => variant.Type == VariantType.UgcGameVariant.EnumValue; 35 | 36 | private string outputDirectory = ""; 37 | public string OutputDirectory 38 | { 39 | get => outputDirectory; 40 | set 41 | { 42 | if (value != outputDirectory) 43 | { 44 | outputDirectory = value; 45 | OnPropertyChange(); 46 | } 47 | } 48 | } 49 | 50 | private bool extractEngineGameVariant = true; 51 | public bool ExtractEngineGameVariant 52 | { 53 | get => extractEngineGameVariant; 54 | set 55 | { 56 | if (value != extractEngineGameVariant) 57 | { 58 | extractEngineGameVariant = value; 59 | OnPropertyChange(); 60 | } 61 | } 62 | } 63 | 64 | private bool generateNewAssetId = true; 65 | public bool GenerateNewAssetId 66 | { 67 | get => generateNewAssetId; 68 | set 69 | { 70 | if (value != generateNewAssetId) 71 | { 72 | generateNewAssetId = value; 73 | OnPropertyChange(); 74 | } 75 | } 76 | } 77 | 78 | private bool generateNewVersionId = true; 79 | public bool GenerateNewVersionId 80 | { 81 | get => generateNewVersionId; 82 | set 83 | { 84 | if (value != generateNewVersionId) 85 | { 86 | generateNewVersionId = value; 87 | OnPropertyChange(); 88 | } 89 | } 90 | } 91 | 92 | public RelayCommand PickOutputDirectoryCommand => new SyncRelayCommand(_ => 93 | { 94 | if (IOService.TrySelectPath(out string? path, PathSelectType.Open, "Select output folder", "Directory|.")) 95 | { 96 | OutputDirectory = path; 97 | } 98 | }); 99 | 100 | private async Task ExtractVariant() 101 | { 102 | string output; 103 | bool success; 104 | string outputDirectory = OutputDirectory == "" ? DefaultOutputDirectory : OutputDirectory; 105 | try 106 | { 107 | var loadedVariant = await variantManager.GetVariant(variant.AssetId, variant.VersionId, VariantType.FromEnum(variant.Type), true, extractEngineGameVariant); 108 | loadedVariant.GenerateGuids(GenerateNewAssetId, GenerateNewVersionId); 109 | await loadedVariant.Save(outputDirectory); 110 | await variantManager.Flush(); 111 | output = "Success\r\n\r\nVariant extracted to " + outputDirectory; 112 | success = true; 113 | } 114 | catch (Exception ex) 115 | { 116 | output = "Error\r\n\r\n" + ex; 117 | success = false; 118 | } 119 | NavigationService.OpenResultWindow(output, success ? outputDirectory : null); 120 | } 121 | 122 | private RelayCommand? extractCommand; 123 | public RelayCommand ExtractCommand 124 | { 125 | get 126 | { 127 | if (extractCommand == null) 128 | { 129 | extractCommand = new AsyncRelayCommand(async _ => await ExtractVariant()); 130 | } 131 | return extractCommand; 132 | } 133 | } 134 | } 135 | } 136 | -------------------------------------------------------------------------------- /GUI/ViewModels/HashUrlViewModel.cs: -------------------------------------------------------------------------------- 1 | using InfiniteVariantTool.Core; 2 | using InfiniteVariantTool.Core.Cache; 3 | using System; 4 | using System.Collections.Generic; 5 | using System.Linq; 6 | using System.Text; 7 | using System.Threading.Tasks; 8 | 9 | namespace InfiniteVariantTool.GUI 10 | { 11 | public class HashUrlViewModel : ViewModel 12 | { 13 | VariantManager variantManager; 14 | public HashUrlViewModel(VariantManager variantManager) 15 | { 16 | this.variantManager = variantManager; 17 | url = ""; 18 | offlineHash = ""; 19 | onlineHash = ""; 20 | lanHash = ""; 21 | } 22 | 23 | private string GetHash(CacheManager cache, string url) 24 | { 25 | if (url == "") 26 | { 27 | return ""; 28 | } 29 | return cache.Api.CallUrl(url)?.Hash.ToString() ?? "N/A"; 30 | } 31 | 32 | private string url; 33 | public string Url 34 | { 35 | get => url; 36 | set 37 | { 38 | if (value != url) 39 | { 40 | url = value; 41 | OnPropertyChange(nameof(Url)); 42 | 43 | OfflineHash = GetHash(variantManager.OfflineCache, url); 44 | OnlineHash = GetHash(variantManager.OnlineCache, url); 45 | LanHash = GetHash(variantManager.LanCache, url); 46 | } 47 | } 48 | } 49 | 50 | private string offlineHash; 51 | public string OfflineHash 52 | { 53 | get => offlineHash; 54 | set 55 | { 56 | if (value != offlineHash) 57 | { 58 | offlineHash = value; 59 | OnPropertyChange(nameof(OfflineHash)); 60 | } 61 | } 62 | } 63 | 64 | private string onlineHash; 65 | public string OnlineHash 66 | { 67 | get => onlineHash; 68 | set 69 | { 70 | if (value != onlineHash) 71 | { 72 | onlineHash = value; 73 | OnPropertyChange(nameof(OnlineHash)); 74 | } 75 | } 76 | } 77 | 78 | private string lanHash; 79 | public string LanHash 80 | { 81 | get => lanHash; 82 | set 83 | { 84 | if (value != lanHash) 85 | { 86 | lanHash = value; 87 | OnPropertyChange(nameof(LanHash)); 88 | } 89 | } 90 | } 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /GUI/ViewModels/MainViewModel.cs: -------------------------------------------------------------------------------- 1 | using InfiniteVariantTool.Core; 2 | using System; 3 | using System.Collections.Generic; 4 | using System.Linq; 5 | using System.Text; 6 | using System.Threading.Tasks; 7 | 8 | namespace InfiniteVariantTool.GUI 9 | { 10 | public class MainViewModel : ViewModel 11 | { 12 | public VariantViewModel VariantViewModel { get; } 13 | public MainViewModel(IOService ioService, NavigationService navigationService) 14 | { 15 | IOService = ioService; 16 | NavigationService = navigationService; 17 | VariantViewModel = new(ioService, navigationService); 18 | } 19 | 20 | private async Task AutoCheckForUpdate() 21 | { 22 | Version? latestVersion; 23 | try 24 | { 25 | Updater updater = new(); 26 | latestVersion = await updater.GetLatestVersion(); 27 | } 28 | catch 29 | { 30 | // fail silently 31 | return; 32 | } 33 | if (latestVersion != null && latestVersion > Updater.CurrentVersion) 34 | { 35 | NavigationService.OpenUpdateWindow(latestVersion); 36 | } 37 | } 38 | 39 | public RelayCommand OpenSettingsWindowCommand => new SyncRelayCommand(_ => NavigationService.OpenSettingsWindow()); 40 | public RelayCommand OpenHashUrlWindowCommand => new SyncRelayCommand(_ => NavigationService.OpenHashUrlWindow(VariantViewModel.VariantManager!), _ => VariantViewModel.VariantManager != null); 41 | public RelayCommand OpenUnpackCacheFileWindowCommand => new SyncRelayCommand(_ => NavigationService.OpenUnpackCacheFileWindow()); 42 | public RelayCommand OpenPackCacheFileWindowCommand => new SyncRelayCommand(_ => NavigationService.OpenPackCacheFileWindow()); 43 | public RelayCommand OpenUnpackLuaBundleWindowCommand => new SyncRelayCommand(_ => NavigationService.OpenUnpackLuaBundleWindow()); 44 | public RelayCommand OpenPackLuaBundleWindowCommand => new SyncRelayCommand(_ => NavigationService.OpenPackLuaBundleWindow()); 45 | public RelayCommand OpenAboutWindowCommand => new SyncRelayCommand(_ => NavigationService.OpenAboutWindow()); 46 | public RelayCommand ManualCheckForUpdateCommand => new SyncRelayCommand(_ => NavigationService.OpenUpdateWindow()); 47 | public RelayCommand AutoCheckForUpdateCommand => new AsyncRelayCommand(async _ => await AutoCheckForUpdate()); 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /GUI/ViewModels/ResultViewModel.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Text; 5 | using System.Threading.Tasks; 6 | using System.Windows; 7 | 8 | namespace InfiniteVariantTool.GUI 9 | { 10 | public class ResultViewModel : ViewModel 11 | { 12 | public string Output { get; set; } 13 | public string? Path { get; set; } 14 | public ResultViewModel(string output, string? path) 15 | { 16 | Output = output; 17 | Path = path; 18 | } 19 | 20 | public RelayCommand CopyCommand => new SyncRelayCommand(_ => Clipboard.SetText(Output)); 21 | public RelayCommand OpenInExplorerCommand => new SyncRelayCommand( 22 | _ => IOService.ShowInExplorer(Path!), 23 | _ => Path != null); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /GUI/ViewModels/SettingsViewModel.cs: -------------------------------------------------------------------------------- 1 | using InfiniteVariantTool.Core; 2 | using InfiniteVariantTool.Core.Cache; 3 | using InfiniteVariantTool.Core.Settings; 4 | using System; 5 | using System.Collections.Generic; 6 | using System.Globalization; 7 | using System.Linq; 8 | using System.Text; 9 | using System.Threading.Tasks; 10 | using System.Windows.Data; 11 | 12 | namespace InfiniteVariantTool.GUI 13 | { 14 | public class SettingsViewModel : ViewModel 15 | { 16 | public SettingsViewModel() 17 | { 18 | gameDirectory = UserSettings.Instance.GameDirectory; 19 | variantDirectory = UserSettings.Instance.VariantDirectory; 20 | selectedLanguageIndex = LanguageToIndex(UserSettings.Instance.Language); 21 | checkForUpdates = UserSettings.Instance.CheckForUpdates; 22 | } 23 | 24 | public void Save() 25 | { 26 | UserSettings.Instance.GameDirectory = GameDirectory; 27 | UserSettings.Instance.VariantDirectory = VariantDirectory; 28 | UserSettings.Instance.Language = IndexToLanguage(SelectedLanguageIndex); 29 | UserSettings.Instance.CheckForUpdates = CheckForUpdates; 30 | UserSettings.Instance.Save(); 31 | } 32 | 33 | public RelayCommand SaveCommand => new SyncRelayCommand(_ => Save()); 34 | public RelayCommand SaveAndCloseCommand => new SyncRelayCommand(_ => 35 | { 36 | Save(); 37 | NavigationService.Close(); 38 | }); 39 | 40 | private string gameDirectory; 41 | public string GameDirectory 42 | { 43 | get => gameDirectory; 44 | set 45 | { 46 | if (value != gameDirectory) 47 | { 48 | gameDirectory = value; 49 | OnPropertyChange(nameof(GameDirectory)); 50 | } 51 | } 52 | } 53 | 54 | public RelayCommand PickGameDirectoryCommand => new SyncRelayCommand(_ => 55 | { 56 | if (IOService.TrySelectPath(out string? path, PathSelectType.Open, "Select game folder", "Directory|.")) 57 | { 58 | GameDirectory = path; 59 | } 60 | }); 61 | 62 | private string variantDirectory; 63 | public string VariantDirectory 64 | { 65 | get => variantDirectory; 66 | set 67 | { 68 | if (value != variantDirectory) 69 | { 70 | variantDirectory = value; 71 | OnPropertyChange(nameof(VariantDirectory)); 72 | } 73 | } 74 | } 75 | 76 | public RelayCommand PickVariantDirectoryCommand => new SyncRelayCommand(_ => 77 | { 78 | if (IOService.TrySelectPath(out string? path, PathSelectType.Open, "Select variant folder", "Directory|.")) 79 | { 80 | VariantDirectory = path; 81 | } 82 | }); 83 | 84 | private int selectedLanguageIndex; 85 | public int SelectedLanguageIndex 86 | { 87 | get => selectedLanguageIndex; 88 | set 89 | { 90 | if (value != selectedLanguageIndex) 91 | { 92 | selectedLanguageIndex = value; 93 | OnPropertyChange(nameof(SelectedLanguageIndex)); 94 | } 95 | } 96 | } 97 | 98 | private List? languageOptions; 99 | public List LanguageOptions 100 | { 101 | get 102 | { 103 | if (languageOptions == null) 104 | { 105 | languageOptions = new(); 106 | languageOptions.Add(new LanguageModel("auto", "auto")); 107 | foreach (var lang in Language.Languages) 108 | { 109 | if (lang.Name != "") 110 | { 111 | languageOptions.Add(new LanguageModel(lang.Name, lang.Code)); 112 | } 113 | } 114 | } 115 | return languageOptions; 116 | } 117 | } 118 | 119 | private bool checkForUpdates; 120 | public bool CheckForUpdates 121 | { 122 | get => checkForUpdates; 123 | set 124 | { 125 | if (value != checkForUpdates) 126 | { 127 | checkForUpdates = value; 128 | OnPropertyChange(nameof(CheckForUpdates)); 129 | } 130 | } 131 | } 132 | 133 | private RelayCommand? openGameDirectoryInExplorerCommand; 134 | public RelayCommand OpenInExplorerCommand 135 | { 136 | get 137 | { 138 | if (openGameDirectoryInExplorerCommand == null) 139 | { 140 | openGameDirectoryInExplorerCommand = new SyncRelayCommand( 141 | _ => IOService.ShowInExplorer(GameDirectory)); 142 | } 143 | return openGameDirectoryInExplorerCommand; 144 | } 145 | } 146 | 147 | private RelayCommand? openVariantDirectoryInExplorerCommand; 148 | public RelayCommand OpenVariantDirectoryInExplorerCommand 149 | { 150 | get 151 | { 152 | if (openVariantDirectoryInExplorerCommand == null) 153 | { 154 | openVariantDirectoryInExplorerCommand = new SyncRelayCommand( 155 | _ => IOService.ShowInExplorer(VariantDirectory)); 156 | } 157 | return openVariantDirectoryInExplorerCommand; 158 | } 159 | } 160 | 161 | private int LanguageToIndex(string code) 162 | { 163 | return Math.Max(0, LanguageOptions.FindIndex(lang => lang.Code == code)); 164 | } 165 | 166 | private string IndexToLanguage(int index) 167 | { 168 | if (index < 0 || index >= LanguageOptions.Count) 169 | { 170 | index = 0; 171 | } 172 | return LanguageOptions[index].Code; 173 | } 174 | } 175 | } 176 | -------------------------------------------------------------------------------- /GUI/ViewModels/UpdaterViewModel.cs: -------------------------------------------------------------------------------- 1 | using InfiniteVariantTool.Core; 2 | using System; 3 | using System.Collections.Generic; 4 | using System.Diagnostics; 5 | using System.Linq; 6 | using System.Reflection; 7 | using System.Text; 8 | using System.Threading.Tasks; 9 | using System.Windows; 10 | 11 | namespace InfiniteVariantTool.GUI 12 | { 13 | public class UpdaterViewModel : ViewModel 14 | { 15 | public bool CheckedForUpdate { get; set; } 16 | 17 | public UpdaterViewModel(Version newVersion) 18 | { 19 | this.newVersion = newVersion; 20 | CheckedForUpdate = true; 21 | } 22 | 23 | public UpdaterViewModel() 24 | { 25 | CheckedForUpdate = false; 26 | } 27 | 28 | public async Task CheckForUpdates() 29 | { 30 | NewVersion = null; 31 | Updater updater = new(); 32 | var latestVersion = await updater.GetLatestVersion(); 33 | if (latestVersion != null && latestVersion > CurrentVersion) 34 | { 35 | NewVersion = latestVersion; 36 | return true; 37 | } 38 | else 39 | { 40 | UpToDate = true; 41 | } 42 | return false; 43 | } 44 | 45 | private bool upToDate; 46 | public bool UpToDate 47 | { 48 | get => upToDate; 49 | set 50 | { 51 | if (value != upToDate) 52 | { 53 | upToDate = value; 54 | OnPropertyChange(); 55 | } 56 | } 57 | } 58 | 59 | public Version CurrentVersion => Updater.CurrentVersion; 60 | public string CurrentVersionStr => CurrentVersion.ToString(3); 61 | 62 | public string? NewVersionStr => NewVersion?.ToString(3); 63 | private Version? newVersion; 64 | public Version? NewVersion 65 | { 66 | get => newVersion; 67 | set 68 | { 69 | if (value != newVersion) 70 | { 71 | newVersion = value; 72 | OnPropertyChange(); 73 | OnPropertyChange(nameof(NewVersionStr)); 74 | } 75 | } 76 | } 77 | 78 | public string UpdateUrl => Updater.GitHubReleaseUrl; 79 | 80 | private RelayCommand? openUpdatePageCommand; 81 | public RelayCommand OpenUpdatePageCommand 82 | { 83 | get 84 | { 85 | if (openUpdatePageCommand == null) 86 | { 87 | openUpdatePageCommand = new SyncRelayCommand(_ => 88 | { 89 | var ps = new ProcessStartInfo(Updater.GitHubReleaseUrl) 90 | { 91 | UseShellExecute = true, 92 | Verb = "open" 93 | }; 94 | Process.Start(ps); 95 | NavigationService.Close(); 96 | }); 97 | } 98 | return openUpdatePageCommand; 99 | } 100 | } 101 | 102 | private RelayCommand? checkForUpdatesCommand; 103 | public RelayCommand CheckForUpdatesCommand 104 | { 105 | get 106 | { 107 | if (checkForUpdatesCommand == null) 108 | { 109 | checkForUpdatesCommand = new AsyncRelayCommand(async _ => await CheckForUpdates()); 110 | } 111 | return checkForUpdatesCommand; 112 | } 113 | } 114 | } 115 | } 116 | -------------------------------------------------------------------------------- /GUI/ViewModels/ViewModelBase.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.ComponentModel; 4 | using System.Linq; 5 | using System.Text; 6 | using System.Threading.Tasks; 7 | 8 | namespace InfiniteVariantTool.GUI 9 | { 10 | public class ViewModel : NotifyPropertyChanged 11 | { 12 | private IOService? ioService; 13 | public IOService IOService 14 | { 15 | get => ioService ?? throw new InvalidOperationException("IOService not instantiated"); 16 | set => ioService = value; 17 | } 18 | 19 | private NavigationService? navService; 20 | public NavigationService NavigationService 21 | { 22 | get => navService ?? throw new InvalidOperationException("NavigationService not instantiated"); 23 | set => navService = value; 24 | } 25 | 26 | private RelayCommand? closeCommand; 27 | public RelayCommand CloseCommand 28 | { 29 | get 30 | { 31 | if (closeCommand == null) 32 | { 33 | closeCommand = new SyncRelayCommand(_ => NavigationService.Close()); 34 | } 35 | return closeCommand; 36 | } 37 | } 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /GUI/Views/AboutWindow.xaml: -------------------------------------------------------------------------------- 1 |  14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 |