diff --git a/EonaCat.NightReign.sln b/EonaCat.NightReign.sln new file mode 100644 index 0000000..2ae6e0d --- /dev/null +++ b/EonaCat.NightReign.sln @@ -0,0 +1,25 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 17 +VisualStudioVersion = 17.14.36121.58 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "EonaCat.NightReign", "EonaCat.NightReign\EonaCat.NightReign.csproj", "{9262F2EA-FE30-7164-AEA2-4C1D022324C0}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {9262F2EA-FE30-7164-AEA2-4C1D022324C0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {9262F2EA-FE30-7164-AEA2-4C1D022324C0}.Debug|Any CPU.Build.0 = Debug|Any CPU + {9262F2EA-FE30-7164-AEA2-4C1D022324C0}.Release|Any CPU.ActiveCfg = Release|Any CPU + {9262F2EA-FE30-7164-AEA2-4C1D022324C0}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {811090C0-4BF2-4F9E-838C-B324FFAD61E6} + EndGlobalSection +EndGlobal diff --git a/EonaCat.NightReign/EonaCat.NightReign.csproj b/EonaCat.NightReign/EonaCat.NightReign.csproj new file mode 100644 index 0000000..d895e81 --- /dev/null +++ b/EonaCat.NightReign/EonaCat.NightReign.csproj @@ -0,0 +1,37 @@ + + + + WinExe + net8.0-windows + enable + true + enable + EonaCat.ico + True + + + + true + EonaCat.snk + + + + + + + + + True + True + Resources.resx + + + + + + ResXFileCodeGenerator + Resources.Designer.cs + + + + \ No newline at end of file diff --git a/EonaCat.NightReign/EonaCat.ico b/EonaCat.NightReign/EonaCat.ico new file mode 100644 index 0000000..406f265 Binary files /dev/null and b/EonaCat.NightReign/EonaCat.ico differ diff --git a/EonaCat.NightReign/EonaCat.snk b/EonaCat.NightReign/EonaCat.snk new file mode 100644 index 0000000..ff5ab33 Binary files /dev/null and b/EonaCat.NightReign/EonaCat.snk differ diff --git a/EonaCat.NightReign/FileEngine.cs b/EonaCat.NightReign/FileEngine.cs new file mode 100644 index 0000000..731c8dd --- /dev/null +++ b/EonaCat.NightReign/FileEngine.cs @@ -0,0 +1,99 @@ +using EonaCat.NightReign.Helpers; +using EonaCat.NightReign.Models; + +namespace EonaCat.NightReign +{ + static class FileEngine + { + private const int HeaderLength = 12; + private const int DataBlock = 32; + private const int DataBlockSize = 64; + + private static readonly List _entries = new(); + private static byte[] _rawData; + + private static readonly string _outputFolder = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), "EonaCat", "NightReign", "Temp"); + + public static string OutputFolder => _outputFolder; + + public static string Decrypt(string inputFile, Action logger = null) + { + _rawData = File.ReadAllBytes(inputFile); + + if (!SL2Helper.IsValidHeader(BND4Entry.FILEIDENTIFIER, _rawData)) + { + logger?.Invoke("Invalid SL2 file."); + return null; + } + + _entries.Clear(); + int numEntries = BitConverter.ToInt32(_rawData, HeaderLength); + + for (int i = 0; i < numEntries; i++) + { + int pos = DataBlockSize + (i * DataBlock); + if (pos + DataBlock > _rawData.Length) + { + break; + } + + if (!SL2Helper.IsValidDivider(_rawData, pos)) + { + continue; + } + + int size = BitConverter.ToInt32(_rawData, pos + 8); + int dataOffset = BitConverter.ToInt32(_rawData, pos + 16); + int footerLength = BitConverter.ToInt32(_rawData, pos + 24); + + var entry = new BND4Entry(_rawData, i, _outputFolder, size, dataOffset, footerLength); + entry.Decrypt(); + _entries.Add(entry); + + logger?.Invoke($"Decrypted Entry #{i}: {entry.Name}"); + } + + FileHelper.TryCreateDirectory(_outputFolder, logger); + return _outputFolder; + } + + public static void Encrypt(string outputFile) + { + var newData = new byte[_rawData.Length]; + Array.Copy(_rawData, newData, _rawData.Length); + + foreach (var entry in _entries) + { + string modifiedPath = Path.Combine(_outputFolder, entry.Name); + if (!File.Exists(modifiedPath)) + { + continue; + } + + byte[] modified = File.ReadAllBytes(modifiedPath); + entry.SetModifiedData(modified); + entry.PatchChecksum(); + + byte[] encrypted = entry.EncryptSL2Data(); + Array.Copy(encrypted, 0, newData, entry.DataOffset, encrypted.Length); + } + + File.WriteAllBytes(outputFile, newData); + } + + public static void RemoveEncryptedFolder() + { + try + { + if (Directory.Exists(_outputFolder)) + { + Directory.Delete(_outputFolder, recursive: true); + } + } + catch (Exception ex) + { + Console.WriteLine($"Failed to delete output directory: {ex.Message}"); + } + } + } +} diff --git a/EonaCat.NightReign/Helpers/BytesHelper.cs b/EonaCat.NightReign/Helpers/BytesHelper.cs new file mode 100644 index 0000000..7d297b9 --- /dev/null +++ b/EonaCat.NightReign/Helpers/BytesHelper.cs @@ -0,0 +1,56 @@ +using System.Security.Cryptography; + +namespace EonaCat.NightReign.Helpers +{ + public static class BytesHelper + { + public static string BytesToIntStr(byte[] data) => + string.Join(",", data.Select(b => b.ToString())); + + public static byte[] Md5Hash(byte[] data) + { + using var md5 = MD5.Create(); + return md5.ComputeHash(data); + } + + public static bool SequenceEquals(this byte[] a, byte[] b) => + a.AsSpan().SequenceEqual(b); + + public static bool ContainsSubsequence(this byte[] array, byte[] subsequence) + { + if (subsequence.Length == 0 || array.Length < subsequence.Length) + return false; + + for (int i = 0; i <= array.Length - subsequence.Length; i++) + { + if (array.AsSpan(i, subsequence.Length).SequenceEqual(subsequence)) + return true; + } + + return false; + } + + public static byte[] ReplaceBytes(byte[] source, byte[] oldBytes, byte[] newBytes) + { + using var ms = new MemoryStream(); + int i = 0; + + while (i < source.Length) + { + if (i <= source.Length - oldBytes.Length && + source.AsSpan(i, oldBytes.Length).SequenceEqual(oldBytes)) + { + ms.Write(newBytes, 0, newBytes.Length); + i += oldBytes.Length; + } + else + { + ms.WriteByte(source[i]); + i++; + } + } + + return ms.ToArray(); + } + } +} diff --git a/EonaCat.NightReign/Helpers/FileHelper.cs b/EonaCat.NightReign/Helpers/FileHelper.cs new file mode 100644 index 0000000..47e4481 --- /dev/null +++ b/EonaCat.NightReign/Helpers/FileHelper.cs @@ -0,0 +1,17 @@ +namespace EonaCat.NightReign.Helpers +{ + internal class FileHelper + { + public static void TryCreateDirectory(string path, Action logCallback) + { + try + { + Directory.CreateDirectory(path); + } + catch (Exception ex) + { + logCallback?.Invoke($"Failed to create output directory: {ex.Message}"); + } + } + } +} diff --git a/EonaCat.NightReign/Helpers/SL2Helper.cs b/EonaCat.NightReign/Helpers/SL2Helper.cs new file mode 100644 index 0000000..299670f --- /dev/null +++ b/EonaCat.NightReign/Helpers/SL2Helper.cs @@ -0,0 +1,17 @@ +using System.Text; + +namespace EonaCat.NightReign.Helpers +{ + internal class SL2Helper + { + private static readonly byte[] DATADIVIDER = [0x40, 0x00, 0x00, 0x00, 0xFF, 0xFF, 0xFF, 0xFF]; + + internal static bool IsValidHeader(string fileIdentifier, byte[] data) => + data != null && + data.Length >= fileIdentifier.Length && + data.Take(fileIdentifier.Length).SequenceEqual(Encoding.ASCII.GetBytes(fileIdentifier)); + + internal static bool IsValidDivider(byte[] data, int position) => + data.Skip(position).Take(DATADIVIDER.Length).SequenceEqual(DATADIVIDER); + } +} diff --git a/EonaCat.NightReign/Helpers/SteamIdHelper.cs b/EonaCat.NightReign/Helpers/SteamIdHelper.cs new file mode 100644 index 0000000..fe4f456 --- /dev/null +++ b/EonaCat.NightReign/Helpers/SteamIdHelper.cs @@ -0,0 +1,62 @@ +using Microsoft.Win32; +using System.Text.RegularExpressions; + +namespace EonaCat.NightReign.Helpers +{ + public static class SteamHelper + { + public const int STEAM_ID_HEX_LENGTH = 16; + public static Dictionary GetAllSteamAccounts() + { + var accounts = new Dictionary(); + + string steamPath = GetSteamInstallPath(); + if (steamPath == null) + return accounts; + + string loginUsersPath = Path.Combine(steamPath, "config", "loginusers.vdf"); + if (!File.Exists(loginUsersPath)) + return accounts; + + string fileContent = File.ReadAllText(loginUsersPath); + + var userBlockPattern = new Regex(@"""(\d{17})""\s*{[^}]*?""AccountName""\s*""([^""]+)""", RegexOptions.Singleline); + + foreach (Match match in userBlockPattern.Matches(fileContent)) + { + if (match.Groups.Count == 3) + { + string steamId = match.Groups[1].Value; + string accountName = match.Groups[2].Value; + accounts[steamId] = accountName; + } + } + + return accounts; + } + + internal static byte[] ConvertToSteamIdBytes(string steamId) + { + if (string.IsNullOrEmpty(steamId) || steamId.Length != 17 || !steamId.All(char.IsDigit)) + { + return null; + } + + var hex = Convert.ToUInt64(steamId).ToString("x").PadLeft(STEAM_ID_HEX_LENGTH, '0'); + var steamIdBytes = Enumerable.Range(0, hex.Length) + .Where(i => i % 2 == 0) + .Select(i => Convert.ToByte(hex.Substring(i, 2), 16)) + .Reverse().ToArray(); + return steamIdBytes; + } + + private static string GetSteamInstallPath() + { + string key = Environment.Is64BitOperatingSystem + ? @"HKEY_LOCAL_MACHINE\SOFTWARE\WOW6432Node\Valve\Steam" + : @"HKEY_LOCAL_MACHINE\SOFTWARE\Valve\Steam"; + + return Registry.GetValue(key, "InstallPath", null) as string; + } +} +} diff --git a/EonaCat.NightReign/MainForm.cs b/EonaCat.NightReign/MainForm.cs new file mode 100644 index 0000000..2614ca8 --- /dev/null +++ b/EonaCat.NightReign/MainForm.cs @@ -0,0 +1,298 @@ +using EonaCat.NightReign.EonaCat.NightReign; +using EonaCat.NightReign.Helpers; + +namespace EonaCat.NightReign +{ + public partial class MainForm : Form + { + private const int STEAM_ID_BYTE_LENGTH = 8; + + private System.ComponentModel.IContainer components = null; + private string _steamId; + + public object NeightReignFileName => "NR0000"; + + public MainForm() + { + InitializeComponent(); + SetupFormUI(); + } + + protected override void Dispose(bool disposing) + { + if (disposing) + { + components?.Dispose(); + } + + base.Dispose(disposing); + } + + private void SetupFormUI() + { + Text = "EonaCat ER NightReign Save Transfer"; + Size = new Size(400, 150); + MaximizeBox = false; + MinimizeBox = false; + FormBorderStyle = FormBorderStyle.FixedDialog; + StartPosition = FormStartPosition.CenterScreen; + + var label = new Label + { + Text = "This tool allows you to change the Steam ID in your Elden Ring NightReign save file.", + Width = 360, + Height = 40, + ForeColor = Color.White, + BackColor = Color.Transparent, + Top = 10, + Left = 10 + }; + + var decryptButton = new Button + { + Text = "Select save file for steam ID change", + Width = 220, + Height = 30, + Top = 50, + Left = 35 + }; + decryptButton.Click += DecryptButton_Click; + + Controls.Add(label); + Controls.Add(decryptButton); + } + + private void InitializeComponent() + { + SuspendLayout(); + AutoScaleDimensions = new SizeF(7F, 15F); + AutoScaleMode = AutoScaleMode.Font; + AutoSize = true; + BackgroundImage = Properties.Resources._1; + ClientSize = new Size(800, 450); + Name = "MainForm"; + ResumeLayout(false); + } + + private void DecryptButton_Click(object sender, EventArgs e) + { + var inputFile = GetInputFile(); + if (string.IsNullOrWhiteSpace(inputFile)) + { + return; + } + + string folderPath; + try + { + folderPath = FileEngine.Decrypt(inputFile, Console.WriteLine); + } + catch (Exception ex) + { + ShowError("Failed to decrypt SL2 file", ex.Message); + return; + } + + var ERDataFiles = Directory.GetFiles(folderPath, "ELDENRING_DATA*").OrderBy(f => f).ToArray(); + string ERData10Path = Path.Combine(folderPath, "ELDENRING_DATA_10"); + + if (!File.Exists(ERData10Path)) + { + ShowError("Missing File", $"ELDENRING_DATA_10 not found in {folderPath}"); + return; + } + + byte[] oldSteamId; + try + { + using var fs = new FileStream(ERData10Path, FileMode.Open, FileAccess.Read); + fs.Seek(0x8, SeekOrigin.Begin); + oldSteamId = new byte[STEAM_ID_BYTE_LENGTH]; + fs.Read(oldSteamId, 0, STEAM_ID_BYTE_LENGTH); + } + catch (Exception ex) + { + ShowError("Failed to read ELDENRING_DATA_10", ex.Message); + return; + } + + Console.WriteLine("Old Steam ID (bytes): " + BitConverter.ToString(oldSteamId)); + + var steamIds = SteamHelper.GetAllSteamAccounts(); + var newSteamId = string.Empty; + byte[] newSteamIdBytes = null; + + // If there are steamIds, show them in a dialog to select + if (steamIds != null && steamIds.Count > 0) + { + var steamIdForm = new SteamIdSelectionForm(steamIds, oldSteamId); + if (steamIdForm.ShowDialog() == DialogResult.OK) + { + newSteamId = steamIdForm.SelectedSteamId; + newSteamIdBytes = SteamHelper.ConvertToSteamIdBytes(newSteamId); + } + } + + if (steamIds == null || steamIds.Count() == 0 || string.IsNullOrEmpty(newSteamId) || newSteamId.Length != 17 || !newSteamId.All(char.IsDigit) || newSteamIdBytes == null) + { + AskSteamIdWindow(x => + { + newSteamIdBytes = x; + }); + } + + if (newSteamIdBytes == null) + { + MessageBox.Show("Invalid Steam ID format. Please enter a valid 17-digit Steam ID.", "Invalid Input", MessageBoxButtons.OK, MessageBoxIcon.Error); + return; + } + + if (oldSteamId.SequenceEqual(newSteamIdBytes)) + { + MessageBox.Show("The new Steam ID is the same as the old one.", "No Changes", MessageBoxButtons.OK, MessageBoxIcon.Information); + return; + } + + Console.WriteLine("New Steam ID (bytes): " + BitConverter.ToString(newSteamIdBytes)); + int filesModified = 0; + + foreach (var file in ERDataFiles) + { + byte[] data = File.ReadAllBytes(file); + if (!data.ContainsSubsequence(oldSteamId)) + { + continue; + } + + var newData = BytesHelper.ReplaceBytes(data, oldSteamId, newSteamIdBytes); + if (!data.SequenceEqual(newData)) + { + File.WriteAllBytes(file, newData); + filesModified++; + } + } + + if (filesModified == 0) + { + MessageBox.Show("No files were modified. The old Steam ID might not be present in any slots.", "Warning", MessageBoxButtons.OK, MessageBoxIcon.Warning); + return; + } + + _steamId = newSteamId; + Console.WriteLine($"Steam ID replaced in {filesModified} file(s)"); + + var outputFile = GetOutputFile(); + if (string.IsNullOrEmpty(outputFile)) + { + return; + } + + try + { + FileEngine.Encrypt(outputFile); + MessageBox.Show($"Save file saved as {outputFile}", "Success", MessageBoxButtons.OK, MessageBoxIcon.Information); + FileEngine.RemoveEncryptedFolder(); + } + catch (Exception ex) + { + ShowError("Failed to re-encrypt and save", ex.Message); + } + } + + private string GetInputFile() + { + using var ofd = new OpenFileDialog + { + Title = "Select SL2 File", + Filter = "SL2 Files (*.sl2)|*.sl2|All Files (*.*)|*.*" + }; + return ofd.ShowDialog() == DialogResult.OK ? ofd.FileName : null; + } + + private string GetOutputFile() + { + string basePath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), "Nightreign"); + string path = !string.IsNullOrEmpty(_steamId) && _steamId.Length == 17 && Directory.Exists(Path.Combine(basePath, _steamId)) + ? Path.Combine(basePath, _steamId) + : basePath; + + using var sfd = new SaveFileDialog + { + Title = "Save New Encrypted SL2 File As", + Filter = "SL2 Files (*.sl2)|*.sl2|All Files (*.*)|*.*", + FileName = $"{NeightReignFileName}.sl2", + DefaultExt = "sl2", + InitialDirectory = path + }; + return sfd.ShowDialog() == DialogResult.OK ? sfd.FileName : null; + } + + private void AskSteamIdWindow(Action callback) + { + var inputForm = new Form + { + Text = "Enter your 17 digits Steam ID (steamID64 (Dec))", + Size = new Size(450, 150), + MaximizeBox = false, + MinimizeBox = false, + FormBorderStyle = FormBorderStyle.FixedDialog, + StartPosition = FormStartPosition.CenterParent, + BackgroundImage = Properties.Resources._1, + ForeColor = Color.White, + }; + + var label = new Label + { + Text = "Enter your 17-digit Steam ID:", + Top = 20, + Left = 20, + Width = 300, + ForeColor = Color.White, + BackColor = Color.Transparent + }; + + var inputBox = new TextBox + { + Top = 50, + Left = 20, + Width = 200 + }; + + var submitBtn = new Button + { + Text = "Submit", + Top = 80, + Left = 20, + BackColor = Color.FromArgb(30, 30, 30), + ForeColor = Color.White, + }; + + submitBtn.Click += (s, e) => + { + string input = inputBox.Text.Trim(); + if (!IsValidSteamId(input)) + { + MessageBox.Show("Steam ID must be exactly 17 digits!", "Invalid Steam ID", MessageBoxButtons.OK, MessageBoxIcon.Error); + return; + } + + _steamId = input; + byte[] steamIdBytes = SteamHelper.ConvertToSteamIdBytes(_steamId); + inputForm.Close(); + callback(steamIdBytes); + }; + + inputForm.Controls.Add(label); + inputForm.Controls.Add(inputBox); + inputForm.Controls.Add(submitBtn); + inputForm.AcceptButton = submitBtn; + inputForm.ShowDialog(); + } + + private bool IsValidSteamId(string input) => + input.Length == 17 && input.All(char.IsDigit); + + private void ShowError(string title, string message) => + MessageBox.Show(message, title, MessageBoxButtons.OK, MessageBoxIcon.Error); + } +} diff --git a/EonaCat.NightReign/MainForm.resx b/EonaCat.NightReign/MainForm.resx new file mode 100644 index 0000000..8b2ff64 --- /dev/null +++ b/EonaCat.NightReign/MainForm.resx @@ -0,0 +1,120 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + \ No newline at end of file diff --git a/EonaCat.NightReign/Models/Bnd4Entry.cs b/EonaCat.NightReign/Models/Bnd4Entry.cs new file mode 100644 index 0000000..5663989 --- /dev/null +++ b/EonaCat.NightReign/Models/Bnd4Entry.cs @@ -0,0 +1,88 @@ +using System.Security.Cryptography; + +namespace EonaCat.NightReign.Models +{ + public class BND4Entry + { + public const string FILEIDENTIFIER = "BND4"; + public int Index { get; } + public int Size { get; } + public int DataOffset { get; } + public int FooterLength { get; } + public string Name => $"ELDENRING_DATA_{Index:00}"; + public bool IsDecrypted { get; private set; } + + private const int IV_SIZE = 16; + private const int PADDING_LENGTH = 28; + private readonly byte[] _rawData; + private readonly byte[] _encryptedData; + private readonly byte[] _iv; + private readonly byte[] _encryptedPayload; + private byte[] _data; + + public string OutputFolder { get; } + + private static readonly byte[] DS2_KEY = { 0x18, 0xF6, 0x32, 0x66, 0x05, 0xBD, 0x17, 0x8A, 0x55, 0x24, 0x52, 0x3A, 0xC0, 0xA0, 0xC6, 0x09 }; + + public BND4Entry(byte[] rawData, int index, string outputFolder, int size, int offset, int footerLength) + { + Index = index; Size = size; DataOffset = offset; FooterLength = footerLength; + _rawData = rawData; + _encryptedData = rawData.Skip(offset).Take(size).ToArray(); + _iv = _encryptedData.Take(IV_SIZE).ToArray(); + _encryptedPayload = _encryptedData.Skip(IV_SIZE).ToArray(); + OutputFolder = outputFolder; + } + + public void Decrypt() + { + using var aes = Aes.Create(); + aes.Key = DS2_KEY; aes.IV = _iv; aes.Mode = CipherMode.CBC; aes.Padding = PaddingMode.None; + + using var transform = aes.CreateDecryptor(); + _data = transform.TransformFinalBlock(_encryptedPayload, 0, _encryptedPayload.Length); + + var filePath = Path.Combine(OutputFolder, Name); + if (!Directory.Exists(OutputFolder)) + { + try + { + Directory.CreateDirectory(OutputFolder); + } + catch (Exception ex) + { + throw new InvalidOperationException($"Failed to create output directory: {ex.Message}"); + } + } + File.WriteAllBytes(filePath, _data); + + IsDecrypted = true; + } + + public void PatchChecksum() + { + int checksumEnd = _data.Length - PADDING_LENGTH; + byte[] checksum = CalculateChecksum(); + Array.Copy(checksum, 0, _data, checksumEnd, checksum.Length); + } + + public byte[] CalculateChecksum() + { + int checksumEnd = _data.Length - 28; + using var md5 = MD5.Create(); + return md5.ComputeHash(_data.Skip(FILEIDENTIFIER.Length).Take(checksumEnd - FILEIDENTIFIER.Length).ToArray()); + } + + public byte[] EncryptSL2Data() + { + using var aes = Aes.Create(); + aes.Key = DS2_KEY; aes.IV = _iv; aes.Mode = CipherMode.CBC; aes.Padding = PaddingMode.None; + + using var transform = aes.CreateEncryptor(); + byte[] encrypted = transform.TransformFinalBlock(_data, 0, _data.Length); + return _iv.Concat(encrypted).ToArray(); + } + + public void SetModifiedData(byte[] data) => _data = data; + } +} diff --git a/EonaCat.NightReign/Program.cs b/EonaCat.NightReign/Program.cs new file mode 100644 index 0000000..a4cf384 --- /dev/null +++ b/EonaCat.NightReign/Program.cs @@ -0,0 +1,15 @@ +namespace EonaCat.NightReign +{ + internal static class Program + { + /// + /// The main entry point for the application. + /// + [STAThread] + static void Main() + { + ApplicationConfiguration.Initialize(); + Application.Run(new MainForm()); + } + } +} \ No newline at end of file diff --git a/EonaCat.NightReign/Properties/Resources.Designer.cs b/EonaCat.NightReign/Properties/Resources.Designer.cs new file mode 100644 index 0000000..112e629 --- /dev/null +++ b/EonaCat.NightReign/Properties/Resources.Designer.cs @@ -0,0 +1,73 @@ +//------------------------------------------------------------------------------ +// +// This code was generated by a tool. +// Runtime Version:4.0.30319.42000 +// +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// +//------------------------------------------------------------------------------ + +namespace EonaCat.NightReign.Properties { + using System; + + + /// + /// A strongly-typed resource class, for looking up localized strings, etc. + /// + // This class was auto-generated by the StronglyTypedResourceBuilder + // class via a tool like ResGen or Visual Studio. + // To add or remove a member, edit your .ResX file then rerun ResGen + // with the /str option, or rebuild your VS project. + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "17.0.0.0")] + [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] + [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] + internal class Resources { + + private static global::System.Resources.ResourceManager resourceMan; + + private static global::System.Globalization.CultureInfo resourceCulture; + + [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] + internal Resources() { + } + + /// + /// Returns the cached ResourceManager instance used by this class. + /// + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] + internal static global::System.Resources.ResourceManager ResourceManager { + get { + if (object.ReferenceEquals(resourceMan, null)) { + global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("EonaCat.NightReign.Properties.Resources", typeof(Resources).Assembly); + resourceMan = temp; + } + return resourceMan; + } + } + + /// + /// Overrides the current thread's CurrentUICulture property for all + /// resource lookups using this strongly typed resource class. + /// + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] + internal static global::System.Globalization.CultureInfo Culture { + get { + return resourceCulture; + } + set { + resourceCulture = value; + } + } + + /// + /// Looks up a localized resource of type System.Drawing.Bitmap. + /// + internal static System.Drawing.Bitmap _1 { + get { + object obj = ResourceManager.GetObject("1", resourceCulture); + return ((System.Drawing.Bitmap)(obj)); + } + } + } +} diff --git a/EonaCat.NightReign/Properties/Resources.resx b/EonaCat.NightReign/Properties/Resources.resx new file mode 100644 index 0000000..dd32c92 --- /dev/null +++ b/EonaCat.NightReign/Properties/Resources.resx @@ -0,0 +1,124 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + + ..\Resources\1.jpg;System.Drawing.Bitmap, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a + + \ No newline at end of file diff --git a/EonaCat.NightReign/Resources/1.jpg b/EonaCat.NightReign/Resources/1.jpg new file mode 100644 index 0000000..b600ce9 Binary files /dev/null and b/EonaCat.NightReign/Resources/1.jpg differ diff --git a/EonaCat.NightReign/SteamIdSelectionForm.cs b/EonaCat.NightReign/SteamIdSelectionForm.cs new file mode 100644 index 0000000..0bed448 --- /dev/null +++ b/EonaCat.NightReign/SteamIdSelectionForm.cs @@ -0,0 +1,116 @@ +using EonaCat.NightReign.Helpers; + +namespace EonaCat.NightReign +{ + namespace EonaCat.NightReign + { + public class SteamIdSelectionForm : Form + { + private List isMatchingIdList = new(); + + private ListBox listBox; + private Button okButton; + private Button cancelButton; + private readonly Dictionary steamAccounts; + private readonly byte[] oldSteamId; + + public string SelectedSteamId { get; private set; } + + public SteamIdSelectionForm(Dictionary steamAccounts, byte[] oldSteamId) + { + this.steamAccounts = steamAccounts ?? new(); + this.oldSteamId = oldSteamId; + + InitializeComponents(); + } + + private void InitializeComponents() + { + Text = "Select Your Steam Account"; + Size = new Size(400, 300); + StartPosition = FormStartPosition.CenterParent; + FormBorderStyle = FormBorderStyle.FixedDialog; + MaximizeBox = false; + MinimizeBox = false; + + listBox = new ListBox + { + Dock = DockStyle.Top, + Height = 200, + DrawMode = DrawMode.OwnerDrawFixed + }; + listBox.DrawItem += ListBox_DrawItem; + + foreach (var kvp in steamAccounts) + { + bool isMatch = oldSteamId != null && oldSteamId.SequenceEqual(SteamHelper.ConvertToSteamIdBytes(kvp.Key)); + listBox.Items.Add($"{kvp.Value} ({kvp.Key})"); + isMatchingIdList.Add(isMatch); + } + + okButton = new Button + { + Text = "OK", + DialogResult = DialogResult.OK, + Anchor = AnchorStyles.Bottom | AnchorStyles.Right, + Width = 80, + Left = 200, + Top = 220 + }; + okButton.Click += OkButton_Click; + + cancelButton = new Button + { + Text = "Cancel", + DialogResult = DialogResult.Cancel, + Anchor = AnchorStyles.Bottom | AnchorStyles.Right, + Width = 80, + Left = 290, + Top = 220 + }; + + Controls.Add(listBox); + Controls.Add(okButton); + Controls.Add(cancelButton); + + AcceptButton = okButton; + CancelButton = cancelButton; + } + + private void ListBox_DrawItem(object sender, DrawItemEventArgs e) + { + if (e.Index < 0 || e.Index >= listBox.Items.Count) return; + + e.DrawBackground(); + + bool isMatch = isMatchingIdList[e.Index]; + string text = listBox.Items[e.Index].ToString(); + + using (Brush brush = new SolidBrush(isMatch ? Color.Red : e.ForeColor)) + { + e.Graphics.DrawString(text, e.Font, brush, e.Bounds); + } + + e.DrawFocusRectangle(); + } + + + private void OkButton_Click(object sender, EventArgs e) + { + if (listBox.SelectedItem != null) + { + string selected = listBox.SelectedItem.ToString(); + var match = steamAccounts.FirstOrDefault(kvp => + selected.Contains(kvp.Key) && selected.Contains(kvp.Value)); + SelectedSteamId = match.Key; + DialogResult = DialogResult.OK; + Close(); + } + else + { + MessageBox.Show("Please select a Steam account.", "Selection Required", MessageBoxButtons.OK, MessageBoxIcon.Warning); + } + } + } + } +} diff --git a/EonaCat.NightReign/SteamIdSelectionForm.resx b/EonaCat.NightReign/SteamIdSelectionForm.resx new file mode 100644 index 0000000..1af7de1 --- /dev/null +++ b/EonaCat.NightReign/SteamIdSelectionForm.resx @@ -0,0 +1,120 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + \ No newline at end of file