├── .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(sender, args)); 25 | } 26 | else 27 | { 28 | if (handler != null) 29 | handler(sender, args); 30 | } 31 | } 32 | 33 | public void Lock() 34 | { 35 | locked++; 36 | } 37 | 38 | public void Unlock() 39 | { 40 | if (locked == 0) 41 | throw new Exception("Called Unlock on an already unlocked LockableEvent."); 42 | locked--; 43 | if (locked == 0) 44 | { 45 | foreach (var t in savedInvokes) 46 | { 47 | if (handler != null) 48 | handler(t.Item1, t.Item2); 49 | } 50 | savedInvokes.Clear(); 51 | } 52 | } 53 | 54 | // If an event triggered while locked, this clears the event to prevent it from running when 55 | // Unlock() is called. 56 | public void Clear() 57 | { 58 | savedInvokes.Clear(); 59 | } 60 | 61 | 62 | // Static methods 63 | 64 | public static LockableEvent operator +(LockableEvent ev, EventHandler handler) 65 | { 66 | ev.handler += handler; 67 | return ev; 68 | } 69 | 70 | public static LockableEvent operator -(LockableEvent ev, EventHandler handler) 71 | { 72 | ev.handler -= handler; 73 | return ev; 74 | } 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /LynnaLib/Util/LockableEventGroup.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Diagnostics; 4 | 5 | namespace Util 6 | { 7 | // Unlike "LockableEvent" class this operates tn the event handlers. Allows one to define a set 8 | // of handlers which can be "locked" to prevent their execution, and "unlocked" later. 9 | public class LockableEventGroup 10 | { 11 | int locked = 0; 12 | 13 | event Action unlockEvent; 14 | List handlers = new List(); 15 | List unlockLevels = new List(); 16 | 17 | public EventHandler Add(EventHandler handler) 18 | { 19 | EventHandler newHandler = (sender, args) => 20 | { 21 | Invoke(() => 22 | { 23 | handler(sender, args); 24 | }); 25 | }; 26 | 27 | return newHandler; 28 | } 29 | 30 | public EventHandler Add(EventHandler handler) 31 | { 32 | return new EventHandler(Add(new EventHandler(handler))); 33 | } 34 | 35 | public void Invoke(Action action) 36 | { 37 | if (locked == 0) 38 | action(); 39 | else 40 | { 41 | AddHandler(action); 42 | } 43 | } 44 | 45 | public void Lock() 46 | { 47 | locked++; 48 | unlockLevels.Add(handlers.Count); 49 | } 50 | 51 | public void Unlock() 52 | { 53 | if (locked == 0) 54 | throw new Exception("Can't unlock a non-locked LockableEventGroup."); 55 | locked--; 56 | unlockLevels.RemoveAt(unlockLevels.Count - 1); 57 | Debug.Assert(locked == unlockLevels.Count); 58 | if (locked == 0) 59 | { 60 | if (unlockEvent != null) 61 | unlockEvent(); 62 | } 63 | } 64 | 65 | // Unlocks and clears all events that would have triggered since the last "Lock()". 66 | public void UnlockAndClear() 67 | { 68 | int firstHandler = unlockLevels[locked - 1]; 69 | for (int i = firstHandler; i < handlers.Count; i++) 70 | unlockEvent -= handlers[i]; 71 | handlers.RemoveRange(firstHandler, handlers.Count - firstHandler); 72 | Unlock(); 73 | } 74 | 75 | 76 | void AddHandler(Action handler) 77 | { 78 | Action func = null; 79 | func = () => 80 | { 81 | unlockEvent -= func; 82 | handler(); 83 | }; 84 | unlockEvent += func; 85 | handlers.Add(func); 86 | } 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /LynnaLib/Util/LogHelper.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.IO; 3 | using log4net; 4 | using System.Reflection; 5 | 6 | namespace Util 7 | { 8 | public class LogHelper 9 | { 10 | /// When using .NET framework this was not necessary. But after switching to .NET Core it 11 | /// became necessary to add this static constructor which loads the log4net config. 12 | static LogHelper() 13 | { 14 | var logRepository = LogManager.GetRepository(Assembly.GetEntryAssembly()); 15 | log4net.Config.XmlConfigurator.Configure(logRepository, new System.IO.FileInfo("log4net.config")); 16 | } 17 | 18 | 19 | /// Returns a logger using the type of the caller's class. 20 | public static log4net.ILog GetLogger() 21 | { 22 | var callingMethod = (new System.Diagnostics.StackTrace()).GetFrame(1).GetMethod(); 23 | return log4net.LogManager.GetLogger(callingMethod.DeclaringType); 24 | } 25 | 26 | public static void AddAppenderToRootLogger(log4net.Appender.IAppender a) 27 | { 28 | ((log4net.Repository.Hierarchy.Hierarchy)LogManager.GetRepository( 29 | Assembly.GetEntryAssembly())).Root.AddAppender(a); 30 | } 31 | 32 | public static void RemoveAppenderFromRootLogger(log4net.Appender.IAppender a) 33 | { 34 | ((log4net.Repository.Hierarchy.Hierarchy)LogManager.GetRepository( 35 | Assembly.GetEntryAssembly())).Root.RemoveAppender(a); 36 | } 37 | } 38 | } 39 | 40 | -------------------------------------------------------------------------------- /LynnaLib/Util/Misc.cs: -------------------------------------------------------------------------------- 1 | using Collections = System.Collections.Generic; 2 | 3 | 4 | public static class Extensions 5 | { 6 | // For SOME REASON the "IReadOnlyList" class is missing some methods that "List" has. Add them 7 | // here. 8 | public static int IndexOf(this Collections.IReadOnlyList self, T elementToFind) 9 | { 10 | int i = 0; 11 | foreach (T element in self) 12 | { 13 | if (Equals(element, elementToFind)) 14 | return i; 15 | i++; 16 | } 17 | return -1; 18 | } 19 | 20 | public static bool Contains(this Collections.IReadOnlyList self, T elementToFind) 21 | { 22 | return self.IndexOf(elementToFind) != -1; 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /LynnaLib/Util/Point.cs: -------------------------------------------------------------------------------- 1 | using System.Numerics; 2 | 3 | namespace Util; 4 | 5 | public struct Point 6 | { 7 | // ================================================================================ 8 | // Constructors 9 | // ================================================================================ 10 | public Point(int x, int y) 11 | { 12 | X = x; 13 | Y = y; 14 | } 15 | 16 | // ================================================================================ 17 | // Variables 18 | // ================================================================================ 19 | 20 | // ================================================================================ 21 | // Properties 22 | // ================================================================================ 23 | 24 | public int X { get; set; } 25 | public int Y { get; set; } 26 | 27 | // ================================================================================ 28 | // Public methods 29 | // ================================================================================ 30 | 31 | public Vector2 AsVector2() 32 | { 33 | return new Vector2(X, Y); 34 | } 35 | 36 | // ================================================================================ 37 | // Implicit operators 38 | // ================================================================================ 39 | public static Point operator+(Point p1, Point p2) 40 | { 41 | return new Point(p1.X + p2.X, p1.Y + p2.Y); 42 | } 43 | 44 | public static Point operator*(Point p1, int mult) 45 | { 46 | return new Point(p1.X * mult, p1.Y * mult); 47 | } 48 | 49 | public static Point operator*(Point p1, Point p2) 50 | { 51 | return new Point(p1.X * p2.X, p1.Y * p2.Y); 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /LynnaLib/Util/Rect.cs: -------------------------------------------------------------------------------- 1 | using System.Numerics; 2 | 3 | namespace Util 4 | { 5 | /// 6 | /// Floating-point rect 7 | /// 8 | public class FRect 9 | { 10 | // ================================================================================ 11 | // Constructors 12 | // ================================================================================ 13 | public FRect(float x, float y, float width, float height) 14 | { 15 | X = x; 16 | Y = y; 17 | Width = width; 18 | Height = height; 19 | } 20 | 21 | // ================================================================================ 22 | // Variables 23 | // ================================================================================ 24 | 25 | // ================================================================================ 26 | // Properties 27 | // ================================================================================ 28 | public float X { get; private set; } 29 | public float Y { get; private set; } 30 | public float Width { get; private set; } 31 | public float Height { get; private set; } 32 | 33 | public Vector2 Size { get { return new Vector2(Width, Height); } } 34 | 35 | public Vector2 TopLeft { get { return new Vector2(X, Y); } } 36 | public Vector2 BottomRight { get { return TopLeft + Size; } } 37 | public Vector2 Center { get { return (TopLeft + BottomRight) / 2; } } 38 | 39 | // ================================================================================ 40 | // Public methods 41 | // ================================================================================ 42 | 43 | public bool Contains(Vector2 point) 44 | { 45 | return point.X >= X && point.X <= X + Width 46 | && point.Y >= Y && point.Y <= Y + Height; 47 | } 48 | 49 | public override bool Equals(object o) 50 | { 51 | return (o is FRect r) && this == r; 52 | } 53 | 54 | public override int GetHashCode() 55 | { 56 | return X.GetHashCode() * 23 + Y.GetHashCode() * 17 + Width.GetHashCode() * 11 + Height.GetHashCode(); 57 | } 58 | 59 | // ================================================================================ 60 | // Operator overloads 61 | // ================================================================================ 62 | 63 | public static bool operator==(FRect rect1, FRect rect2) 64 | { 65 | if (rect1 is null || rect2 is null) 66 | return (rect1 is null && rect2 is null); 67 | 68 | return rect1.X == rect2.X 69 | && rect1.Y == rect2.Y 70 | && rect1.Width == rect2.Width 71 | && rect1.Height == rect2.Height; 72 | } 73 | 74 | public static bool operator!=(FRect rect1, FRect rect2) 75 | { 76 | return !(rect1 == rect2); 77 | } 78 | 79 | public static FRect operator*(FRect rect1, float scale) 80 | { 81 | return new FRect(rect1.X * scale, rect1.Y * scale, 82 | rect1.Width * scale, rect1.Height * scale); 83 | } 84 | 85 | // ================================================================================ 86 | // Static methods 87 | // ================================================================================ 88 | 89 | /// 90 | /// Return a rectangle which contains the two points. 91 | /// 92 | public static FRect FromVectors(Vector2 p1, Vector2 p2) 93 | { 94 | Vector2 tl = new Vector2( 95 | Math.Min(p1.X, p2.X), 96 | Math.Min(p1.Y, p2.Y) 97 | ); 98 | Vector2 br = new Vector2( 99 | Math.Max(p1.X, p2.X), 100 | Math.Max(p1.Y, p2.Y) 101 | ); 102 | 103 | return new FRect(tl.X, tl.Y, br.X - tl.X, br.Y - tl.Y); 104 | } 105 | } 106 | } 107 | -------------------------------------------------------------------------------- /LynnaLib/Util/SubStream.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.IO; 3 | 4 | namespace Util 5 | { 6 | public class SubStream : Stream 7 | { 8 | public override bool CanRead 9 | { 10 | get { return stream.CanRead; } 11 | } 12 | public override bool CanSeek 13 | { 14 | get { return stream.CanSeek; } 15 | } 16 | public override bool CanTimeout 17 | { 18 | get { return stream.CanTimeout; } 19 | } 20 | public override bool CanWrite 21 | { 22 | get { return stream.CanWrite; } 23 | } 24 | public override long Length 25 | { 26 | get { return _length; } 27 | } 28 | public override long Position 29 | { 30 | get { return _position; } 31 | set { _position = value; } 32 | } 33 | 34 | long _length; 35 | long _position; 36 | 37 | Stream stream; 38 | int streamOffset; 39 | 40 | public SubStream(Stream stream, int offset, int size) 41 | { 42 | this.stream = stream; 43 | this.streamOffset = offset; 44 | 45 | _length = size; 46 | } 47 | 48 | public override void Flush() 49 | { 50 | stream.Flush(); 51 | } 52 | 53 | public override void SetLength(long len) 54 | { 55 | throw new NotImplementedException(); 56 | } 57 | 58 | public override long Seek(long dest, SeekOrigin origin) 59 | { 60 | switch (origin) 61 | { 62 | case SeekOrigin.End: 63 | Position = Length - dest; 64 | break; 65 | case SeekOrigin.Begin: 66 | Position = dest; 67 | break; 68 | case SeekOrigin.Current: 69 | Position += dest; 70 | break; 71 | } 72 | return Position; 73 | } 74 | 75 | public override int Read(byte[] buffer, int offset, int count) 76 | { 77 | int size = count; 78 | if (Position + size > Length) 79 | size = (int)(Length - Position); 80 | stream.Position = Position + streamOffset; 81 | stream.Read(buffer, offset, size); 82 | Position = Position + size; 83 | return size; 84 | } 85 | public override void Write(byte[] buffer, int offset, int count) 86 | { 87 | if (Position + count > Length) 88 | throw new Exception("Write operation passes end of stream"); 89 | stream.Position = Position + streamOffset; 90 | stream.Write(buffer, offset, count); 91 | Position = Position + count; 92 | if (Position > Length) 93 | Position = Length; 94 | } 95 | 96 | public override int ReadByte() 97 | { 98 | stream.Position = Position + streamOffset; 99 | int ret = stream.ReadByte(); 100 | Position++; 101 | return ret; 102 | } 103 | public override void WriteByte(byte value) 104 | { 105 | stream.Position = Position + streamOffset; 106 | stream.WriteByte(value); 107 | Position++; 108 | } 109 | } 110 | } 111 | -------------------------------------------------------------------------------- /LynnaLib/Util/Wla.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | // Class of static functions 4 | 5 | namespace Util 6 | { 7 | public class Wla 8 | { 9 | public static string ToHex(int data, int digits) 10 | { 11 | return "$" + data.ToString("x" + digits); 12 | } 13 | public static string ToHalfByte(byte data) 14 | { 15 | // TODO: assert size? 16 | return ToHex(data, 1); 17 | } 18 | public static string ToByte(byte data) 19 | { 20 | return ToHex(data, 2); 21 | } 22 | public static string ToWord(int data) 23 | { 24 | // TODO: assert size? 25 | return ToHex(data, 4); 26 | } 27 | public static string ToBinary(int data) 28 | { 29 | string s = Convert.ToString(data, 2); 30 | while (s.Length < 8) 31 | s = "0" + s; 32 | return "%" + s; 33 | } 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /LynnaLib/ValueReference.cs: -------------------------------------------------------------------------------- 1 | namespace LynnaLib; 2 | 3 | public enum ValueReferenceType 4 | { 5 | String = 0, 6 | Int, 7 | Bool 8 | } 9 | 10 | // This is a stub for now 11 | public class ValueModifiedEventArgs : EventArgs 12 | { 13 | } 14 | 15 | 16 | /// 17 | /// A ValueReference is a reference to some kind of data, usually a "Data" instance, but could also 18 | /// be from a file stream or something more abstract. 19 | /// 20 | public abstract class ValueReference 21 | { 22 | 23 | private static readonly log4net.ILog log = log4net.LogManager.GetLogger(System.Reflection.MethodBase.GetCurrentMethod().DeclaringType); 24 | 25 | // ================================================================================ 26 | // Constuctors 27 | // ================================================================================ 28 | 29 | public ValueReference(Project project, 30 | ValueReferenceType type, string constantsMappingString) 31 | { 32 | Project = project; 33 | ValueType = type; 34 | 35 | if (constantsMappingString != null) 36 | { 37 | this.ConstantsMappingString = constantsMappingString; 38 | constantsMapping = (ConstantsMapping)typeof(Project).GetProperty(ConstantsMappingString) 39 | .GetValue(Project); 40 | } 41 | } 42 | 43 | 44 | // ================================================================================ 45 | // Variables 46 | // ================================================================================ 47 | 48 | ConstantsMapping constantsMapping; 49 | string transactionDescription; 50 | bool transactionMerge; 51 | 52 | // ================================================================================ 53 | // Properties 54 | // ================================================================================ 55 | 56 | public Project Project 57 | { 58 | get; private set; 59 | } 60 | public int MaxValue { get; protected set; } 61 | public int MinValue { get; protected set; } 62 | 63 | // TODO: Move this to ValueReferenceDescriptor 64 | public ValueReferenceType ValueType { get; protected set; } 65 | 66 | 67 | // Other properties 68 | 69 | public string ConstantsMappingString { get; private set; } 70 | public ConstantsMapping ConstantsMapping { get { return constantsMapping; } } 71 | 72 | public event EventHandler ModifiedEvent; 73 | 74 | 75 | // ================================================================================ 76 | // Public methods 77 | // ================================================================================ 78 | 79 | public abstract string GetStringValue(); 80 | public abstract int GetIntValue(); 81 | public abstract void SetValue(string s); 82 | public abstract void SetValue(int i); 83 | 84 | // TODO: Remove these functions in favor of just using the Modified event 85 | public void AddValueModifiedHandler(EventHandler handler) 86 | { 87 | ModifiedEvent += handler; 88 | } 89 | public void RemoveValueModifiedHandler(EventHandler handler) 90 | { 91 | ModifiedEvent -= handler; 92 | } 93 | 94 | // Subclasses must call this to raise the event 95 | protected void RaiseModifiedEvent(ValueModifiedEventArgs args) 96 | { 97 | ModifiedEvent?.Invoke(this, args); 98 | } 99 | 100 | 101 | /// 102 | /// Modifications to this ValueReference will register transactions with the given name. For undo/redo. 103 | /// 104 | public void EnableTransactions(string transactionDescription, bool merge = false) 105 | { 106 | this.transactionDescription = transactionDescription; 107 | this.transactionMerge = merge; 108 | } 109 | 110 | // Sets the value to its default. 111 | public abstract void Initialize(); 112 | 113 | 114 | // ================================================================================ 115 | // Protected methods 116 | // ================================================================================ 117 | 118 | protected void BeginTransaction() 119 | { 120 | if (transactionDescription != null) 121 | Project.BeginTransaction(transactionDescription, transactionMerge); 122 | } 123 | 124 | protected void EndTransaction() 125 | { 126 | if (transactionDescription != null) 127 | Project.EndTransaction(); 128 | } 129 | } 130 | -------------------------------------------------------------------------------- /LynnaLib/ValueReferenceDescriptor.cs: -------------------------------------------------------------------------------- 1 | namespace LynnaLib; 2 | 3 | /// 4 | /// Wrapper over a ValueReference providing additional information for editing such as 5 | /// documentation, tooltip, etc. 6 | /// 7 | public class ValueReferenceDescriptor 8 | { 9 | // ================================================================================ 10 | // Constructors 11 | // ================================================================================ 12 | public ValueReferenceDescriptor(ValueReference valueReference, 13 | string name, 14 | bool editable = true, 15 | string tooltip = null) 16 | { 17 | this.valueReference = valueReference; 18 | this.Name = name; 19 | this.Editable = editable; 20 | this.Tooltip = tooltip; 21 | 22 | if (ValueReference.ConstantsMapping != null) 23 | { 24 | Documentation = valueReference.ConstantsMapping.OverallDocumentation; 25 | Documentation.Name = "Field: " + Name; 26 | } 27 | } 28 | 29 | // ================================================================================ 30 | // Variables 31 | // ================================================================================ 32 | ValueReference valueReference; 33 | 34 | // ================================================================================ 35 | // Properties 36 | // ================================================================================ 37 | 38 | public Project Project { get { return valueReference.Project; } } 39 | public ValueReference ValueReference { get { return valueReference; } } 40 | public ValueReferenceType ValueType { get { return ValueReference.ValueType; } } 41 | 42 | public string Name { get; private set; } 43 | public bool Editable { get; private set; } 44 | public string Tooltip { get; private set; } 45 | 46 | public ConstantsMapping ConstantsMapping { get { return ValueReference.ConstantsMapping; } } 47 | // This documentation tends to change based on what the current value is... 48 | public Documentation Documentation { get; private set; } 49 | 50 | 51 | // ================================================================================ 52 | // Public methods 53 | // ================================================================================ 54 | 55 | /// 56 | /// Returns a field from documentation (ie. "@desc{An interaction}"). 57 | /// 58 | public string GetDocumentationField(string name) 59 | { 60 | if (Documentation == null) 61 | return null; 62 | return Documentation.GetField(name); 63 | } 64 | 65 | 66 | // Passthrough functions to ValueReference for convenience 67 | 68 | public string GetStringValue() 69 | { 70 | return ValueReference.GetStringValue(); 71 | } 72 | 73 | public int GetIntValue() 74 | { 75 | return ValueReference.GetIntValue(); 76 | } 77 | 78 | public void SetValue(string s) 79 | { 80 | ValueReference.SetValue(s); 81 | } 82 | 83 | public void SetValue(int i) 84 | { 85 | ValueReference.SetValue(i); 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /LynnaLib/ValueReferenceGroup.cs: -------------------------------------------------------------------------------- 1 | namespace LynnaLib; 2 | 3 | /// 4 | /// This class contains a list of ValueReferences and allows you to look them up by name to get 5 | /// or set them. 6 | /// 7 | public class ValueReferenceGroup 8 | { 9 | readonly IList descriptors; 10 | readonly LockableEvent lockableModifiedEvent = new LockableEvent(); 11 | 12 | string transactionDescription; 13 | bool transactionMerge; 14 | 15 | 16 | public ValueReferenceGroup(IList refs) 17 | { 18 | lockableModifiedEvent += (sender, args) => ModifiedEvent?.Invoke(sender, args); 19 | descriptors = new List(); 20 | foreach (var desc in refs) 21 | { 22 | descriptors.Add(desc); 23 | 24 | desc.ValueReference.AddValueModifiedHandler( 25 | (sender, args) => lockableModifiedEvent?.Invoke(sender, args)); 26 | } 27 | } 28 | 29 | 30 | public event EventHandler ModifiedEvent; 31 | 32 | 33 | 34 | // Properties 35 | 36 | public Project Project 37 | { 38 | get 39 | { 40 | if (descriptors.Count == 0) 41 | return null; 42 | return descriptors[0].Project; 43 | } 44 | } 45 | 46 | public int Count 47 | { 48 | get { return descriptors.Count; } 49 | } 50 | 51 | 52 | // Indexers 53 | 54 | public ValueReferenceDescriptor this[int i] 55 | { 56 | get { return descriptors[i]; } 57 | } 58 | public ValueReferenceDescriptor this[string name] 59 | { 60 | get 61 | { 62 | return GetDescriptor(name); 63 | } 64 | } 65 | 66 | 67 | // Public methods 68 | 69 | public IList GetDescriptors() 70 | { 71 | return descriptors; 72 | } 73 | public ValueReferenceDescriptor GetDescriptor(string name) 74 | { 75 | foreach (ValueReferenceDescriptor desc in descriptors) 76 | { 77 | if (desc.Name == name) 78 | { 79 | return desc; 80 | } 81 | } 82 | throw new InvalidLookupException("Couldn't find ValueReference corresponding to \"" + name + "\"."); 83 | } 84 | 85 | public int GetIndexOf(ValueReferenceDescriptor r) 86 | { 87 | int i = 0; 88 | foreach (ValueReferenceDescriptor s in descriptors) 89 | { 90 | if (s.Name == r.Name) 91 | return i; 92 | i++; 93 | } 94 | return -1; 95 | } 96 | 97 | public bool HasValue(string name) 98 | { 99 | foreach (var r in descriptors) 100 | if (r.Name == name) 101 | return true; 102 | return false; 103 | } 104 | 105 | 106 | public string GetValue(string name) 107 | { 108 | return GetDescriptor(name).GetStringValue(); 109 | } 110 | public int GetIntValue(string name) 111 | { 112 | ValueReferenceDescriptor desc = GetDescriptor(name); 113 | return desc.GetIntValue(); 114 | } 115 | 116 | public void SetValue(string name, string value) 117 | { 118 | ValueReferenceDescriptor desc = GetDescriptor(name); 119 | desc.SetValue(value); 120 | } 121 | public void SetValue(string name, int value) 122 | { 123 | ValueReferenceDescriptor desc = GetDescriptor(name); 124 | desc.SetValue(value); 125 | } 126 | 127 | // TODO: remove these, use the public event instead 128 | public void AddValueModifiedHandler(EventHandler handler) 129 | { 130 | ModifiedEvent += handler; 131 | } 132 | public void RemoveValueModifiedHandler(EventHandler handler) 133 | { 134 | ModifiedEvent -= handler; 135 | } 136 | 137 | /// Call this to prevent events from firing until EndAtomicOperation is called. 138 | public void BeginAtomicOperation() 139 | { 140 | lockableModifiedEvent.Lock(); 141 | // TODO: Would be ideal if this also locked events for the ValueReferences themselves 142 | } 143 | 144 | public void EndAtomicOperation() 145 | { 146 | lockableModifiedEvent.Unlock(); 147 | } 148 | 149 | public void CopyFrom(ValueReferenceGroup vrg) 150 | { 151 | BeginTransaction(); 152 | BeginAtomicOperation(); 153 | 154 | foreach (var desc in descriptors) 155 | { 156 | desc.SetValue(vrg.GetValue(desc.Name)); 157 | } 158 | 159 | EndAtomicOperation(); 160 | EndTransaction(); 161 | } 162 | 163 | public void EnableTransactions(string description, bool merge = false) 164 | { 165 | transactionDescription = $"{description}#All fields"; 166 | transactionMerge = merge; 167 | foreach (var descriptor in descriptors) 168 | { 169 | string desc = $"{description}#{descriptor.Name}"; 170 | descriptor.ValueReference.EnableTransactions(desc, merge); 171 | } 172 | } 173 | 174 | 175 | // ================================================================================ 176 | // Protected methods 177 | // ================================================================================ 178 | 179 | // ================================================================================ 180 | // Private methods 181 | // ================================================================================ 182 | 183 | void BeginTransaction() 184 | { 185 | if (transactionDescription != null) 186 | Project.BeginTransaction(transactionDescription, transactionMerge); 187 | } 188 | 189 | void EndTransaction() 190 | { 191 | if (transactionDescription != null) 192 | Project.EndTransaction(); 193 | } 194 | } 195 | -------------------------------------------------------------------------------- /LynnaLib/ValueReferenceWrapper.cs: -------------------------------------------------------------------------------- 1 | namespace LynnaLib; 2 | 3 | /// 4 | /// Wrapper around a ValueReference that implicitly converts to an int (or back to a ValueReference). 5 | /// 6 | public class IntValueReferenceWrapper 7 | { 8 | // ================================================================================ 9 | // Constructors 10 | // ================================================================================ 11 | public IntValueReferenceWrapper(ValueReference vr) 12 | { 13 | this.ValueReference = vr; 14 | } 15 | 16 | // ================================================================================ 17 | // Properties 18 | // ================================================================================ 19 | 20 | public ValueReference ValueReference { get; private set; } 21 | 22 | // ================================================================================ 23 | // Implicit conversion 24 | // ================================================================================ 25 | public static implicit operator int(IntValueReferenceWrapper wrapper) 26 | { 27 | return wrapper.ValueReference.GetIntValue(); 28 | } 29 | 30 | public static implicit operator ValueReference(IntValueReferenceWrapper wrapper) 31 | { 32 | return wrapper.ValueReference; 33 | } 34 | 35 | public static implicit operator IntValueReferenceWrapper(ValueReference vr) 36 | { 37 | return new IntValueReferenceWrapper(vr); 38 | } 39 | } 40 | 41 | /// 42 | /// Wrapper around a ValueReference that implicitly converts to a bool (or back to a ValueReference). 43 | /// 44 | public class BoolValueReferenceWrapper 45 | { 46 | // ================================================================================ 47 | // Constructors 48 | // ================================================================================ 49 | public BoolValueReferenceWrapper(ValueReference vr) 50 | { 51 | this.ValueReference = vr; 52 | } 53 | 54 | // ================================================================================ 55 | // Properties 56 | // ================================================================================ 57 | 58 | public ValueReference ValueReference { get; private set; } 59 | 60 | // ================================================================================ 61 | // Implicit conversion 62 | // ================================================================================ 63 | public static implicit operator bool(BoolValueReferenceWrapper wrapper) 64 | { 65 | return wrapper.ValueReference.GetIntValue() != 0; 66 | } 67 | 68 | public static implicit operator ValueReference(BoolValueReferenceWrapper wrapper) 69 | { 70 | return wrapper.ValueReference; 71 | } 72 | 73 | public static implicit operator BoolValueReferenceWrapper(ValueReference vr) 74 | { 75 | return new BoolValueReferenceWrapper(vr); 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /LynnaLib/WarpDestData.cs: -------------------------------------------------------------------------------- 1 | namespace LynnaLib 2 | { 3 | public class WarpDestData : Data 4 | { 5 | public static string WarpCommand = "m_WarpDest"; 6 | 7 | 8 | private static List GetWarpValueReferences(Data data) 9 | { 10 | return new List { 11 | DataValueReference.Descriptor(data,"Map",0,DataValueType.Byte), 12 | DataValueReference.Descriptor(data,"Y",1,DataValueType.ByteBits,4,7), 13 | DataValueReference.Descriptor(data,"X",1,DataValueType.ByteBits,0,3), 14 | DataValueReference.Descriptor(data,"Parameter",2,DataValueType.HalfByte), 15 | DataValueReference.Descriptor(data,"Transition",3,DataValueType.HalfByte, 16 | constantsMappingString:"DestTransitionMapping"), 17 | }; 18 | } 19 | 20 | 21 | // Private variables 22 | 23 | class WarpDestState : Data.DataState 24 | { 25 | public HashSet> referenceSet; 26 | 27 | public override void CaptureInitialState(FileComponent parent) 28 | { 29 | parent.Project.TransactionManager.CaptureInitialState(parent); 30 | } 31 | } 32 | 33 | ValueReferenceGroup vrg; 34 | 35 | 36 | // Properties 37 | 38 | private WarpDestState State { get { return base.state as WarpDestState; } } 39 | 40 | public int Map 41 | { 42 | get 43 | { 44 | return vrg.GetIntValue("Map"); 45 | } 46 | set 47 | { 48 | vrg.SetValue("Map", value); 49 | } 50 | } 51 | public int Y 52 | { 53 | get 54 | { 55 | return vrg.GetIntValue("Y"); 56 | } 57 | set 58 | { 59 | vrg.SetValue("Y", value); 60 | } 61 | } 62 | public int X 63 | { 64 | get 65 | { 66 | return vrg.GetIntValue("X"); 67 | } 68 | set 69 | { 70 | vrg.SetValue("X", value); 71 | } 72 | } 73 | public int Parameter 74 | { 75 | get 76 | { 77 | return vrg.GetIntValue("Parameter"); 78 | } 79 | set 80 | { 81 | vrg.SetValue("Parameter", value); 82 | } 83 | } 84 | public int Transition 85 | { 86 | get 87 | { 88 | return vrg.GetIntValue("Transition"); 89 | } 90 | set 91 | { 92 | vrg.SetValue("Transition", value); 93 | } 94 | } 95 | 96 | public ValueReferenceGroup ValueReferenceGroup 97 | { 98 | get { return vrg; } 99 | } 100 | 101 | 102 | public WarpDestData(Project p, string id, string command, IEnumerable values, 103 | FileParser parser, IList spacing) 104 | : base(p, id, command, values, 3, parser, spacing, () => new WarpDestState()) 105 | { 106 | vrg = new ValueReferenceGroup(GetWarpValueReferences(this)); 107 | 108 | State.referenceSet = new HashSet>(); 109 | } 110 | 111 | /// 112 | /// State-based constructor, for network transfer (located via reflection) 113 | /// 114 | private WarpDestData(Project p, string id, TransactionState state) 115 | : base(p, id, state) 116 | { 117 | } 118 | 119 | public void AddReference(WarpSourceData data) 120 | { 121 | Project.TransactionManager.CaptureInitialState(this); 122 | if (!State.referenceSet.Add(new(data))) 123 | throw new Exception("Internal error: Warp dest data reference set state invalid"); 124 | } 125 | 126 | public void RemoveReference(WarpSourceData data) 127 | { 128 | Project.TransactionManager.CaptureInitialState(this); 129 | if (!State.referenceSet.Remove(new(data))) 130 | throw new Exception("Internal error: Warp dest data reference set state invalid"); 131 | } 132 | 133 | public int GetNumReferences() 134 | { 135 | return State.referenceSet.Count; 136 | } 137 | 138 | public override void OnInitializedFromTransfer() 139 | { 140 | vrg = new ValueReferenceGroup(GetWarpValueReferences(this)); 141 | } 142 | } 143 | } 144 | -------------------------------------------------------------------------------- /LynnaLib/WorldMap.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.IO; 3 | 4 | namespace LynnaLib 5 | { 6 | public class WorldMap : Map, ProjectDataInstantiator 7 | { 8 | // Stub state - we don't actually need to track anything through undo/redo. Only have this 9 | // due to requirements of the base class. 10 | class State : TransactionState 11 | { 12 | public required int group { get; init; } 13 | public required Season season { get; init; } 14 | } 15 | 16 | State state; 17 | 18 | private WorldMap(Project p, int group, Season season) : base(p, $"{group}_{season}") 19 | { 20 | if (!p.IsInConstructor) 21 | throw new Exception("Dungeons should not be loaded outside of the Project constructor."); 22 | 23 | state = new() 24 | { 25 | group = group, 26 | season = season, 27 | }; 28 | } 29 | 30 | /// 31 | /// State-based constructor, for network transfer (located via reflection) 32 | /// 33 | private WorldMap(Project p, string id, TransactionState s) 34 | : base(p, id) 35 | { 36 | this.state = (State)s; 37 | 38 | if (!Project.GroupSeasonIsValid(state.group, state.season)) 39 | throw new DeserializationException($"Bad group/season pair: {state.group}, {state.season}"); 40 | } 41 | 42 | // TODO: Remove this - not necessary 43 | static ProjectDataType ProjectDataInstantiator.Instantiate(Project p, string id) 44 | { 45 | string[] split = id.Split("_"); 46 | if (split.Length != 2) 47 | throw new DeserializationException(); 48 | 49 | int group; 50 | Season season; 51 | if (!int.TryParse(split[0], out group)) 52 | throw new DeserializationException(); 53 | if (!Season.TryParse(split[1], out season)) 54 | throw new DeserializationException(); 55 | 56 | if (group < 0 || group >= p.NumGroups) 57 | throw new DeserializationException(); 58 | if (!p.GroupSeasonIsValid(group, season)) 59 | throw new DeserializationException(); 60 | 61 | return new WorldMap(p, group, season); 62 | } 63 | 64 | 65 | // Map properties 66 | public override int MainGroup 67 | { 68 | get 69 | { 70 | return state.group; 71 | } 72 | } 73 | 74 | public override int MapWidth 75 | { 76 | get 77 | { 78 | return 16; 79 | } 80 | } 81 | public override int MapHeight 82 | { 83 | get 84 | { 85 | return 16; 86 | } 87 | } 88 | public override int RoomWidth 89 | { 90 | get 91 | { 92 | return GetRoom(0, 0).Width; 93 | } 94 | } 95 | public override int RoomHeight 96 | { 97 | get 98 | { 99 | return GetRoom(0, 0).Height; 100 | } 101 | } 102 | public override Season Season { get { return state.season; } } 103 | 104 | 105 | // Map methods 106 | 107 | public override Room GetRoom(int x, int y, int floor = 0) 108 | { 109 | return Project.GetIndexedDataType(MainGroup * 0x100 + x + y * 16); 110 | } 111 | public override IEnumerable<(int x, int y, int floor)> GetRoomPositions(Room room) 112 | { 113 | if (room.Group != MainGroup) 114 | return new List<(int, int, int)>(); 115 | return new List<(int, int, int)> { (room.Index % 16, (room.Index & 0xff) / 16, 0 ) }; 116 | } 117 | public override bool GetRoomPosition(Room room, out int x, out int y, out int floor) 118 | { 119 | if (room.Group != MainGroup) 120 | { 121 | // Not in this group 122 | x = -1; 123 | y = -1; 124 | floor = -1; 125 | return false; 126 | } 127 | x = room.Index % 16; 128 | y = (room.Index % 0x100) / 16; 129 | floor = 0; 130 | return true; 131 | } 132 | 133 | // ================================================================================ 134 | // TrackedProjectData implementation 135 | // ================================================================================ 136 | 137 | public override TransactionState GetState() 138 | { 139 | return state; 140 | } 141 | public override void SetState(TransactionState state) 142 | { 143 | this.state = (State)state; 144 | } 145 | public override void InvokeUndoEvents(TransactionState oldState) 146 | { 147 | 148 | } 149 | } 150 | } 151 | -------------------------------------------------------------------------------- /images/preview-brush.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Stewmath/LynnaLab/a17ac808addf0002586b05ce2d6488c5137672f8/images/preview-brush.gif -------------------------------------------------------------------------------- /images/preview-general.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Stewmath/LynnaLab/a17ac808addf0002586b05ce2d6488c5137672f8/images/preview-general.png -------------------------------------------------------------------------------- /images/preview-objects.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Stewmath/LynnaLab/a17ac808addf0002586b05ce2d6488c5137672f8/images/preview-objects.png -------------------------------------------------------------------------------- /images/preview-quickspawn-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Stewmath/LynnaLab/a17ac808addf0002586b05ce2d6488c5137672f8/images/preview-quickspawn-1.png -------------------------------------------------------------------------------- /images/preview-quickspawn-2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Stewmath/LynnaLab/a17ac808addf0002586b05ce2d6488c5137672f8/images/preview-quickspawn-2.png -------------------------------------------------------------------------------- /images/preview-warps.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Stewmath/LynnaLab/a17ac808addf0002586b05ce2d6488c5137672f8/images/preview-warps.png --------------------------------------------------------------------------------