Add project restore files and NuGet cache for GsaViewer

- Created project.nuget.cache to store NuGet package cache information.
- Added project.packagespec.json to define project restore settings and dependencies.
- Included rider.project.restore.info for Rider IDE integration.
This commit is contained in:
2026-04-09 16:56:58 +02:00
commit d6d621dc92
170 changed files with 8191 additions and 0 deletions

13
.idea/.idea.GsaViewer/.idea/.gitignore generated vendored Normal file
View File

@ -0,0 +1,13 @@
# Default ignored files
/shelf/
/workspace.xml
# Rider ignored files
/modules.xml
/contentModel.xml
/projectSettingsUpdater.xml
/.idea.GsaViewer.iml
# Editor-based HTTP Client requests
/httpRequests/
# Datasource local storage ignored files
/dataSources/
/dataSources.local.xml

10
.idea/.idea.GsaViewer/.idea/avalonia.xml generated Normal file
View File

@ -0,0 +1,10 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="AvaloniaProject">
<option name="projectPerEditor">
<map>
<entry key="Views/MainWindow.axaml" value="GsaViewer.csproj" />
</map>
</option>
</component>
</project>

View File

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="AgentMigrationStateService">
<option name="migrationStatus" value="COMPLETED" />
</component>
</project>

View File

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="AskMigrationStateService">
<option name="migrationStatus" value="COMPLETED" />
</component>
</project>

View File

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="Ask2AgentMigrationStateService">
<option name="migrationStatus" value="COMPLETED" />
</component>
</project>

View File

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="EditMigrationStateService">
<option name="migrationStatus" value="COMPLETED" />
</component>
</project>

View File

@ -0,0 +1,4 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="Encoding" addBOMForNewFiles="with BOM under Windows, with no BOM otherwise" />
</project>

View File

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="UserContentModel">
<attachedFolders />
<explicitIncludes />
<explicitExcludes />
</component>
</project>

View File

@ -0,0 +1,10 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="MaterialThemeProjectNewConfig">
<option name="metadata">
<MTProjectMetadataState>
<option name="userId" value="24a2abbf:19d72afb14b:-7ffd" />
</MTProjectMetadataState>
</option>
</component>
</project>

10
App.axaml Normal file
View File

@ -0,0 +1,10 @@
<Application xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
x:Class="GsaViewer.App"
RequestedThemeVariant="Default">
<!-- "Default" ThemeVariant follows system theme variant. "Dark" or "Light" are other available options. -->
<Application.Styles>
<FluentTheme />
</Application.Styles>
</Application>

47
App.axaml.cs Normal file
View File

@ -0,0 +1,47 @@
using Avalonia;
using Avalonia.Controls.ApplicationLifetimes;
using Avalonia.Data.Core;
using Avalonia.Data.Core.Plugins;
using System.Linq;
using Avalonia.Markup.Xaml;
using GsaViewer.ViewModels;
using GsaViewer.Views;
namespace GsaViewer;
public partial class App : Application
{
public override void Initialize()
{
AvaloniaXamlLoader.Load(this);
}
public override void OnFrameworkInitializationCompleted()
{
if (ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop)
{
// Avoid duplicate validations from both Avalonia and the CommunityToolkit.
// More info: https://docs.avaloniaui.net/docs/guides/development-guides/data-validation#manage-validationplugins
DisableAvaloniaDataAnnotationValidation();
desktop.MainWindow = new MainWindow
{
DataContext = new MainWindowViewModel(),
};
}
base.OnFrameworkInitializationCompleted();
}
private void DisableAvaloniaDataAnnotationValidation()
{
// Get an array of plugins to remove
var dataValidationPluginsToRemove =
BindingPlugins.DataValidators.OfType<DataAnnotationsValidationPlugin>().ToArray();
// remove each entry found
foreach (var plugin in dataValidationPluginsToRemove)
{
BindingPlugins.DataValidators.Remove(plugin);
}
}
}

BIN
Assets/avalonia-logo.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 172 KiB

View File

@ -0,0 +1,88 @@
using System.IO.Compression;
namespace GsaEditor.Core.Compression;
/// <summary>
/// Provides zlib compression and decompression utilities for GSA archive entries.
/// The GSA format stores zlib-wrapped data: 2-byte header (CMF+FLG) + deflate stream + 4-byte Adler-32 checksum.
/// This helper skips the 2-byte zlib header and uses <see cref="DeflateStream"/> for the core deflate data.
/// </summary>
public static class ZlibHelper
{
/// <summary>
/// Decompresses zlib-wrapped data into a byte array of the specified original size.
/// Skips the 2-byte zlib header before feeding data to <see cref="DeflateStream"/>.
/// </summary>
/// <param name="compressedData">The zlib-wrapped compressed data (header + deflate + checksum).</param>
/// <param name="originalLength">The expected decompressed size in bytes.</param>
/// <returns>The decompressed byte array.</returns>
/// <exception cref="InvalidDataException">Thrown when decompression fails or data is corrupt.</exception>
public static byte[] Decompress(byte[] compressedData, uint originalLength)
{
if (compressedData.Length < 2)
throw new InvalidDataException("Compressed data is too short to contain a zlib header.");
// Skip the 2-byte zlib header (typically 0x78 0x9C for default compression)
using var ms = new MemoryStream(compressedData, 2, compressedData.Length - 2);
using var deflate = new DeflateStream(ms, CompressionMode.Decompress);
var result = new byte[originalLength];
int totalRead = 0;
while (totalRead < result.Length)
{
int bytesRead = deflate.Read(result, totalRead, result.Length - totalRead);
if (bytesRead == 0)
break;
totalRead += bytesRead;
}
return result;
}
/// <summary>
/// Compresses data using zlib format: 2-byte header + deflate stream + 4-byte Adler-32 checksum.
/// </summary>
/// <param name="data">The uncompressed data to compress.</param>
/// <returns>The zlib-wrapped compressed byte array.</returns>
public static byte[] Compress(byte[] data)
{
using var output = new MemoryStream();
// Write zlib header: CMF=0x78 (deflate method, 32K window), FLG=0x9C (default level, valid check bits)
output.WriteByte(0x78);
output.WriteByte(0x9C);
using (var deflate = new DeflateStream(output, CompressionLevel.Optimal, leaveOpen: true))
{
deflate.Write(data, 0, data.Length);
}
// Write Adler-32 checksum in big-endian byte order
uint adler = ComputeAdler32(data);
output.WriteByte((byte)(adler >> 24));
output.WriteByte((byte)(adler >> 16));
output.WriteByte((byte)(adler >> 8));
output.WriteByte((byte)adler);
return output.ToArray();
}
/// <summary>
/// Computes the Adler-32 checksum of the given data.
/// </summary>
/// <param name="data">The data to checksum.</param>
/// <returns>The 32-bit Adler-32 checksum value.</returns>
private static uint ComputeAdler32(byte[] data)
{
const uint MOD_ADLER = 65521;
uint a = 1, b = 0;
for (int i = 0; i < data.Length; i++)
{
a = (a + data[i]) % MOD_ADLER;
b = (b + a) % MOD_ADLER;
}
return (b << 16) | a;
}
}

View File

@ -0,0 +1,8 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
<GenerateDocumentationFile>true</GenerateDocumentationFile>
</PropertyGroup>
</Project>

View File

@ -0,0 +1,70 @@
using System.Xml.Linq;
namespace GsaEditor.Core.IO;
/// <summary>
/// Represents a single entry parsed from a .idx NML index file.
/// </summary>
public class GsaIndexEntry
{
/// <summary>
/// The file alias (relative path) of the entry.
/// </summary>
public string Id { get; set; } = string.Empty;
/// <summary>
/// Compression method: 0 = raw, 1 = zlib.
/// </summary>
public int Method { get; set; }
/// <summary>
/// Original (decompressed) size in bytes.
/// </summary>
public uint Length { get; set; }
/// <summary>
/// Compressed size in bytes.
/// </summary>
public uint CompressedLength { get; set; }
/// <summary>
/// Byte offset of the data block within the archive.
/// </summary>
public long Offset { get; set; }
}
/// <summary>
/// Parses .idx NML index files (XML-compatible format) into a list of <see cref="GsaIndexEntry"/>.
/// </summary>
public static class GsaIndexReader
{
/// <summary>
/// Reads index entries from the specified .idx file.
/// </summary>
/// <param name="filePath">The path to the .idx file.</param>
/// <returns>A list of parsed index entries.</returns>
public static List<GsaIndexEntry> Read(string filePath)
{
var doc = XDocument.Load(filePath);
var entries = new List<GsaIndexEntry>();
var root = doc.Root;
if (root == null || root.Name.LocalName != "Index")
return entries;
foreach (var entryEl in root.Elements("Entry"))
{
var entry = new GsaIndexEntry
{
Id = entryEl.Element("Id")?.Value ?? string.Empty,
Method = int.TryParse(entryEl.Element("Method")?.Value, out var m) ? m : 0,
Length = uint.TryParse(entryEl.Element("Len")?.Value, out var l) ? l : 0,
CompressedLength = uint.TryParse(entryEl.Element("CompLen")?.Value, out var cl) ? cl : 0,
Offset = long.TryParse(entryEl.Element("Offset")?.Value, out var o) ? o : 0
};
entries.Add(entry);
}
return entries;
}
}

View File

@ -0,0 +1,36 @@
using System.Xml.Linq;
using GsaEditor.Core.Models;
namespace GsaEditor.Core.IO;
/// <summary>
/// Writes a .idx NML index file from a <see cref="GsaArchive"/>.
/// The index file is XML-compatible and lists each entry with its alias, compression method,
/// sizes, and data offset for fast lookup without a full sequential scan.
/// </summary>
public static class GsaIndexWriter
{
/// <summary>
/// Writes the index file for the given archive to the specified path.
/// </summary>
/// <param name="archive">The archive whose entries will be indexed.</param>
/// <param name="filePath">The output .idx file path.</param>
public static void Write(GsaArchive archive, string filePath)
{
var doc = new XDocument(
new XElement("Index",
archive.Entries.Select(e =>
new XElement("Entry",
new XElement("Id", e.Alias),
new XElement("Method", e.IsCompressed ? 1 : 0),
new XElement("Len", e.OriginalLength),
new XElement("CompLen", e.CompressedLength),
new XElement("Offset", e.DataOffset)
)
)
)
);
doc.Save(filePath);
}
}

View File

@ -0,0 +1,163 @@
using System.Text;
using GsaEditor.Core.Models;
namespace GsaEditor.Core.IO;
/// <summary>
/// Reads a .gsa binary archive from a stream or file and produces a <see cref="GsaArchive"/>.
/// Supports both NARC (legacy) and NARD (enhanced with padding) format variants.
/// </summary>
public static class GsaReader
{
/// <summary>Magic word for the NARC (legacy) format.</summary>
private const uint MAGIC_NARC = 0x4E415243;
/// <summary>Magic word for the NARD (enhanced) format.</summary>
private const uint MAGIC_NARD = 0x4E415244;
/// <summary>Maximum allowed alias length in characters.</summary>
private const int MAX_ALIAS_LENGTH = 511;
/// <summary>
/// Reads a GSA archive from the specified file path.
/// </summary>
/// <param name="filePath">The path to the .gsa archive file.</param>
/// <returns>A fully populated <see cref="GsaArchive"/>.</returns>
/// <exception cref="InvalidDataException">Thrown if the file has an invalid magic word.</exception>
public static GsaArchive Read(string filePath)
{
using var stream = File.OpenRead(filePath);
return Read(stream);
}
/// <summary>
/// Reads a GSA archive from a stream by performing a sequential scan of all entries.
/// </summary>
/// <param name="stream">A readable, seekable stream positioned at the start of the archive.</param>
/// <returns>A fully populated <see cref="GsaArchive"/>.</returns>
/// <exception cref="InvalidDataException">Thrown if the stream has an invalid magic word.</exception>
public static GsaArchive Read(Stream stream)
{
using var reader = new BinaryReader(stream, Encoding.ASCII, leaveOpen: true);
var archive = new GsaArchive();
// Read global header
uint magic = reader.ReadUInt32();
if (magic == MAGIC_NARC)
{
archive.Format = GsaFormat.NARC;
}
else if (magic == MAGIC_NARD)
{
archive.Format = GsaFormat.NARD;
archive.OffsetPadding = reader.ReadInt32();
archive.SizePadding = reader.ReadInt32();
}
else
{
throw new InvalidDataException(
$"Invalid GSA magic word: 0x{magic:X8}. Expected NARC (0x{MAGIC_NARC:X8}) or NARD (0x{MAGIC_NARD:X8}).");
}
// Sequentially read all entries until EOF
while (stream.Position < stream.Length)
{
var entry = ReadEntry(reader, archive);
if (entry == null)
break;
archive.Entries.Add(entry);
}
return archive;
}
/// <summary>
/// Reads a single file entry from the current stream position.
/// </summary>
/// <param name="reader">The binary reader positioned at the start of an entry.</param>
/// <param name="archive">The parent archive (used for NARD padding alignment).</param>
/// <returns>A populated <see cref="GsaEntry"/>, or null if no more entries can be read.</returns>
private static GsaEntry? ReadEntry(BinaryReader reader, GsaArchive archive)
{
var stream = reader.BaseStream;
if (stream.Position >= stream.Length)
return null;
// NARD: align stream position before reading entry header
if (archive.Format == GsaFormat.NARD && archive.OffsetPadding > 0)
AlignStream(stream, archive.OffsetPadding);
// Check if enough bytes remain for the alias length field
if (stream.Length - stream.Position < 4)
return null;
// Read alias
uint aliasLength = reader.ReadUInt32();
if (aliasLength == 0 || aliasLength > MAX_ALIAS_LENGTH)
return null;
if (stream.Length - stream.Position < aliasLength)
return null;
byte[] aliasBytes = reader.ReadBytes((int)aliasLength);
string alias = Encoding.ASCII.GetString(aliasBytes);
// Read compression flag (bit 0: 1=compressed, 0=raw)
byte compressionByte = reader.ReadByte();
bool isCompressed = (compressionByte & 0x01) != 0;
// Read original (decompressed) length
uint originalLength = reader.ReadUInt32();
// Read compressed length (only present if compressed)
uint compressedLength;
if (isCompressed)
{
compressedLength = reader.ReadUInt32();
}
else
{
compressedLength = originalLength;
}
// NARD: align stream position before reading the data block
if (archive.Format == GsaFormat.NARD && archive.OffsetPadding > 0)
AlignStream(stream, archive.OffsetPadding);
// Read raw data
long dataOffset = stream.Position;
uint dataSize = isCompressed ? compressedLength : originalLength;
if (stream.Length - stream.Position < dataSize)
return null;
byte[] rawData = reader.ReadBytes((int)dataSize);
return new GsaEntry
{
Alias = alias,
IsCompressed = isCompressed,
OriginalLength = originalLength,
CompressedLength = compressedLength,
RawData = rawData,
DataOffset = dataOffset
};
}
/// <summary>
/// Advances the stream position to the next aligned boundary.
/// </summary>
/// <param name="stream">The stream to align.</param>
/// <param name="alignment">The alignment boundary in bytes.</param>
private static void AlignStream(Stream stream, int alignment)
{
if (alignment <= 1) return;
long pos = stream.Position;
long remainder = pos % alignment;
if (remainder != 0)
{
stream.Position = pos + (alignment - remainder);
}
}
}

View File

@ -0,0 +1,114 @@
using System.Text;
using GsaEditor.Core.Models;
namespace GsaEditor.Core.IO;
/// <summary>
/// Writes a <see cref="GsaArchive"/> to a binary .gsa stream or file.
/// Supports both NARC (legacy) and NARD (enhanced with padding) format variants.
/// </summary>
public static class GsaWriter
{
/// <summary>Magic word for the NARC (legacy) format.</summary>
private const uint MAGIC_NARC = 0x4E415243;
/// <summary>Magic word for the NARD (enhanced) format.</summary>
private const uint MAGIC_NARD = 0x4E415244;
/// <summary>
/// Writes the archive to the specified file path.
/// </summary>
/// <param name="archive">The archive to write.</param>
/// <param name="filePath">The output file path.</param>
public static void Write(GsaArchive archive, string filePath)
{
using var stream = File.Create(filePath);
Write(archive, stream);
}
/// <summary>
/// Writes the archive to the given stream.
/// Each entry's <see cref="GsaEntry.DataOffset"/> is updated to reflect the new position in the output.
/// </summary>
/// <param name="archive">The archive to write.</param>
/// <param name="stream">A writable stream.</param>
public static void Write(GsaArchive archive, Stream stream)
{
using var writer = new BinaryWriter(stream, Encoding.ASCII, leaveOpen: true);
// Write global header
if (archive.Format == GsaFormat.NARC)
{
writer.Write(MAGIC_NARC);
}
else
{
writer.Write(MAGIC_NARD);
writer.Write(archive.OffsetPadding);
writer.Write(archive.SizePadding);
}
// Write each entry sequentially
foreach (var entry in archive.Entries)
{
WriteEntry(writer, entry, archive);
}
}
/// <summary>
/// Writes a single file entry at the current stream position.
/// </summary>
/// <param name="writer">The binary writer.</param>
/// <param name="entry">The entry to write.</param>
/// <param name="archive">The parent archive (used for NARD padding alignment).</param>
private static void WriteEntry(BinaryWriter writer, GsaEntry entry, GsaArchive archive)
{
// NARD: align stream position before writing entry header
if (archive.Format == GsaFormat.NARD && archive.OffsetPadding > 0)
WritePadding(writer, archive.OffsetPadding);
// Write alias
byte[] aliasBytes = Encoding.ASCII.GetBytes(entry.Alias);
writer.Write((uint)aliasBytes.Length);
writer.Write(aliasBytes);
// Write compression flag
writer.Write((byte)(entry.IsCompressed ? 1 : 0));
// Write original (decompressed) length
writer.Write(entry.OriginalLength);
// Write compressed length (only if compressed)
if (entry.IsCompressed)
{
writer.Write(entry.CompressedLength);
}
// NARD: align stream position before writing the data block
if (archive.Format == GsaFormat.NARD && archive.OffsetPadding > 0)
WritePadding(writer, archive.OffsetPadding);
// Update the entry's data offset to reflect the new position
entry.DataOffset = writer.BaseStream.Position;
// Write raw data
writer.Write(entry.RawData);
}
/// <summary>
/// Writes zero-bytes to pad the stream to the next aligned boundary.
/// </summary>
/// <param name="writer">The binary writer.</param>
/// <param name="alignment">The alignment boundary in bytes.</param>
private static void WritePadding(BinaryWriter writer, int alignment)
{
if (alignment <= 1) return;
long pos = writer.BaseStream.Position;
long remainder = pos % alignment;
if (remainder != 0)
{
int paddingBytes = (int)(alignment - remainder);
writer.Write(new byte[paddingBytes]);
}
}
}

