fixed GSA writing, added always-edit button and search in file and for file

This commit is contained in:
2026-07-21 15:50:52 +02:00
parent cbf0891ba4
commit d171c34236
42 changed files with 538 additions and 61 deletions

View File

@ -1,4 +1,4 @@
using System.Xml.Linq;
using System.Text.RegularExpressions;
namespace GsaEditor.Core.IO;
@ -34,10 +34,20 @@ public class GsaIndexEntry
}
/// <summary>
/// Parses .idx NML index files (XML-compatible format) into a list of <see cref="GsaIndexEntry"/>.
/// Parses .idx NML index files into a list of <see cref="GsaIndexEntry"/>.
/// NML is the engine's own tag syntax (&lt;Name=value&gt; blocks, see <see cref="GsaIndexWriter"/>),
/// not standard XML.
/// </summary>
public static class GsaIndexReader
{
private static readonly Regex EntryRegex = new(
@"<Entry=(?<body>.*?)\n\s*>",
RegexOptions.Singleline | RegexOptions.Compiled);
private static readonly Regex TagRegex = new(
@"<(?<name>\w+)=""?(?<value>[^"">]*)""?>",
RegexOptions.Compiled);
/// <summary>
/// Reads index entries from the specified .idx file.
/// </summary>
@ -45,23 +55,36 @@ public static class GsaIndexReader
/// <returns>A list of parsed index entries.</returns>
public static List<GsaIndexEntry> Read(string filePath)
{
var doc = XDocument.Load(filePath);
var text = File.ReadAllText(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"))
foreach (Match entryMatch in EntryRegex.Matches(text))
{
var entry = new GsaIndexEntry
var entry = new GsaIndexEntry();
foreach (Match tag in TagRegex.Matches(entryMatch.Groups["body"].Value))
{
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
};
var value = tag.Groups["value"].Value;
switch (tag.Groups["name"].Value)
{
case "ID":
entry.Id = value;
break;
case "Method":
entry.Method = value.Equals("Zlib", StringComparison.OrdinalIgnoreCase) ? 1 : 0;
break;
case "Len":
entry.Length = uint.TryParse(value, out var l) ? l : 0;
break;
case "CompLen":
entry.CompressedLength = uint.TryParse(value, out var cl) ? cl : 0;
break;
case "Offset":
entry.Offset = long.TryParse(value, out var o) ? o : 0;
break;
}
}
entries.Add(entry);
}

View File

@ -1,36 +1,62 @@
using System.Xml.Linq;
using System.Text;
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.
/// The NML format is the engine's own tag syntax (NOT standard XML):
/// <code>
/// &lt;Version=1.0&gt;
/// &lt;Index=
/// &lt;Entry=
/// &lt;ID="path/in/archive"&gt;
/// &lt;CompLen=123&gt;
/// &lt;Len=456&gt;
/// &lt;Offset=789&gt;
/// &lt;Method="Zlib"&gt;
/// &gt;
/// &gt;
/// </code>
/// Tab indentation, LF line endings, no BOM. Method is "Zlib" or "Raw".
/// The engine parses this file to locate entries, so the format must match exactly.
/// </summary>
public static class GsaIndexWriter
{
/// <summary>
/// The engine's bootstrap script is loaded by sequential archive scan and is
/// deliberately absent from the shipped index files, so it is skipped here too.
/// </summary>
private const string BootstrapAlias = "bootstrap.txt";
/// <summary>
/// Writes the index file for the given archive to the specified path.
/// Entry offsets must be up to date (i.e. call after <see cref="GsaWriter.Write(GsaArchive, string)"/>).
/// </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)
)
)
)
);
var sb = new StringBuilder();
sb.Append("<Version=1.0>\n");
sb.Append("<Index=\n");
doc.Save(filePath);
foreach (var e in archive.Entries)
{
if (string.Equals(e.Alias, BootstrapAlias, StringComparison.OrdinalIgnoreCase))
continue;
sb.Append("\t<Entry=\n");
sb.Append("\t\t<ID=\"").Append(e.Alias).Append("\">\n");
sb.Append("\t\t<CompLen=").Append(e.CompressedLength).Append(">\n");
sb.Append("\t\t<Len=").Append(e.OriginalLength).Append(">\n");
sb.Append("\t\t<Offset=").Append(e.DataOffset).Append(">\n");
sb.Append("\t\t<Method=\"").Append(e.IsCompressed ? "Zlib" : "Raw").Append("\">\n");
sb.Append("\t>\n");
}
sb.Append(">\n");
File.WriteAllText(filePath, sb.ToString(), new UTF8Encoding(encoderShouldEmitUTF8Identifier: false));
}
}

View File

@ -15,6 +15,12 @@ public static class GsaWriter
/// <summary>Magic word for the NARD (enhanced) format.</summary>
private const uint MAGIC_NARD = 0x4E415244;
/// <summary>
/// End-of-archive terminator written where the next entry's alias length would be.
/// The engine's sequential reader stops when it hits this invalid length.
/// </summary>
private const uint TERMINATOR = 0xFFFFFFFF;
/// <summary>
/// Writes the archive to the specified file path.
/// </summary>
@ -53,6 +59,11 @@ public static class GsaWriter
{
WriteEntry(writer, entry, archive);
}
// Write the end-of-archive terminator, aligned like an entry header
if (archive.Format == GsaFormat.NARD && archive.OffsetPadding > 0)
WritePadding(writer, archive.OffsetPadding);
writer.Write(TERMINATOR);
}
/// <summary>

