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:
70
GsaEditor.Core/IO/GsaIndexReader.cs
Normal file
70
GsaEditor.Core/IO/GsaIndexReader.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
36
GsaEditor.Core/IO/GsaIndexWriter.cs
Normal file
36
GsaEditor.Core/IO/GsaIndexWriter.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
163
GsaEditor.Core/IO/GsaReader.cs
Normal file
163
GsaEditor.Core/IO/GsaReader.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
114
GsaEditor.Core/IO/GsaWriter.cs
Normal file
114
GsaEditor.Core/IO/GsaWriter.cs
Normal 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]);
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user