View File

@ -0,0 +1,27 @@
namespace GsaEditor.Core.Models;
/// <summary>
/// Represents a loaded GSA archive with its global header information and file entries.
/// </summary>
public class GsaArchive
{
/// <summary>
/// The format variant of this archive (NARC or NARD).
/// </summary>
public GsaFormat Format { get; set; } = GsaFormat.NARC;
/// <summary>
/// (NARD only) Byte alignment for data offsets. Zero or unused for NARC.
/// </summary>
public int OffsetPadding { get; set; }
/// <summary>
/// (NARD only) Byte alignment for data sizes. Zero or unused for NARC.
/// </summary>
public int SizePadding { get; set; }
/// <summary>
/// The ordered list of file entries in the archive.
/// </summary>
public List<GsaEntry> Entries { get; set; } = new();
}

View File

@ -0,0 +1,77 @@
using GsaEditor.Core.Compression;
namespace GsaEditor.Core.Models;
/// <summary>
/// Represents a single file entry within a GSA archive.
/// </summary>
public class GsaEntry
{
/// <summary>
/// Relative path (alias) of the file within the archive, using '/' as separator.
/// Maximum 511 characters.
/// </summary>
public string Alias { get; set; } = string.Empty;
/// <summary>
/// Whether this entry's data is zlib-compressed.
/// </summary>
public bool IsCompressed { get; set; }
/// <summary>
/// Original (decompressed) size of the file in bytes.
/// </summary>
public uint OriginalLength { get; set; }
/// <summary>
/// Compressed size of the file in bytes. Equal to <see cref="OriginalLength"/> if not compressed.
/// </summary>
public uint CompressedLength { get; set; }
/// <summary>
/// Raw data as stored in the archive (compressed bytes if <see cref="IsCompressed"/> is true).
/// </summary>
public byte[] RawData { get; set; } = Array.Empty<byte>();
/// <summary>
/// Byte offset of this entry's data block within the archive file.
/// </summary>
public long DataOffset { get; set; }
/// <summary>
/// Returns the decompressed data for this entry.
/// If the entry is not compressed, returns <see cref="RawData"/> directly.
/// </summary>
/// <returns>The decompressed file content.</returns>
public byte[] GetDecompressedData()
{
if (!IsCompressed)
return RawData;
return ZlibHelper.Decompress(RawData, OriginalLength);
}
/// <summary>
/// Sets the entry data from uncompressed bytes, optionally compressing with zlib.
/// Updates <see cref="OriginalLength"/>, <see cref="CompressedLength"/>,
/// <see cref="IsCompressed"/>, and <see cref="RawData"/>.
/// </summary>
/// <param name="uncompressedData">The uncompressed file content.</param>
/// <param name="compress">Whether to compress the data with zlib.</param>
public void SetData(byte[] uncompressedData, bool compress)
{
OriginalLength = (uint)uncompressedData.Length;
IsCompressed = compress;
if (compress)
{
RawData = ZlibHelper.Compress(uncompressedData);
CompressedLength = (uint)RawData.Length;
}
else
{
RawData = uncompressedData;
CompressedLength = OriginalLength;
}
}
}

View File

@ -0,0 +1,18 @@
namespace GsaEditor.Core.Models;
/// <summary>
/// Identifies the archive format variant.
/// </summary>
public enum GsaFormat
{
/// <summary>
/// Legacy format with no padding support. Magic word = 0x4E415243 ("NARC").
/// </summary>
NARC,
/// <summary>
/// Enhanced format with offset and size padding for console DVD alignment.
/// Magic word = 0x4E415244 ("NARD").
/// </summary>
NARD
}

View File

@ -0,0 +1,23 @@
{
"runtimeTarget": {
"name": ".NETCoreApp,Version=v8.0",
"signature": ""
},
"compilationOptions": {},
"targets": {
".NETCoreApp,Version=v8.0": {
"GsaEditor.Core/1.0.0": {
"runtime": {
"GsaEditor.Core.dll": {}
}
}
}
},
"libraries": {
"GsaEditor.Core/1.0.0": {
"type": "project",
"serviceable": false,
"sha512": ""
}
}
}

Binary file not shown.

Binary file not shown.

View File

@ -0,0 +1,276 @@
<?xml version="1.0"?>
<doc>
<assembly>
<name>GsaEditor.Core</name>
</assembly>
<members>
<member name="T:GsaEditor.Core.Compression.ZlibHelper">
<summary>
Provides zlib compression and decompression utilities for GSA archive entries.
The GSA format stores zlib-wrapped data: 2-byte header (CMF+FLG) + deflate stream + 4-byte Adler-32 checksum.
This helper skips the 2-byte zlib header and uses <see cref="T:System.IO.Compression.DeflateStream"/> for the core deflate data.
</summary>
</member>
<member name="M:GsaEditor.Core.Compression.ZlibHelper.Decompress(System.Byte[],System.UInt32)">
<summary>
Decompresses zlib-wrapped data into a byte array of the specified original size.
Skips the 2-byte zlib header before feeding data to <see cref="T:System.IO.Compression.DeflateStream"/>.
</summary>
<param name="compressedData">The zlib-wrapped compressed data (header + deflate + checksum).</param>
<param name="originalLength">The expected decompressed size in bytes.</param>
<returns>The decompressed byte array.</returns>
<exception cref="T:System.IO.InvalidDataException">Thrown when decompression fails or data is corrupt.</exception>
</member>
<member name="M:GsaEditor.Core.Compression.ZlibHelper.Compress(System.Byte[])">
<summary>
Compresses data using zlib format: 2-byte header + deflate stream + 4-byte Adler-32 checksum.
</summary>
<param name="data">The uncompressed data to compress.</param>
<returns>The zlib-wrapped compressed byte array.</returns>
</member>
<member name="M:GsaEditor.Core.Compression.ZlibHelper.ComputeAdler32(System.Byte[])">
<summary>
Computes the Adler-32 checksum of the given data.
</summary>
<param name="data">The data to checksum.</param>
<returns>The 32-bit Adler-32 checksum value.</returns>
</member>
<member name="T:GsaEditor.Core.IO.GsaIndexEntry">
<summary>
Represents a single entry parsed from a .idx NML index file.
</summary>
</member>
<member name="P:GsaEditor.Core.IO.GsaIndexEntry.Id">
<summary>
The file alias (relative path) of the entry.
</summary>
</member>
<member name="P:GsaEditor.Core.IO.GsaIndexEntry.Method">
<summary>
Compression method: 0 = raw, 1 = zlib.
</summary>
</member>
<member name="P:GsaEditor.Core.IO.GsaIndexEntry.Length">
<summary>
Original (decompressed) size in bytes.
</summary>
</member>
<member name="P:GsaEditor.Core.IO.GsaIndexEntry.CompressedLength">
<summary>
Compressed size in bytes.
</summary>
</member>
<member name="P:GsaEditor.Core.IO.GsaIndexEntry.Offset">
<summary>
Byte offset of the data block within the archive.
</summary>
</member>
<member name="T:GsaEditor.Core.IO.GsaIndexReader">
<summary>
Parses .idx NML index files (XML-compatible format) into a list of <see cref="T:GsaEditor.Core.IO.GsaIndexEntry"/>.
</summary>
</member>
<member name="M:GsaEditor.Core.IO.GsaIndexReader.Read(System.String)">
<summary>
Reads index entries from the specified .idx file.
</summary>
<param name="filePath">The path to the .idx file.</param>
<returns>A list of parsed index entries.</returns>
</member>
<member name="T:GsaEditor.Core.IO.GsaIndexWriter">
<summary>
Writes a .idx NML index file from a <see cref="T:GsaEditor.Core.Models.GsaArchive"/>.
The index file is XML-compatible and lists each entry with its alias, compression method,
sizes, and data offset for fast lookup without a full sequential scan.
</summary>
</member>
<member name="M:GsaEditor.Core.IO.GsaIndexWriter.Write(GsaEditor.Core.Models.GsaArchive,System.String)">
<summary>
Writes the index file for the given archive to the specified path.
</summary>
<param name="archive">The archive whose entries will be indexed.</param>
<param name="filePath">The output .idx file path.</param>
</member>
<member name="T:GsaEditor.Core.IO.GsaReader">
<summary>
Reads a .gsa binary archive from a stream or file and produces a <see cref="T:GsaEditor.Core.Models.GsaArchive"/>.
Supports both NARC (legacy) and NARD (enhanced with padding) format variants.
</summary>
</member>
<member name="F:GsaEditor.Core.IO.GsaReader.MAGIC_NARC">
<summary>Magic word for the NARC (legacy) format.</summary>
</member>
<member name="F:GsaEditor.Core.IO.GsaReader.MAGIC_NARD">
<summary>Magic word for the NARD (enhanced) format.</summary>
</member>
<member name="F:GsaEditor.Core.IO.GsaReader.MAX_ALIAS_LENGTH">
<summary>Maximum allowed alias length in characters.</summary>
</member>
<member name="M:GsaEditor.Core.IO.GsaReader.Read(System.String)">
<summary>
Reads a GSA archive from the specified file path.
</summary>
<param name="filePath">The path to the .gsa archive file.</param>
<returns>A fully populated <see cref="T:GsaEditor.Core.Models.GsaArchive"/>.</returns>
<exception cref="T:System.IO.InvalidDataException">Thrown if the file has an invalid magic word.</exception>
</member>
<member name="M:GsaEditor.Core.IO.GsaReader.Read(System.IO.Stream)">
<summary>
Reads a GSA archive from a stream by performing a sequential scan of all entries.
</summary>
<param name="stream">A readable, seekable stream positioned at the start of the archive.</param>
<returns>A fully populated <see cref="T:GsaEditor.Core.Models.GsaArchive"/>.</returns>
<exception cref="T:System.IO.InvalidDataException">Thrown if the stream has an invalid magic word.</exception>
</member>
<member name="M:GsaEditor.Core.IO.GsaReader.ReadEntry(System.IO.BinaryReader,GsaEditor.Core.Models.GsaArchive)">
<summary>
Reads a single file entry from the current stream position.
</summary>
<param name="reader">The binary reader positioned at the start of an entry.</param>
<param name="archive">The parent archive (used for NARD padding alignment).</param>
<returns>A populated <see cref="T:GsaEditor.Core.Models.GsaEntry"/>, or null if no more entries can be read.</returns>
</member>
<member name="M:GsaEditor.Core.IO.GsaReader.AlignStream(System.IO.Stream,System.Int32)">
<summary>
Advances the stream position to the next aligned boundary.
</summary>
<param name="stream">The stream to align.</param>
<param name="alignment">The alignment boundary in bytes.</param>
</member>
<member name="T:GsaEditor.Core.IO.GsaWriter">
<summary>
Writes a <see cref="T:GsaEditor.Core.Models.GsaArchive"/> to a binary .gsa stream or file.
Supports both NARC (legacy) and NARD (enhanced with padding) format variants.
</summary>
</member>
<member name="F:GsaEditor.Core.IO.GsaWriter.MAGIC_NARC">
<summary>Magic word for the NARC (legacy) format.</summary>
</member>
<member name="F:GsaEditor.Core.IO.GsaWriter.MAGIC_NARD">
<summary>Magic word for the NARD (enhanced) format.</summary>
</member>
<member name="M:GsaEditor.Core.IO.GsaWriter.Write(GsaEditor.Core.Models.GsaArchive,System.String)">
<summary>
Writes the archive to the specified file path.
</summary>
<param name="archive">The archive to write.</param>
<param name="filePath">The output file path.</param>
</member>
<member name="M:GsaEditor.Core.IO.GsaWriter.Write(GsaEditor.Core.Models.GsaArchive,System.IO.Stream)">
<summary>
Writes the archive to the given stream.
Each entry's <see cref="P:GsaEditor.Core.Models.GsaEntry.DataOffset"/> is updated to reflect the new position in the output.
</summary>
<param name="archive">The archive to write.</param>
<param name="stream">A writable stream.</param>
</member>
<member name="M:GsaEditor.Core.IO.GsaWriter.WriteEntry(System.IO.BinaryWriter,GsaEditor.Core.Models.GsaEntry,GsaEditor.Core.Models.GsaArchive)">
<summary>
Writes a single file entry at the current stream position.
</summary>
<param name="writer">The binary writer.</param>
<param name="entry">The entry to write.</param>
<param name="archive">The parent archive (used for NARD padding alignment).</param>
</member>
<member name="M:GsaEditor.Core.IO.GsaWriter.WritePadding(System.IO.BinaryWriter,System.Int32)">
<summary>
Writes zero-bytes to pad the stream to the next aligned boundary.
</summary>
<param name="writer">The binary writer.</param>
<param name="alignment">The alignment boundary in bytes.</param>
</member>
<member name="T:GsaEditor.Core.Models.GsaArchive">
<summary>
Represents a loaded GSA archive with its global header information and file entries.
</summary>
</member>
<member name="P:GsaEditor.Core.Models.GsaArchive.Format">
<summary>
The format variant of this archive (NARC or NARD).
</summary>
</member>
<member name="P:GsaEditor.Core.Models.GsaArchive.OffsetPadding">
<summary>
(NARD only) Byte alignment for data offsets. Zero or unused for NARC.
</summary>
</member>
<member name="P:GsaEditor.Core.Models.GsaArchive.SizePadding">
<summary>
(NARD only) Byte alignment for data sizes. Zero or unused for NARC.
</summary>
</member>
<member name="P:GsaEditor.Core.Models.GsaArchive.Entries">
<summary>
The ordered list of file entries in the archive.
</summary>
</member>
<member name="T:GsaEditor.Core.Models.GsaEntry">
<summary>
Represents a single file entry within a GSA archive.
</summary>
</member>
<member name="P:GsaEditor.Core.Models.GsaEntry.Alias">
<summary>
Relative path (alias) of the file within the archive, using '/' as separator.
Maximum 511 characters.
</summary>
</member>
<member name="P:GsaEditor.Core.Models.GsaEntry.IsCompressed">
<summary>
Whether this entry's data is zlib-compressed.
</summary>
</member>
<member name="P:GsaEditor.Core.Models.GsaEntry.OriginalLength">
<summary>
Original (decompressed) size of the file in bytes.
</summary>
</member>
<member name="P:GsaEditor.Core.Models.GsaEntry.CompressedLength">
<summary>
Compressed size of the file in bytes. Equal to <see cref="P:GsaEditor.Core.Models.GsaEntry.OriginalLength"/> if not compressed.
</summary>
</member>
<member name="P:GsaEditor.Core.Models.GsaEntry.RawData">
<summary>
Raw data as stored in the archive (compressed bytes if <see cref="P:GsaEditor.Core.Models.GsaEntry.IsCompressed"/> is true).
</summary>
</member>
<member name="P:GsaEditor.Core.Models.GsaEntry.DataOffset">
<summary>
Byte offset of this entry's data block within the archive file.
</summary>
</member>
<member name="M:GsaEditor.Core.Models.GsaEntry.GetDecompressedData">
<summary>
Returns the decompressed data for this entry.
If the entry is not compressed, returns <see cref="P:GsaEditor.Core.Models.GsaEntry.RawData"/> directly.
</summary>
<returns>The decompressed file content.</returns>
</member>
<member name="M:GsaEditor.Core.Models.GsaEntry.SetData(System.Byte[],System.Boolean)">
<summary>
Sets the entry data from uncompressed bytes, optionally compressing with zlib.
Updates <see cref="P:GsaEditor.Core.Models.GsaEntry.OriginalLength"/>, <see cref="P:GsaEditor.Core.Models.GsaEntry.CompressedLength"/>,
<see cref="P:GsaEditor.Core.Models.GsaEntry.IsCompressed"/>, and <see cref="P:GsaEditor.Core.Models.GsaEntry.RawData"/>.
</summary>
<param name="uncompressedData">The uncompressed file content.</param>
<param name="compress">Whether to compress the data with zlib.</param>
</member>
<member name="T:GsaEditor.Core.Models.GsaFormat">
<summary>
Identifies the archive format variant.
</summary>
</member>
<member name="F:GsaEditor.Core.Models.GsaFormat.NARC">
<summary>
Legacy format with no padding support. Magic word = 0x4E415243 ("NARC").
</summary>
</member>
<member name="F:GsaEditor.Core.Models.GsaFormat.NARD">
<summary>
Enhanced format with offset and size padding for console DVD alignment.
Magic word = 0x4E415244 ("NARD").
</summary>
</member>
</members>
</doc>

View File

@ -0,0 +1,4 @@
// <autogenerated />
using System;
using System.Reflection;
[assembly: global::System.Runtime.Versioning.TargetFrameworkAttribute(".NETCoreApp,Version=v8.0", FrameworkDisplayName = ".NET 8.0")]

View File

@ -0,0 +1,22 @@
//------------------------------------------------------------------------------
// <auto-generated>
// This code was generated by a tool.
//
// Changes to this file may cause incorrect behavior and will be lost if
// the code is regenerated.
// </auto-generated>
//------------------------------------------------------------------------------
using System;
using System.Reflection;
[assembly: System.Reflection.AssemblyCompanyAttribute("GsaEditor.Core")]
[assembly: System.Reflection.AssemblyConfigurationAttribute("Debug")]
[assembly: System.Reflection.AssemblyFileVersionAttribute("1.0.0.0")]
[assembly: System.Reflection.AssemblyInformationalVersionAttribute("1.0.0")]
[assembly: System.Reflection.AssemblyProductAttribute("GsaEditor.Core")]
[assembly: System.Reflection.AssemblyTitleAttribute("GsaEditor.Core")]
[assembly: System.Reflection.AssemblyVersionAttribute("1.0.0.0")]
// Généré par la classe MSBuild WriteCodeFragment.

View File

@ -0,0 +1 @@
68387796343863a0e5c11cd0f612a1b364816551cf5816c6d556d410439a3710

View File

