Skip to content

Commit

Permalink
label importer: improve label importer
Browse files Browse the repository at this point in the history
- importer logic is larger enough that this deserved a little more love. break it out into a few classes
- better support for BSNES+ formats (the one I am using was a little different but pretty compatible with the previous version)
- slightly better error handling
  • Loading branch information
binary1230 committed Dec 3, 2022
1 parent 6cdfa21 commit 73223cf
Show file tree
Hide file tree
Showing 8 changed files with 242 additions and 96 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
using Diz.Core.serialization.xml_serializer;
using Diz.Core.util;
using Diz.Cpu._65816;
using Diz.Import;
using Diz.Import.bizhawk;
using Diz.Import.bsnes.usagemap;
using Diz.LogWriter;
Expand Down Expand Up @@ -204,7 +205,7 @@ public void ImportLabelsCsv(ILabelEditorView labelEditor, bool replaceAll)
var errLine = 0;
try
{
Project.Data.Labels.ImportLabelsFromCsv(importFilename, replaceAll, ref errLine);
Project.Data.Labels.ImportLabelsFromCsv(importFilename, replaceAll, out errLine);
labelEditor.RepopulateFromData();
}
catch (Exception ex)
Expand Down
1 change: 1 addition & 0 deletions Diz.Core.Interfaces/LabelInterfaces.cs
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ public interface ILabelProvider
void RemoveLabel(int snesAddress);

void SetAll(Dictionary<int, IAnnotationLabel> newLabels);
void AppendLabels(Dictionary<int, IAnnotationLabel> newLabels);
}

public interface IReadOnlyLabels
Expand Down
12 changes: 12 additions & 0 deletions Diz.Core/model/LabelProvider.cs
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,13 @@ public void SetAll(Dictionary<int, IAnnotationLabel> newLabels)

OnLabelChanged?.Invoke(this, EventArgs.Empty);
}

public void AppendLabels(Dictionary<int, IAnnotationLabel> newLabels)
{
NormalProvider.AppendLabels(newLabels);

OnLabelChanged?.Invoke(this, EventArgs.Empty);
}

#region "Equality"
public bool Equals(LabelsServiceWithTemp other)
Expand Down Expand Up @@ -276,6 +283,11 @@ public void RemoveLabel(int snesAddress)
public void SetAll(Dictionary<int, IAnnotationLabel> newLabels)
{
DeleteAllLabels();
AppendLabels(newLabels);
}

