Initial version

This commit is contained in:
EonaCat 2025-06-20 09:22:57 +02:00
parent 9b6ae2df15
commit 58badbe19c
18 changed files with 1267 additions and 0 deletions

25
EonaCat.NightReign.sln Normal file
View File

@ -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

View File

@ -0,0 +1,37 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>WinExe</OutputType>
<TargetFramework>net8.0-windows</TargetFramework>
<Nullable>enable</Nullable>
<UseWindowsForms>true</UseWindowsForms>
<ImplicitUsings>enable</ImplicitUsings>
<ApplicationIcon>EonaCat.ico</ApplicationIcon>
<SignAssembly>True</SignAssembly>
</PropertyGroup>
<PropertyGroup>
<SignAssembly>true</SignAssembly>
<AssemblyOriginatorKeyFile>EonaCat.snk</AssemblyOriginatorKeyFile>
</PropertyGroup>
<ItemGroup>
<Content Include="EonaCat.ico" />
</ItemGroup>
<ItemGroup>
<Compile Update="Properties\Resources.Designer.cs">
<DesignTime>True</DesignTime>
<AutoGen>True</AutoGen>
<DependentUpon>Resources.resx</DependentUpon>
</Compile>
</ItemGroup>
<ItemGroup>
<EmbeddedResource Update="Properties\Resources.resx">
<Generator>ResXFileCodeGenerator</Generator>
<LastGenOutput>Resources.Designer.cs</LastGenOutput>
</EmbeddedResource>
</ItemGroup>
</Project>

Binary file not shown.

After

Width:  |  Height:  |  Size: 248 KiB

Binary file not shown.

View File

@ -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<BND4Entry> _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<string> 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}");
}
}
}
}

View File

@ -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();
}
}
}

View File

@ -0,0 +1,17 @@
namespace EonaCat.NightReign.Helpers
{
internal class FileHelper
{
public static void TryCreateDirectory(string path, Action<string> logCallback)
{
try
{
Directory.CreateDirectory(path);
}
catch (Exception ex)
{
logCallback?.Invoke($"Failed to create output directory: {ex.Message}");
}
}
}
}

View File

@ -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);
}
}

View File

@ -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<string, string> GetAllSteamAccounts()
{
var accounts = new Dictionary<string, string>();
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;
}
}
}

View File

@ -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<byte[]> 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);
}
}

View File

@ -0,0 +1,120 @@
<?xml version="1.0" encoding="utf-8"?>
<root>
<!--
Microsoft ResX Schema
Version 2.0
The primary goals of this format is to allow a simple XML format
that is mostly human readable. The generation and parsing of the
various data types are done through the TypeConverter classes
associated with the data types.
Example:
... ado.net/XML headers & schema ...
<resheader name="resmimetype">text/microsoft-resx</resheader>
<resheader name="version">2.0</resheader>
<resheader name="reader">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader>
<resheader name="writer">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader>
<data name="Name1"><value>this is my long string</value><comment>this is a comment</comment></data>
<data name="Color1" type="System.Drawing.Color, System.Drawing">Blue</data>
<data name="Bitmap1" mimetype="application/x-microsoft.net.object.binary.base64">
<value>[base64 mime encoded serialized .NET Framework object]</value>
</data>
<data name="Icon1" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
<value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
<comment>This is a comment</comment>
</data>
There are any number of "resheader" rows that contain simple
name/value pairs.
Each data row contains a name, and value. The row also contains a
type or mimetype. Type corresponds to a .NET class that support
text/value conversion through the TypeConverter architecture.
Classes that don't support this are serialized and stored with the
mimetype set.
The mimetype is used for serialized objects, and tells the
ResXResourceReader how to depersist the object. This is currently not
extensible. For a given mimetype the value must be set accordingly:
Note - application/x-microsoft.net.object.binary.base64 is the format
that the ResXResourceWriter will generate, however the reader can
read any of the formats listed below.
mimetype: application/x-microsoft.net.object.binary.base64
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Binary.BinaryFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.soap.base64
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Soap.SoapFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.bytearray.base64
value : The object must be serialized into a byte array
: using a System.ComponentModel.TypeConverter
: and then encoded with base64 encoding.
-->
<xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
<xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
<xsd:element name="root" msdata:IsDataSet="true">
<xsd:complexType>
<xsd:choice maxOccurs="unbounded">
<xsd:element name="metadata">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" />
</xsd:sequence>
<xsd:attribute name="name" use="required" type="xsd:string" />
<xsd:attribute name="type" type="xsd:string" />
<xsd:attribute name="mimetype" type="xsd:string" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="assembly">
<xsd:complexType>
<xsd:attribute name="alias" type="xsd:string" />
<xsd:attribute name="name" type="xsd:string" />
</xsd:complexType>
</xsd:element>
<xsd:element name="data">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
<xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1" />
<xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
<xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="resheader">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" />
</xsd:complexType>
</xsd:element>
</xsd:choice>
</xsd:complexType>
</xsd:element>
</xsd:schema>
<resheader name="resmimetype">
<value>text/microsoft-resx</value>
</resheader>
<resheader name="version">
<value>2.0</value>
</resheader>
<resheader name="reader">
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<resheader name="writer">
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
</root>