@ -0,0 +1,15 @@
is_global = true
build_property.TargetFramework = net8.0
build_property.TargetPlatformMinVersion =
build_property.UsingMicrosoftNETSdkWeb =
build_property.ProjectTypeGuids =
build_property.InvariantGlobalization =
build_property.PlatformNeutralAssembly =
build_property.EnforceExtendedAnalyzerRules =
build_property._SupportedPlatformList = Linux,macOS,Windows
build_property.RootNamespace = GsaEditor.Core
build_property.ProjectDir = C:\Users\simulateur\Desktop\GsaViewer\GsaEditor.Core\
build_property.EnableComHosting =
build_property.EnableGeneratedComInterfaceComImportInterop =
build_property.EffectiveAnalysisLevelStyle = 8.0
build_property.EnableCodeStyleSeverity =

View File

@ -0,0 +1,8 @@
// <auto-generated/>
global using global::System;
global using global::System.Collections.Generic;
global using global::System.IO;
global using global::System.Linq;
global using global::System.Net.Http;
global using global::System.Threading;
global using global::System.Threading.Tasks;

View File

@ -0,0 +1 @@
f57ab49a8539dcd5eaae9903c06c691e87a9ead2bd6a913638151d11a0b5901a

View File

@ -0,0 +1,13 @@
C:\Users\simulateur\Desktop\GsaViewer\GsaEditor.Core\bin\Debug\net8.0\GsaEditor.Core.deps.json
C:\Users\simulateur\Desktop\GsaViewer\GsaEditor.Core\bin\Debug\net8.0\GsaEditor.Core.dll
C:\Users\simulateur\Desktop\GsaViewer\GsaEditor.Core\bin\Debug\net8.0\GsaEditor.Core.pdb
C:\Users\simulateur\Desktop\GsaViewer\GsaEditor.Core\bin\Debug\net8.0\GsaEditor.Core.xml
C:\Users\simulateur\Desktop\GsaViewer\GsaEditor.Core\obj\Debug\net8.0\GsaEditor.Core.GeneratedMSBuildEditorConfig.editorconfig
C:\Users\simulateur\Desktop\GsaViewer\GsaEditor.Core\obj\Debug\net8.0\GsaEditor.Core.AssemblyInfoInputs.cache
C:\Users\simulateur\Desktop\GsaViewer\GsaEditor.Core\obj\Debug\net8.0\GsaEditor.Core.AssemblyInfo.cs
C:\Users\simulateur\Desktop\GsaViewer\GsaEditor.Core\obj\Debug\net8.0\GsaEditor.Core.csproj.CoreCompileInputs.cache
C:\Users\simulateur\Desktop\GsaViewer\GsaEditor.Core\obj\Debug\net8.0\GsaEditor.Core.dll
C:\Users\simulateur\Desktop\GsaViewer\GsaEditor.Core\obj\Debug\net8.0\refint\GsaEditor.Core.dll
C:\Users\simulateur\Desktop\GsaViewer\GsaEditor.Core\obj\Debug\net8.0\GsaEditor.Core.xml
C:\Users\simulateur\Desktop\GsaViewer\GsaEditor.Core\obj\Debug\net8.0\GsaEditor.Core.pdb
C:\Users\simulateur\Desktop\GsaViewer\GsaEditor.Core\obj\Debug\net8.0\ref\GsaEditor.Core.dll

Binary file not shown.

Binary file not shown.

View File

@ -0,0 +1,276 @@
<?xml version="1.0"?>
<doc>
<assembly>
<name>GsaEditor.Core</name>
</assembly>
<members>
<member name="T:GsaEditor.Core.Compression.ZlibHelper">
<summary>
Provides zlib compression and decompression utilities for GSA archive entries.
The GSA format stores zlib-wrapped data: 2-byte header (CMF+FLG) + deflate stream + 4-byte Adler-32 checksum.
This helper skips the 2-byte zlib header and uses <see cref="T:System.IO.Compression.DeflateStream"/> for the core deflate data.
</summary>
</member>
<member name="M:GsaEditor.Core.Compression.ZlibHelper.Decompress(System.Byte[],System.UInt32)">
<summary>
Decompresses zlib-wrapped data into a byte array of the specified original size.
Skips the 2-byte zlib header before feeding data to <see cref="T:System.IO.Compression.DeflateStream"/>.
</summary>
<param name="compressedData">The zlib-wrapped compressed data (header + deflate + checksum).</param>
<param name="originalLength">The expected decompressed size in bytes.</param>
<returns>The decompressed byte array.</returns>
<exception cref="T:System.IO.InvalidDataException">Thrown when decompression fails or data is corrupt.</exception>
</member>
<member name="M:GsaEditor.Core.Compression.ZlibHelper.Compress(System.Byte[])">
<summary>
Compresses data using zlib format: 2-byte header + deflate stream + 4-byte Adler-32 checksum.
</summary>
<param name="data">The uncompressed data to compress.</param>
<returns>The zlib-wrapped compressed byte array.</returns>
</member>
<member name="M:GsaEditor.Core.Compression.ZlibHelper.ComputeAdler32(System.Byte[])">
<summary>
Computes the Adler-32 checksum of the given data.
</summary>
<param name="data">The data to checksum.</param>
<returns>The 32-bit Adler-32 checksum value.</returns>
</member>
<member name="T:GsaEditor.Core.IO.GsaIndexEntry">
<summary>
Represents a single entry parsed from a .idx NML index file.
</summary>
</member>
<member name="P:GsaEditor.Core.IO.GsaIndexEntry.Id">
<summary>
The file alias (relative path) of the entry.
</summary>
</member>
<member name="P:GsaEditor.Core.IO.GsaIndexEntry.Method">
<summary>
Compression method: 0 = raw, 1 = zlib.
</summary>
</member>
<member name="P:GsaEditor.Core.IO.GsaIndexEntry.Length">
<summary>
Original (decompressed) size in bytes.
</summary>
</member>
<member name="P:GsaEditor.Core.IO.GsaIndexEntry.CompressedLength">
<summary>
Compressed size in bytes.
</summary>
</member>
<member name="P:GsaEditor.Core.IO.GsaIndexEntry.Offset">
<summary>
Byte offset of the data block within the archive.
</summary>
</member>
<member name="T:GsaEditor.Core.IO.GsaIndexReader">
<summary>
Parses .idx NML index files (XML-compatible format) into a list of <see cref="T:GsaEditor.Core.IO.GsaIndexEntry"/>.
</summary>
</member>
<member name="M:GsaEditor.Core.IO.GsaIndexReader.Read(System.String)">
<summary>
Reads index entries from the specified .idx file.
</summary>
<param name="filePath">The path to the .idx file.</param>
<returns>A list of parsed index entries.</returns>
</member>
<member name="T:GsaEditor.Core.IO.GsaIndexWriter">
<summary>
Writes a .idx NML index file from a <see cref="T:GsaEditor.Core.Models.GsaArchive"/>.
The index file is XML-compatible and lists each entry with its alias, compression method,
sizes, and data offset for fast lookup without a full sequential scan.
</summary>
</member>
<member name="M:GsaEditor.Core.IO.GsaIndexWriter.Write(GsaEditor.Core.Models.GsaArchive,System.String)">
<summary>
Writes the index file for the given archive to the specified path.
</summary>
<param name="archive">The archive whose entries will be indexed.</param>
<param name="filePath">The output .idx file path.</param>
</member>
<member name="T:GsaEditor.Core.IO.GsaReader">
<summary>
Reads a .gsa binary archive from a stream or file and produces a <see cref="T:GsaEditor.Core.Models.GsaArchive"/>.
Supports both NARC (legacy) and NARD (enhanced with padding) format variants.
</summary>
</member>
<member name="F:GsaEditor.Core.IO.GsaReader.MAGIC_NARC">
<summary>Magic word for the NARC (legacy) format.</summary>
</member>
<member name="F:GsaEditor.Core.IO.GsaReader.MAGIC_NARD">
<summary>Magic word for the NARD (enhanced) format.</summary>
</member>
<member name="F:GsaEditor.Core.IO.GsaReader.MAX_ALIAS_LENGTH">
<summary>Maximum allowed alias length in characters.</summary>
</member>
<member name="M:GsaEditor.Core.IO.GsaReader.Read(System.String)">
<summary>
Reads a GSA archive from the specified file path.
</summary>
<param name="filePath">The path to the .gsa archive file.</param>
<returns>A fully populated <see cref="T:GsaEditor.Core.Models.GsaArchive"/>.</returns>
<exception cref="T:System.IO.InvalidDataException">Thrown if the file has an invalid magic word.</exception>
</member>
<member name="M:GsaEditor.Core.IO.GsaReader.Read(System.IO.Stream)">
<summary>
Reads a GSA archive from a stream by performing a sequential scan of all entries.
</summary>
<param name="stream">A readable, seekable stream positioned at the start of the archive.</param>
<returns>A fully populated <see cref="T:GsaEditor.Core.Models.GsaArchive"/>.</returns>
<exception cref="T:System.IO.InvalidDataException">Thrown if the stream has an invalid magic word.</exception>
</member>
<member name="M:GsaEditor.Core.IO.GsaReader.ReadEntry(System.IO.BinaryReader,GsaEditor.Core.Models.GsaArchive)">
<summary>
Reads a single file entry from the current stream position.
</summary>
<param name="reader">The binary reader positioned at the start of an entry.</param>
<param name="archive">The parent archive (used for NARD padding alignment).</param>
<returns>A populated <see cref="T:GsaEditor.Core.Models.GsaEntry"/>, or null if no more entries can be read.</returns>
</member>
<member name="M:GsaEditor.Core.IO.GsaReader.AlignStream(System.IO.Stream,System.Int32)">
<summary>
Advances the stream position to the next aligned boundary.
</summary>
<param name="stream">The stream to align.</param>
<param name="alignment">The alignment boundary in bytes.</param>
</member>
<member name="T:GsaEditor.Core.IO.GsaWriter">
<summary>
Writes a <see cref="T:GsaEditor.Core.Models.GsaArchive"/> to a binary .gsa stream or file.
Supports both NARC (legacy) and NARD (enhanced with padding) format variants.
</summary>
</member>
<member name="F:GsaEditor.Core.IO.GsaWriter.MAGIC_NARC">
<summary>Magic word for the NARC (legacy) format.</summary>
</member>
<member name="F:GsaEditor.Core.IO.GsaWriter.MAGIC_NARD">
<summary>Magic word for the NARD (enhanced) format.</summary>
</member>
<member name="M:GsaEditor.Core.IO.GsaWriter.Write(GsaEditor.Core.Models.GsaArchive,System.String)">
<summary>
Writes the archive to the specified file path.
</summary>
<param name="archive">The archive to write.</param>
<param name="filePath">The output file path.</param>
</member>
<member name="M:GsaEditor.Core.IO.GsaWriter.Write(GsaEditor.Core.Models.GsaArchive,System.IO.Stream)">
<summary>
Writes the archive to the given stream.
Each entry's <see cref="P:GsaEditor.Core.Models.GsaEntry.DataOffset"/> is updated to reflect the new position in the output.
</summary>
<param name="archive">The archive to write.</param>
<param name="stream">A writable stream.</param>
</member>
<member name="M:GsaEditor.Core.IO.GsaWriter.WriteEntry(System.IO.BinaryWriter,GsaEditor.Core.Models.GsaEntry,GsaEditor.Core.Models.GsaArchive)">
<summary>
Writes a single file entry at the current stream position.
</summary>
<param name="writer">The binary writer.</param>
<param name="entry">The entry to write.</param>
<param name="archive">The parent archive (used for NARD padding alignment).</param>
</member>
<member name="M:GsaEditor.Core.IO.GsaWriter.WritePadding(System.IO.BinaryWriter,System.Int32)">
<summary>
Writes zero-bytes to pad the stream to the next aligned boundary.
</summary>
<param name="writer">The binary writer.</param>
<param name="alignment">The alignment boundary in bytes.</param>
</member>
<member name="T:GsaEditor.Core.Models.GsaArchive">
<summary>
Represents a loaded GSA archive with its global header information and file entries.
</summary>
</member>
<member name="P:GsaEditor.Core.Models.GsaArchive.Format">
<summary>
The format variant of this archive (NARC or NARD).
</summary>
</member>
<member name="P:GsaEditor.Core.Models.GsaArchive.OffsetPadding">
<summary>
(NARD only) Byte alignment for data offsets. Zero or unused for NARC.
</summary>
</member>
<member name="P:GsaEditor.Core.Models.GsaArchive.SizePadding">
<summary>
(NARD only) Byte alignment for data sizes. Zero or unused for NARC.
</summary>
</member>
<member name="P:GsaEditor.Core.Models.GsaArchive.Entries">
<summary>
The ordered list of file entries in the archive.
</summary>
</member>
<member name="T:GsaEditor.Core.Models.GsaEntry">
<summary>
Represents a single file entry within a GSA archive.
</summary>
</member>
<member name="P:GsaEditor.Core.Models.GsaEntry.Alias">
<summary>
Relative path (alias) of the file within the archive, using '/' as separator.
Maximum 511 characters.
</summary>
</member>
<member name="P:GsaEditor.Core.Models.GsaEntry.IsCompressed">
<summary>
Whether this entry's data is zlib-compressed.
</summary>
</member>
<member name="P:GsaEditor.Core.Models.GsaEntry.OriginalLength">
<summary>
Original (decompressed) size of the file in bytes.
</summary>
</member>
<member name="P:GsaEditor.Core.Models.GsaEntry.CompressedLength">
<summary>
Compressed size of the file in bytes. Equal to <see cref="P:GsaEditor.Core.Models.GsaEntry.OriginalLength"/> if not compressed.
</summary>
</member>
<member name="P:GsaEditor.Core.Models.GsaEntry.RawData">
<summary>
Raw data as stored in the archive (compressed bytes if <see cref="P:GsaEditor.Core.Models.GsaEntry.IsCompressed"/> is true).
</summary>
</member>
<member name="P:GsaEditor.Core.Models.GsaEntry.DataOffset">
<summary>
Byte offset of this entry's data block within the archive file.
</summary>
</member>
<member name="M:GsaEditor.Core.Models.GsaEntry.GetDecompressedData">
<summary>
Returns the decompressed data for this entry.
If the entry is not compressed, returns <see cref="P:GsaEditor.Core.Models.GsaEntry.RawData"/> directly.
</summary>
<returns>The decompressed file content.</returns>
</member>
<member name="M:GsaEditor.Core.Models.GsaEntry.SetData(System.Byte[],System.Boolean)">
<summary>
Sets the entry data from uncompressed bytes, optionally compressing with zlib.
Updates <see cref="P:GsaEditor.Core.Models.GsaEntry.OriginalLength"/>, <see cref="P:GsaEditor.Core.Models.GsaEntry.CompressedLength"/>,
<see cref="P:GsaEditor.Core.Models.GsaEntry.IsCompressed"/>, and <see cref="P:GsaEditor.Core.Models.GsaEntry.RawData"/>.
</summary>
<param name="uncompressedData">The uncompressed file content.</param>
<param name="compress">Whether to compress the data with zlib.</param>
</member>
<member name="T:GsaEditor.Core.Models.GsaFormat">
<summary>
Identifies the archive format variant.
</summary>
</member>
<member name="F:GsaEditor.Core.Models.GsaFormat.NARC">
<summary>
Legacy format with no padding support. Magic word = 0x4E415243 ("NARC").
</summary>
</member>
<member name="F:GsaEditor.Core.Models.GsaFormat.NARD">
<summary>
Enhanced format with offset and size padding for console DVD alignment.
Magic word = 0x4E415244 ("NARD").
</summary>
</member>
</members>
</doc>

Binary file not shown.

View File

@ -0,0 +1,81 @@
{
"format": 1,
"restore": {
"C:\\Users\\simulateur\\Desktop\\GsaViewer\\GsaEditor.Core\\GsaEditor.Core.csproj": {}
},
"projects": {
"C:\\Users\\simulateur\\Desktop\\GsaViewer\\GsaEditor.Core\\GsaEditor.Core.csproj": {
"version": "1.0.0",
"restore": {
"projectUniqueName": "C:\\Users\\simulateur\\Desktop\\GsaViewer\\GsaEditor.Core\\GsaEditor.Core.csproj",
"projectName": "GsaEditor.Core",
"projectPath": "C:\\Users\\simulateur\\Desktop\\GsaViewer\\GsaEditor.Core\\GsaEditor.Core.csproj",
"packagesPath": "C:\\Users\\simulateur\\.nuget\\packages\\",
"outputPath": "C:\\Users\\simulateur\\Desktop\\GsaViewer\\GsaEditor.Core\\obj\\",
"projectStyle": "PackageReference",
"configFilePaths": [
"C:\\Users\\simulateur\\AppData\\Roaming\\NuGet\\NuGet.Config"
],
"originalTargetFrameworks": [
"net8.0"
],
"sources": {
"https://api.nuget.org/v3/index.json": {}
},
"frameworks": {
"net8.0": {
"targetAlias": "net8.0",
"projectReferences": {}
}
},
"warningProperties": {
"warnAsError": [
"NU1605"
]
},
"restoreAuditProperties": {
"enableAudit": "true",
"auditLevel": "low",
"auditMode": "direct"
},
"SdkAnalysisLevel": "9.0.300"
},
"frameworks": {
"net8.0": {
"targetAlias": "net8.0",
"imports": [
"net461",
"net462",
"net47",
"net471",
"net472",
"net48",
"net481"
],
"assetTargetFallback": true,
"warn": true,
"downloadDependencies": [
{
"name": "Microsoft.AspNetCore.App.Ref",
"version": "[8.0.21, 8.0.21]"
},
{
"name": "Microsoft.NETCore.App.Ref",
"version": "[8.0.21, 8.0.21]"
},
{
"name": "Microsoft.WindowsDesktop.App.Ref",
"version": "[8.0.21, 8.0.21]"
}
],
"frameworkReferences": {
"Microsoft.NETCore.App": {
"privateAssets": "all"
}
},
"runtimeIdentifierGraphPath": "C:\\Program Files\\dotnet\\sdk\\9.0.306/PortableRuntimeIdentifierGraph.json"
}
}
}
}
}

View File

