using Mesen.Config; using Mesen.Debugger.Labels; using Mesen.Interop; using Mesen.Utilities; using Mesen.Windows; using System; using System.Collections.Generic; using System.IO; using System.Linq; using System.Text; using System.Text.RegularExpressions; using System.Threading.Tasks; namespace Mesen.Debugger.Integration; public class PceasSymbolImporter : ISymbolProvider { private static Regex _definitionRegex = new Regex(@"^([0-9a-fA-F]{8}) ([^\s]*)", RegexOptions.Compiled); private static Regex _symbolRegex = new Regex(@"^([0-9a-fA-F]{2,4})[:]{0,1}([0-9a-fA-F]{4}) ([0-9a-fA-F]{8}) ([0-9a-fA-F]{4}):([0-9a-fA-F]{8}):([0-9a-fA-F]{2}) ([^\s]*)", RegexOptions.Compiled); private static Regex _fileRegex = new Regex(@"^([0-9a-fA-F]{4}) (.*)", RegexOptions.Compiled); private static Regex _filePathRegex = new Regex(@"^(""([^;""]*)""\s*;{0,1}\s*(.*))|(.*)", RegexOptions.Compiled); private static Regex _srcMappingRegex = new Regex(@"^([0-9a-fA-F]{2}):([0-9a-fA-F]{4}) ([0-9a-fA-F]{8}) ([0-9a-fA-F]{4}):([0-9a-fA-F]{8}):([0-9a-fA-F]{2})", RegexOptions.Compiled); private Dictionary _sourceFiles = new(); private Dictionary _addressByLine = new(); private Dictionary _linesByAddress = new(); private List _symbols = new(); private RomFormat _format; public DateTime SymbolFileStamp { get; private set; } public string SymbolPath { get; private set; } = ""; public List SourceFiles { get { return _sourceFiles.Values.ToList(); } } public PceasSymbolImporter() { _format = EmuApi.GetRomInfo().Format; } public static bool IsValidFile(string content) { return content.Contains("; Generated by PCEAS"); } public AddressInfo? GetLineAddress(SourceFileInfo file, int lineIndex) { AddressInfo address; if(_addressByLine.TryGetValue(file.Name.ToString() + "_" + lineIndex.ToString(), out address)) { return address; } return null; } public AddressInfo? GetLineEndAddress(SourceFileInfo file, int lineIndex) { return null; } public string GetSourceCodeLine(int prgRomAddress) { throw new NotImplementedException(); } public SourceCodeLocation? GetSourceCodeLineInfo(AddressInfo address) { string key = address.Type.ToString() + address.Address.ToString(); SourceCodeLocation location; if(_linesByAddress.TryGetValue(key, out location)) { return location; } return null; } public SourceSymbol? GetSymbol(string word, int scopeStart, int scopeEnd) { return InternalGetSymbol(word, scopeStart, scopeEnd, true); } private SourceSymbol? InternalGetSymbol(string word, int scopeStart, int scopeEnd, bool retry) { foreach(SymbolInfo symbol in _symbols) { if(symbol.Name == word) { //TODOv2 SCOPE return symbol.SourceSymbol; } } if(retry) { //Try again with an underscore prefix (because C symbols are usually exported with one) return InternalGetSymbol("_" + word, scopeStart, scopeEnd, false); } return null; } public AddressInfo? GetSymbolAddressInfo(SourceSymbol symbol) { if(symbol.InternalSymbol is SymbolInfo dbgSymbol) { return dbgSymbol.Address; } return null; } public SourceCodeLocation? GetSymbolDefinition(SourceSymbol symbol) { return null; } public List GetSymbols() { return _symbols.Select(s => s.SourceSymbol).ToList(); } public int GetSymbolSize(SourceSymbol srcSymbol) { return 1; } private string[] ProcessSourceFile(string filename, string comment, string[] data) { int ignoreColumnCount = 0; Regex regex = new Regex("IgnoreColumns=(\\d*)"); Match m = regex.Match(comment); if(m.Success) { int.TryParse(m.Groups[1].Value, out ignoreColumnCount); } if(ignoreColumnCount > 0) { return data.Select(line => { if(line.StartsWith('#')) { //Columns that start with # in listing are converted to comments return ";" + line; } else if(line.Length >= ignoreColumnCount) { //Ignore first X columns (contains address, byte code, etc.) return line.Substring(ignoreColumnCount); } return line; }).ToArray(); } else { return data; } } private AddressInfo GetLabelAddress(int bank, int addr) { if(bank == 0xF8 && bank <= 0xFB) { return new AddressInfo() { Address = (bank - 0xF8) * 0x2000 + (addr & 0x1FFF), Type = MemoryType.PceWorkRam }; } else if(bank == 0xF7) { return new AddressInfo() { Address = (addr & 0x1FFF), Type = MemoryType.PceSaveRam }; } else if(bank == 0xFF) { return new AddressInfo() { Address = (addr & 0x1FFF), Type = MemoryType.PceMemory }; } else if(_format == RomFormat.PceCdRom && (bank >= 0x68 && bank <= 0x7F)) { return new AddressInfo() { Address = (bank - 0x68) * 0x2000 + (addr & 0x1FFF), Type = MemoryType.PceCardRam }; } else if(_format == RomFormat.PceCdRom && (bank >= 0x80 && bank <= 0x87)) { return new AddressInfo() { Address = (bank - 0x80) * 0x2000 + (addr & 0x1FFF), Type = MemoryType.PceCdromRam }; } else if(bank > 0xFF) { return new AddressInfo() { Address = (bank - 0x80) * 0x2000 + (addr & 0x1FFF), Type = MemoryType.PcePrgRom }; } else if(bank < 0x80) { return new AddressInfo() { Address = bank * 0x2000 + (addr & 0x1FFF), Type = MemoryType.PcePrgRom }; } return default; } public void Import(string path, bool showResult) { SymbolFileStamp = File.GetLastWriteTime(path); string basePath = Path.GetDirectoryName(path) ?? ""; SymbolPath = basePath; string[] lines = File.ReadAllLines(path); Dictionary labels = new Dictionary(); byte[] cdlData = new byte[DebugApi.GetMemorySize(MemoryType.PcePrgRom)]; int errorCount = 0; for(int i = 0; i < lines.Length; i++) { string str = lines[i].Trim(); if(str == "[symbols]") { for(; i < lines.Length; i++) { str = lines[i].Trim(); int commentStart = str.IndexOf(';'); if(commentStart >= 0) { str = str.Substring(0, commentStart); } if(str.Length > 0) { Match m = _symbolRegex.Match(str); if(m.Success) { int bank = Int32.Parse(m.Groups[1].Value, System.Globalization.NumberStyles.HexNumber); int addr = Int32.Parse(m.Groups[2].Value, System.Globalization.NumberStyles.HexNumber); long lengthFlags = long.Parse(m.Groups[3].Value, System.Globalization.NumberStyles.HexNumber); uint length = (uint)(lengthFlags & ~0xC0000000); if((lengthFlags & 0x80000000) != 0) { //Code, always use length = 1 length = 1; } string originalLabel = m.Groups[7].Value; string label = LabelManager.InvalidLabelRegex.Replace(originalLabel, "_"); if(!LabelManager.LabelRegex.IsMatch(label)) { //ignore labels that don't respect the label naming restrictions errorCount++; continue; } AddressInfo absAddr = GetLabelAddress(bank, addr); if(absAddr.Address < 0) { errorCount++; continue; } SymbolInfo symbol = new(originalLabel, absAddr); _symbols.Add(symbol); string orgLabel = label; int j = 1; while(labels.ContainsKey(label)) { label = orgLabel + j.ToString(); j++; } if(ConfigManager.Config.Debug.Integration.IsMemoryTypeImportEnabled(absAddr.Type)) { labels[label] = new CodeLabel() { Label = label, Address = (UInt32)absAddr.Address, MemoryType = absAddr.Type, Comment = "", Flags = CodeLabelFlags.None, Length = length <= 0 ? 1 : length }; } } } else { break; } } } else if(str == "[definitions]") { for(; i < lines.Length; i++) { str = lines[i].Trim(); if(str.Length > 0) { Match m = _definitionRegex.Match(str); if(m.Success) { int value = Int32.Parse(m.Groups[1].Value, System.Globalization.NumberStyles.HexNumber); string label = m.Groups[2].Value; SymbolInfo symbol = new(label, value); _symbols.Add(symbol); } } else { break; } } } else if(str == "[source-files]") { for(; i < lines.Length; i++) { if(lines[i].Length > 0) { Match m = _fileRegex.Match(lines[i]); if(m.Success) { int fileId = Int32.Parse(m.Groups[1].Value, System.Globalization.NumberStyles.HexNumber); Match fileMatch = _filePathRegex.Match(m.Groups[2].Value); if(fileMatch.Success) { string filePath = fileMatch.Groups[2].Success ? fileMatch.Groups[2].Value : fileMatch.Groups[4].Value; string comment = fileMatch.Groups[3].Value; string fullPath; if(Path.IsPathFullyQualified(filePath)) { fullPath = filePath; } else { string? srcBasePath = basePath; fullPath = Path.Combine(srcBasePath, filePath); while(!File.Exists(fullPath)) { //Go back up folder structure to attempt to find the file string oldPath = srcBasePath; srcBasePath = Path.GetDirectoryName(srcBasePath); if(srcBasePath == null || srcBasePath == oldPath) { break; } fullPath = Path.Combine(srcBasePath, filePath); } } string[] fileData = this.ProcessSourceFile(filePath, comment, File.Exists(fullPath) ? File.ReadAllLines(fullPath) : Array.Empty()); string ext = Path.GetExtension(filePath).ToLower(); bool isAsm = ext != ".c" && ext != ".h"; _sourceFiles[fileId] = new SourceFileInfo(filePath, isAsm, new PceasSourceFile() { Data = fileData }); } } } else { break; } } } else if(str == "[bank-to-source]") { for(; i < lines.Length; i++) { if(lines[i].Length > 0) { Match m = _srcMappingRegex.Match(lines[i]); if(m.Success) { int bank = Int32.Parse(m.Groups[1].Value, System.Globalization.NumberStyles.HexNumber); int addr = Int32.Parse(m.Groups[2].Value, System.Globalization.NumberStyles.HexNumber); int fileId = Int32.Parse(m.Groups[4].Value, System.Globalization.NumberStyles.HexNumber); int lineNumber = Int32.Parse(m.Groups[5].Value, System.Globalization.NumberStyles.HexNumber); if(lineNumber > 0) { lineNumber--; } int columnNumber = Int32.Parse(m.Groups[6].Value, System.Globalization.NumberStyles.HexNumber); AddressInfo absAddr = GetLabelAddress(bank, addr); if(absAddr.Address >= 0) { long lengthFlags = long.Parse(m.Groups[3].Value, System.Globalization.NumberStyles.HexNumber); int length = (int)(lengthFlags & ~0xC0000000); if(absAddr.Type == MemoryType.PcePrgRom) { //Build CDL data based on the extra flags present in the mappings if((lengthFlags & 0x40000000) != 0) { cdlData[absAddr.Address] |= (byte)CdlFlags.SubEntryPoint; } byte cdlFlags = (lengthFlags & 0x80000000) != 0 ? (byte)CdlFlags.Code : (byte)CdlFlags.Data; for(long j = 0; j < length; j++) { if(absAddr.Address + j < cdlData.Length) { cdlData[absAddr.Address + j] |= cdlFlags; } else { break; } } } _addressByLine[_sourceFiles[fileId].Name + "_" + lineNumber.ToString()] = absAddr; SourceCodeLocation loc = new SourceCodeLocation(_sourceFiles[fileId], lineNumber); for(int j = 0; j < length; j++) { //Map this line of code to every address it represents _linesByAddress[absAddr.Type.ToString() + (absAddr.Address + j).ToString()] = loc; } } } } else { break; } } } } DebugApi.SetCdlData(MemoryType.PcePrgRom, cdlData, cdlData.Length); LabelManager.SetLabels(labels.Values, true); if(showResult) { if(errorCount > 0) { MesenMsgBox.Show(null, "ImportLabelsWithErrors", MessageBoxButtons.OK, MessageBoxIcon.Warning, labels.Count.ToString(), errorCount.ToString()); } else { MesenMsgBox.Show(null, "ImportLabels", MessageBoxButtons.OK, MessageBoxIcon.Info, labels.Count.ToString()); } } } class PceasSourceFile : IFileDataProvider { public string[] Data { get; init; } = Array.Empty(); } private readonly struct SymbolInfo { public string Name { get; } public int? Value { get; } = null; public AddressInfo? Address { get; } = null; public SourceSymbol SourceSymbol { get => new SourceSymbol(Name, Address?.Address ?? Value, this); } public SymbolInfo(string name, AddressInfo address) { Name = name; Address = address; } public SymbolInfo(string name, int value) { Name = name; Value = value; } } }