View File

@ -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;
}
}

View File

@ -0,0 +1,15 @@
namespace EonaCat.NightReign
{
internal static class Program
{
/// <summary>
/// The main entry point for the application.
/// </summary>
[STAThread]
static void Main()
{
ApplicationConfiguration.Initialize();
Application.Run(new MainForm());
}
}
}

View File

@ -0,0 +1,73 @@
//------------------------------------------------------------------------------
// <auto-generated>
// 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.
// </auto-generated>
//------------------------------------------------------------------------------
namespace EonaCat.NightReign.Properties {
using System;
/// <summary>
/// A strongly-typed resource class, for looking up localized strings, etc.
/// </summary>
// 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() {
}
/// <summary>
/// Returns the cached ResourceManager instance used by this class.
/// </summary>
[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;
}
}
/// <summary>
/// Overrides the current thread's CurrentUICulture property for all
/// resource lookups using this strongly typed resource class.
/// </summary>
[global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)]
internal static global::System.Globalization.CultureInfo Culture {
get {
return resourceCulture;
}
set {
resourceCulture = value;
}
}
/// <summary>
/// Looks up a localized resource of type System.Drawing.Bitmap.
/// </summary>
internal static System.Drawing.Bitmap _1 {
get {
object obj = ResourceManager.GetObject("1", resourceCulture);
return ((System.Drawing.Bitmap)(obj));
}
}
}
}

View File

@ -0,0 +1,124 @@
<?xml version="1.0" encoding="utf-8"?>
<root>
<!--
Microsoft ResX Schema
Version 2.0
The primary goals of this format is to allow a simple XML format
that is mostly human readable. The generation and parsing of the
various data types are done through the TypeConverter classes
associated with the data types.
Example:
... ado.net/XML headers & schema ...
<resheader name="resmimetype">text/microsoft-resx</resheader>
<resheader name="version">2.0</resheader>
<resheader name="reader">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader>
<resheader name="writer">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader>
<data name="Name1"><value>this is my long string</value><comment>this is a comment</comment></data>
<data name="Color1" type="System.Drawing.Color, System.Drawing">Blue</data>
<data name="Bitmap1" mimetype="application/x-microsoft.net.object.binary.base64">
<value>[base64 mime encoded serialized .NET Framework object]</value>
</data>
<data name="Icon1" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
<value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
<comment>This is a comment</comment>
</data>
There are any number of "resheader" rows that contain simple
name/value pairs.
Each data row contains a name, and value. The row also contains a
type or mimetype. Type corresponds to a .NET class that support
text/value conversion through the TypeConverter architecture.
Classes that don't support this are serialized and stored with the
mimetype set.
The mimetype is used for serialized objects, and tells the
ResXResourceReader how to depersist the object. This is currently not
extensible. For a given mimetype the value must be set accordingly:
Note - application/x-microsoft.net.object.binary.base64 is the format
that the ResXResourceWriter will generate, however the reader can
read any of the formats listed below.
mimetype: application/x-microsoft.net.object.binary.base64
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Binary.BinaryFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.soap.base64
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Soap.SoapFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.bytearray.base64
value : The object must be serialized into a byte array
: using a System.ComponentModel.TypeConverter
: and then encoded with base64 encoding.
-->
<xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
<xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
<xsd:element name="root" msdata:IsDataSet="true">
<xsd:complexType>
<xsd:choice maxOccurs="unbounded">
<xsd:element name="metadata">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" />
</xsd:sequence>
<xsd:attribute name="name" use="required" type="xsd:string" />
<xsd:attribute name="type" type="xsd:string" />
<xsd:attribute name="mimetype" type="xsd:string" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="assembly">
<xsd:complexType>
<xsd:attribute name="alias" type="xsd:string" />
<xsd:attribute name="name" type="xsd:string" />
</xsd:complexType>
</xsd:element>
<xsd:element name="data">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
<xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1" />
<xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
<xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="resheader">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" />
</xsd:complexType>
</xsd:element>
</xsd:choice>
</xsd:complexType>
</xsd:element>
</xsd:schema>
<resheader name="resmimetype">
<value>text/microsoft-resx</value>
</resheader>
<resheader name="version">
<value>2.0</value>
</resheader>
<resheader name="reader">
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<resheader name="writer">
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<assembly alias="System.Windows.Forms" name="System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089" />
<data name="1" type="System.Resources.ResXFileRef, System.Windows.Forms">
<value>..\Resources\1.jpg;System.Drawing.Bitmap, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a</value>
</data>
</root>