@ -0,0 +1,15 @@
<?xml version="1.0" encoding="utf-8" standalone="no"?>
<Project ToolsVersion="14.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<PropertyGroup Condition=" '$(ExcludeRestorePackageImports)' != 'true' ">
<RestoreSuccess Condition=" '$(RestoreSuccess)' == '' ">True</RestoreSuccess>
<RestoreTool Condition=" '$(RestoreTool)' == '' ">NuGet</RestoreTool>
<ProjectAssetsFile Condition=" '$(ProjectAssetsFile)' == '' ">$(MSBuildThisFileDirectory)project.assets.json</ProjectAssetsFile>
<NuGetPackageRoot Condition=" '$(NuGetPackageRoot)' == '' ">$(UserProfile)\.nuget\packages\</NuGetPackageRoot>
<NuGetPackageFolders Condition=" '$(NuGetPackageFolders)' == '' ">C:\Users\simulateur\.nuget\packages\</NuGetPackageFolders>
<NuGetProjectStyle Condition=" '$(NuGetProjectStyle)' == '' ">PackageReference</NuGetProjectStyle>
<NuGetToolVersion Condition=" '$(NuGetToolVersion)' == '' ">6.14.0</NuGetToolVersion>
</PropertyGroup>
<ItemGroup Condition=" '$(ExcludeRestorePackageImports)' != 'true' ">
<SourceRoot Include="C:\Users\simulateur\.nuget\packages\" />
</ItemGroup>
</Project>

View File

@ -0,0 +1,2 @@
<?xml version="1.0" encoding="utf-8" standalone="no"?>
<Project ToolsVersion="14.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003" />

View File

@ -0,0 +1,86 @@
{
"version": 3,
"targets": {
"net8.0": {}
},
"libraries": {},
"projectFileDependencyGroups": {
"net8.0": []
},
"packageFolders": {
"C:\\Users\\simulateur\\.nuget\\packages\\": {}
},
"project": {
"version": "1.0.0",
"restore": {
"projectUniqueName": "C:\\Users\\simulateur\\Desktop\\GsaViewer\\GsaEditor.Core\\GsaEditor.Core.csproj",
"projectName": "GsaEditor.Core",
"projectPath": "C:\\Users\\simulateur\\Desktop\\GsaViewer\\GsaEditor.Core\\GsaEditor.Core.csproj",
"packagesPath": "C:\\Users\\simulateur\\.nuget\\packages\\",
"outputPath": "C:\\Users\\simulateur\\Desktop\\GsaViewer\\GsaEditor.Core\\obj\\",
"projectStyle": "PackageReference",
"configFilePaths": [
"C:\\Users\\simulateur\\AppData\\Roaming\\NuGet\\NuGet.Config"
],
"originalTargetFrameworks": [
"net8.0"
],
"sources": {
"https://api.nuget.org/v3/index.json": {}
},
"frameworks": {
"net8.0": {
"targetAlias": "net8.0",
"projectReferences": {}
}
},
"warningProperties": {
"warnAsError": [
"NU1605"
]
},
"restoreAuditProperties": {
"enableAudit": "true",
"auditLevel": "low",
"auditMode": "direct"
},
"SdkAnalysisLevel": "9.0.300"
},
"frameworks": {
"net8.0": {
"targetAlias": "net8.0",
"imports": [
"net461",
"net462",
"net47",
"net471",
"net472",
"net48",
"net481"
],
"assetTargetFallback": true,
"warn": true,
"downloadDependencies": [
{
"name": "Microsoft.AspNetCore.App.Ref",
"version": "[8.0.21, 8.0.21]"
},
{
"name": "Microsoft.NETCore.App.Ref",
"version": "[8.0.21, 8.0.21]"
},
{
"name": "Microsoft.WindowsDesktop.App.Ref",
"version": "[8.0.21, 8.0.21]"
}
],
"frameworkReferences": {
"Microsoft.NETCore.App": {
"privateAssets": "all"
}
},
"runtimeIdentifierGraphPath": "C:\\Program Files\\dotnet\\sdk\\9.0.306/PortableRuntimeIdentifierGraph.json"
}
}
}
}

View File

@ -0,0 +1,12 @@
{
"version": 2,
"dgSpecHash": "8Lf0I0TQO2o=",
"success": true,
"projectFilePath": "C:\\Users\\simulateur\\Desktop\\GsaViewer\\GsaEditor.Core\\GsaEditor.Core.csproj",
"expectedPackageFiles": [
"C:\\Users\\simulateur\\.nuget\\packages\\microsoft.netcore.app.ref\\8.0.21\\microsoft.netcore.app.ref.8.0.21.nupkg.sha512",
"C:\\Users\\simulateur\\.nuget\\packages\\microsoft.windowsdesktop.app.ref\\8.0.21\\microsoft.windowsdesktop.app.ref.8.0.21.nupkg.sha512",
"C:\\Users\\simulateur\\.nuget\\packages\\microsoft.aspnetcore.app.ref\\8.0.21\\microsoft.aspnetcore.app.ref.8.0.21.nupkg.sha512"
],
"logs": []
}

25
GsaEditor.sln Normal file
View File

@ -0,0 +1,25 @@
Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio Version 17
VisualStudioVersion = 17.0.31903.59
MinimumVisualStudioVersion = 10.0.40219.1
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "GsaEditor", "GsaEditor\GsaEditor.csproj", "{A1B2C3D4-E5F6-7890-ABCD-EF1234567890}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "GsaEditor.Core", "GsaEditor.Core\GsaEditor.Core.csproj", "{B2C3D4E5-F6A7-8901-BCDE-F12345678901}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Release|Any CPU = Release|Any CPU
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{A1B2C3D4-E5F6-7890-ABCD-EF1234567890}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{A1B2C3D4-E5F6-7890-ABCD-EF1234567890}.Debug|Any CPU.Build.0 = Debug|Any CPU
{A1B2C3D4-E5F6-7890-ABCD-EF1234567890}.Release|Any CPU.ActiveCfg = Release|Any CPU
{A1B2C3D4-E5F6-7890-ABCD-EF1234567890}.Release|Any CPU.Build.0 = Release|Any CPU
{B2C3D4E5-F6A7-8901-BCDE-F12345678901}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{B2C3D4E5-F6A7-8901-BCDE-F12345678901}.Debug|Any CPU.Build.0 = Debug|Any CPU
{B2C3D4E5-F6A7-8901-BCDE-F12345678901}.Release|Any CPU.ActiveCfg = Release|Any CPU
{B2C3D4E5-F6A7-8901-BCDE-F12345678901}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
EndGlobal

9
GsaEditor/App.axaml Normal file
View File

@ -0,0 +1,9 @@
<Application xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
x:Class="GsaEditor.App"
RequestedThemeVariant="Default">
<Application.Styles>
<FluentTheme />
</Application.Styles>
</Application>

35
GsaEditor/App.axaml.cs Normal file
View File

@ -0,0 +1,35 @@
using Avalonia;
using Avalonia.Controls.ApplicationLifetimes;
using Avalonia.Data.Core.Plugins;
using Avalonia.Markup.Xaml;
using GsaEditor.ViewModels;
using GsaEditor.Views;
namespace GsaEditor;
public partial class App : Application
{
public override void Initialize()
{
AvaloniaXamlLoader.Load(this);
}
public override void OnFrameworkInitializationCompleted()
{
if (ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop)
{
// Avoid duplicate validations from both Avalonia and the CommunityToolkit.
var dataValidationPluginsToRemove =
BindingPlugins.DataValidators.OfType<DataAnnotationsValidationPlugin>().ToArray();
foreach (var plugin in dataValidationPluginsToRemove)
BindingPlugins.DataValidators.Remove(plugin);
desktop.MainWindow = new MainWindow
{
DataContext = new MainWindowViewModel()
};
}
base.OnFrameworkInitializationCompleted();
}
}

View File

@ -0,0 +1,31 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>WinExe</OutputType>
<TargetFramework>net8.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
<BuiltInComInteropSupport>true</BuiltInComInteropSupport>
<ApplicationManifest>app.manifest</ApplicationManifest>
<AvaloniaUseCompiledBindingsByDefault>true</AvaloniaUseCompiledBindingsByDefault>
</PropertyGroup>
<ItemGroup>
<AvaloniaResource Include="Assets\**"/>
</ItemGroup>
<ItemGroup>
<PackageReference Include="Avalonia" Version="11.3.6"/>
<PackageReference Include="Avalonia.Desktop" Version="11.3.6"/>
<PackageReference Include="Avalonia.Themes.Fluent" Version="11.3.6"/>
<PackageReference Include="Avalonia.Fonts.Inter" Version="11.3.6"/>
<PackageReference Include="Avalonia.Diagnostics" Version="11.3.6">
<IncludeAssets Condition="'$(Configuration)' != 'Debug'">None</IncludeAssets>
<PrivateAssets Condition="'$(Configuration)' != 'Debug'">All</PrivateAssets>
</PackageReference>
<PackageReference Include="CommunityToolkit.Mvvm" Version="8.2.1"/>
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\GsaEditor.Core\GsaEditor.Core.csproj"/>
</ItemGroup>
</Project>

View File

@ -0,0 +1,204 @@
using Avalonia;
using Avalonia.Controls;
using Avalonia.Layout;
namespace GsaEditor.Helpers;
/// <summary>
/// Provides simple dialog utilities (confirm, message, input) using programmatically-created Avalonia windows.
/// No third-party controls or custom themes are used.
/// </summary>
public static class Dialogs
{
/// <summary>
/// Shows a confirmation dialog with Yes/No buttons.
/// </summary>
/// <param name="parent">The parent window.</param>
/// <param name="title">Dialog title.</param>
/// <param name="message">Message to display.</param>
/// <returns>True if the user clicked Yes, false otherwise.</returns>
public static async Task<bool> ConfirmAsync(Window parent, string title, string message)
{
var result = false;
var dialog = new Window
{
Title = title,
Width = 420,
Height = 170,
WindowStartupLocation = WindowStartupLocation.CenterOwner,
CanResize = false,
ShowInTaskbar = false
};
var messageBlock = new TextBlock
{
Text = message,
TextWrapping = Avalonia.Media.TextWrapping.Wrap,
Margin = new Thickness(0, 0, 0, 15)
};
var yesButton = new Button
{
Content = "Yes",
Width = 80,
HorizontalContentAlignment = HorizontalAlignment.Center
};
yesButton.Click += (_, _) =>
{
result = true;
dialog.Close();
};
var noButton = new Button
{
Content = "No",
Width = 80,
HorizontalContentAlignment = HorizontalAlignment.Center
};
noButton.Click += (_, _) =>
{
result = false;
dialog.Close();
};
var buttonPanel = new StackPanel
{
Orientation = Orientation.Horizontal,
HorizontalAlignment = HorizontalAlignment.Right,
Spacing = 10,
Children = { yesButton, noButton }
};
dialog.Content = new StackPanel
{
Margin = new Thickness(20),
Spacing = 5,
Children = { messageBlock, buttonPanel }
};
await dialog.ShowDialog(parent);
return result;
}
/// <summary>
/// Shows a simple message dialog with an OK button.
/// </summary>
/// <param name="parent">The parent window.</param>
/// <param name="title">Dialog title.</param>
/// <param name="message">Message to display.</param>
public static async Task ShowMessageAsync(Window parent, string title, string message)
{
var dialog = new Window
{
Title = title,
Width = 420,
Height = 180,
WindowStartupLocation = WindowStartupLocation.CenterOwner,
CanResize = false,
ShowInTaskbar = false
};
var messageBlock = new TextBlock
{
Text = message,
TextWrapping = Avalonia.Media.TextWrapping.Wrap,
Margin = new Thickness(0, 0, 0, 15)
};
var okButton = new Button
{
Content = "OK",
Width = 80,
HorizontalAlignment = HorizontalAlignment.Right,
HorizontalContentAlignment = HorizontalAlignment.Center
};
okButton.Click += (_, _) => dialog.Close();
dialog.Content = new StackPanel
{
Margin = new Thickness(20),
Spacing = 5,
Children = { messageBlock, okButton }
};
await dialog.ShowDialog(parent);
}
/// <summary>
/// Shows an input dialog with a text box, OK and Cancel buttons.
/// </summary>
/// <param name="parent">The parent window.</param>
/// <param name="title">Dialog title.</param>
/// <param name="prompt">Prompt text shown above the input.</param>
/// <param name="defaultValue">Default value in the text box.</param>
/// <returns>The entered string, or null if cancelled.</returns>
public static async Task<string?> InputAsync(Window parent, string title, string prompt, string defaultValue = "")
{
string? result = null;
var dialog = new Window
{
Title = title,
Width = 450,
Height = 180,
WindowStartupLocation = WindowStartupLocation.CenterOwner,
CanResize = false,
ShowInTaskbar = false
};
var promptBlock = new TextBlock
{
Text = prompt,
Margin = new Thickness(0, 0, 0, 5)
};
var textBox = new TextBox
{
Text = defaultValue,
Margin = new Thickness(0, 0, 0, 10)
};
var okButton = new Button
{
Content = "OK",
Width = 80,
HorizontalContentAlignment = HorizontalAlignment.Center
};
okButton.Click += (_, _) =>
{
result = textBox.Text;
dialog.Close();
};
var cancelButton = new Button
{
Content = "Cancel",
Width = 80,
HorizontalContentAlignment = HorizontalAlignment.Center
};
cancelButton.Click += (_, _) =>
{
result = null;
dialog.Close();
};
var buttonPanel = new StackPanel
{
Orientation = Orientation.Horizontal,
HorizontalAlignment = HorizontalAlignment.Right,
Spacing = 10,
Children = { okButton, cancelButton }
};
dialog.Content = new StackPanel
{
Margin = new Thickness(20),
Spacing = 5,
Children = { promptBlock, textBox, buttonPanel }
};
await dialog.ShowDialog(parent);
return result;
}
}

View File

@ -0,0 +1,59 @@
using System.Text;
namespace GsaEditor.Helpers;
/// <summary>
/// Produces a classic hex + ASCII dump string from a byte array.
/// </summary>
public static class HexDumper
{
/// <summary>
/// Formats a byte array as a hex dump with offset, hex values, and printable ASCII characters.
/// </summary>
/// <param name="data">The data to dump.</param>
/// <param name="maxBytes">Maximum number of bytes to include in the dump.</param>
/// <returns>A formatted hex dump string.</returns>
public static string Dump(byte[] data, int maxBytes = 512)
{
var sb = new StringBuilder();
int length = Math.Min(data.Length, maxBytes);
const int bytesPerLine = 16;
for (int offset = 0; offset < length; offset += bytesPerLine)
{
// Offset column
sb.Append($"{offset:X8} ");
int lineLen = Math.Min(bytesPerLine, length - offset);
// Hex columns
for (int i = 0; i < bytesPerLine; i++)
{
if (i < lineLen)
sb.Append($"{data[offset + i]:X2} ");
else
sb.Append(" ");
if (i == 7) sb.Append(' ');
}
sb.Append(' ');
// ASCII column
for (int i = 0; i < lineLen; i++)
{
byte b = data[offset + i];
sb.Append(b >= 0x20 && b < 0x7F ? (char)b : '.');
}
sb.AppendLine();
}
if (data.Length > maxBytes)
{
sb.AppendLine($"... ({data.Length - maxBytes} more bytes)");
}
return sb.ToString();
}
}

17
GsaEditor/Program.cs Normal file
View File

@ -0,0 +1,17 @@
using Avalonia;
using System;
namespace GsaEditor;
sealed class Program
{
[STAThread]
public static void Main(string[] args) => BuildAvaloniaApp()
.StartWithClassicDesktopLifetime(args);
public static AppBuilder BuildAvaloniaApp()
=> AppBuilder.Configure<App>()
.UsePlatformDetect()
.WithInterFont()
.LogToTrace();
}

View File

@ -0,0 +1,40 @@
using System.Collections.ObjectModel;
using CommunityToolkit.Mvvm.ComponentModel;
using GsaEditor.Core.Models;
namespace GsaEditor.ViewModels;
/// <summary>
/// Represents a node in the archive tree view.
/// Can be a folder (with children) or a leaf file node (with an associated <see cref="GsaEntry"/>).
/// </summary>
public partial class EntryTreeNodeViewModel : ViewModelBase
{
[ObservableProperty]
private string _name = string.Empty;
/// <summary>
/// Full relative path of this node within the archive (using '/' separator).
/// </summary>
public string FullPath { get; set; } = string.Empty;
/// <summary>
/// Whether this node represents a folder (true) or a file (false).
/// </summary>
public bool IsFolder { get; set; }
/// <summary>
/// Whether this node represents a file.
/// </summary>
public bool IsFile => !IsFolder;
/// <summary>
/// The archive entry this node represents. Null for folder nodes.
/// </summary>
public GsaEntry? Entry { get; set; }
/// <summary>
/// Child nodes (sub-folders and files within this folder).
/// </summary>
public ObservableCollection<EntryTreeNodeViewModel> Children { get; } = new();
}

View File

@ -0,0 +1,93 @@
using CommunityToolkit.Mvvm.ComponentModel;
using GsaEditor.Core.Models;
namespace GsaEditor.ViewModels;
/// <summary>
/// Wraps a <see cref="GsaEntry"/> for display in the details panel.
/// </summary>
public partial class EntryViewModel : ViewModelBase
{
private readonly GsaEntry _entry;
[ObservableProperty]
private string _alias = string.Empty;
[ObservableProperty]
private bool _isCompressed;
/// <summary>
/// Callback invoked when entry data is modified (compression toggle, alias change).
/// </summary>
public Action? OnModified { get; set; }
public EntryViewModel(GsaEntry entry)
{
_entry = entry;
_alias = entry.Alias;
_isCompressed = entry.IsCompressed;
}
/// <summary>
/// The underlying <see cref="GsaEntry"/>.
/// </summary>
public GsaEntry Entry => _entry;
/// <summary>
/// Original (decompressed) file size in bytes.
/// </summary>
public uint OriginalSize => _entry.OriginalLength;
/// <summary>
/// Compressed file size in bytes.
/// </summary>
public uint CompressedSize => _entry.CompressedLength;
/// <summary>
/// Compression ratio as a percentage (0100). Higher means more compression.
/// </summary>
public string CompressionRatio => OriginalSize > 0
? $"{(1.0 - (double)CompressedSize / OriginalSize) * 100.0:F1}%"
: "0.0%";
/// <summary>
/// Human-readable original size string.
/// </summary>
public string OriginalSizeText => FormatSize(OriginalSize);
/// <summary>
/// Human-readable compressed size string.
/// </summary>
public string CompressedSizeText => FormatSize(CompressedSize);
partial void OnAliasChanged(string value)
{
if (_entry.Alias != value)
{
_entry.Alias = value;
OnModified?.Invoke();
}
}
partial void OnIsCompressedChanged(bool value)
{
if (_entry.IsCompressed != value)
{
var decompressed = _entry.GetDecompressedData();
_entry.SetData(decompressed, value);
OnPropertyChanged(nameof(OriginalSize));
OnPropertyChanged(nameof(CompressedSize));
OnPropertyChanged(nameof(CompressionRatio));
OnPropertyChanged(nameof(OriginalSizeText));
OnPropertyChanged(nameof(CompressedSizeText));
OnModified?.Invoke();
}
}
private static string FormatSize(long bytes)
{
if (bytes < 1024) return $"{bytes} B";
if (bytes < 1024 * 1024) return $"{bytes / 1024.0:F1} KB";
return $"{bytes / (1024.0 * 1024.0):F1} MB";
}
}

