fixed GSA writing, added always-edit button and search in file and for file
This commit is contained in:
@ -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 (<Name=value> 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);
|
||||
}
|
||||
|
||||
|
||||
@ -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>
|
||||
/// <Version=1.0>
|
||||
/// <Index=
|
||||
/// <Entry=
|
||||
/// <ID="path/in/archive">
|
||||
/// <CompLen=123>
|
||||
/// <Len=456>
|
||||
/// <Offset=789>
|
||||
/// <Method="Zlib">
|
||||
/// >
|
||||
/// >
|
||||
/// </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));
|
||||
}
|
||||
}
|
||||
|
||||
@ -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>
|
||||
|
||||
Reference in New Issue
Block a user