public void AppendLabels(Dictionary<int, IAnnotationLabel> newLabels)
{
foreach (var key in newLabels.Keys)
{
Labels.Add(key, newLabels[key]);
Expand Down
96 changes: 2 additions & 94 deletions Diz.Core/util/Util.cs
Original file line number Diff line number Diff line change
Expand Up @@ -126,13 +126,13 @@ public static int ParseHexOrBase10String(string data)
return int.Parse(data);
}

// this function is a little weird and redundant maybe?
public static IEnumerable<string> ReadLines(string path)
{
using var fs = new FileStream(path, FileMode.Open, FileAccess.Read, FileShare.ReadWrite, 0x1000, FileOptions.SequentialScan);
using var sr = new StreamReader(fs, Encoding.UTF8);

string line;
while ((line = sr.ReadLine()) != null)
while (sr.ReadLine() is { } line)
{
yield return line;
}
Expand Down Expand Up @@ -467,98 +467,6 @@ public static bool FieldIsEqual<T>(T field, T value, bool compareRefOnly = false

public static class ContentUtils
{
public static Dictionary<int, Label> ReadLabelsFromCsv(string importFilename, out int errLine)
{
var newValues = new Dictionary<int, Label>();
var lines = Util.ReadLines(importFilename).ToArray();

var validLabelChars = new Regex(@"^([a-zA-Z0-9_\-]*)$");

// Coming in from BSNES symbol map if it begins with the header
var fromBSNES = lines.Length > 0 && lines[0].StartsWith("#SNES65816");
var inSection = string.Empty;

errLine = 0;

for (var i = 0; i < lines.Length; i++)
{
var label = new Label();

string labelAddress = string.Empty;

errLine = i + 1;

if (fromBSNES)
{
// Skip the line if it's empty or a comment
if (lines[i].Trim().Length == 0 || lines[i].StartsWith('#')) continue;

// Set which INI section we are in
if (lines[i].StartsWith('[') && lines[i].EndsWith(']'))
{
inSection = lines[i];
continue;
}

if (inSection == "[SYMBOL]")
{
string[] symbols = lines[i].Trim().Split(' ');
labelAddress = symbols[0].Replace(":", "").ToUpper(); // Remove bank colon
label.Name = symbols[1].Replace(".", "_"); // Replace dots which are valid in BSNES
}
else if (inSection == "[COMMENT]")
{
string[] comments = lines[i].Trim().Split(' ', 2);
labelAddress = comments[0].Replace(":", "").ToUpper(); // Remove bank colon
label.Comment = comments[1].Replace("\"", ""); // Remove quotes
}
}
// NOTE: this is kind of a risky way to parse CSV files, won't deal with weirdness in the comments
// section. replace with something better
else
{
Util.SplitOnFirstComma(lines[i], out labelAddress, out var remainder);
Util.SplitOnFirstComma(remainder, out var labelName, out var labelComment);

label.Name = labelName.Trim();
label.Comment = labelComment;
}

if (!validLabelChars.Match(label.Name).Success)
throw new InvalidDataException("invalid label name: " + label.Name);

var address = int.Parse(labelAddress, NumberStyles.HexNumber, null);
if (newValues.ContainsKey(address))
{
// Update empty label properties instead of overwriting the entire object
// if there are multiple definitions (like from BSNES or handmade CSV)
var thisLabel = newValues[address];
if (thisLabel.Name.IsEmpty()) thisLabel.Name = label.Name;
if (thisLabel.Comment.IsEmpty()) thisLabel.Comment = label.Comment;
}
else
{
newValues.Add(address, label);
}
}

errLine = -1;
return newValues;
}

public static void ImportLabelsFromCsv(this ILabelProvider labelProvider, string importFilename, bool replaceAll, ref int errLine)
{
var labelsFromCsv = ReadLabelsFromCsv(importFilename, out errLine);

if (replaceAll)
labelProvider.DeleteAllLabels();

foreach (var (key, value) in labelsFromCsv)
{
labelProvider.AddLabel(key, value, true);
}
}

public static object SingleOrDefaultOfType<T>(this IEnumerable<T> enumerable, Type desiredType)
{
return enumerable.SingleOrDefault(item => item.GetType() == desiredType);
Expand Down
113 changes: 113 additions & 0 deletions Diz.Import/src/LabelImporter.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
using System.Globalization;
using System.Text.RegularExpressions;
using Diz.Core.Interfaces;
using Diz.Core.model;
using Diz.Core.util;
using Diz.Import.bsnes;

namespace Diz.Import;

public abstract class LabelImporter
{
// after import, if there was an error, this will be the line# of what it was.
// if -1, we parsed the entire file.
public int LastErrorLineNumber { get; private set; } = -1;

// we don't modify labels in the open project directly, instead we read them
// into here and only return this on success.
private readonly Dictionary<int, IAnnotationLabel> newLabels = new();

public virtual Dictionary<int, IAnnotationLabel> ReadLabelsFromFile(string importFilename)
{
newLabels.Clear();
LastErrorLineNumber = 0;

var lineIndex = 0;
foreach (var line in Util.ReadLines(importFilename))
{
LastErrorLineNumber = lineIndex + 1;
ParseLine(line);
lineIndex++;
}

if (lineIndex == 0)
throw new InvalidDataException("No lines in file, can't import.");

LastErrorLineNumber = -1;
return newLabels;
}

private void ParseLine(string line)
{
var labelFound = TryParseLabelFromLine(line);
if (labelFound == null)
return;

var (label, labelAddress) = labelFound.Value;
TryImportLabel(label, labelAddress);
}

private void TryImportLabel(IAnnotationLabel label, string labelAddress)
{
var validLabelChars = new Regex(@"^([a-zA-Z0-9_\-]*)$");
if (!validLabelChars.Match(label.Name).Success)
throw new InvalidDataException("invalid label name: " + label.Name);

var address = int.Parse(labelAddress, NumberStyles.HexNumber, null);
if (!newLabels.ContainsKey(address))
{
newLabels.Add(address, label);
}
else
{
// Update empty label properties instead of overwriting the entire object
// if there are multiple definitions (like from BSNES or handmade CSV)
var thisLabel = newLabels[address];

if (thisLabel.Name.IsEmpty())
thisLabel.Name = label.Name;

if (thisLabel.Comment.IsEmpty())
thisLabel.Comment = label.Comment;
}
}

protected abstract (IAnnotationLabel label, string labelAddress)? TryParseLabelFromLine(string line);
}

public static class LabelImporterUtils
{
// exception handling/line# stuff needs a little rework, messy.
public static void ImportLabelsFromCsv(this ILabelProvider labelProvider, string importFilename, bool replaceAll, out int errLine)
{
// could probably do this part more elegantly
errLine = 0;
LabelImporter? importer = null;
if (BsnesSymbolLabelImporter.IsFileCompatible(importFilename))
{
importer = new BsnesSymbolLabelImporter();
}
else if (LabelImporterCsv.IsFileCompatible(importFilename))
{
importer = new LabelImporterCsv();
}

if (importer == null)
{
throw new InvalidDataException($"No importer was found that can import a file named:\n'{importFilename}'");
}

var labelsFromFile = importer.ReadLabelsFromFile(importFilename);
if (importer.LastErrorLineNumber != -1)
{
errLine = importer.LastErrorLineNumber;
throw new InvalidDataException(
$"Error importing file:\n'{importFilename}'\nNear line#: {importer.LastErrorLineNumber}");
}

if (replaceAll)
labelProvider.DeleteAllLabels();

labelProvider.AppendLabels(labelsFromFile);
}
}
25 changes: 25 additions & 0 deletions Diz.Import/src/LabelImporterCsv.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
using Diz.Core.Interfaces;
using Diz.Core.model;
using Diz.Core.util;

namespace Diz.Import;

public class LabelImporterCsv : LabelImporter
{
public static bool IsFileCompatible(string importFilename) =>
importFilename.ToLower().EndsWith(".csv");

protected override (IAnnotationLabel label, string labelAddress)? TryParseLabelFromLine(string line)
{
// TODO: replace with something better. this is kind of a risky/fragile way to parse CSV lines.
// it won't deal with weirdness in the comments, quotes, etc.
Util.SplitOnFirstComma(line, out var labelAddress, out var remainder);
Util.SplitOnFirstComma(remainder, out var labelName, out var labelComment);
var label = new Label
{
Name = labelName.Trim(),
Comment = labelComment
};
return (label, labelAddress);
}
}
85 changes: 85 additions & 0 deletions Diz.Import/src/bsnes/BsnesSymbolLabelImporter.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
using Diz.Core.Interfaces;
using Diz.Core.model;

namespace Diz.Import.bsnes;

// there's a few different flavors of .sym.cpu files.
// one is here: https://github.com/BenjaminSchulte/fma-snes65816/blob/master/docs/symbols.adoc
// another is from the BSNES+ debugger, which is slightly different. try and support both here if we can, or, split out the parser if needed.

public class BsnesSymbolLabelImporter : LabelImporter
{
private string currentBsnesSection = "";

public override Dictionary<int, IAnnotationLabel> ReadLabelsFromFile(string importFilename)
{
currentBsnesSection = "";
return base.ReadLabelsFromFile(importFilename);
}

public static bool IsFileCompatible(string importFilename)
{
// Coming in from BSNES symbol map if it begins with the header
return importFilename.ToLower().EndsWith(".cpu.sym");

// here's another way to check if the file contents match.
// this signature can be present (but isn't always) present in some BSNES versions (it's not in BSNES+)
// the above filename extension check is probably sufficient for all of it though
// lines.Length > 0 && lines[0].StartsWith("#SNES65816");
}

protected override (IAnnotationLabel label, string labelAddress)? TryParseLabelFromLine(string line)
{
if (ShouldSkipLineBecauseCommentOrWhitespace(line))
return null;

// did we enter a new section? if so, note it, and move to next line
if (TryParseBsnesSection(line))
return null;

switch (currentBsnesSection)
{
case "[LABELS]":
case "[SYMBOL]":
{
var symbols = line.Trim().Split(' ');
var labelAddress = ParseSnesAddress(symbols[0]);
var label = new Label
{
Name = symbols[1].Replace(".", "_") // Replace dots which are valid in BSNES
};
return (label, labelAddress);
}
case "[COMMENT]":
{
var comments = line.Trim().Split(' ', 2);
var labelAddress = ParseSnesAddress(comments[0]);
var label = new Label
{
Comment = comments[1].Replace("\"", "") // Remove quotes
};
return (label, labelAddress);
}
}

return null;
}

private bool TryParseBsnesSection(string line)
{
// BSNES symbol files are multiple INI sections like "[symbol]"
// we only care about a few of them for Diztinguish
// if we hit a section header, consume it, keep going
if (!line.StartsWith('[') || !line.EndsWith(']'))
return false;

currentBsnesSection = line.ToUpper();
return true;
}

private static string ParseSnesAddress(string symbols) =>
symbols.Replace(":", "").ToUpper();

private static bool ShouldSkipLineBecauseCommentOrWhitespace(string line) =>
line.Trim().Length == 0 || line.StartsWith('#') || line.StartsWith(';');
}
Loading

0 comments on commit 73223cf

Please sign in to comment.