diff --git a/.github/workflows/dotnet.yml b/.github/workflows/dotnet.yml index 2a8677e4..59ec27d4 100644 --- a/.github/workflows/dotnet.yml +++ b/.github/workflows/dotnet.yml @@ -1,12 +1,13 @@ -name: .NET Windows Desktop +name: .NET 6 Win Desktop -on: -- push -- pull_request +on: [ push, pull_request ] jobs: + build: + runs-on: windows-latest + steps: - name: Inject slug/short variables uses: rlespinasse/github-slug-action@v3.x @@ -21,24 +22,28 @@ jobs: - name: Print version run: echo "version ${{ steps.ver.outputs.ver }}" - + - name: Checkout - uses: actions/checkout@v1 + uses: actions/checkout@v2 - name: Setup dotnet uses: actions/setup-dotnet@v1 with: - dotnet-version: 5.0.x + dotnet-version: 6.0.x - - name: Nuget (restore from cache) - uses: actions/cache@v1 - id: cache - with: - path: ~/.nuget/packages - key: ${{ runner.os }}-nuget-${{ hashFiles('**/packages.lock.json') }} - - - name: NuGet (Restore from scratch if needed) - if: steps.cache.outputs.cache-hit != 'true' +# not currently working consistently, don't cache +# - name: Nuget (restore from cache) +# uses: actions/cache@v1 +# id: cache +# with: +# path: ~/.nuget/packages +# key: ${{ runner.os }}-nuget-${{ hashFiles('**/packages.lock.json') }} +# +# - name: NuGet (Restore from scratch if needed) +# if: steps.cache.outputs.cache-hit != 'true' +# run: dotnet restore --locked-mode + + - name: NuGet (Restore always from scratch) run: dotnet restore --locked-mode - name: Build @@ -52,7 +57,7 @@ jobs: - uses: papeloto/action-zip@v1 with: - files: DiztinGUIsh/bin/Release/net48/publish + files: DiztinGUIsh\bin\Release\net6.0-windows7.0\publish\ dest: ${{ steps.ver.outputs.ver }}.zip - name: Upload Artifact diff --git a/.gitignore b/.gitignore index 496664a1..5520eaa3 100644 --- a/.gitignore +++ b/.gitignore @@ -175,7 +175,7 @@ publish/ *.azurePubxml # Note: Comment the next line if you want to checkin your web deploy settings, # but database connection strings (with potential passwords) will be unencrypted -# *.pubxml +#*.pubxml # Diztinguish: we chose to do this. *.publishproj # Microsoft Azure Web App publish settings. Comment the next line if you want to @@ -337,3 +337,4 @@ ASALocalRun/ # Local History for Visual Studio .localhistory/ +/MigrationBackup diff --git a/Directory.Build.targets b/Directory.Build.targets new file mode 100644 index 00000000..9fa08373 --- /dev/null +++ b/Directory.Build.targets @@ -0,0 +1,8 @@ + + + $(DefineConstants);DIZ_3_BRANCH + + + $(DefineConstants);DEBUG;TRACE + + \ No newline at end of file diff --git a/Diz.Controllers/Diz.Controllers.csproj b/Diz.Controllers/Diz.Controllers.csproj new file mode 100644 index 00000000..12ff29ab --- /dev/null +++ b/Diz.Controllers/Diz.Controllers.csproj @@ -0,0 +1,24 @@ + + + + net6.0 + true + true + true + 3.0.0 + + + + + + + + + + 2.2.0 + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + diff --git a/Diz.Controllers/packages.lock.json b/Diz.Controllers/packages.lock.json new file mode 100644 index 00000000..98aaebc0 --- /dev/null +++ b/Diz.Controllers/packages.lock.json @@ -0,0 +1,293 @@ +{ + "version": 1, + "dependencies": { + "net6.0": { + "GitInfo": { + "type": "Direct", + "requested": "[2.2.0, )", + "resolved": "2.2.0", + "contentHash": "SKTmjt40sYqoDB3S7HFVUToUN/vl6Zrt+PuSHdjbQDlcBx0JAZBdS7BXVbVKyy+0iaaF3FBd4ZxspIlKQCC9Lw==" + }, + "ExtendedXmlSerializer": { + "type": "Transitive", + "resolved": "3.7.1", + "contentHash": "G1RQYBhFKZKiLy/mkMtpE1t/GbY3CL/TwUGSSPZw1WmMQT4lT0wHx4ml7anUDfTCnNO0RGA1gTZJUBJYUjgldg==", + "dependencies": { + "LightInject": "6.3.5", + "NReco.LambdaParser": "1.0.12", + "Sprache": "2.3.1", + "System.Collections.Immutable": "1.7.1", + "System.Interactive": "4.1.1", + "System.Runtime.CompilerServices.Unsafe": "4.7.1", + "System.ValueTuple": "4.5.0" + } + }, + "FluentValidation": { + "type": "Transitive", + "resolved": "10.1.0", + "contentHash": "RxhhfY9IcEY2qUMYjoUxegInbuE5Bwll7dVLsXpiJf25g0ztmzUK+HHqtPcub1caPemhMJsC+NwjHei+NgAkvA==" + }, + "JetBrains.Annotations": { + "type": "Transitive", + "resolved": "2021.1.0", + "contentHash": "n9JSw5Z+F+6gp9vSv4aLH6p/bx3GAYA6FZVq1wJq/TJySv/kPgFKLGFeS7A8Xa5X4/GWorh5gd43yjamUgnBNA==" + }, + "JetBrains.Profiler.Api": { + "type": "Transitive", + "resolved": "1.1.7", + "contentHash": "scN2nYNc7yGUNS3wF6fwuoAKZwWW2aMBHDqeVENrpJjRFKzEOWWrOGzNfr/WZft9+XzIz+ajshrx/dzz8Xp5Ig==" + }, + "JetBrains.Profiler.SelfApi": { + "type": "Transitive", + "resolved": "2.1.1", + "contentHash": "wvMMwi4RquT4bBELUuAKLylqrp3d0/EN7qnRTBPs98NZVE2pTDeIr6e30ZuNLhPrgDot1hMsAo/ewKhjq8gIlg==", + "dependencies": { + "JetBrains.Profiler.Api": "1.1.7" + } + }, + "LightInject": { + "type": "Transitive", + "resolved": "6.3.5", + "contentHash": "ri0PjphQnzYeQIXbJj4dCe7wX6/3ZwcKlJO2uz5Y8MjoTAVkTx2V5gDLsVdc1a/6h2oQbapUVrJ74dtU9XsjPQ==" + }, + "Microsoft.NETCore.Platforms": { + "type": "Transitive", + "resolved": "5.0.0", + "contentHash": "VyPlqzH2wavqquTcYpkIIAQ6WdenuKoFN0BdYBbCWsclXacSOHNQn66Gt4z5NBqEYW0FAPm5rlvki9ZiCij5xQ==" + }, + "Microsoft.NETCore.Targets": { + "type": "Transitive", + "resolved": "1.1.3", + "contentHash": "3Wrmi0kJDzClwAC+iBdUBpEKmEle8FQNsCs77fkiOIw/9oYA07bL1EZNX0kQ2OMN3xpwvl0vAtOCYY3ndDNlhQ==" + }, + "NReco.LambdaParser": { + "type": "Transitive", + "resolved": "1.0.12", + "contentHash": "f1nye8fMJCYfJeR+Qb7qlcvC8et4Y4TLf2GLfaFoeISgI+yisaJkbNSGfKCpFBNeVF/iPDkyFmr4J28dv342/Q==" + }, + "SharpZipLib": { + "type": "Transitive", + "resolved": "1.3.2", + "contentHash": "WSdeDReL8eugMCw5BH/tFAZpgR+YsYMwm6kIvqg3J8LbfRjbbebmEzn63AbEveqyMOljBO68g6tCCv165wMkSg==" + }, + "Sprache": { + "type": "Transitive", + "resolved": "2.3.1", + "contentHash": "Q+mXeiTxiUYG3lKYF6TS82/SyB4F2613Q1yXTMwg4jWGHEEVC3yrzHtNcI4B3qnDI0+eJsezGJ0V+cToUytHWw==", + "dependencies": { + "System.Globalization": "4.3.0", + "System.Linq": "4.3.0", + "System.Private.Uri": "4.3.2", + "System.Runtime": "4.3.0", + "System.Text.RegularExpressions": "4.3.0" + } + }, + "System.Collections": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "3Dcj85/TBdVpL5Zr+gEEBUuFe2icOnLalmEh9hfck1PTYbbyWuZgh4fmm2ysCLTrqLQw6t3TgTyJ+VLp+Qb+Lw==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.1.0", + "Microsoft.NETCore.Targets": "1.1.0", + "System.Runtime": "4.3.0" + } + }, + "System.Collections.Immutable": { + "type": "Transitive", + "resolved": "1.7.1", + "contentHash": "B43Zsz5EfMwyEbnObwRxW5u85fzJma3lrDeGcSAV1qkhSRTNY5uXAByTn9h9ddNdhM+4/YoLc/CI43umjwIl9Q==" + }, + "System.Diagnostics.Debug": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "ZUhUOdqmaG5Jk3Xdb8xi5kIyQYAA4PnTNlHx1mu9ZY3qv4ELIdKbnL/akbGaKi2RnNUWaZsAs31rvzFdewTj2g==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.1.0", + "Microsoft.NETCore.Targets": "1.1.0", + "System.Runtime": "4.3.0" + } + }, + "System.Diagnostics.Tracing": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "rswfv0f/Cqkh78rA5S8eN8Neocz234+emGCtTF3lxPY96F+mmmUen6tbn0glN6PMvlKQb9bPAY5e9u7fgPTkKw==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.1.0", + "Microsoft.NETCore.Targets": "1.1.0", + "System.Runtime": "4.3.0" + } + }, + "System.Globalization": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "kYdVd2f2PAdFGblzFswE4hkNANJBKRmsfa2X5LG2AcWE1c7/4t0pYae1L8vfZ5xvE2nK/R9JprtToA61OSHWIg==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.1.0", + "Microsoft.NETCore.Targets": "1.1.0", + "System.Runtime": "4.3.0" + } + }, + "System.Interactive": { + "type": "Transitive", + "resolved": "4.1.1", + "contentHash": "pL6aZm6Im/KSpQADvJMr3sK5d/Q/iQbC4TQ2zQfiDZ+qzAskuMgrtgnCheAwLX1vC5YDTXpXnVjeMzfxUZzfjQ==" + }, + "System.IO": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "3qjaHvxQPDpSOYICjUoTsmoq5u6QJAFRUITgeT/4gqkF1bajbSmb1kwSxEA8AHlofqgcKJcM8udgieRNhaJ5Cg==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.1.0", + "Microsoft.NETCore.Targets": "1.1.0", + "System.Runtime": "4.3.0", + "System.Text.Encoding": "4.3.0", + "System.Threading.Tasks": "4.3.0" + } + }, + "System.Linq": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "5DbqIUpsDp0dFftytzuMmc0oeMdQwjcP/EWxsksIz/w1TcFRkZ3yKKz0PqiYFMmEwPSWw+qNVqD7PJ889JzHbw==", + "dependencies": { + "System.Collections": "4.3.0", + "System.Diagnostics.Debug": "4.3.0", + "System.Resources.ResourceManager": "4.3.0", + "System.Runtime": "4.3.0", + "System.Runtime.Extensions": "4.3.0" + } + }, + "System.Private.Uri": { + "type": "Transitive", + "resolved": "4.3.2", + "contentHash": "o1+7RJnu3Ik3PazR7Z7tJhjPdE000Eq2KGLLWhqJJKXj04wrS8lwb1OFtDF9jzXXADhUuZNJZlPc98uwwqmpFA==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.1.1", + "Microsoft.NETCore.Targets": "1.1.3" + } + }, + "System.Reflection": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "KMiAFoW7MfJGa9nDFNcfu+FpEdiHpWgTcS2HdMpDvt9saK3y/G4GwprPyzqjFH9NTaGPQeWNHU+iDlDILj96aQ==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.1.0", + "Microsoft.NETCore.Targets": "1.1.0", + "System.IO": "4.3.0", + "System.Reflection.Primitives": "4.3.0", + "System.Runtime": "4.3.0" + } + }, + "System.Reflection.Primitives": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "5RXItQz5As4xN2/YUDxdpsEkMhvw3e6aNveFXUn4Hl/udNTCNhnKp8lT9fnc3MhvGKh1baak5CovpuQUXHAlIA==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.1.0", + "Microsoft.NETCore.Targets": "1.1.0", + "System.Runtime": "4.3.0" + } + }, + "System.Resources.Extensions": { + "type": "Transitive", + "resolved": "5.0.0", + "contentHash": "KBiqY/+W6hWVwucHBHO76JkgBGyawlxCcP946MhK8dNjalaBgZkyHaux8ko58PENTqQHHjoDa7cfIkAchfJPPA==" + }, + "System.Resources.ResourceManager": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "/zrcPkkWdZmI4F92gL/TPumP98AVDu/Wxr3CSJGQQ+XN6wbRZcyfSKVoPo17ilb3iOr0cCRqJInGwNMolqhS8A==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.1.0", + "Microsoft.NETCore.Targets": "1.1.0", + "System.Globalization": "4.3.0", + "System.Reflection": "4.3.0", + "System.Runtime": "4.3.0" + } + }, + "System.Runtime": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "JufQi0vPQ0xGnAczR13AUFglDyVYt4Kqnz1AZaiKZ5+GICq0/1MH/mO/eAJHt/mHW1zjKBJd7kV26SrxddAhiw==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.1.0", + "Microsoft.NETCore.Targets": "1.1.0" + } + }, + "System.Runtime.CompilerServices.Unsafe": { + "type": "Transitive", + "resolved": "4.7.1", + "contentHash": "zOHkQmzPCn5zm/BH+cxC1XbUS3P4Yoi3xzW7eRgVpDR2tPGSzyMZ17Ig1iRkfJuY0nhxkQQde8pgePNiA7z7TQ==" + }, + "System.Runtime.Extensions": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "guW0uK0fn5fcJJ1tJVXYd7/1h5F+pea1r7FLSOz/f8vPEqbR2ZAknuRDvTQ8PzAilDveOxNjSfr0CHfIQfFk8g==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.1.0", + "Microsoft.NETCore.Targets": "1.1.0", + "System.Runtime": "4.3.0" + } + }, + "System.Text.Encoding": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "BiIg+KWaSDOITze6jGQynxg64naAPtqGHBwDrLaCtixsa5bKiR8dpPOHA7ge3C0JJQizJE+sfkz1wV+BAKAYZw==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.1.0", + "Microsoft.NETCore.Targets": "1.1.0", + "System.Runtime": "4.3.0" + } + }, + "System.Text.Encoding.CodePages": { + "type": "Transitive", + "resolved": "5.0.0", + "contentHash": "NyscU59xX6Uo91qvhOs2Ccho3AR2TnZPomo1Z0K6YpyztBPM/A5VbkzOO19sy3A3i1TtEnTxA7bCe3Us+r5MWg==", + "dependencies": { + "Microsoft.NETCore.Platforms": "5.0.0" + } + }, + "System.Text.RegularExpressions": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "RpT2DA+L660cBt1FssIE9CAGpLFdFPuheB7pLpKpn6ZXNby7jDERe8Ua/Ne2xGiwLVG2JOqziiaVCGDon5sKFA==", + "dependencies": { + "System.Runtime": "4.3.0" + } + }, + "System.Threading.Tasks": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "LbSxKEdOUhVe8BezB/9uOGGppt+nZf6e1VFyw6v3DN6lqitm0OSn2uXMOdtP0M3W4iMcqcivm2J6UgqiwwnXiA==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.1.0", + "Microsoft.NETCore.Targets": "1.1.0", + "System.Runtime": "4.3.0" + } + }, + "System.ValueTuple": { + "type": "Transitive", + "resolved": "4.5.0", + "contentHash": "okurQJO6NRE/apDIP23ajJ0hpiNmJ+f0BwOlB/cSqTLQlw5upkf+5+96+iG2Jw40G1fCVCyPz/FhIABUjMR+RQ==" + }, + "diz.core": { + "type": "Project", + "dependencies": { + "ExtendedXmlSerializer": "3.7.1", + "FluentValidation": "10.1.0", + "JetBrains.Annotations": "2021.1.0", + "JetBrains.Profiler.SelfApi": "2.1.1", + "SharpZipLib": "1.3.2", + "System.Diagnostics.Tracing": "4.3.0", + "System.Resources.Extensions": "5.0.0", + "System.Text.Encoding.CodePages": "5.0.0" + } + }, + "diz.logwriter": { + "type": "Project", + "dependencies": { + "Diz.Core": "3.0.0" + } + } + } + } +} \ No newline at end of file diff --git a/Diz.Controllers/src/controllers/ByteViewerController.cs b/Diz.Controllers/src/controllers/ByteViewerController.cs new file mode 100644 index 00000000..87ea215a --- /dev/null +++ b/Diz.Controllers/src/controllers/ByteViewerController.cs @@ -0,0 +1,277 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel; +using System.Linq; +using System.Runtime.CompilerServices; +using Diz.Controllers.interfaces; +using Diz.Core.datasubset; +using Diz.Core.model.byteSources; +using Diz.Core.model.snes; +using Diz.Core.util; +using JetBrains.Annotations; +using LightInject; + +// this class and structure is a mess because it was just enough refactoring to +// get a lot of this logic out of the implementation classes. it needs further iteration and simplification +// +// 1) TODO: need to catch notifychanged from labels and comments or else updates won't propagate + +namespace Diz.Controllers.controllers +{ + public class RomByteDataBindingGridController : RomByteDataBindingController + { + public void BeginEditingLabel() + { + ViewGrid.BeginEditingSelectionLabel(); + } + + public void BeginEditingComment() + { + ViewGrid.BeginEditingSelectionComment(); + } + } + + public class RomByteDataBindingController : ByteDataBindingGridController + where + TRow : class, IGridRow + { + protected override IEnumerable GetByteItems() + { + // TODO: note: underlying data source could be a sparse set, so, enumerating here we + // may have to deal with null bytes or gaps in the sequence. + + // right now, return everything 1:1. + // in the future, this would be the place to make a filtered or subset of this list. + return Data.RomByteSource.Bytes; + } + + #region Filters + + private bool filterShowOpcodesOnly; + public bool FilterShowOpcodesOnly + { + get => filterShowOpcodesOnly; + set + { + filterShowOpcodesOnly = value; + UpdateFilters(); + } + } + + private void UpdateFilters() + { + /*if (ViewGrid?.DataSource == null) + return; + + ViewGrid.DataSource.RemoveFilter(); + + if (FilterShowOpcodesOnly) + ViewGrid.DataSource.Filter = new PredicateItemFilter(IsRomByteOpcode);*/ + } + + // private static bool IsRomByteOpcode(IDataGridRow romByteRow) + // { + // return romByteRow.ByteEntry.TypeFlag == FlagType.Opcode; + // } + + #endregion + } + + public abstract class ByteDataBindingGridController : + DataBindingController, + IBytesGridDataController, + INotifyPropertyChangedExt + + // TODO: eventually, we should try and get rid of "ByteOffsetData" here to make this more generic. + where TItem : ByteEntry + + where TRow : class, IGridRow + { + private DataSubsetWithSelection dataSubset; + + // stores just the current Rom bytes in view (subset of larger data source) + public DataSubsetWithSelection DataSubset + { + get => dataSubset; + private set => this.SetField(ref dataSubset, value); + } + + public IBytesGridViewer ViewGrid + { + get => View as IBytesGridViewer; + set + { + // this is getting a bit messy, rethink it. + + if (ViewGrid != null) + ViewGrid.SelectedOffsetChanged -= ViewGridOnSelectedOffsetChanged; + + this.SetField(ref view, value); + View = view; + + if (ViewGrid != null) + ViewGrid.SelectedOffsetChanged += ViewGridOnSelectedOffsetChanged; + } + } + + private void ViewGridOnSelectedOffsetChanged(object sender, IBytesGridViewer.SelectedOffsetChangedEventArgs e) + { + if (e.RowIndex == -1) + return; + + DataSubset?.SelectRow(e.RowIndex); + } + + protected void CreateDataSubset(List dataBindSource) + { + // TODO: create our class via container and use property-based dependency injection + var rowLoader = Service.Container.GetInstance>(); + + rowLoader.Data = Data; + rowLoader.View = ViewGrid; + + DataSubset = new() + { + Items = dataBindSource, + RowLoader = rowLoader + }; + } + + protected override void DataBind() + { + if (ViewGrid == null || Data == null) + return; + + if (DataSubset != null) + DataSubset.PropertyChanged -= RowsOnPropertyChanged; + + var dataBindSource = GetDataSourceForBind(); + + CreateDataSubset(dataBindSource); + + DataSubset.PropertyChanged += RowsOnPropertyChanged; + + DataSubset.Items = dataBindSource; + + DataSubset.StartingRowLargeIndex = 0; + MatchCachedRowsToView(); + + DataSubset.SelectedLargeIndex = 0; + + ViewGrid.DataSource = dataBindSource; + } + + public void MatchCachedRowsToView() + { + DataSubset.RowCount = ViewGrid.TargetNumberOfRowsToShow; + } + + private void RowsOnPropertyChanged(object sender, PropertyChangedEventArgs e) + { + PropertyChanged?.Invoke(sender, e); + + if (e.PropertyName == nameof(DataSubsetWithSelection.SelectedLargeIndex)) + { + OnSelectedRowChanged(); + } + } + + private void OnSelectedRowChanged() + { + // NOP, currently. views are handling this themselves. + } + + private List GetDataSourceForBind() + { + if (ViewGrid == null || Data == null) + return null; + + return GetByteItems().ToList(); + } + + // return which RomBytes we are interested in. + // NOTE: this can be a subset of all RomBytes + // (i.e. just the SPC700 section of a ROM, + // or just the bytes marked as Instructions, etc) + // + // GetByteItems() is a SUBSET of the entire available Rom. + // DataSubset will show an additional smaller subset of GetByteItems() + // + // example: + + // - all possible data: Rom: 4MB of bytes, read from disk. + // - first subset of the above list: GetByteItems() + // i.e. can return stuff like a filtered and sorted list of any bytes in the Rom + // like, just the bytes marked as graphics or something. + // - subset of GetByteItems() i.e. DataSubset in a table displaying part of GetByteItems() + // i.e. this is what is actually showing up on the screen + // + // It's a little indirect, but it's extremely flexible. + protected abstract IEnumerable GetByteItems(); + + public event PropertyChangedEventHandler PropertyChanged; + + [NotifyPropertyChangedInvocator] + public void OnPropertyChanged([CallerMemberName] string propertyName = null) + { + PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)); + } + } + + + public abstract class DataBindingController : DataController + { + protected IViewer view; + + public override IViewer View + { + get => view; + set + { + view = value; + DataBind(); + } + } + + private Data data; + public override Data Data + { + get => data; + set + { + data = value; + DataBind(); + } + } + + protected abstract void DataBind(); + } + + public abstract class DataController : IDataController + { + private IViewer view; + + public virtual Data Data { get; set; } + public event EventHandler Closed; + + public virtual IViewer View + { + get => view; + set + { + if (view is IFormViewer formViewerBefore) + formViewerBefore.Closed -= OnClosed; + + view = value; + + if (view is IFormViewer formViewerAfter) + formViewerAfter.Closed += OnClosed; + } + } + + private void OnClosed(object sender, EventArgs e) + { + Closed?.Invoke(this, e); + } + } +} \ No newline at end of file diff --git a/Diz.Controllers/src/controllers/GlobalViewControllers.cs b/Diz.Controllers/src/controllers/GlobalViewControllers.cs new file mode 100644 index 00000000..38e5009e --- /dev/null +++ b/Diz.Controllers/src/controllers/GlobalViewControllers.cs @@ -0,0 +1,80 @@ +using System; +using System.Collections.Generic; +using System.IO; +using Diz.Controllers.interfaces; +using Diz.Core.model; +using Diz.Core.util; +using LightInject; + +namespace Diz.Controllers.controllers +{ + public interface IControllersManager : IProjectOpenRequester, IRootControllerProvider + { + public event EventHandler AllFormsClosed; + } + + public interface IRootControllerProvider + { + void ShowNewStartForm(); + void ShowNewProjectEditorForm(Project project); + } + + public class ControllersManager : IControllersManager + { + public List Controllers { get; } = new(); + public event EventHandler AllFormsClosed; + public event EventHandler ProjectOpenRequested; + + private void TrackFormController(IFormController controller) + { + Controllers.Add(controller); + controller.Closed += OnControllerClosed; + } + + private void OnControllerClosed(object sender, EventArgs e) + { + Controllers.RemoveAll(c => ReferenceEquals(c, sender)); + if (Controllers.Count == 0) + AllFormsClosed?.Invoke(this, new EventArgs()); + } + + private void RegisterAndShowController(IFormController controller) + { + TrackFormController(controller); + controller.Show(); + } + + #region Public GUI methods, for opening new types of forms etc + + public void ShowNewProjectEditorForm(Project project) + { + if (project == null) + throw new InvalidDataException("project must not be null"); + + void BeforeFormShow(IMainFormController controller) => + controller.SetProject(project.Session?.ProjectFileName ?? "", project); + + ShowNewProjectEditorForm(BeforeFormShow); + } + + public void ShowNewStartForm() + { + var controller = Service.Container.GetInstance(); + + // controller.View.ProjectOpenRequested += (sender, e) => ProjectOpenRequested?.Invoke(sender, e); + if (ProjectOpenRequested != null) + controller.View.ProjectOpenRequested += ProjectOpenRequested; + + RegisterAndShowController(controller); + } + + public void ShowNewProjectEditorForm(Action beforeShow = null) + { + var controller = Service.Container.GetInstance(); + beforeShow?.Invoke(controller); + RegisterAndShowController(controller); + } + + #endregion + } +} \ No newline at end of file diff --git a/Diz.Controllers/src/controllers/IProjectView.cs b/Diz.Controllers/src/controllers/IProjectView.cs new file mode 100644 index 00000000..eec38171 --- /dev/null +++ b/Diz.Controllers/src/controllers/IProjectView.cs @@ -0,0 +1,43 @@ +using System; +using Diz.Controllers.interfaces; +using Diz.Core.commands; +using Diz.LogWriter; + +namespace Diz.Controllers.controllers +{ + public interface ILongRunningTaskHandler + { + public delegate void LongRunningTaskHandler(Action task, string description = null); + LongRunningTaskHandler TaskHandler { get; } + } + + public interface IProjectView : ILongRunningTaskHandler + { + void OnProjectOpenWarning(string warningMsg); + void OnProjectOpenFail(string errorMsg); + void OnExportFinished(LogCreatorOutput.OutputResult result); + + string AskToSelectNewRomFilename(string promptSubject, string promptText); + IImportRomDialogView GetImportView(); + + bool PromptHarshAutoStep(int offset, out int newOffset, out int count); + MarkCommand PromptMarkMany(int offset, int whichIndex); + void ShowOffsetOutOfRangeMsg(); + } + + public interface ISnesNavigation + { + public class HistoryArgs + { + public string Description { get; set; } + public string Position { get; set; } + } + + /// + /// Select a PC offset + /// + /// PC [not SNES] offset + /// if non-null, record this event in the project history + void SelectOffset(int pcOffset, HistoryArgs historyArgs = null); + } +} diff --git a/DiztinGUIsh/window/dialog/ImportROMDialogController.cs b/Diz.Controllers/src/controllers/ImportROMDialogController.cs similarity index 83% rename from DiztinGUIsh/window/dialog/ImportROMDialogController.cs rename to Diz.Controllers/src/controllers/ImportROMDialogController.cs index 4e447566..6cd988f5 100644 --- a/DiztinGUIsh/window/dialog/ImportROMDialogController.cs +++ b/Diz.Controllers/src/controllers/ImportROMDialogController.cs @@ -1,19 +1,11 @@ using System.ComponentModel; using System.Diagnostics; +using Diz.Controllers.interfaces; using Diz.Core.serialization; using Diz.Core.util; -namespace DiztinGUIsh.window.dialog +namespace Diz.Controllers.controllers { - public interface IImportRomDialogView - { - bool PromptToConfirmAction(string msg); - bool ShowAndWaitForUserToConfirmSettings(); - ImportRomDialogController Controller { get; set; } - bool GetVectorValue(int i, int j); - void RefreshUi(); - } - public class ImportRomDialogController { public IImportRomDialogView View { get; set; } @@ -24,7 +16,7 @@ public class ImportRomDialogController public ImportRomSettings PromptUserForImportOptions(string romFilename) { - Builder = ImportRomSettingsBuilder.CreateFor(romFilename); + Builder = new ImportRomSettingsBuilder(romFilename); return !PromptUserForOptions() ? null : Builder.CreateSettings(); @@ -38,7 +30,12 @@ private bool PromptUserForOptions() Builder.ImportSettings.PropertyChanged += ImportSettingsOnPropertyChanged; Refresh(); - return View.ShowAndWaitForUserToConfirmSettings(); + var result = View.ShowAndWaitForUserToConfirmSettings(); + + Builder.ImportSettings.PropertyChanged -= ImportSettingsOnPropertyChanged; + View = null; + + return result; } private void ImportSettingsOnPropertyChanged(object sender, PropertyChangedEventArgs e) diff --git a/Diz.Controllers/src/controllers/MainFormController.cs b/Diz.Controllers/src/controllers/MainFormController.cs new file mode 100644 index 00000000..6e9d2269 --- /dev/null +++ b/Diz.Controllers/src/controllers/MainFormController.cs @@ -0,0 +1,522 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel; +using System.Diagnostics; +using System.IO; +using Diz.Controllers.interfaces; +using Diz.Controllers.util; +using Diz.Core.export; +using Diz.Core.import; +using Diz.Core.model; +using Diz.Core.model.byteSources; +using Diz.Core.model.snes; +using Diz.Core.serialization; +using Diz.Core.util; +using Diz.LogWriter; + +namespace Diz.Controllers.controllers +{ + public class MainFormController : IMainFormController + { + private FlagType currentMarkFlag = FlagType.Data8Bit; + private int selectedSnesOffset; + public ILongRunningTaskHandler.LongRunningTaskHandler TaskHandler => ProgressBarJob.RunAndWaitForCompletion; + + public event EventHandler Closed; + + public int SelectedSnesOffset + { + get => selectedSnesOffset; + set + { + selectedSnesOffset = value; + + // TODO: event propagation/etc + } + } + + public Data Data => Project?.Data; + + public event IProjectController.ProjectChangedEvent ProjectChanged; + + private readonly IDataGridEditorForm dataGridEditorForm; + + public MainFormController(IDataGridEditorForm view) + { + Debug.Assert(view != null); + dataGridEditorForm = view; + + dataGridEditorForm.MainFormController = this; + dataGridEditorForm.Closed += OnClosed; + } + + private void OnClosed(object sender, EventArgs e) => Closed?.Invoke(this, e); + + public void Show() => dataGridEditorForm.Show(); + + public Project Project { get; set; } + + public bool MoveWithStep { get; set; } = true; + + public FlagType CurrentMarkFlag + { + get => currentMarkFlag; + set + { + currentMarkFlag = value; + } + } + + // there's probably better ways to handle this. + // probably replace with a UI like "start task" and "stop task" + // so we can flip up a progress bar and remove it. + public void DoLongRunningTask(Action task, string description = null) + { + if (dataGridEditorForm.TaskHandler != null) + dataGridEditorForm.TaskHandler(task, description); + else + task(); + } + + public void OpenProject(string filename) + { + new ProjectOpenerGuiController { Handler = this } + .OpenProject(filename); + } + + public void OnProjectOpenSuccess(string filename, Project project) + { + SetProject(filename, project); + } + + public void SetProject(string filename, Project project) + { + Project = project; + Project.PropertyChanged += Project_PropertyChanged; + + ProjectChanged?.Invoke(this, new IProjectController.ProjectChangedEventArgs + { + ChangeType = IProjectController.ProjectChangedEventArgs.ProjectChangedType.Opened, + Filename = filename, + Project = project, + }); + } + + public void OnProjectOpenWarning(string warnings) => + dataGridEditorForm.OnProjectOpenWarning(warnings); + + public void OnProjectOpenFail(string fatalError) => + dataGridEditorForm.OnProjectOpenFail(fatalError); + + public string AskToSelectNewRomFilename(string error) => + dataGridEditorForm.AskToSelectNewRomFilename("Error", $"{error} Link a new ROM now?"); + + public Project OpenProject(string filename, bool showPopupAlertOnLoaded) + { + return new ProjectOpenerGuiController { Handler = this } + .OpenProject(filename); + } + + private void Project_PropertyChanged(object sender, PropertyChangedEventArgs e) + { + // TODO: use this to listen to interesting change events in Project/Data + // so we can react appropriately. + } + + public void SaveProject(string filename) + { + DoLongRunningTask(delegate { new ProjectFileManager().Save(Project, filename); }, + $"Saving {Path.GetFileName(filename)}..."); + } + + public void ImportBizHawkCdl(string filename) + { + BizHawkCdlImporter.Import(filename, Project.Data); + + ProjectChanged?.Invoke(this, new IProjectController.ProjectChangedEventArgs() + { + ChangeType = IProjectController.ProjectChangedEventArgs.ProjectChangedType.Imported, + Filename = filename, + Project = Project, + }); + } + + private static ImportRomDialogController SetupImportController(IProjectView projectView) + { + // let the user select settings on the GUI + var importController = new ImportRomDialogController {View = projectView.GetImportView()}; + importController.View.Controller = importController; + return importController; + } + + public bool ImportRomAndCreateNewProject(string romFilename) + { + var importController = SetupImportController(dataGridEditorForm); + var importSettings = importController.PromptUserForImportOptions(romFilename); + + if (importSettings != null) + { + CloseProject(); + + // actually do the import + ImportRomAndCreateNewProject(importSettings); + return true; + } + + return false; + } + + private void ImportRomAndCreateNewProject(ImportRomSettings importSettings) + { + var project = ImportUtils.ImportRomAndCreateNewProject(importSettings); + OnProjectOpenSuccess(project.Session?.ProjectFileName, project); + } + + public void WriteAssemblyOutput() + { + WriteAssemblyOutput(Project.LogWriterSettings); + } + + private void WriteAssemblyOutput(LogWriterSettings settings) + { + if (!RomDataPresent()) + return; + + var lc = new LogCreator() + { + Settings = settings, + Data = Project.Data, + }; + + LogCreatorOutput.OutputResult result = null; + DoLongRunningTask(delegate { result = lc.CreateLog(); }, "Exporting assembly source code..."); + + dataGridEditorForm.OnExportFinished(result); + } + + public void UpdateExportSettings(LogWriterSettings selectedSettings) => + Project.LogWriterSettings = selectedSettings; + + public void MarkProjectAsUnsaved() + { + // eventually set this via INotifyPropertyChanged or similar, + // instead of having to do it manually + if (Project?.Session != null) + Project.Session.UnsavedChanges = true; + } + + public long ImportBsnesUsageMap(string fileName) + { + if (!RomDataPresent()) + return 0L; + + var linesModified = BsnesUsageMapImporter.ImportUsageMap(File.ReadAllBytes(fileName), Project.Data); + + if (linesModified > 0) + MarkProjectAsUnsaved(); + + return linesModified; + } + + public long ImportBsnesTraceLogs(string[] fileNames) + { + if (!RomDataPresent()) + return 0L; + + var importer = new BsnesTraceLogImporter(Project.Data); + + // TODO: differentiate between binary-formatted and text-formatted files + // probably look for a newline within 80 characters + // call importer.ImportTraceLogLineBinary() + + // caution: trace logs can be gigantic, even a few seconds can be > 1GB + // inside here, performance becomes critical. + LargeFilesReader.ReadFilesLines(fileNames, + (line) => { importer.ImportTraceLogLine(line); }); + + if (importer.CurrentStats.NumRomBytesModified > 0) + MarkProjectAsUnsaved(); + + return importer.CurrentStats.NumRomBytesModified; + } + + public long ImportBsnesTraceLogsBinary(IEnumerable filenames) + { + if (!RomDataPresent()) + return 0L; + + var importer = new BsnesTraceLogImporter(Project.Data); + + foreach (var file in filenames) + { + using Stream source = File.OpenRead(file); + const int bytesPerPacket = 22; + var buffer = new byte[bytesPerPacket]; + int bytesRead; + while ((bytesRead = source.Read(buffer, 0, bytesPerPacket)) > 0) + { + Debug.Assert(bytesRead == 22); + importer.ImportTraceLogLineBinary(buffer); + } + } + + return importer.CurrentStats.NumRomBytesModified; + } + + public void CloseProject() + { + if (Project == null) + return; + + ProjectChanged?.Invoke(this, new IProjectController.ProjectChangedEventArgs() + { + ChangeType = IProjectController.ProjectChangedEventArgs.ProjectChangedType.Closing + }); + + Project = null; + } + + public int MarkMany(int markProperty, int markStart, object markValue, int markCount) + { + if (!RomDataPresent() || Project?.Data == null) + return 0; + + var destination = markProperty switch + { + 0 => Project.Data.MarkTypeFlag(markStart, (FlagType) markValue, markCount), + 1 => Project.Data.MarkDataBank(markStart, (int) markValue, markCount), + 2 => Project.Data.MarkDirectPage(markStart, (int) markValue, markCount), + 3 => Project.Data.MarkMFlag(markStart, (bool) markValue, markCount), + 4 => Project.Data.MarkXFlag(markStart, (bool) markValue, markCount), + 5 => Project.Data.MarkArchitecture(markStart, (Architecture) markValue, markCount), + _ => 0 + }; + + MarkProjectAsUnsaved(); + + return destination; + } + + private bool RomDataPresent() => Project?.Data?.GetRomSize() > 0; + + public void Step(int offset) + { + if (!RomDataPresent()) + return; + + MarkProjectAsUnsaved(); + SelectedSnesOffset = Step(offset, false); + } + + public void StepIn(int offset) + { + if (!RomDataPresent()) + return; + + MarkProjectAsUnsaved(); + SelectedSnesOffset = Step(offset, true); + } + + private int Step(int offset, bool branch) + { + return Project?.Data?.Step(offset, branch, false, offset - 1) ?? 0; + } + + public void AutoStepSafe(int offset) + { + if (!RomDataPresent()) + return; + + MarkProjectAsUnsaved(); + + var destinationOffset = DoAutoStep(offset, false, 0); + + if (MoveWithStep) + SelectedSnesOffset = destinationOffset; + } + + public void AutoStepHarsh(int offset) + { + if (!RomDataPresent()) + return; + + if (!dataGridEditorForm.PromptHarshAutoStep(offset, out var newOffset, out var count)) + return; + + MarkProjectAsUnsaved(); + + var destinationOffset = DoAutoStep(newOffset, true, count); + + if (MoveWithStep) + SelectedSnesOffset = destinationOffset; + } + + private int DoAutoStep(int newOffset, bool harsh, int count) + { + return Project?.Data?.AutoStep(newOffset, harsh, count) ?? 0; + } + + public void Mark(int offset) + { + if (!RomDataPresent()) + return; + + MarkProjectAsUnsaved(); + var newOffset = Project?.Data?.MarkTypeFlag( + offset, CurrentMarkFlag, RomUtil.GetByteLengthForFlag(CurrentMarkFlag)) ?? -1; + + if (newOffset != -1) + SelectedSnesOffset = newOffset; + } + + private int FindIntermediateAddress(int offset) + { + if (!RomDataPresent() || Project?.Data == null) + return -1; + + var ia = Project.Data.GetIntermediateAddressOrPointer(offset); + if (ia < 0) + return -1; + + return Project.Data.ConvertSnesToPc(ia); + } + + private bool FindUnreached(int offset, bool end, bool direction, out int unreached) + { + unreached = -1; + if (!RomDataPresent() || Project?.Data == null) + return false; + + var size = Project.Data.GetRomSize(); + unreached = end ? (direction ? 0 : size - 1) : offset; + + if (direction) + { + if (!end) + while (unreached < size - 1 && IsUnreached(unreached)) + unreached++; + + while (unreached < size - 1 && IsReached(unreached)) + unreached++; + } + else + { + if (unreached > 0) + unreached--; + + while (unreached > 0 && IsReached(unreached)) + unreached--; + } + + while (unreached > 0 && IsUnreached(unreached - 1)) + unreached--; + + return IsUnreached(unreached); + } + + private bool IsReached(int offset) + { + return Project?.Data?.GetFlag(offset) != FlagType.Unreached; + } + + private bool IsUnreached(int offset) + { + return Project?.Data?.GetFlag(offset) == FlagType.Unreached; + } + + public void MarkMany(int offset, int whichIndex) + { + if (!RomDataPresent()) + return; + + var markCmd = dataGridEditorForm.PromptMarkMany(offset, whichIndex); + if (markCmd == null) + return; + + var destination = MarkMany(markCmd.Property, markCmd.Start, markCmd.Value, markCmd.Count); + + if (MoveWithStep) + SelectedSnesOffset = destination; + } + + public void GoToIntermediateAddress(int offset) + { + if (!RomDataPresent()) + return; + + var snesOffset = FindIntermediateAddress(offset); + if (snesOffset == -1) + return; + + SelectedSnesOffset = snesOffset; + } + + public void OnUserChangedSelection(ByteEntry newSelection) + { + // when user clicks on a new row in the child data grid editor, this fires + SelectedSnesOffset = newSelection.ParentIndex; + } + + private bool IsOffsetInRange(int offset) + { + if (!RomDataPresent()) + return false; + + return offset >= 0 && offset < Project?.Data?.GetRomSize(); + } + + public void GoTo(int offset) + { + if (IsOffsetInRange(offset)) + SelectedSnesOffset = offset; + else + dataGridEditorForm.ShowOffsetOutOfRangeMsg(); + } + + public void GoToUnreached(bool end, bool direction) + { + if (!FindUnreached(SelectedSnesOffset, end, direction, out var unreached)) + return; + + SelectedSnesOffset = unreached; + } + + public void SetDataBank(int romOffset, int result) + { + Data?.SetDataBank(romOffset, result); + } + + public void SetDirectPage(int romOffset, int result) + { + Data?.SetDirectPage(romOffset, result); + } + + public void SetMFlag(int romOffset, bool value) + { + Data?.SetMFlag(romOffset, value); + } + + public void SetXFlag(int romOffset, bool value) + { + Data?.SetXFlag(romOffset, value); + } + + public void ImportLabelsCsv(ILabelEditorView labelEditor, bool replaceAll) + { + var importFilename = labelEditor.PromptForCsvFilename(); + if (string.IsNullOrEmpty(importFilename)) + return; + + var errLine = 0; + try + { + Data.Labels.ImportLabelsFromCsv(importFilename, replaceAll, ref errLine); + labelEditor.RepopulateFromData(); + } + catch (Exception ex) + { + labelEditor.ShowLineItemError(ex.Message, errLine); + } + } + } +} \ No newline at end of file diff --git a/DiztinGUIsh/controller/MarkManyController.cs b/Diz.Controllers/src/controllers/MarkManyController.cs similarity index 89% rename from DiztinGUIsh/controller/MarkManyController.cs rename to Diz.Controllers/src/controllers/MarkManyController.cs index 5dbae5c0..d0e5a88c 100644 --- a/DiztinGUIsh/controller/MarkManyController.cs +++ b/Diz.Controllers/src/controllers/MarkManyController.cs @@ -1,9 +1,9 @@ using System; +using Diz.Controllers.interfaces; using Diz.Core; using Diz.Core.commands; using Diz.Core.model; using Diz.Core.util; -using DiztinGUIsh.controller; namespace Diz.Controllers.controllers { @@ -11,10 +11,10 @@ public class MarkManyController : IMarkManyController { public IDataRange DataRange { get; } public IMarkManyView MarkManyView { get; } - public IReadOnlySnesRomBase Data { get; } + public IReadOnlySnesRom Data { get; } public int DesiredStartingCount { get; set; } = 0x10; - public MarkManyController(int offset, int whichIndex, IReadOnlySnesRomBase data, IMarkManyView view) + public MarkManyController(int offset, int whichIndex, IReadOnlySnesRom data, IMarkManyView view) { Data = data; MarkManyView = view; @@ -32,7 +32,7 @@ public MarkManyController(int offset, int whichIndex, IReadOnlySnesRomBase data, } private MarkCommand CreateCommandFromView() => - new MarkCommand + new() { Start = DataRange.StartIndex, Count = DataRange.RangeCount, diff --git a/Diz.Controllers/src/controllers/NavigationEntry.cs b/Diz.Controllers/src/controllers/NavigationEntry.cs new file mode 100644 index 00000000..a004d626 --- /dev/null +++ b/Diz.Controllers/src/controllers/NavigationEntry.cs @@ -0,0 +1,35 @@ +using System.ComponentModel; +using System.ComponentModel.DataAnnotations; +using Diz.Core.model.snes; +using Diz.Core.util; + +namespace Diz.Controllers.controllers +{ + public class NavigationEntry + { + [Browsable(false)] + public Data Data { get; } + + [Browsable(false)] + public int SnesOffset { get; } + + public NavigationEntry(int snesOffset, ISnesNavigation.HistoryArgs historyArgs, Data data) + { + SnesOffset = snesOffset; + Description = historyArgs?.Description ?? ""; + Position = historyArgs?.Position ?? ""; + + Data = data; + } + + [DisplayName("SNES Offset")] + [Editable(false)] + public string Address => Util.ToHexString6(SnesOffset); + + [Editable(false)] + public string Description { get; } + + [Editable(false)] + public string Position { get; } + } +} \ No newline at end of file diff --git a/DiztinGUIsh/util/ProgressBarWorker.cs b/Diz.Controllers/src/controllers/ProgressBarWorker.cs similarity index 75% rename from DiztinGUIsh/util/ProgressBarWorker.cs rename to Diz.Controllers/src/controllers/ProgressBarWorker.cs index b9bfc757..da8288e2 100644 --- a/DiztinGUIsh/util/ProgressBarWorker.cs +++ b/Diz.Controllers/src/controllers/ProgressBarWorker.cs @@ -1,22 +1,28 @@ using System; +using System.Diagnostics; +using System.Runtime.InteropServices; using System.Threading; -using DiztinGUIsh.window.dialog; +using Diz.Controllers.interfaces; +using Diz.Core.util; +using ExtendedXmlSerializer.Configuration; +using LightInject; -namespace DiztinGUIsh +namespace Diz.Controllers.controllers { - // TODO: use https://www.wpf-tutorial.com/misc/multi-threading-with-the-backgroundworker/ backgroundworker + // TODO: replace this with Task and async/await. don't use threads directly. public abstract class ProgressBarWorker { - private ProgressDialog dialog; + public IProgressView View { get; set; } + public bool IsMarquee { get; init; } + public string TextOverride { get; init; } + private bool isRunning; private Thread backgroundThread; - public bool IsMarquee { get; set; } - public string TextOverride { get; set; } protected void UpdateProgress(int i) { - // i must be in range of 0 to 100 - dialog.UpdateProgress(i); + Debug.Assert(i >= 0 && i <= 100); + View?.Report(i); } protected abstract void Thread_DoWork(); @@ -42,20 +48,28 @@ protected virtual void Setup() isRunning = true; - dialog = new ProgressDialog(IsMarquee, TextOverride); + Debug.Assert(View != null); + + + View.IsMarquee = IsMarquee; + View.TextOverride = TextOverride; // setup, but don't start, the new thread backgroundThread = new Thread(Thread_Main); - // honestly, not sure about this. works around some Invoke() stuff - backgroundThread.SetApartmentState(ApartmentState.STA); + // honestly, not sure about this. works around some weird Invoke() stuff + // this all needs to be ripped out. there's another branch with the WIP version of that. + if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) + { + backgroundThread.SetApartmentState(ApartmentState.STA); + } } // blocking function private void WaitForJobToFinish() { // blocks til worker thread closes this dialog box - dialog.ShowDialog(); + View.PromptDialog(); } // called from a new worker thread @@ -65,7 +79,7 @@ private void Thread_Main() { // BAD APPROACH. we should instead get an event // I'm too lazy right now. TODO FIXME - while (!dialog.Visible) + while (!View.Visible) Thread.Sleep(50); Thread_DoWork(); @@ -80,26 +94,26 @@ private void Thread_Main() private void SignalJobIsDone() { // unblock the main thread from ShowDialog() - dialog?.BeginInvoke(new Action(() => dialog.Close())); + View?.SignalJobIsDone(); } } -} - -namespace DiztinGUIsh -{ public class ProgressBarJob : ProgressBarWorker { // a version that keeps calling 'callback' until it returns -1 public static void Loop(long maxProgress, NextAction callback, string overrideTxt = null) { + var view = Service.Container.GetInstance(); + Debug.Assert(view != null); + var j = new ProgressBarJob { MaxProgress = maxProgress, Callback = callback, IsMarquee = maxProgress == -1, TextOverride = overrideTxt, + View = view, }; j.Run(); } diff --git a/Diz.Controllers/src/controllers/ProjectOpenerGuiController.cs b/Diz.Controllers/src/controllers/ProjectOpenerGuiController.cs new file mode 100644 index 00000000..cd4ba426 --- /dev/null +++ b/Diz.Controllers/src/controllers/ProjectOpenerGuiController.cs @@ -0,0 +1,65 @@ +using System; +using System.IO; +using System.Linq; +using Diz.Controllers.interfaces; +using Diz.Core.model; +using Diz.Core.serialization; + +namespace Diz.Controllers.controllers +{ + public class ProjectOpenerGuiController + { + public IProjectOpenerHandler Handler { get; init; } + + public Project OpenProject(string filename) + { + Project project = null; + var errorMsg = ""; + var warningMsg = ""; + + DoLongRunningTask(() => + { + try + { + var (project1, warning) = new ProjectFileManager() + { + RomPromptFn = Handler.AskToSelectNewRomFilename + }.Open(filename); + + project = project1; + warningMsg = warning; + } + catch (AggregateException ex) + { + project = null; + errorMsg = ex.InnerExceptions.Select(e => e.Message).Aggregate((line, val) => line += val + "\n"); + } + catch (Exception ex) + { + project = null; + errorMsg = ex.Message; + } + }, $"Opening {Path.GetFileName(filename)}..."); + + if (project == null) + { + Handler.OnProjectOpenFail(errorMsg); + return null; + } + + if (warningMsg != "") + Handler.OnProjectOpenWarning(warningMsg); + + Handler.OnProjectOpenSuccess(filename, project); + return project; + } + + private void DoLongRunningTask(Action task, string description) + { + if (Handler.TaskHandler != null) + Handler.TaskHandler(task, description); + else + task(); + } + } +} \ No newline at end of file diff --git a/Diz.Controllers/src/controllers/ProjectsController.cs b/Diz.Controllers/src/controllers/ProjectsController.cs new file mode 100644 index 00000000..ef3c7478 --- /dev/null +++ b/Diz.Controllers/src/controllers/ProjectsController.cs @@ -0,0 +1,156 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.IO; +using System.Linq; +using Diz.Controllers.interfaces; +using Diz.Core.model; +using Diz.Core.util; + +namespace Diz.Controllers.controllers +{ + /// + /// Loads and caches Project objects, and stores references to the last loaded project. + /// + public class ProjectsManager : IProjectsManager + { + private readonly IProjectLoader projectLoader; + + public Dictionary LoadedProjects { get; } = new(); + + // this needs some rework, it's used all over the place in odd ways. + public static string LastOpenedProjectFilename { get; set; } + + public ProjectsManager(IProjectLoader projectLoader) + { + this.projectLoader = projectLoader; + } + + /// + /// Get a loaded project associated with the filename + /// If it's already loaded, we'll return a copy of the loaded project. This may + /// be shared with already opened views. If it's not loaded, load from disk. + /// + /// + /// + public Project GetProject(string filename) + { + if (string.IsNullOrEmpty(filename)) + return null; + + if (LoadedProjects.ContainsKey(filename)) + return LoadedProjects[filename]; + + var project = projectLoader.LoadProject(filename); + if (project == null) + return null; + + LoadedProjects.Add(filename, project); + return project; + } + + /// + /// Get a sample project, loaded from hardcoded data inside the app itself. Suitable for testing/demo purposes + /// and doesn't need to read anything from the disk. + /// + /// Demo project with demo ROM loaded + public Project GetSampleProject() => + GetProject(ProjectLoaderWithSampleDataDecorator.MagicSampleProjectName); + + public Project GetLastOpenedProject() + { + // this is kinda messy/dumb, rethink. + Debug.Assert(LoadedProjects.Values.Count == 1); + return LoadedProjects.Values.Select(project => project).FirstOrDefault(); + } + + public void OpenProjectFile(string filename) + { + if (filename == null) + return; + + AfterProjectOpenAttempt(filename, GetProject(filename)); + } + + public void OpenLastLoadedProject() => + AfterProjectOpenAttempt("", GetLastOpenedProject()); + + private void AfterProjectOpenAttempt(string filenameAttempted, Project loadedProject) + { + if (loadedProject == null) + { + var filenameInfo = !string.IsNullOrEmpty(filenameAttempted) + ? $" (filename={filenameAttempted})" + : ""; + + throw new InvalidDataException($"Failed to open project{filenameInfo}"); + } + + OnProjectOpened?.Invoke(this, loadedProject); + } + + public event EventHandler OnProjectOpened; + } + + /// + /// Interface to Load Project objects + /// + public interface IProjectLoader + { + public Project LoadProject(string filename); + } + + /// + /// A loader that reads from disk + /// + public class ProjectFileLoader : IProjectLoader + { + public IProjectOpenerHandler ProjectOpenerHandler { get; init; } + + public Project LoadProject(string filename) + { + return ProjectOpenerHandler?.OpenProject(filename, showPopupAlertOnLoaded: false); + } + } + + /// + /// A decorator for IProjectLoader that returns a project created from our internal sample data + /// (instead of data from disk). Pass in the special string constant here to use. + /// + public class ProjectLoaderWithSampleDataDecorator : IProjectLoader + { + // pass in this magic string to load the "sample data" project. + // great for testing and for showing sample stuff in the app. + // + // ReSharper disable once MemberCanBeProtected.Global + public const string MagicSampleProjectName = "sampleproject111111112"; + + private readonly IProjectLoader previous; + + public ProjectLoaderWithSampleDataDecorator(IProjectLoader previous) + { + Debug.Assert(previous != null); + this.previous = previous; + } + + public Project LoadProject(string filename) + { + return filename != MagicSampleProjectName + ? previous.LoadProject(filename) + : CreateNewSampleProject(); + } + + private static Project CreateNewSampleProject() + { + var project = new Project { + Data = SampleRomData.CreateSampleData().Data, + }; + + project.Session = new ProjectSession(project) { + ProjectFileName = MagicSampleProjectName, + }; + + return project; + } + } +} \ No newline at end of file diff --git a/Diz.Controllers/src/controllers/StartFormController.cs b/Diz.Controllers/src/controllers/StartFormController.cs new file mode 100644 index 00000000..46d63fed --- /dev/null +++ b/Diz.Controllers/src/controllers/StartFormController.cs @@ -0,0 +1,26 @@ +using System; +using System.Diagnostics; +using Diz.Controllers.interfaces; + +namespace Diz.Controllers.controllers +{ + public class StartFormController : IStartFormController + { + public IStartFormViewer View { get; } + + + public StartFormController(IStartFormViewer view) + { + Debug.Assert(view != null); + View = view; + View.Closed += ViewOnClosed; + } + + public void Show() => View.Show(); + + [Obsolete] + public event EventHandler Closed; + + private void ViewOnClosed(object sender, EventArgs e) => Closed?.Invoke(this, e); + } +} diff --git a/Diz.Controllers/src/interfaces/IControllers.cs b/Diz.Controllers/src/interfaces/IControllers.cs new file mode 100644 index 00000000..ecbf11a4 --- /dev/null +++ b/Diz.Controllers/src/interfaces/IControllers.cs @@ -0,0 +1,170 @@ +using System; +using System.ComponentModel; +using Diz.Controllers.controllers; +using Diz.Core; +using Diz.Core.commands; +using Diz.Core.datasubset; +using Diz.Core.export; +using Diz.Core.model; +using Diz.Core.model.byteSources; +using Diz.Core.model.snes; + +// NOTE: lots of these interfaces were created temporarily for major refactoring. +// when that process is finished, we should probably take a pass here to simplify anything +// that ended up being unnecessary or over-complicated + +namespace Diz.Controllers.interfaces +{ + public interface IController + { + + } + + public interface ICloseHandler + { + public event EventHandler Closed; + } + + public interface IFormController : IController, ICloseHandler, IShowable + { + + } + + public interface IDataController : IController + { + Data Data { get; } + } + + public interface IBytesGridDataController : IDataController, INotifyPropertyChanged + { + IBytesGridViewer ViewGrid { get; set; } + DataSubsetWithSelection DataSubset { get; } + void MatchCachedRowsToView(); + } + + public interface IStartFormController : IFormController + { + public IStartFormViewer View { get; } + } + + public interface IProjectOpener + { + public void OpenFileWithNewView(string filename); + public void OpenNewViewOfLastLoadedProject(); + } + + public interface IProjectController + { + public Project Project { get; } + + public class ProjectChangedEventArgs + { + public enum ProjectChangedType + { + Invalid, + Saved, + Opened, + Imported, + Closing + } + + public ProjectChangedType ChangeType; + public Project Project; + public string Filename; + } + + delegate void ProjectChangedEvent(object sender, ProjectChangedEventArgs e); + + event ProjectChangedEvent ProjectChanged; + + void SaveProject(string filename); + + void OpenProject(string fileName); + + void MarkProjectAsUnsaved(); + + bool ImportRomAndCreateNewProject(string romFilename); + } + + public interface IProjectOpenerHandler : ILongRunningTaskHandler + { + public void OnProjectOpenSuccess(string filename, Project project); + public void OnProjectOpenWarning(string warnings); + public void OnProjectOpenFail(string fatalError); + public string AskToSelectNewRomFilename(string error); + + Project OpenProject(string filename, bool showPopupAlertOnLoaded); + } + + public interface I65816CpuOperations + { + void Step(int offset); + void StepIn(int offset); + void AutoStepHarsh(int offset); + void AutoStepSafe(int offset); + void Mark(int offset); + public void MarkMany(int offset, int whichIndex); + + void SetDataBank(int romOffset, int result); + void SetDirectPage(int romOffset, int result); + void SetMFlag(int romOffset, bool value); + void SetXFlag(int romOffset, bool value); + } + + public interface IExportDisassembly + { + void UpdateExportSettings(LogWriterSettings selectedSettings); + void WriteAssemblyOutput(); + } + + public interface ITraceLogImporters + { + void ImportBizHawkCdl(string filename); + long ImportBsnesUsageMap(string fileName); + long ImportBsnesTraceLogs(string[] fileNames); + } + + public interface IProjectNavigation + { + public int SelectedSnesOffset { get; set; } + + void GoTo(int offset); + void GoToUnreached(bool end, bool direction); + void GoToIntermediateAddress(int offset); + void OnUserChangedSelection(ByteEntry newSelection); + } + + public interface ILabelImporter + { + void ImportLabelsCsv(ILabelEditorView labelEditor, bool replaceAll); + } + + public interface IMainFormController : + + IFormController, + + // TODO: shouldn't have the word 'Grid' in here for Main Form controller. refactor + // either naming or functionality. + // IBytesGridViewerDataController, + + IProjectController, + I65816CpuOperations, + IExportDisassembly, + IProjectOpenerHandler, + ITraceLogImporters, + IProjectNavigation, + ILabelImporter + { + public FlagType CurrentMarkFlag { get; set; } + public bool MoveWithStep { get; set; } + + void SetProject(string filename, Project project); + } + + public interface IMarkManyController : IController + { + IDataRange DataRange { get; } + IReadOnlySnesRom Data { get; } + MarkCommand GetMarkCommand(); + } +} \ No newline at end of file diff --git a/Diz.Controllers/src/interfaces/IViewers.cs b/Diz.Controllers/src/interfaces/IViewers.cs new file mode 100644 index 00000000..baf46ae6 --- /dev/null +++ b/Diz.Controllers/src/interfaces/IViewers.cs @@ -0,0 +1,119 @@ +using System; +using System.Collections.Generic; +using Diz.Controllers.controllers; +using Diz.Controllers.util; +using Diz.Core.util; + +namespace Diz.Controllers.interfaces +{ + public interface IViewer + { + + } + + public interface IFormViewer : IViewer, ICloseHandler, IShowable + { + + } + + public interface IShowable + { + void Show(); + } + + public interface IStartFormViewer : IProjectOpenRequester, IFormViewer + { + + } + + public record ProjectOpenEventArgs + { + public string Filename { get; init; } + public bool OpenLast { get; init; } + } + + public interface IProjectOpenRequester + { + public event EventHandler ProjectOpenRequested; + } + + public interface ICloseable + { + void Close(); + } + + public interface IModalDialog + { + /// + /// Show the dialog to the user and wait for them to complete + /// the steps on the view + /// + /// True if steps were completed and we have a valid result + bool PromptDialog(); + } + + public interface IProgressView : ICloseable, IModalDialog, IProgress { + public bool IsMarquee { get; set; } + public string TextOverride { get; set; } + bool Visible { get; set; } + + /// + /// Signal that a job (potentially running in another task/thread) has completed. + /// CAUTION: Implementers should use thread-safety measures, this may be called + /// from a different thread than any other calls + /// + void SignalJobIsDone(); + } + + public interface IMarkManyView : IViewer, IModalDialog + { + int Property { get; } + int Column { set; } // TODO: make enum with different types supported. + IMarkManyController Controller { get; set; } + object GetFinalValue(); + } + + public interface IBytesGridViewer : IRowBaseViewer, IViewer + { + public List DataSource { get; set; } + int TargetNumberOfRowsToShow { get; } + + void SelectRow(int row); + + + void BeginEditingSelectionComment(); + void BeginEditingSelectionLabel(); + + public class SelectedOffsetChangedEventArgs : EventArgs + { + public TByteItem Row { get; init; } + public int RowIndex { get; init; } + } + + public delegate void SelectedOffsetChange(object sender, SelectedOffsetChangedEventArgs e); + + public event SelectedOffsetChange SelectedOffsetChanged; + } + + public interface ILabelEditorView + { + string PromptForCsvFilename(); + void RepopulateFromData(); + void ShowLineItemError(string exMessage, int errLine); + } + + + public interface IImportRomDialogView + { + bool PromptToConfirmAction(string msg); + bool ShowAndWaitForUserToConfirmSettings(); + ImportRomDialogController Controller { get; set; } + bool GetVectorValue(int i, int j); + void RefreshUi(); + } + + public interface IDataGridEditorForm : IFormViewer, IProjectView + { + IMainFormController MainFormController { get; set; } + } +} \ No newline at end of file diff --git a/Diz.Controllers/src/interfaces/Interfaces.cs b/Diz.Controllers/src/interfaces/Interfaces.cs new file mode 100644 index 00000000..5ad4f302 --- /dev/null +++ b/Diz.Controllers/src/interfaces/Interfaces.cs @@ -0,0 +1,63 @@ +using System; +using System.ComponentModel; +using Diz.Core.datasubset; +using Diz.Core.model; +using Diz.Core.model.byteSources; +using Diz.Core.model.snes; + +namespace Diz.Controllers.interfaces +{ + public interface IDizApplication + { + public class Args + { + public string FileToOpen { get; set; } + } + + void Run(Args args); + } + + public interface IGridRow + { + Data Data { get; init; } + TItem Item { get; } + } + + public interface IDataGridRow : IGridRow, INotifyPropertyChanged + { + + } + + public interface IDataSubsetRomByteDataGridLoader : IDataSubsetLoader + { + // probably this needs to be refactored away, this exists for dependency injection resolution only + // across the Controller and Gui layer + + public IBytesGridViewer View { get; set; } + public Data Data { get; set; } + } + + public interface IProjectsManager : IProjects, IProjectLoadListener, ISampleProjectLoader, ILastProjectLoaded { } + + public interface IProjectLoadListener + { + public event EventHandler OnProjectOpened; + void OpenProjectFile(string filename); + } + + public interface IProjects + { + Project GetProject(string filename); + } + + public interface ISampleProjectLoader + { + Project GetSampleProject(); + } + + public interface ILastProjectLoaded + { + Project GetLastOpenedProject(); + void OpenLastLoadedProject(); + } +} \ No newline at end of file diff --git a/Diz.Controllers/src/services/Registration.cs b/Diz.Controllers/src/services/Registration.cs new file mode 100644 index 00000000..c478ed93 --- /dev/null +++ b/Diz.Controllers/src/services/Registration.cs @@ -0,0 +1,58 @@ +using Diz.Controllers.controllers; +using Diz.Controllers.interfaces; +using Diz.Core.model; +using Diz.Core.model.byteSources; +using JetBrains.Annotations; +using LightInject; + +namespace Diz.Controllers.services +{ + [UsedImplicitly] + public class DizControllersCompositionRoot : ICompositionRoot + { + public void Compose(IServiceRegistry serviceRegistry) + { + // TODO: might be able to make some of these register using + // "open generics" to be more flexible. + + serviceRegistry.Register( + typeof(IDataController), + typeof(RomByteDataBindingController>) + ); + + serviceRegistry.Register(); + + serviceRegistry.Register( + typeof(IBytesGridDataController<,>), + typeof(RomByteDataBindingController<>) + ); + + serviceRegistry.Register( + typeof(IBytesGridDataController), + typeof(RomByteDataBindingGridController) + ); + + serviceRegistry.Register(); + + serviceRegistry.Register( + (factory, offset, whichIndex, data) => + { + var view = factory.GetInstance(); + var markManyController = new MarkManyController(offset, whichIndex, data, view); + markManyController.MarkManyView.Controller = markManyController; + return markManyController; + }); + + serviceRegistry.Register(); + + serviceRegistry.Register(); + serviceRegistry.Decorate( + typeof(IProjectLoader), + typeof(ProjectLoaderWithSampleDataDecorator)); + + serviceRegistry.Register(); + + serviceRegistry.RegisterSingleton("SampleProjectLoader"); + } + } +} \ No newline at end of file diff --git a/DiztinGUIsh/util/LargeFilesReader.cs b/Diz.Controllers/src/util/LargeFilesReader.cs similarity index 96% rename from DiztinGUIsh/util/LargeFilesReader.cs rename to Diz.Controllers/src/util/LargeFilesReader.cs index 1616e48e..1cc2046a 100644 --- a/DiztinGUIsh/util/LargeFilesReader.cs +++ b/Diz.Controllers/src/util/LargeFilesReader.cs @@ -1,9 +1,10 @@ using System; using System.Collections.Generic; using System.IO; +using Diz.Controllers.controllers; using Diz.Core.util; -namespace DiztinGUIsh.util +namespace Diz.Controllers.util { public class LargeFilesReader : ProgressBarWorker { diff --git a/Diz.Controllers/src/util/RomByteRowBase.cs b/Diz.Controllers/src/util/RomByteRowBase.cs new file mode 100644 index 00000000..065152b0 --- /dev/null +++ b/Diz.Controllers/src/util/RomByteRowBase.cs @@ -0,0 +1,328 @@ +using System; +using System.ComponentModel; +using System.ComponentModel.DataAnnotations; +using System.Drawing; +using System.Globalization; +using System.Runtime.CompilerServices; +using Diz.Controllers.interfaces; +using Diz.Core.model; +using Diz.Core.model.byteSources; +using Diz.Core.model.snes; +using Diz.Core.util; +using JetBrains.Annotations; + +namespace Diz.Controllers.util +{ + public class RomByteRowBase : INotifyPropertyChangedExt + { + [DisplayName("Label")] + [Editable(true)] + // [CellStyleFormatter(GetBackColorInOut)] + public string Label + { + get => Data.Labels.GetLabelName(Data.ConvertPCtoSnes(ByteEntry.ParentIndex)); + + // todo (validate for valid label characters) + // (note: validation implemented in Furious's branch, integrate here) + set + { + Data.Labels.AddLabel( + Data.ConvertPCtoSnes(ByteEntry.ParentIndex), + new Label {Name = value}, + true); + OnPropertyChanged(); + } + } + + [DisplayName("PC")] + [ReadOnly(true)] + public string Offset => Util.ToHexString6(Data.ConvertPCtoSnes(ByteEntry.ParentIndex)); + + // show the byte two different ways: ascii and numeric + [DisplayName("@")] + [ReadOnly(true)] + public char AsciiCharRep => + ByteEntry?.Byte == null ? ' ' : ByteEntry.Byte.ToString()[0]; + + [DisplayName("#")] + [ReadOnly(true)] + public string NumericRep => + Util.NumberToBaseString(ByteEntry.ParentIndex, NumberBase); + + [DisplayName("<*>")] + [ReadOnly(true)] + public string Point => + RomUtil.PointToString(ByteEntry.Point); + + [DisplayName("Instruction")] + [ReadOnly(true)] + public string Instruction + { + get + { + // NOTE: this does not handle instructions whose opcodes cross banks correctly. + // if we hit this situation, just return empty for the grid, it's likely real instruction won't do this? + var romOffset = ByteEntry.ParentIndex; + var len = Data.GetInstructionLength(romOffset); + return romOffset + len <= Data.GetRomSize() ? Data.GetInstruction(romOffset) : ""; + } + } + + [DisplayName("IA")] + [ReadOnly(true)] + // ReSharper disable once InconsistentNaming + public string IA + { + get + { + var ia = Data.GetIntermediateAddressOrPointer(ByteEntry.ParentIndex); + return ia >= 0 ? Util.ToHexString6(ia) : ""; + } + } + + [DisplayName("Flag")] + [ReadOnly(true)] + public string TypeFlag => + Util.GetEnumDescription(Data.GetFlag(ByteEntry.ParentIndex)); + + [DisplayName("B")] + [Editable(true)] + public string DataBank + { + get => Util.NumberToBaseString(Data.GetDataBank(ByteEntry.ParentIndex), Util.NumberBase.Hexadecimal, 2); + set + { + if (!int.TryParse(value, NumberStyles.HexNumber, null, out var parsed)) + return; + + Data.SetDataBank(ByteEntry.ParentIndex, parsed); + OnPropertyChanged(); + } + } + + [DisplayName("D")] + [Editable(true)] + public string DirectPage + { + get => Util.NumberToBaseString(Data.GetDirectPage(ByteEntry.ParentIndex), Util.NumberBase.Hexadecimal, 4); + set + { + if (!int.TryParse(value, NumberStyles.HexNumber, null, out var parsed)) + return; + + Data.SetDirectPage(ByteEntry.ParentIndex, parsed); + OnPropertyChanged(); + } + } + + [DisplayName("M")] + [Editable(true)] + public string MFlag + { + get => RomUtil.BoolToSize(Data.GetMFlag(ByteEntry.ParentIndex)); + set + { + Data.SetMFlag(ByteEntry.ParentIndex, value is "8" or "M"); + OnPropertyChanged(); + } + } + + [DisplayName("X")] + [Editable(true)] + public string XFlag + { + get => RomUtil.BoolToSize(Data.GetXFlag(ByteEntry.ParentIndex)); + set + { + Data.SetXFlag(ByteEntry.ParentIndex, value is "8" or "X"); + OnPropertyChanged(); + } + } + + [DisplayName("Comment")] + [Editable(true)] + public string Comment + { + get => Data.GetCommentText(Data.ConvertPCtoSnes(ByteEntry.ParentIndex)); + set + { + Data.AddComment(Data.ConvertPCtoSnes(ByteEntry.ParentIndex), value, true); + OnPropertyChanged(); + } + } + + private readonly ByteEntry byteEntry; + + [Browsable(false)] + public ByteEntry ByteEntry + { + get => byteEntry; + init + { + this.SetField(PropertyChanged, ref byteEntry, value); + // tmp disable // if (ByteOffset != null) + // ByteOffset.PropertyChanged += OnRomBytePropertyChanged; + } + } + + [Browsable(false)] public Data Data { get; init; } + [Browsable(false)] public IRowBaseViewer ParentView { get; init; } + [Browsable(false)] private Util.NumberBase NumberBase => + ParentView?.NumberBaseToShow ?? Util.NumberBase.Hexadecimal; + + [Browsable(false)] public event PropertyChangedEventHandler PropertyChanged; + + private void OnRomBytePropertyChanged(object sender, PropertyChangedEventArgs e) + { + void OnInstructionRelatedChanged() + { + OnPropertyChanged(nameof(Instruction)); + OnPropertyChanged(nameof(IA)); + } + + // NOTE: if any properties under ByteOffset change, make sure the names update here + switch (e.PropertyName) + { + case nameof(ByteEntry.Byte): + OnPropertyChanged(nameof(AsciiCharRep)); + OnPropertyChanged(nameof(NumericRep)); + OnInstructionRelatedChanged(); + break; + case nameof(ByteEntry.Arch): + OnInstructionRelatedChanged(); + break; + case nameof(ByteEntry.DataBank): + case nameof(ByteEntry.DirectPage): + case nameof(ByteEntry.XFlag): + case nameof(ByteEntry.MFlag): + case nameof(ByteEntry.TypeFlag): + case nameof(ByteEntry.Point): + OnPropertyChanged(e.PropertyName); + break; + } + } + + [NotifyPropertyChangedInvocator] + public virtual void OnPropertyChanged([CallerMemberName] string propertyName = null) => + PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)); + + #region Formatting + protected Color? GetBackgroundColorForMarkedAsOpcode(string colPropName) + { + // TODO: eventually, don't match strings here. + // instead, look for the appropriate attribute attached to romByteRow and let that + // attribute hook in here. + return colPropName switch + { + nameof(Point) => GetBackColorInOut(), + nameof(Instruction) => GetInstructionBackgroundColor(), + nameof(DataBank) => GetDataBankColor(), + nameof(DirectPage) => GetDirectPageColor(), + nameof(MFlag) => GetMFlagColor(), + nameof(XFlag) => GetXFlagColor(), + _ => null + }; + } + + private Color? GetBackColorInOut() + { + int r = 255, g = 255, b = 255; + if ((ByteEntry.Point & (InOutPoint.EndPoint | InOutPoint.OutPoint)) != 0) g -= 50; + if ((ByteEntry.Point & InOutPoint.InPoint) != 0) r -= 50; + if ((ByteEntry.Point & InOutPoint.ReadPoint) != 0) b -= 50; + return Color.FromArgb(r, g, b); + } + + private Color? GetInstructionBackgroundColor() + { + var opcode = ByteEntry.Byte; + var isWeirdInstruction = + opcode == 0x40 || opcode == 0xCB || opcode == 0xDB || opcode == 0xF8 || // RTI WAI STP SED + opcode == 0xFB || opcode == 0x00 || opcode == 0x02 || opcode == 0x42 // XCE BRK COP WDM + ; + return isWeirdInstruction ? Color.Yellow : null; + } + + private Color? GetDataBankColor() + { + switch (ByteEntry.Byte) + { + // PLB MVP MVN + case 0xAB: + case 0x44: + case 0x54: + return Color.OrangeRed; + // PHB + case 0x8B: + return Color.Yellow; + default: + return null; + } + } + + private Color? GetDirectPageColor() + { + switch (ByteEntry.Byte) + { + // PLD TCD + case 0x2B: + case 0x5B: + return Color.OrangeRed; + + // PHD TDC + case 0x0B: + case 0x7B: + return Color.Yellow; + + default: + return null; + } + } + + public Color? GetMFlagColor() => GetMxFlagColor(0x20); + public Color? GetXFlagColor() => GetMxFlagColor(0x10); + + private Color? GetMxFlagColor(int nextByteMask) + { + var nextByte = Data.GetNextRomByte(ByteEntry.ParentIndex) ?? 0; + switch (ByteEntry.Byte) + { + // PLP + // SEP REP, *iff* relevant bit is set on next byte + case 0x28: + case 0xC2 or 0xE2 when (nextByte & nextByteMask) != 0: + return Color.OrangeRed; + case 0x08: // PHP + return Color.Yellow; + default: + return null; + } + } + + #endregion + } + + public static class RomByteRowAttributes + { + public static bool IsColumnEditable(string propertyName) => TestAttribute((EditableAttribute attr) => attr?.AllowEdit ?? false, propertyName); + + public static string GetColumnDisplayName(string propertyName) => TestAttribute((DisplayNameAttribute attr) => attr?.DisplayName, propertyName); + + public static bool GetColumnIsReadOnly(string propertyName) => TestAttribute((ReadOnlyAttribute attr) => attr?.IsReadOnly ?? false, propertyName); + + public static bool IsPropertyBrowsable(string propertyName) => TestAttribute((BrowsableAttribute attr) => attr?.Browsable ?? true, propertyName); + + private static TResult TestAttribute( + Func getValueFn, string memberName) + where TAttribute : Attribute + { + return Util.GetPropertyAttribute(getValueFn, typeof(RomByteRowBase), memberName); + } + } + + public interface IRowBaseViewer + { + Util.NumberBase NumberBaseToShow { get; } + TItem SelectedByteOffset { get; } + } +} \ No newline at end of file diff --git a/Diz.Core/Diz.Core.csproj b/Diz.Core/Diz.Core.csproj index 9f5f92ea..0cd40497 100644 --- a/Diz.Core/Diz.Core.csproj +++ b/Diz.Core/Diz.Core.csproj @@ -1,21 +1,25 @@  - + - net48 + net6.0 false false Diz.Core Diz.Core - 8.0 true + true true + 3.0.0 + 3.7.1 - - 0.7.3 + + 2.1.2 + all + runtime; build; native; contentfiles; analyzers; buildtransitive 2021.1.0 @@ -27,6 +31,7 @@ 4.3.0 + 10.1.0 diff --git a/Diz.Core/arch/CPU65C816.cs b/Diz.Core/arch/CPU65C816.cs index 44cbf02d..2c7956e4 100644 --- a/Diz.Core/arch/CPU65C816.cs +++ b/Diz.Core/arch/CPU65C816.cs @@ -1,16 +1,117 @@ -using Diz.Core.model; +using System.Collections.Generic; +using System.IO; +using Diz.Core.model; +using Diz.Core.model.byteSources; +using Diz.Core.model.snes; using Diz.Core.util; namespace Diz.Core.arch { - public class Cpu65C816 + public class CpuDispatcher { - private readonly Data data; - public Cpu65C816(Data data) + private Cpu cpuDefault; + private Cpu65C816 cpu65C816; + private CpuSpc700 cpuSpc700; + private CpuSuperFx cpuSuperFx; + + public Cpu Cpu(Data data, int offset) + { + var arch = data.GetArchitecture(offset); + + return arch switch + { + Architecture.Cpu65C816 => cpu65C816 ??= new Cpu65C816(), + Architecture.Apuspc700 => cpuSpc700 ??= new CpuSpc700(), + Architecture.GpuSuperFx => cpuSuperFx ??= new CpuSuperFx(), + _ => cpuDefault ??= new Cpu() + }; + } + } + + public class Cpu + { + public virtual int Step(Data data, int offset, bool branch, bool force, int prevOffset) => offset; + public virtual int GetInstructionLength(Data data, int offset) => 1; + public virtual int GetIntermediateAddress(Data data, int offset, bool resolve) => -1; + public virtual void MarkInOutPoints(Data data, int offset) {} // nop + public virtual string GetInstruction(Data data, int offset) => ""; + + // TODO: cleanup this function signature + protected virtual bool DoOneAutoStepNormal(ICpuOperableByteSource byteSource, List seenBranches, ref int newOffset, out int nextOffset, ref int prevOffset, Stack stack) + { + nextOffset = -1; + return false; + } + + public int AutoStep(ICpuOperableByteSource byteSource, int offset, bool harsh, int amount) + { + return harsh + ? AutoStepHarsh(byteSource, offset, amount) + : AutoStepNormal(byteSource, offset); + } + + private int AutoStepNormal(ICpuOperableByteSource byteSource, int offset) + { + var newOffset = offset; + var prevOffset = newOffset - 1; + + var stack = new Stack(); + var seenBranches = new List(); + var keepGoing = true; + + while (keepGoing) + { + keepGoing = DoOneAutoStepNormal(byteSource, seenBranches, ref newOffset, out _, ref prevOffset, stack); + + var flag = byteSource.GetFlag(newOffset); + if (!(flag == FlagType.Unreached || flag == FlagType.Opcode || flag == FlagType.Operand)) + keepGoing = false; + } + + return newOffset; + } + + private int AutoStepHarsh(ICpuOperableByteSource byteSource, int offset, int amount) { - this.data = data; + var newOffset = offset; + var prevOffset = offset - 1; + + while (newOffset < offset + amount) + { + var nextOffset = byteSource.Step(newOffset, false, true, prevOffset); + prevOffset = newOffset; + newOffset = nextOffset; + } + + return newOffset; } - public int Step(int offset, bool branch, bool force, int prevOffset) + } + + // a base Cpu for common things for real but mostly placeholder CPU types. + public abstract class CpuGenericHelper : Cpu + { + protected override bool DoOneAutoStepNormal(ICpuOperableByteSource byteSource, List seenBranches, ref int newOffset, out int nextOffset, ref int prevOffset, Stack stack) + { + nextOffset = byteSource.Step(newOffset, false, true, prevOffset); + prevOffset = newOffset; + newOffset = nextOffset; + return true; + } + } + + public class CpuSpc700 : CpuGenericHelper + { + + } + + public class CpuSuperFx : CpuGenericHelper + { + + } + + public class Cpu65C816 : Cpu + { + public override int Step(Data data, int offset, bool branch, bool force, int prevOffset) { var opcode = data.GetRomByte(offset); var prevDirectPage = data.GetDirectPage(offset); @@ -39,7 +140,7 @@ public int Step(int offset, bool branch, bool force, int prevOffset) data.SetXFlag(offset, prevX); data.SetMFlag(offset, prevM); - var length = GetInstructionLength(offset); + var length = GetInstructionLength(data, offset); // TODO: I don't think this is handling execution bank boundary wrapping correctly? -Dom // If we run over the edge of a bank, we need to go back to the beginning of that bank, not go into @@ -61,7 +162,7 @@ public int Step(int offset, bool branch, bool force, int prevOffset) data.SetMFlag(offset + i, prevM); } - MarkInOutPoints(offset); + MarkInOutPoints(data, offset); var nextOffset = offset + length; @@ -71,19 +172,96 @@ public int Step(int offset, bool branch, bool force, int prevOffset) opcode != 0x22)))) return nextOffset; - var iaNextOffsetPc = data.ConvertSnesToPc(GetIntermediateAddress(offset, true)); + var iaNextOffsetPc = data.ConvertSnesToPc(GetIntermediateAddress(data, offset, true)); if (iaNextOffsetPc >= 0) nextOffset = iaNextOffsetPc; return nextOffset; } + protected override bool DoOneAutoStepNormal(ICpuOperableByteSource byteSource, List seenBranches, + ref int newOffset, out int nextOffset, ref int prevOffset, Stack stack) + { + nextOffset = newOffset; + if (seenBranches.Contains(newOffset)) + return false; + + var opcode = byteSource.GetRomByteUnsafe(newOffset); + + nextOffset = byteSource.Step(newOffset, false, false, prevOffset); + var jumpOffset = byteSource.Step(newOffset, true, false, prevOffset); + + var shouldStop = + opcode == 0x40 || opcode == 0xCB || opcode == 0xDB || opcode == 0xF8 // RTI WAI STP SED + || opcode == 0xFB || opcode == 0x00 || opcode == 0x02 || opcode == 0x42 // XCE BRK COP WDM + || opcode == 0x6C || opcode == 0x7C || opcode == 0xDC || opcode == 0xFC; // JMP JMP JML JSR + + if ( opcode == 0x4C || opcode == 0x5C || opcode == 0x80 || opcode == 0x82 // JMP JML BRA BRL + || opcode == 0x10 || opcode == 0x30 || opcode == 0x50 || opcode == 0x70 // BPL BMI BVC BVS + || opcode == 0x90 || opcode == 0xB0 || opcode == 0xD0 || opcode == 0xF0 // BCC BCS BNE BEQ + ) + { + seenBranches.Add(newOffset); + } + + switch (opcode) + { + // PHP + case 0x08: + stack.Push(byteSource.GetMxFlags(newOffset)); + break; + + // PLP + case 0x28: + if (stack.Count == 0) + return false; + + byteSource.SetMxFlags(newOffset, stack.Pop()); + break; + + // RTS RTL + case 0x60: case 0x6B: + if (stack.Count == 0) + return false; + + prevOffset = newOffset; + newOffset = stack.Pop(); + break; + + // JSR JSL + case 0x20: case 0x22: + stack.Push(nextOffset); + prevOffset = newOffset; + newOffset = jumpOffset; + break; + + default: + prevOffset = newOffset; + newOffset = nextOffset; + break; + } + + return !shouldStop; + } + // input: ROM offset // return: a SNES address - public int GetIntermediateAddress(int offset, bool resolve) + public override int GetIntermediateAddress(Data data, int offset, bool resolve) { - int bank, directPage, operand, programCounter; + int bank; + int programCounter; + + #if !DIZ_3_BRANCH + // old way var opcode = data.GetRomByte(offset); + #else + // new way + var byteEntry = GetByteEntryRom(data, offset); + var opcode = byteEntry?.Byte; + #endif + + if (opcode == null) + return -1; var mode = GetAddressMode(data, offset); switch (mode) @@ -98,9 +276,11 @@ public int GetIntermediateAddress(int offset, bool resolve) case AddressMode.DirectPageLongIndirectYIndex: if (resolve) { - directPage = data.GetDirectPage(offset); - operand = data.GetRomByte(offset + 1); - return (directPage + operand) & 0xFFFF; + var directPage = data.GetDirectPage(offset); + var operand = data.GetRomByte(offset + 1); + if (!operand.HasValue) + return -1; + return (directPage + (int)operand) & 0xFFFF; } else { @@ -108,74 +288,184 @@ public int GetIntermediateAddress(int offset, bool resolve) } case AddressMode.DirectPageSIndex: case AddressMode.DirectPageSIndexIndirectYIndex: - return data.GetRomByte(offset + 1); + return data.GetRomByte(offset + 1) ?? -1; case AddressMode.Address: case AddressMode.AddressXIndex: case AddressMode.AddressYIndex: case AddressMode.AddressXIndexIndirect: - bank = (opcode == 0x20 || opcode == 0x4C || opcode == 0x7C || opcode == 0xFC) ? - data.ConvertPCtoSnes(offset) >> 16 : - data.GetDataBank(offset); - operand = data.GetRomWord(offset + 1); - return (bank << 16) | operand; + { + bank = (opcode == 0x20 || opcode == 0x4C || opcode == 0x7C || opcode == 0xFC) + ? data.ConvertPCtoSnes(offset) >> 16 + : data.GetDataBank(offset); + var operand = data.GetRomWord(offset + 1); + if (!operand.HasValue) + return -1; + + return (bank << 16) | (int)operand; + } case AddressMode.AddressIndirect: case AddressMode.AddressLongIndirect: - operand = data.GetRomWord(offset + 1); + { + var operand = data.GetRomWord(offset + 1) ?? -1; return operand; + } case AddressMode.Long: case AddressMode.LongXIndex: - operand = data.GetRomLong(offset + 1); + { + var operand = data.GetRomLong(offset + 1) ?? -1; return operand; + } case AddressMode.Relative8: + { programCounter = data.ConvertPCtoSnes(offset + 2); bank = programCounter >> 16; - offset = (sbyte)data.GetRomByte(offset + 1); - return (bank << 16) | ((programCounter + offset) & 0xFFFF); + var romByte = data.GetRomByte(offset + 1); + if (!romByte.HasValue) + return -1; + + return (bank << 16) | ((programCounter + (sbyte)romByte) & 0xFFFF); + } case AddressMode.Relative16: + { programCounter = data.ConvertPCtoSnes(offset + 3); bank = programCounter >> 16; - offset = (short)data.GetRomWord(offset + 1); - return (bank << 16) | ((programCounter + offset) & 0xFFFF); + var romByte = data.GetRomWord(offset + 1); + if (!romByte.HasValue) + return -1; + + return (bank << 16) | ((programCounter + (short)romByte) & 0xFFFF); + } } return -1; } - public string GetInstruction(int offset) + #if DIZ_3_BRANCH + // get a compiled byte entry representing all info at an offset contained in any layer. + // return null if there's no entries in that index + // input: ROM offset + // new code should be migrated to use this instead of GetRomByte() + private static ByteEntry GetByteEntryRom(Data data, int romOffset) { - AddressMode mode = GetAddressMode(data, offset); - string format = GetInstructionFormatString(offset); - string mnemonic = GetMnemonic(offset); - string op1, op2 = ""; - if (mode == AddressMode.BlockMove) - { - op1 = Util.NumberToBaseString(data.GetRomByte(offset + 1), Util.NumberBase.Hexadecimal, 2, true); - op2 = Util.NumberToBaseString(data.GetRomByte(offset + 2), Util.NumberBase.Hexadecimal, 2, true); - } - else if (mode == AddressMode.Constant8 || mode == AddressMode.Immediate8) + return GetByteEntrySnes(data, data.ConvertPCtoSnes(romOffset)); + } + + // get a compiled byte entry representing all info at an offset contained in any layer. + // return null if there's no entries in that index + // input: SNES address + // new code should be migrated to use this instead of GetSnesByte() + private static ByteEntry GetByteEntrySnes(Data data, int snesAddress) + { + return data.BuildFlatByteEntryForSnes(snesAddress); + } + #endif + + public override string GetInstruction(Data data, int offset) + { + var mode = GetAddressMode(data, offset); + if (mode == null) + throw new InvalidDataException("Expected non-null mode"); + + var format = GetInstructionFormatString(data, offset); + var mnemonic = GetMnemonic(data, offset); + + int numDigits1 = 0, numDigits2 = 0; + int? value1 = null, value2 = null; + var identified = false; + + switch (mode) { - op1 = Util.NumberToBaseString(data.GetRomByte(offset + 1), Util.NumberBase.Hexadecimal, 2, true); + case AddressMode.BlockMove: + identified = true; + numDigits1 = numDigits2 = 2; + value1 = data.GetRomByte(offset + 1); + value2 = data.GetRomByte(offset + 2); + break; + case AddressMode.Constant8: + case AddressMode.Immediate8: + identified = true; + numDigits1 = 2; + value1 = data.GetRomByte(offset + 1); + break; + case AddressMode.Immediate16: + identified = true; + numDigits1 = 4; + value1 = data.GetRomWord(offset + 1); + break; } - else if (mode == AddressMode.Immediate16) + + string op1, op2 = ""; + if (identified) { - op1 = Util.NumberToBaseString(data.GetRomWord(offset + 1), Util.NumberBase.Hexadecimal, 4, true); + op1 = CreateHexStr(value1, numDigits1); + op2 = CreateHexStr(value2, numDigits2); } else { // dom note: this is where we could inject expressions if needed. it gives stuff like "$F001". // we could substitute our expression of "$#F000 + $#01" or "some_struct.member" like "player.hp" // the expression must be verified to always match the bytes in the file [unless we allow overriding] - op1 = FormatOperandAddress(offset, mode); + op1 = FormatOperandAddress(data, offset, mode.Value); } + return string.Format(format, mnemonic, op1, op2); } - public int GetInstructionLength(int offset) + private static string CreateHexStr(int? v, int numDigits) + { + if (numDigits == 0) + return ""; + + if (v == null) + throw new InvalidDataException("Expected non-null input value, got null"); + + return Util.NumberToBaseString((int) v, Util.NumberBase.Hexadecimal, numDigits, true); + } + + public override int GetInstructionLength(Data data, int offset) { var mode = GetAddressMode(data, offset); - return InstructionLength(mode); + + // not sure if this is the right thing. probably fine, if we hit this. + // we're in a weird mess anyway. + if (mode == null) + return 1; + + return GetInstructionLength(mode.Value); + } + + public override void MarkInOutPoints(Data data, int offset) + { + var opcode = data.GetRomByte(offset); + var iaOffsetPc = data.ConvertSnesToPc(data.GetIntermediateAddress(offset, true)); + + // set read point on EA + if (iaOffsetPc >= 0 && ( // these are all read/write/math instructions + ((opcode & 0x04) != 0) || ((opcode & 0x0F) == 0x01) || ((opcode & 0x0F) == 0x03) || + ((opcode & 0x1F) == 0x12) || ((opcode & 0x1F) == 0x19)) && + (opcode != 0x45) && (opcode != 0x55) && (opcode != 0xF5) && (opcode != 0x4C) && + (opcode != 0x5C) && (opcode != 0x6C) && (opcode != 0x7C) && (opcode != 0xDC) && (opcode != 0xFC) + ) data.SetInOutPoint(iaOffsetPc, InOutPoint.ReadPoint); + + // set end point on offset + if (opcode == 0x40 || opcode == 0x4C || opcode == 0x5C || opcode == 0x60 // RTI JMP JML RTS + || opcode == 0x6B || opcode == 0x6C || opcode == 0x7C || opcode == 0x80 // RTL JMP JMP BRA + || opcode == 0x82 || opcode == 0xDB || opcode == 0xDC // BRL STP JML + ) data.SetInOutPoint(offset, InOutPoint.EndPoint); + + // set out point on offset + // set in point on EA + if (iaOffsetPc >= 0 && ( + opcode == 0x4C || opcode == 0x5C || opcode == 0x80 || opcode == 0x82 // JMP JML BRA BRL + || opcode == 0x10 || opcode == 0x30 || opcode == 0x50 || opcode == 0x70 // BPL BMI BVC BVS + || opcode == 0x90 || opcode == 0xB0 || opcode == 0xD0 || opcode == 0xF0 // BCC BCS BNE BEQ + || opcode == 0x20 || opcode == 0x22)) // JSR JSL + { + data.SetInOutPoint(offset, InOutPoint.OutPoint); + data.SetInOutPoint(iaOffsetPc, InOutPoint.InPoint); + } } - public static int InstructionLength(AddressMode mode) + private static int GetInstructionLength(AddressMode mode) { switch (mode) { @@ -209,75 +499,55 @@ public static int InstructionLength(AddressMode mode) case AddressMode.Long: case AddressMode.LongXIndex: return 4; - } - - return 1; - } - - public void MarkInOutPoints(int offset) - { - int opcode = data.GetRomByte(offset); - int iaOffsetPc = data.ConvertSnesToPc(data.GetIntermediateAddress(offset, true)); - - // set read point on EA - if (iaOffsetPc >= 0 && ( // these are all read/write/math instructions - ((opcode & 0x04) != 0) || ((opcode & 0x0F) == 0x01) || ((opcode & 0x0F) == 0x03) || - ((opcode & 0x1F) == 0x12) || ((opcode & 0x1F) == 0x19)) && - (opcode != 0x45) && (opcode != 0x55) && (opcode != 0xF5) && (opcode != 0x4C) && - (opcode != 0x5C) && (opcode != 0x6C) && (opcode != 0x7C) && (opcode != 0xDC) && (opcode != 0xFC) - ) data.SetInOutPoint(iaOffsetPc, InOutPoint.ReadPoint); - - // set end point on offset - if (opcode == 0x40 || opcode == 0x4C || opcode == 0x5C || opcode == 0x60 // RTI JMP JML RTS - || opcode == 0x6B || opcode == 0x6C || opcode == 0x7C || opcode == 0x80 // RTL JMP JMP BRA - || opcode == 0x82 || opcode == 0xDB || opcode == 0xDC // BRL STP JML - ) data.SetInOutPoint(offset, InOutPoint.EndPoint); - - // set out point on offset - // set in point on EA - if (iaOffsetPc >= 0 && ( - opcode == 0x4C || opcode == 0x5C || opcode == 0x80 || opcode == 0x82 // JMP JML BRA BRL - || opcode == 0x10 || opcode == 0x30 || opcode == 0x50 || opcode == 0x70 // BPL BMI BVC BVS - || opcode == 0x90 || opcode == 0xB0 || opcode == 0xD0 || opcode == 0xF0 // BCC BCS BNE BEQ - || opcode == 0x20 || opcode == 0x22)) // JSR JSL - { - data.SetInOutPoint(offset, InOutPoint.OutPoint); - data.SetInOutPoint(iaOffsetPc, InOutPoint.InPoint); + default: + return 1; } } - private string FormatOperandAddress(int offset, AddressMode mode) + private string FormatOperandAddress(IReadOnlySnesRom data, int offset, AddressMode mode) { - int address = data.GetIntermediateAddress(offset); + var address = data.GetIntermediateAddress(offset); if (address < 0) return ""; - var label = data.GetLabelName(address); + var label = data.Labels.GetLabelName(address); if (label != "") return label; var count = BytesToShow(mode); - if (mode == AddressMode.Relative8 || mode == AddressMode.Relative16) address = data.GetRomWord(offset + 1); + if (mode == AddressMode.Relative8 || mode == AddressMode.Relative16) + { + var romWord = data.GetRomWord(offset + 1); + if (!romWord.HasValue) + return ""; + + address = (int)romWord; + } + address &= ~(-1 << (8 * count)); return Util.NumberToBaseString(address, Util.NumberBase.Hexadecimal, 2 * count, true); } - private string GetMnemonic(int offset, bool showHint = true) + private string GetMnemonic(IReadOnlyCpuOperableByteSource data, int offset, bool showHint = true) { - var mn = Mnemonics[data.GetRomByte(offset)]; + var mn = Mnemonics[data.GetRomByteUnsafe(offset)]; if (!showHint) return mn; var mode = GetAddressMode(data, offset); - var count = BytesToShow(mode); + if (mode == null) + return mn; + + var count = BytesToShow(mode.Value); - if (mode == AddressMode.Constant8 || mode == AddressMode.Relative16 || mode == AddressMode.Relative8) return mn; + if (mode == AddressMode.Constant8 || mode == AddressMode.Relative16 || mode == AddressMode.Relative8) + return mn; return count switch { - 1 => mn += ".B", - 2 => mn += ".W", - 3 => mn += ".L", + 1 => mn + ".B", + 2 => mn + ".W", + 3 => mn + ".L", _ => mn }; } @@ -319,7 +589,7 @@ private static int BytesToShow(AddressMode mode) // {0} = mnemonic // {1} = intermediate address / label OR operand 1 for block move // {2} = operand 2 for block move - private string GetInstructionFormatString(int offset) + private string GetInstructionFormatString(IReadOnlyCpuOperableByteSource data, int offset) { var mode = GetAddressMode(data, offset); switch (mode) @@ -367,14 +637,17 @@ private string GetInstructionFormatString(int offset) } return ""; } - - public static AddressMode GetAddressMode(Data data, int offset) + + public static AddressMode? GetAddressMode(IReadOnlyCpuOperableByteSource data, int offset) { var opcode = data.GetRomByte(offset); + if (!opcode.HasValue) + return null; + var mFlag = data.GetMFlag(offset); var xFlag = data.GetXFlag(offset); - return GetAddressMode(opcode, mFlag, xFlag); + return GetAddressMode(opcode.Value, mFlag, xFlag); } public static AddressMode GetAddressMode(int opcode, bool mFlag, bool xFlag) diff --git a/Diz.Core/arch/CpuOperations.cs b/Diz.Core/arch/CpuOperations.cs new file mode 100644 index 00000000..45850b9b --- /dev/null +++ b/Diz.Core/arch/CpuOperations.cs @@ -0,0 +1,20 @@ +using Diz.Core.model; + +namespace Diz.Core.arch +{ + public interface IReadOnlyCpuOperableByteSource : IReadOnlyByteSource + { + public Architecture GetArchitecture(int i); + public FlagType GetFlag(int i); + int GetMxFlags(int i); + + bool GetMFlag(int i); + bool GetXFlag(int i); + } + + public interface ICpuOperableByteSource : IReadOnlyCpuOperableByteSource + { + void SetMxFlags(int i, int mx); + int Step(int offset, bool branch, bool force, int prevOffset); + } +} \ No newline at end of file diff --git a/Diz.Core/commands/Commands.cs b/Diz.Core/commands/Commands.cs index 551d3d83..6134a598 100644 --- a/Diz.Core/commands/Commands.cs +++ b/Diz.Core/commands/Commands.cs @@ -1,10 +1,10 @@ namespace Diz.Core.commands { - public class MarkCommand + public record MarkCommand { - public int Property { get; set; } - public int Start { get; set; } - public int Count { get; set; } - public object Value { get; set; } + public int Property { get; init; } + public int Start { get; init; } + public int Count { get; init; } + public object Value { get; init; } } } \ No newline at end of file diff --git a/Diz.Core/datasubset/DataSubset.cs b/Diz.Core/datasubset/DataSubset.cs new file mode 100644 index 00000000..0e10af53 --- /dev/null +++ b/Diz.Core/datasubset/DataSubset.cs @@ -0,0 +1,233 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel; +using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; +using System.Runtime.CompilerServices; +using Diz.Core.util; +using JetBrains.Annotations; + +namespace Diz.Core.datasubset +{ + public interface IDataSubsetLoader + { + // provide a row (either retrieve from cache or make a new one, either way) + TRow RowValueNeeded(int largeOffset, DataSubset subset); + + void OnBigWindowChangeStart(DataSubset subset); + void OnBigWindowChangeFinished(DataSubset subset); + } + + public class DataSubset : INotifyPropertyChangedExt + { + // the full range of items to pick from. + // anything that deals with "largeIndex" refers to an index into THIS list. + // + // note: client may filter or sort this list ahead of time, it doesn't have to be 1:1 with the underlying data + public List Items + { + get => items; + set + { + DropRowCache(); + this.SetField(PropertyChanged, ref items, value); + } + } + + public IDataSubsetLoader RowLoader { get; init; } + + // rows (relative) + public int StartingRowLargeIndex + { + get => startingRowLargeIndex; + set + { + if (Items == null) + throw new ArgumentException("RomBytes must be set before setting view dimensions"); + + if (!IsValidLargeOffset(value)) + throw new ArgumentException("StartingRowLargeIndex is out of range"); + + // validate window range is OK. + if (value + RowCount > Items.Count) + throw new ArgumentException("Window size is out of range"); + + UpdateDimensions(RowCount, value, + () => this.SetField(PropertyChanged, ref startingRowLargeIndex, value)); + } + } + + // zero is OK. + public int RowCount + { + get => rowCount; + set + { + if (value < 0) + throw new ArgumentOutOfRangeException(nameof(RowCount)); + + if (Items == null) + throw new ArgumentException("RomBytes must be set before setting view dimensions"); + + if (value != 0 && !IsValidLargeOffset(value - 1)) + throw new ArgumentException("Count out of range"); + + // validate window range is OK. + if (!IsValidLargeOffset(StartingRowLargeIndex)) + throw new ArgumentException("starting large index is out of range"); + + if (StartingRowLargeIndex + value > Items.Count) + { + EndingRowLargeIndex = Items.Count - 1; + } + + UpdateDimensions(value, StartingRowLargeIndex, + () => this.SetField(PropertyChanged, ref rowCount, value)); + } + } + + protected virtual void UpdateDimensions(int newRowCount, int newStartingRowLargeIndex, Action updateAction) + { + if (newRowCount != RowCount || newStartingRowLargeIndex != StartingRowLargeIndex) + OnWindowDimensionsChanging(newStartingRowLargeIndex, newRowCount); + + updateAction(); + } + + // called right before we change StartingRowLargeIndex and RowCount + [SuppressMessage("ReSharper", "UnusedParameter.Local")] + private void OnWindowDimensionsChanging(int newRowStartingIndex, int newRowCount) + { + DropRowCache(); + } + + public int EndingRowLargeIndex + { + get => StartingRowLargeIndex + RowCount - 1; + set => StartingRowLargeIndex = value - RowCount + 1; + } + + // main idea here is, this list never changes until we scroll, in which case we drop and re-add + // everything. recalculating this list should never do anything that involves a lot of processing. + // instead, we'll leave the heavy lifting to cachedRows, which can do fancier things if needed + // like predict which rows might be needed later. + public List OutputRows + { + get + { + if (outputRows != null) + return outputRows; + + DropRowCache(); + CacheRows(); + + return outputRows; + } + } + + private int startingRowLargeIndex; + private int rowCount; + private List outputRows; + private List items; + + // this only ever needs to happen when the dimensions change + // if startingIndex and count don't change, this doesn't need to be recalculated. + private void CacheRows() + { + Debug.Assert(outputRows == null); + + outputRows = new List(RowCount); + + RowLoader.OnBigWindowChangeStart(this); + for (var i = StartingRowLargeIndex; i < StartingRowLargeIndex + RowCount; ++i) + { + var newRow = RowValueNeededForLargeOffset(i); + outputRows.Add(newRow); + } + SetNotifyChangedForAllRows(register: true); + RowLoader.OnBigWindowChangeFinished(this); + } + + private void DropRowCache() + { + if (outputRows == null) + return; + + SetNotifyChangedForAllRows(register: false); + + outputRows = null; + } + + private void SetNotifyChangedForAllRows(bool register) + { + foreach (var row in outputRows) + { + if (!(row is INotifyPropertyChanged iNotify)) + continue; + + if (register) + iNotify.PropertyChanged += OnRowPropertyChanged; + else + iNotify.PropertyChanged -= OnRowPropertyChanged; + } + } + + private void OnRowPropertyChanged(object sender, PropertyChangedEventArgs e) + { + // underlying data in one of the visible rows just changed, so pass that along so + // listeners can get a notification that they should refresh the data. + + PropertyChanged?.Invoke(sender, e); + } + + // key thing: this class (DataSubset) will itself cache the current outputRows + // which don't need to change as long as the view doesn't change. + // + // RowLoader's job is: + // - must cache all visible rows + // - optionally, selectively cache some rows no longer in view anymore + // + // the goal is: for small amounts of scrolling, make sure repopulating outputRows + // is a quick operation. this will be true if RowLoader does a good job saving recently + // cached rows and predicting which ones might be needed soon. + // + // this also keeps the complex caching logic can stay out of this class and in RowLoader. + // + // example: if a user is looking at 10 rows in the middle of a 100 count data source, + // the screen GUI only need 10 row objects to exist. however, RowLoader might choose to also cache + // an extra +/- 25 most recently used and rows that might be probably used in the near future. + // so if the user is scrolling around in the same area, they might hit some of the non-visible cache + // when rows are needed. that will speed up the GUI operations. + // + // if needed, in the future, predictive row caching could be done on a background thread as well. + protected TRow RowValueNeededForLargeOffset(int largeOffset) => + RowLoader.RowValueNeeded(largeOffset, this); + + public bool IsRowOffsetValid(int rowOffset) => + rowOffset >= 0 && rowOffset < RowCount; + + public bool IsLargeOffsetContainedInVisibleRows(int largeOffset) => + largeOffset >= startingRowLargeIndex && largeOffset <= EndingRowLargeIndex; + + public bool IsValidLargeOffset(int largeOffset) => + largeOffset >= 0 && largeOffset < Items?.Count; + + public int GetRowIndexFromLargeOffset(int largeOffset) => + !IsLargeOffsetContainedInVisibleRows(largeOffset) + ? -1 + : largeOffset - startingRowLargeIndex; + + public int GetLargeOffsetFromRowOffset(int rowOffset) => + !IsRowOffsetValid(rowOffset) + ? -1 + : rowOffset + startingRowLargeIndex; + + public event PropertyChangedEventHandler PropertyChanged; + + [NotifyPropertyChangedInvocator] + public void OnPropertyChanged([CallerMemberName] string propertyName = null) + { + PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)); + } + } +} \ No newline at end of file diff --git a/Diz.Core/datasubset/DataSubsetSupport.cs b/Diz.Core/datasubset/DataSubsetSupport.cs new file mode 100644 index 00000000..2e8b6253 --- /dev/null +++ b/Diz.Core/datasubset/DataSubsetSupport.cs @@ -0,0 +1,118 @@ +using System.Collections.Generic; +using System.Linq; + +namespace Diz.Core.datasubset +{ + public abstract class DataSubsetLoader : IDataSubsetLoader + { + public class Entry + { + public TRow Row { get; set; } + public int AgeScore { get; set; } // 0 = newer, higher = older + } + + // map large data index offset to a row + // + // sometimes this will contain extra or not enough rows, and we'll page them in and out as needed. + // this dictionary ALWAYS includes all of the currently displayed rows, + // but also, can include more cached rows that we can kick out as needed to save memory. + private readonly Dictionary cachedRows = new(); + + // tune as needed. if user can see about 20 rows at a time, we'll keep around 10x that in memory. + // if cached rows are in memory, it'll make small scrolling (like bouncing around near the same + // couple of rows) already cached + // + // this can be jacked WAY up with little effect except using more memory. + // hike it if you need more perf. + public int TargetCachedMultiplier { get; init; } = 15; + + public int TargetCachedRows { get; protected set; } + + // we'll allow going a certain percentage over the target before cleaning up. + // that way we're only cleaning up in chunks and not in individual rows. + public int FuzzThreshold => (int)(TargetCachedRows / (float)TargetCachedMultiplier); + + public virtual void OnBigWindowChangeStart(DataSubset subset) + { + TargetCachedRows = subset.RowCount * TargetCachedMultiplier; + + IncrementAllAgeScores(); + } + + private void IncrementAllAgeScores() + { + foreach (var entry in cachedRows) + entry.Value.AgeScore++; + } + + public virtual TRow RowValueNeeded(int largeOffset, DataSubset subset) + { + var entry = GetOrCreateRowEntry(largeOffset, subset); + entry.AgeScore = 0; // any recent rows will always be aged at zero + return entry.Row; + } + + private Entry GetOrCreateRowEntry(int largeIndex, DataSubset subset) + { + if (cachedRows.TryGetValue(largeIndex, out var entry)) + return entry; + + // assume this creation is expensive, we're optimizing to minimize # initializations here + entry = new Entry + { + Row = CreateNewRow(subset, largeIndex), + }; + + cachedRows[largeIndex] = entry; + return entry; + } + + protected abstract TRow CreateNewRow(DataSubset subset, int largeIndex); + + // this is a hint that big changes just finished up (like recreating the rows due to a scroll), + // so it's likely a good time to kick irrelevant rows out of the cache. + // + // we could do a bunch of clever stuff, I'm just going to a really simple age check + // and kick out the oldest rows (rows that haven't been in any view for a while) + // which are furthest away from the current window + public virtual void OnBigWindowChangeFinished(DataSubset subset) + { + // see if we're about 10% over our target, and if so, dump about 10% of the cache. + // it's OK to go over so that we're not constantly dumping cache with every small change. + if (cachedRows.Count <= TargetCachedRows + FuzzThreshold) + return; + + // we're over our target, so start dropping the oldest least useful stuff from the cache + var oldestDeletionCandidates = (from kvp in cachedRows + where kvp.Value.AgeScore != 0 + orderby kvp.Value.AgeScore descending + select kvp).Take(FuzzThreshold).ToList(); + + foreach (var entry in oldestDeletionCandidates) + { + cachedRows.Remove(entry.Key); + } + } + } + + public class DataSubsetSimpleLoader : IDataSubsetLoader where TRow : new() + { + // no caching, just create a new row as needed each time + // if performance is an issue, use another strategy. + public TRow RowValueNeeded(int largeOffset, DataSubset subset) + { + TRow newRow = new(); + + PopulateRow?.Invoke(ref newRow, largeOffset); + + return newRow; + } + + public delegate void PopulateNewlyCreatedRow(ref TRow newlyCreatedRow, int largeIndex); + + public PopulateNewlyCreatedRow PopulateRow { get; set; } + + public void OnBigWindowChangeStart(DataSubset subset) {} + public void OnBigWindowChangeFinished(DataSubset subset) {} + } +} \ No newline at end of file diff --git a/Diz.Core/datasubset/DataSubsetWithSelection.cs b/Diz.Core/datasubset/DataSubsetWithSelection.cs new file mode 100644 index 00000000..6e0e0817 --- /dev/null +++ b/Diz.Core/datasubset/DataSubsetWithSelection.cs @@ -0,0 +1,111 @@ +using System; +using System.Diagnostics; +using Diz.Core.util; + +namespace Diz.Core.datasubset +{ + // controls what rows are visible and scrolls intelligently based on our offset + public class DataSubsetWithSelection : DataSubset + { + public TRow SelectedRow => + RowValueNeededForLargeOffset(SelectedLargeIndex); + + public int SelectedRowIndex => + GetRowIndexFromLargeOffset(SelectedLargeIndex); + + // when set: when the start or end range changes, the selected row will be + // clamped to be within the Start..End range. + public bool WindowResizeKeepsSelectionInRange + { + get => windowResizeKeepsSelectionInRange; + set + { + this.SetField(ref windowResizeKeepsSelectionInRange, value); + ClampSelectionIfNeeded(); + } + } + private bool windowResizeKeepsSelectionInRange; + + // when set: when the selection is changed, the start and end points will move + // to keep the selection inside the range. + public bool EnsureBoundariesEncompassWhenSelectionChanges + { + get => ensureBoundariesEncompassWhenSelectionChanges; + set + { + this.SetField(ref ensureBoundariesEncompassWhenSelectionChanges, value); + EnsureViewContainsLargeIndex(SelectedLargeIndex); + } + } + private bool ensureBoundariesEncompassWhenSelectionChanges = true; + + private void EnsureViewContainsLargeIndex(int largeIndex) + { + if (RowCount == 0) + return; + + Debug.Assert(IsValidLargeOffset(largeIndex)); + + if (largeIndex < StartingRowLargeIndex) + { + StartingRowLargeIndex = largeIndex; + } + else if (largeIndex > EndingRowLargeIndex) + { + EndingRowLargeIndex = largeIndex; + } + } + + public int SelectedLargeIndex + { + get => selectedLargeIndex; + set + { + if (!IsValidLargeOffset(value)) + throw new ArgumentException("Invalid large value"); + + // var clampedValue = GetClampedIndexIfNeeded(value); + + if (NotifyPropertyChangedExtensions.FieldIsEqual(selectedLargeIndex, value)) + return; + + selectedLargeIndex = value; + + EnsureViewContainsSelectionIfNeeded(); + + OnPropertyChanged(); + } + } + + public int LargestPossibleStartingLargeIndex => Items.Count - RowCount; + + private int selectedLargeIndex; + + + private void EnsureViewContainsSelectionIfNeeded() + { + if (ensureBoundariesEncompassWhenSelectionChanges) + EnsureViewContainsLargeIndex(SelectedLargeIndex); + } + + private void ClampSelectionIfNeeded() => + SelectedLargeIndex = GetClampedIndexIfNeeded(SelectedLargeIndex); + + private int GetClampedIndexIfNeeded(int largeIndex) => + !windowResizeKeepsSelectionInRange + ? largeIndex + : GetLargeIndexClampedToVisibleRows(largeIndex); + + public int GetLargeIndexClampedToVisibleRows(int largeIndexToClamp) => + Util.Clamp(largeIndexToClamp, StartingRowLargeIndex, EndingRowLargeIndex); + + public void SelectRow(int rowIndex) => + SelectedLargeIndex = GetLargeOffsetFromRowOffset(rowIndex); + + protected override void UpdateDimensions(int newRowCount, int newStartingRowLargeIndex, Action updateAction) + { + base.UpdateDimensions(newRowCount, newStartingRowLargeIndex, updateAction); + ClampSelectionIfNeeded(); + } + } +} \ No newline at end of file diff --git a/Diz.Core/export/LogCreator.Setup.cs b/Diz.Core/export/LogCreator.Setup.cs deleted file mode 100644 index f39ca201..00000000 --- a/Diz.Core/export/LogCreator.Setup.cs +++ /dev/null @@ -1,176 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Diagnostics; -using System.IO; -using System.Linq; -using System.Reflection; -using Diz.Core.model; - -namespace Diz.Core.export -{ - // setup-specific parts of LogCreator - // TODO: probably should move this stuff into a helper class so it's not a partial - public partial class LogCreator - { - protected class AssemblerHandler : Attribute - { - public string Token; - public int Length; - } - - public class OutputResult - { - public bool Success; - public int ErrorCount = -1; - public LogCreator LogCreator; - public string OutputStr = ""; // only set if outputString=true - } - - - // DONT use directly [except to setup the caching] - private static Dictionary> _parametersCache; - - // SAFE to use directly. - protected static Dictionary> Parameters - { - get - { - CacheAssemblerAttributeInfo(); - return _parametersCache; - } - } - - protected static void CacheAssemblerAttributeInfo() - { - if (_parametersCache != null) - return; - - _parametersCache = new Dictionary>(); - - var methodsWithAttributes = typeof(LogCreator) - .GetMethods(BindingFlags.NonPublic | BindingFlags.Instance) - .Where( - x => x.GetCustomAttributes(typeof(AssemblerHandler), false).FirstOrDefault() != null - ); - - foreach (var method in methodsWithAttributes) - { - var assemblerHandler = method.GetCustomAttribute(); - var token = assemblerHandler.Token; - var length = assemblerHandler.Length; - - // check your method signature if you hit this stuff. - Debug.Assert(method.GetParameters().Length == 2); - - Debug.Assert(method.GetParameters()[0].ParameterType == typeof(int)); - Debug.Assert(method.GetParameters()[0].Name == "offset" || method.GetParameters()[0].Name == "snesOffset"); - - Debug.Assert(method.GetParameters()[1].ParameterType == typeof(int)); - Debug.Assert(method.GetParameters()[1].Name == "length"); - - Debug.Assert(method.ReturnType == typeof(string)); - - _parametersCache.Add(token, (new Tuple(method, length))); - } - - Debug.Assert(_parametersCache.Count != 0); - } - - protected string GetParameter(int offset, string parameter, int length) - { - if (!Parameters.TryGetValue(parameter, out var methodAndLength)) - { - throw new InvalidDataException($"Unknown parameter: {parameter}"); - } - - var methodInfo = methodAndLength.Item1; - var callParams = new object[] { offset, length }; - - var returnValue = methodInfo.Invoke(this, callParams); - - Debug.Assert(returnValue is string); - return returnValue as string; - } - - public void AddExtraLabel(int i, Label v) - { - Debug.Assert(v != null); - if (ExtraLabels.ContainsKey(i)) - return; - - v.CleanUp(); - - ExtraLabels.Add(i, v); - } - - // Generate labels like "CODE_856469" and "DATA_763525" - // These will be combined with the original labels to produce our final assembly - // These labels exist only for the duration of this export, and then are discarded. - // - // TODO: generate some nice looking "+"/"-" labels here. - protected void GenerateAdditionalExtraLabels() - { - if (Settings.Unlabeled == FormatUnlabeled.ShowNone) - return; - - for (var pointer = 0; pointer < Data.GetRomSize(); pointer += GetLineByteLength(pointer)) - { - GenerateLabelIfNeededAt(pointer); - } - } - - private void GenerateLabelIfNeededAt(int pcoffset) - { - var snes = GetAddressOfAnyUsefulLabelsAt(pcoffset); - if (snes == -1) - return; - - var labelName = Data.GetDefaultLabel(snes); - AddExtraLabel(snes, new Label() { - Name = labelName, - }); - } - - protected int GetAddressOfAnyUsefulLabelsAt(int pcoffset) - { - if (Settings.Unlabeled == FormatUnlabeled.ShowAll) - return Data.ConvertPCtoSnes(pcoffset); // this may not be right either... - - var flag = Data.GetFlag(pcoffset); - var usefulToCreateLabelFrom = - flag == FlagType.Opcode || flag == FlagType.Pointer16Bit || - flag == FlagType.Pointer24Bit || flag == FlagType.Pointer32Bit; - - if (!usefulToCreateLabelFrom) - return -1; - - var snesIa = Data.GetIntermediateAddressOrPointer(pcoffset); - var pc = Data.ConvertSnesToPc(snesIa); - return pc >= 0 ? snesIa : -1; - } - - public static bool ValidateFormat(string formatString) - { - var tokens = formatString.ToLower().Split('%'); - - // not valid if format has an odd amount of %s - if (tokens.Length % 2 == 0) return false; - - for (int i = 1; i < tokens.Length; i += 2) - { - int indexOfColon = tokens[i].IndexOf(':'); - string kind = indexOfColon >= 0 ? tokens[i].Substring(0, indexOfColon) : tokens[i]; - - // not valid if base token isn't one we know of - if (!Parameters.ContainsKey(kind)) - return false; - - // not valid if parameter isn't an integer - if (indexOfColon >= 0 && !int.TryParse(tokens[i].Substring(indexOfColon + 1), out _)) - return false; - } - - return true; - } - } -} \ No newline at end of file diff --git a/Diz.Core/export/LogCreator.cs b/Diz.Core/export/LogCreator.cs deleted file mode 100644 index 771dd66c..00000000 --- a/Diz.Core/export/LogCreator.cs +++ /dev/null @@ -1,676 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Diagnostics; -using Diz.Core.model; -using Diz.Core.util; -using IX.Observable; - -namespace Diz.Core.export -{ - public partial class LogCreator - { - public enum FormatUnlabeled - { - ShowAll = 0, - ShowInPoints = 1, // TODO Add Show In Points with +/- labels - ShowNone = 2 - } - - public enum FormatStructure - { - SingleFile = 0, - OneBankPerFile = 1 - } - - public LogWriterSettings Settings { get; set; } - public Data Data { get; set; } - - protected LogCreatorOutput Output; - protected Dictionary ExtraLabels { get; set; } = new Dictionary(); - protected List> ParseList; - protected List LabelsWeVisited; - protected int BankSize; - protected ObservableDictionary BackupOfOriginalLabelsBeforeModifying; - - public virtual OutputResult CreateLog() - { - OutputResult result; - - try - { - Init(); - WriteLog(); - result = GetResult(); - CloseOutput(result); - } - finally - { - Cleanup(); // critical to ALWAYS do this so we restore labels - } - - return result; - } - - protected virtual void Init() - { - InitOutput(); - SetupParseList(); - - BankSize = RomUtil.GetBankSize(Data.RomMapMode); - ErrorCount = 0; - LabelsWeVisited = new List(); - - GenerateAdditionalExtraLabels(); - WriteGeneratedLabelsIntoUnderlyingData(); // MODIFIES DATA. MAKE SURE TO UNDO THIS. - } - - private void InitOutput() - { - if (Settings.OutputToString) - Output = new LogCreatorStringOutput(); - else - Output = new LogCreatorStreamOutput(); - - Output.Init(this); - } - - protected virtual void Cleanup() - { - // restore original labels. SUPER IMPORTANT THIS HAPPENS WHen WE'RE DONE - // TODO: make this unnecessary by not modifying the underlying data. - RestoreUnderlyingDataLabels(); - } - - protected void RestoreUnderlyingDataLabels() - { - // SUPER IMPORTANT. THIS MUST GET DONE, ALWAYS. PROTECT THIS WITH TRY/CATCH - - if (BackupOfOriginalLabelsBeforeModifying != null) - Data.Labels = BackupOfOriginalLabelsBeforeModifying; - - BackupOfOriginalLabelsBeforeModifying = null; - } - - - private void CloseOutput(OutputResult result) - { - Output?.Finish(result); - Output = null; - } - - private OutputResult GetResult() - { - var result = new OutputResult() - { - ErrorCount = ErrorCount, - Success = true, - LogCreator = this - }; - - if (Settings.OutputToString) - result.OutputStr = ((LogCreatorStringOutput)Output)?.OutputString; - - return result; - } - - protected void WriteGeneratedLabelsIntoUnderlyingData() - { - // WARNING: THIS MODIFIES THE UNDERLYING DATA TO ADD MORE LABELS - // *** if not properly cleaned up, the original project file's label list can get trashed. *** - // always call this FN with a try/finally that cleans up after. - - // TODO: I really don't like us modifying and restoring the - // underlying labels or anything in Data. Data should ideally be immutable by us. - // we should either clone all of Data before modifying, or generate these labels on the fly. - BackupOfOriginalLabelsBeforeModifying = Data.Labels; - Data.Labels = new ObservableDictionary(Data.Labels); - - // write the new generated labels in, don't let them overwrite any real labels - // i.e. if the user defined a label like "PlayerSwimmingSprites", and our auto-generated - // labels also contain a label at the same address, then ignore our auto-generated label, - // only use the explicit user-created label. - foreach (var label in ExtraLabels) - { - Data.AddLabel(label.Key, label.Value, false); - } - } - - public int GetRomSize() - { - return Settings.RomSizeOverride != -1 ? Settings.RomSizeOverride : Data.GetRomSize(); - } - - protected virtual void WriteLog() - { - var size = GetRomSize(); - - WriteMainIncludes(size); - var pointer = WriteMainAssembly(size); - WriteLabels(pointer); - } - - private int WriteMainAssembly(int size) - { - // perf: this is the meat of the export, takes a while - var pointer = 0; - var bank = -1; - while (pointer < size) - { - WriteAddress(ref pointer, ref bank); - } - return pointer; - } - - protected void WriteSpecial(string special) - { - const int doesntMatter = 0; - var line = GetLine(doesntMatter, special); - Output.WriteLine(line); - } - - protected void WriteMainIncludes(int size) - { - WriteSpecial("map"); - WriteSpecial("empty"); - WriteMainBankIncludes(size); - } - - private void WriteMainBankIncludes(int size) - { - if (Settings.Structure != FormatStructure.OneBankPerFile) - return; - - for (var i = 0; i < size; i += BankSize) - Output.WriteLine(GetLine(i, "incsrc")); - - Output.WriteLine(GetLine(-1, "incsrc")); - } - - protected void SetupParseList() - { - var split = Settings.Format.Split('%'); - ParseList = new List>(); - for (var i = 0; i < split.Length; i++) - { - if (i % 2 == 0) ParseList.Add(Tuple.Create(split[i], int.MaxValue)); - else - { - var colon = split[i].IndexOf(':'); - - Tuple tuple; - if (colon < 0) - { - var s1 = split[i]; - var succeeded = Parameters.TryGetValue(s1, out var x); - Debug.Assert(succeeded); - var s2 = x.Item2; - - tuple = new Tuple(s1, s2); - } - else - { - tuple = new Tuple(split[i].Substring(0, colon), int.Parse(split[i].Substring(colon + 1))); - } - - ParseList.Add(tuple); - } - } - } - - protected void WriteAddress(ref int pointer, ref int currentBank) - { - SwitchBanksIfNeeded(pointer, ref currentBank); - - WriteBlankLineIfStartingNewParagraph(pointer); - WriteTheRealLine(pointer); - WriteBlankLineIfEndPoint(pointer); - - pointer += GetLineByteLength(pointer); - } - - private void WriteTheRealLine(int pointer) - { - Output.WriteLine(GetLine(pointer, null)); - } - - private void WriteBlankLineIfEndPoint(int pointer) - { - if ((Data.GetInOutPoint(pointer) & InOutPoint.EndPoint) != 0) - Output.WriteLine(GetLine(pointer, "empty")); - } - - private void WriteBlankLineIfStartingNewParagraph(int pointer) - { - var isLocationAReadPoint = (Data.GetInOutPoint(pointer) & InOutPoint.ReadPoint) != 0; - var anyLabelsPresent = Data.Labels.TryGetValue(pointer, out var label) && label.Name.Length > 0; - - if (isLocationAReadPoint || anyLabelsPresent) - Output.WriteLine(GetLine(pointer, "empty")); - } - - private void SwitchBanksIfNeeded(int pointer, ref int currentBank) - { - var snesAddress = Data.ConvertPCtoSnes(pointer); - - var thisBank = snesAddress >> 16; - - if (thisBank == currentBank) - return; - - OpenNewBank(pointer, thisBank); - currentBank = thisBank; - - if (snesAddress % BankSize == 0) - return; - - ReportError(pointer, "An instruction crossed a bank boundary."); - } - - private void OpenNewBank(int pointer, int thisBank) - { - Output.SwitchToBank(thisBank); - - Output.WriteLine(GetLine(pointer, "empty")); - Output.WriteLine(GetLine(pointer, "org")); - Output.WriteLine(GetLine(pointer, "empty")); - } - - protected void WriteLabels(int pointer) - { - var unvisitedLabels = GetUnvisitedLabels(); - WriteAnyUnivisitedLabels(pointer, unvisitedLabels); - PrintAllLabelsIfRequested(pointer, unvisitedLabels); - } - - private Dictionary GetUnvisitedLabels() - { - var unvisitedLabels = new Dictionary(); - - // part 1: important: include all labels we aren't defining somewhere else. needed for disassembly - foreach (var pair in Data.Labels) - { - if (LabelsWeVisited.Contains(pair.Key)) - continue; - - // this label was not defined elsewhere in our disassembly, so we need to include it in labels.asm - unvisitedLabels.Add(pair.Key, pair.Value); - } - - return unvisitedLabels; - } - - private void WriteAnyUnivisitedLabels(int pointer, Dictionary unvisitedLabels) - { - SwitchOutputStream(pointer, "labels"); - - foreach (var pair in unvisitedLabels) - Output.WriteLine(GetLine(pair.Key, "labelassign")); - } - - private void PrintAllLabelsIfRequested(int pointer, Dictionary unvisitedLabels) - { - // part 2: optional: if requested, print all labels regardless of use. - // Useful for debugging, documentation, or reverse engineering workflow. - // this file shouldn't need to be included in the build, it's just reference documentation - - if (!Settings.IncludeUnusedLabels) - return; - - SwitchOutputStream(pointer, "all-labels.txt"); // TODO: csv in the future. escape commas - foreach (var pair in Data.Labels) - { - // not the best place to add formatting, TODO: cleanup - var category = unvisitedLabels.ContainsKey(pair.Key) ? "UNUSED" : "USED"; - Output.WriteLine($";!^!-{category}-! " + GetLine(pair.Key, "labelassign")); - } - } - - protected void SwitchOutputStream(int pointer, string streamName) - { - Output.SwitchToStream(streamName); - - // write an extra blank line if we would normally switch files here - if (Settings.Structure == FormatStructure.SingleFile) - Output.WriteLine(GetLine(pointer, "empty")); - } - - // -------------------------- - #region WriteOperations - - protected string GetLine(int offset, string special) - { - if (special == "empty" && !Settings.OutputExtraWhitespace) - return ""; - - var isSpecial = special != null; - var line = ""; - - foreach (var t in ParseList) - { - if (t.Item2 == int.MaxValue) // string literal - { - line += t.Item1; - } - else if (isSpecial) // special parameter (replaces code & everything else = empty) - { - var v1 = (t.Item1 == "code" ? special : "empty"); - line += GetParameter(offset, $"%{v1}", t.Item2); - } - else // normal parameter - { - line += GetParameter(offset, t.Item1, t.Item2); - } - } - - if (!isSpecial) - CheckForErrorsAtLine(offset); - - return line; - } - - private void CheckForErrorsAtLine(int offset) - { - // throw out some errors if stuff looks fishy - ErrorIfOperand(offset); - ErrorIfAdjacentOperandsSeemWrong(offset); - ErrorIfBranchToNonInstruction(offset); - } - - private FlagType GetFlagButSwapOpcodeForOperand(int offset) - { - var flag = Data.GetFlag(offset); - if (flag == FlagType.Opcode) - return FlagType.Operand; - - return flag; - } - - protected int GetLineByteLength(int offset) - { - int max = 1, step = 1; - var size = Data.GetRomSize(); - - switch (Data.GetFlag(offset)) - { - case FlagType.Opcode: - return Data.OpcodeByteLength(offset); - case FlagType.Unreached: - case FlagType.Operand: - case FlagType.Data8Bit: - case FlagType.Graphics: - case FlagType.Music: - case FlagType.Empty: - max = Settings.DataPerLine; - break; - case FlagType.Text: - max = 21; - break; - case FlagType.Data16Bit: - step = 2; - max = Settings.DataPerLine; - break; - case FlagType.Data24Bit: - step = 3; - max = Settings.DataPerLine; - break; - case FlagType.Data32Bit: - step = 4; - max = Settings.DataPerLine; - break; - case FlagType.Pointer16Bit: - step = 2; - max = 2; - break; - case FlagType.Pointer24Bit: - step = 3; - max = 3; - break; - case FlagType.Pointer32Bit: - step = 4; - max = 4; - break; - } - - int min = step, myBank = offset / BankSize; - while ( - min < max && - offset + min < size && - Data.GetFlag(offset + min) == Data.GetFlag(offset) && - Data.GetLabelName(Data.ConvertPCtoSnes(offset + min)) == "" && - (offset + min) / BankSize == myBank - ) min += step; - return min; - } - - // just a % - [AssemblerHandler(Token = "", Length = 1)] - protected string GetPercent(int offset, int length) - { - return "%"; - } - - // all spaces - [AssemblerHandler(Token = "%empty", Length = 1)] - protected string GetEmpty(int offset, int length) - { - return string.Format("{0," + length + "}", ""); - } - - // trim to length - // negative length = right justified - [AssemblerHandler(Token = "label", Length = -22)] - protected string GetLabel(int offset, int length) - { - // what we're given: a PC offset in ROM. - // what we need to find: any labels (SNES addresses) that refer to it. - // - // i.e. given that we are at PC offset = 0, - // we find valid SNES offsets mirrored of 0xC08000 and 0x808000 which both refer to the same place - // - // TODO: we need to deal with that mirroring here - // TODO: eventually, support multiple labels tagging the same address, it may not always be just one. - - var snesOffset = Data.ConvertPCtoSnes(offset); - var label = Data.GetLabelName(snesOffset); - if (label == null) - return ""; - - LabelsWeVisited.Add(snesOffset); - - var noColon = label.Length == 0 || label[0] == '-' || label[0] == '+'; - return string.Format("{0," + (length * -1) + "}", label + (noColon ? "" : ":")); - } - - // trim to length - [AssemblerHandler(Token = "code", Length = 37)] - protected string GetCode(int offset, int length) - { - var bytes = GetLineByteLength(offset); - string code = ""; - - switch (Data.GetFlag(offset)) - { - case FlagType.Opcode: - code = Data.GetInstruction(offset); - break; - case FlagType.Unreached: - case FlagType.Operand: - case FlagType.Data8Bit: - case FlagType.Graphics: - case FlagType.Music: - case FlagType.Empty: - code = Data.GetFormattedBytes(offset, 1, bytes); - break; - case FlagType.Data16Bit: - code = Data.GetFormattedBytes(offset, 2, bytes); - break; - case FlagType.Data24Bit: - code = Data.GetFormattedBytes(offset, 3, bytes); - break; - case FlagType.Data32Bit: - code = Data.GetFormattedBytes(offset, 4, bytes); - break; - case FlagType.Pointer16Bit: - code = Data.GetPointer(offset, 2); - break; - case FlagType.Pointer24Bit: - code = Data.GetPointer(offset, 3); - break; - case FlagType.Pointer32Bit: - code = Data.GetPointer(offset, 4); - break; - case FlagType.Text: - code = Data.GetFormattedText(offset, bytes); - break; - } - - return string.Format("{0," + (length * -1) + "}", code); - } - - [AssemblerHandler(Token = "%org", Length = 37)] - protected string GetOrg(int offset, int length) - { - string org = "ORG " + Util.NumberToBaseString(Data.ConvertPCtoSnes(offset), Util.NumberBase.Hexadecimal, 6, true); - return string.Format("{0," + (length * -1) + "}", org); - } - - [AssemblerHandler(Token = "%map", Length = 37)] - protected string GetMap(int offset, int length) - { - string s = ""; - switch (Data.RomMapMode) - { - case RomMapMode.LoRom: s = "lorom"; break; - case RomMapMode.HiRom: s = "hirom"; break; - case RomMapMode.Sa1Rom: s = "sa1rom"; break; // todo - case RomMapMode.ExSa1Rom: s = "exsa1rom"; break; // todo - case RomMapMode.SuperFx: s = "sfxrom"; break; // todo - case RomMapMode.ExHiRom: s = "exhirom"; break; - case RomMapMode.ExLoRom: s = "exlorom"; break; - } - return string.Format("{0," + (length * -1) + "}", s); - } - - // 0+ = bank_xx.asm, -1 = labels.asm - [AssemblerHandler(Token = "%incsrc", Length = 1)] - protected string GetIncSrc(int offset, int length) - { - string s = "incsrc \"labels.asm\""; - if (offset >= 0) - { - int bank = Data.ConvertPCtoSnes(offset) >> 16; - s = string.Format("incsrc \"bank_{0}.asm\"", Util.NumberToBaseString(bank, Util.NumberBase.Hexadecimal, 2)); - } - return string.Format("{0," + (length * -1) + "}", s); - } - - [AssemblerHandler(Token = "%bankcross", Length = 1)] - protected string GetBankCross(int offset, int length) - { - string s = "check bankcross off"; - return string.Format("{0," + (length * -1) + "}", s); - } - - // length forced to 6 - [AssemblerHandler(Token = "ia", Length = 6)] - protected string GetIntermediateAddress(int offset, int length) - { - int ia = Data.GetIntermediateAddressOrPointer(offset); - return ia >= 0 ? Util.NumberToBaseString(ia, Util.NumberBase.Hexadecimal, 6) : " "; - } - - // length forced to 6 - [AssemblerHandler(Token = "pc", Length = 6)] - protected string GetProgramCounter(int offset, int length) - { - return Util.NumberToBaseString(Data.ConvertPCtoSnes(offset), Util.NumberBase.Hexadecimal, 6); - } - - // trim to length - [AssemblerHandler(Token = "offset", Length = -6)] - protected string GetOffset(int offset, int length) - { - return string.Format("{0," + (length * -1) + "}", Util.NumberToBaseString(offset, Util.NumberBase.Hexadecimal, 0)); - } - - // length forced to 8 - [AssemblerHandler(Token = "bytes", Length = 8)] - protected string GetRawBytes(int offset, int length) - { - string bytes = ""; - if (Data.GetFlag(offset) == FlagType.Opcode) - { - for (var i = 0; i < Data.GetInstructionLength(offset); i++) - { - bytes += Util.NumberToBaseString(Data.GetRomByte(offset + i), Util.NumberBase.Hexadecimal); - } - } - return $"{bytes,-8}"; - } - - // trim to length - [AssemblerHandler(Token = "comment", Length = 1)] - protected string GetComment(int offset, int length) - { - var snesOffset = Data.ConvertPCtoSnes(offset); - return string.Format("{0," + (length * -1) + "}", Data.GetComment(snesOffset)); - } - - // length forced to 2 - [AssemblerHandler(Token = "b", Length = 2)] - protected string GetDataBank(int offset, int length) - { - return Util.NumberToBaseString(Data.GetDataBank(offset), Util.NumberBase.Hexadecimal, 2); - } - - // length forced to 4 - [AssemblerHandler(Token = "d", Length = 4)] - protected string GetDirectPage(int offset, int length) - { - return Util.NumberToBaseString(Data.GetDirectPage(offset), Util.NumberBase.Hexadecimal, 4); - } - - // if length == 1, M/m, else 08/16 - [AssemblerHandler(Token = "m", Length = 1)] - protected string GetMFlag(int offset, int length) - { - var m = Data.GetMFlag(offset); - if (length == 1) return m ? "M" : "m"; - else return m ? "08" : "16"; - } - - // if length == 1, X/x, else 08/16 - [AssemblerHandler(Token = "x", Length = 1)] - protected string GetXFlag(int offset, int length) - { - var x = Data.GetXFlag(offset); - if (length == 1) return x ? "X" : "x"; - else return x ? "08" : "16"; - } - - // output label at snes offset, and its value - [AssemblerHandler(Token = "%labelassign", Length = 1)] - protected string GetLabelAssign(int offset, int length) - { - var labelName = Data.GetLabelName(offset); - var offsetStr = Util.NumberToBaseString(offset, Util.NumberBase.Hexadecimal, 6, true); - var labelComment = Data.GetLabelComment(offset); - - if (string.IsNullOrEmpty(labelName)) - return ""; - - labelComment ??= ""; - - var finalCommentText = ""; - - // TODO: sorry, probably not the best way to stuff this in here, consider putting it in the %comment% section in the future. -Dom - if (Settings.PrintLabelSpecificComments && labelComment != "") - finalCommentText = $"; !^ {labelComment} ^!"; - - string s = $"{labelName} = {offsetStr}{finalCommentText}"; - return string.Format("{0," + (length * -1) + "}", s); - } - } - - #endregion -} diff --git a/Diz.Core/export/LogInterfaces.cs b/Diz.Core/export/LogInterfaces.cs new file mode 100644 index 00000000..f9dccd0d --- /dev/null +++ b/Diz.Core/export/LogInterfaces.cs @@ -0,0 +1,32 @@ +using System.Collections.Generic; +using Diz.Core.model; + +namespace Diz.Core.export +{ + public interface ILogCreatorDataSource : IReadOnlySnesRom + { + ITemporaryLabelProvider TemporaryLabelProvider { get; } + } + + // would love to redesign so we can get rid of this class and all this temporary label stuff. + public interface ITemporaryLabelProvider + { + // add a temporary label which will be cleared out when we are finished the export + // this should not add a label if a real label already exists. + public void AddTemporaryLabel(int snesAddress, Label label); + public void ClearTemporaryLabels(); + } + + public interface ILabelService : + ILabelProvider, + IReadOnlyLabelProvider + { + } + + public interface ILabelServiceWithTempLabels : + ILabelService, + ITemporaryLabelProvider + { + + } +} \ No newline at end of file diff --git a/Diz.Core/export/LogWriterSettings.cs b/Diz.Core/export/LogWriterSettings.cs index 724bd6e2..cbcf8b89 100644 --- a/Diz.Core/export/LogWriterSettings.cs +++ b/Diz.Core/export/LogWriterSettings.cs @@ -1,79 +1,83 @@ -using System.IO; +#nullable enable + +using Diz.Core.util; + +/* + * TODO: + * Couple things for ongoing refactors: + * 1) This class should live with the Diz.LogWriter project (not Diz.Core) + * 2) It should inherit some kind of IDocRootItem class and be saved alongside Diz.Project + * 3) Diz.Core shouldn't have to know about anything in here or anything about assembly export/generation + * 4) Probably use dependency injection starting with this system to register settings providers in Diz like this one + */ namespace Diz.Core.export { - public struct LogWriterSettings + public record LogWriterSettings { - // struct because we want to make a bunch of copies of this struct. - // The plumbing could use a pass of something like 'ref readonly' because: - // https://docs.microsoft.com/en-us/dotnet/csharp/language-reference/keywords/ref#reference-return-values + public const string DefaultStr = "%label:-22% %code:37%;%pc%|%bytes%|%ia%; %comment%"; + + public enum FormatUnlabeled + { + ShowAll = 0, + ShowInPoints = 1, // TODO Add Show In Points with +/- labels + ShowNone = 2 + } - public string Format; - public int DataPerLine; - public LogCreator.FormatUnlabeled Unlabeled; - public LogCreator.FormatStructure Structure; - public bool IncludeUnusedLabels; - public bool PrintLabelSpecificComments; - public bool OutputExtraWhitespace; + public enum FormatStructure + { + SingleFile = 0, + OneBankPerFile = 1 + } + + public string Format { get; init; } + public int DataPerLine { get; init; } + public FormatUnlabeled Unlabeled { get; init; } + public FormatStructure Structure { get; init; } + public bool IncludeUnusedLabels { get; init; } + public bool PrintLabelSpecificComments { get; init; } + public bool OutputExtraWhitespace { get; init; } - private bool wasInitialized; - public int RomSizeOverride; // specify an override for the # of bytes to assemble. default is the entire ROM + // specify an override for the # of bytes to assemble. default is the entire ROM + public int RomSizeOverride { get; init; } + public string FileOrFolderOutPath { get; init; } - public string FileOrFolderOutPath; - public bool OutputToString; - public string ErrorFilename; + public bool OutputToString { get; init; } + public string ErrorFilename { get; init; } + + public LogWriterSettings WithPathRelativeTo(string newFileNameAndPath, string pathToMakeRelativeTo) => + this with + { + FileOrFolderOutPath = Util.TryGetRelativePath(newFileNameAndPath, pathToMakeRelativeTo) + }; - public void SetDefaults() + public LogWriterSettings() { - Format = "%label:-22% %code:37%;%pc%|%bytes%|%ia%; %comment%"; + Format = DefaultStr; DataPerLine = 8; - Unlabeled = LogCreator.FormatUnlabeled.ShowInPoints; - Structure = LogCreator.FormatStructure.OneBankPerFile; + Unlabeled = FormatUnlabeled.ShowInPoints; + Structure = FormatStructure.OneBankPerFile; IncludeUnusedLabels = false; PrintLabelSpecificComments = false; - FileOrFolderOutPath = ""; // path to output file or folder - wasInitialized = true; RomSizeOverride = -1; ErrorFilename = "errors.txt"; OutputExtraWhitespace = true; + FileOrFolderOutPath = ""; // path to output file or folder } + } - // return null if no error, or message if there is - public string Validate() - { - // for now, just make sure it was initialized somewhere by someone - if (!wasInitialized) - return "Not initialized"; - - // TODO: add more validation. - - if (OutputToString) - { - if (Structure == LogCreator.FormatStructure.OneBankPerFile) - return "Can't use one-bank-per-file output with string output"; - - if (FileOrFolderOutPath != "") - return "Can't use one-bank-per-file output with file output"; - } - else - { - if (FileOrFolderOutPath == "") - return "No file path set"; - - if (!Directory.Exists(Path.GetDirectoryName(FileOrFolderOutPath))) - return "File or folder output directory doesn't exist"; - - if (Structure == LogCreator.FormatStructure.SingleFile) - { - // don't check for existence, just that what we have appears to be a filename and - // not a directory. - if (Path.GetFileName(FileOrFolderOutPath) == string.Empty) - return "Output path doesn't appear to be a valid file selection"; - } - } + public static class LogWriterSettingsExtensions + { + public static LogWriterSettings GetDefaultsIfInvalid(this LogWriterSettings @this) => + @this.IsValid() ? @this : new LogWriterSettings(); - return null; + public static string? Validate(this LogWriterSettings @this) + { + var results = new LogWriterSettingsValidator().Validate(@this); + return !results.IsValid ? results.ToString() : null; } - } + public static bool IsValid(this LogWriterSettings @this) => + @this.Validate() != null; + } } diff --git a/Diz.Core/export/LogWriterSettingsValidator.cs b/Diz.Core/export/LogWriterSettingsValidator.cs new file mode 100644 index 00000000..2248da1f --- /dev/null +++ b/Diz.Core/export/LogWriterSettingsValidator.cs @@ -0,0 +1,69 @@ +using System.IO; +using FluentValidation; + +namespace Diz.Core.export +{ + public class LogWriterSettingsValidator : AbstractValidator + { + protected virtual AbstractValidator MultiValidator => + new LogWriterSettingsOutputMultipleFiles(); + + protected virtual AbstractValidator StringValidator => + new LogWriterSettingsOutputString(); + + public LogWriterSettingsValidator() + { + void ValidateFileOutput() => Include(MultiValidator); + void ValidateStringOutput() => Include(StringValidator); + + When(x => x.OutputToString, ValidateStringOutput).Otherwise(ValidateFileOutput); + } + } + + public class LogWriterSettingsOutputString : AbstractValidator + { + public LogWriterSettingsOutputString() + { + // runs when OutputToString == true + + RuleFor(x => x.Structure) + .NotEqual(LogWriterSettings.FormatStructure.OneBankPerFile) + .WithMessage("Can't use one-bank-per-file output with string output enabled"); + + RuleFor(x => x.FileOrFolderOutPath) + .Empty() + .WithMessage("Can't use one-bank-per-file output with valid file or path specified"); + } + } + + public class LogWriterSettingsOutputMultipleFiles : AbstractValidator + { + // protected virtual string GetFileName(string path) => Path.GetFileName(path); + public virtual bool DirectoryExists(string path) => + Directory.Exists(Path.GetDirectoryName(path)); + + // this is not the most bulletproof thing in the world. + // it's hard to validate without hitting the disk, you should follow this with additional checks + public static bool PathLooksLikeDirectoryNameOnly(string fileOrFolderPath) => + Path.GetFileName(fileOrFolderPath) == string.Empty || + !Path.HasExtension(fileOrFolderPath); + + public LogWriterSettingsOutputMultipleFiles() + { + // runs when OutputToString == false + + RuleFor(x => x.FileOrFolderOutPath) + .NotEmpty() + .WithMessage("No file path set") + + .Must(DirectoryExists) + .WithMessage("Directory doesn't exist"); + + // verify what we have appears to be a filename and NOT a directory + RuleFor(x => x.FileOrFolderOutPath) + .Must(PathLooksLikeDirectoryNameOnly) + .When(settings => settings.Structure == LogWriterSettings.FormatStructure.OneBankPerFile) + .WithMessage("Output directory doesn't appear to be a valid"); + } + } +} \ No newline at end of file diff --git a/Diz.Core/import/BSNESImportStreamProcessor.cs b/Diz.Core/import/BSNESImportStreamProcessor.cs index da50434c..bb3b77cb 100644 --- a/Diz.Core/import/BSNESImportStreamProcessor.cs +++ b/Diz.Core/import/BSNESImportStreamProcessor.cs @@ -12,7 +12,7 @@ namespace Diz.Core.import { public abstract class PoolItem { - public bool isFree = true; + public bool IsFree = true; } public class ObjPool where T : PoolItem, new() @@ -27,8 +27,8 @@ public ObjPool() public T Get() { var item = Alloc(); - Debug.Assert(item.isFree); - item.isFree = false; + Debug.Assert(item.IsFree); + item.IsFree = false; return item; } @@ -42,8 +42,8 @@ public void Return(ref T item) if (item == null) return; - Debug.Assert(!item.isFree); - item.isFree = true; + Debug.Assert(!item.IsFree); + item.IsFree = true; DeAlloc(item); item = null; @@ -60,24 +60,24 @@ public class BsnesImportStreamProcessor public class CompressedWorkItem : PoolItem { public byte[] Header; - public const int headerSize = 9; + public const int HeaderSize = 9; public byte[] CompressedBuffer; public int CompressedSize; public byte[] UncompressedBuffer; public int UncompressedSize; - public bool wasDecompressed; + public bool WasDecompressed; - public byte[] tmpHeader; - public List listHeads = new List(); + public byte[] TmpHeader; + public readonly List ListHeads = new(); } public class WorkItem : PoolItem { public byte[] Buffer; public bool AbridgedFormat; - public WorkItem next; + public WorkItem Next; } private ObjPool poolCompressedWorkItems; @@ -126,7 +126,7 @@ private CompressedWorkItem ReadPacketFromStream(Stream stream) try { - Util.ReadNext(stream, item.Header, CompressedWorkItem.headerSize); + Util.ReadNext(stream, item.Header, CompressedWorkItem.HeaderSize); } catch (EndOfStreamException) { @@ -138,7 +138,7 @@ private CompressedWorkItem ReadPacketFromStream(Stream stream) Markers.WriteFlag("initial read"); #endif - if (item.Header.Length != CompressedWorkItem.headerSize) + if (item.Header.Length != CompressedWorkItem.HeaderSize) throw new InvalidDataException($"invalid header length for compressed data chunk"); if (item.Header[0] != 'Z') @@ -190,12 +190,12 @@ private CompressedWorkItem AllocateCompressedWorkItem() var item = AllocCompressedWorkItem(); item.CompressedSize = item.UncompressedSize = 0; - item.wasDecompressed = false; + item.WasDecompressed = false; - if (item.Header != null && item.Header.Length == CompressedWorkItem.headerSize) + if (item.Header != null && item.Header.Length == CompressedWorkItem.HeaderSize) return item; - item.Header = new byte[CompressedWorkItem.headerSize]; + item.Header = new byte[CompressedWorkItem.HeaderSize]; return item; } @@ -218,14 +218,14 @@ public void FreeCompressedWorkItem(ref CompressedWorkItem compressedItem) // keep the capacity, but kill the contents. // also, go a little overkill and kill the references to WorkItem list heads inside the List. - if (compressedItem.listHeads != null) + if (compressedItem.ListHeads != null) { - for (var i = 0; i < compressedItem.listHeads.Count; ++i) + for (var i = 0; i < compressedItem.ListHeads.Count; ++i) { - compressedItem.listHeads[0] = null; + compressedItem.ListHeads[0] = null; } - compressedItem.listHeads.Clear(); + compressedItem.ListHeads.Clear(); } poolCompressedWorkItems?.Return(ref compressedItem); @@ -238,7 +238,7 @@ public void FreeWorkItem(ref WorkItem workItem) // don't kill the big buffers. main point of this pool is to hopefully re-use them later. - workItem.next = null; + workItem.Next = null; poolWorkItems?.Return(ref workItem); } @@ -248,11 +248,12 @@ private WorkItem AllocateWorkItem(byte workItemLen) var workItem = AllocWorkItem(); // turn this on if you ever think you have a memroy alloc issue - const bool seriousChecking = false; - if (seriousChecking && (workItem.isFree || workItem.next != null)) + #if SERIOUS_DEBUG_MEM_VERIFY + if (workItem.IsFree || workItem.Next != null) { Debugger.Break(); } + #endif workItem.AbridgedFormat = false; @@ -288,7 +289,7 @@ private static void AllocateUncompressedBuffer(CompressedWorkItem compressedWork public void DecompressWorkItem(CompressedWorkItem compressedWorkItem) { - Debug.Assert(!compressedWorkItem.wasDecompressed); + Debug.Assert(!compressedWorkItem.WasDecompressed); AllocateUncompressedBuffer(compressedWorkItem); var decompressedLength = 0; @@ -300,7 +301,7 @@ public void DecompressWorkItem(CompressedWorkItem compressedWorkItem) compressedWorkItem.UncompressedSize); } - compressedWorkItem.wasDecompressed = true; + compressedWorkItem.WasDecompressed = true; if (decompressedLength != compressedWorkItem.UncompressedSize) throw new InvalidDataException("incorrect decompressed data size"); diff --git a/Diz.Core/import/BSNESTraceLogCapture.cs b/Diz.Core/import/BSNESTraceLogCapture.cs index c01ffa2d..16aa0283 100644 --- a/Diz.Core/import/BSNESTraceLogCapture.cs +++ b/Diz.Core/import/BSNESTraceLogCapture.cs @@ -8,7 +8,7 @@ using System.Net.Sockets; using System.Threading; using System.Threading.Tasks; -using Diz.Core.model; +using Diz.Core.model.snes; using Diz.Core.util; #if PROFILING using Microsoft.ConcurrencyVisualizer.Instrumentation; @@ -27,8 +27,8 @@ public class BsnesTraceLogCapture private BsnesTraceLogImporter importer; - private int statsBytesToProcess = 0; - private int statsCompressedBlocksToProcess = 0; + private int statsBytesToProcess; + private int statsCompressedBlocksToProcess; private BsnesTraceLogImporter.Stats cachedStats; public bool Running { get; protected set; } @@ -75,7 +75,7 @@ protected virtual BsnesTraceLogImporter CreateTraceLogImporter(Data data) { // multi-threaded version // return new BsnesTraceLogImporter(data, streamProcessor.CancelToken.Token, taskManager); - return new BsnesTraceLogImporter(data); + return new(data); } protected virtual IWorkerTaskManager CreateWorkerTaskManager() @@ -109,12 +109,10 @@ protected virtual void Main() #endif } - private const int maxNumCompressedItemsToProcess = -1; // debug only. - // set a limit for the max# of worker tasks allowed to operate on the compressed data. tweak this number as needed. // this is purely for throttling and not for thread safety, otherwise # of Tasks will run out of control. - private SemaphoreSlim compressedWorkersLimit = new SemaphoreSlim(4,4); - private SemaphoreSlim uncompressedWorkersLimit = new SemaphoreSlim(4, 4); + private readonly SemaphoreSlim compressedWorkersLimit = new(4,4); + private readonly SemaphoreSlim uncompressedWorkersLimit = new(4, 4); private void ProcessStreamData(Stream networkStream) { @@ -151,12 +149,16 @@ private void ProcessStreamData(Stream networkStream) Stats_MarkQueued(compressedItem); count++; - if (maxNumCompressedItemsToProcess != -1 && count >= maxNumCompressedItemsToProcess) - return; + + // enable for debugging/perf testing if needed: + // if (MaxNumCompressedItemsToProcess != -1 && count >= MaxNumCompressedItemsToProcess) + // return; } Trace.WriteLine($"Processed {count} compressed work items."); } + + // private const int MaxNumCompressedItemsToProcess = -1; // debug only. private async void ProcessCompressedWorkItem(BsnesImportStreamProcessor.CompressedWorkItem compressedItem) { @@ -180,10 +182,10 @@ private async void ProcessCompressedWorkItem(BsnesImportStreamProcessor.Compress private IEnumerable DispatchWorkersForCompressedWorkItem(BsnesImportStreamProcessor.CompressedWorkItem compressedItem) { - var subTasks = new List(capacity: compressedItem.listHeads.Count); - for (var i = 0; i < compressedItem.listHeads.Count; ++i) + var subTasks = new List(capacity: compressedItem.ListHeads.Count); + for (var i = 0; i < compressedItem.ListHeads.Count; ++i) { - var workItemListHead = compressedItem.listHeads[i]; + var workItemListHead = compressedItem.ListHeads[i]; subTasks.Add(taskManager.Run(() => { @@ -208,7 +210,7 @@ private IEnumerable DispatchWorkersForCompressedWorkItem(BsnesImportStream } })); - compressedItem.listHeads[i] = null; // remove the reference. + compressedItem.ListHeads[i] = null; // remove the reference. } return subTasks; @@ -216,10 +218,10 @@ private IEnumerable DispatchWorkersForCompressedWorkItem(BsnesImportStream private void PartitionWorkItemQueue(BsnesImportStreamProcessor.CompressedWorkItem compressedItem) { - Debug.Assert(compressedItem.wasDecompressed); + Debug.Assert(compressedItem.WasDecompressed); using var stream = new MemoryStream(compressedItem.UncompressedBuffer, 0, compressedItem.UncompressedSize); - compressedItem.tmpHeader ??= new byte[2]; + compressedItem.TmpHeader ??= new byte[2]; // tune this as needed. // we want parallel jobs going, but, we don't want too many of them at once. @@ -228,18 +230,18 @@ private void PartitionWorkItemQueue(BsnesImportStreamProcessor.CompressedWorkIte bool keepGoing; var itemsRemainingBeforeEnd = numItemsPerTask; - Debug.Assert(compressedItem.listHeads != null && compressedItem.listHeads.Count == 0); + Debug.Assert(compressedItem.ListHeads != null && compressedItem.ListHeads.Count == 0); BsnesImportStreamProcessor.WorkItem currentHead = null; BsnesImportStreamProcessor.WorkItem currentItem = null; do { - var nextItem = ReadNextWorkItem(stream, compressedItem.tmpHeader); + var nextItem = ReadNextWorkItem(stream, compressedItem.TmpHeader); if (nextItem != null) { - Debug.Assert(nextItem.next == null); + Debug.Assert(nextItem.Next == null); if (currentHead == null) { @@ -248,7 +250,7 @@ private void PartitionWorkItemQueue(BsnesImportStreamProcessor.CompressedWorkIte } else { - currentItem.next = nextItem; + currentItem.Next = nextItem; } currentItem = nextItem; @@ -263,8 +265,8 @@ private void PartitionWorkItemQueue(BsnesImportStreamProcessor.CompressedWorkIte // finish list if (currentHead != null) { - Debug.Assert(currentItem.next == null); - compressedItem.listHeads.Add(currentHead); + Debug.Assert(currentItem.Next == null); + compressedItem.ListHeads.Add(currentHead); } // reset list @@ -288,12 +290,12 @@ private void DecompressWorkItem(BsnesImportStreamProcessor.CompressedWorkItem co Debug.Assert(compressedItem.CompressedBuffer != null); Debug.Assert(compressedItem.UncompressedSize != 0); Debug.Assert(compressedItem.UncompressedSize != 0); - Debug.Assert(!compressedItem.wasDecompressed); + Debug.Assert(!compressedItem.WasDecompressed); streamProcessor.DecompressWorkItem(compressedItem); Debug.Assert(compressedItem.UncompressedBuffer != null); - Debug.Assert(compressedItem.wasDecompressed); + Debug.Assert(compressedItem.WasDecompressed); } private void ProcessWorkItemsLinkedList(BsnesImportStreamProcessor.WorkItem workItemListHead) @@ -306,7 +308,7 @@ private void ProcessWorkItemsLinkedList(BsnesImportStreamProcessor.WorkItem work var current = workItemListHead; while (current != null) { ProcessWorkItem(current); - var next = current.next; + var next = current.Next; streamProcessor.FreeWorkItem(ref current); current = next; @@ -371,7 +373,7 @@ public class BsnesTraceLogFileCapture : BsnesTraceLogCapture { private readonly byte[] bytes; - public BsnesTraceLogFileCapture(string dataFile) : base() + public BsnesTraceLogFileCapture(string dataFile) { bytes = File.ReadAllBytes(dataFile); } @@ -381,16 +383,17 @@ protected override Stream GetInputStream() return new MemoryStream(bytes); } - protected override IWorkerTaskManager CreateWorkerTaskManager() - { - return base.CreateWorkerTaskManager(); // regular version (multithreaded) - // return new WorkerTaskManagerSynchronous(); // single-threaded version (for testing/debug only) - } + // Example for setting up different versions: + // protected override IWorkerTaskManager CreateWorkerTaskManager() + // { + // return base.CreateWorkerTaskManager(); // regular version (multithreaded) + // // return new WorkerTaskManagerSynchronous(); // single-threaded version (for testing/debug only) + // } protected override BsnesTraceLogImporter CreateTraceLogImporter(Data data) { // single-threaded version - return new BsnesTraceLogImporter(data); + return new(data); } } diff --git a/Diz.Core/import/BSNESTraceLogImporter.cs b/Diz.Core/import/BSNESTraceLogImporter.cs index 372cc00b..74eca74b 100644 --- a/Diz.Core/import/BSNESTraceLogImporter.cs +++ b/Diz.Core/import/BSNESTraceLogImporter.cs @@ -1,5 +1,6 @@ using System.IO; using Diz.Core.model; +using Diz.Core.model.snes; using Diz.Core.util; namespace Diz.Core.import @@ -23,8 +24,8 @@ public BsnesTraceLogImporter(Data data) InitObjectPool(); } - // Mark collected trace data for a RomByte (which should be an opcode) AND any of the operands that follow us. - private void SetOpcodeAndOperandsFromTraceData(ModificationData modData, int instructionByteLen = -1) + // Mark collected trace data for a ByteOffset (which should be an opcode) AND any of the operands that follow us. + private void SetOpcodeAndOperandsFromTraceData(ModificationData modDataIn, int instructionByteLen = -1) { // extremely performance-intensive function. be really careful when adding stuff var currentOffset = 0; @@ -33,7 +34,7 @@ private void SetOpcodeAndOperandsFromTraceData(ModificationData modData, int ins bool Prep(ModificationData modData) { numBytesAnalyzed++; - UpdatePCAddress(modData); + UpdatePcAddress(modData); if (!IsOkToSetThisRomByte(modData.Pc, instructionByteLen, currentOffset)) return false; @@ -41,15 +42,15 @@ bool Prep(ModificationData modData) return true; } - while (Prep(modData)) + while (Prep(modDataIn)) { - ApplyModification(modData); + ApplyModification(modDataIn); - modData.SnesAddress = GetNextSNESAddress(modData.SnesAddress); + modDataIn.SnesAddress = GetNextSnesAddress(modDataIn.SnesAddress); currentOffset++; } - FreeModificationData(ref modData); // sets to Null after called + FreeModificationData(ref modDataIn); // sets to Null after called currentStats.NumRomBytesAnalyzed += numBytesAnalyzed; } @@ -91,14 +92,14 @@ private bool IsOkToSetThisRomByte(int pc, int instructionByteLen, int opIndex) private FlagType GetFlag(int pc) { - data.RomBytes[pc].Lock.EnterReadLock(); + data.RomByteSource?.Bytes[pc].Lock.EnterReadLock(); try { return data.GetFlag(pc); } finally { - data.RomBytes[pc].Lock.ExitReadLock(); + data.RomByteSource?.Bytes[pc].Lock.ExitReadLock(); } } diff --git a/Diz.Core/import/BSNESUsageMapImporter.cs b/Diz.Core/import/BSNESUsageMapImporter.cs index b05797a0..00636ff0 100644 --- a/Diz.Core/import/BSNESUsageMapImporter.cs +++ b/Diz.Core/import/BSNESUsageMapImporter.cs @@ -1,11 +1,14 @@ using System; +using System.Diagnostics.CodeAnalysis; using Diz.Core.model; +using Diz.Core.model.snes; namespace Diz.Core.import { public class BsnesUsageMapImporter { [Flags] + [SuppressMessage("ReSharper", "UnusedMember.Local")] private enum BsnesPlusUsage : byte { UsageRead = 0x80, @@ -28,7 +31,7 @@ public static int ImportUsageMap(byte[] usageMap, Data data) private int Run() { prevFlags = 0; - + var modified = 0; for (var snesOffset = 0; snesOffset <= 0xFFFFFF; snesOffset++) { diff --git a/Diz.Core/import/BizHawkCdlImporter.cs b/Diz.Core/import/BizHawkCdlImporter.cs index 4f6e12d9..fecb784c 100644 --- a/Diz.Core/import/BizHawkCdlImporter.cs +++ b/Diz.Core/import/BizHawkCdlImporter.cs @@ -3,12 +3,13 @@ using System.IO; using System.Linq; using Diz.Core.model; +using Diz.Core.model.snes; namespace Diz.Core.import { public class BizHawkCdlImporter { - private readonly Dictionary> cdl = new Dictionary>(); + private readonly Dictionary> cdl = new Dictionary>(); [Flags] public enum Flag : byte @@ -75,21 +76,21 @@ private void CopyInto(Data data) for (var offset = 0; offset < size; offset++) { var cdlFlag = cdlRomFlags[offset]; - if (cdlFlag == BizHawkCdlImporter.Flag.None) + if (cdlFlag == Flag.None) continue; var type = FlagType.Unreached; - if ((cdlFlag & BizHawkCdlImporter.Flag.ExecFirst) != 0) + if ((cdlFlag & Flag.ExecFirst) != 0) { type = FlagType.Opcode; - m = (cdlFlag & BizHawkCdlImporter.Flag.CpumFlag) != 0; - x = (cdlFlag & BizHawkCdlImporter.Flag.CpuxFlag) != 0; + m = (cdlFlag & Flag.CpumFlag) != 0; + x = (cdlFlag & Flag.CpuxFlag) != 0; } - else if ((cdlFlag & BizHawkCdlImporter.Flag.ExecOperand) != 0) + else if ((cdlFlag & Flag.ExecOperand) != 0) type = FlagType.Operand; - else if ((cdlFlag & BizHawkCdlImporter.Flag.CpuData) != 0) + else if ((cdlFlag & Flag.CpuData) != 0) type = FlagType.Data8Bit; - else if ((cdlFlag & BizHawkCdlImporter.Flag.DmaData) != 0) + else if ((cdlFlag & Flag.DmaData) != 0) type = FlagType.Data8Bit; data.MarkTypeFlag(offset, type, 1); diff --git a/Diz.Core/import/BsnesTraceLogImporter.Caching.cs b/Diz.Core/import/BsnesTraceLogImporter.Caching.cs index fa1c22ef..98a92860 100644 --- a/Diz.Core/import/BsnesTraceLogImporter.Caching.cs +++ b/Diz.Core/import/BsnesTraceLogImporter.Caching.cs @@ -1,6 +1,4 @@ using System; -using System.ComponentModel; -using System.IO; namespace Diz.Core.import { @@ -33,14 +31,14 @@ public int D, Db, Flags, - FN, - FV, - FM, - FX, - FD, - FI, - FZ, - FC; + Fn, + Fv, + Fm, + Fx, + Fd, + Fi, + Fz, + Fc; public int LastLineLength { get; set; } = -1; @@ -59,14 +57,14 @@ int GetIndexOfDataAfterToken(string token) Flags = Db + 3; // flags: nvmxdizc - FN = Flags + 0; - FV = Flags + 1; - FM = Flags + 2; - FX = Flags + 3; - FD = Flags + 4; - FI = Flags + 5; - FZ = Flags + 6; - FC = Flags + 7; + Fn = Flags + 0; + Fv = Flags + 1; + Fm = Flags + 2; + Fx = Flags + 3; + Fd = Flags + 4; + Fi = Flags + 5; + Fz = Flags + 6; + Fc = Flags + 7; } } } diff --git a/Diz.Core/import/BsnesTraceLogImporter.ModificationsList.cs b/Diz.Core/import/BsnesTraceLogImporter.ModificationsList.cs index 871331b1..763b9032 100644 --- a/Diz.Core/import/BsnesTraceLogImporter.ModificationsList.cs +++ b/Diz.Core/import/BsnesTraceLogImporter.ModificationsList.cs @@ -1,4 +1,5 @@ using Diz.Core.model; +using Diz.Core.model.byteSources; namespace Diz.Core.import { @@ -24,51 +25,51 @@ public class ModificationData : PoolItem public bool MFlagSet; // we will set these if any of the above field were modified - public bool changed; - public bool mDb, mMarks, mDp, mX, mM; + public bool Changed; + public bool MDb, MMarks, MDp, Mx, Mm; // precondition: rombyte (minimum of) read lock already acquired - private void CompareToExisting(RomByteData romByte) + private void CompareToExisting(ByteEntry romByte) { - mDb = romByte.DataBank != DataBank; - mMarks = romByte.TypeFlag != FlagType; - mDp = romByte.DirectPage != DirectPage; - mX = romByte.XFlag != XFlagSet; - mM = romByte.MFlag != MFlagSet; + MDb = romByte.DataBank != DataBank; + MMarks = romByte.TypeFlag != FlagType; + MDp = romByte.DirectPage != DirectPage; + Mx = romByte.XFlag != XFlagSet; + Mm = romByte.MFlag != MFlagSet; - changed = mMarks || mDb || mDp || mX || mM; + Changed = MMarks || MDb || MDp || Mx || Mm; } - // precondition: rombyte (minimum of) read lock already acquired - private void ApplyModification(RomByte romByte) + // precondition: ByteEntry (minimum of) read lock already acquired + private void ApplyModification(ByteEntry byteOffset) { - romByte.Lock.EnterWriteLock(); + byteOffset.Lock.EnterWriteLock(); try { - romByte.TypeFlag = FlagType; - romByte.DataBank = (byte) DataBank; - romByte.DirectPage = 0xFFFF & DirectPage; - romByte.XFlag = XFlagSet; - romByte.MFlag = MFlagSet; + byteOffset.TypeFlag = FlagType; + byteOffset.DataBank = (byte) DataBank; + byteOffset.DirectPage = 0xFFFF & DirectPage; + byteOffset.XFlag = XFlagSet; + byteOffset.MFlag = MFlagSet; } finally { - romByte.Lock.ExitWriteLock(); + byteOffset.Lock.ExitWriteLock(); } } - public void ApplyModificationIfNeeded(RomByte romByte) + public void ApplyModificationIfNeeded(ByteEntry byteOffset) { - romByte.Lock.EnterUpgradeableReadLock(); + byteOffset.Lock.EnterUpgradeableReadLock(); try { - CompareToExisting(romByte); - if (changed) - ApplyModification(romByte); + CompareToExisting(byteOffset); + if (Changed) + ApplyModification(byteOffset); } finally { - romByte.Lock.ExitUpgradeableReadLock(); + byteOffset.Lock.ExitUpgradeableReadLock(); } } } @@ -90,13 +91,13 @@ private void ApplyModification(ModificationData modData) private void ApplyModificationIfNeeded(ModificationData modData) { - var romByte = data.RomBytes[modData.Pc]; + var romByte = data.RomByteSource?.Bytes[modData.Pc]; modData.ApplyModificationIfNeeded(romByte); } private void UpdateStats(ModificationData modData) { - if (!modData.changed) + if (!modData.Changed) return; statsLock.EnterWriteLock(); @@ -104,11 +105,11 @@ private void UpdateStats(ModificationData modData) { currentStats.NumRomBytesModified++; - currentStats.NumMarksModified += modData.mMarks ? 1 : 0; - currentStats.NumDbModified += modData.mDb ? 1 : 0; - currentStats.NumDpModified += modData.mDp ? 1 : 0; - currentStats.NumXFlagsModified += modData.mX ? 1 : 0; - currentStats.NumMFlagsModified += modData.mM ? 1 : 0; + currentStats.NumMarksModified += modData.MMarks ? 1 : 0; + currentStats.NumDbModified += modData.MDb ? 1 : 0; + currentStats.NumDpModified += modData.MDp ? 1 : 0; + currentStats.NumXFlagsModified += modData.Mx ? 1 : 0; + currentStats.NumMFlagsModified += modData.Mm ? 1 : 0; } finally { diff --git a/Diz.Core/import/BsnesTraceLogImporter.Parsers.cs b/Diz.Core/import/BsnesTraceLogImporter.Parsers.cs index 78e3cba1..95e6b025 100644 --- a/Diz.Core/import/BsnesTraceLogImporter.Parsers.cs +++ b/Diz.Core/import/BsnesTraceLogImporter.Parsers.cs @@ -10,7 +10,7 @@ public ModificationData AllocateModificationData() { var modData = modificationDataPool.Get(); - modData.changed = false; + modData.Changed = false; return modData; } @@ -29,7 +29,7 @@ public void ImportTraceLogLine(string line) if (!ParseTextLine(line, modData)) return; - UpdatePCAddress(modData); + UpdatePcAddress(modData); SetOpcodeAndOperandsFromTraceData(modData); // note: frees modData, don't use after } @@ -40,7 +40,7 @@ public void ImportTraceLogLineBinary(byte[] bytes, bool abridgedFormat = true) var modData = AllocateModificationData(); ParseBinary(bytes, abridgedFormat, out var opcodeLen, modData); - UpdatePCAddress(modData); + UpdatePcAddress(modData); SetOpcodeAndOperandsFromTraceData(modData, opcodeLen); // note: frees modData, don't use after } @@ -69,11 +69,11 @@ public bool ParseTextLine(string line, ModificationData modData) // 'X' (if emulation mode) or 'B' (if native mode) = unchecked in bsnesplus debugger UI = (8bit) // 'x' or '.' = checked (16bit) - modData.XFlagSet = line[TextImportFormatCached.FX] == 'X' || line[TextImportFormatCached.FX] == 'B'; + modData.XFlagSet = line[TextImportFormatCached.Fx] == 'X' || line[TextImportFormatCached.Fx] == 'B'; // 'M' (if emulation mode) or '1' (if native mode) = unchecked in bsnesplus debugger UI = (8bit) // 'm' or '.' = checked (16bit) - modData.MFlagSet = line[TextImportFormatCached.FM] == 'M' || line[TextImportFormatCached.FM] == '1'; + modData.MFlagSet = line[TextImportFormatCached.Fm] == 'M' || line[TextImportFormatCached.Fm] == '1'; // TODO: we could capture native vs emulation mode here and mark that. diff --git a/Diz.Core/import/BsnesTraceLogImporter.Util.cs b/Diz.Core/import/BsnesTraceLogImporter.Util.cs index a7ab4e77..e4069da2 100644 --- a/Diz.Core/import/BsnesTraceLogImporter.Util.cs +++ b/Diz.Core/import/BsnesTraceLogImporter.Util.cs @@ -12,7 +12,7 @@ private int ConvertSnesToPc(int modDataSnesAddress) return RomUtil.ConvertSnesToPc(modDataSnesAddress, romMapModeCached, romSizeCached); } - private static int GetNextSNESAddress(int modDataSnesAddress) + private static int GetNextSnesAddress(int modDataSnesAddress) { return RomUtil.CalculateSnesOffsetWithWrap(modDataSnesAddress, 1); } @@ -22,7 +22,7 @@ private static FlagType GetFlagForInstructionPosition(int currentIndex) return currentIndex == 0 ? FlagType.Opcode : FlagType.Operand; } - private void UpdatePCAddress(ModificationData modData) + private void UpdatePcAddress(ModificationData modData) { modData.Pc = ConvertSnesToPc(modData.SnesAddress); } diff --git a/Diz.Core/model/Annotation.cs b/Diz.Core/model/Annotation.cs new file mode 100644 index 00000000..281f2c78 --- /dev/null +++ b/Diz.Core/model/Annotation.cs @@ -0,0 +1,379 @@ +using System; +using System.ComponentModel; +using System.Runtime.CompilerServices; +using Diz.Core.util; +using JetBrains.Annotations; + +#if DIZ_3_BRANCH +using Diz.Core.model.byteSources; +#endif + +namespace Diz.Core.model +{ + public abstract class Annotation : AnnotationBase + #if !DIZ_3_BRANCH + {} + #else + , IParentAware + { + public ByteEntry Parent { get; protected set; } + public void OnParentChanged(ByteEntry parent) + { + Parent = parent; + } + } + #endif + + public abstract class AnnotationBase : INotifyPropertyChangedExt + { + public event PropertyChangedEventHandler PropertyChanged; + + [NotifyPropertyChangedInvocator] + public virtual void OnPropertyChanged([CallerMemberName] string propertyName = null) + { + PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)); + } + } + + public class MarkAnnotation : Annotation, IComparable, IComparable + { + public FlagType TypeFlag + { + get => typeFlag; + set => this.SetField(ref typeFlag, value); + } + + private FlagType typeFlag = FlagType.Unreached; + + protected bool Equals(MarkAnnotation other) + { + return typeFlag == other.typeFlag; + } + + public override bool Equals(object obj) + { + if (ReferenceEquals(null, obj)) return false; + if (ReferenceEquals(this, obj)) return true; + if (obj.GetType() != GetType()) return false; + return Equals((MarkAnnotation) obj); + } + + public override int GetHashCode() + { + return (int) typeFlag; + } + + public int CompareTo(MarkAnnotation other) + { + if (ReferenceEquals(this, other)) return 0; + if (ReferenceEquals(null, other)) return 1; + return typeFlag.CompareTo(other.typeFlag); + } + + public int CompareTo(object obj) + { + if (ReferenceEquals(null, obj)) return 1; + if (ReferenceEquals(this, obj)) return 0; + return obj is MarkAnnotation other ? CompareTo(other) : throw new ArgumentException($"Object must be of type {nameof(MarkAnnotation)}"); + } + } + + public class ByteAnnotation : Annotation, IComparable, IComparable + { + public byte Val + { + get => dataByte; + set => this.SetField(ref dataByte, value); + } + + private byte dataByte; + + #region Generated Comparison + public int CompareTo(ByteAnnotation other) + { + if (ReferenceEquals(this, other)) return 0; + if (ReferenceEquals(null, other)) return 1; + return dataByte.CompareTo(other.dataByte); + } + + public int CompareTo(object obj) + { + if (ReferenceEquals(null, obj)) return 1; + if (ReferenceEquals(this, obj)) return 0; + return obj is ByteAnnotation other ? CompareTo(other) : throw new ArgumentException($"Object must be of type {nameof(ByteAnnotation)}"); + } + + protected bool Equals(ByteAnnotation other) + { + return dataByte == other?.dataByte; + } + + public override bool Equals(object obj) + { + if (ReferenceEquals(null, obj)) return false; + if (ReferenceEquals(this, obj)) return true; + if (obj.GetType() != GetType()) return false; + return Equals((ByteAnnotation) obj); + } + + public override int GetHashCode() + { + return HashCode.Combine(base.GetHashCode(), dataByte); + } + #endregion + } + + public class OpcodeAnnotation : Annotation, IComparable, IComparable + { + public byte DataBank + { + get => dataBank; + set => this.SetField(ref dataBank, value); + } + + public int DirectPage + { + get => directPage; + set => this.SetField(ref directPage, value); + } + + public bool XFlag + { + get => xFlag; + set => this.SetField(ref xFlag, value); + } + + public bool MFlag + { + get => mFlag; + set => this.SetField(ref mFlag, value); + } + + public Architecture Arch + { + get => arch; + set => this.SetField(ref arch, value); + } + + private byte dataBank; + private int directPage; + private bool xFlag; + private bool mFlag; + + private Architecture arch; + + #region Equality + protected bool Equals(OpcodeAnnotation other) + { + return DataBank == other.DataBank && DirectPage == other.DirectPage && XFlag == other.XFlag && MFlag == other.MFlag && Arch == other.Arch; + } + + public override bool Equals(object obj) + { + if (ReferenceEquals(null, obj)) return false; + if (ReferenceEquals(this, obj)) return true; + return obj.GetType() == GetType() && Equals((OpcodeAnnotation)obj); + } + + public override int GetHashCode() + { + unchecked + { + var hashCode = DataBank.GetHashCode(); + hashCode = (hashCode * 397) ^ DirectPage; + hashCode = (hashCode * 397) ^ XFlag.GetHashCode(); + hashCode = (hashCode * 397) ^ MFlag.GetHashCode(); + hashCode = (hashCode * 397) ^ (int)Arch; + return hashCode; + } + } + public int CompareTo(OpcodeAnnotation other) + { + if (ReferenceEquals(this, other)) return 0; + if (ReferenceEquals(null, other)) return 1; + var dataBankComparison = dataBank.CompareTo(other.dataBank); + if (dataBankComparison != 0) return dataBankComparison; + var directPageComparison = directPage.CompareTo(other.directPage); + if (directPageComparison != 0) return directPageComparison; + var xFlagComparison = xFlag.CompareTo(other.xFlag); + if (xFlagComparison != 0) return xFlagComparison; + var mFlagComparison = mFlag.CompareTo(other.mFlag); + if (mFlagComparison != 0) return mFlagComparison; + return arch.CompareTo(other.arch); + } + + public int CompareTo(object obj) + { + if (ReferenceEquals(null, obj)) return 1; + if (ReferenceEquals(this, obj)) return 0; + return obj is OpcodeAnnotation other ? CompareTo(other) : throw new ArgumentException($"Object must be of type {nameof(OpcodeAnnotation)}"); + } + #endregion + } + + // technically, this computed data can be re-created at any time and we keep it because: + // 1) serialize so we don't have to recompute on load + // 2) so it's faster when figuring out what to display to the user (vs recomputing on the fly) + public class BranchAnnotation : Annotation, IComparable, IComparable + { + // never modify fields directly. only go through the public fields + // cached mark if it's an in vs out point + private InOutPoint point = InOutPoint.None; + + // cached data + public InOutPoint Point + { + get => point; + set => this.SetField(ref point, value); + } + + #region Equality + protected bool Equals(BranchAnnotation other) + { + return Point == other.Point; + } + + public override bool Equals(object obj) + { + if (ReferenceEquals(null, obj)) return false; + if (ReferenceEquals(this, obj)) return true; + return obj.GetType() == GetType() && Equals((BranchAnnotation)obj); + } + + public override int GetHashCode() + { + var hashCode = (int)Point; + return hashCode; + } + + public int CompareTo(BranchAnnotation other) + { + if (ReferenceEquals(this, other)) return 0; + if (ReferenceEquals(null, other)) return 1; + return point.CompareTo(other.point); + } + + public int CompareTo(object obj) + { + if (ReferenceEquals(null, obj)) return 1; + if (ReferenceEquals(this, obj)) return 0; + return obj is BranchAnnotation other ? CompareTo(other) : throw new ArgumentException($"Object must be of type {nameof(BranchAnnotation)}"); + } + #endregion + } + + // represent a label at a particular SNES address + // + // Comments here are for the LABEL itself, and not so much about where they're used. + // i.e. a label for 0x7E0020 might store a character's HP in RAM. It would look like: + // - address: 0x7E0020 (if HiRom, 0x7EXXXX means it's a RAM address) + // - label: "character_3_hp" + // - comment: "this address is only used in RAM during battle sequences" + public class Label : Annotation, IReadOnlyLabel, IComparable