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