├── .editorconfig
├── .gitattributes
├── .gitignore
├── .gitmodules
├── .vscode
├── launch.json
└── tasks.json
├── BUILDING.md
├── LICENSE
├── LynnaLab.sln
├── LynnaLab
├── Fonts
│ ├── HyliaSerifBeta-Regular.ttf
│ ├── ReggaeOne-Regular.ttf
│ ├── RocknRollOne-Regular.ttf
│ ├── ZeldaDXTTBRK.ttf
│ ├── ZeldaOracles.ttf
│ ├── minishcap__v10.ttf
│ └── tingle_tuner.ttf
├── Images
│ ├── Pegasus_Seed_OOX.png
│ └── icon.bmp
├── LynnaLab.csproj
├── Properties
│ └── PublishProfiles
│ │ ├── portable.pubxml
│ │ └── win-x64.pubxml
├── Shaders
│ ├── OpenGL
│ │ ├── imgui-frag.glsl
│ │ ├── imgui-vertex.glsl
│ │ ├── tileset-frag.glsl
│ │ └── tileset-vertex.glsl
│ └── SPIR-V
│ │ ├── generate-spirv.sh
│ │ ├── imgui-frag.glsl
│ │ ├── imgui-frag.spv
│ │ ├── imgui-vertex.glsl
│ │ ├── imgui-vertex.spv
│ │ ├── tileset-frag.glsl
│ │ ├── tileset-frag.spv
│ │ ├── tileset-vertex.glsl
│ │ └── tileset-vertex.spv
├── aliases.sh
├── build-setup.sh
├── disassemblyFiles.txt
├── icon.ico
├── log4net.config
├── publish.sh
├── src
│ ├── Brush.cs
│ ├── BuildDialog.cs
│ ├── DocumentationDialog.cs
│ ├── GlobalConfig.cs
│ ├── ImGuiX.cs
│ ├── MapTextureCacher.cs
│ ├── Modal.cs
│ ├── NetworkDialog.cs
│ ├── Program.cs
│ ├── ProjectWorkspace.cs
│ ├── RoomTextureCacher.cs
│ ├── SDLUtil
│ │ ├── InputSnapshot.cs
│ │ ├── SDLHelper.cs
│ │ └── SDLWindow.cs
│ ├── SettingsDialog.cs
│ ├── TilesetTextureCacher.cs
│ ├── Top.cs
│ ├── VeldridBackend
│ │ ├── ImGuiController.cs
│ │ ├── Startup.cs
│ │ ├── VeldridBackend.cs
│ │ └── VeldridTexture.cs
│ └── Widget
│ │ ├── DungeonEditor.cs
│ │ ├── FilePicker.cs
│ │ ├── Frame.cs
│ │ ├── GfxViewer.cs
│ │ ├── ImGuiLL.cs
│ │ ├── Minimap.cs
│ │ ├── ObjectBox.cs
│ │ ├── ObjectGroupEditor.cs
│ │ ├── ProcessOutputView.cs
│ │ ├── RoomEditor.cs
│ │ ├── RoomLayoutEditor.cs
│ │ ├── ScratchPad.cs
│ │ ├── SelectionBox.cs
│ │ ├── SizedWidget.cs
│ │ ├── TileGrid.cs
│ │ ├── TilesetCloner.cs
│ │ ├── TilesetEditor.cs
│ │ ├── TilesetViewer.cs
│ │ ├── TransactionDialog.cs
│ │ └── WarpEditor.cs
└── windows-setup.bat
├── LynnaLib.Tests
├── LynnaLib.Tests.csproj
├── TestDictionaryLinkedList.cs
├── TestNetwork.cs
├── TestProject.cs
└── log4net.config
├── LynnaLib
├── AbstractBoolValueReference.cs
├── AbstractIntValueReference.cs
├── Animation.cs
├── AnimationGroup.cs
├── Chest.cs
├── ConstantsMapping.cs
├── Data.cs
├── DataValueReference.cs
├── Documentation.cs
├── DocumentationFileComponent.cs
├── Dungeon.cs
├── EnemyObject.cs
├── Exceptions.cs
├── FakeTileset.cs
├── FileComponent.cs
├── FileParser.cs
├── GameObject.cs
├── GbGraphics.cs
├── GfxHeaderData.cs
├── Global.cs
├── GraphicsState.cs
├── IGfxHeader.cs
├── InteractionObject.cs
├── LynnaLib.csproj
├── Map.cs
├── MemoryFileStream.cs
├── Network.cs
├── ObjectAnimation.cs
├── ObjectAnimationFrame.cs
├── ObjectData.cs
├── ObjectDefinition.cs
├── ObjectGfxHeaderData.cs
├── ObjectGroup.cs
├── PaletteHeaderData.cs
├── PaletteHeaderGroup.cs
├── PartObject.cs
├── PngGfxStream.cs
├── Project.cs
├── ProjectConfig.cs
├── ProjectDataType.cs
├── RawObjectGroup.cs
├── RealTileset.cs
├── ReloadableStream.cs
├── Room.cs
├── RoomLayout.cs
├── Serialization.cs
├── StreamValueReference.cs
├── Tileset.cs
├── TilesetHeaderGroup.cs
├── TilesetLayoutHeaderData.cs
├── Transaction.cs
├── TreasureGroup.cs
├── TreasureObject.cs
├── Util
│ ├── Accessor.cs
│ ├── Bitmap.cs
│ ├── Cacher.cs
│ ├── CairoHelper1.cs
│ ├── CircularStack.cs
│ ├── Color.cs
│ ├── DictionaryLinkedList.cs
│ ├── EventWrapper.cs
│ ├── Helper.cs
│ ├── IStream.cs
│ ├── LockableEvent.cs
│ ├── LockableEventGroup.cs
│ ├── LogHelper.cs
│ ├── Misc.cs
│ ├── ObjectDumper.cs
│ ├── Point.cs
│ ├── Rect.cs
│ ├── SubStream.cs
│ └── Wla.cs
├── ValueReference.cs
├── ValueReferenceDescriptor.cs
├── ValueReferenceGroup.cs
├── ValueReferenceWrapper.cs
├── Warp.cs
├── WarpDestData.cs
├── WarpDestGroup.cs
├── WarpGroup.cs
├── WarpSourceData.cs
└── WorldMap.cs
├── README.md
├── images
├── preview-brush.gif
├── preview-general.png
├── preview-objects.png
├── preview-quickspawn-1.png
├── preview-quickspawn-2.png
└── preview-warps.png
└── random-stuff
├── agesCollisions
└── seasonsCollisions
/.editorconfig:
--------------------------------------------------------------------------------
1 | root = true
2 |
3 | [*.cs]
4 | max_line_length = 100
5 |
--------------------------------------------------------------------------------
/.gitattributes:
--------------------------------------------------------------------------------
1 | * text=auto eol=lf
2 |
3 | *.bat eol=crlf
4 | *.ps1 eol=crlf
5 |
6 | *.png binary
7 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | #Autosave files
2 | *~
3 | .*.swp
4 |
5 | #build
6 | [Oo]bj/
7 | [Bb]in/
8 | packages/
9 | TestResults/
10 |
11 | # globs
12 | Makefile.in
13 | *.DS_Store
14 | *.sln.cache
15 | *.suo
16 | *.cache
17 | *.pidb
18 | *.userprefs
19 | *.usertasks
20 | config.log
21 | config.make
22 | config.status
23 | aclocal.m4
24 | install-sh
25 | autom4te.cache/
26 | *.user
27 | *.tar.gz
28 | tarballs/
29 | test-results/
30 | Thumbs.db
31 |
32 | #Mac bundle stuff
33 | *.dmg
34 | *.app
35 |
36 | #resharper
37 | *_Resharper.*
38 | *.Resharper
39 |
40 | #dotCover
41 | *.dotCover
42 |
43 | #Visual Studio
44 | .vs/
45 |
46 | #Misc
47 | LynnaLib/version.txt
48 | global_config.yaml
49 | imgui.ini
50 |
--------------------------------------------------------------------------------
/.gitmodules:
--------------------------------------------------------------------------------
1 | [submodule "oracles-disasm"]
2 | path = oracles-disasm
3 | url = https://github.com/Stewmath/oracles-disasm.git
4 |
--------------------------------------------------------------------------------
/.vscode/launch.json:
--------------------------------------------------------------------------------
1 | {
2 | "version": "0.2.0",
3 | "configurations": [
4 | {
5 | "name": "LynnaLab (Debug)",
6 | "type": "coreclr",
7 | "request": "launch",
8 | "preLaunchTask": "build-debug",
9 | "program": "${workspaceFolder}/LynnaLab/bin/Debug/net8.0/LynnaLab.dll",
10 | "args": ["oracles-disasm"],
11 | },
12 | {
13 | "name": "LynnaLab (Release)",
14 | "type": "coreclr",
15 | "request": "launch",
16 | "preLaunchTask": "build-release",
17 | "program": "${workspaceFolder}/LynnaLab/bin/Release/net8.0/LynnaLab.dll",
18 | "args": ["oracles-disasm"],
19 | },
20 | {
21 | "name": "LynnaLab (Release, no args)",
22 | "type": "coreclr",
23 | "request": "launch",
24 | "preLaunchTask": "build-release",
25 | "program": "${workspaceFolder}/LynnaLab/bin/Release/net8.0/LynnaLab.dll",
26 | "args": [],
27 | }
28 | ]
29 | }
--------------------------------------------------------------------------------
/.vscode/tasks.json:
--------------------------------------------------------------------------------
1 | {
2 | "version": "2.0.0",
3 | "tasks": [
4 | {
5 | "label": "build-release",
6 | "type": "process",
7 | "group": "build",
8 | "problemMatcher": [],
9 | "command": "dotnet",
10 | "args": [
11 | "build",
12 | "/p:Configuration=Release",
13 | "/p:GenerateFullPaths=true",
14 | "/consoleloggerparameters:NoSummary",
15 | "/p:Platform=\"x86\"",
16 | ],
17 | },
18 | {
19 | "label": "build-debug",
20 | "type": "process",
21 | "group": "build",
22 | "problemMatcher": [],
23 | "command": "dotnet",
24 | "args": [
25 | "build",
26 | "/p:Configuration=Debug",
27 | "/p:GenerateFullPaths=true",
28 | "/consoleloggerparameters:NoSummary",
29 | "/p:Platform=\"x86\"",
30 | ],
31 | }
32 | ]
33 | }
--------------------------------------------------------------------------------
/BUILDING.md:
--------------------------------------------------------------------------------
1 | For the most part, Visual Studio (or the "dotnet" commandline tool) should be able to resolve the
2 | project's dependencies automatically. GTK will also be installed as necessary, if on Windows.
3 |
4 | You must also have "git" in your path for the build process to succeed (so that it can get the
5 | version to be shown in LynnaLab's titlebar). On Windows you can download git from
6 | [here](https://git-scm.com/download/win). (Ensure that the options you select will put git into your
7 | PATH variable; this happens by default).
8 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | The MIT License (MIT)
2 |
3 | Copyright (c) 2015 Matthew Stewart
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/LynnaLab.sln:
--------------------------------------------------------------------------------
1 |
2 | Microsoft Visual Studio Solution File, Format Version 12.00
3 | # Visual Studio Version 16
4 | VisualStudioVersion = 16.0.30413.136
5 | MinimumVisualStudioVersion = 10.0.40219.1
6 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "LynnaLab", "LynnaLab\LynnaLab.csproj", "{21072A00-C7C0-41FF-8103-4186BB75056E}"
7 | EndProject
8 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "LynnaLib", "LynnaLib\LynnaLib.csproj", "{C57A460A-90D5-4B50-AA96-1D11CA6D6801}"
9 | EndProject
10 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "LynnaLib.Tests", "LynnaLib.Tests\LynnaLib.Tests.csproj", "{1817167D-2C8D-487C-ACC0-8CA8A4A1D6B0}"
11 | EndProject
12 | Global
13 | GlobalSection(SolutionConfigurationPlatforms) = preSolution
14 | Debug|x86 = Debug|x86
15 | Release|x86 = Release|x86
16 | EndGlobalSection
17 | GlobalSection(ProjectConfigurationPlatforms) = postSolution
18 | {21072A00-C7C0-41FF-8103-4186BB75056E}.Debug|x86.ActiveCfg = Debug|Any CPU
19 | {21072A00-C7C0-41FF-8103-4186BB75056E}.Debug|x86.Build.0 = Debug|Any CPU
20 | {21072A00-C7C0-41FF-8103-4186BB75056E}.Release|x86.ActiveCfg = Release|Any CPU
21 | {21072A00-C7C0-41FF-8103-4186BB75056E}.Release|x86.Build.0 = Release|Any CPU
22 | {1817167D-2C8D-487C-ACC0-8CA8A4A1D6B0}.Debug|x86.ActiveCfg = Debug|Any CPU
23 | {1817167D-2C8D-487C-ACC0-8CA8A4A1D6B0}.Debug|x86.Build.0 = Debug|Any CPU
24 | {1817167D-2C8D-487C-ACC0-8CA8A4A1D6B0}.Release|x86.ActiveCfg = Release|Any CPU
25 | {1817167D-2C8D-487C-ACC0-8CA8A4A1D6B0}.Release|x86.Build.0 = Release|Any CPU
26 | EndGlobalSection
27 | GlobalSection(SolutionProperties) = preSolution
28 | HideSolutionNode = FALSE
29 | EndGlobalSection
30 | GlobalSection(ExtensibilityGlobals) = postSolution
31 | SolutionGuid = {32A6158D-AB9A-4428-A4B6-A997AA0CC551}
32 | EndGlobalSection
33 | GlobalSection(MonoDevelopProperties) = preSolution
34 | Policies = $0
35 | $0.TextStylePolicy = $1
36 | $1.NoTabsAfterNonTabs = True
37 | $1.scope = text/x-csharp
38 | $1.EolMarker = Unix
39 | $1.TabsToSpaces = True
40 | $0.CSharpFormattingPolicy = $2
41 | $2.IndentSwitchBody = True
42 | $2.BeforeMethodDeclarationParentheses = False
43 | $2.BeforeMethodCallParentheses = False
44 | $2.BeforeConstructorDeclarationParentheses = False
45 | $2.NewLineBeforeConstructorInitializerColon = NewLine
46 | $2.NewLineAfterConstructorInitializerColon = SameLine
47 | $2.BeforeDelegateDeclarationParentheses = False
48 | $2.NewParentheses = False
49 | $2.SpacesBeforeBrackets = False
50 | $2.scope = text/x-csharp
51 | $2.NewLinesForBracesInMethods = False
52 | $2.IndentSwitchSection = False
53 | $2.NewLinesForBracesInProperties = False
54 | $2.NewLinesForBracesInAccessors = False
55 | $2.NewLinesForBracesInAnonymousMethods = False
56 | $2.NewLinesForBracesInControlBlocks = False
57 | $2.NewLinesForBracesInAnonymousTypes = False
58 | $2.NewLinesForBracesInObjectCollectionArrayInitializers = False
59 | $2.NewLinesForBracesInLambdaExpressionBody = False
60 | $2.NewLineForElse = False
61 | $2.NewLineForMembersInObjectInit = False
62 | $2.NewLineForMembersInAnonymousTypes = False
63 | $2.NewLineForClausesInQuery = False
64 | $0.DotNetNamingPolicy = $3
65 | $0.StandardHeader = $4
66 | EndGlobalSection
67 | EndGlobal
68 |
--------------------------------------------------------------------------------
/LynnaLab/Fonts/HyliaSerifBeta-Regular.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Stewmath/LynnaLab/a17ac808addf0002586b05ce2d6488c5137672f8/LynnaLab/Fonts/HyliaSerifBeta-Regular.ttf
--------------------------------------------------------------------------------
/LynnaLab/Fonts/ReggaeOne-Regular.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Stewmath/LynnaLab/a17ac808addf0002586b05ce2d6488c5137672f8/LynnaLab/Fonts/ReggaeOne-Regular.ttf
--------------------------------------------------------------------------------
/LynnaLab/Fonts/RocknRollOne-Regular.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Stewmath/LynnaLab/a17ac808addf0002586b05ce2d6488c5137672f8/LynnaLab/Fonts/RocknRollOne-Regular.ttf
--------------------------------------------------------------------------------
/LynnaLab/Fonts/ZeldaDXTTBRK.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Stewmath/LynnaLab/a17ac808addf0002586b05ce2d6488c5137672f8/LynnaLab/Fonts/ZeldaDXTTBRK.ttf
--------------------------------------------------------------------------------
/LynnaLab/Fonts/ZeldaOracles.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Stewmath/LynnaLab/a17ac808addf0002586b05ce2d6488c5137672f8/LynnaLab/Fonts/ZeldaOracles.ttf
--------------------------------------------------------------------------------
/LynnaLab/Fonts/minishcap__v10.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Stewmath/LynnaLab/a17ac808addf0002586b05ce2d6488c5137672f8/LynnaLab/Fonts/minishcap__v10.ttf
--------------------------------------------------------------------------------
/LynnaLab/Fonts/tingle_tuner.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Stewmath/LynnaLab/a17ac808addf0002586b05ce2d6488c5137672f8/LynnaLab/Fonts/tingle_tuner.ttf
--------------------------------------------------------------------------------
/LynnaLab/Images/Pegasus_Seed_OOX.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Stewmath/LynnaLab/a17ac808addf0002586b05ce2d6488c5137672f8/LynnaLab/Images/Pegasus_Seed_OOX.png
--------------------------------------------------------------------------------
/LynnaLab/Images/icon.bmp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Stewmath/LynnaLab/a17ac808addf0002586b05ce2d6488c5137672f8/LynnaLab/Images/icon.bmp
--------------------------------------------------------------------------------
/LynnaLab/LynnaLab.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | WinExe
5 | net8.0
6 | icon.ico
7 |
8 |
9 |
10 | true
11 | AnyCPU
12 |
13 |
14 |
15 |
16 | true
17 | AnyCPU
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 | Always
29 | true
30 |
31 |
32 | Always
33 | true
34 |
35 |
36 | Always
37 | true
38 |
39 |
40 | Always
41 | true
42 |
43 |
44 | Always
45 | true
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 |
56 |
57 |
58 |
59 |
60 |
61 |
62 |
63 |
64 |
65 |
66 |
67 |
72 |
73 |
74 |
75 |
76 |
77 |
78 |
81 |
82 |
83 |
86 |
87 |
88 |
--------------------------------------------------------------------------------
/LynnaLab/Properties/PublishProfiles/portable.pubxml:
--------------------------------------------------------------------------------
1 |
2 |
5 |
6 |
7 | Release
8 | x86
9 | bin\Release\Publish\LynnaLab-portable
10 | FileSystem
11 | net8.0
12 | false
13 |
14 |
15 |
--------------------------------------------------------------------------------
/LynnaLab/Properties/PublishProfiles/win-x64.pubxml:
--------------------------------------------------------------------------------
1 |
2 |
5 |
6 |
7 | Release
8 | x86
9 | bin\Release\Publish\LynnaLab-win64
10 | FileSystem
11 | net8.0
12 | win-x64
13 | false
14 | True
15 | False
16 |
17 |
18 |
--------------------------------------------------------------------------------
/LynnaLab/Shaders/OpenGL/imgui-frag.glsl:
--------------------------------------------------------------------------------
1 | #version 330 core
2 |
3 | uniform sampler2D PointTexture;
4 | uniform sampler2D BilinearTexture;
5 |
6 | layout(std140) uniform FragGlobalsStruct
7 | {
8 | int InterpolationMode;
9 | float alpha;
10 | };
11 |
12 | in vec4 color;
13 | in vec2 texCoord;
14 |
15 | out vec4 outputColor;
16 |
17 | // from https://stackoverflow.com/questions/13501081/efficient-bicubic-filtering-code-in-glsl
18 | vec4 cubic(float v){
19 | vec4 n = vec4(1.0, 2.0, 3.0, 4.0) - v;
20 | vec4 s = n * n * n;
21 | float x = s.x;
22 | float y = s.y - 4.0 * s.x;
23 | float z = s.z - 4.0 * s.y + 6.0 * s.x;
24 | float w = 6.0 - x - y - z;
25 | return vec4(x, y, z, w) * (1.0/6.0);
26 | }
27 |
28 | vec4 textureBicubic(sampler2D sampler, vec2 texCoords){
29 | vec2 texSize = textureSize(sampler, 0);
30 | vec2 invTexSize = 1.0 / texSize;
31 |
32 | texCoords = texCoords * texSize - 0.5;
33 |
34 |
35 | vec2 fxy = fract(texCoords);
36 | texCoords -= fxy;
37 |
38 | vec4 xcubic = cubic(fxy.x);
39 | vec4 ycubic = cubic(fxy.y);
40 |
41 | vec4 c = texCoords.xxyy + vec2 (-0.5, +1.5).xyxy;
42 |
43 | vec4 s = vec4(xcubic.xz + xcubic.yw, ycubic.xz + ycubic.yw);
44 | vec4 offset = c + vec4 (xcubic.yw, ycubic.yw) / s;
45 |
46 | offset *= invTexSize.xxyy;
47 |
48 | vec4 sample0 = texture(sampler, offset.xz);
49 | vec4 sample1 = texture(sampler, offset.yz);
50 | vec4 sample2 = texture(sampler, offset.xw);
51 | vec4 sample3 = texture(sampler, offset.yw);
52 |
53 | float sx = s.x / (s.x + s.y);
54 | float sy = s.z / (s.z + s.w);
55 |
56 | return mix(mix(sample3, sample2, sx),
57 | mix(sample1, sample0, sx), sy);
58 | }
59 |
60 | void main()
61 | {
62 | vec4 newColor;
63 |
64 | if (InterpolationMode == 0) // Nearest Neighbor
65 | newColor = texture(PointTexture, texCoord);
66 | else if (InterpolationMode == 1) // Bilinear
67 | newColor = texture(BilinearTexture, texCoord);
68 | else if (InterpolationMode == 2) // Bicubic
69 | newColor = textureBicubic(PointTexture, texCoord);
70 |
71 | outputColor = newColor * vec4(color.rgb, alpha * color.a);
72 | }
73 |
--------------------------------------------------------------------------------
/LynnaLab/Shaders/OpenGL/imgui-vertex.glsl:
--------------------------------------------------------------------------------
1 | #version 330 core
2 |
3 | layout(std140) uniform ProjectionMatrixBuffer
4 | {
5 | mat4 projection_matrix;
6 | };
7 |
8 | // Determines the window of the source texture to read from.
9 | // Normally, topLeft=(0, 0), bottomRight=(1, 1) to read the whole texture.
10 | layout(std140) uniform SourceViewportBuffer
11 | {
12 | vec2 topLeft;
13 | vec2 bottomRight;
14 | };
15 |
16 | in vec2 in_position;
17 | in vec2 in_texCoord;
18 | in vec4 in_color;
19 |
20 | out vec4 color;
21 | out vec2 texCoord;
22 |
23 | void main()
24 | {
25 | gl_Position = projection_matrix * vec4(in_position, 0, 1);
26 | color = in_color;
27 |
28 | texCoord = in_texCoord * (bottomRight - topLeft) + topLeft;
29 | }
30 |
--------------------------------------------------------------------------------
/LynnaLab/Shaders/OpenGL/tileset-frag.glsl:
--------------------------------------------------------------------------------
1 | #version 330
2 |
3 | uniform usampler2D TilesetGfx; // R16: 1 x (8 * 16 * 16)
4 | uniform usampler2D TilesetMap; // R8: 32 x 32 (subtile indices)
5 | uniform usampler2D TilesetFlags; // R8: 32 x 32 (subtile flags)
6 | uniform sampler2D TilesetPalette;
7 |
8 | in vec4 color;
9 | in vec2 texCoord;
10 |
11 | out vec4 outputColor;
12 |
13 | void main()
14 | {
15 | vec4 newColor;
16 |
17 | uint canvasX = uint(texCoord.x * 256);
18 | uint canvasY = uint(texCoord.y * 256);
19 |
20 | //uint metaTileIndex = (canvasY / 16) * 16 + (canvasX / 16);
21 |
22 | uint subTileIndex = uint(texture(TilesetMap, texCoord).x) ^ 0x80u;
23 | uint subTileFlags = uint(texture(TilesetFlags, texCoord).x);
24 |
25 | uint gfxX = canvasX % 8u;
26 | uint gfxY;
27 |
28 | if ((subTileFlags & 0x40u) != 0u) // Flip Y
29 | gfxY = subTileIndex * 8u + (7u - (canvasY % 8u));
30 | else
31 | gfxY = subTileIndex * 8u + (canvasY % 8u);
32 |
33 | uint palette = subTileFlags & 7u;
34 |
35 | // Decode a tile from the gameboy's native format.
36 | // TilesetGfx is a 1xY texture where each 16-bit "pixel" is actually 2 bytes representing a row
37 | // of 8 pixels (exactly as it is stored in the gameboy's vram).
38 | uint line = uint(texture(TilesetGfx, vec2(0, gfxY / float(8 * 16 * 16))).x); // 16-bit int containing the line pixels
39 |
40 | // The x-position in the line we're to draw
41 | uint x;
42 | if ((subTileFlags & 0x20u) != 0u) // Flip X
43 | x = gfxX;
44 | else
45 | x = 7u - gfxX;
46 |
47 | // Get the 2-bit color index (0-3)
48 | line = line >> x;
49 | uint colorIndex = (line & 1u) | ((line >> 7u) & 2u);
50 |
51 | colorIndex = palette * 4u + colorIndex;
52 |
53 | // Now get the color associated with the color index
54 | newColor = texture(TilesetPalette, vec2(colorIndex, 0) / textureSize(TilesetPalette, 0).x);
55 |
56 | outputColor = newColor * vec4(color.rgb, color.a);
57 | }
58 |
--------------------------------------------------------------------------------
/LynnaLab/Shaders/OpenGL/tileset-vertex.glsl:
--------------------------------------------------------------------------------
1 | #version 330 core
2 |
3 | in vec2 in_position;
4 |
5 | out vec4 color;
6 | out vec2 texCoord;
7 |
8 | void main()
9 | {
10 | gl_Position = vec4(in_position, 0, 1);
11 | texCoord = in_position * 0.5 + 0.5;
12 | color = vec4(1, 1, 1, 1);
13 | }
14 |
--------------------------------------------------------------------------------
/LynnaLab/Shaders/SPIR-V/generate-spirv.sh:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 | glslangValidator -V imgui-vertex.glsl -o imgui-vertex.spv -S vert || exit 1
3 | glslangValidator -V imgui-frag.glsl -o imgui-frag.spv -S frag || exit 1
4 | glslangValidator -V tileset-vertex.glsl -o tileset-vertex.spv -S vert || exit 1
5 | glslangValidator -V tileset-frag.glsl -o tileset-frag.spv -S frag || exit 1
6 |
--------------------------------------------------------------------------------
/LynnaLab/Shaders/SPIR-V/imgui-frag.glsl:
--------------------------------------------------------------------------------
1 | #version 450
2 |
3 | #extension GL_ARB_separate_shader_objects : enable
4 | #extension GL_ARB_shading_language_420pack : enable
5 | #extension GL_EXT_samplerless_texture_functions : enable
6 |
7 | layout(set = 1, binding = 0) uniform texture2D Texture;
8 | layout(set = 1, binding = 1) uniform sampler PointSampler;
9 |
10 | layout(set = 1, binding = 4) uniform FragGlobalsStruct
11 | {
12 | int interpolationMode;
13 | float alpha;
14 | };
15 |
16 | layout (location = 0) in vec4 color;
17 | layout (location = 1) in vec2 texCoord;
18 |
19 | layout (location = 0) out vec4 outputColor;
20 |
21 | void main()
22 | {
23 | vec4 newColor = color * texture(sampler2D(Texture, PointSampler), texCoord);
24 | outputColor = vec4(newColor.rgb, alpha * newColor.a);
25 | }
26 |
--------------------------------------------------------------------------------
/LynnaLab/Shaders/SPIR-V/imgui-frag.spv:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Stewmath/LynnaLab/a17ac808addf0002586b05ce2d6488c5137672f8/LynnaLab/Shaders/SPIR-V/imgui-frag.spv
--------------------------------------------------------------------------------
/LynnaLab/Shaders/SPIR-V/imgui-vertex.glsl:
--------------------------------------------------------------------------------
1 | #version 450
2 |
3 | #extension GL_ARB_separate_shader_objects : enable
4 | #extension GL_ARB_shading_language_420pack : enable
5 |
6 | layout (location = 0) in vec2 in_position;
7 | layout (location = 1) in vec2 in_texCoord;
8 | layout (location = 2) in vec4 in_color;
9 |
10 | layout (binding = 0) uniform ProjectionMatrixBuffer
11 | {
12 | mat4 projection_matrix;
13 | };
14 |
15 | // Determines the window of the source texture to read from.
16 | // Normally, topLeft=(0, 0), bottomRight=(1, 1) to read the whole texture.
17 | layout (set = 1, binding = 5) uniform SourceViewportBuffer
18 | {
19 | vec2 topLeft;
20 | vec2 bottomRight;
21 | };
22 |
23 | layout (location = 0) out vec4 color;
24 | layout (location = 1) out vec2 texCoord;
25 |
26 | out gl_PerVertex
27 | {
28 | vec4 gl_Position;
29 | };
30 |
31 | void main()
32 | {
33 | gl_Position = projection_matrix * vec4(in_position, 0, 1);
34 | color = in_color;
35 |
36 | texCoord = in_texCoord * (bottomRight - topLeft) + topLeft;
37 | }
38 |
--------------------------------------------------------------------------------
/LynnaLab/Shaders/SPIR-V/imgui-vertex.spv:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Stewmath/LynnaLab/a17ac808addf0002586b05ce2d6488c5137672f8/LynnaLab/Shaders/SPIR-V/imgui-vertex.spv
--------------------------------------------------------------------------------
/LynnaLab/Shaders/SPIR-V/tileset-frag.glsl:
--------------------------------------------------------------------------------
1 | #version 450
2 |
3 | #extension GL_ARB_separate_shader_objects : enable
4 | #extension GL_ARB_shading_language_420pack : enable
5 | #extension GL_EXT_samplerless_texture_functions : enable // For textureSize
6 |
7 | layout(set = 1, binding = 0) uniform utexture2D TilesetGfx; // R16: 1 x (8 * 16 * 16)
8 | layout(set = 1, binding = 1) uniform utexture2D TilesetMap; // R8: 32 x 32 (subtile indices)
9 | layout(set = 1, binding = 2) uniform utexture2D TilesetFlags; // R8: 32 x 32 (subtile flags)
10 | layout(set = 1, binding = 3) uniform texture2D TilesetPalette; // RGBA: (8 * 4) x 1
11 | layout(set = 1, binding = 4) uniform sampler PointSampler;
12 |
13 | layout (location = 0) in vec4 color;
14 | layout (location = 1) in vec2 texCoord;
15 |
16 | layout (location = 0) out vec4 outputColor;
17 |
18 | void main()
19 | {
20 | vec4 newColor;
21 |
22 | uint canvasX = uint(texCoord.x * 256);
23 | uint canvasY = uint(texCoord.y * 256);
24 |
25 | //uint metaTileIndex = (canvasY / 16) * 16 + (canvasX / 16);
26 |
27 | uint subTileIndex = uint(texture(usampler2D(TilesetMap, PointSampler), texCoord).x) ^ 0x80;
28 | uint subTileFlags = uint(texture(usampler2D(TilesetFlags, PointSampler), texCoord).x);
29 |
30 | uint gfxX = canvasX % 8u;
31 | uint gfxY;
32 |
33 | if ((subTileFlags & 0x40u) != 0u) // Flip Y
34 | gfxY = subTileIndex * 8u + (7u - (canvasY % 8u));
35 | else
36 | gfxY = subTileIndex * 8u + (canvasY % 8u);
37 |
38 | uint palette = subTileFlags & 7u;
39 |
40 | // Decode a tile from the gameboy's native format.
41 | // TilesetGfx is a 1xY texture where each 16-bit "pixel" is actually 2 bytes representing a row
42 | // of 8 pixels (exactly as it is stored in the gameboy's vram).
43 | uint line = uint(texture(usampler2D(TilesetGfx, PointSampler), vec2(0, gfxY / float(8 * 16 * 16))).x); // 16-bit int containing the line pixels
44 |
45 | // The x-position in the line we're to draw
46 | uint x;
47 | if ((subTileFlags & 0x20u) != 0u) // Flip X
48 | x = gfxX;
49 | else
50 | x = 7u - gfxX;
51 |
52 | // Get the 2-bit color index (0-3)
53 | line = line >> x;
54 | uint colorIndex = (line & 1u) | ((line >> 7u) & 2u);
55 |
56 | colorIndex = palette * 4u + colorIndex;
57 |
58 | // Now get the color associated with the color index
59 | newColor = texture(sampler2D(TilesetPalette, PointSampler), vec2(colorIndex, 0) / textureSize(TilesetPalette, 0).x);
60 |
61 | outputColor = newColor * vec4(color.rgb, color.a);
62 | }
63 |
--------------------------------------------------------------------------------
/LynnaLab/Shaders/SPIR-V/tileset-frag.spv:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Stewmath/LynnaLab/a17ac808addf0002586b05ce2d6488c5137672f8/LynnaLab/Shaders/SPIR-V/tileset-frag.spv
--------------------------------------------------------------------------------
/LynnaLab/Shaders/SPIR-V/tileset-vertex.glsl:
--------------------------------------------------------------------------------
1 | #version 450 core
2 |
3 | #extension GL_ARB_separate_shader_objects : enable
4 | #extension GL_ARB_shading_language_420pack : enable
5 |
6 | layout (location = 0) in vec2 in_position;
7 |
8 | layout (location = 0) out vec4 color;
9 | layout (location = 1) out vec2 texCoord;
10 |
11 | out gl_PerVertex
12 | {
13 | vec4 gl_Position;
14 | };
15 |
16 | void main()
17 | {
18 | gl_Position = vec4(in_position, 0, 1);
19 | texCoord = in_position * 0.5 + 0.5;
20 | color = vec4(1, 1, 1, 1);
21 |
22 | // The vertices I passed in are for the OpenGL coordinate space; need conversion in vulkan.
23 | gl_Position.y = -gl_Position.y;
24 | }
25 |
--------------------------------------------------------------------------------
/LynnaLab/Shaders/SPIR-V/tileset-vertex.spv:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Stewmath/LynnaLab/a17ac808addf0002586b05ce2d6488c5137672f8/LynnaLab/Shaders/SPIR-V/tileset-vertex.spv
--------------------------------------------------------------------------------
/LynnaLab/aliases.sh:
--------------------------------------------------------------------------------
1 | alias run='(cd bin/Debug/netcoreapp3.1; ./LynnaLab ../../../../oracles-disasm; cd ../../..)'
2 | alias r='run'
3 | alias make='dotnet build'
4 |
--------------------------------------------------------------------------------
/LynnaLab/build-setup.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 | #
3 | # Run this script to:
4 | # - Build WLA-DX v10.6 and copy it to /usr/local/bin
5 | # - Clone oracles-disasm and checkout the hack-base branch
6 | #
7 | # When run on Windows though "windows-setup.bat" this should set up everything
8 | # required to start using LynnaLab immediately. It should also work on Linux
9 | # but you'll need to ensure that the necessary dependencies are installed (git,
10 | # make, python, python-yaml, cmake, gcc), and you'll probably want to modify
11 | # SETUP_DIR to avoid cluttering your home directory.
12 | #
13 | # LynnaLab is hardcoded to look for oracles-disasm at this location on windows,
14 | # so don't change without a good reason.
15 | SETUP_DIR=~
16 |
17 |
18 | BOLD="\033[1;37m"
19 | NC="\033[0m"
20 |
21 | function heading
22 | {
23 | echo
24 | echo -e "${BOLD}$@$NC"
25 | }
26 |
27 |
28 | cd "$SETUP_DIR"
29 |
30 | if [[ $MSYSTEM == "UCRT64" ]]; then
31 | heading "Installing MSYS2 dependencies..."
32 | pacman -S --needed --noconfirm git make mingw-w64-ucrt-x86_64-python mingw-w64-ucrt-x86_64-python-yaml mingw-w64-ucrt-x86_64-cmake mingw-w64-ucrt-x86_64-gcc
33 | fi
34 |
35 | # Building wla-dx instead of downloading release from github because github
36 | # version depends on some C runtime libraries that may not be installed by
37 | # default. Anyway doing it this way allows the script to work on linux too.
38 | heading "Cloning wla-dx..."
39 | git clone https://github.com/vhelin/wla-dx "$SETUP_DIR/wla-dx"
40 | cd "$SETUP_DIR/wla-dx"
41 | git -c advice.detachedHead=false checkout v10.6
42 |
43 | heading "Building wla-dx..."
44 | rm -R build 2>/dev/null
45 | mkdir build && cd build && cmake .. && cmake --build . --config Release -- wla-gb wlalink
46 |
47 | heading "Copying wla-dx binaries to /usr/local/bin..."
48 | mkdir -p /usr/local/bin 2>/dev/null
49 | cp "$SETUP_DIR"/wla-dx/build/binaries/* /usr/local/bin/
50 |
51 | heading "Cloning oracles-disasm..."
52 | git clone https://github.com/stewmath/oracles-disasm "$SETUP_DIR/oracles-disasm"
53 |
54 | heading "Checking out hack-base branch..."
55 | cd "$SETUP_DIR/oracles-disasm" && git checkout hack-base
56 |
57 | heading "Dependencies and oracles-disasm downloaded, now you can run LynnaLab!"
58 |
--------------------------------------------------------------------------------
/LynnaLab/disassemblyFiles.txt:
--------------------------------------------------------------------------------
1 | - Files that may be read from and written to include:
2 | - Files in the "data/" folder
3 | - Files in the "rooms/" folder
4 | - Files in the "tilesets/" folder
5 | - All files in the "constants" folder are read.
6 | - "include/wram.s" is read.
7 | - w3TileMappingIndices and w3TileCollisions are expected to be defined.
8 | - It currently doesn't handle multiple ramsections for the same bank
9 | properly.
10 | - It also doesn't understand enums yet, only ramsections and defines.
11 |
--------------------------------------------------------------------------------
/LynnaLab/icon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Stewmath/LynnaLab/a17ac808addf0002586b05ce2d6488c5137672f8/LynnaLab/icon.ico
--------------------------------------------------------------------------------
/LynnaLab/log4net.config:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
28 |
29 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
--------------------------------------------------------------------------------
/LynnaLab/publish.sh:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env bash
2 | #
3 | # Run this script to generate distributable builds in the bin/Release/Publish directory. Obviously
4 | # this is a bash script so it works best on Linux.
5 |
6 | projectdir="$PWD"
7 | projectfile="$projectdir/LynnaLab.csproj"
8 | publishbasedir="$projectdir/bin/Release/Publish"
9 | profiledir="$projectdir/Properties/PublishProfiles"
10 | versionfile="$projectdir/../LynnaLib/version.txt"
11 |
12 | # WINDOWS
13 | #==========================================================================
14 | publishdirname="LynnaLab-win64"
15 |
16 | mkdir -p "$publishbasedir"
17 | cd "$publishbasedir"
18 | rm -R "$publishdirname/"
19 |
20 | dotnet publish "$projectfile" /p:PublishProfile="$profiledir/win-x64.pubxml"
21 |
22 | gitversion=$(cat "$versionfile")
23 | zipname="$publishbasedir/LynnaLab-$gitversion-win64.zip"
24 |
25 | # Zip it
26 | echo "Compressing the archive..."
27 | rm "$zipname"
28 | zip -q -r "$zipname" "$publishdirname/"
29 |
30 | # LINUX/PORTABLE
31 | #==========================================================================
32 | publishdirname="LynnaLab-portable"
33 |
34 | rm -R "$publishdirname/"
35 | dotnet publish "$projectfile" /p:PublishProfile="$profiledir/portable.pubxml"
36 |
37 | gitversion=$(cat "$versionfile")
38 | zipname="$publishbasedir/LynnaLab-$gitversion-mac-linux.zip"
39 |
40 | # Zip it
41 | echo "Compressing the archive..."
42 | cd "$publishbasedir"
43 | rm "$zipname"
44 | zip -q -r "$zipname" "$publishdirname/"
45 |
--------------------------------------------------------------------------------
/LynnaLab/src/DocumentationDialog.cs:
--------------------------------------------------------------------------------
1 | namespace LynnaLab;
2 |
3 | public class DocumentationDialog : Frame
4 | {
5 | // ================================================================================
6 | // Constructors
7 | // ================================================================================
8 | public DocumentationDialog(ProjectWorkspace workspace, string name)
9 | : base(name)
10 | {
11 | this.Workspace = workspace;
12 | base.DisplayName = "Documentation";
13 | base.DefaultSize = new Vector2(700, 700);
14 | }
15 |
16 | // ================================================================================
17 | // Variables
18 | // ================================================================================
19 |
20 | // ================================================================================
21 | // Properties
22 | // ================================================================================
23 | public ProjectWorkspace Workspace { get; private set; }
24 | public Project Project { get { return Workspace.Project; } }
25 |
26 | // The documentation currently being displayed
27 | public Documentation Documentation { get; private set; }
28 |
29 | // ================================================================================
30 | // Public methods
31 | // ================================================================================
32 |
33 | public override void Render()
34 | {
35 | ImGui.PushFont(Top.InfoFont);
36 |
37 | if (Documentation == null)
38 | {
39 | ImGui.TextWrapped("No documentation found.");
40 | ImGui.PopFont();
41 | return;
42 | }
43 |
44 | if (Documentation.Description != null && Documentation.Description != "")
45 | {
46 | ImGui.TextWrapped(Documentation.Description);
47 | ImGuiX.ShiftCursorScreenPos(0.0f, 10.0f);
48 | }
49 |
50 | if (ImGui.BeginTable("Field table", 2, ImGuiTableFlags.Resizable | ImGuiTableFlags.Borders))
51 | {
52 | ImGui.TableSetupColumn(Documentation.KeyName);
53 | ImGui.TableSetupColumn("Description");
54 | ImGui.TableHeadersRow();
55 |
56 | foreach (string key in Documentation.Keys)
57 | {
58 | ImGui.TableNextRow();
59 | ImGui.TableSetColumnIndex(0);
60 | ImGui.Text(key);
61 | ImGui.TableSetColumnIndex(1);
62 | ImGui.TextWrapped(Documentation.GetField(key));
63 | }
64 |
65 | ImGui.EndTable();
66 | }
67 |
68 | ImGui.PopFont();
69 | }
70 |
71 | public void SetDocumentation(Documentation doc)
72 | {
73 | Documentation = doc;
74 | if (Documentation != null)
75 | DisplayName = "Documentation: " + Documentation.Name;
76 | else
77 | DisplayName = "Documentation";
78 | }
79 | }
80 |
--------------------------------------------------------------------------------
/LynnaLab/src/GlobalConfig.cs:
--------------------------------------------------------------------------------
1 | using System.IO;
2 | using YamlDotNet.Serialization;
3 |
4 | namespace LynnaLab;
5 |
6 | /// Class to manage global configuration (stored in the LynnaLab program
7 | /// folder). Distinct from per-project configuration (see ProjectConfig.cs
8 | /// in LynnaLib).
9 | public class GlobalConfig
10 | {
11 | static readonly string ConfigFile = "global_config.yaml";
12 | static readonly string ConfigFileComment = @"
13 | # User config file for LynnaLab. You shouldn't need to edit this directly,
14 | # but if you know what you're doing you can edit the commands to build and run the game.
15 |
16 | ".TrimStart();
17 |
18 | static readonly string DefaultMenuFont = "ZeldaOracles.ttf";
19 | static readonly string DefaultInfoFont = "RocknRollOne-Regular.ttf";
20 |
21 |
22 | GlobalConfig oldValues;
23 |
24 | public static bool FileExists()
25 | {
26 | return File.Exists(ConfigFile);
27 | }
28 |
29 | public static GlobalConfig Load()
30 | {
31 | if (!FileExists())
32 | return null;
33 | var input = System.IO.File.ReadAllText(ConfigFile);
34 | var deserializer = new DeserializerBuilder()
35 | .IgnoreUnmatchedProperties()
36 | .Build();
37 |
38 | GlobalConfig retval = null;
39 | try
40 | {
41 | retval = deserializer.Deserialize(input);
42 | }
43 | catch (Exception)
44 | {
45 | Modal.DisplayMessageModal("Error", "Error parsing global_config.yaml. Default settings will be used.");
46 | }
47 |
48 | if (retval != null)
49 | {
50 | // Validate values
51 | if (retval.DisplayScaleFactor < 1.0f)
52 | retval.DisplayScaleFactor = 1.0f;
53 | if (!Top.AvailableFonts.Contains(retval.MenuFont))
54 | retval.MenuFont = DefaultMenuFont;
55 | if (!Top.AvailableFonts.Contains(retval.InfoFont))
56 | retval.InfoFont = DefaultInfoFont;
57 |
58 | retval.oldValues = new GlobalConfig(retval);
59 | }
60 | return retval;
61 | }
62 |
63 |
64 | public GlobalConfig() { }
65 |
66 | /// Copy constructor: Copy all fields from another instance
67 | public GlobalConfig(GlobalConfig c)
68 | {
69 | var fields = this.GetType().GetFields();
70 | foreach (var field in fields)
71 | {
72 | field.SetValue(this, field.GetValue(c));
73 | }
74 | }
75 |
76 | public void Save()
77 | {
78 | if (this.Equals(oldValues))
79 | return;
80 |
81 | var serializer = new SerializerBuilder()
82 | .Build();
83 | var yaml = serializer.Serialize(this);
84 | System.IO.File.WriteAllText(ConfigFile, ConfigFileComment + yaml);
85 |
86 | oldValues = new GlobalConfig(this);
87 | }
88 |
89 | // Variables imported from YAML config file
90 |
91 | // Advanced settings
92 | public string MakeCommand { get; set; }
93 | public string EmulatorCommand { get; set; }
94 |
95 | // Display
96 | public bool LightMode { get; set; } = false;
97 | public Interpolation Interpolation { get; set; } = Interpolation.Bilinear;
98 | public bool DarkenDuplicateRooms { get; set; } = true;
99 | public bool ShowBrushPreview { get; set; } = true;
100 | public bool OverrideSystemScaling { get; set; } = false;
101 | public float DisplayScaleFactor { get; set; } = 1.0f;
102 |
103 | // Fonts
104 | public string MenuFont { get; set; } = DefaultMenuFont;
105 | public string InfoFont { get; set; } = DefaultInfoFont;
106 | public int MenuFontSize { get; set; } = 18;
107 | public int InfoFontSize { get; set; } = 20;
108 |
109 | // Build & Run dialog
110 | public bool CloseRunDialogWithEmulator { get; set; } = false;
111 | public bool CloseEmulatorWithRunDialog { get; set; } = false;
112 |
113 | // Other
114 | public bool AutoAdjustGroupNumber { get; set; } = true;
115 | public bool ScrollToZoom { get; set; } = true;
116 | }
117 |
--------------------------------------------------------------------------------
/LynnaLab/src/Program.cs:
--------------------------------------------------------------------------------
1 | // global using directives apply to all files in the project (C#10 feature).
2 | global using System;
3 | global using System.Collections.Generic;
4 | global using System.Linq;
5 | global using System.Numerics;
6 | global using ImGuiNET;
7 | global using LynnaLib;
8 | global using Util;
9 | global using OneOf;
10 | global using OneOf.Types;
11 |
12 | global using Debug = System.Diagnostics.Debug;
13 |
14 | // Veldrid backend stuff
15 | global using Interpolation = VeldridBackend.Interpolation;
16 | global using Palette = VeldridBackend.VeldridPalette;
17 | global using TextureBase = VeldridBackend.VeldridTextureBase;
18 | global using RgbaTexture = VeldridBackend.VeldridRgbaTexture;
19 | global using TextureWindow = VeldridBackend.VeldridTextureWindow;
20 | global using TextureModifiedEventArgs = VeldridBackend.TextureModifiedEventArgs;
21 |
22 | using System.Runtime.InteropServices;
23 |
24 | [assembly: log4net.Config.XmlConfigurator(Watch = true)]
25 |
26 | namespace LynnaLab;
27 |
28 | class Program
29 | {
30 | public static Exception handlingException;
31 |
32 | ///
33 | /// Program entry point.
34 | ///
35 | static int Main(string[] args)
36 | {
37 | try
38 | {
39 | if (args.Length >= 2)
40 | Top.Load(args[0], args[1]);
41 | else if (args.Length >= 1)
42 | Top.Load(args[0]);
43 | else
44 | {
45 | if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
46 | {
47 | string path = $"C:\\msys64\\home\\{Environment.UserName}\\oracles-disasm";
48 | Top.Load(path, implicitWindowsOpen: true);
49 | }
50 | else
51 | Top.Load();
52 | }
53 |
54 | while (!Top.Backend.Exited)
55 | {
56 | Top.Run();
57 | }
58 | }
59 | catch (Exception e)
60 | {
61 | // This is our generic exception handler - we print the exception to the console and
62 | // attempt to create a messagebox with SDL.
63 |
64 | // If we end up here twice then something must be wrong with our exception handler,
65 | // so just give up and throw it. (This is only relevant to the old imgui-based exception
66 | // handler, not the current SDL-based one.)
67 | if (handlingException != null)
68 | throw handlingException;
69 |
70 | handlingException = e;
71 | Console.WriteLine("Unhandled exception occurred!");
72 | Console.WriteLine(e);
73 |
74 | string message = "An unhandled exception occurred! Take a screenshot and show this to Stewmat.\n\nLynnaLab will now terminate. Click \"Save & exit\" to attempt to save your project before closing.\n\n" + e;
75 | int option = SDLUtil.SDLHelper.ShowErrorMessageBox(
76 | "Error",
77 | message,
78 | new string[] { "Save & exit", "Don't save" });
79 |
80 | if (option == 0)
81 | Top.SaveProject();
82 |
83 | return 1;
84 | }
85 |
86 | return 0;
87 | }
88 | }
89 |
--------------------------------------------------------------------------------
/LynnaLab/src/RoomTextureCacher.cs:
--------------------------------------------------------------------------------
1 | namespace LynnaLab;
2 |
3 | ///
4 | /// Caches textures for room layouts. Actually just reads off the cached overworld map image with
5 | /// the desired room in it.
6 | ///
7 | public class RoomTextureCacher : IDisposeNotifier
8 | {
9 | // ================================================================================
10 | // Constructors
11 | // ================================================================================
12 | public RoomTextureCacher(ProjectWorkspace workspace, RoomLayout layout)
13 | {
14 | Workspace = workspace;
15 | Layout = layout;
16 |
17 | GenerateTexture();
18 | }
19 |
20 | // ================================================================================
21 | // Variables
22 | // ================================================================================
23 |
24 | /*
25 | Dictionary> tilesetEventWrappers
26 | = new Dictionary>();
27 | */
28 |
29 | TextureBase roomTexture;
30 | RgbaTexture mapTexture; // Retrieved from MapTextureCacher
31 |
32 | // ================================================================================
33 | // Properties
34 | // ================================================================================
35 |
36 | public ProjectWorkspace Workspace { get; private set; }
37 | public RoomLayout Layout { get; private set; }
38 |
39 | // ================================================================================
40 | // Events
41 | // ================================================================================
42 |
43 | public event EventHandler DisposedEvent;
44 |
45 | // ================================================================================
46 | // Public methods
47 | // ================================================================================
48 |
49 | public TextureBase GetTexture()
50 | {
51 | return roomTexture;
52 | }
53 |
54 | // Should never be called except when closing a project.
55 | public void Dispose()
56 | {
57 | roomTexture.Dispose();
58 | roomTexture = null;
59 | // Don't dispose mapTexture as we're not the owner
60 | DisposedEvent?.Invoke(this, null);
61 | }
62 |
63 | // ================================================================================
64 | // Protected methods
65 | // ================================================================================
66 |
67 | void GenerateTexture()
68 | {
69 | // Get the overworld map image with the room we want
70 | int x = Layout.Room.Index % 16;
71 | int y = (Layout.Room.Index % 256) / 16;
72 | var roomSize = Layout.Size * 16;
73 | this.mapTexture = Workspace.GetCachedMapTexture((Workspace.Project.GetWorldMap(Layout.Room.Group, Layout.Season), 0));
74 | roomTexture = Top.Backend.CreateTextureWindow(mapTexture, new Point(x, y) * roomSize, roomSize);
75 | }
76 | }
77 |
--------------------------------------------------------------------------------
/LynnaLab/src/SDLUtil/InputSnapshot.cs:
--------------------------------------------------------------------------------
1 | using SDL;
2 |
3 | namespace SDLUtil;
4 |
5 | public enum MouseButton
6 | {
7 | Left,
8 | Right,
9 | Middle,
10 | Button1,
11 | Button2
12 | }
13 |
14 | public struct KeyEvent
15 | {
16 | public KeyEvent(SDL_Keycode key, bool down)
17 | {
18 | this.Key = key;
19 | this.Down = down;
20 | }
21 | public SDL_Keycode Key;
22 | public bool Down;
23 | }
24 |
25 | ///
26 | /// Interface to use for seeing what kinds of inputs have been received since last poll.
27 | ///
28 | public interface InputSnapshot
29 | {
30 | // ================================================================================
31 | // Properties
32 | // ================================================================================
33 |
34 | public Vector2 MousePosition { get; }
35 | public float WheelDelta { get; }
36 | public IReadOnlyList KeyCharPresses { get; }
37 | public IReadOnlyList KeyEvents { get; }
38 |
39 | // ================================================================================
40 | // Public methods
41 | // ================================================================================
42 |
43 | public bool IsMouseDown(MouseButton button);
44 | }
45 |
46 | ///
47 | /// Implementation of InputSnapshot for SDL.
48 | ///
49 | internal class SDLInputSnapshot : InputSnapshot
50 | {
51 | public Vector2 MousePosition { get; set; }
52 | public float WheelDelta { get; set; }
53 | public IReadOnlyList KeyCharPresses { get; set; }
54 | public IReadOnlyList KeyEvents { get; set; }
55 |
56 | public SDL_MouseButtonFlags mouseButtonFlags;
57 |
58 | public bool IsMouseDown(MouseButton button)
59 | {
60 | SDL_MouseButtonFlags mask;
61 | switch (button)
62 | {
63 | case MouseButton.Left:
64 | mask = SDL_MouseButtonFlags.SDL_BUTTON_LMASK;
65 | break;
66 | case MouseButton.Right:
67 | mask = SDL_MouseButtonFlags.SDL_BUTTON_RMASK;
68 | break;
69 | case MouseButton.Middle:
70 | mask = SDL_MouseButtonFlags.SDL_BUTTON_MMASK;
71 | break;
72 | case MouseButton.Button1:
73 | mask = SDL_MouseButtonFlags.SDL_BUTTON_X1MASK;
74 | break;
75 | case MouseButton.Button2:
76 | mask = SDL_MouseButtonFlags.SDL_BUTTON_X2MASK;
77 | break;
78 | default:
79 | throw new Exception($"Unrecognized mouse button: {button}");
80 | }
81 |
82 | return (mouseButtonFlags & mask) != 0;
83 | }
84 | }
85 |
--------------------------------------------------------------------------------
/LynnaLab/src/SDLUtil/SDLHelper.cs:
--------------------------------------------------------------------------------
1 | using System.Runtime.CompilerServices;
2 | using System.Runtime.InteropServices;
3 | using SDL;
4 | using static SDL.SDL3;
5 |
6 | namespace SDLUtil;
7 |
8 | public static class SDLHelper
9 | {
10 | // ================================================================================
11 | // Public methods
12 | // ================================================================================
13 |
14 | public static unsafe void ShowOpenFileDialog(SDLWindow window,
15 | string location,
16 | IReadOnlyList<(string name, string pattern)> filters,
17 | Action callback)
18 | {
19 | SDL_DialogFileFilter[] sfilters = new SDL_DialogFileFilter[filters.Count];
20 |
21 | for (int i=0; i callback)
42 | {
43 | GCHandle handle = GCHandle.Alloc(callback);
44 | SDL_ShowOpenFolderDialog(&OpenFileCallback, GCHandle.ToIntPtr(handle), window.Handle, location, false);
45 | }
46 |
47 | public static unsafe void RunOnMainThread(Action action)
48 | {
49 | GCHandle handle = GCHandle.Alloc(action);
50 | SDL_RunOnMainThread(&RunOnMainThreadCallback, GCHandle.ToIntPtr(handle), false);
51 | }
52 |
53 | ///
54 | /// Show a message box with the given buttons, returns the index of the button pressed.
55 | ///
56 | public static unsafe int ShowErrorMessageBox(string title, string message, IReadOnlyList buttons)
57 | {
58 | SDL_MessageBoxData data;
59 | data.flags = SDL_MessageBoxFlags.SDL_MESSAGEBOX_ERROR;
60 | data.window = null;
61 | data.title = (byte*)Marshal.StringToCoTaskMemUTF8(title);
62 | data.message = (byte*)Marshal.StringToCoTaskMemUTF8(message);
63 | data.numbuttons = buttons.Count;
64 | data.buttons = (SDL_MessageBoxButtonData*)Marshal.AllocCoTaskMem(sizeof(SDL_MessageBoxButtonData) * buttons.Count);
65 | data.colorScheme = null;
66 |
67 | for (int i=0; i
109 | {
110 | GCHandle handle = GCHandle.FromIntPtr(userdata);
111 | ((Action)handle.Target)(name);
112 | handle.Free();
113 | });
114 | }
115 |
116 | [UnmanagedCallersOnly(CallConvs = [typeof(CallConvCdecl)])]
117 | private unsafe static void RunOnMainThreadCallback(nint userdata)
118 | {
119 | GCHandle handle = GCHandle.FromIntPtr(userdata);
120 | ((Action)handle.Target)();
121 | handle.Free();
122 | }
123 | }
124 |
--------------------------------------------------------------------------------
/LynnaLab/src/TilesetTextureCacher.cs:
--------------------------------------------------------------------------------
1 | namespace LynnaLab;
2 |
3 | ///
4 | /// Caches textures for tilesets arranged in a 16x16 configuration.
5 | ///
6 | public class TilesetTextureCacher : IDisposeNotifier
7 | {
8 | // ================================================================================
9 | // Constructors
10 | // ================================================================================
11 | public TilesetTextureCacher(ProjectWorkspace workspace, Tileset tileset)
12 | {
13 | Workspace = workspace;
14 | Tileset = tileset;
15 |
16 | GenerateTexture();
17 | }
18 |
19 | // ================================================================================
20 | // Variables
21 | // ================================================================================
22 |
23 | RgbaTexture tilesetTexture;
24 | bool modified = false;
25 |
26 | // ================================================================================
27 | // Properties
28 | // ================================================================================
29 |
30 | public ProjectWorkspace Workspace { get; private set; }
31 | public Tileset Tileset { get; private set; }
32 |
33 | // ================================================================================
34 | // Events
35 | // ================================================================================
36 |
37 | public event EventHandler DisposedEvent;
38 |
39 | // ================================================================================
40 | // Public methods
41 | // ================================================================================
42 |
43 | public TextureBase GetTexture()
44 | {
45 | return tilesetTexture;
46 | }
47 |
48 | ///
49 | /// Called once per frame. Renders anything that was queued up.
50 | ///
51 | public void UpdateFrame()
52 | {
53 | if (modified)
54 | Render();
55 | modified = false;
56 | }
57 |
58 | public void Dispose()
59 | {
60 | tilesetTexture.Dispose();
61 | tilesetTexture = null;
62 | DisposedEvent?.Invoke(this, null);
63 | }
64 |
65 | // ================================================================================
66 | // Private methods
67 | // ================================================================================
68 |
69 | void GenerateTexture()
70 | {
71 | tilesetTexture = Top.Backend.CreateTexture(256, 256, renderTarget: true);
72 | Render();
73 |
74 | Tileset.TileModifiedEvent += (sender, tile) =>
75 | {
76 | modified = true;
77 | };
78 |
79 | Tileset.DisposedEvent += (sender) =>
80 | {
81 | Dispose();
82 | };
83 | }
84 |
85 |
86 | void Render()
87 | {
88 | Top.Backend.RenderTileset(tilesetTexture, Tileset);
89 | }
90 |
91 | ///
92 | /// This redraws a single tile using software rendering.
93 | ///
94 | /// Our GPU pipeline is only capable of redrawing the entire tileset at once. Even so, that
95 | /// works well enough, so this function is unused. (Anyway, there could be annoying cases where
96 | /// this gets called multiple times for the same tile in a single frame - so it's not ideal
97 | /// anyway.)
98 | ///
99 | void DrawTile(int x, int y)
100 | {
101 | int index = x + y * 16;
102 | var bitmap = Tileset.GetTileBitmap(index);
103 | var bitmapTexture = Top.TextureFromBitmapTracked(bitmap);
104 |
105 | bitmapTexture.DrawOn(tilesetTexture,
106 | new Point(0, 0),
107 | new Point(x * 16, y * 16),
108 | new Point(16, 16));
109 | }
110 | }
111 |
--------------------------------------------------------------------------------
/LynnaLab/src/Widget/GfxViewer.cs:
--------------------------------------------------------------------------------
1 | namespace LynnaLab;
2 |
3 | public class GfxViewer : TileGrid
4 | {
5 | // ================================================================================
6 | // Constructors
7 | // ================================================================================
8 |
9 | public GfxViewer(ProjectWorkspace workspace, string name) : base(name)
10 | {
11 | Workspace = workspace;
12 |
13 | base.TileWidth = 8;
14 | base.TileHeight = 8;
15 | base.Width = 0;
16 | base.Height = 0;
17 | base.RequestedScale = 2;
18 | base.Selectable = true;
19 | }
20 |
21 |
22 | // ================================================================================
23 | // Variables
24 | // ================================================================================
25 |
26 | RgbaTexture texture;
27 |
28 | GraphicsState graphicsState;
29 | int offsetStart, offsetEnd;
30 |
31 | // ================================================================================
32 | // Properties
33 | // ================================================================================
34 |
35 | public ProjectWorkspace Workspace { get; private set; }
36 |
37 | public override TextureBase Texture
38 | {
39 | get { return texture; }
40 | }
41 |
42 | // ================================================================================
43 | // Public methods
44 | // ================================================================================
45 |
46 | public void SetGraphicsState(GraphicsState state, int offsetStart, int offsetEnd, int width = -1, int scale = 2)
47 | {
48 | var tileModifiedHandler = (int bank, int tile) =>
49 | {
50 | if (bank == -1 && tile == -1) // Full invalidation
51 | RedrawAll();
52 | else
53 | Draw(tile + bank * 0x180);
54 | };
55 |
56 | if (graphicsState != null)
57 | graphicsState.RemoveTileModifiedHandler(tileModifiedHandler);
58 | if (state != null)
59 | state.AddTileModifiedHandler(tileModifiedHandler);
60 |
61 | graphicsState = state;
62 |
63 | int size = (offsetEnd - offsetStart) / 16;
64 | if (width == -1)
65 | width = (int)Math.Sqrt(size);
66 | int height = size / width;
67 |
68 | this.offsetStart = offsetStart;
69 | this.offsetEnd = offsetEnd;
70 |
71 | Width = width;
72 | Height = height;
73 | TileWidth = 8;
74 | TileHeight = 8;
75 | RequestedScale = scale;
76 |
77 | // NOTE: Should dispose this at some point
78 | texture = Top.Backend.CreateTexture(Width * TileWidth, Height * TileHeight);
79 |
80 | RedrawAll();
81 | }
82 |
83 | // ================================================================================
84 | // Private methods
85 | // ================================================================================
86 |
87 | void RedrawAll()
88 | {
89 | for (int i = offsetStart / 16; i < offsetEnd / 16; i++)
90 | Draw(i);
91 | }
92 |
93 | ///
94 | /// Draw a tile. This uses CPU-based drawing and is not very efficient.
95 | ///
96 | void Draw(int tile)
97 | {
98 | int offset = tile * 16;
99 |
100 | if (!(offset >= offsetStart && offset < offsetEnd))
101 | return;
102 |
103 | int x = ((offset - offsetStart) / 16) % Width;
104 | int y = ((offset - offsetStart) / 16) / Width;
105 |
106 | int bank = 0;
107 | if (offset >= 0x1800)
108 | {
109 | offset -= 0x1800;
110 | bank = 1;
111 | }
112 | byte[] data = new byte[16];
113 | Array.Copy(graphicsState.VramBuffer[bank], offset, data, 0, 16);
114 |
115 | using (Bitmap _subImage = GbGraphics.RawTileToBitmap(data))
116 | {
117 | RgbaTexture subTexture = Top.Backend.TextureFromBitmap(_subImage);
118 | subTexture.DrawOn(texture,
119 | new Point(0, 0),
120 | new Point(x * 8, y * 8),
121 | new Point(8, 8));
122 | subTexture.Dispose();
123 | }
124 | }
125 | }
126 |
--------------------------------------------------------------------------------
/LynnaLab/src/Widget/ProcessOutputView.cs:
--------------------------------------------------------------------------------
1 | using System.Diagnostics;
2 | using System.Text.RegularExpressions;
3 |
4 | namespace LynnaLab;
5 |
6 | public class ProcessOutputView
7 | {
8 | // ================================================================================
9 | // Constructors
10 | // ================================================================================
11 | public ProcessOutputView()
12 | {
13 |
14 | }
15 |
16 | // ================================================================================
17 | // Variables
18 | // ================================================================================
19 |
20 | int jumpToBottom;
21 | Process process;
22 | List textList = new List();
23 |
24 | // ================================================================================
25 | // Properties
26 | // ================================================================================
27 |
28 | // ================================================================================
29 | // Public methods
30 | // ================================================================================
31 |
32 | public void Render()
33 | {
34 | ImGui.PushFont(Top.InfoFont);
35 | foreach (var entry in textList)
36 | {
37 | if (entry.color != null)
38 | {
39 | ImGui.PushStyleColor(ImGuiCol.Text, (Color)entry.color);
40 | ImGui.TextWrapped(entry.text);
41 | ImGui.PopStyleColor();
42 | }
43 | else
44 | ImGui.TextWrapped(entry.text);
45 | }
46 |
47 | if (jumpToBottom != 0)
48 | {
49 | ImGui.SetScrollY(ImGui.GetScrollMaxY());
50 | jumpToBottom--;
51 | }
52 | ImGui.PopFont();
53 | }
54 |
55 | public void AppendText(string text, string preset = "system")
56 | {
57 | if (text == null)
58 | return;
59 | Vector4? color;
60 | switch (preset)
61 | {
62 | case "code":
63 | color = null;
64 | break;
65 | case "error":
66 | color = new Vector4(1.0f, 0.0f, 0.0f, 1.0f);
67 | break;
68 | case "system":
69 | default:
70 | color = new Vector4(0.7f, 0.7f, 0.7f, 1.0f);
71 | break;
72 | }
73 | this.textList.Add(new TextEntry { text = StripAnsiCodes(text), color = color });
74 | jumpToBottom = 2;
75 | }
76 |
77 | ///
78 | /// Returns false if the process failed to start.
79 | ///
80 | public bool AttachAndStartProcess(Process process)
81 | {
82 | if (process != null)
83 | {
84 | process.OutputDataReceived -= AppendTextHandler;
85 | process.ErrorDataReceived -= AppendTextHandler;
86 | }
87 |
88 | this.process = process;
89 | process.OutputDataReceived += AppendTextHandler;
90 | process.ErrorDataReceived += AppendTextHandler;
91 |
92 | if (!process.Start())
93 | return false;
94 |
95 | process.BeginOutputReadLine();
96 | process.BeginErrorReadLine();
97 |
98 | return true;
99 | }
100 |
101 | // ================================================================================
102 | // Private methods
103 | // ================================================================================
104 |
105 | void AppendTextHandler(object sender, DataReceivedEventArgs args)
106 | {
107 | Helper.MainThreadInvoke(() =>
108 | {
109 | AppendText(args.Data, "code");
110 | });
111 | }
112 |
113 | static string StripAnsiCodes(string input)
114 | {
115 | string pattern = @"\x1B\[[0-9;]*[mK]";
116 | return Regex.Replace(input, pattern, string.Empty);
117 | }
118 | }
119 |
120 | struct TextEntry
121 | {
122 | public string text;
123 | public Vector4? color;
124 | }
125 |
--------------------------------------------------------------------------------
/LynnaLab/src/Widget/SelectionBox.cs:
--------------------------------------------------------------------------------
1 | namespace LynnaLab;
2 |
3 | ///
4 | /// A "box" allowing one to select items in it, drag them to change ordering, and right-click to
5 | /// bring up a menu.
6 | ///
7 | public abstract class SelectionBox : TileGrid
8 | {
9 | // ================================================================================
10 | // Constructors
11 | // ================================================================================
12 |
13 | public SelectionBox(string name) : base(name)
14 | {
15 | base.Width = 8;
16 | base.Height = 2;
17 | base.TileWidth = 18;
18 | base.TileHeight = 18;
19 | base.TilePaddingX = 5;
20 | base.TilePaddingY = 5;
21 |
22 | base.Selectable = true;
23 |
24 | // TODO
25 | //base.BackgroundColor = Color.FromRgbDbl(0.8, 0.8, 0.8);
26 | base.HoverColor = Color.Cyan;
27 |
28 | TileGridEventHandler dragCallback = (sender, args) =>
29 | {
30 | if (args.selectedIndex == SelectedIndex)
31 | return;
32 | if (SelectedIndex != -1 && args.selectedIndex != -1)
33 | OnMoveSelection(SelectedIndex, args.selectedIndex);
34 | SelectedIndex = args.selectedIndex;
35 | };
36 |
37 | base.AddMouseAction(MouseButton.LeftClick, MouseModifier.Any, MouseAction.Drag,
38 | GridAction.Callback, dragCallback);
39 |
40 |
41 | // TODO
42 | // this.ButtonPressEvent += (sender, args) =>
43 | // {
44 | // if (args.Event.Button == 3)
45 | // {
46 | // if (HoveringIndex != -1)
47 | // SelectedIndex = HoveringIndex;
48 | // ShowPopupMenu(args.Event);
49 | // }
50 | // };
51 | }
52 |
53 | // ================================================================================
54 | // Public methods
55 | // ================================================================================
56 |
57 | public override void Render()
58 | {
59 | // Grey rectangle in background
60 | var pos = ImGui.GetCursorScreenPos();
61 | ImGui.GetWindowDrawList().AddRectFilled(pos, pos + WidgetSize, Color.FromRgb(0x40, 0x40, 0x40).ToUInt());
62 |
63 | base.Render();
64 |
65 | // Catch right clicks outside any existing components
66 | ImGui.SetCursorScreenPos(base.origin);
67 | if (ImGui.InvisibleButton("Background button", base.WidgetSize, ImGuiButtonFlags.MouseButtonRight))
68 | {
69 | ImGui.OpenPopup("AddPopupMenu");
70 | }
71 |
72 | // Popup menu (right clicked on an empty spot)
73 | if (ImGui.BeginPopup("AddPopupMenu"))
74 | {
75 | RenderPopupMenu();
76 | }
77 | }
78 |
79 | public void SetSelectedIndex(int index)
80 | {
81 | SelectedIndex = index;
82 | }
83 |
84 | // ================================================================================
85 | // Protected methods
86 | // ================================================================================
87 |
88 | protected abstract void OnMoveSelection(int oldIndex, int newIndex);
89 |
90 | ///
91 | /// Invoked after right-clicking on an empty spot within an "ImGui.BeginPopup" context
92 | ///
93 | protected abstract void RenderPopupMenu();
94 | }
95 |
--------------------------------------------------------------------------------
/LynnaLab/src/Widget/SizedWidget.cs:
--------------------------------------------------------------------------------
1 | namespace LynnaLab;
2 |
3 | ///
4 | /// A widget whose size is precisely defined.
5 | ///
6 | public abstract class SizedWidget
7 | {
8 | // ================================================================================
9 | // Properties
10 | // ================================================================================
11 | public abstract Vector2 WidgetSize { get; }
12 |
13 | // Whether to center the widget within the available region
14 | public bool CenterX { get; set; }
15 | public bool CenterY { get; set; }
16 |
17 | // Leave this much space above and to the left of the start of the render area.
18 | // Usually this is (0,0). Specify this in coordinates BEFORE global scaling.
19 | public Vector2 RenderOffset { get; set; }
20 |
21 | // ================================================================================
22 | // Public methods
23 | // ================================================================================
24 |
25 | ///
26 | /// Call this just before rendering begins to set up some helper stuff. ImGui cursor position
27 | /// should be at the start of the render area when this is called.
28 | ///
29 | public void RenderPrep()
30 | {
31 | ImGuiX.ShiftCursorScreenPos(RenderOffset * ImGuiX.ScaleUnit);
32 | var cursor = ImGui.GetCursorScreenPos();
33 | var avail = ImGui.GetContentRegionAvail();
34 |
35 | float x = cursor.X;
36 | float y = cursor.Y;
37 | if (CenterX)
38 | x += (avail.X - WidgetSize.X) / 2;
39 | if (CenterY)
40 | y += (avail.Y - WidgetSize.Y) / 2;
41 |
42 | origin = new Vector2(x, y);
43 | ImGui.SetCursorScreenPos(origin);
44 | drawList = ImGui.GetWindowDrawList();
45 | }
46 |
47 | ///
48 | /// Get the position of the mouse relative to the widget origin
49 | ///
50 | public Vector2 GetRelativeMousePos()
51 | {
52 | var io = ImGui.GetIO();
53 | var mousePos = io.MousePos - origin;
54 | return new Vector2((int)mousePos.X, (int)mousePos.Y);
55 | }
56 |
57 | public void AddRect(FRect rect, Color color, float thickness = 1.0f)
58 | {
59 | drawList.AddRect(
60 | origin + new Vector2(rect.X, rect.Y),
61 | origin + new Vector2(rect.X + rect.Width, rect.Y + rect.Height),
62 | color.ToUInt(),
63 | 0,
64 | 0,
65 | thickness);
66 | }
67 |
68 | public void AddRectFilled(FRect rect, Color color)
69 | {
70 | drawList.AddRectFilled(
71 | origin + new Vector2(rect.X, rect.Y),
72 | origin + new Vector2(rect.X + rect.Width, rect.Y + rect.Height),
73 | color.ToUInt());
74 | }
75 |
76 | // ================================================================================
77 | // Variables
78 | // ================================================================================
79 | protected Vector2 origin;
80 | protected ImDrawListPtr drawList;
81 | }
82 |
--------------------------------------------------------------------------------
/LynnaLab/src/Widget/TilesetViewer.cs:
--------------------------------------------------------------------------------
1 | namespace LynnaLab;
2 |
3 | ///
4 | /// Viewing a tileset & selecting tiles from it
5 | ///
6 | public class TilesetViewer : TileGrid
7 | {
8 | // ================================================================================
9 | // Constructors
10 | // ================================================================================
11 | public TilesetViewer(ProjectWorkspace workspace)
12 | : base("Tileset Viewer")
13 | {
14 | this.Workspace = workspace;
15 |
16 | base.TileWidth = 16;
17 | base.TileHeight = 16;
18 | base.Width = 16;
19 | base.Height = 16;
20 | base.Selectable = true;
21 | base.TooltipImagePreview = true;
22 |
23 | base.OnHover = (tile) =>
24 | {
25 | if (!base.TooltipImagePreview)
26 | return;
27 | ImGui.BeginTooltip();
28 | ImGui.PushFont(Top.InfoFont);
29 | if (subTileMode)
30 | {
31 | var (t, x, y) = ToSubTileIndex(tile);
32 | ImGui.Text($"Subtile {Tileset.GetSubTileIndex(t, x, y):X2}");
33 | ImGui.Text($"Palette {Tileset.GetSubTilePalette(t, x, y)}");
34 | ImGuiX.Checkbox("Flip X", (Tileset.GetSubTileFlags(t, x, y) & 0x20) != 0, (_) => {});
35 | ImGuiX.Checkbox("Flip Y", (Tileset.GetSubTileFlags(t, x, y) & 0x40) != 0, (_) => {});
36 | ImGuiX.Checkbox("Priority", (Tileset.GetSubTileFlags(t, x, y) & 0x80) != 0, (_) => {});
37 | }
38 | else
39 | {
40 | ImGui.Text($"Tile {tile:X2}");
41 | }
42 | ImGui.PopFont();
43 | ImGui.EndTooltip();
44 | };
45 | }
46 |
47 | // ================================================================================
48 | // Variables
49 | // ================================================================================
50 | Tileset tileset;
51 | TextureBase image;
52 | bool subTileMode; // If true, we select subtiles (1/4th of a tile) instead of whole tiles
53 |
54 | // ================================================================================
55 | // Properties
56 | // ================================================================================
57 |
58 | public ProjectWorkspace Workspace { get; private set; }
59 | public Project Project { get { return Workspace.Project; } }
60 |
61 | public Tileset Tileset { get { return tileset; } }
62 |
63 | public bool SubTileMode
64 | {
65 | get { return subTileMode; }
66 | set
67 | {
68 | if (subTileMode == value)
69 | return;
70 | subTileMode = value;
71 | if (subTileMode)
72 | {
73 | base.TileWidth = 8;
74 | base.TileHeight = 8;
75 | base.Width = 32;
76 | base.Height = 32;
77 | }
78 | else
79 | {
80 | base.TileWidth = 16;
81 | base.TileHeight = 16;
82 | base.Width = 16;
83 | base.Height = 16;
84 | }
85 | }
86 | }
87 |
88 | public override TextureBase Texture
89 | {
90 | get
91 | {
92 | return image;
93 | }
94 | }
95 |
96 | // ================================================================================
97 | // Public methods
98 | // ================================================================================
99 |
100 | public override void Render()
101 | {
102 | base.Render();
103 | }
104 |
105 | public void SetTileset(Tileset t)
106 | {
107 | if (tileset != t)
108 | {
109 | tileset = t;
110 | OnTilesetChanged();
111 | }
112 | }
113 |
114 | ///
115 | /// In subtile mode, takes a TileGrid index and returns:
116 | /// - t: The tile in the tileset (0-255)
117 | /// - x: The x offset of the subtile (0-1)
118 | /// - y: The y offset of the subtile (0-1)
119 | ///
120 | public (int, int, int) ToSubTileIndex(int index)
121 | {
122 | int t = ((index % 32) / 2) + ((index / 32) / 2) * 16;
123 | int x = index % 2;
124 | int y = (index / 32) % 2;
125 |
126 | return (t, x, y);
127 | }
128 |
129 | // ================================================================================
130 | // Private methods
131 | // ================================================================================
132 |
133 | ///
134 | /// Called when the tileset is changed
135 | ///
136 | void OnTilesetChanged()
137 | {
138 | image = null;
139 |
140 | if (tileset != null)
141 | {
142 | image = Workspace.GetCachedTilesetTexture(tileset);
143 | }
144 | }
145 | }
146 |
--------------------------------------------------------------------------------
/LynnaLab/src/Widget/TransactionDialog.cs:
--------------------------------------------------------------------------------
1 | namespace LynnaLab;
2 |
3 | public class TransactionDialog : Frame
4 | {
5 | // ================================================================================
6 | // Constructors
7 | // ================================================================================
8 | public TransactionDialog(ProjectWorkspace workspace, string name)
9 | : base(name)
10 | {
11 | this.Workspace = workspace;
12 | base.DefaultSize = new Vector2(350, 400);
13 | }
14 |
15 | // ================================================================================
16 | // Variables
17 | // ================================================================================
18 |
19 |
20 | // ================================================================================
21 | // Properties
22 | // ================================================================================
23 |
24 | public ProjectWorkspace Workspace { get; private set; }
25 | public Project Project { get { return Workspace.Project; } }
26 | public TransactionManager TransactionManager { get { return Project.TransactionManager; } }
27 |
28 | // ================================================================================
29 | // Public methods
30 | // ================================================================================
31 |
32 | public override void Render()
33 | {
34 | ImGui.PushFont(Top.InfoFont);
35 |
36 | if (TransactionManager.constructingTransaction.Empty)
37 | {
38 | ImGui.Text("Pending transaction: None");
39 | ImGui.Separator();
40 | }
41 | else
42 | {
43 | ImGui.Text("Pending transaction: " + TransactionManager.constructingTransaction.Description);
44 | }
45 |
46 | ImGui.Text("Transaction History:");
47 |
48 | int index = 0;
49 | foreach (var transactionNode in TransactionManager.TransactionHistory.Reverse())
50 | {
51 | DrawTransaction(transactionNode, index++);
52 | }
53 |
54 | ImGui.PopFont();
55 | }
56 |
57 | // ================================================================================
58 | // Private methods
59 | // ================================================================================
60 |
61 | void DrawTransaction(TransactionNode node, int index)
62 | {
63 | string keyString = "###Transaction" + index;
64 | if (ImGui.CollapsingHeader((node.Apply ? "Apply" : "Unapply") + $": {node.Description}{keyString}"))
65 | {
66 | ImGui.Text("CreatorID: " + node.Transaction.CreatorID);
67 | ImGui.Text("TransactionID: " + node.Transaction.TransactionID);
68 | }
69 | }
70 | }
71 |
--------------------------------------------------------------------------------
/LynnaLab/windows-setup.bat:
--------------------------------------------------------------------------------
1 | @echo off
2 | setlocal
3 |
4 |
5 | rem Executing this file will set up everything required to use LynnaLab, in
6 | rem particular downloading oracles-disasm and the dependencies needed to
7 | rem build it. MSYS2 must be installed first.
8 |
9 |
10 | set "msys_path=C:\msys64"
11 |
12 | if not exist "%msys_path%\msys2_shell.cmd" (
13 | echo MSYS2 does not appear to be installed. Install it with default settings, then rerun this script.
14 | echo https://www.msys2.org/
15 | pause
16 | exit /b 1
17 | )
18 |
19 | call :RunMsysCommand "./build-setup.sh"
20 |
21 | pause
22 |
23 | goto :eof
24 |
25 |
26 | rem Run a bash command in an MSYS UCRT64 environment.
27 | :RunMsysCommand
28 | call %msys_path%\msys2_shell.cmd -defterm -no-start -ucrt64 -here -shell bash -c "%~1"
29 | goto :eof
30 |
31 | endlocal
32 |
--------------------------------------------------------------------------------
/LynnaLib.Tests/LynnaLib.Tests.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | net9.0
5 | enable
6 | enable
7 | false
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 | Always
24 | true
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
--------------------------------------------------------------------------------
/LynnaLib.Tests/TestDictionaryLinkedList.cs:
--------------------------------------------------------------------------------
1 | using System.Text.Json;
2 | using Util;
3 |
4 | namespace LynnaLib.Tests;
5 |
6 | public class TestDictionaryLinkedList
7 | {
8 | [Fact]
9 | public void TestSerialize()
10 | {
11 | DictionaryLinkedList list = new();
12 | TrySerialize(list, "[]");
13 | list.AddLast(3);
14 | TrySerialize(list, "[3]");
15 | list.AddLast(4);
16 | TrySerialize(list, "[3,4]");
17 | list.AddAfter(3, 5);
18 | TrySerialize(list, "[3,5,4]");
19 | list.Remove(5);
20 | TrySerialize(list, "[3,4]");
21 | }
22 |
23 | [Fact]
24 | public void TestDeserialize()
25 | {
26 | TryDeserialize("[]", new int[] {});
27 | TryDeserialize("[2]", new int[] {2});
28 | TryDeserialize("[5,7]", new int[] {5,7});
29 | TryDeserialize("[10,8,5000]", new int[] {10,8,5000});
30 | }
31 |
32 | void TryDeserialize(string data, IEnumerable expected)
33 | {
34 | var dict = JsonSerializer.Deserialize>(data);
35 | Assert.True(dict?.SequenceEqual(expected),
36 | $"Deserialization error!\nExpected: {expected.ToArray()}\nGot: {dict?.ToArray()}");
37 | }
38 |
39 | void TrySerialize(DictionaryLinkedList data, string expected)
40 | {
41 | string s = JsonSerializer.Serialize>(data);
42 | Assert.True(s == expected,
43 | $"Serialization error!\nExpected: {expected}\nGot: {s}");
44 | }
45 | }
46 |
--------------------------------------------------------------------------------
/LynnaLib.Tests/TestProject.cs:
--------------------------------------------------------------------------------
1 | namespace LynnaLib.Tests;
2 |
3 | public class TestProject
4 | {
5 | [Fact]
6 | public static void TestProject1()
7 | {
8 | Project p = LoadProject(Game.Seasons);
9 |
10 | // Warp test: Adding/deleting position warps
11 | {
12 | WarpGroup group = p.GetRoom(0).GetWarpGroup();
13 | group.AddWarp(WarpSourceType.Position);
14 | Assert.Equal(4, group.Count);
15 | group.RemoveWarp(3);
16 | Assert.Equal(3, group.Count);
17 | }
18 | }
19 |
20 | [Fact]
21 | public static void TestUndo()
22 | {
23 | Project p = LoadProject(Game.Ages);
24 |
25 | // undo, redo, undo of 1-tile change
26 | {
27 | var layout = p.GetRoomLayout(10, Season.None);
28 | Assert.Equal(98, layout.GetTile(5, 4));
29 | layout.SetTile(5, 4, 40);
30 | Assert.Equal(40, layout.GetTile(5, 4));
31 | p.TransactionManager.Undo();
32 | Assert.Equal(98, layout.GetTile(5, 4));
33 | p.TransactionManager.Redo();
34 | Assert.Equal(40, layout.GetTile(5, 4));
35 | p.TransactionManager.Undo();
36 | Assert.Equal(98, layout.GetTile(5, 4));
37 | }
38 |
39 | // undo/redo of object creation
40 | {
41 | var group = p.GetRoom(0).GetObjectGroup();
42 | Assert.Equal(0, group.GetNumObjects());
43 | group.AddObject(ObjectType.Interaction);
44 | Assert.Equal(1, group.GetNumObjects());
45 | p.TransactionManager.Undo();
46 | Assert.Equal(0, group.GetNumObjects());
47 | p.TransactionManager.Redo();
48 | Assert.Equal(1, group.GetNumObjects());
49 | }
50 |
51 | // More object creation undo/redo testing (this used to cause stale data errors with InstanceResolvers)
52 | {
53 | var group = p.GetRoom(1).GetObjectGroup();
54 | Assert.Equal(0, group.GetNumObjects());
55 | group.AddObject(ObjectType.Interaction);
56 | Assert.Equal(1, group.GetNumObjects());
57 | group.GetObject(0).SetX(0x50);
58 | p.TransactionManager.Undo();
59 | p.TransactionManager.Undo();
60 | p.TransactionManager.Redo();
61 | p.TransactionManager.Redo();
62 | }
63 |
64 | // undo/redo of warp creation
65 | {
66 | var group = p.GetRoom(0).GetWarpGroup();
67 | Assert.Equal(0, group.Count);
68 | group.AddWarp(WarpSourceType.Standard);
69 | Assert.Equal(1, group.Count);
70 | p.TransactionManager.Undo();
71 | Assert.Equal(0, group.Count);
72 | p.TransactionManager.Redo();
73 | Assert.Equal(1, group.Count);
74 | }
75 | }
76 |
77 | // ================================================================================
78 | // Static methods
79 | // ================================================================================
80 |
81 | public static Project LoadProject(Game game)
82 | {
83 | string dir = "../../../../oracles-disasm";
84 | ProjectConfig config = ProjectConfig.Load(dir);
85 | return new Project(dir, game, config);
86 | }
87 | }
88 |
--------------------------------------------------------------------------------
/LynnaLib.Tests/log4net.config:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
31 |
32 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
--------------------------------------------------------------------------------
/LynnaLib/AbstractBoolValueReference.cs:
--------------------------------------------------------------------------------
1 | namespace LynnaLib;
2 |
3 | ///
4 | /// Similar to AbstractIntValueReference
5 | ///
6 | public class AbstractBoolValueReference : AbstractIntValueReference
7 | {
8 | private static readonly log4net.ILog log = log4net.LogManager.GetLogger(System.Reflection.MethodBase.GetCurrentMethod().DeclaringType);
9 |
10 |
11 | // ================================================================================
12 | // Constuctors
13 | // ================================================================================
14 |
15 | public AbstractBoolValueReference(Project project,
16 | Func getter, Action setter,
17 | ValueReferenceType type = ValueReferenceType.Bool,
18 | string constantsMappingString = null)
19 | : base(
20 | project,
21 | getter: () => getter() ? 1 : 0,
22 | setter: (v) => setter(v != 0 ? true : false),
23 | maxValue: 1,
24 | type: type,
25 | constantsMappingString: constantsMappingString)
26 | { }
27 |
28 |
29 | // ================================================================================
30 | // Public methods
31 | // ================================================================================
32 |
33 | // ================================================================================
34 | // Static methods
35 | // ================================================================================
36 |
37 | ///
38 | /// Helper function to create a DataValueReference wrapped around a ValueReferenceDescriptor in
39 | /// a single function call.
40 | ///
41 | public static ValueReferenceDescriptor Descriptor(
42 | Project project,
43 | string name,
44 | Func getter,
45 | Action setter,
46 | ValueReferenceType type = ValueReferenceType.Int,
47 | bool editable = true,
48 | string constantsMappingString = null,
49 | string tooltip = null)
50 | {
51 | var vr = new AbstractBoolValueReference(project, getter, setter, type,
52 | constantsMappingString);
53 | var descriptor = new ValueReferenceDescriptor(vr, name, editable, tooltip);
54 | return descriptor;
55 | }
56 | }
57 |
--------------------------------------------------------------------------------
/LynnaLib/AbstractIntValueReference.cs:
--------------------------------------------------------------------------------
1 | namespace LynnaLib;
2 |
3 | ///
4 | /// A ValueReference which isn't directly tied to any data; instead it takes getter and setter
5 | /// functions for data modifications.
6 | /// A caveat about using this: if this is used as a layer on top of actual Data values, then if
7 | /// those Data values are changed, the event handlers installed by "AddValueModifiedHandler"
8 | /// won't trigger. They will only trigger if modifications are made through this class.
9 | ///
10 | public class AbstractIntValueReference : ValueReference
11 | {
12 |
13 | private static readonly log4net.ILog log = log4net.LogManager.GetLogger(System.Reflection.MethodBase.GetCurrentMethod().DeclaringType);
14 |
15 |
16 | // ================================================================================
17 | // Constructors
18 | // ================================================================================
19 |
20 |
21 | public AbstractIntValueReference(Project project, Func getter, Action setter, int maxValue, int minValue = 0, ValueReferenceType type = ValueReferenceType.Int,
22 | string constantsMappingString = null)
23 | : base(project, type, constantsMappingString)
24 | {
25 | this.getter = getter;
26 | this.setter = setter;
27 |
28 | base.MaxValue = maxValue;
29 | base.MinValue = minValue;
30 | }
31 |
32 | public AbstractIntValueReference(ValueReference r, int maxValue, int minValue = 0, Func getter = null, Action setter = null)
33 | : base(r.Project, r.ValueType, r.ConstantsMappingString)
34 | {
35 | this.MaxValue = maxValue;
36 | this.MinValue = minValue;
37 | this.getter = getter;
38 | this.setter = setter;
39 |
40 | if (this.getter == null)
41 | this.getter = r.GetIntValue;
42 | if (this.setter == null)
43 | this.setter = r.SetValue;
44 | }
45 |
46 | // ================================================================================
47 | // Variables
48 | // ================================================================================
49 |
50 | Func getter;
51 | Action setter;
52 |
53 | // ================================================================================
54 | // Public methods
55 | // ================================================================================
56 |
57 | public override string GetStringValue()
58 | {
59 | return Wla.ToHex(GetIntValue(), 2);
60 | }
61 | public override int GetIntValue()
62 | {
63 | return getter();
64 | }
65 | public override void SetValue(string s)
66 | {
67 | base.BeginTransaction();
68 | SetValue(Project.Eval(s));
69 | base.EndTransaction();
70 | }
71 | public override void SetValue(int i)
72 | {
73 | if (i > MaxValue)
74 | {
75 | log.Warn(string.Format("Tried to set value to {0} (max value is {1})", i, MaxValue));
76 | i = MaxValue;
77 | }
78 | if (i < MinValue)
79 | {
80 | log.Warn(string.Format("Tried to set value to {0} (min value is {1})", i, MinValue));
81 | i = MinValue;
82 | }
83 | if (i == GetIntValue())
84 | return;
85 |
86 | base.BeginTransaction();
87 | setter(i);
88 | RaiseModifiedEvent(null);
89 | base.EndTransaction();
90 | }
91 |
92 | public override void Initialize()
93 | {
94 | throw new NotImplementedException();
95 | }
96 |
97 | // ================================================================================
98 | // Static methods
99 | // ================================================================================
100 |
101 | ///
102 | /// Helper function to create a DataValueReference wrapped around a ValueReferenceDescriptor in
103 | /// a single function call.
104 | ///
105 | public static ValueReferenceDescriptor Descriptor(
106 | Project project,
107 | string name,
108 | Func getter,
109 | Action setter,
110 | int maxValue,
111 | int minValue = 0,
112 | ValueReferenceType type = ValueReferenceType.Int,
113 | bool editable = true,
114 | string constantsMappingString = null,
115 | string tooltip = null)
116 | {
117 | var vr = new AbstractIntValueReference(project, getter, setter,
118 | maxValue, minValue, type, constantsMappingString);
119 | var descriptor = new ValueReferenceDescriptor(vr, name, editable, tooltip);
120 | return descriptor;
121 | }
122 | }
123 |
--------------------------------------------------------------------------------
/LynnaLib/Animation.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 |
4 | namespace LynnaLib
5 | {
6 | public class Animation : ProjectDataType, ProjectDataInstantiator
7 | {
8 | public int NumIndices
9 | {
10 | get { return gfxHeaderIndices.Count; }
11 | }
12 |
13 | // Parallel lists
14 | List gfxHeaderIndices = new List();
15 | List counters = new List();
16 |
17 | private Animation(Project p, string label) : base(p, label)
18 | {
19 | FileParser parser = Project.GetFileWithLabel(label);
20 | Data data = parser.GetData(label);
21 | while (data != null && data.CommandLowerCase == ".db")
22 | {
23 | counters.Add(Project.Eval(data.GetValue(0)));
24 | data = data.NextData;
25 | if (data.CommandLowerCase != ".db")
26 | throw new Exception("Malformatted animation data");
27 | gfxHeaderIndices.Add(Project.Eval(data.GetValue(0)));
28 | data = data.NextData;
29 | }
30 | }
31 |
32 | static ProjectDataType ProjectDataInstantiator.Instantiate(Project p, string id)
33 | {
34 | return new Animation(p, id);
35 | }
36 |
37 | public int GetCounter(int i)
38 | {
39 | return counters[i];
40 | }
41 | public GfxHeaderData GetGfxHeader(int i)
42 | {
43 | int index = gfxHeaderIndices[i];
44 | FileParser parser = Project.GetFileWithLabel("animationGfxHeaders");
45 | var header = parser.GetData("animationGfxHeaders") as GfxHeaderData;
46 | for (int j = 0; j < index; j++)
47 | header = header.NextData as GfxHeaderData;
48 | return header;
49 | }
50 | }
51 | }
52 |
--------------------------------------------------------------------------------
/LynnaLib/AnimationGroup.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.IO;
3 | using System.Collections.Generic;
4 |
5 | namespace LynnaLib
6 | {
7 | public class AnimationGroup : ProjectIndexedDataType, IndexedProjectDataInstantiator
8 | {
9 | public int NumAnimations
10 | {
11 | get { return _numAnimations; }
12 | }
13 |
14 | int _numAnimations;
15 | Animation[] animations = new Animation[4];
16 |
17 | private AnimationGroup(Project p, int i) : base(p, i)
18 | {
19 | FileParser parser = Project.GetFileWithLabel("animationGroupTable");
20 | Data pointer = parser.GetData("animationGroupTable", 2 * Index);
21 | string label = pointer.GetValue(0);
22 |
23 | Data data = parser.GetData(label);
24 | int b1 = Project.Eval(data.GetValue(0));
25 | data = data.NextData;
26 | int bits = b1 & 0xf;
27 |
28 | if (bits >= 0xf)
29 | _numAnimations = 4;
30 | else if (bits >= 0x7)
31 | _numAnimations = 3;
32 | else if (bits >= 0x3)
33 | _numAnimations = 2;
34 | else if (bits >= 0x1)
35 | _numAnimations = 1;
36 | else
37 | _numAnimations = 0;
38 |
39 | for (int j = 0; j < NumAnimations; j++)
40 | {
41 | if (data.CommandLowerCase != ".dw")
42 | throw new Exception("Malformatted animation group data (index 0x" +
43 | Index.ToString("x") + "\n");
44 | animations[j] = Project.GetDataType(data.GetValue(0), createIfMissing: true);
45 | data = data.NextData;
46 | }
47 | }
48 |
49 | static ProjectDataType IndexedProjectDataInstantiator.Instantiate(Project p, int index)
50 | {
51 | return new AnimationGroup(p, index);
52 | }
53 |
54 | public Animation GetAnimationIndex(int i)
55 | {
56 | return animations[i];
57 | }
58 | }
59 | }
60 |
--------------------------------------------------------------------------------
/LynnaLib/Documentation.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.Text.Json.Serialization;
4 |
5 | namespace LynnaLib
6 | {
7 |
8 | ///
9 | /// This provides an interface that the ValueReferenceEditor can use to create infoboxes.
10 | ///
11 | /// Can be constructed either with manual data, or with a DocumentationFileComponent.
12 | ///
13 | /// Though this roughly corresponds to DocumentationFileComponent, the user should generally filter
14 | /// out exactly what they want before passing this to DocumentationDialog.
15 | ///
16 | /// This contains a list of fields, which are interpreted as a list of possible values for whatever
17 | /// this is documenting (referred to by the "Title").
18 | ///
19 | public class Documentation
20 | {
21 | [JsonInclude]
22 | Dictionary _fieldDict;
23 | [JsonInclude]
24 | SortedSet _fieldKeys; // Maintained separately from documentationParams to preserve original case
25 |
26 | public string Name { get; set; }
27 | public string KeyName { get; set; } = "Key";
28 | public string Description { get; set; }
29 |
30 | [JsonIgnore]
31 | public ICollection Keys
32 | {
33 | get
34 | {
35 | return _fieldKeys;
36 | }
37 | }
38 |
39 |
40 | ///
41 | /// Build documentation with manual data
42 | ///
43 | public Documentation(string name, string desc, ICollection> _values)
44 | {
45 | Name = name;
46 | _fieldDict = new Dictionary();
47 | _fieldKeys = new SortedSet();
48 |
49 | if (_values != null)
50 | {
51 | foreach (Tuple tup in _values)
52 | {
53 | _fieldDict[tup.Item1.ToLower()] = tup.Item2;
54 | _fieldKeys.Add(tup.Item1);
55 | }
56 | }
57 |
58 | Description = desc;
59 | }
60 |
61 | public Documentation(DocumentationFileComponent fileComponent, string name)
62 | {
63 | Name = name;
64 | _fieldDict = new Dictionary();
65 |
66 | foreach (string key in fileComponent.Keys)
67 | {
68 | if (key == "desc")
69 | Description = fileComponent.GetField(key);
70 | else
71 | _fieldDict[key.ToLower()] = fileComponent.GetField(key);
72 | }
73 |
74 | _fieldKeys = new SortedSet(fileComponent.Keys);
75 | _fieldKeys.Remove("desc");
76 | }
77 |
78 | public Documentation(Documentation d)
79 | {
80 | _fieldDict = new Dictionary(d._fieldDict);
81 | _fieldKeys = new SortedSet(d._fieldKeys);
82 | Description = d.Description;
83 | Name = d.Name;
84 | KeyName = d.KeyName;
85 | }
86 |
87 | // Blank constructor just for deserialization
88 | public Documentation()
89 | {
90 |
91 | }
92 |
93 |
94 | public string GetField(string field)
95 | {
96 | field = field.ToLower();
97 | return _fieldDict.GetValueOrDefault(field);
98 | }
99 |
100 | public void SetField(string field, string value)
101 | {
102 | _fieldKeys.Add(field);
103 | _fieldDict[field.ToLower()] = value;
104 | }
105 |
106 | public void RemoveField(string field)
107 | {
108 | _fieldKeys.Remove(field);
109 | _fieldDict.Remove(field.ToLower());
110 | }
111 |
112 |
113 | public Documentation GetSubDocumentation(string field)
114 | {
115 | string value = GetField(field);
116 | if (value == null)
117 | return null;
118 |
119 | Documentation newDoc = new Documentation(this);
120 |
121 | List newKeys = new List();
122 | Dictionary newFields = DocumentationFileComponent.ParseDoc(value, newKeys);
123 |
124 | foreach (string key in newKeys)
125 | {
126 | newDoc._fieldKeys.Add(key);
127 | newDoc._fieldDict[key.ToLower()] = newFields[key];
128 | }
129 |
130 | newDoc.Name = Name + " (" + field + ")";
131 |
132 | return newDoc;
133 | }
134 | }
135 |
136 | }
137 |
--------------------------------------------------------------------------------
/LynnaLib/EnemyObject.cs:
--------------------------------------------------------------------------------
1 | using System;
2 |
3 | namespace LynnaLib
4 | {
5 |
6 | ///
7 | /// An enemy object. The "index" is the full ID (2 bytes, including subid).
8 | ///
9 | public class EnemyObject : GameObject, IndexedProjectDataInstantiator
10 | {
11 |
12 | readonly Data objectData;
13 |
14 | readonly byte _objectGfxHeaderIndex;
15 | readonly byte _collisionReactionSet;
16 | readonly byte _tileIndexBase;
17 | readonly byte _oamFlagsBase;
18 |
19 | private EnemyObject(Project p, int i) : base(p, i)
20 | {
21 | try
22 | {
23 | if ((i >> 8) >= 0x80)
24 | throw new ProjectErrorException($"Invalid enemy index {i:x2}");
25 |
26 | objectData = p.GetData("enemyData", ID * 4);
27 |
28 | _objectGfxHeaderIndex = (byte)objectData.GetIntValue(0);
29 | _collisionReactionSet = (byte)objectData.GetIntValue(1);
30 |
31 | byte lookupIndex; // TODO: use this
32 | byte b3;
33 |
34 | if (objectData.GetNumValues() == 4)
35 | {
36 | lookupIndex = (byte)objectData.GetIntValue(2);
37 | b3 = (byte)(objectData.GetIntValue(3));
38 | }
39 | else
40 | {
41 | Data subidData = Project.GetData(objectData.GetValue(2));
42 | int count = SubID;
43 |
44 | // If this points to more data, follow the pointer
45 | while (count > 0)
46 | {
47 | count--;
48 | var next = subidData.NextData;
49 | if (next.CommandLowerCase == "m_enemysubiddata")
50 | {
51 | subidData = next;
52 | continue;
53 | }
54 | else if (next.CommandLowerCase == "m_enemysubiddataend")
55 | {
56 | break;
57 | }
58 | else
59 | {
60 | throw new ProjectErrorException("Enemy Subid data ended unexpectedly");
61 | }
62 | }
63 | lookupIndex = (byte)subidData.GetIntValue(0);
64 | b3 = (byte)(subidData.GetIntValue(1));
65 | }
66 |
67 | _tileIndexBase = (byte)((b3 & 0xf) * 2);
68 | _oamFlagsBase = (byte)(b3 >> 4);
69 | }
70 | catch (InvalidLookupException e)
71 | {
72 | Console.WriteLine(e.ToString());
73 | objectData = null;
74 | }
75 | catch (FormatException e)
76 | {
77 | Console.WriteLine(e.ToString());
78 | objectData = null;
79 | }
80 | }
81 |
82 | static ProjectDataType IndexedProjectDataInstantiator.Instantiate(Project p, int index)
83 | {
84 | return new EnemyObject(p, index);
85 | }
86 |
87 |
88 | // GameObject properties
89 | public override string TypeName
90 | {
91 | get { return "Enemy"; }
92 | }
93 |
94 | public override ConstantsMapping IDConstantsMapping
95 | {
96 | get { return Project.EnemyMapping; }
97 | }
98 |
99 |
100 | public override bool DataValid
101 | {
102 | get { return objectData != null; }
103 | }
104 |
105 | public override byte ObjectGfxHeaderIndex
106 | {
107 | get { return _objectGfxHeaderIndex; }
108 | }
109 | public override byte TileIndexBase
110 | {
111 | get { return _tileIndexBase; }
112 | }
113 | public override byte OamFlagsBase
114 | {
115 | get { return _oamFlagsBase; }
116 | }
117 | public override byte DefaultAnimationIndex
118 | {
119 | get { return 0; }
120 | }
121 | }
122 | }
123 |
--------------------------------------------------------------------------------
/LynnaLib/Exceptions.cs:
--------------------------------------------------------------------------------
1 | using System;
2 |
3 | namespace LynnaLib
4 | {
5 |
6 | // An exception resulting from some kind of unexpected thing in the disassembly (possibly caused by
7 | // the user).
8 | public class ProjectErrorException : Exception
9 | {
10 | public ProjectErrorException() { }
11 | public ProjectErrorException(string s) : base(s) { }
12 | public ProjectErrorException(string message, Exception inner)
13 | : base(message, inner) { }
14 | }
15 |
16 | // Used with Project, FileParser, etc. when trying to look up labels and such
17 | public class InvalidLookupException : ProjectErrorException
18 | {
19 | public InvalidLookupException() { }
20 | public InvalidLookupException(string s) : base(s) { }
21 | }
22 |
23 | // Generic error resulting from unexpected things in the disassembly files (where there is
24 | // definitely something unexpected and wrong with the disassembly itself).
25 | public class AssemblyErrorException : ProjectErrorException
26 | {
27 | public AssemblyErrorException()
28 | : base() { }
29 | public AssemblyErrorException(string message)
30 | : base(message) { }
31 | public AssemblyErrorException(string message, Exception inner)
32 | : base(message, inner) { }
33 | }
34 |
35 | public class DuplicateLabelException : AssemblyErrorException
36 | {
37 | public DuplicateLabelException()
38 | : base() { }
39 | public DuplicateLabelException(string message)
40 | : base(message) { }
41 | public DuplicateLabelException(string message, Exception inner)
42 | : base(message, inner) { }
43 | }
44 |
45 | // Used by PngGfxStream when an image is formatted in an unexpected way
46 | public class InvalidImageException : ProjectErrorException
47 | {
48 | public InvalidImageException() : base() { }
49 | public InvalidImageException(string s) : base(s) { }
50 | public InvalidImageException(Exception e) : base(e.Message) { }
51 | }
52 |
53 | // Used by Treasure class when you try to instantiate one that doesn't exist
54 | public class InvalidTreasureException : ProjectErrorException
55 | {
56 | public InvalidTreasureException() : base() { }
57 | public InvalidTreasureException(string s) : base(s) { }
58 | public InvalidTreasureException(Exception e) : base(e.Message) { }
59 | }
60 |
61 | // Used by PaletteHeaderGroup class when you try to instantiate one that doesn't exist
62 | public class InvalidPaletteHeaderGroupException : ProjectErrorException
63 | {
64 | public InvalidPaletteHeaderGroupException() : base() { }
65 | public InvalidPaletteHeaderGroupException(string s) : base(s) { }
66 | public InvalidPaletteHeaderGroupException(Exception e) : base(e.Message) { }
67 | }
68 |
69 | // Used by ObjectAnimation.cs and ObjectAnimationFrame.cs.
70 | public class InvalidAnimationException : ProjectErrorException
71 | {
72 | public InvalidAnimationException() : base() { }
73 | public InvalidAnimationException(string s) : base(s) { }
74 | public InvalidAnimationException(Exception e) : base(e.Message) { }
75 | }
76 |
77 | // This is different from "InvalidAnimationException" because it's not really an error; the
78 | // animation simply hasn't been defined.
79 | public class NoAnimationException : InvalidAnimationException
80 | {
81 | public NoAnimationException() : base() { }
82 | public NoAnimationException(string s) : base(s) { }
83 | public NoAnimationException(Exception e) : base(e.Message) { }
84 | }
85 |
86 | ///
87 | /// Deserialization (mainly with System.Text.Json)
88 | ///
89 | public class DeserializationException : Exception
90 | {
91 | public DeserializationException() : base() { }
92 | public DeserializationException(string s) : base(s) { }
93 | public DeserializationException(Exception e) : base(e.Message) { }
94 | }
95 |
96 | ///
97 | /// Exceptions that occur during network transmission
98 | ///
99 | public class NetworkException : Exception
100 | {
101 | public NetworkException() : base() { }
102 | public NetworkException(string s) : base(s) { }
103 | public NetworkException(Exception e) : base(e.Message) { }
104 | }
105 | }
106 |
--------------------------------------------------------------------------------
/LynnaLib/GfxHeaderData.cs:
--------------------------------------------------------------------------------
1 | namespace LynnaLib
2 | {
3 | // Class represents macro:
4 | // m_GfxHeader filename destAddress [size] [startOffset]
5 | // 0 1 2 3
6 | // Other types of gfx headers not supported here.
7 | public class GfxHeaderData : Data, IGfxHeader
8 | {
9 | IStream gfxStream;
10 |
11 | public int DestAddr
12 | {
13 | get { return GetIntValue(1) & 0xfff0; }
14 | }
15 | public int DestBank
16 | {
17 | get { return GetIntValue(1) & 0x000f; }
18 | }
19 |
20 | // Properties from IGfxHeader
21 | public int? SourceAddr
22 | {
23 | get { return null; }
24 | }
25 | public int? SourceBank
26 | {
27 | get { return null; }
28 | }
29 |
30 | public IStream GfxStream { get { return gfxStream; } }
31 |
32 | // The number of blocks (16 bytes each) to be read.
33 | public int BlockCount
34 | {
35 | get {
36 | if (GetNumValues() >= 3)
37 | return Project.Eval(GetValue(2));
38 | else
39 | return (int)gfxStream.Length / 16;
40 | }
41 | }
42 |
43 | public GfxHeaderData(Project p, string id, string command, IEnumerable values, FileParser parser, IList spacing)
44 | : base(p, id, command, values, 6, parser, spacing)
45 | {
46 | string filename = GetValue(0);
47 |
48 | IStream stream = Project.GetGfxStream(filename);
49 |
50 | if (stream == null)
51 | {
52 | throw new Exception("Could not find graphics file " + filename + ".");
53 | }
54 |
55 | gfxStream = stream;
56 |
57 | // Adjust the gfx stream if we're supposed to omit part of it
58 | if (GetNumValues() >= 3)
59 | {
60 | int start = 0;
61 | if (GetNumValues() >= 4)
62 | start = GetIntValue(3);
63 | // TODO: Fix this - graphics should reference a subset of the full file
64 | //gfxStream = new SubStream(gfxStream, start, BlockCount * 16);
65 | }
66 | }
67 |
68 | ///
69 | /// State-based constructor, for network transfer (located via reflection)
70 | ///
71 | private GfxHeaderData(Project p, string id, TransactionState state)
72 | : base(p, id, state)
73 | {
74 | }
75 |
76 | public override void OnInitializedFromTransfer()
77 | {
78 | gfxStream = Project.GetGfxStream(GetValue(0));
79 | }
80 | }
81 |
82 | }
83 |
--------------------------------------------------------------------------------
/LynnaLib/Global.cs:
--------------------------------------------------------------------------------
1 | global using System;
2 | global using System.Collections.Generic;
3 | global using System.Linq;
4 | global using OneOf;
5 |
6 | global using Debug = System.Diagnostics.Debug;
7 | global using ImageSharp = SixLabors.ImageSharp;
8 |
9 | global using Util;
10 |
--------------------------------------------------------------------------------
/LynnaLib/IGfxHeader.cs:
--------------------------------------------------------------------------------
1 | using System.IO;
2 |
3 | namespace LynnaLib
4 | {
5 |
6 | ///
7 | /// Represents a "Gfx Header"; a reference to some gfx data as well as where that data should be
8 | /// loaded to.
9 | ///
10 | public interface IGfxHeader
11 | {
12 | // May be null, if GfxStream is in use (source is from ROM)
13 | int? SourceAddr { get; }
14 | int? SourceBank { get; }
15 |
16 | // May be null, if source is from RAM
17 | IStream GfxStream { get; }
18 |
19 | // The number of blocks (16 bytes each) to be read.
20 | int BlockCount { get; }
21 | }
22 | }
23 |
--------------------------------------------------------------------------------
/LynnaLib/InteractionObject.cs:
--------------------------------------------------------------------------------
1 | using System;
2 |
3 | namespace LynnaLib
4 | {
5 |
6 | ///
7 | /// An interaction object. The "index" is the full ID (2 bytes, including subid).
8 | ///
9 | public class InteractionObject : GameObject, IndexedProjectDataInstantiator
10 | {
11 |
12 | readonly Data objectData;
13 |
14 | readonly byte b0, b1, b2;
15 |
16 | private InteractionObject(Project p, int i) : base(p, i)
17 | {
18 | try
19 | {
20 | objectData = p.GetData("interactionData", ID * 3);
21 |
22 | // If this points to more data, follow the pointer
23 | if (objectData.GetNumValues() == 1)
24 | {
25 | string label = objectData.GetValue(0);
26 | objectData = p.GetData(label);
27 |
28 | int count = SubID;
29 | while (count > 0)
30 | {
31 | count--;
32 | var next = objectData.NextData;
33 | if (next.CommandLowerCase == "m_interactionsubiddata")
34 | {
35 | objectData = next;
36 | continue;
37 | }
38 | else if (next.CommandLowerCase == "m_interactionsubiddataend"
39 | || next.CommandLowerCase == "m_continuebithelperunsetlast")
40 | {
41 | break;
42 | }
43 | else
44 | {
45 | throw new ProjectErrorException("Interaction Subid data ended unexpectedly");
46 | }
47 | }
48 | }
49 |
50 | b0 = (byte)objectData.GetIntValue(0);
51 | b1 = (byte)objectData.GetIntValue(1);
52 | b2 = (byte)objectData.GetIntValue(2);
53 | }
54 | catch (InvalidLookupException e)
55 | {
56 | Console.WriteLine(e.ToString());
57 | objectData = null;
58 | }
59 | catch (FormatException e)
60 | {
61 | Console.WriteLine(e.ToString());
62 | objectData = null;
63 | }
64 | }
65 |
66 | static ProjectDataType IndexedProjectDataInstantiator.Instantiate(Project p, int index)
67 | {
68 | return new InteractionObject(p, index);
69 | }
70 |
71 |
72 | // GameObject properties
73 | public override string TypeName
74 | {
75 | get { return "Interaction"; }
76 | }
77 |
78 | public override ConstantsMapping IDConstantsMapping
79 | {
80 | get { return Project.InteractionMapping; }
81 | }
82 |
83 |
84 | public override bool DataValid
85 | {
86 | get { return objectData != null; }
87 | }
88 |
89 | public override byte ObjectGfxHeaderIndex
90 | {
91 | get { return b0; }
92 | }
93 | public override byte TileIndexBase
94 | {
95 | get { return (byte)(b1 & 0x7f); }
96 | }
97 | public override byte OamFlagsBase
98 | {
99 | get { return (byte)((b2 >> 4) & 0xf); }
100 | }
101 | public override byte DefaultAnimationIndex
102 | {
103 | get { return (byte)(b2 & 0xf); }
104 | }
105 | }
106 | }
107 |
--------------------------------------------------------------------------------
/LynnaLib/LynnaLib.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | net8.0
5 |
6 |
7 |
8 | true
9 |
10 |
11 |
12 | git describe --always --abbrev=7 > "$(MSBuildProjectDirectory)/version.txt"
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
--------------------------------------------------------------------------------
/LynnaLib/Map.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.IO;
3 | using System.Collections.Generic;
4 |
5 | namespace LynnaLib
6 | {
7 | // Represents a "Map", or layout of rooms. Subclass "WorldMap" represents a group of 256 rooms
8 | // laid out in a square, while the "Dungeon" subclass represents an 8x8 dungeon with a tweakable
9 | // layout, which also has multiple floors.
10 | public abstract class Map : TrackedProjectData
11 | {
12 | // This is called "MainGroup" instead of "Group" because the "Dungeon" subclass doesn't
13 | // really have a single canonical group. Most rooms in a dungeon belong to group 4 or 5,
14 | // except for sidescrolling rooms, which belong to group 6 or 7. The "main" group is
15 | // considered to be 4 or 5 in this case.
16 | public abstract int MainGroup { get; }
17 |
18 | public abstract int MapWidth { get; }
19 | public abstract int MapHeight { get; }
20 | public abstract int RoomWidth { get; }
21 | public abstract int RoomHeight { get; }
22 | public abstract Season Season { get; }
23 |
24 | protected Map(Project p, string id) : base(p, id)
25 | {
26 | }
27 |
28 | ///
29 | /// Gets the Room at the given position.
30 | ///
31 | public abstract Room GetRoom(int x, int y, int floor = 0);
32 |
33 | ///
34 | /// Get all locations of a room on the map. Returns an empty list if it's not on the map.
35 | ///
36 | public abstract IEnumerable<(int x, int y, int floor)> GetRoomPositions(Room room);
37 |
38 | ///
39 | /// Gets just one location of a room on a map. Returns false if it's not on the map.
40 | ///
41 | public abstract bool GetRoomPosition(Room room, out int x, out int y, out int floor);
42 |
43 | ///
44 | /// Gets just one location of a room on a map. Returns false if it's not on the map.
45 | ///
46 | public bool GetRoomPosition(Room room, out int x, out int y)
47 | {
48 | int f;
49 | return GetRoomPosition(room, out x, out y, out f);
50 | }
51 |
52 | ///
53 | /// Gets the RoomLayout at the given position.
54 | ///
55 | public RoomLayout GetRoomLayout(int x, int y, int floor = 0)
56 | {
57 | return GetRoom(x, y, floor).GetLayout(Season);
58 | }
59 | }
60 | }
61 |
--------------------------------------------------------------------------------
/LynnaLib/ObjectAnimation.cs:
--------------------------------------------------------------------------------
1 | using System.Collections.Generic;
2 | using System.Linq;
3 |
4 | namespace LynnaLib
5 | {
6 |
7 | ///
8 | /// Represents an animation for an object.
9 | ///
10 | /// If the animation doesn't loop, there's currently no way to know when it stops, which is
11 | /// problematic...
12 | ///
13 | /// This will throw an "InvalidAnimationException" whenever something unexpected happens (which
14 | /// seems common, particularly when animations are undefined for an object).
15 | ///
16 | public class ObjectAnimation
17 | {
18 | readonly GameObject _gameObject;
19 | readonly int _animationIndex;
20 | readonly Data _animationData;
21 |
22 | List loadedFrames = new List();
23 |
24 | public Project Project
25 | {
26 | get { return _gameObject.Project; }
27 | }
28 |
29 | public string AnimationTableName
30 | {
31 | get
32 | {
33 | string s = _gameObject.TypeName.ToLower() + "AnimationTable";
34 | return Project.GetData(s, _gameObject.ID * 2).GetValue(0);
35 | }
36 | }
37 | public string OamTableName
38 | {
39 | get
40 | {
41 | string s = _gameObject.TypeName.ToLower() + "OamDataTable";
42 | return Project.GetData(s, _gameObject.ID * 2).GetValue(0);
43 | }
44 | }
45 |
46 | public ObjectGfxHeaderData ObjectGfxHeaderData
47 | {
48 | get { return _gameObject.ObjectGfxHeaderData; }
49 | }
50 | public byte TileIndexBase
51 | {
52 | get { return _gameObject.TileIndexBase; }
53 | }
54 | public byte OamFlagsBase
55 | {
56 | get { return _gameObject.OamFlagsBase; }
57 | }
58 |
59 | public ObjectAnimation(GameObject gameObject, int animationIndex)
60 | {
61 | _gameObject = gameObject;
62 | _animationIndex = animationIndex;
63 |
64 | if (!_gameObject.DataValid)
65 | throw new InvalidAnimationException();
66 | if (_gameObject.ObjectGfxHeaderIndex == 0)
67 | throw new NoAnimationException();
68 |
69 | try
70 | {
71 | _animationData = Project.GetData(Project.GetData(AnimationTableName, animationIndex * 2).GetValue(0));
72 | }
73 | catch (InvalidLookupException e)
74 | {
75 | throw new InvalidAnimationException(e);
76 | }
77 | }
78 |
79 |
80 | public ObjectAnimationFrame GetFrame(int i)
81 | {
82 | if (i < loadedFrames.Count)
83 | return loadedFrames[i];
84 |
85 | if (loadedFrames.Count == 0)
86 | loadedFrames.Add(new ObjectAnimationFrame(this, _animationData));
87 |
88 | try
89 | {
90 | Data data = loadedFrames.Last().AnimDataStart;
91 |
92 | while (true)
93 | {
94 | if (i < loadedFrames.Count)
95 | return loadedFrames[i];
96 |
97 | data = data.NextData;
98 | data = data.NextData;
99 | data = data.NextData;
100 |
101 | var frame = new ObjectAnimationFrame(this, data);
102 | loadedFrames.Add(frame);
103 | }
104 | }
105 | catch (InvalidLookupException e)
106 | {
107 | throw new InvalidAnimationException(e);
108 | }
109 | }
110 |
111 | ///
112 | /// Returns the array of palettes (8 palettes of 4 colors each) in use.
113 | ///
114 | /// If a particular palette is undefined, it will be null (ie. palette[i] == null)
115 | ///
116 | public Color[][] GetPalettes()
117 | {
118 | Color[][] palettes = new Color[8][];
119 | Color[][] standardPal = Project.GetStandardSpritePalettes();
120 | Color[][] customPal = _gameObject.GetCustomPalettes();
121 |
122 | for (int i = 0; i < 6; i++)
123 | {
124 | palettes[i] = standardPal[i];
125 | }
126 |
127 | if (customPal == null)
128 | return palettes;
129 |
130 | for (int i = 0; i < 8; i++)
131 | {
132 | if (customPal[i] != null)
133 | palettes[i] = customPal[i];
134 | }
135 | return palettes;
136 | }
137 | }
138 |
139 | }
140 |
--------------------------------------------------------------------------------
/LynnaLib/ObjectGfxHeaderData.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.IO;
4 |
5 | namespace LynnaLib
6 | {
7 | // Class represents macro:
8 | // m_ObjectGfxHeader filename [continue]
9 | // 0 1
10 | // Other types of gfx headers not supported here.
11 | public class ObjectGfxHeaderData : Data, IGfxHeader
12 | {
13 | IStream gfxStream;
14 |
15 | public int? SourceAddr
16 | {
17 | get { return null; }
18 | }
19 | public int? SourceBank
20 | {
21 | get { return null; }
22 | }
23 |
24 | public IStream GfxStream { get { return gfxStream; } }
25 |
26 | // The number of blocks (16 bytes each) to be read.
27 | public int BlockCount
28 | {
29 | get { return 0x20; }
30 | }
31 |
32 | // True if the bit indicating that there is a next value is set.
33 | public bool ShouldHaveNext
34 | {
35 | get { return GetNumValues() >= 2 && (GetIntValue(1)) != 0; }
36 | }
37 |
38 | // Should only request this if the "ShouldHaveNext" property is true.
39 | public ObjectGfxHeaderData NextGfxHeader
40 | {
41 | get
42 | {
43 | return NextData as ObjectGfxHeaderData;
44 | }
45 | }
46 |
47 |
48 | public ObjectGfxHeaderData(Project p, string id, string command, IEnumerable values, FileParser parser, IList spacing)
49 | : base(p, id, command, values, 3, parser, spacing)
50 | {
51 | string filename = GetValue(0);
52 |
53 | IStream stream = Project.GetGfxStream(filename);
54 | if (stream == null)
55 | {
56 | throw new Exception("Could not find graphics file " + filename);
57 | }
58 | gfxStream = stream;
59 | }
60 |
61 | ///
62 | /// State-based constructor, for network transfer (located via reflection)
63 | ///
64 | private ObjectGfxHeaderData(Project p, string id, TransactionState state)
65 | : base(p, id, state)
66 | {
67 | }
68 |
69 | public override void OnInitializedFromTransfer()
70 | {
71 | gfxStream = Project.GetGfxStream(GetValue(0));
72 | }
73 | }
74 |
75 | }
76 |
--------------------------------------------------------------------------------
/LynnaLib/PaletteHeaderGroup.cs:
--------------------------------------------------------------------------------
1 | using System;
2 |
3 | namespace LynnaLib
4 | {
5 | /// This basically consists of a list of "PaletteHeaderData"s. Keep in mind that some
6 | /// PaletteHeaderGroups are identical (reference the same data).
7 | public class PaletteHeaderGroup : ProjectIndexedDataType, IndexedProjectDataInstantiator
8 | {
9 | readonly PaletteHeaderData firstPaletteHeader;
10 |
11 | public event EventHandler ModifiedEvent;
12 |
13 |
14 | public PaletteHeaderData FirstPaletteHeader
15 | {
16 | get { return firstPaletteHeader; }
17 | }
18 |
19 | /// Name of the label pointing to the data
20 | public string LabelName { get; private set; }
21 |
22 | /// Name of the constant alias (ie. PALH_40)
23 | public string ConstantAliasName
24 | {
25 | get
26 | {
27 | return Project.PaletteHeaderMapping.ByteToString(Index);
28 | }
29 | }
30 |
31 | private PaletteHeaderGroup(Project project, int index) : base(project, index)
32 | {
33 | try
34 | {
35 | LabelName = "paletteHeader" + index.ToString("x2");
36 | Data headerData = Project.GetData(LabelName);
37 |
38 | if (!(headerData is PaletteHeaderData))
39 | throw new InvalidPaletteHeaderGroupException("Expected palette header group " + index.ToString("X") + " to start with palette header data");
40 | firstPaletteHeader = (PaletteHeaderData)headerData;
41 | }
42 | catch (InvalidLookupException e)
43 | {
44 | throw new InvalidPaletteHeaderGroupException(e.Message);
45 | }
46 | InstallEventHandlers();
47 | }
48 |
49 | static ProjectDataType IndexedProjectDataInstantiator.Instantiate(Project p, int index)
50 | {
51 | return new PaletteHeaderGroup(p, index);
52 | }
53 |
54 | // TODO: error handling
55 | public Color[][] GetObjPalettes()
56 | {
57 | Color[][] ret = new Color[8][];
58 |
59 | Foreach((palette) =>
60 | {
61 | Color[][] palettes = palette.GetPalettes();
62 | if (palette.PaletteType == PaletteType.Sprite)
63 | {
64 | for (int i = 0; i < palette.NumPalettes; i++)
65 | {
66 | ret[i + palette.FirstPalette] = palettes[i];
67 | }
68 | }
69 | });
70 | return ret;
71 | }
72 |
73 | public void Foreach(Action action)
74 | {
75 | PaletteHeaderData palette = firstPaletteHeader;
76 | while (true)
77 | {
78 | action(palette);
79 | Data nextData = palette.NextData;
80 | if (nextData is PaletteHeaderData)
81 | {
82 | palette = palette.NextData as PaletteHeaderData;
83 | continue;
84 | }
85 | else if (nextData.CommandLowerCase == "m_paletteheaderend")
86 | {
87 | break;
88 | }
89 | else
90 | {
91 | throw new ProjectErrorException("Expected palette data to end with m_PaletteHeaderEnd");
92 | }
93 | }
94 | }
95 |
96 |
97 | void InstallEventHandlers()
98 | {
99 | Foreach((palette) =>
100 | {
101 | palette.PaletteDataModifiedEvent += (sender, args) =>
102 | {
103 | ModifiedEvent?.Invoke(this, null);
104 | };
105 | });
106 | }
107 | }
108 | }
109 |
--------------------------------------------------------------------------------
/LynnaLib/PartObject.cs:
--------------------------------------------------------------------------------
1 | using System;
2 |
3 | namespace LynnaLib
4 | {
5 |
6 | ///
7 | /// An interaction object. The "index" is the full ID (2 bytes, including subid).
8 | ///
9 | public class PartObject : GameObject, IndexedProjectDataInstantiator
10 | {
11 |
12 | readonly Data objectData;
13 |
14 | readonly byte _objectGfxHeaderIndex;
15 | readonly byte _tileIndexBase;
16 | readonly byte _oamFlagsBase;
17 |
18 | private PartObject(Project p, int i) : base(p, i)
19 | {
20 | try
21 | {
22 | objectData = p.GetData("partData", ID * 8);
23 |
24 | Data data = objectData;
25 |
26 | _objectGfxHeaderIndex = (byte)data.GetIntValue(0);
27 | for (int j = 0; j < 5; j++)
28 | data = data.NextData;
29 | _tileIndexBase = (byte)data.GetIntValue(0);
30 | data = data.NextData;
31 | _oamFlagsBase = (byte)data.GetIntValue(0);
32 | }
33 | catch (InvalidLookupException e)
34 | {
35 | Console.WriteLine(e.ToString());
36 | objectData = null;
37 | }
38 | catch (FormatException e)
39 | {
40 | Console.WriteLine(e.ToString());
41 | objectData = null;
42 | }
43 | }
44 |
45 | static ProjectDataType IndexedProjectDataInstantiator.Instantiate(Project p, int index)
46 | {
47 | return new PartObject(p, index);
48 | }
49 |
50 |
51 | // GameObject properties
52 | public override string TypeName
53 | {
54 | get { return "Part"; }
55 | }
56 |
57 | public override ConstantsMapping IDConstantsMapping
58 | {
59 | get { return Project.PartMapping; }
60 | }
61 |
62 |
63 | public override bool DataValid
64 | {
65 | get { return objectData != null; }
66 | }
67 |
68 | public override byte ObjectGfxHeaderIndex
69 | {
70 | get { return _objectGfxHeaderIndex; }
71 | }
72 | public override byte TileIndexBase
73 | {
74 | get { return _tileIndexBase; }
75 | }
76 | public override byte OamFlagsBase
77 | {
78 | get { return _oamFlagsBase; }
79 | }
80 | public override byte DefaultAnimationIndex
81 | {
82 | get { return 0; }
83 | }
84 | }
85 | }
86 |
--------------------------------------------------------------------------------
/LynnaLib/ProjectConfig.cs:
--------------------------------------------------------------------------------
1 | using System.IO;
2 | using System.Text.Json.Serialization;
3 | using YamlDotNet.Serialization;
4 |
5 | namespace LynnaLib
6 | {
7 | ///
8 | /// This is the contents of the "config.yaml" file deserialized with YamlDotNet.
9 | ///
10 | /// This is also serialized with System.Text.Json during network transmission. Sure, why not.
11 | ///
12 | public class ProjectConfig
13 | {
14 | private static readonly log4net.ILog log = LogHelper.GetLogger();
15 |
16 | public static ProjectConfig Load(string basepath)
17 | {
18 | string configDirectory = basepath + "/LynnaLab/";
19 | string filename = configDirectory + "/config.yaml";
20 |
21 | ProjectConfig config;
22 |
23 | try
24 | {
25 | var input = new StringReader(File.ReadAllText(filename));
26 | var deserializer = new DeserializerBuilder().IgnoreUnmatchedProperties().Build();
27 |
28 | config = deserializer.Deserialize(input);
29 | config.filename = filename;
30 | return config;
31 | }
32 | catch (Exception ex) when (ex is FileNotFoundException || ex is DirectoryNotFoundException)
33 | {
34 | log.Warn("Couldn't open config file '" + filename + "'.");
35 | return null;
36 | }
37 | }
38 |
39 | // Variables imported from YAML config file
40 | [JsonInclude, JsonRequired]
41 | public string EditingGame { get; private set; }
42 | [JsonInclude, JsonRequired]
43 | public bool ExpandedTilesets { get; private set; }
44 |
45 | // Filename of config file. Don't de/serialize this for security reasons.
46 | [JsonIgnore]
47 | string filename;
48 |
49 |
50 | public void SetEditingGame(string value)
51 | {
52 | SetVariable("EditingGame", value);
53 | EditingGame = value;
54 | }
55 |
56 | public bool EditingGameIsValid()
57 | {
58 | return EditingGame != null && (EditingGame == "ages" || EditingGame == "seasons");
59 | }
60 |
61 | /// Set a variable to a value and save it immediately. Not using a proper YAML parser for
62 | /// this because I want to preserve comments. This code is not good but it will work for
63 | /// this specific use case.
64 | void SetVariable(string variable, string value)
65 | {
66 | string[] lines = File.ReadAllLines(filename);
67 |
68 | for (int i=0; i
16 | /// Unique identifier (not including the type name)
17 | ///
18 | public string Identifier
19 | {
20 | get { return identifier; }
21 | }
22 |
23 | ///
24 | /// Full identifier including type
25 | ///
26 | public string FullIdentifier
27 | {
28 | get { return Project.GetFullIdentifier(GetType(), Identifier); }
29 | }
30 |
31 | protected ProjectDataType(Project p, string identifier)
32 | {
33 | project = p;
34 | this.identifier = identifier;
35 |
36 | Project.AddDataType(GetType(), this);
37 | }
38 | }
39 |
40 | public abstract class ProjectIndexedDataType : ProjectDataType
41 | {
42 | readonly int _index;
43 |
44 | public int Index
45 | {
46 | get { return _index; }
47 | }
48 |
49 | internal ProjectIndexedDataType(Project p, int index)
50 | : base(p, index.ToString())
51 | {
52 | _index = index;
53 | }
54 | }
55 | }
56 |
--------------------------------------------------------------------------------
/LynnaLib/ReloadableStream.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.IO;
3 | using System.Runtime.InteropServices;
4 | using Util;
5 |
6 | /// Base class for Stream objects which can watch for changes to their respective files.
7 | public abstract class ReloadableStream : Stream
8 | {
9 | private static readonly log4net.ILog log = LogHelper.GetLogger();
10 |
11 | FileSystemWatcher watcher;
12 |
13 | public ReloadableStream(string filename)
14 | {
15 | // FileSystemWatcher doesn't work well on Linux. Creating hundreds or thousands of these
16 | // uses up system resources in a way that causes the OS to complain.
17 | // Automatic file reloading is disabled on Linux until a good workaround is found.
18 | if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux))
19 | return;
20 |
21 | watcher = new FileSystemWatcher();
22 | watcher.Path = Path.GetDirectoryName(filename);
23 | watcher.Filter = Path.GetFileName(filename);
24 | watcher.NotifyFilter = NotifyFilters.LastWrite;
25 |
26 | watcher.Changed += (o, a) =>
27 | {
28 | log.Info($"File {filename} changed, triggering reload event");
29 |
30 | // Use MainThreadInvoke to avoid any threading headaches
31 | Helper.MainThreadInvoke(() =>
32 | {
33 | Reload();
34 | if (ExternallyModifiedEvent != null)
35 | ExternallyModifiedEvent(this, null);
36 | });
37 | };
38 |
39 | watcher.EnableRaisingEvents = true;
40 | }
41 |
42 | // Event which triggers when the stream is modified externally
43 | public event EventHandler ExternallyModifiedEvent;
44 |
45 | // Function which handles reloading the data
46 | protected abstract void Reload();
47 |
48 |
49 | public override void Close()
50 | {
51 | watcher?.Dispose();
52 | base.Close();
53 | }
54 | }
55 |
--------------------------------------------------------------------------------
/LynnaLib/TilesetHeaderGroup.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.IO;
4 |
5 | namespace LynnaLib
6 | {
7 |
8 | // This class is only used when "ExpandedTilesets" is false in config.yaml.
9 | public class TilesetHeaderGroup : ProjectIndexedDataType, IndexedProjectDataInstantiator
10 | {
11 | readonly TrackedStream mappingsDataFile, collisionsDataFile;
12 |
13 | private TilesetHeaderGroup(Project p, int i) : base(p, i)
14 | {
15 | FileParser tableFile = Project.GetFileWithLabel("tilesetLayoutTable");
16 | Data pointerData = tableFile.GetData("tilesetLayoutTable", Index * 2);
17 | string labelName = pointerData.GetValue(0);
18 |
19 | FileParser headerFile = Project.GetFileWithLabel(labelName);
20 | TilesetLayoutHeaderData headerData = headerFile.GetData(labelName) as TilesetLayoutHeaderData;
21 | bool next = true;
22 |
23 | while (next)
24 | {
25 | if (headerData == null)
26 | throw new Exception("Expected tileset header group " + Index.ToString("X") + " to reference tileset header data (m_TilesetHeader)");
27 |
28 | TrackedStream dataFile = headerData.ReferencedData;
29 | dataFile.Position = 0;
30 | if (headerData.DestAddress == Project.Eval("w3TileMappingIndices"))
31 | {
32 | // Mappings
33 | mappingsDataFile = dataFile;
34 | }
35 | else if (headerData.DestAddress == Project.Eval("w3TileCollisions"))
36 | {
37 | // Collisions
38 | collisionsDataFile = dataFile;
39 | }
40 |
41 | if (headerData.ShouldHaveNext())
42 | {
43 | headerData = headerData.NextData as TilesetLayoutHeaderData;
44 | if (headerData != null)
45 | next = true;
46 | }
47 | else
48 | next = false;
49 | }
50 | }
51 |
52 | static ProjectDataType IndexedProjectDataInstantiator.Instantiate(Project p, int index)
53 | {
54 | return new TilesetHeaderGroup(p, index);
55 | }
56 |
57 | public byte GetMappingsData(int i)
58 | {
59 | if (mappingsDataFile == null)
60 | throw new Exception("Tileset header group 0x" + Index.ToString("X") + " does not reference mapping data.");
61 | mappingsDataFile.Seek(i, SeekOrigin.Begin);
62 | return (byte)mappingsDataFile.ReadByte();
63 | }
64 | public byte GetCollisionsData(int i)
65 | {
66 | if (collisionsDataFile == null)
67 | throw new Exception("Tileset header group 0x" + Index.ToString("X") + " does not reference collision data.");
68 | collisionsDataFile.Seek(i, SeekOrigin.Begin);
69 | return (byte)collisionsDataFile.ReadByte();
70 | }
71 |
72 | public void SetMappingsData(int i, byte value)
73 | {
74 | if (mappingsDataFile == null)
75 | throw new Exception("Tileset header group 0x" + Index.ToString("X") + " does not reference mapping data.");
76 | mappingsDataFile.Seek(i, SeekOrigin.Begin);
77 | mappingsDataFile.WriteByte(value);
78 | }
79 | public void SetCollisionsData(int i, byte value)
80 | {
81 | if (collisionsDataFile == null)
82 | throw new Exception("Tileset header group 0x" + Index.ToString("X") + " does not reference collision data.");
83 | collisionsDataFile.Seek(i, SeekOrigin.Begin);
84 | collisionsDataFile.WriteByte(value);
85 | }
86 |
87 | // No need for a save function, dataFiles are tracked elsewhere
88 | }
89 | }
90 |
--------------------------------------------------------------------------------
/LynnaLib/TilesetLayoutHeaderData.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.IO;
4 |
5 | namespace LynnaLib
6 | {
7 | // Data macro "m_TilesetLayoutHeader"
8 | public class TilesetLayoutHeaderData : Data
9 | {
10 |
11 | readonly TrackedStream referencedData;
12 |
13 | public int DictionaryIndex
14 | {
15 | get { return Project.Eval(GetValue(0)); }
16 | }
17 | public TrackedStream ReferencedData
18 | {
19 | get
20 | {
21 | return referencedData;
22 | }
23 | }
24 | public int DestAddress
25 | {
26 | get { return Project.Eval(GetValue(2)); }
27 | }
28 | public int DestBank
29 | {
30 | get { return Project.Eval(":" + GetValue(2)); }
31 | }
32 | public int DataSize
33 | {
34 | get { return Project.Eval(GetValue(3)); }
35 | }
36 |
37 |
38 | public TilesetLayoutHeaderData(Project p, string id, string command, IEnumerable values, FileParser parser, IList spacing)
39 | : base(p, id, command, values, 8, parser, spacing)
40 | {
41 | try
42 | {
43 | referencedData = Project.GetFileStream("tileset_layouts/" + Project.GameString + "/" + GetValue(1) + ".bin");
44 | }
45 | catch (FileNotFoundException)
46 | {
47 | // Default is to copy from 00 I guess
48 | // TODO: copy this into its own file?
49 | LogHelper.GetLogger().Warn("Missing tileset layout file: " + GetValue(1));
50 | string filename = GetValue(1).Substring(0, GetValue(1).Length - 2);
51 | referencedData = Project.GetFileStream("tileset_layouts/" + Project.GameString + "/" + filename + "00.bin");
52 | }
53 | }
54 |
55 | public bool ShouldHaveNext()
56 | {
57 | return (Project.Eval(GetValue(4)) & 0x80) == 0x80;
58 | }
59 | }
60 |
61 | }
62 |
--------------------------------------------------------------------------------
/LynnaLib/Util/Accessor.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Linq.Expressions;
3 |
4 | namespace Util;
5 |
6 | ///
7 | /// Helper class allowing one to effectively pass a property as a reference. See ImGuiX.Checkbox
8 | /// function for usage example.
9 | ///
10 | /// From: https://stackoverflow.com/questions/1402803/passing-properties-by-reference-in-c-sharp
11 | ///
12 | /// I'm unsure about the performance of this. It involves compiling some IL code (but not much).
13 | ///
14 | public class Accessor
15 | {
16 | public Accessor(Expression> expression)
17 | {
18 | if (expression.Body is not MemberExpression memberExpression)
19 | throw new ArgumentException("expression must return a field or property");
20 | var parameterExpression = Expression.Parameter(typeof(T));
21 |
22 | _setter = Expression.Lambda>(
23 | Expression.Assign(memberExpression, parameterExpression), parameterExpression).Compile();
24 | _getter = expression.Compile();
25 | }
26 |
27 | public void Set(T value) => _setter(value);
28 | public T Get() => _getter();
29 |
30 | private readonly Action _setter;
31 | private readonly Func _getter;
32 | }
33 |
--------------------------------------------------------------------------------
/LynnaLib/Util/Cacher.cs:
--------------------------------------------------------------------------------
1 | namespace Util;
2 |
3 | public interface IDisposeNotifier : IDisposable
4 | {
5 | public event EventHandler DisposedEvent;
6 | }
7 |
8 | ///
9 | /// Simple class that lets you look up something based on a key and returns the cached value, or
10 | /// creates the value for you if it doesn't exist already.
11 | ///
12 | /// This listens to the DisposedEvent on the ValueClass and automatically removes any values that
13 | /// are disposed.
14 | ///
15 | public class Cacher : IDisposable where ValueClass : IDisposeNotifier
16 | {
17 | // ================================================================================
18 | // Constructors
19 | // ================================================================================
20 |
21 | public Cacher(Func generator)
22 | {
23 | this.generator = generator;
24 | }
25 |
26 | // ================================================================================
27 | // Variables
28 | // ================================================================================
29 |
30 | Dictionary cache = new();
31 | Dictionary cacheByValue = new();
32 | Func generator;
33 |
34 | // ================================================================================
35 | // Properties
36 | // ================================================================================
37 |
38 | public IEnumerable Values { get { return cache.Values; } }
39 |
40 | // ================================================================================
41 | // Public methods
42 | // ================================================================================
43 |
44 | public bool HasKey(KeyClass key)
45 | {
46 | return cache.ContainsKey(key);
47 | }
48 |
49 | public ValueClass GetOrCreate(KeyClass key)
50 | {
51 | ValueClass tx;
52 | if (cache.TryGetValue(key, out tx))
53 | return tx;
54 |
55 | tx = generator(key);
56 | tx.DisposedEvent += OnChildDisposed;
57 | cache[key] = tx;
58 | cacheByValue[tx] = key;
59 | return tx;
60 | }
61 |
62 | public bool TryGetValue(KeyClass key, out ValueClass value)
63 | {
64 | return cache.TryGetValue(key, out value);
65 | }
66 |
67 | public void DisposeKey(KeyClass key)
68 | {
69 | var tx = cache[key];
70 | tx.Dispose(); // Should invoke OnChildDisposed
71 | Debug.Assert(!cache.ContainsKey(key));
72 | }
73 |
74 | public virtual void Dispose()
75 | {
76 | foreach (KeyClass key in cache.Keys)
77 | {
78 | DisposeKey(key);
79 | }
80 | cache = null;
81 | cacheByValue = null;
82 | }
83 |
84 | // ================================================================================
85 | // Private methods
86 | // ================================================================================
87 |
88 | void OnChildDisposed(object sender, object args)
89 | {
90 | ValueClass value = (ValueClass)sender;
91 | KeyClass key = cacheByValue[value];
92 |
93 | cache.Remove(key);
94 | cacheByValue.Remove(value);
95 |
96 | value.DisposedEvent -= OnChildDisposed;
97 | }
98 | }
99 |
--------------------------------------------------------------------------------
/LynnaLib/Util/CairoHelper1.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 |
4 | namespace LynnaLib
5 | {
6 | class InvalidBitmapFormatException : Exception
7 | {
8 | public InvalidBitmapFormatException(String s) : base(s) { }
9 | }
10 | }
11 |
--------------------------------------------------------------------------------
/LynnaLib/Util/CircularStack.cs:
--------------------------------------------------------------------------------
1 | using System.Collections;
2 |
3 | namespace Util;
4 |
5 | ///
6 | /// Behaves like a stack with a set maximum capacity. If the maximum is exceeded, the oldest
7 | /// elements are dropped
8 | ///
9 | public class CircularStack : IEnumerable
10 | {
11 | // ================================================================================
12 | // Constructors
13 | // ================================================================================
14 | public CircularStack(int capacity)
15 | {
16 | this.Capacity = capacity;
17 | elements = new T[capacity];
18 | }
19 |
20 | // ================================================================================
21 | // Variables
22 | // ================================================================================
23 |
24 | T[] elements;
25 | int nextPos = 0;
26 | int version = 0;
27 |
28 | // ================================================================================
29 | // Properties
30 | // ================================================================================
31 |
32 | public int Count { get; private set; }
33 | public int Capacity { get; private set; }
34 |
35 | // ================================================================================
36 | // Public methods
37 | // ================================================================================
38 |
39 | public void Push(T e)
40 | {
41 | elements[nextPos] = e;
42 | nextPos = NextIndex();
43 | Count++;
44 | if (Count > Capacity)
45 | Count = Capacity;
46 | version++;
47 | }
48 |
49 | public T Pop()
50 | {
51 | if (Count == 0)
52 | throw new InvalidOperationException("Tried to pop from an empty stack");
53 |
54 | nextPos = PrevIndex();
55 | T retval = elements[nextPos];
56 | elements[nextPos] = default;
57 | Count--;
58 | version++;
59 | return retval;
60 | }
61 |
62 | public T Peek()
63 | {
64 | if (Count == 0)
65 | throw new InvalidOperationException("Tried to peek from an empty stack");
66 |
67 | return elements[PrevIndex()];
68 | }
69 |
70 | public void Clear()
71 | {
72 | elements = new T[Capacity];
73 | nextPos = 0;
74 | Count = 0;
75 | version++;
76 | }
77 |
78 | IEnumerator IEnumerable.GetEnumerator()
79 | {
80 | int pos = (this.nextPos - Count + Capacity) % Capacity;
81 | int v = version;
82 | for (int i=0; i).GetEnumerator();
95 | }
96 |
97 | // ================================================================================
98 | // Private methods
99 | // ================================================================================
100 |
101 | int NextIndex()
102 | {
103 | return (nextPos + 1) % Capacity;
104 | }
105 |
106 | int PrevIndex()
107 | {
108 | if (nextPos == 0)
109 | return Capacity - 1;
110 | return nextPos - 1;
111 | }
112 | }
113 |
--------------------------------------------------------------------------------
/LynnaLib/Util/Color.cs:
--------------------------------------------------------------------------------
1 | #nullable enable
2 |
3 | using System.Numerics;
4 | using System.Text.Json.Serialization;
5 | using PixelFormats = SixLabors.ImageSharp.PixelFormats;
6 |
7 | namespace LynnaLib
8 | {
9 | ///
10 | /// Represents a color. Immutable. Supports implicit conversion to and from
11 | /// ImageSharp.PixelFormats.Rgba32 (used by the Bitmap class) and Vector4 (used by ImGui).
12 | ///
13 | public struct Color
14 | {
15 | [JsonInclude]
16 | int r, g, b, a;
17 |
18 | public int R
19 | {
20 | get { return r; }
21 | }
22 | public int G
23 | {
24 | get { return g; }
25 | }
26 | public int B
27 | {
28 | get { return b; }
29 | }
30 | public int A
31 | {
32 | get { return a; }
33 | }
34 |
35 | public static Color FromRgb(int r, int g, int b)
36 | {
37 | return FromRgba(r, g, b, 255);
38 | }
39 |
40 | public static Color FromRgba(int r, int g, int b, int a)
41 | {
42 | Color c = new Color();
43 | c.r = r;
44 | c.g = g;
45 | c.b = b;
46 | c.a = a;
47 | return c;
48 | }
49 |
50 | public static Color FromRgbDbl(double r, double g, double b)
51 | {
52 | return FromRgbaDbl(r, g, b, 1.0);
53 | }
54 |
55 | public static Color FromRgbaDbl(double r, double g, double b, double a)
56 | {
57 | Color c = new Color();
58 | c.r = (int)(r * 255);
59 | c.g = (int)(g * 255);
60 | c.b = (int)(b * 255);
61 | c.a = (int)(a * 255);
62 | return c;
63 | }
64 |
65 | public override bool Equals(object? obj)
66 | {
67 | return obj is Color c && this == c;
68 | }
69 |
70 | public override int GetHashCode()
71 | {
72 | return r.GetHashCode() * 3 + b.GetHashCode() * 5 + g.GetHashCode() * 7 + a.GetHashCode();
73 | }
74 |
75 | public static bool operator==(Color c1, Color c2)
76 | {
77 | return c1.r == c2.r && c1.g == c2.g && c1.b == c2.b && c1.a == c2.a;
78 | }
79 |
80 | public static bool operator!=(Color c1, Color c2)
81 | {
82 | return !(c1 == c2);
83 | }
84 |
85 | public override string ToString()
86 | {
87 | return $"R: {R}, G: {G}, B: {B}, A: {A}";
88 | }
89 |
90 | // ================================================================================
91 | // Conversion
92 | // ================================================================================
93 |
94 | // Not implicit because automatic conversion to uint could be a bit too permissive
95 | public uint ToUInt()
96 | {
97 | return (uint)(R | (G<<8) | (B<<16) | (A<<24));
98 | }
99 |
100 | public static implicit operator PixelFormats.Rgba32(Color c)
101 | {
102 | return new PixelFormats.Rgba32((Vector4)c);
103 | }
104 |
105 | public static implicit operator Color(PixelFormats.Rgba32 c)
106 | {
107 | return Color.FromRgba(c.R, c.G, c.B, c.A);
108 | }
109 |
110 | public static implicit operator Vector4(Color c)
111 | {
112 | return new Vector4(c.r / 255.0f, c.g / 255.0f, c.b / 255.0f, c.a / 255.0f);
113 | }
114 |
115 | public static implicit operator Color(Vector4 c)
116 | {
117 | return Color.FromRgbaDbl(c.X, c.Y, c.Z, c.W);
118 | }
119 |
120 |
121 |
122 | // ================================================================================
123 | // Constants
124 | // ================================================================================
125 |
126 | // Some colors copied over from System.Drawing to keep things consistent
127 | public static readonly Color Black = FromRgb(0x00, 0x00, 0x00);
128 | public static readonly Color Blue = FromRgb(0x00, 0x00, 0xff);
129 | public static readonly Color Cyan = FromRgb(0x00, 0xff, 0xff);
130 | public static readonly Color DarkOrange = FromRgb(0xff, 0x8c, 0x00);
131 | public static readonly Color Gray = FromRgb(0x80, 0x80, 0x80);
132 | public static readonly Color Green = FromRgb(0x00, 0x80, 0x00);
133 | public static readonly Color Lime = FromRgb(0x00, 0xff, 0x00);
134 | public static readonly Color Red = FromRgb(0xff, 0x00, 0x00);
135 | public static readonly Color Purple = FromRgb(0x80, 0x00, 0x80);
136 | public static readonly Color White = FromRgb(0xff, 0xff, 0xff);
137 | public static readonly Color Yellow = FromRgb(0xff, 0xff, 0x00);
138 | }
139 | }
140 |
--------------------------------------------------------------------------------
/LynnaLib/Util/Helper.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.IO;
4 | using System.Reflection;
5 | using System.Linq;
6 | using System.Threading.Tasks;
7 |
8 | namespace Util
9 | {
10 |
11 | // Some static functions
12 | public class Helper
13 | {
14 | public static Action mainThreadInvokeFunction;
15 |
16 | // LynnaLib doesn't import Gtk, but to help with thread safety, we use this function to
17 | // help ensure everything important runs on the main thread.
18 | //
19 | // Gtk breaks badly when you do stuff on other threads, which can happen when using certain
20 | // library callbacks.
21 | public static void MainThreadInvoke(Action action)
22 | {
23 | if (mainThreadInvokeFunction != null)
24 | mainThreadInvokeFunction(action);
25 | else
26 | throw new NotImplementedException("MainThreadInvoke not implemented");
27 | }
28 |
29 | // Convert a string range into a list of ints.
30 | // Example: "13,15-18" => {13,15,16,17,18}
31 | public static HashSet GetIntListFromRange(string s)
32 | {
33 | if (s == "")
34 | return new HashSet();
35 |
36 | Func> f = str =>
37 | {
38 | int i = str.IndexOf('-');
39 | if (i == -1)
40 | {
41 | int n = Convert.ToInt32(str, 16);
42 | return new Tuple(n, n);
43 | }
44 | int n1 = Convert.ToInt32(str.Substring(0, i), 16);
45 | int n2 = Convert.ToInt32(str.Substring(i + 1), 16);
46 | return new Tuple(n1, n2);
47 | };
48 |
49 | int ind = s.IndexOf(',');
50 | if (ind == -1)
51 | {
52 | var ret = new HashSet();
53 | var tuple = f(s);
54 | for (int i = tuple.Item1; i <= tuple.Item2; i++)
55 | {
56 | ret.Add(i);
57 | }
58 | return ret;
59 | }
60 | HashSet ret2 = GetIntListFromRange(s.Substring(0, ind));
61 | ret2.UnionWith(GetIntListFromRange(s.Substring(ind + 1)));
62 | return ret2;
63 | }
64 |
65 | // Like Directory.GetFiles(), but guaranteed to be sorted.
66 | public static IList GetSortedFiles(string dir)
67 | {
68 | List files = new List(Directory.GetFiles(dir));
69 | files.Sort();
70 | return files;
71 | }
72 |
73 | // Like Directory.GetDirectories(), but guaranteed to be sorted, and it doesn't return the
74 | // full path of the directory.
75 | public static IList GetSortedDirectories(string dir)
76 | {
77 | List files = new List(Directory.GetDirectories(dir));
78 | files.Sort();
79 | return new List(files.Select((x) => x.Substring(x.LastIndexOf("/") + 1)));
80 | }
81 |
82 | // Get a sttream of a resource file
83 | public static Stream GetResourceStream(string name, Assembly assembly = null)
84 | {
85 | if (assembly == null)
86 | assembly = Assembly.GetCallingAssembly();
87 | return assembly.GetManifestResourceStream(name);
88 | }
89 |
90 | // Read a resource file
91 | public static string ReadResourceFile(string name)
92 | {
93 | Stream stream = GetResourceStream(name, Assembly.GetCallingAssembly());
94 | string data = new StreamReader(stream).ReadToEnd();
95 | stream.Close();
96 | return data;
97 | }
98 |
99 | ///
100 | /// Returns a version string representing the current git commit.
101 | ///
102 | public static string GetVersionString()
103 | {
104 | return Helper.ReadResourceFile("LynnaLib.version.txt").Trim();
105 | }
106 |
107 | ///
108 | /// Throw an exception if the value is false. An alternative to Debug.Assert that applies to
109 | /// release builds.
110 | ///
111 | public static void Assert(bool condition, string message=null)
112 | {
113 | if (!condition)
114 | throw new Exception(message);
115 | }
116 |
117 | // From: https://stackoverflow.com/questions/5617320/given-full-path-check-if-path-is-subdirectory-of-some-other-path-or-otherwise
118 | public static bool IsSubPathOf(string subPath, string basePath)
119 | {
120 | var rel = Path.GetRelativePath(
121 | basePath.Replace('\\', '/'),
122 | subPath.Replace('\\', '/'))
123 | .Replace('\\', '/');
124 | return rel != "."
125 | && rel != ".."
126 | && !rel.StartsWith("../")
127 | && !Path.IsPathRooted(rel);
128 | }
129 |
130 | ///
131 | /// Like Task.WhenAll, but triggers an exception if any task triggers an exception.
132 | ///
133 | public static async Task WhenAllWithExceptions(IEnumerable t)
134 | {
135 | HashSet tasks = new(t);
136 |
137 | while (tasks.Count != 0)
138 | {
139 | Task finished = await Task.WhenAny(tasks);
140 | if (finished.IsFaulted)
141 | await finished; // Will trigger exception
142 | if (!tasks.Remove(finished))
143 | throw new Exception("WhenAllWithExceptions: Internal error");
144 | }
145 | }
146 | }
147 |
148 | }
149 |
--------------------------------------------------------------------------------
/LynnaLib/Util/IStream.cs:
--------------------------------------------------------------------------------
1 | namespace Util;
2 |
3 | ///
4 | /// Simple interface that resembles the Stream class (though also has extra stuff like
5 | /// "ModifiedEvent").
6 | ///
7 | public interface IStream
8 | {
9 | public long Length { get; }
10 | public long Position { get; set; }
11 |
12 | public event EventHandler ModifiedEvent;
13 |
14 | public long Seek(long dest, System.IO.SeekOrigin origin = System.IO.SeekOrigin.Begin);
15 |
16 | public int Read(byte[] buffer, int offset, int count);
17 | public int ReadByte();
18 | public ReadOnlySpan ReadAllBytes();
19 |
20 | public void Write(byte[] buffer, int offset, int count);
21 | public void WriteAllBytes(ReadOnlySpan data);
22 | public void WriteByte(byte value);
23 | }
24 |
25 | // Arguments for modification callback
26 | public class StreamModifiedEventArgs
27 | {
28 | public readonly long modifiedRangeStart; // First changed address (inclusive)
29 | public readonly long modifiedRangeEnd; // Last changed address (exclusive)
30 |
31 | public StreamModifiedEventArgs(long s, long e)
32 | {
33 | modifiedRangeStart = s;
34 | modifiedRangeEnd = e;
35 | }
36 |
37 | public bool ByteChanged(long position)
38 | {
39 | return position >= modifiedRangeStart && position < modifiedRangeEnd;
40 | }
41 |
42 | public static StreamModifiedEventArgs All(IStream stream)
43 | {
44 | return new StreamModifiedEventArgs(0, stream.Length);
45 | }
46 |
47 | public static StreamModifiedEventArgs FromChangedRange(byte[] first, byte[] second)
48 | {
49 | if (first.Length == second.Length)
50 | {
51 | // Compare the new and old data to try to optimize which parts we mark as modified.
52 | int start = 0, end = second.Length - 1;
53 |
54 | while (start < second.Length && first[start] == second[start])
55 | start++;
56 | if (start == second.Length)
57 | return null;
58 | while (first[end] == second[end])
59 | end--;
60 | end++;
61 | if (start >= end)
62 | return null;
63 | return new StreamModifiedEventArgs(start, end);
64 | }
65 | else
66 | {
67 | // Just mark everything as modified
68 | return new StreamModifiedEventArgs(0, second.Length);
69 | }
70 | }
71 | }
72 |
--------------------------------------------------------------------------------
/LynnaLib/Util/LockableEvent.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 |
4 | namespace Util
5 | {
6 | // This is just like an Event, but you can "Lock" it to pause callbacks, and "Unlock" it to
7 | // resume them. Useful for doing atomic operations.
8 | public class LockableEvent
9 | {
10 | EventHandler handler;
11 |
12 | int locked = 0;
13 |
14 | List> savedInvokes = new List>();
15 |
16 | public LockableEvent()
17 | {
18 | }
19 |
20 | public void Invoke(object sender, T args)
21 | {
22 | if (locked != 0)
23 | {
24 | savedInvokes.Add(new Tuple