Binary file not shown.

After

Width:  |  Height:  |  Size: 350 KiB

View File

@ -0,0 +1,116 @@
using EonaCat.NightReign.Helpers;
namespace EonaCat.NightReign
{
namespace EonaCat.NightReign
{
public class SteamIdSelectionForm : Form
{
private List<bool> isMatchingIdList = new();
private ListBox listBox;
private Button okButton;
private Button cancelButton;
private readonly Dictionary<string, string> steamAccounts;
private readonly byte[] oldSteamId;
public string SelectedSteamId { get; private set; }
public SteamIdSelectionForm(Dictionary<string, string> 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);
}
}
}
}
}

View File

@ -0,0 +1,120 @@
<?xml version="1.0" encoding="utf-8"?>
<root>
<!--
Microsoft ResX Schema
Version 2.0
The primary goals of this format is to allow a simple XML format
that is mostly human readable. The generation and parsing of the
various data types are done through the TypeConverter classes
associated with the data types.
Example:
... ado.net/XML headers & schema ...
<resheader name="resmimetype">text/microsoft-resx</resheader>
<resheader name="version">2.0</resheader>
<resheader name="reader">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader>
<resheader name="writer">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader>
<data name="Name1"><value>this is my long string</value><comment>this is a comment</comment></data>
<data name="Color1" type="System.Drawing.Color, System.Drawing">Blue</data>
<data name="Bitmap1" mimetype="application/x-microsoft.net.object.binary.base64">
<value>[base64 mime encoded serialized .NET Framework object]</value>
</data>
<data name="Icon1" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
<value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
<comment>This is a comment</comment>
</data>
There are any number of "resheader" rows that contain simple
name/value pairs.
Each data row contains a name, and value. The row also contains a
type or mimetype. Type corresponds to a .NET class that support
text/value conversion through the TypeConverter architecture.
Classes that don't support this are serialized and stored with the
mimetype set.
The mimetype is used for serialized objects, and tells the
ResXResourceReader how to depersist the object. This is currently not
extensible. For a given mimetype the value must be set accordingly:
Note - application/x-microsoft.net.object.binary.base64 is the format
that the ResXResourceWriter will generate, however the reader can
read any of the formats listed below.
mimetype: application/x-microsoft.net.object.binary.base64
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Binary.BinaryFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.soap.base64
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Soap.SoapFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.bytearray.base64
value : The object must be serialized into a byte array
: using a System.ComponentModel.TypeConverter
: and then encoded with base64 encoding.
-->
<xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
<xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
<xsd:element name="root" msdata:IsDataSet="true">
<xsd:complexType>
<xsd:choice maxOccurs="unbounded">
<xsd:element name="metadata">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" />
</xsd:sequence>
<xsd:attribute name="name" use="required" type="xsd:string" />
<xsd:attribute name="type" type="xsd:string" />
<xsd:attribute name="mimetype" type="xsd:string" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="assembly">
<xsd:complexType>
<xsd:attribute name="alias" type="xsd:string" />
<xsd:attribute name="name" type="xsd:string" />
</xsd:complexType>
</xsd:element>
<xsd:element name="data">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
<xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1" />
<xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
<xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="resheader">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" />
</xsd:complexType>
</xsd:element>
</xsd:choice>
</xsd:complexType>
</xsd:element>
</xsd:schema>
<resheader name="resmimetype">
<value>text/microsoft-resx</value>
</resheader>
<resheader name="version">
<value>2.0</value>
</resheader>
<resheader name="reader">
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<resheader name="writer">
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
</root>