View File

@ -0,0 +1,726 @@
using System.Collections.ObjectModel;
using System.Text;
using Avalonia.Controls;
using Avalonia.Media.Imaging;
using Avalonia.Platform.Storage;
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using GsaEditor.Core.Compression;
using GsaEditor.Core.IO;
using GsaEditor.Core.Models;
using GsaEditor.Helpers;
namespace GsaEditor.ViewModels;
public partial class MainWindowViewModel : ViewModelBase
{
private static readonly HashSet<string> TextExtensions = new(StringComparer.OrdinalIgnoreCase)
{
".txt", ".nml", ".xml", ".json", ".csv", ".ini", ".cfg", ".log", ".nut"
};
private static readonly HashSet<string> ImageExtensions = new(StringComparer.OrdinalIgnoreCase)
{
".png", ".jpg", ".jpeg", ".bmp"
};
private static readonly HashSet<string> UnsupportedImageExtensions = new(StringComparer.OrdinalIgnoreCase)
{
".tga", ".dds"
};
private static readonly FilePickerFileType GsaFileType = new("GSA Archives")
{
Patterns = new[] { "*.gsa" }
};
private static readonly FilePickerFileType AllFilesType = new("All Files")
{
Patterns = new[] { "*" }
};
private Window? _window;
private GsaArchive? _archive;
[ObservableProperty]
[NotifyPropertyChangedFor(nameof(Title))]
[NotifyPropertyChangedFor(nameof(StatusArchivePath))]
[NotifyPropertyChangedFor(nameof(HasArchive))]
private string? _archivePath;
[ObservableProperty]
[NotifyPropertyChangedFor(nameof(Title))]
private bool _isDirty;
[ObservableProperty]
private bool _isLoading;
[ObservableProperty]
[NotifyPropertyChangedFor(nameof(HasSelectedFile))]
[NotifyPropertyChangedFor(nameof(HasSelectedFolder))]
[NotifyPropertyChangedFor(nameof(HasSelectedEntry))]
private EntryTreeNodeViewModel? _selectedNode;
[ObservableProperty]
private EntryViewModel? _selectedEntry;
[ObservableProperty]
[NotifyPropertyChangedFor(nameof(IsTextPreview))]
[NotifyPropertyChangedFor(nameof(IsImagePreview))]
[NotifyPropertyChangedFor(nameof(IsHexPreview))]
[NotifyPropertyChangedFor(nameof(HasPreview))]
private PreviewMode _previewMode = PreviewMode.None;
[ObservableProperty]
private string? _previewText;
[ObservableProperty]
private Bitmap? _previewImage;
[ObservableProperty]
private string? _hexDumpText;
[ObservableProperty]
private bool _isEditingText;
public ObservableCollection<EntryTreeNodeViewModel> TreeRoots { get; } = new();
// --- Derived properties ---
public string Title => ArchivePath != null
? $"GsaEditor - {Path.GetFileName(ArchivePath)}{(IsDirty ? " *" : "")}"
: "GsaEditor";
public bool HasArchive => _archive != null;
public bool HasSelectedEntry => SelectedNode?.Entry != null;
public bool HasSelectedFile => SelectedNode != null && !SelectedNode.IsFolder;
public bool HasSelectedFolder => SelectedNode != null && SelectedNode.IsFolder;
public bool IsTextPreview => PreviewMode == PreviewMode.Text;
public bool IsImagePreview => PreviewMode == PreviewMode.Image;
public bool IsHexPreview => PreviewMode == PreviewMode.HexDump;
public bool HasPreview => PreviewMode != PreviewMode.None;
// --- Status bar ---
public string StatusArchivePath => ArchivePath ?? "No archive loaded";
public string StatusEntryCount => _archive != null
? $"{_archive.Entries.Count} entries"
: "";
public string StatusSizeInfo
{
get
{
if (_archive == null) return "";
long totalOriginal = _archive.Entries.Sum(e => (long)e.OriginalLength);
long totalCompressed = _archive.Entries.Sum(e => (long)e.CompressedLength);
return $"Total: {FormatSize(totalOriginal)} / Compressed: {FormatSize(totalCompressed)}";
}
}
/// <summary>
/// Sets the parent window reference for dialog display.
/// </summary>
public void SetWindow(Window window)
{
_window = window;
}
// =========================================================================
// Selection changed
// =========================================================================
partial void OnSelectedNodeChanged(EntryTreeNodeViewModel? value)
{
IsEditingText = false;
if (value?.Entry != null)
{
var entryVm = new EntryViewModel(value.Entry);
entryVm.OnModified = () =>
{
IsDirty = true;
NotifyStatusChanged();
};
SelectedEntry = entryVm;
UpdatePreview(value.Entry);
}
else
{
SelectedEntry = null;
ClearPreview();
}
}
private void UpdatePreview(GsaEntry entry)
{
var ext = Path.GetExtension(entry.Alias).ToLowerInvariant();
if (TextExtensions.Contains(ext))
{
PreviewMode = PreviewMode.Text;
var data = entry.GetDecompressedData();
PreviewText = Encoding.UTF8.GetString(data);
PreviewImage?.Dispose();
PreviewImage = null;
HexDumpText = null;
}
else if (ImageExtensions.Contains(ext))
{
try
{
var data = entry.GetDecompressedData();
using var ms = new MemoryStream(data);
PreviewImage?.Dispose();
PreviewImage = new Bitmap(ms);
PreviewMode = PreviewMode.Image;
}
catch
{
PreviewImage = null;
PreviewMode = PreviewMode.HexDump;
HexDumpText = "Preview not available for this image.";
}
PreviewText = null;
}
else if (UnsupportedImageExtensions.Contains(ext))
{
PreviewMode = PreviewMode.HexDump;
HexDumpText = $"Preview not available for {ext.ToUpperInvariant()} files.";
PreviewText = null;
PreviewImage?.Dispose();
PreviewImage = null;
}
else
{
PreviewMode = PreviewMode.HexDump;
var data = entry.GetDecompressedData();
HexDumpText = HexDumper.Dump(data, 512);
PreviewText = null;
PreviewImage?.Dispose();
PreviewImage = null;
}
}
private void ClearPreview()
{
PreviewMode = PreviewMode.None;
PreviewText = null;
PreviewImage?.Dispose();
PreviewImage = null;
HexDumpText = null;
}
// =========================================================================
// File menu commands
// =========================================================================
[RelayCommand]
private async Task OpenArchive()
{
if (_window == null) return;
if (IsDirty)
{
var confirmed = await Dialogs.ConfirmAsync(_window, "Unsaved Changes",
"You have unsaved changes. Open a new archive without saving?");
if (!confirmed) return;
}
var files = await _window.StorageProvider.OpenFilePickerAsync(new FilePickerOpenOptions
{
Title = "Open GSA Archive",
FileTypeFilter = new[] { GsaFileType, AllFilesType },
AllowMultiple = false
});
if (files.Count == 0) return;
var path = files[0].Path.LocalPath;
await LoadArchiveAsync(path);
}
private async Task LoadArchiveAsync(string path)
{
IsLoading = true;
ClearPreview();
SelectedEntry = null;
SelectedNode = null;
try
{
var archive = await Task.Run(() => GsaReader.Read(path));
_archive = archive;
ArchivePath = path;
IsDirty = false;
BuildTree();
NotifyStatusChanged();
}
catch (Exception ex)
{
if (_window != null)
await Dialogs.ShowMessageAsync(_window, "Error", $"Failed to open archive:\n{ex.Message}");
}
finally
{
IsLoading = false;
}
}
[RelayCommand]
private async Task Save()
{
if (_archive == null || _window == null) return;
if (string.IsNullOrEmpty(ArchivePath))
{
await SaveAs();
return;
}
try
{
await Task.Run(() => GsaWriter.Write(_archive, ArchivePath!));
// Generate .idx alongside
var idxPath = Path.ChangeExtension(ArchivePath, ".idx");
await Task.Run(() => GsaIndexWriter.Write(_archive, idxPath));
IsDirty = false;
}
catch (Exception ex)
{
await Dialogs.ShowMessageAsync(_window, "Error", $"Failed to save archive:\n{ex.Message}");
}
}
[RelayCommand]
private async Task SaveAs()
{
if (_archive == null || _window == null) return;
var file = await _window.StorageProvider.SaveFilePickerAsync(new FilePickerSaveOptions
{
Title = "Save GSA Archive As",
FileTypeChoices = new[] { GsaFileType },
DefaultExtension = "gsa",
SuggestedFileName = ArchivePath != null ? Path.GetFileName(ArchivePath) : "archive.gsa"
});
if (file == null) return;
var path = file.Path.LocalPath;
try
{
await Task.Run(() => GsaWriter.Write(_archive, path));
var idxPath = Path.ChangeExtension(path, ".idx");
await Task.Run(() => GsaIndexWriter.Write(_archive, idxPath));
ArchivePath = path;
IsDirty = false;
}
catch (Exception ex)
{
await Dialogs.ShowMessageAsync(_window, "Error", $"Failed to save archive:\n{ex.Message}");
}
}
[RelayCommand]
private async Task CloseArchive()
{
if (_window == null) return;
if (IsDirty)
{
var confirmed = await Dialogs.ConfirmAsync(_window, "Unsaved Changes",
"You have unsaved changes. Close without saving?");
if (!confirmed) return;
}
_archive = null;
ArchivePath = null;
IsDirty = false;
TreeRoots.Clear();
SelectedEntry = null;
SelectedNode = null;
ClearPreview();
NotifyStatusChanged();
}
[RelayCommand]
private void Exit()
{
_window?.Close();
}
// =========================================================================
// Edit menu commands
// =========================================================================
[RelayCommand]
private async Task AddFiles()
{
if (_archive == null || _window == null) return;
var files = await _window.StorageProvider.OpenFilePickerAsync(new FilePickerOpenOptions
{
Title = "Add Files to Archive",
AllowMultiple = true,
FileTypeFilter = new[] { AllFilesType }
});
if (files.Count == 0) return;
string prefix = "";
if (SelectedNode != null && SelectedNode.IsFolder)
{
prefix = SelectedNode.FullPath;
if (!prefix.EndsWith("/")) prefix += "/";
}
foreach (var file in files)
{
var filePath = file.Path.LocalPath;
var fileName = Path.GetFileName(filePath);
var alias = prefix + fileName;
var data = await Task.Run(() => File.ReadAllBytes(filePath));
var entry = new GsaEntry();
entry.Alias = alias;
entry.SetData(data, compress: true);
_archive.Entries.Add(entry);
}
IsDirty = true;
BuildTree();
NotifyStatusChanged();
}
[RelayCommand]
private async Task ExtractAll()
{
if (_archive == null || _window == null) return;
var folders = await _window.StorageProvider.OpenFolderPickerAsync(new FolderPickerOpenOptions
{
Title = "Select Extraction Folder"
});
if (folders.Count == 0) return;
var basePath = folders[0].Path.LocalPath;
try
{
await Task.Run(() =>
{
foreach (var entry in _archive.Entries)
{
var outputPath = Path.Combine(basePath, entry.Alias.Replace('/', Path.DirectorySeparatorChar));
var dir = Path.GetDirectoryName(outputPath);
if (dir != null) Directory.CreateDirectory(dir);
var data = entry.GetDecompressedData();
File.WriteAllBytes(outputPath, data);
}
});
await Dialogs.ShowMessageAsync(_window, "Extract All",
$"Successfully extracted {_archive.Entries.Count} files to:\n{basePath}");
}
catch (Exception ex)
{
await Dialogs.ShowMessageAsync(_window, "Error", $"Extraction failed:\n{ex.Message}");
}
}
[RelayCommand]
private async Task Repack()
{
if (_archive == null || _window == null) return;
IsLoading = true;
try
{
await Task.Run(() =>
{
foreach (var entry in _archive.Entries)
{
var decompressed = entry.GetDecompressedData();
entry.SetData(decompressed, entry.IsCompressed);
}
});
IsDirty = true;
BuildTree();
NotifyStatusChanged();
if (SelectedNode?.Entry != null)
{
var entryVm = new EntryViewModel(SelectedNode.Entry);
entryVm.OnModified = () => { IsDirty = true; NotifyStatusChanged(); };
SelectedEntry = entryVm;
}
}
finally
{
IsLoading = false;
}
}
// =========================================================================
// Context menu commands
// =========================================================================
[RelayCommand]
private async Task ExtractEntry()
{
if (SelectedNode?.Entry == null || _window == null) return;
var entry = SelectedNode.Entry;
var file = await _window.StorageProvider.SaveFilePickerAsync(new FilePickerSaveOptions
{
Title = "Extract File",
SuggestedFileName = Path.GetFileName(entry.Alias),
FileTypeChoices = new[] { AllFilesType }
});
if (file == null) return;
try
{
var data = entry.GetDecompressedData();
var path = file.Path.LocalPath;
await File.WriteAllBytesAsync(path, data);
}
catch (Exception ex)
{
await Dialogs.ShowMessageAsync(_window, "Error", $"Extraction failed:\n{ex.Message}");
}
}
[RelayCommand]
private async Task ReplaceEntry()
{
if (SelectedNode?.Entry == null || _archive == null || _window == null) return;
var files = await _window.StorageProvider.OpenFilePickerAsync(new FilePickerOpenOptions
{
Title = "Replace Entry With",
AllowMultiple = false,
FileTypeFilter = new[] { AllFilesType }
});
if (files.Count == 0) return;
var filePath = files[0].Path.LocalPath;
var data = await Task.Run(() => File.ReadAllBytes(filePath));
var entry = SelectedNode.Entry;
entry.SetData(data, entry.IsCompressed);
IsDirty = true;
NotifyStatusChanged();
// Refresh the selected entry view
var entryVm = new EntryViewModel(entry);
entryVm.OnModified = () => { IsDirty = true; NotifyStatusChanged(); };
SelectedEntry = entryVm;
UpdatePreview(entry);
}
[RelayCommand]
private void DeleteEntry()
{
if (SelectedNode?.Entry == null || _archive == null) return;
_archive.Entries.Remove(SelectedNode.Entry);
IsDirty = true;
SelectedNode = null;
SelectedEntry = null;
ClearPreview();
BuildTree();
NotifyStatusChanged();
}
[RelayCommand]
private async Task RenameEntry()
{
if (SelectedNode?.Entry == null || _window == null) return;
var entry = SelectedNode.Entry;
var result = await Dialogs.InputAsync(_window, "Rename Entry", "New alias:", entry.Alias);
if (result != null && result != entry.Alias && result.Length > 0)
{
entry.Alias = result;
IsDirty = true;
BuildTree();
NotifyStatusChanged();
if (SelectedEntry != null)
{
var entryVm = new EntryViewModel(entry);
entryVm.OnModified = () => { IsDirty = true; NotifyStatusChanged(); };
SelectedEntry = entryVm;
}
}
}
[RelayCommand]
private async Task ExtractFolder()
{
if (SelectedNode == null || !SelectedNode.IsFolder || _archive == null || _window == null) return;
var folders = await _window.StorageProvider.OpenFolderPickerAsync(new FolderPickerOpenOptions
{
Title = "Select Extraction Folder"
});
if (folders.Count == 0) return;
var basePath = folders[0].Path.LocalPath;
var prefix = SelectedNode.FullPath;
if (!prefix.EndsWith("/")) prefix += "/";
try
{
var matching = _archive.Entries.Where(e => e.Alias.StartsWith(prefix, StringComparison.Ordinal)).ToList();
await Task.Run(() =>
{
foreach (var entry in matching)
{
var relativePath = entry.Alias.Substring(prefix.Length);
var outputPath = Path.Combine(basePath, relativePath.Replace('/', Path.DirectorySeparatorChar));
var dir = Path.GetDirectoryName(outputPath);
if (dir != null) Directory.CreateDirectory(dir);
var data = entry.GetDecompressedData();
File.WriteAllBytes(outputPath, data);
}
});
await Dialogs.ShowMessageAsync(_window, "Extract Folder",
$"Successfully extracted files to:\n{basePath}");
}
catch (Exception ex)
{
await Dialogs.ShowMessageAsync(_window, "Error", $"Extraction failed:\n{ex.Message}");
}
}
// =========================================================================
// Text editing commands
// =========================================================================
[RelayCommand]
private void StartEdit()
{
IsEditingText = true;
}
[RelayCommand]
private void SaveEdit()
{
if (SelectedNode?.Entry == null || PreviewText == null) return;
var entry = SelectedNode.Entry;
var newData = Encoding.UTF8.GetBytes(PreviewText);
entry.SetData(newData, entry.IsCompressed);
IsDirty = true;
IsEditingText = false;
var entryVm = new EntryViewModel(entry);
entryVm.OnModified = () => { IsDirty = true; NotifyStatusChanged(); };
SelectedEntry = entryVm;
NotifyStatusChanged();
}
[RelayCommand]
private void CancelEdit()
{
IsEditingText = false;
if (SelectedNode?.Entry != null)
{
PreviewText = Encoding.UTF8.GetString(SelectedNode.Entry.GetDecompressedData());
}
}
// =========================================================================
// About
// =========================================================================
[RelayCommand]
private async Task ShowAbout()
{
if (_window == null) return;
await Dialogs.ShowMessageAsync(_window, "About GsaEditor",
"GsaEditor v1.0\n\nGSA Archive Viewer & Editor\nBuilt with Avalonia UI");
}
// =========================================================================
// Tree building
// =========================================================================
private void BuildTree()
{
TreeRoots.Clear();
if (_archive == null) return;
foreach (var entry in _archive.Entries)
{
var parts = entry.Alias.Split('/');
var currentLevel = TreeRoots;
var currentPath = "";
for (int i = 0; i < parts.Length; i++)
{
var part = parts[i];
currentPath = i == 0 ? part : currentPath + "/" + part;
bool isLeaf = (i == parts.Length - 1);
var existing = currentLevel.FirstOrDefault(n => n.Name == part);
if (existing == null)
{
existing = new EntryTreeNodeViewModel
{
Name = part,
FullPath = currentPath,
IsFolder = !isLeaf,
Entry = isLeaf ? entry : null
};
currentLevel.Add(existing);
}
if (!isLeaf)
{
currentLevel = existing.Children;
}
}
}
}
// =========================================================================
// Helpers
// =========================================================================
private void NotifyStatusChanged()
{
OnPropertyChanged(nameof(StatusEntryCount));
OnPropertyChanged(nameof(StatusSizeInfo));
OnPropertyChanged(nameof(HasArchive));
}
private static string FormatSize(long bytes)
{
if (bytes < 1024) return $"{bytes} B";
if (bytes < 1024 * 1024) return $"{bytes / 1024.0:F1} KB";
return $"{bytes / (1024.0 * 1024.0):F1} MB";
}
}

