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