View File

@ -67,7 +67,9 @@
</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"/>.
Parses .idx NML index files into a list of <see cref="T:GsaEditor.Core.IO.GsaIndexEntry"/>.
NML is the engine's own tag syntax (&lt;Name=value&gt; blocks, see <see cref="T:GsaEditor.Core.IO.GsaIndexWriter"/>),
not standard XML.
</summary>
</member>
<member name="M:GsaEditor.Core.IO.GsaIndexReader.Read(System.String)">
@ -80,13 +82,33 @@
<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.
The NML format is the engine's own tag syntax (NOT standard XML):
<code>
&lt;Version=1.0&gt;
&lt;Index=
&lt;Entry=
&lt;ID="path/in/archive"&gt;
&lt;CompLen=123&gt;
&lt;Len=456&gt;
&lt;Offset=789&gt;
&lt;Method="Zlib"&gt;
&gt;
&gt;
</code>
Tab indentation, LF line endings, no BOM. Method is "Zlib" or "Raw".
The engine parses this file to locate entries, so the format must match exactly.
</summary>
</member>
<member name="F:GsaEditor.Core.IO.GsaIndexWriter.BootstrapAlias">
<summary>
The engine's bootstrap script is loaded by sequential archive scan and is
deliberately absent from the shipped index files, so it is skipped here too.
</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.
Entry offsets must be up to date (i.e. call after <see cref="M:GsaEditor.Core.IO.GsaWriter.Write(GsaEditor.Core.Models.GsaArchive,System.String)"/>).
</summary>
<param name="archive">The archive whose entries will be indexed.</param>
<param name="filePath">The output .idx file path.</param>
@ -149,6 +171,12 @@
<member name="F:GsaEditor.Core.IO.GsaWriter.MAGIC_NARD">
<summary>Magic word for the NARD (enhanced) format.</summary>
</member>
<member name="F:GsaEditor.Core.IO.GsaWriter.TERMINATOR">
<summary>
End-of-archive terminator written where the next entry's alias length would be.
The engine's sequential reader stops when it hits this invalid length.
</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.

View File

@ -13,10 +13,10 @@ 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+d6d621dc92b3083d8e47827baa0ccf59d5b0a4c4")]
[assembly: System.Reflection.AssemblyInformationalVersionAttribute("1.0.0+cbf0891ba467ebec2dd40e9b34eb1f2d6379d7dd")]
[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.
// Generated by the MSBuild WriteCodeFragment class.

View File

@ -1 +1 @@
27416b735d6b5e6056e4f76966d345ebebb5a18e412e4170cdbcf381f9ab7092
24bad01704fb232d5dd4f84bfcb34e9e1b11dc1475b9ee697c24bd2eb1122246

View File

@ -67,7 +67,9 @@
</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"/>.
Parses .idx NML index files into a list of <see cref="T:GsaEditor.Core.IO.GsaIndexEntry"/>.
NML is the engine's own tag syntax (&lt;Name=value&gt; blocks, see <see cref="T:GsaEditor.Core.IO.GsaIndexWriter"/>),
not standard XML.
</summary>
</member>
<member name="M:GsaEditor.Core.IO.GsaIndexReader.Read(System.String)">
@ -80,13 +82,33 @@
<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.
The NML format is the engine's own tag syntax (NOT standard XML):
<code>
&lt;Version=1.0&gt;
&lt;Index=
&lt;Entry=
&lt;ID="path/in/archive"&gt;
&lt;CompLen=123&gt;
&lt;Len=456&gt;
&lt;Offset=789&gt;
&lt;Method="Zlib"&gt;
&gt;
&gt;
</code>
Tab indentation, LF line endings, no BOM. Method is "Zlib" or "Raw".
The engine parses this file to locate entries, so the format must match exactly.
</summary>
</member>
<member name="F:GsaEditor.Core.IO.GsaIndexWriter.BootstrapAlias">
<summary>
The engine's bootstrap script is loaded by sequential archive scan and is
deliberately absent from the shipped index files, so it is skipped here too.
</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.
Entry offsets must be up to date (i.e. call after <see cref="M:GsaEditor.Core.IO.GsaWriter.Write(GsaEditor.Core.Models.GsaArchive,System.String)"/>).
</summary>
<param name="archive">The archive whose entries will be indexed.</param>
<param name="filePath">The output .idx file path.</param>
@ -149,6 +171,12 @@
<member name="F:GsaEditor.Core.IO.GsaWriter.MAGIC_NARD">
<summary>Magic word for the NARD (enhanced) format.</summary>
</member>
<member name="F:GsaEditor.Core.IO.GsaWriter.TERMINATOR">
<summary>
End-of-archive terminator written where the next entry's alias length would be.
The engine's sequential reader stops when it hits this invalid length.
</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.