View File

@ -0,0 +1,12 @@
namespace GsaEditor.ViewModels;
/// <summary>
/// Determines which preview panel to show for the selected archive entry.
/// </summary>
public enum PreviewMode
{
None,
Text,
Image,
HexDump
}

View File

@ -0,0 +1,7 @@
using CommunityToolkit.Mvvm.ComponentModel;
namespace GsaEditor.ViewModels;
public class ViewModelBase : ObservableObject
{
}

View File

@ -0,0 +1,240 @@
<Window xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:vm="using:GsaEditor.ViewModels"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
mc:Ignorable="d" d:DesignWidth="1100" d:DesignHeight="700"
x:Class="GsaEditor.Views.MainWindow"
x:CompileBindings="False"
Title="{Binding Title}"
Width="1100" Height="700"
MinWidth="800" MinHeight="500">
<Design.DataContext>
<vm:MainWindowViewModel/>
</Design.DataContext>
<DockPanel>
<!-- ============================================================ -->
<!-- Menu Bar -->
<!-- ============================================================ -->
<Menu DockPanel.Dock="Top">
<MenuItem Header="_File">
<MenuItem Header="_Open Archive..." Command="{Binding OpenArchiveCommand}"
InputGesture="Ctrl+O"/>
<MenuItem Header="_Save" Command="{Binding SaveCommand}"
InputGesture="Ctrl+S"
IsEnabled="{Binding HasArchive}"/>
<MenuItem Header="Save _As..." Command="{Binding SaveAsCommand}"
InputGesture="Ctrl+Shift+S"
IsEnabled="{Binding HasArchive}"/>
<Separator/>
<MenuItem Header="_Close" Command="{Binding CloseArchiveCommand}"
IsEnabled="{Binding HasArchive}"/>
<MenuItem Header="E_xit" Command="{Binding ExitCommand}"
InputGesture="Alt+F4"/>
</MenuItem>
<MenuItem Header="_Edit">
<MenuItem Header="_Add File(s)..." Command="{Binding AddFilesCommand}"
IsEnabled="{Binding HasArchive}"/>
<MenuItem Header="_Extract All..." Command="{Binding ExtractAllCommand}"
IsEnabled="{Binding HasArchive}"/>
<Separator/>
<MenuItem Header="_Repack" Command="{Binding RepackCommand}"
IsEnabled="{Binding HasArchive}"/>
</MenuItem>
<MenuItem Header="_About">
<MenuItem Header="About GsaEditor" Command="{Binding ShowAboutCommand}"/>
</MenuItem>
</Menu>
<!-- ============================================================ -->
<!-- Status Bar -->
<!-- ============================================================ -->
<Border DockPanel.Dock="Bottom" Padding="8,4"
BorderThickness="0,1,0,0" BorderBrush="#CCCCCC"
Background="#F5F5F5">
<Grid ColumnDefinitions="*,Auto,Auto">
<TextBlock Grid.Column="0" Text="{Binding StatusArchivePath}"
VerticalAlignment="Center" FontSize="12"
Foreground="#555555"/>
<TextBlock Grid.Column="1" Text="{Binding StatusEntryCount}"
VerticalAlignment="Center" FontSize="12"
Foreground="#555555" Margin="20,0,0,0"/>
<TextBlock Grid.Column="2" Text="{Binding StatusSizeInfo}"
VerticalAlignment="Center" FontSize="12"
Foreground="#555555" Margin="20,0,0,0"/>
</Grid>
</Border>
<!-- ============================================================ -->
<!-- Loading Overlay -->
<!-- ============================================================ -->
<Border IsVisible="{Binding IsLoading}" Background="#80000000"
ZIndex="100" HorizontalAlignment="Stretch" VerticalAlignment="Stretch">
<TextBlock Text="Loading..." FontSize="20"
Foreground="White"
HorizontalAlignment="Center"
VerticalAlignment="Center"/>
</Border>
<!-- ============================================================ -->
<!-- Main Content: Left Tree + Right Details -->
<!-- ============================================================ -->
<Grid ColumnDefinitions="300,5,*">
<!-- ======================================================== -->
<!-- Left Panel: Archive Tree -->
<!-- ======================================================== -->
<Border Grid.Column="0" BorderThickness="0,0,1,0" BorderBrush="#DDDDDD">
<DockPanel>
<TextBlock DockPanel.Dock="Top" Text="Archive Contents"
FontWeight="SemiBold" Padding="8,6" FontSize="13"
Background="#EEEEEE"/>
<TreeView Name="ArchiveTree"
ItemsSource="{Binding TreeRoots}"
SelectionChanged="TreeView_SelectionChanged">
<TreeView.ContextMenu>
<ContextMenu>
<!-- File node operations -->
<MenuItem Header="Extract" Command="{Binding ExtractEntryCommand}"
IsVisible="{Binding HasSelectedFile}"/>
<MenuItem Header="Replace" Command="{Binding ReplaceEntryCommand}"
IsVisible="{Binding HasSelectedFile}"/>
<MenuItem Header="Delete" Command="{Binding DeleteEntryCommand}"
IsVisible="{Binding HasSelectedFile}"/>
<MenuItem Header="Rename" Command="{Binding RenameEntryCommand}"
IsVisible="{Binding HasSelectedFile}"/>
<!-- Folder node operations -->
<MenuItem Header="Extract Folder" Command="{Binding ExtractFolderCommand}"
IsVisible="{Binding HasSelectedFolder}"/>
<!-- Always available -->
<Separator/>
<MenuItem Header="Add File(s)..." Command="{Binding AddFilesCommand}"
IsEnabled="{Binding HasArchive}"/>
</ContextMenu>
</TreeView.ContextMenu>
<TreeView.ItemTemplate>
<TreeDataTemplate ItemsSource="{Binding Children}">
<StackPanel Orientation="Horizontal" Spacing="5">
<TextBlock Text="📁" IsVisible="{Binding IsFolder}"/>
<TextBlock Text="📄" IsVisible="{Binding IsFile}"/>
<TextBlock Text="{Binding Name}"/>
</StackPanel>
</TreeDataTemplate>
</TreeView.ItemTemplate>
</TreeView>
</DockPanel>
</Border>
<!-- Splitter -->
<GridSplitter Grid.Column="1" ResizeDirection="Columns"
Background="#CCCCCC"/>
<!-- ======================================================== -->
<!-- Right Panel: Details & Preview -->
<!-- ======================================================== -->
<Grid Grid.Column="2" RowDefinitions="Auto,*">
<!-- ================================================== -->
<!-- Top Bar: Entry Info -->
<!-- ================================================== -->
<Border Grid.Row="0" Padding="10,8"
Background="#FAFAFA"
BorderThickness="0,0,0,1" BorderBrush="#DDDDDD"
IsVisible="{Binding HasSelectedEntry}">
<Grid RowDefinitions="Auto,Auto">
<!-- Row 0: Alias -->
<Grid Grid.Row="0" ColumnDefinitions="Auto,*" Margin="0,0,0,6">
<TextBlock Grid.Column="0" Text="Alias:"
VerticalAlignment="Center" Margin="0,0,8,0"
FontWeight="SemiBold"/>
<TextBox Grid.Column="1"
Text="{Binding SelectedEntry.Alias, Mode=TwoWay}"
Watermark="File path in archive"/>
</Grid>
<!-- Row 1: Size info + compression toggle -->
<StackPanel Grid.Row="1" Orientation="Horizontal" Spacing="20">
<StackPanel Orientation="Horizontal" Spacing="4">
<TextBlock Text="Size:" FontWeight="SemiBold" VerticalAlignment="Center"/>
<TextBlock Text="{Binding SelectedEntry.OriginalSizeText}" VerticalAlignment="Center"/>
</StackPanel>
<StackPanel Orientation="Horizontal" Spacing="4">
<TextBlock Text="Compressed:" FontWeight="SemiBold" VerticalAlignment="Center"/>
<TextBlock Text="{Binding SelectedEntry.CompressedSizeText}" VerticalAlignment="Center"/>
</StackPanel>
<StackPanel Orientation="Horizontal" Spacing="4">
<TextBlock Text="Ratio:" FontWeight="SemiBold" VerticalAlignment="Center"/>
<TextBlock Text="{Binding SelectedEntry.CompressionRatio}" VerticalAlignment="Center"/>
</StackPanel>
<CheckBox Content="Compressed"
IsChecked="{Binding SelectedEntry.IsCompressed, Mode=TwoWay}"
VerticalAlignment="Center"/>
</StackPanel>
</Grid>
</Border>
<!-- ================================================== -->
<!-- Content Area: Preview -->
<!-- ================================================== -->
<Panel Grid.Row="1">
<!-- No selection / No archive message -->
<TextBlock Text="Open a GSA archive and select a file to preview."
HorizontalAlignment="Center" VerticalAlignment="Center"
Foreground="#999999" FontSize="14"
IsVisible="{Binding !HasPreview}"/>
<!-- Text Preview -->
<DockPanel IsVisible="{Binding IsTextPreview}">
<StackPanel DockPanel.Dock="Top" Orientation="Horizontal"
Spacing="5" Margin="10,6">
<Button Content="Edit" Command="{Binding StartEditCommand}"
IsVisible="{Binding !IsEditingText}"/>
<Button Content="Save" Command="{Binding SaveEditCommand}"
IsVisible="{Binding IsEditingText}"/>
<Button Content="Cancel" Command="{Binding CancelEditCommand}"
IsVisible="{Binding IsEditingText}"/>
</StackPanel>
<TextBox Text="{Binding PreviewText, Mode=TwoWay}"
IsReadOnly="{Binding !IsEditingText}"
FontFamily="Consolas, Courier New, monospace"
FontSize="13"
AcceptsReturn="True"
TextWrapping="NoWrap"
Margin="10,0,10,10"/>
</DockPanel>
<!-- Image Preview -->
<ScrollViewer IsVisible="{Binding IsImagePreview}"
HorizontalScrollBarVisibility="Auto"
VerticalScrollBarVisibility="Auto">
<Image Source="{Binding PreviewImage}"
Stretch="Uniform"
MaxWidth="800" MaxHeight="600"
Margin="10"
HorizontalAlignment="Center"
VerticalAlignment="Center"/>
</ScrollViewer>
<!-- Hex Dump Preview -->
<TextBox IsVisible="{Binding IsHexPreview}"
Text="{Binding HexDumpText}"
IsReadOnly="True"
FontFamily="Consolas, Courier New, monospace"
FontSize="13"
AcceptsReturn="True"
TextWrapping="NoWrap"
Margin="10"/>
</Panel>
</Grid>
</Grid>
</DockPanel>
</Window>

View File

@ -0,0 +1,52 @@
using Avalonia.Controls;
using Avalonia.Interactivity;
using GsaEditor.ViewModels;
namespace GsaEditor.Views;
public partial class MainWindow : Window
{
private bool _forceClose;
public MainWindow()
{
InitializeComponent();
}
protected override void OnLoaded(RoutedEventArgs e)
{
base.OnLoaded(e);
if (DataContext is MainWindowViewModel vm)
{
vm.SetWindow(this);
}
}
private void TreeView_SelectionChanged(object? sender, SelectionChangedEventArgs e)
{
if (DataContext is MainWindowViewModel vm && sender is TreeView tree)
{
vm.SelectedNode = tree.SelectedItem as EntryTreeNodeViewModel;
}
}
protected override async void OnClosing(WindowClosingEventArgs e)
{
base.OnClosing(e);
if (_forceClose) return;
if (DataContext is MainWindowViewModel vm && vm.IsDirty)
{
e.Cancel = true;
var confirmed = await Helpers.Dialogs.ConfirmAsync(this, "Unsaved Changes",
"You have unsaved changes. Close without saving?");
if (confirmed)
{
_forceClose = true;
Close();
}
}
}
}

11
GsaEditor/app.manifest Normal file
View File

@ -0,0 +1,11 @@
<?xml version="1.0" encoding="utf-8"?>
<assembly manifestVersion="1.0" xmlns="urn:schemas-microsoft-com:asm.v1">
<assemblyIdentity version="1.0.0.0" name="GsaEditor.Desktop"/>
<compatibility xmlns="urn:schemas-microsoft-com:compatibility.v1">
<application>
<!-- Windows 10 and Windows 11 -->
<supportedOS Id="{8e0f7a12-bfb3-4fe8-b9a5-48fd50a15a9a}" />
</application>
</compatibility>
</assembly>

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@ -0,0 +1,276 @@
<?xml version="1.0"?>
<doc>
<assembly>
<name>GsaEditor.Core</name>
</assembly>
<members>
<member name="T:GsaEditor.Core.Compression.ZlibHelper">
<summary>
Provides zlib compression and decompression utilities for GSA archive entries.
The GSA format stores zlib-wrapped data: 2-byte header (CMF+FLG) + deflate stream + 4-byte Adler-32 checksum.
This helper skips the 2-byte zlib header and uses <see cref="T:System.IO.Compression.DeflateStream"/> for the core deflate data.
</summary>
</member>
<member name="M:GsaEditor.Core.Compression.ZlibHelper.Decompress(System.Byte[],System.UInt32)">
<summary>
Decompresses zlib-wrapped data into a byte array of the specified original size.
Skips the 2-byte zlib header before feeding data to <see cref="T:System.IO.Compression.DeflateStream"/>.
</summary>
<param name="compressedData">The zlib-wrapped compressed data (header + deflate + checksum).</param>
<param name="originalLength">The expected decompressed size in bytes.</param>
<returns>The decompressed byte array.</returns>
<exception cref="T:System.IO.InvalidDataException">Thrown when decompression fails or data is corrupt.</exception>
</member>
<member name="M:GsaEditor.Core.Compression.ZlibHelper.Compress(System.Byte[])">
<summary>
Compresses data using zlib format: 2-byte header + deflate stream + 4-byte Adler-32 checksum.
</summary>
<param name="data">The uncompressed data to compress.</param>
<returns>The zlib-wrapped compressed byte array.</returns>
</member>
<member name="M:GsaEditor.Core.Compression.ZlibHelper.ComputeAdler32(System.Byte[])">
<summary>
Computes the Adler-32 checksum of the given data.
</summary>
<param name="data">The data to checksum.</param>
<returns>The 32-bit Adler-32 checksum value.</returns>
</member>
<member name="T:GsaEditor.Core.IO.GsaIndexEntry">
<summary>
Represents a single entry parsed from a .idx NML index file.
</summary>
</member>
<member name="P:GsaEditor.Core.IO.GsaIndexEntry.Id">
<summary>
The file alias (relative path) of the entry.
</summary>
</member>
<member name="P:GsaEditor.Core.IO.GsaIndexEntry.Method">
<summary>
Compression method: 0 = raw, 1 = zlib.
</summary>
</member>
<member name="P:GsaEditor.Core.IO.GsaIndexEntry.Length">
<summary>
Original (decompressed) size in bytes.
</summary>
</member>
<member name="P:GsaEditor.Core.IO.GsaIndexEntry.CompressedLength">
<summary>
Compressed size in bytes.
</summary>
</member>
<member name="P:GsaEditor.Core.IO.GsaIndexEntry.Offset">
<summary>
Byte offset of the data block within the archive.
</summary>
</member>
<member name="T:GsaEditor.Core.IO.GsaIndexReader">
<summary>
Parses .idx NML index files (XML-compatible format) into a list of <see cref="T:GsaEditor.Core.IO.GsaIndexEntry"/>.
</summary>
</member>
<member name="M:GsaEditor.Core.IO.GsaIndexReader.Read(System.String)">
<summary>
Reads index entries from the specified .idx file.
</summary>
<param name="filePath">The path to the .idx file.</param>
<returns>A list of parsed index entries.</returns>
</member>
<member name="T:GsaEditor.Core.IO.GsaIndexWriter">
<summary>
Writes a .idx NML index file from a <see cref="T:GsaEditor.Core.Models.GsaArchive"/>.
The index file is XML-compatible and lists each entry with its alias, compression method,
sizes, and data offset for fast lookup without a full sequential scan.
</summary>
</member>
<member name="M:GsaEditor.Core.IO.GsaIndexWriter.Write(GsaEditor.Core.Models.GsaArchive,System.String)">
<summary>
Writes the index file for the given archive to the specified path.
</summary>
<param name="archive">The archive whose entries will be indexed.</param>
<param name="filePath">The output .idx file path.</param>
</member>
<member name="T:GsaEditor.Core.IO.GsaReader">
<summary>
Reads a .gsa binary archive from a stream or file and produces a <see cref="T:GsaEditor.Core.Models.GsaArchive"/>.
Supports both NARC (legacy) and NARD (enhanced with padding) format variants.
</summary>
</member>
<member name="F:GsaEditor.Core.IO.GsaReader.MAGIC_NARC">
<summary>Magic word for the NARC (legacy) format.</summary>
</member>
<member name="F:GsaEditor.Core.IO.GsaReader.MAGIC_NARD">
<summary>Magic word for the NARD (enhanced) format.</summary>
</member>
<member name="F:GsaEditor.Core.IO.GsaReader.MAX_ALIAS_LENGTH">
<summary>Maximum allowed alias length in characters.</summary>
</member>
<member name="M:GsaEditor.Core.IO.GsaReader.Read(System.String)">
<summary>
Reads a GSA archive from the specified file path.
</summary>
<param name="filePath">The path to the .gsa archive file.</param>
<returns>A fully populated <see cref="T:GsaEditor.Core.Models.GsaArchive"/>.</returns>
<exception cref="T:System.IO.InvalidDataException">Thrown if the file has an invalid magic word.</exception>
</member>
<member name="M:GsaEditor.Core.IO.GsaReader.Read(System.IO.Stream)">
<summary>
Reads a GSA archive from a stream by performing a sequential scan of all entries.
</summary>
<param name="stream">A readable, seekable stream positioned at the start of the archive.</param>
<returns>A fully populated <see cref="T:GsaEditor.Core.Models.GsaArchive"/>.</returns>
<exception cref="T:System.IO.InvalidDataException">Thrown if the stream has an invalid magic word.</exception>
</member>
<member name="M:GsaEditor.Core.IO.GsaReader.ReadEntry(System.IO.BinaryReader,GsaEditor.Core.Models.GsaArchive)">
<summary>
Reads a single file entry from the current stream position.
</summary>
<param name="reader">The binary reader positioned at the start of an entry.</param>
<param name="archive">The parent archive (used for NARD padding alignment).</param>
<returns>A populated <see cref="T:GsaEditor.Core.Models.GsaEntry"/>, or null if no more entries can be read.</returns>
</member>
<member name="M:GsaEditor.Core.IO.GsaReader.AlignStream(System.IO.Stream,System.Int32)">
<summary>
Advances the stream position to the next aligned boundary.
</summary>
<param name="stream">The stream to align.</param>
<param name="alignment">The alignment boundary in bytes.</param>
</member>
<member name="T:GsaEditor.Core.IO.GsaWriter">
<summary>
Writes a <see cref="T:GsaEditor.Core.Models.GsaArchive"/> to a binary .gsa stream or file.
Supports both NARC (legacy) and NARD (enhanced with padding) format variants.
</summary>
</member>
<member name="F:GsaEditor.Core.IO.GsaWriter.MAGIC_NARC">
<summary>Magic word for the NARC (legacy) format.</summary>
</member>
<member name="F:GsaEditor.Core.IO.GsaWriter.MAGIC_NARD">
<summary>Magic word for the NARD (enhanced) format.</summary>
</member>
<member name="M:GsaEditor.Core.IO.GsaWriter.Write(GsaEditor.Core.Models.GsaArchive,System.String)">
<summary>
Writes the archive to the specified file path.
</summary>
<param name="archive">The archive to write.</param>
<param name="filePath">The output file path.</param>
</member>
<member name="M:GsaEditor.Core.IO.GsaWriter.Write(GsaEditor.Core.Models.GsaArchive,System.IO.Stream)">
<summary>
Writes the archive to the given stream.
Each entry's <see cref="P:GsaEditor.Core.Models.GsaEntry.DataOffset"/> is updated to reflect the new position in the output.
</summary>
<param name="archive">The archive to write.</param>
<param name="stream">A writable stream.</param>
</member>
<member name="M:GsaEditor.Core.IO.GsaWriter.WriteEntry(System.IO.BinaryWriter,GsaEditor.Core.Models.GsaEntry,GsaEditor.Core.Models.GsaArchive)">
<summary>
Writes a single file entry at the current stream position.
</summary>
<param name="writer">The binary writer.</param>
<param name="entry">The entry to write.</param>
<param name="archive">The parent archive (used for NARD padding alignment).</param>
</member>
<member name="M:GsaEditor.Core.IO.GsaWriter.WritePadding(System.IO.BinaryWriter,System.Int32)">
<summary>
Writes zero-bytes to pad the stream to the next aligned boundary.
</summary>
<param name="writer">The binary writer.</param>
<param name="alignment">The alignment boundary in bytes.</param>
</member>
<member name="T:GsaEditor.Core.Models.GsaArchive">
<summary>
Represents a loaded GSA archive with its global header information and file entries.
</summary>
</member>
<member name="P:GsaEditor.Core.Models.GsaArchive.Format">
<summary>
The format variant of this archive (NARC or NARD).
</summary>
</member>
<member name="P:GsaEditor.Core.Models.GsaArchive.OffsetPadding">
<summary>
(NARD only) Byte alignment for data offsets. Zero or unused for NARC.
</summary>
</member>
<member name="P:GsaEditor.Core.Models.GsaArchive.SizePadding">
<summary>
(NARD only) Byte alignment for data sizes. Zero or unused for NARC.
</summary>
</member>
<member name="P:GsaEditor.Core.Models.GsaArchive.Entries">
<summary>
The ordered list of file entries in the archive.
</summary>
</member>
<member name="T:GsaEditor.Core.Models.GsaEntry">
<summary>
Represents a single file entry within a GSA archive.
</summary>
</member>
<member name="P:GsaEditor.Core.Models.GsaEntry.Alias">
<summary>
Relative path (alias) of the file within the archive, using '/' as separator.
Maximum 511 characters.
</summary>
</member>
<member name="P:GsaEditor.Core.Models.GsaEntry.IsCompressed">
<summary>
Whether this entry's data is zlib-compressed.
</summary>
</member>
<member name="P:GsaEditor.Core.Models.GsaEntry.OriginalLength">
<summary>
Original (decompressed) size of the file in bytes.
</summary>
</member>
<member name="P:GsaEditor.Core.Models.GsaEntry.CompressedLength">
<summary>
Compressed size of the file in bytes. Equal to <see cref="P:GsaEditor.Core.Models.GsaEntry.OriginalLength"/> if not compressed.
</summary>
</member>
<member name="P:GsaEditor.Core.Models.GsaEntry.RawData">
<summary>
Raw data as stored in the archive (compressed bytes if <see cref="P:GsaEditor.Core.Models.GsaEntry.IsCompressed"/> is true).
</summary>
</member>
<member name="P:GsaEditor.Core.Models.GsaEntry.DataOffset">
<summary>
Byte offset of this entry's data block within the archive file.
</summary>
</member>
<member name="M:GsaEditor.Core.Models.GsaEntry.GetDecompressedData">
<summary>
Returns the decompressed data for this entry.
If the entry is not compressed, returns <see cref="P:GsaEditor.Core.Models.GsaEntry.RawData"/> directly.
</summary>
<returns>The decompressed file content.</returns>
</member>
<member name="M:GsaEditor.Core.Models.GsaEntry.SetData(System.Byte[],System.Boolean)">
<summary>
Sets the entry data from uncompressed bytes, optionally compressing with zlib.
Updates <see cref="P:GsaEditor.Core.Models.GsaEntry.OriginalLength"/>, <see cref="P:GsaEditor.Core.Models.GsaEntry.CompressedLength"/>,
<see cref="P:GsaEditor.Core.Models.GsaEntry.IsCompressed"/>, and <see cref="P:GsaEditor.Core.Models.GsaEntry.RawData"/>.
</summary>
<param name="uncompressedData">The uncompressed file content.</param>
<param name="compress">Whether to compress the data with zlib.</param>
</member>
<member name="T:GsaEditor.Core.Models.GsaFormat">
<summary>
Identifies the archive format variant.
</summary>
</member>
<member name="F:GsaEditor.Core.Models.GsaFormat.NARC">
<summary>
Legacy format with no padding support. Magic word = 0x4E415243 ("NARC").
</summary>
</member>
<member name="F:GsaEditor.Core.Models.GsaFormat.NARD">
<summary>
Enhanced format with offset and size padding for console DVD alignment.
Magic word = 0x4E415244 ("NARD").
</summary>
</member>
</members>
</doc>

View File

@ -0,0 +1,681 @@
{
"runtimeTarget": {
"name": ".NETCoreApp,Version=v8.0",
"signature": ""
},
"compilationOptions": {},
"targets": {
".NETCoreApp,Version=v8.0": {
"GsaEditor/1.0.0": {
"dependencies": {
"Avalonia": "11.3.6",
"Avalonia.Desktop": "11.3.6",
"Avalonia.Diagnostics": "11.3.6",
"Avalonia.Fonts.Inter": "11.3.6",
"Avalonia.Themes.Fluent": "11.3.6",
"CommunityToolkit.Mvvm": "8.2.1",
"GsaEditor.Core": "1.0.0"
},
"runtime": {
"GsaEditor.dll": {}
}
},
"Avalonia/11.3.6": {
"dependencies": {
"Avalonia.BuildServices": "0.0.31",
"Avalonia.Remote.Protocol": "11.3.6",
"MicroCom.Runtime": "0.11.0"
},
"runtime": {
"lib/net8.0/Avalonia.Base.dll": {
"assemblyVersion": "11.3.6.0",
"fileVersion": "11.3.6.0"
},
"lib/net8.0/Avalonia.Controls.dll": {
"assemblyVersion": "11.3.6.0",
"fileVersion": "11.3.6.0"
},
"lib/net8.0/Avalonia.DesignerSupport.dll": {
"assemblyVersion": "0.7.0.0",
"fileVersion": "0.7.0.0"
},
"lib/net8.0/Avalonia.Dialogs.dll": {
"assemblyVersion": "11.3.6.0",
"fileVersion": "11.3.6.0"
},
"lib/net8.0/Avalonia.Markup.Xaml.dll": {
"assemblyVersion": "11.3.6.0",
"fileVersion": "11.3.6.0"
},
"lib/net8.0/Avalonia.Markup.dll": {
"assemblyVersion": "11.3.6.0",
"fileVersion": "11.3.6.0"
},
"lib/net8.0/Avalonia.Metal.dll": {
"assemblyVersion": "11.3.6.0",
"fileVersion": "11.3.6.0"
},
"lib/net8.0/Avalonia.MicroCom.dll": {
"assemblyVersion": "11.3.6.0",
"fileVersion": "11.3.6.0"
},
"lib/net8.0/Avalonia.OpenGL.dll": {
"assemblyVersion": "11.3.6.0",
"fileVersion": "11.3.6.0"
},
"lib/net8.0/Avalonia.Vulkan.dll": {
"assemblyVersion": "11.3.6.0",
"fileVersion": "11.3.6.0"
},
"lib/net8.0/Avalonia.dll": {
"assemblyVersion": "11.3.6.0",
"fileVersion": "11.3.6.0"
}
}
},
"Avalonia.Angle.Windows.Natives/2.1.25547.20250602": {
"runtimeTargets": {
"runtimes/win-arm64/native/av_libglesv2.dll": {
"rid": "win-arm64",
"assetType": "native",
"fileVersion": "2.1.25606.0"
},
"runtimes/win-x64/native/av_libglesv2.dll": {
"rid": "win-x64",
"assetType": "native",
"fileVersion": "2.1.25606.0"
},
"runtimes/win-x86/native/av_libglesv2.dll": {
"rid": "win-x86",
"assetType": "native",
"fileVersion": "2.1.25606.0"
}
}
},
"Avalonia.BuildServices/0.0.31": {},
"Avalonia.Controls.ColorPicker/11.3.6": {
"dependencies": {
"Avalonia": "11.3.6",
"Avalonia.Remote.Protocol": "11.3.6"
},
"runtime": {
"lib/net8.0/Avalonia.Controls.ColorPicker.dll": {
"assemblyVersion": "11.3.6.0",
"fileVersion": "11.3.6.0"
}
}
},
"Avalonia.Desktop/11.3.6": {
"dependencies": {
"Avalonia": "11.3.6",
"Avalonia.Native": "11.3.6",
"Avalonia.Skia": "11.3.6",
"Avalonia.Win32": "11.3.6",
"Avalonia.X11": "11.3.6"
},
"runtime": {
"lib/net8.0/Avalonia.Desktop.dll": {
"assemblyVersion": "11.3.6.0",
"fileVersion": "11.3.6.0"
}
}
},
"Avalonia.Diagnostics/11.3.6": {
"dependencies": {
"Avalonia": "11.3.6",
"Avalonia.Controls.ColorPicker": "11.3.6",
"Avalonia.Themes.Simple": "11.3.6"
},
"runtime": {
"lib/net8.0/Avalonia.Diagnostics.dll": {
"assemblyVersion": "11.3.6.0",
"fileVersion": "11.3.6.0"
}
}
},
"Avalonia.Fonts.Inter/11.3.6": {
"dependencies": {
"Avalonia": "11.3.6"
},
"runtime": {
"lib/net8.0/Avalonia.Fonts.Inter.dll": {
"assemblyVersion": "11.3.6.0",
"fileVersion": "11.3.6.0"
}
}
},
"Avalonia.FreeDesktop/11.3.6": {
"dependencies": {
"Avalonia": "11.3.6",
"Tmds.DBus.Protocol": "0.21.2"
},
"runtime": {
"lib/net8.0/Avalonia.FreeDesktop.dll": {
"assemblyVersion": "11.3.6.0",
"fileVersion": "11.3.6.0"
}
}
},
"Avalonia.Native/11.3.6": {
"dependencies": {
"Avalonia": "11.3.6"
},
"runtime": {
"lib/net8.0/Avalonia.Native.dll": {
"assemblyVersion": "11.3.6.0",
"fileVersion": "11.3.6.0"
}
},
"runtimeTargets": {
"runtimes/osx/native/libAvaloniaNative.dylib": {
"rid": "osx",
"assetType": "native",
"fileVersion": "0.0.0.0"
}
}
},
"Avalonia.Remote.Protocol/11.3.6": {
"runtime": {
"lib/net8.0/Avalonia.Remote.Protocol.dll": {
"assemblyVersion": "11.3.6.0",
"fileVersion": "11.3.6.0"
}
}
},
"Avalonia.Skia/11.3.6": {
"dependencies": {
"Avalonia": "11.3.6",
"HarfBuzzSharp": "8.3.1.1",
"HarfBuzzSharp.NativeAssets.Linux": "8.3.1.1",
"HarfBuzzSharp.NativeAssets.WebAssembly": "8.3.1.1",
"SkiaSharp": "2.88.9",
"SkiaSharp.NativeAssets.Linux": "2.88.9",
"SkiaSharp.NativeAssets.WebAssembly": "2.88.9"
},
"runtime": {
"lib/net8.0/Avalonia.Skia.dll": {
"assemblyVersion": "11.3.6.0",
"fileVersion": "11.3.6.0"
}
}
},
"Avalonia.Themes.Fluent/11.3.6": {
"dependencies": {
"Avalonia": "11.3.6"
},
"runtime": {
"lib/net8.0/Avalonia.Themes.Fluent.dll": {
"assemblyVersion": "11.3.6.0",
"fileVersion": "11.3.6.0"
}
}
},
"Avalonia.Themes.Simple/11.3.6": {
"dependencies": {
"Avalonia": "11.3.6"
},
"runtime": {
"lib/net8.0/Avalonia.Themes.Simple.dll": {
"assemblyVersion": "11.3.6.0",
"fileVersion": "11.3.6.0"
}
}
},
"Avalonia.Win32/11.3.6": {
"dependencies": {
"Avalonia": "11.3.6",
"Avalonia.Angle.Windows.Natives": "2.1.25547.20250602"
},
"runtime": {
"lib/net8.0/Avalonia.Win32.Automation.dll": {
"assemblyVersion": "11.3.6.0",
"fileVersion": "11.3.6.0"
},
"lib/net8.0/Avalonia.Win32.dll": {
"assemblyVersion": "11.3.6.0",
"fileVersion": "11.3.6.0"
}
}
},
"Avalonia.X11/11.3.6": {
"dependencies": {
"Avalonia": "11.3.6",
"Avalonia.FreeDesktop": "11.3.6",
"Avalonia.Skia": "11.3.6"
},
"runtime": {
"lib/net8.0/Avalonia.X11.dll": {
"assemblyVersion": "11.3.6.0",
"fileVersion": "11.3.6.0"
}
}
},
"CommunityToolkit.Mvvm/8.2.1": {
"runtime": {
"lib/net6.0/CommunityToolkit.Mvvm.dll": {
"assemblyVersion": "8.2.0.0",
"fileVersion": "8.2.1.1"
}
}
},
"HarfBuzzSharp/8.3.1.1": {
"dependencies": {
"HarfBuzzSharp.NativeAssets.Win32": "8.3.1.1",
"HarfBuzzSharp.NativeAssets.macOS": "8.3.1.1"
},
"runtime": {
"lib/net8.0/HarfBuzzSharp.dll": {
"assemblyVersion": "1.0.0.0",
"fileVersion": "8.3.1.1"
}
}
},
"HarfBuzzSharp.NativeAssets.Linux/8.3.1.1": {
"runtimeTargets": {
"runtimes/linux-arm/native/libHarfBuzzSharp.so": {
"rid": "linux-arm",
"assetType": "native",
"fileVersion": "0.0.0.0"
},
"runtimes/linux-arm64/native/libHarfBuzzSharp.so": {
"rid": "linux-arm64",
"assetType": "native",
"fileVersion": "0.0.0.0"
},
"runtimes/linux-loongarch64/native/libHarfBuzzSharp.so": {
"rid": "linux-loongarch64",
"assetType": "native",
"fileVersion": "0.0.0.0"
},
"runtimes/linux-musl-arm/native/libHarfBuzzSharp.so": {
"rid": "linux-musl-arm",
"assetType": "native",
"fileVersion": "0.0.0.0"
},
"runtimes/linux-musl-arm64/native/libHarfBuzzSharp.so": {
"rid": "linux-musl-arm64",
"assetType": "native",
"fileVersion": "0.0.0.0"
},
"runtimes/linux-musl-loongarch64/native/libHarfBuzzSharp.so": {
"rid": "linux-musl-loongarch64",
"assetType": "native",
"fileVersion": "0.0.0.0"
},
"runtimes/linux-musl-riscv64/native/libHarfBuzzSharp.so": {
"rid": "linux-musl-riscv64",
"assetType": "native",
"fileVersion": "0.0.0.0"
},
"runtimes/linux-musl-x64/native/libHarfBuzzSharp.so": {
"rid": "linux-musl-x64",
"assetType": "native",
"fileVersion": "0.0.0.0"
},
"runtimes/linux-riscv64/native/libHarfBuzzSharp.so": {
"rid": "linux-riscv64",
"assetType": "native",
"fileVersion": "0.0.0.0"
},
"runtimes/linux-x64/native/libHarfBuzzSharp.so": {
"rid": "linux-x64",
"assetType": "native",
"fileVersion": "0.0.0.0"
},
"runtimes/linux-x86/native/libHarfBuzzSharp.so": {
"rid": "linux-x86",
"assetType": "native",
"fileVersion": "0.0.0.0"
}
}
},
"HarfBuzzSharp.NativeAssets.macOS/8.3.1.1": {
"runtimeTargets": {
"runtimes/osx/native/libHarfBuzzSharp.dylib": {
"rid": "osx",
"assetType": "native",
"fileVersion": "0.0.0.0"
}
}
},
"HarfBuzzSharp.NativeAssets.WebAssembly/8.3.1.1": {},
"HarfBuzzSharp.NativeAssets.Win32/8.3.1.1": {
"runtimeTargets": {
"runtimes/win-arm64/native/libHarfBuzzSharp.dll": {
"rid": "win-arm64",
"assetType": "native",
"fileVersion": "0.0.0.0"
},
"runtimes/win-x64/native/libHarfBuzzSharp.dll": {
"rid": "win-x64",
"assetType": "native",
"fileVersion": "0.0.0.0"
},
"runtimes/win-x86/native/libHarfBuzzSharp.dll": {
"rid": "win-x86",
"assetType": "native",
"fileVersion": "0.0.0.0"
}
}
},
"MicroCom.Runtime/0.11.0": {
"runtime": {
"lib/net5.0/MicroCom.Runtime.dll": {
"assemblyVersion": "0.11.0.0",
"fileVersion": "0.11.0.0"
}
}
},
"SkiaSharp/2.88.9": {
"dependencies": {
"SkiaSharp.NativeAssets.Win32": "2.88.9",
"SkiaSharp.NativeAssets.macOS": "2.88.9"
},
"runtime": {
"lib/net6.0/SkiaSharp.dll": {
"assemblyVersion": "2.88.0.0",
"fileVersion": "2.88.9.0"
}
}
},
"SkiaSharp.NativeAssets.Linux/2.88.9": {
"dependencies": {
"SkiaSharp": "2.88.9"
},
"runtimeTargets": {
"runtimes/linux-arm/native/libSkiaSharp.so": {
"rid": "linux-arm",
"assetType": "native",
"fileVersion": "0.0.0.0"
},
"runtimes/linux-arm64/native/libSkiaSharp.so": {
"rid": "linux-arm64",
"assetType": "native",
"fileVersion": "0.0.0.0"
},
"runtimes/linux-musl-x64/native/libSkiaSharp.so": {
"rid": "linux-musl-x64",
"assetType": "native",
"fileVersion": "0.0.0.0"
},
"runtimes/linux-x64/native/libSkiaSharp.so": {
"rid": "linux-x64",
"assetType": "native",
"fileVersion": "0.0.0.0"
}
}
},
"SkiaSharp.NativeAssets.macOS/2.88.9": {
"runtimeTargets": {
"runtimes/osx/native/libSkiaSharp.dylib": {
"rid": "osx",
"assetType": "native",
"fileVersion": "0.0.0.0"
}
}
},
"SkiaSharp.NativeAssets.WebAssembly/2.88.9": {},
"SkiaSharp.NativeAssets.Win32/2.88.9": {
"runtimeTargets": {
"runtimes/win-arm64/native/libSkiaSharp.dll": {
"rid": "win-arm64",
"assetType": "native",
"fileVersion": "0.0.0.0"
},
"runtimes/win-x64/native/libSkiaSharp.dll": {
"rid": "win-x64",
"assetType": "native",
"fileVersion": "0.0.0.0"
},
"runtimes/win-x86/native/libSkiaSharp.dll": {
"rid": "win-x86",
"assetType": "native",
"fileVersion": "0.0.0.0"
}
}
},
"System.IO.Pipelines/8.0.0": {
"runtime": {
"lib/net8.0/System.IO.Pipelines.dll": {
"assemblyVersion": "8.0.0.0",
"fileVersion": "8.0.23.53103"
}
}
},
"Tmds.DBus.Protocol/0.21.2": {
"dependencies": {
"System.IO.Pipelines": "8.0.0"
},
"runtime": {
"lib/net8.0/Tmds.DBus.Protocol.dll": {
"assemblyVersion": "0.21.2.0",
"fileVersion": "0.21.2.0"
}
}
},
"GsaEditor.Core/1.0.0": {
"runtime": {
"GsaEditor.Core.dll": {
"assemblyVersion": "1.0.0.0",
"fileVersion": "1.0.0.0"
}
}
}
}
},
"libraries": {
"GsaEditor/1.0.0": {
"type": "project",
"serviceable": false,
"sha512": ""
},
"Avalonia/11.3.6": {
"type": "package",
"serviceable": true,
"sha512": "sha512-h8V6Cmklaak2h+WVp5gcsprwmrCLQylfNmIQnOQyiETzOazKGmFhlcf+K9zoupxfkhOQXsbSJpb/elyCkrjlig==",
"path": "avalonia/11.3.6",
"hashPath": "avalonia.11.3.6.nupkg.sha512"
},
"Avalonia.Angle.Windows.Natives/2.1.25547.20250602": {
"type": "package",
"serviceable": true,
"sha512": "sha512-ZL0VLc4s9rvNNFt19Pxm5UNAkmKNylugAwJPX9ulXZ6JWs/l6XZihPWWTyezaoNOVyEPU8YbURtW7XMAtqXH5A==",
"path": "avalonia.angle.windows.natives/2.1.25547.20250602",
"hashPath": "avalonia.angle.windows.natives.2.1.25547.20250602.nupkg.sha512"
},
"Avalonia.BuildServices/0.0.31": {
"type": "package",
"serviceable": true,
"sha512": "sha512-KmCN6Hc+45q4OnF10ge450yVUvWuxU6bdQiyKqiSvrHKpahNrEdk0kG6Ip6GHk2SKOCttGQuA206JVdkldEENg==",
"path": "avalonia.buildservices/0.0.31",
"hashPath": "avalonia.buildservices.0.0.31.nupkg.sha512"
},
"Avalonia.Controls.ColorPicker/11.3.6": {
"type": "package",
"serviceable": true,
"sha512": "sha512-zgJFM7P7hOS9qcuSUqL2tjBFIsi03qiwAztYjtjtKjfBvLBOrVcmL5qHwjfjeLRLtjHprjMraAHtu3O2vi+51g==",
"path": "avalonia.controls.colorpicker/11.3.6",
"hashPath": "avalonia.controls.colorpicker.11.3.6.nupkg.sha512"
},
"Avalonia.Desktop/11.3.6": {
"type": "package",
"serviceable": true,
"sha512": "sha512-+r/3tPkf2E7rxbKYYd5bRq9vdaPCvuhf+PAKFmKqdQrzjWPSP4/Dg0VDIcgPsv+A2Tq82x67ZUJGQyhiLfsMVQ==",
"path": "avalonia.desktop/11.3.6",
"hashPath": "avalonia.desktop.11.3.6.nupkg.sha512"
},
"Avalonia.Diagnostics/11.3.6": {
"type": "package",
"serviceable": true,
"sha512": "sha512-iOGrfU/0TudsfHARpsreVt5V1oaer838IghYdgZjlOvGKmh5J1uc5GOSBmBodUpuPDE78wmdVrJZLC57qsndzg==",
"path": "avalonia.diagnostics/11.3.6",
"hashPath": "avalonia.diagnostics.11.3.6.nupkg.sha512"
},
"Avalonia.Fonts.Inter/11.3.6": {
"type": "package",
"serviceable": true,
"sha512": "sha512-ASuCuosS8elNsRxNpdZE/UrmMSh1wtwoqRDEgpdcbVuMRUH/8ElCur5PdyWhJSwIit/YXaS+xb2xQAGOnvT45w==",
"path": "avalonia.fonts.inter/11.3.6",
"hashPath": "avalonia.fonts.inter.11.3.6.nupkg.sha512"
},
"Avalonia.FreeDesktop/11.3.6": {
"type": "package",
"serviceable": true,
"sha512": "sha512-vI+m3r5GRoF0CS1EpMG+gx9qCQVkCwlsHyW6+UWxNNpL2IZ9S3zPt/GNgG6vFrhZpBTsoG+OZnZ9XPRT+KkqUw==",
"path": "avalonia.freedesktop/11.3.6",
"hashPath": "avalonia.freedesktop.11.3.6.nupkg.sha512"
},
"Avalonia.Native/11.3.6": {
"type": "package",
"serviceable": true,
"sha512": "sha512-QaIKZLfquJ12Rpwo9pQ2V24TfpJI2tQJeYe235G4cJnI3d699DvMUcntQA1VLDCwAK3xHOSTyoN737oU3f9ErQ==",
"path": "avalonia.native/11.3.6",
"hashPath": "avalonia.native.11.3.6.nupkg.sha512"
},
"Avalonia.Remote.Protocol/11.3.6": {
"type": "package",
"serviceable": true,
"sha512": "sha512-i1lW1ZA5Ghe19kbPiC2pMq48P/9CcOWi8hFj0Y8UZr/czCQok3YJRwU5z8vKsJ5xsMvzpPf+GO/6Qu9Dy2yLvg==",
"path": "avalonia.remote.protocol/11.3.6",
"hashPath": "avalonia.remote.protocol.11.3.6.nupkg.sha512"
},
"Avalonia.Skia/11.3.6": {
"type": "package",
"serviceable": true,
"sha512": "sha512-qM1qGCUxPpheLwQ6gr+I4h2dhFz3L9Apa1MhiUsUELzSI6FqfzX2+67M8f/6gOHAtrjwxkjpanNW6eF9zW+g7Q==",
"path": "avalonia.skia/11.3.6",
"hashPath": "avalonia.skia.11.3.6.nupkg.sha512"
},
"Avalonia.Themes.Fluent/11.3.6": {
"type": "package",
"serviceable": true,
"sha512": "sha512-YQ3x66qgMqDxbQoLTqYKGA7yXnxi8fzDL9+CITPrXrVZimlemWmjYqE0NWgd2c78gpp7dNEV4eYzwbbr8bH+9A==",
"path": "avalonia.themes.fluent/11.3.6",
"hashPath": "avalonia.themes.fluent.11.3.6.nupkg.sha512"
},
"Avalonia.Themes.Simple/11.3.6": {
"type": "package",
"serviceable": true,
"sha512": "sha512-MuwYjUI9qMdu7TYyJbBntWlZRJWi6QmAYhMEBEdDgiEO0yUpTiKNLpYSn+MS8Xh9Jm9N4krclBigW7FYJkqLOA==",
"path": "avalonia.themes.simple/11.3.6",
"hashPath": "avalonia.themes.simple.11.3.6.nupkg.sha512"
},
"Avalonia.Win32/11.3.6": {
"type": "package",
"serviceable": true,
"sha512": "sha512-KHft8utwz5ktcwHdEAwAPfNGdwqoE0qrwVmPjGYggqJAI4PuaiATkdSObSCXfxRVs2H9aqVB8hiEVB+9rvkxYw==",
"path": "avalonia.win32/11.3.6",
"hashPath": "avalonia.win32.11.3.6.nupkg.sha512"
},
"Avalonia.X11/11.3.6": {
"type": "package",
"serviceable": true,
"sha512": "sha512-6NKE1py4zbkMJg++2rMJHyjEXNo2JU0Nj9nEMnXh2gyHyfclhULIGIqmpsLvJ7Ljv1+6iLDqArCFRn+mi1hANw==",
"path": "avalonia.x11/11.3.6",
"hashPath": "avalonia.x11.11.3.6.nupkg.sha512"
},
"CommunityToolkit.Mvvm/8.2.1": {
"type": "package",
"serviceable": true,
"sha512": "sha512-I24ofWVEdplxYjUez9/bljv/qb8r8Ccj6cvYXHexNBegLaD3iDy3QrzAAOYVMmfGWIXxlU1ZtECQNfU07+6hXQ==",
"path": "communitytoolkit.mvvm/8.2.1",
"hashPath": "communitytoolkit.mvvm.8.2.1.nupkg.sha512"
},
"HarfBuzzSharp/8.3.1.1": {
"type": "package",
"serviceable": true,
"sha512": "sha512-tLZN66oe/uiRPTZfrCU4i8ScVGwqHNh5MHrXj0yVf4l7Mz0FhTGnQ71RGySROTmdognAs0JtluHkL41pIabWuQ==",
"path": "harfbuzzsharp/8.3.1.1",
"hashPath": "harfbuzzsharp.8.3.1.1.nupkg.sha512"
},
"HarfBuzzSharp.NativeAssets.Linux/8.3.1.1": {
"type": "package",
"serviceable": true,
"sha512": "sha512-3EZ1mpIiKWRLL5hUYA82ZHteeDIVaEA/Z0rA/wU6tjx6crcAkJnBPwDXZugBSfo8+J3EznvRJf49uMsqYfKrHg==",
"path": "harfbuzzsharp.nativeassets.linux/8.3.1.1",
"hashPath": "harfbuzzsharp.nativeassets.linux.8.3.1.1.nupkg.sha512"
},
"HarfBuzzSharp.NativeAssets.macOS/8.3.1.1": {
"type": "package",
"serviceable": true,
"sha512": "sha512-jbtCsgftcaFLCA13tVKo5iWdElJScrulLTKJre36O4YQTIlwDtPPqhRZNk+Y0vv4D1gxbscasGRucUDfS44ofQ==",
"path": "harfbuzzsharp.nativeassets.macos/8.3.1.1",
"hashPath": "harfbuzzsharp.nativeassets.macos.8.3.1.1.nupkg.sha512"
},
"HarfBuzzSharp.NativeAssets.WebAssembly/8.3.1.1": {
"type": "package",
"serviceable": true,
"sha512": "sha512-loJweK2u/mH/3C2zBa0ggJlITIszOkK64HLAZB7FUT670dTg965whLFYHDQo69NmC4+d9UN0icLC9VHidXaVCA==",
"path": "harfbuzzsharp.nativeassets.webassembly/8.3.1.1",
"hashPath": "harfbuzzsharp.nativeassets.webassembly.8.3.1.1.nupkg.sha512"
},
"HarfBuzzSharp.NativeAssets.Win32/8.3.1.1": {
"type": "package",
"serviceable": true,
"sha512": "sha512-UsJtQsfAJoFDZrXc4hCUfRPMqccfKZ0iumJ/upcUjz/cmsTgVFGNEL5yaJWmkqsuFYdMWbj/En5/kS4PFl9hBA==",
"path": "harfbuzzsharp.nativeassets.win32/8.3.1.1",
"hashPath": "harfbuzzsharp.nativeassets.win32.8.3.1.1.nupkg.sha512"
},
"MicroCom.Runtime/0.11.0": {
"type": "package",
"serviceable": true,
"sha512": "sha512-MEnrZ3UIiH40hjzMDsxrTyi8dtqB5ziv3iBeeU4bXsL/7NLSal9F1lZKpK+tfBRnUoDSdtcW3KufE4yhATOMCA==",
"path": "microcom.runtime/0.11.0",
"hashPath": "microcom.runtime.0.11.0.nupkg.sha512"
},
"SkiaSharp/2.88.9": {
"type": "package",
"serviceable": true,
"sha512": "sha512-3MD5VHjXXieSHCleRLuaTXmL2pD0mB7CcOB1x2kA1I4bhptf4e3R27iM93264ZYuAq6mkUyX5XbcxnZvMJYc1Q==",
"path": "skiasharp/2.88.9",
"hashPath": "skiasharp.2.88.9.nupkg.sha512"
},
"SkiaSharp.NativeAssets.Linux/2.88.9": {
"type": "package",
"serviceable": true,
"sha512": "sha512-cWSaJKVPWAaT/WIn9c8T5uT/l4ETwHxNJTkEOtNKjphNo8AW6TF9O32aRkxqw3l8GUdUo66Bu7EiqtFh/XG0Zg==",
"path": "skiasharp.nativeassets.linux/2.88.9",
"hashPath": "skiasharp.nativeassets.linux.2.88.9.nupkg.sha512"
},
"SkiaSharp.NativeAssets.macOS/2.88.9": {
"type": "package",
"serviceable": true,
"sha512": "sha512-Nv5spmKc4505Ep7oUoJ5vp3KweFpeNqxpyGDWyeEPTX2uR6S6syXIm3gj75dM0YJz7NPvcix48mR5laqs8dPuA==",
"path": "skiasharp.nativeassets.macos/2.88.9",
"hashPath": "skiasharp.nativeassets.macos.2.88.9.nupkg.sha512"
},
"SkiaSharp.NativeAssets.WebAssembly/2.88.9": {
"type": "package",
"serviceable": true,
"sha512": "sha512-kt06RccBHSnAs2wDYdBSfsjIDbY3EpsOVqnlDgKdgvyuRA8ZFDaHRdWNx1VHjGgYzmnFCGiTJBnXFl5BqGwGnA==",
"path": "skiasharp.nativeassets.webassembly/2.88.9",
"hashPath": "skiasharp.nativeassets.webassembly.2.88.9.nupkg.sha512"
},
"SkiaSharp.NativeAssets.Win32/2.88.9": {
"type": "package",
"serviceable": true,
"sha512": "sha512-wb2kYgU7iy84nQLYZwMeJXixvK++GoIuECjU4ECaUKNuflyRlJKyiRhN1MAHswvlvzuvkrjRWlK0Za6+kYQK7w==",
"path": "skiasharp.nativeassets.win32/2.88.9",
"hashPath": "skiasharp.nativeassets.win32.2.88.9.nupkg.sha512"
},
"System.IO.Pipelines/8.0.0": {
"type": "package",
"serviceable": true,
"sha512": "sha512-FHNOatmUq0sqJOkTx+UF/9YK1f180cnW5FVqnQMvYUN0elp6wFzbtPSiqbo1/ru8ICp43JM1i7kKkk6GsNGHlA==",
"path": "system.io.pipelines/8.0.0",
"hashPath": "system.io.pipelines.8.0.0.nupkg.sha512"
},
"Tmds.DBus.Protocol/0.21.2": {
"type": "package",
"serviceable": true,
"sha512": "sha512-ScSMrUrrw8px4kK1Glh0fZv/HQUlg1078bNXNPfRPKQ3WbRzV9HpsydYEOgSoMK5LWICMf2bMwIFH0pGjxjcMA==",
"path": "tmds.dbus.protocol/0.21.2",
"hashPath": "tmds.dbus.protocol.0.21.2.nupkg.sha512"
},
"GsaEditor.Core/1.0.0": {
"type": "project",
"serviceable": false,
"sha512": ""
}
}
}

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@ -0,0 +1,13 @@
{
"runtimeOptions": {
"tfm": "net8.0",
"framework": {
"name": "Microsoft.NETCore.App",
"version": "8.0.0"
},
"configProperties": {
"System.Runtime.InteropServices.BuiltInComInterop.IsSupported": true,
"System.Runtime.Serialization.EnableUnsafeBinaryFormatterSerialization": false
}
}
}

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Some files were not shown because too many files have changed in this diff Show More