├── .gitattributes ├── .gitignore ├── License.txt ├── Readme.md ├── TheKingsTable.sln ├── TheKingsTable ├── App.axaml ├── App.axaml.cs ├── Assets │ └── avalonia-logo.ico ├── CommonAvaloniaHandlers.cs ├── CommonInteractions.cs ├── Controls │ ├── BitEditor.axaml │ ├── BitEditor.axaml.cs │ ├── EntityEditor.axaml │ ├── EntityEditor.axaml.cs │ ├── EntitySelector.axaml │ ├── EntitySelector.axaml.cs │ ├── FileSystemSelector.axaml │ ├── FileSystemSelector.axaml.cs │ ├── StageEditorToolMenu.cs │ ├── StageEditorToolMenuStyles.axaml │ ├── StageRenderer.cs │ ├── StageTableLocationEditor.axaml │ ├── StageTableLocationEditor.axaml.cs │ ├── TextEditorWrapper.axaml │ ├── TextEditorWrapper.axaml.cs │ ├── TileMouseHelper.cs │ ├── TileSelectionEditor.axaml │ ├── TileSelectionEditor.axaml.cs │ ├── TilesetViewer.cs │ ├── WizardControl.cs │ └── WizardStyles.axaml ├── Converters │ ├── EntityListConverter.cs │ ├── ShortConverter.cs │ └── StageEntryListConverter.cs ├── Models │ ├── AvaloniaClipboard.cs │ └── EditorManager.cs ├── Program.cs ├── TheKingsTable.csproj ├── Utilities.cs ├── ViewLocator.cs ├── ViewModels │ ├── Editors │ │ ├── ProjectEditorViewModel.cs │ │ ├── StageEditorViewModel.cs │ │ ├── StageTableListViewModel.cs │ │ └── TextScriptEditorViewModel.cs │ ├── MainWindowViewModel.cs │ ├── ViewModelBase.cs │ ├── WizardViewModel.cs │ └── WizardWindowViewModel.cs ├── Views │ ├── Editors │ │ ├── ProjectEditor.axaml │ │ ├── StageEditor.axaml │ │ ├── StageTableEditors.axaml │ │ └── TextScriptEditor.axaml │ ├── MainWindow.axaml │ ├── MainWindow.axaml.cs │ ├── WizardView.axaml │ ├── WizardView.axaml.cs │ ├── WizardWindow.axaml │ └── WizardWindow.axaml.cs ├── nuget.config └── tiletypes.png └── appveyor.yml /.gitattributes: -------------------------------------------------------------------------------- 1 | ############################################################################### 2 | # Set default behavior to automatically normalize line endings. 3 | ############################################################################### 4 | * text=auto 5 | 6 | ############################################################################### 7 | # Set default behavior for command prompt diff. 8 | # 9 | # This is need for earlier builds of msysgit that does not have it on by 10 | # default for csharp files. 11 | # Note: This is only used by command line 12 | ############################################################################### 13 | #*.cs diff=csharp 14 | 15 | ############################################################################### 16 | # Set the merge driver for project and solution files 17 | # 18 | # Merging from the command prompt will add diff markers to the files if there 19 | # are conflicts (Merging from VS is not affected by the settings below, in VS 20 | # the diff markers are never inserted). Diff markers may cause the following 21 | # file extensions to fail to load in VS. An alternative would be to treat 22 | # these files as binary and thus will always conflict and require user 23 | # intervention with every merge. To do so, just uncomment the entries below 24 | ############################################################################### 25 | #*.sln merge=binary 26 | #*.csproj merge=binary 27 | #*.vbproj merge=binary 28 | #*.vcxproj merge=binary 29 | #*.vcproj merge=binary 30 | #*.dbproj merge=binary 31 | #*.fsproj merge=binary 32 | #*.lsproj merge=binary 33 | #*.wixproj merge=binary 34 | #*.modelproj merge=binary 35 | #*.sqlproj merge=binary 36 | #*.wwaproj merge=binary 37 | 38 | ############################################################################### 39 | # behavior for image files 40 | # 41 | # image files are treated as binary by default. 42 | ############################################################################### 43 | #*.jpg binary 44 | #*.png binary 45 | #*.gif binary 46 | 47 | ############################################################################### 48 | # diff behavior for common document formats 49 | # 50 | # Convert binary document formats to text before diffing them. This feature 51 | # is only available from the command line. Turn it on by uncommenting the 52 | # entries below. 53 | ############################################################################### 54 | #*.doc diff=astextplain 55 | #*.DOC diff=astextplain 56 | #*.docx diff=astextplain 57 | #*.DOCX diff=astextplain 58 | #*.dot diff=astextplain 59 | #*.DOT diff=astextplain 60 | #*.pdf diff=astextplain 61 | #*.PDF diff=astextplain 62 | #*.rtf diff=astextplain 63 | #*.RTF diff=astextplain 64 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ## Ignore Visual Studio temporary files, build results, and 2 | ## files generated by popular Visual Studio add-ons. 3 | ## 4 | ## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore 5 | 6 | # User-specific files 7 | *.rsuser 8 | *.suo 9 | *.user 10 | *.userosscache 11 | *.sln.docstates 12 | 13 | # User-specific files (MonoDevelop/Xamarin Studio) 14 | *.userprefs 15 | 16 | # Build results 17 | [Dd]ebug/ 18 | [Dd]ebugPublic/ 19 | [Rr]elease/ 20 | [Rr]eleases/ 21 | x64/ 22 | x86/ 23 | [Aa][Rr][Mm]/ 24 | [Aa][Rr][Mm]64/ 25 | bld/ 26 | [Bb]in/ 27 | [Oo]bj/ 28 | [Ll]og/ 29 | 30 | # Visual Studio 2015/2017 cache/options directory 31 | .vs/ 32 | # Uncomment if you have tasks that create the project's static files in wwwroot 33 | #wwwroot/ 34 | 35 | # Visual Studio 2017 auto generated files 36 | Generated\ Files/ 37 | 38 | # MSTest test Results 39 | [Tt]est[Rr]esult*/ 40 | [Bb]uild[Ll]og.* 41 | 42 | # NUNIT 43 | *.VisualState.xml 44 | TestResult.xml 45 | 46 | # Build Results of an ATL Project 47 | [Dd]ebugPS/ 48 | [Rr]eleasePS/ 49 | dlldata.c 50 | 51 | # Benchmark Results 52 | BenchmarkDotNet.Artifacts/ 53 | 54 | # .NET Core 55 | project.lock.json 56 | project.fragment.lock.json 57 | artifacts/ 58 | 59 | # StyleCop 60 | StyleCopReport.xml 61 | 62 | # Files built by Visual Studio 63 | *_i.c 64 | *_p.c 65 | *_h.h 66 | *.ilk 67 | *.meta 68 | *.obj 69 | *.iobj 70 | *.pch 71 | *.pdb 72 | *.ipdb 73 | *.pgc 74 | *.pgd 75 | *.rsp 76 | *.sbr 77 | *.tlb 78 | *.tli 79 | *.tlh 80 | *.tmp 81 | *.tmp_proj 82 | *_wpftmp.csproj 83 | *.log 84 | *.vspscc 85 | *.vssscc 86 | .builds 87 | *.pidb 88 | *.svclog 89 | *.scc 90 | 91 | # Chutzpah Test files 92 | _Chutzpah* 93 | 94 | # Visual C++ cache files 95 | ipch/ 96 | *.aps 97 | *.ncb 98 | *.opendb 99 | *.opensdf 100 | *.sdf 101 | *.cachefile 102 | *.VC.db 103 | *.VC.VC.opendb 104 | 105 | # Visual Studio profiler 106 | *.psess 107 | *.vsp 108 | *.vspx 109 | *.sap 110 | 111 | # Visual Studio Trace Files 112 | *.e2e 113 | 114 | # TFS 2012 Local Workspace 115 | $tf/ 116 | 117 | # Guidance Automation Toolkit 118 | *.gpState 119 | 120 | # ReSharper is a .NET coding add-in 121 | _ReSharper*/ 122 | *.[Rr]e[Ss]harper 123 | *.DotSettings.user 124 | 125 | # JustCode is a .NET coding add-in 126 | .JustCode 127 | 128 | # TeamCity is a build add-in 129 | _TeamCity* 130 | 131 | # DotCover is a Code Coverage Tool 132 | *.dotCover 133 | 134 | # AxoCover is a Code Coverage Tool 135 | .axoCover/* 136 | !.axoCover/settings.json 137 | 138 | # Visual Studio code coverage results 139 | *.coverage 140 | *.coveragexml 141 | 142 | # NCrunch 143 | _NCrunch_* 144 | .*crunch*.local.xml 145 | nCrunchTemp_* 146 | 147 | # MightyMoose 148 | *.mm.* 149 | AutoTest.Net/ 150 | 151 | # Web workbench (sass) 152 | .sass-cache/ 153 | 154 | # Installshield output folder 155 | [Ee]xpress/ 156 | 157 | # DocProject is a documentation generator add-in 158 | DocProject/buildhelp/ 159 | DocProject/Help/*.HxT 160 | DocProject/Help/*.HxC 161 | DocProject/Help/*.hhc 162 | DocProject/Help/*.hhk 163 | DocProject/Help/*.hhp 164 | DocProject/Help/Html2 165 | DocProject/Help/html 166 | 167 | # Click-Once directory 168 | publish/ 169 | 170 | # Publish Web Output 171 | *.[Pp]ublish.xml 172 | *.azurePubxml 173 | # Note: Comment the next line if you want to checkin your web deploy settings, 174 | # but database connection strings (with potential passwords) will be unencrypted 175 | *.pubxml 176 | *.publishproj 177 | 178 | # Microsoft Azure Web App publish settings. Comment the next line if you want to 179 | # checkin your Azure Web App publish settings, but sensitive information contained 180 | # in these scripts will be unencrypted 181 | PublishScripts/ 182 | 183 | # NuGet Packages 184 | *.nupkg 185 | # The packages folder can be ignored because of Package Restore 186 | **/[Pp]ackages/* 187 | # except build/, which is used as an MSBuild target. 188 | !**/[Pp]ackages/build/ 189 | # Uncomment if necessary however generally it will be regenerated when needed 190 | #!**/[Pp]ackages/repositories.config 191 | # NuGet v3's project.json files produces more ignorable files 192 | *.nuget.props 193 | *.nuget.targets 194 | 195 | # Microsoft Azure Build Output 196 | csx/ 197 | *.build.csdef 198 | 199 | # Microsoft Azure Emulator 200 | ecf/ 201 | rcf/ 202 | 203 | # Windows Store app package directories and files 204 | AppPackages/ 205 | BundleArtifacts/ 206 | Package.StoreAssociation.xml 207 | _pkginfo.txt 208 | *.appx 209 | 210 | # Visual Studio cache files 211 | # files ending in .cache can be ignored 212 | *.[Cc]ache 213 | # but keep track of directories ending in .cache 214 | !?*.[Cc]ache/ 215 | 216 | # Others 217 | ClientBin/ 218 | ~$* 219 | *~ 220 | *.dbmdl 221 | *.dbproj.schemaview 222 | *.jfm 223 | *.pfx 224 | *.publishsettings 225 | orleans.codegen.cs 226 | 227 | # Including strong name files can present a security risk 228 | # (https://github.com/github/gitignore/pull/2483#issue-259490424) 229 | #*.snk 230 | 231 | # Since there are multiple workflows, uncomment next line to ignore bower_components 232 | # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) 233 | #bower_components/ 234 | 235 | # RIA/Silverlight projects 236 | Generated_Code/ 237 | 238 | # Backup & report files from converting an old project file 239 | # to a newer Visual Studio version. Backup files are not needed, 240 | # because we have git ;-) 241 | _UpgradeReport_Files/ 242 | Backup*/ 243 | UpgradeLog*.XML 244 | UpgradeLog*.htm 245 | ServiceFabricBackup/ 246 | *.rptproj.bak 247 | 248 | # SQL Server files 249 | *.mdf 250 | *.ldf 251 | *.ndf 252 | 253 | # Business Intelligence projects 254 | *.rdl.data 255 | *.bim.layout 256 | *.bim_*.settings 257 | *.rptproj.rsuser 258 | *- Backup*.rdl 259 | 260 | # Microsoft Fakes 261 | FakesAssemblies/ 262 | 263 | # GhostDoc plugin setting file 264 | *.GhostDoc.xml 265 | 266 | # Node.js Tools for Visual Studio 267 | .ntvs_analysis.dat 268 | node_modules/ 269 | 270 | # Visual Studio 6 build log 271 | *.plg 272 | 273 | # Visual Studio 6 workspace options file 274 | *.opt 275 | 276 | # Visual Studio 6 auto-generated workspace file (contains which files were open etc.) 277 | *.vbw 278 | 279 | # Visual Studio LightSwitch build output 280 | **/*.HTMLClient/GeneratedArtifacts 281 | **/*.DesktopClient/GeneratedArtifacts 282 | **/*.DesktopClient/ModelManifest.xml 283 | **/*.Server/GeneratedArtifacts 284 | **/*.Server/ModelManifest.xml 285 | _Pvt_Extensions 286 | 287 | # Paket dependency manager 288 | .paket/paket.exe 289 | paket-files/ 290 | 291 | # FAKE - F# Make 292 | .fake/ 293 | 294 | # JetBrains Rider 295 | .idea/ 296 | *.sln.iml 297 | 298 | # CodeRush personal settings 299 | .cr/personal 300 | 301 | # Python Tools for Visual Studio (PTVS) 302 | __pycache__/ 303 | *.pyc 304 | 305 | # Cake - Uncomment if you are using it 306 | # tools/** 307 | # !tools/packages.config 308 | 309 | # Tabs Studio 310 | *.tss 311 | 312 | # Telerik's JustMock configuration file 313 | *.jmconfig 314 | 315 | # BizTalk build output 316 | *.btp.cs 317 | *.btm.cs 318 | *.odx.cs 319 | *.xsd.cs 320 | 321 | # OpenCover UI analysis results 322 | OpenCover/ 323 | 324 | # Azure Stream Analytics local run output 325 | ASALocalRun/ 326 | 327 | # MSBuild Binary and Structured Log 328 | *.binlog 329 | 330 | # NVidia Nsight GPU debugger configuration file 331 | *.nvuser 332 | 333 | # MFractors (Xamarin productivity tool) working folder 334 | .mfractor/ 335 | 336 | # Local History for Visual Studio 337 | .localhistory/ 338 | 339 | # BeatPulse healthcheck temp database 340 | healthchecksdb -------------------------------------------------------------------------------- /Readme.md: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Brayconn/TheKingsTable/a6e20631ff60a28ff1fbd0d6cba702e147b81973/Readme.md -------------------------------------------------------------------------------- /TheKingsTable.sln: -------------------------------------------------------------------------------- 1 |  2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio Version 17 4 | VisualStudioVersion = 17.1.32414.318 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CaveStoryModdingFramework", "..\CaveStoryModdingFramework\CaveStoryModdingFramework\CaveStoryModdingFramework.csproj", "{00C92507-872A-47C4-A632-6946B0F0935E}" 7 | EndProject 8 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CaveStoryModdingFrameworkTests", "..\CaveStoryModdingFramework\CaveStoryModdingFrameworkTests\CaveStoryModdingFrameworkTests.csproj", "{B4CC6E9D-3860-434D-A2A0-814263286D6E}" 9 | EndProject 10 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "PETools", "..\PETools\PETools\PETools.csproj", "{B3B5FCAC-3F15-4839-BB20-81DDF6047794}" 11 | EndProject 12 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "TheKingsTable", "TheKingsTable\TheKingsTable.csproj", "{2A32BB3E-B1D1-460C-8733-D376EF42EFDC}" 13 | EndProject 14 | Global 15 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 16 | Debug|Any CPU = Debug|Any CPU 17 | Release|Any CPU = Release|Any CPU 18 | EndGlobalSection 19 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 20 | {8150F0E5-0B43-4B09-BFFC-4D0E1F2EFABC}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 21 | {8150F0E5-0B43-4B09-BFFC-4D0E1F2EFABC}.Debug|Any CPU.Build.0 = Debug|Any CPU 22 | {8150F0E5-0B43-4B09-BFFC-4D0E1F2EFABC}.Release|Any CPU.ActiveCfg = Release|Any CPU 23 | {8150F0E5-0B43-4B09-BFFC-4D0E1F2EFABC}.Release|Any CPU.Build.0 = Release|Any CPU 24 | {00C92507-872A-47C4-A632-6946B0F0935E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 25 | {00C92507-872A-47C4-A632-6946B0F0935E}.Debug|Any CPU.Build.0 = Debug|Any CPU 26 | {00C92507-872A-47C4-A632-6946B0F0935E}.Release|Any CPU.ActiveCfg = Release|Any CPU 27 | {00C92507-872A-47C4-A632-6946B0F0935E}.Release|Any CPU.Build.0 = Release|Any CPU 28 | {4582AE75-D1E5-4228-BD5F-F9389DA86EA4}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 29 | {4582AE75-D1E5-4228-BD5F-F9389DA86EA4}.Debug|Any CPU.Build.0 = Debug|Any CPU 30 | {4582AE75-D1E5-4228-BD5F-F9389DA86EA4}.Release|Any CPU.ActiveCfg = Release|Any CPU 31 | {4582AE75-D1E5-4228-BD5F-F9389DA86EA4}.Release|Any CPU.Build.0 = Release|Any CPU 32 | {B3B5FCAC-3F15-4839-BB20-81DDF6047794}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 33 | {B3B5FCAC-3F15-4839-BB20-81DDF6047794}.Debug|Any CPU.Build.0 = Debug|Any CPU 34 | {B3B5FCAC-3F15-4839-BB20-81DDF6047794}.Release|Any CPU.ActiveCfg = Release|Any CPU 35 | {B3B5FCAC-3F15-4839-BB20-81DDF6047794}.Release|Any CPU.Build.0 = Release|Any CPU 36 | {CCF06D34-860E-4F13-BB3C-E46E8FA955A2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 37 | {CCF06D34-860E-4F13-BB3C-E46E8FA955A2}.Debug|Any CPU.Build.0 = Debug|Any CPU 38 | {CCF06D34-860E-4F13-BB3C-E46E8FA955A2}.Release|Any CPU.ActiveCfg = Release|Any CPU 39 | {CCF06D34-860E-4F13-BB3C-E46E8FA955A2}.Release|Any CPU.Build.0 = Release|Any CPU 40 | {2BCB2AFA-27DB-4B92-83F8-02B859F1E1F4}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 41 | {2BCB2AFA-27DB-4B92-83F8-02B859F1E1F4}.Debug|Any CPU.Build.0 = Debug|Any CPU 42 | {2BCB2AFA-27DB-4B92-83F8-02B859F1E1F4}.Release|Any CPU.ActiveCfg = Release|Any CPU 43 | {2BCB2AFA-27DB-4B92-83F8-02B859F1E1F4}.Release|Any CPU.Build.0 = Release|Any CPU 44 | {01D4486A-D72A-421D-842F-AB35EF1CDFBB}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 45 | {01D4486A-D72A-421D-842F-AB35EF1CDFBB}.Debug|Any CPU.Build.0 = Debug|Any CPU 46 | {01D4486A-D72A-421D-842F-AB35EF1CDFBB}.Release|Any CPU.ActiveCfg = Release|Any CPU 47 | {01D4486A-D72A-421D-842F-AB35EF1CDFBB}.Release|Any CPU.Build.0 = Release|Any CPU 48 | {2A32BB3E-B1D1-460C-8733-D376EF42EFDC}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 49 | {2A32BB3E-B1D1-460C-8733-D376EF42EFDC}.Debug|Any CPU.Build.0 = Debug|Any CPU 50 | {2A32BB3E-B1D1-460C-8733-D376EF42EFDC}.Release|Any CPU.ActiveCfg = Release|Any CPU 51 | {2A32BB3E-B1D1-460C-8733-D376EF42EFDC}.Release|Any CPU.Build.0 = Release|Any CPU 52 | {B4CC6E9D-3860-434D-A2A0-814263286D6E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 53 | {B4CC6E9D-3860-434D-A2A0-814263286D6E}.Debug|Any CPU.Build.0 = Debug|Any CPU 54 | {B4CC6E9D-3860-434D-A2A0-814263286D6E}.Release|Any CPU.ActiveCfg = Release|Any CPU 55 | {B4CC6E9D-3860-434D-A2A0-814263286D6E}.Release|Any CPU.Build.0 = Release|Any CPU 56 | EndGlobalSection 57 | GlobalSection(SolutionProperties) = preSolution 58 | HideSolutionNode = FALSE 59 | EndGlobalSection 60 | GlobalSection(ExtensibilityGlobals) = postSolution 61 | SolutionGuid = {890BEC3A-DD97-4A03-A27C-437F20F41290} 62 | EndGlobalSection 63 | EndGlobal 64 | -------------------------------------------------------------------------------- /TheKingsTable/App.axaml: -------------------------------------------------------------------------------- 1 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | -------------------------------------------------------------------------------- /TheKingsTable/App.axaml.cs: -------------------------------------------------------------------------------- 1 | using Avalonia; 2 | using Avalonia.Controls.ApplicationLifetimes; 3 | using Avalonia.Markup.Xaml; 4 | using TheKingsTable.ViewModels; 5 | using TheKingsTable.Views; 6 | 7 | namespace TheKingsTable 8 | { 9 | public class App : Application 10 | { 11 | public override void Initialize() 12 | { 13 | AvaloniaXamlLoader.Load(this); 14 | } 15 | 16 | public override void OnFrameworkInitializationCompleted() 17 | { 18 | if (ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop) 19 | { 20 | desktop.MainWindow = new MainWindow 21 | { 22 | DataContext = new MainWindowViewModel(), 23 | }; 24 | } 25 | 26 | base.OnFrameworkInitializationCompleted(); 27 | } 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /TheKingsTable/Assets/avalonia-logo.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Brayconn/TheKingsTable/a6e20631ff60a28ff1fbd0d6cba702e147b81973/TheKingsTable/Assets/avalonia-logo.ico -------------------------------------------------------------------------------- /TheKingsTable/CommonAvaloniaHandlers.cs: -------------------------------------------------------------------------------- 1 | using Avalonia.Controls; 2 | using ReactiveUI; 3 | using System; 4 | using System.Collections.Generic; 5 | using System.Linq; 6 | using System.Text; 7 | using System.Threading.Tasks; 8 | using MessageBox.Avalonia; 9 | using MessageBox.Avalonia.DTO; 10 | 11 | namespace TheKingsTable 12 | { 13 | public static class CommonAvaloniaHandlers 14 | { 15 | static List PrepareFilters(FileSelection input) 16 | { 17 | var filters = new List(input.Filters.Count / 2); 18 | foreach (var filter in input.Filters) 19 | { 20 | filters.Add(new FileDialogFilter() 21 | { 22 | Name = filter.Item1, 23 | Extensions = new List() { filter.Item2 } 24 | }); 25 | } 26 | return filters; 27 | } 28 | public static async Task ShowOpenFileBrowser(InteractionContext context, Window parent) 29 | { 30 | var o = new OpenFileDialog() 31 | { 32 | Title = context.Input.Title, 33 | AllowMultiple = false, 34 | Directory = context.Input.Start, 35 | }; 36 | o.Filters = PrepareFilters(context.Input); 37 | 38 | var result = await o.ShowAsync(parent); 39 | //cancelling the dialog returns null on windows 40 | //but I think it returns empty array on mac...? 41 | context.SetOutput(result?.Length > 0 ? result[0] : null); 42 | } 43 | 44 | public static async Task ShowSaveFileBrowser(InteractionContext context, Window parent) 45 | { 46 | var s = new SaveFileDialog() 47 | { 48 | Title = context.Input.Title, 49 | Directory = context.Input.Start 50 | }; 51 | s.Filters = PrepareFilters(context.Input); 52 | 53 | var result = await s.ShowAsync(parent); 54 | context.SetOutput(result); 55 | } 56 | 57 | public static async Task ShowFolderBrowser(InteractionContext context, Window parent) 58 | { 59 | var o = new OpenFolderDialog() 60 | { 61 | Title = context.Input.Title, 62 | Directory = context.Input.Start 63 | }; 64 | var result = await o.ShowAsync(parent); 65 | context.SetOutput(result); 66 | } 67 | 68 | public static async Task ShowYesNoMessage(InteractionContext context, Window parent) 69 | { 70 | var m = MessageBoxManager.GetMessageBoxStandardWindow(new MessageBoxStandardParams 71 | { 72 | ButtonDefinitions = MessageBox.Avalonia.Enums.ButtonEnum.YesNo, 73 | ContentTitle = context.Input.Title, 74 | ContentMessage = context.Input.Question 75 | }); 76 | var res = await m.ShowDialog(parent); 77 | context.SetOutput(res == MessageBox.Avalonia.Enums.ButtonResult.Yes); 78 | } 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /TheKingsTable/CommonInteractions.cs: -------------------------------------------------------------------------------- 1 | using ReactiveUI; 2 | using System; 3 | using System.Collections.Generic; 4 | using System.Reactive; 5 | 6 | namespace TheKingsTable 7 | { 8 | public class FileSelection : FolderSelection 9 | { 10 | public List> Filters { get; } 11 | public FileSelection(string title, List> filters, string start = "") : base(title, start) 12 | { 13 | Filters = filters; 14 | } 15 | } 16 | public class FolderSelection 17 | { 18 | public string Title { get; } 19 | public string Start { get; set; } 20 | public FolderSelection(string title, string start = "") 21 | { 22 | Title = title; 23 | Start = start; 24 | } 25 | } 26 | public class Words 27 | { 28 | public string Title { get; } 29 | public string Question { get; } 30 | 31 | public Words(string title, string question) 32 | { 33 | Title = title; 34 | Question = question; 35 | } 36 | } 37 | public static class CommonInteractions 38 | { 39 | public static readonly Interaction BrowseToOpenFile = new Interaction(); 40 | public static readonly Interaction BrowseToSaveFile = new Interaction(); 41 | public static readonly Interaction BrowseForFolder = new Interaction(); 42 | public static readonly Interaction IsOk = new Interaction(); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /TheKingsTable/Controls/BitEditor.axaml: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | 7 | 8 | 24 | 25 | -------------------------------------------------------------------------------- /TheKingsTable/Controls/BitEditor.axaml.cs: -------------------------------------------------------------------------------- 1 | using Avalonia; 2 | using Avalonia.Controls; 3 | using Avalonia.Controls.Metadata; 4 | using Avalonia.Controls.Primitives; 5 | 6 | namespace TheKingsTable.Controls 7 | { 8 | [TemplatePart(Name = PART_BitList, Type = typeof(ListBox))] 9 | public class BitEditor : TemplatedControl 10 | { 11 | public const string PART_BitList = nameof(PART_BitList); 12 | 13 | #region Length Styled Property 14 | public static readonly StyledProperty LengthProperty = 15 | AvaloniaProperty.Register(nameof(Length), 0); 16 | 17 | public int Length 18 | { 19 | get => GetValue(LengthProperty); 20 | set => SetValue(LengthProperty, value); 21 | } 22 | #endregion 23 | 24 | #region Value Styled Property 25 | public static readonly StyledProperty ValueProperty = 26 | AvaloniaProperty.Register(nameof(Value), null); 27 | 28 | public long? Value 29 | { 30 | get => GetValue(ValueProperty); 31 | set => SetValue(ValueProperty, value); 32 | } 33 | #endregion 34 | 35 | public BitEditor() 36 | { 37 | //LengthProperty.Changed.Subscribe(x => UpdateBitAmount(x.NewValue.Value)); 38 | //ValueProperty.Changed.Subscribe(x => UpdateBitValues(x.NewValue.Value)); 39 | } 40 | 41 | ListBox BitList; 42 | protected override void OnApplyTemplate(TemplateAppliedEventArgs e) 43 | { 44 | base.OnApplyTemplate(e); 45 | BitList = e.NameScope.Find(PART_BitList); 46 | } 47 | 48 | public void UpdateBitAmount(int amount) 49 | { 50 | 51 | } 52 | public void UpdateBitValues(long? value) 53 | { 54 | 55 | } 56 | 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /TheKingsTable/Controls/EntityEditor.axaml: -------------------------------------------------------------------------------- 1 | 5 | 6 | 7 | 8 | 9 | 45 | 46 | -------------------------------------------------------------------------------- /TheKingsTable/Controls/EntityEditor.axaml.cs: -------------------------------------------------------------------------------- 1 | using Avalonia; 2 | using Avalonia.Controls; 3 | using Avalonia.Controls.Metadata; 4 | using Avalonia.Controls.Primitives; 5 | using Avalonia.Data; 6 | using System; 7 | 8 | namespace TheKingsTable.Controls 9 | { 10 | [TemplatePart(Name = PART_XEditor, Type = typeof(TextBox))] 11 | [TemplatePart(Name = PART_YEditor, Type = typeof(TextBox))] 12 | [TemplatePart(Name = PART_FlagEditor, Type = typeof(TextBox))] 13 | [TemplatePart(Name = PART_EventEditor, Type = typeof(TextBox))] 14 | [TemplatePart(Name = PART_TypeEditor, Type = typeof(TextBox))] 15 | [TemplatePart(Name = PART_RawBitEditor, Type = typeof(TextBox))] 16 | [TemplatePart(Name = PART_ListBitEditor, Type = typeof(BitEditor))] 17 | public class EntityEditor : TemplatedControl 18 | { 19 | public const string PART_XEditor = nameof(PART_XEditor); 20 | public const string PART_YEditor = nameof(PART_YEditor); 21 | public const string PART_FlagEditor = nameof(PART_FlagEditor); 22 | public const string PART_EventEditor = nameof(PART_EventEditor); 23 | public const string PART_TypeEditor = nameof(PART_TypeEditor); 24 | public const string PART_RawBitEditor = nameof(PART_RawBitEditor); 25 | public const string PART_ListBitEditor = nameof(PART_ListBitEditor); 26 | 27 | TextBox XEditor, YEditor, FlagEditor, EventEditor, TypeEditor, RawBitEditor; 28 | BitEditor ListBitEditor; 29 | 30 | #region X Styled Property 31 | public static readonly StyledProperty XProperty = 32 | AvaloniaProperty.Register(nameof(X), null, defaultBindingMode:BindingMode.TwoWay); 33 | 34 | public short? X 35 | { 36 | get => GetValue(XProperty); 37 | set => SetValue(XProperty, value); 38 | } 39 | #endregion 40 | 41 | #region Y Styled Property 42 | public static readonly StyledProperty YProperty = 43 | AvaloniaProperty.Register(nameof(Y), null, defaultBindingMode: BindingMode.TwoWay); 44 | 45 | public short? Y 46 | { 47 | get => GetValue(YProperty); 48 | set => SetValue(YProperty, value); 49 | } 50 | #endregion 51 | 52 | #region Flag Styled Property 53 | public static readonly StyledProperty FlagProperty = 54 | AvaloniaProperty.Register(nameof(Flag), null, defaultBindingMode: BindingMode.TwoWay); 55 | 56 | public short? Flag 57 | { 58 | get => GetValue(FlagProperty); 59 | set => SetValue(FlagProperty, value); 60 | } 61 | #endregion 62 | 63 | #region Event Styled Property 64 | public static readonly StyledProperty EventProperty = 65 | AvaloniaProperty.Register(nameof(Event), null, defaultBindingMode: BindingMode.TwoWay); 66 | 67 | public short? Event 68 | { 69 | get => GetValue(EventProperty); 70 | set => SetValue(EventProperty, value); 71 | } 72 | #endregion 73 | 74 | #region Type Styled Property 75 | public static readonly StyledProperty TypeProperty = 76 | AvaloniaProperty.Register(nameof(Type), null, defaultBindingMode: BindingMode.TwoWay); 77 | 78 | public short? Type 79 | { 80 | get => GetValue(TypeProperty); 81 | set => SetValue(TypeProperty, value); 82 | } 83 | #endregion 84 | 85 | #region Bits Styled Property 86 | public static readonly StyledProperty BitsProperty = 87 | AvaloniaProperty.Register(nameof(Bits), null, defaultBindingMode: BindingMode.TwoWay); 88 | 89 | public short? Bits 90 | { 91 | get => GetValue(BitsProperty); 92 | set => SetValue(BitsProperty, value); 93 | } 94 | #endregion 95 | 96 | Control lastFocused; 97 | Control LastFocused 98 | { 99 | get => lastFocused; 100 | set 101 | { 102 | if(value != LastFocused) 103 | { 104 | if (lastFocused == XEditor) 105 | SetIfDifferent(XProperty, GetNumber(XEditor.Text)); 106 | else if (LastFocused == YEditor) 107 | SetIfDifferent(YProperty, GetNumber(YEditor.Text)); 108 | else if (LastFocused == FlagEditor) 109 | SetIfDifferent(FlagProperty, GetFlag(FlagEditor.Text)); 110 | else if (LastFocused == EventEditor) 111 | SetIfDifferent(EventProperty, short.TryParse(EventEditor.Text, out var e) ? e : null); 112 | else if (LastFocused == TypeEditor) 113 | SetIfDifferent(TypeProperty, GetEntityType(TypeEditor.Text)); 114 | else if(LastFocused == RawBitEditor) 115 | { 116 | if (short.TryParse(RawBitEditor.Text, out var b)) 117 | ListBitEditor.Value = cachedBits = b; 118 | if (value != ListBitEditor) 119 | SetIfDifferent(BitsProperty, cachedBits); 120 | } 121 | else if(LastFocused == ListBitEditor) 122 | { 123 | RawBitEditor.Text = (cachedBits = (short)ListBitEditor.Value).ToString(); 124 | if (value != RawBitEditor) 125 | SetIfDifferent(BitsProperty, cachedBits); 126 | } 127 | lastFocused = value; 128 | } 129 | } 130 | } 131 | 132 | void SetIfDifferent(StyledProperty prop, short? value) 133 | { 134 | if(value != null && GetValue(prop) != value) 135 | SetValue(prop, value); 136 | } 137 | 138 | short? GetNumber(string text) 139 | { 140 | return short.TryParse(text, out var num) ? num : null; 141 | } 142 | //TODO get from project 143 | short? GetFlag(string text) 144 | { 145 | return short.TryParse(text, out var flag) ? flag : null; 146 | } 147 | //TODO get from project 148 | private short? GetEntityType(string text) 149 | { 150 | return short.TryParse(text, out var type) ? Type : null; 151 | } 152 | 153 | short cachedBits = 0; 154 | protected override void OnApplyTemplate(TemplateAppliedEventArgs e) 155 | { 156 | base.OnApplyTemplate(e); 157 | XEditor = e.NameScope.Find(PART_XEditor); 158 | YEditor = e.NameScope.Find(PART_YEditor); 159 | FlagEditor = e.NameScope.Find(PART_FlagEditor); 160 | EventEditor = e.NameScope.Find(PART_EventEditor); 161 | TypeEditor = e.NameScope.Find(PART_TypeEditor); 162 | RawBitEditor = e.NameScope.Find(PART_RawBitEditor); 163 | ListBitEditor = e.NameScope.Find(PART_ListBitEditor); 164 | 165 | XEditor.GotFocus += UpdateFocus; 166 | YEditor.GotFocus += UpdateFocus; 167 | FlagEditor.GotFocus += UpdateFocus; 168 | EventEditor.GotFocus += UpdateFocus; 169 | TypeEditor.GotFocus += UpdateFocus; 170 | RawBitEditor.GotFocus += UpdateFocus; 171 | ListBitEditor.GotFocus += UpdateFocus; 172 | 173 | XEditor.LostFocus += UpdateFocus; 174 | YEditor.LostFocus += UpdateFocus; 175 | FlagEditor.LostFocus += UpdateFocus; 176 | EventEditor.LostFocus += UpdateFocus; 177 | TypeEditor.LostFocus += UpdateFocus; 178 | RawBitEditor.LostFocus += UpdateFocus; 179 | ListBitEditor.LostFocus += UpdateFocus; 180 | 181 | XProperty.Changed.Subscribe(x => XEditor.Text = x.NewValue.Value.ToString()); 182 | YProperty.Changed.Subscribe(x => YEditor.Text = x.NewValue.Value.ToString()); 183 | FlagProperty.Changed.Subscribe(x => FlagEditor.Text = x.NewValue.Value.ToString()); //TODO convert to proper value 184 | EventProperty.Changed.Subscribe(x => EventEditor.Text = x.NewValue.ToString()); 185 | TypeProperty.Changed.Subscribe(x => TypeEditor.Text = x.NewValue.Value.ToString()); 186 | BitsProperty.Changed.Subscribe(x => 187 | { 188 | RawBitEditor.Text = x.NewValue.Value.ToString(); 189 | ListBitEditor.Value = x.NewValue.Value; 190 | }); 191 | } 192 | 193 | private void UpdateFocus(object? sender, EventArgs e) 194 | { 195 | if (sender is Control c) 196 | LastFocused = c; 197 | } 198 | } 199 | } 200 | -------------------------------------------------------------------------------- /TheKingsTable/Controls/EntitySelector.axaml: -------------------------------------------------------------------------------- 1 | 5 | 6 | 7 | 8 | 9 | 33 | 34 | -------------------------------------------------------------------------------- /TheKingsTable/Controls/EntitySelector.axaml.cs: -------------------------------------------------------------------------------- 1 | using Avalonia; 2 | using Avalonia.Controls; 3 | using Avalonia.Controls.Metadata; 4 | using Avalonia.Controls.Primitives; 5 | using Avalonia.Interactivity; 6 | using CaveStoryModdingFramework.Entities; 7 | using System.Collections.Generic; 8 | using System.Linq; 9 | 10 | namespace TheKingsTable.Controls 11 | { 12 | [TemplatePart(Name = PART_EntityList, Type = typeof(ListBox))] 13 | public class EntitySelector : TemplatedControl 14 | { 15 | public const string PART_EntityList = nameof(PART_EntityList); 16 | 17 | #region SelectedEntity Styled Property 18 | public static readonly StyledProperty SelectedEntityProperty = 19 | AvaloniaProperty.Register(nameof(SelectedEntity), 0, defaultBindingMode:Avalonia.Data.BindingMode.TwoWay); 20 | 21 | public short SelectedEntity 22 | { 23 | get => GetValue(SelectedEntityProperty); 24 | set => SetValue(SelectedEntityProperty, value); 25 | } 26 | #endregion 27 | 28 | #region Entities Styled Property 29 | public static readonly StyledProperty?> EntitiesProperty = 30 | AvaloniaProperty.Register?>(nameof(Entities), null); 31 | 32 | public Dictionary? Entities 33 | { 34 | get => GetValue(EntitiesProperty); 35 | set => SetValue(EntitiesProperty, value); 36 | } 37 | #endregion 38 | 39 | ListBox EntityList; 40 | protected override void OnApplyTemplate(TemplateAppliedEventArgs e) 41 | { 42 | base.OnApplyTemplate(e); 43 | EntityList = e.NameScope.Find(PART_EntityList); 44 | EntityList.SelectionChanged += SelectionChanged; 45 | } 46 | 47 | void SelectionChanged(object? sender, SelectionChangedEventArgs e) 48 | { 49 | if(Entities != null && e.AddedItems.Count > 0 && e.AddedItems[0] is KeyValuePair ent) 50 | { 51 | SelectedEntity = (short)ent.Key; 52 | //SelectedEntity = (short)Entities.First(x => x.Value == ent).Key; 53 | } 54 | } 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /TheKingsTable/Controls/FileSystemSelector.axaml: -------------------------------------------------------------------------------- 1 | 8 | 9 | 16 | 17 | -------------------------------------------------------------------------------- /TheKingsTable/Controls/FileSystemSelector.axaml.cs: -------------------------------------------------------------------------------- 1 | using Avalonia; 2 | using Avalonia.Controls; 3 | using Avalonia.Interactivity; 4 | using Avalonia.Markup.Xaml; 5 | using System; 6 | using System.Collections.Generic; 7 | using System.IO; 8 | using System.Reactive.Linq; 9 | 10 | namespace TheKingsTable.Controls 11 | { 12 | public partial class FileSystemSelector : UserControl 13 | { 14 | #region Text Styled Property 15 | public static readonly StyledProperty TextProperty = 16 | //TextBox.TextProperty.AddOwner(); 17 | AvaloniaProperty.Register(nameof(Text)); 18 | public string Text { get => GetValue(TextProperty); set => SetValue(TextProperty, value); } 19 | #endregion 20 | 21 | #region StartDirectory Styled Property 22 | public static readonly StyledProperty StartDirectoryProperty = 23 | AvaloniaProperty.Register(nameof(StartDirectory)); 24 | public string StartDirectory { get => GetValue(StartDirectoryProperty); set => SetValue(StartDirectoryProperty, value); } 25 | #endregion 26 | 27 | #region Watermark Styled Property 28 | public static readonly StyledProperty WatermarkProperty = 29 | TextBox.WatermarkProperty.AddOwner(); 30 | //AvaloniaProperty.Register(nameof(Watermark)); 31 | public string Watermark { get => GetValue(WatermarkProperty); set => SetValue(WatermarkProperty, value); } 32 | #endregion 33 | 34 | #region Description Styled Property 35 | public static readonly StyledProperty DescriptionProperty = 36 | AvaloniaProperty.Register(nameof(Description)); 37 | public string Description { get => GetValue(DescriptionProperty); set => SetValue(DescriptionProperty, value); } 38 | #endregion 39 | 40 | #region WindowTitle Styled Property 41 | public static readonly StyledProperty WindowTitleProperty = 42 | AvaloniaProperty.Register(nameof(WindowTitle)); 43 | public string WindowTitle { get => GetValue(WindowTitleProperty); set => SetValue(WindowTitleProperty, value); } 44 | #endregion 45 | 46 | #region Filters Styled Properties 47 | public static readonly StyledProperty FiltersProperty = 48 | AvaloniaProperty.Register(nameof(Filters)); 49 | public string Filters { get => GetValue(FiltersProperty); set => SetValue(FiltersProperty, value); } 50 | #endregion 51 | 52 | #region IsSave Styled Property 53 | public static readonly StyledProperty IsSaveProperty = 54 | AvaloniaProperty.Register(nameof(IsSave), false); 55 | 56 | public bool IsSave 57 | { 58 | get => GetValue(IsSaveProperty); 59 | set => SetValue(IsSaveProperty, value); 60 | } 61 | #endregion 62 | 63 | public FileSystemSelector() 64 | { 65 | InitializeComponent(); 66 | } 67 | 68 | private void InitializeComponent() 69 | { 70 | AvaloniaXamlLoader.Load(this); 71 | } 72 | 73 | public async void OnBrowse(object sender, RoutedEventArgs e) 74 | { 75 | string? result = null; 76 | 77 | string start; 78 | if (!string.IsNullOrWhiteSpace(Text)) 79 | start = Path.GetDirectoryName(Text) ?? ""; 80 | else 81 | start = StartDirectory; 82 | 83 | //filters means we're finding files 84 | if (!string.IsNullOrEmpty(Filters)) 85 | { 86 | var splitFilters = Filters.Split("|"); 87 | var formattedFilters = new List>(splitFilters.Length / 2); 88 | for (int i = 0; i < splitFilters.Length; i += 2) 89 | formattedFilters.Add(new Tuple(splitFilters[i], splitFilters[i + 1])); 90 | 91 | var interaction = IsSave ? CommonInteractions.BrowseToSaveFile : CommonInteractions.BrowseToOpenFile; 92 | 93 | result = await interaction.Handle(new FileSelection(WindowTitle, formattedFilters, start)); 94 | } 95 | //none means we're finding directories 96 | else 97 | { 98 | result = await CommonInteractions.BrowseForFolder 99 | .Handle(new FolderSelection(WindowTitle, start)); 100 | } 101 | if (result != null) 102 | { 103 | Text = result; 104 | } 105 | } 106 | } 107 | } 108 | -------------------------------------------------------------------------------- /TheKingsTable/Controls/StageEditorToolMenu.cs: -------------------------------------------------------------------------------- 1 | using Avalonia; 2 | using Avalonia.Controls; 3 | using Avalonia.Controls.Metadata; 4 | using Avalonia.Controls.Primitives; 5 | using Avalonia.Data; 6 | using Avalonia.Styling; 7 | using System; 8 | using System.Collections.Generic; 9 | using System.Linq; 10 | using System.Text; 11 | using System.Threading.Tasks; 12 | using TheKingsTable.ViewModels.Editors; 13 | 14 | namespace TheKingsTable.Controls 15 | { 16 | [TemplatePart(Name = PART_Tabs, Type = typeof(TabControl))] 17 | public class StageEditorToolMenu : TemplatedControl, IStyleable 18 | { 19 | public const string PART_Tabs = nameof(PART_Tabs); 20 | 21 | Type IStyleable.StyleKey => typeof(StageEditorToolMenu); 22 | 23 | #region StageEditor Styled Property 24 | public static readonly StyledProperty StageEditorProperty = 25 | AvaloniaProperty.Register(nameof(StageEditor), null); 26 | 27 | public StageEditorViewModel? StageEditor 28 | { 29 | get => GetValue(StageEditorProperty); 30 | set => SetValue(StageEditorProperty, value); 31 | } 32 | #endregion 33 | 34 | #region EditorMode Styled Property 35 | public static readonly StyledProperty EditorModeProperty = 36 | AvaloniaProperty.Register(nameof(EditorMode), StageEditorViewModel.Editors.Tile, defaultBindingMode:BindingMode.TwoWay); 37 | 38 | public StageEditorViewModel.Editors EditorMode 39 | { 40 | get => GetValue(EditorModeProperty); 41 | set => SetValue(EditorModeProperty, value); 42 | } 43 | #endregion 44 | 45 | #region TabType Attached Property 46 | public static readonly AttachedProperty TabTypeProperty 47 | = AvaloniaProperty.RegisterAttached( 48 | "TabType", null); 49 | 50 | public static void SetTabType(AvaloniaObject element, StageEditorViewModel.Editors? value) 51 | { 52 | element.SetValue(TabTypeProperty, value); 53 | } 54 | 55 | public static StageEditorViewModel.Editors? GetTabType(AvaloniaObject element) 56 | { 57 | return element.GetValue(TabTypeProperty); 58 | } 59 | #endregion 60 | 61 | TabControl Tabs; 62 | protected override void OnApplyTemplate(TemplateAppliedEventArgs e) 63 | { 64 | base.OnApplyTemplate(e); 65 | Tabs = e.NameScope.Find(PART_Tabs); 66 | Tabs.SelectionChanged += Tabs_SelectionChanged; 67 | } 68 | 69 | private void Tabs_SelectionChanged(object? sender, SelectionChangedEventArgs e) 70 | { 71 | if(e.AddedItems.Count > 0 && e.AddedItems[0] is AvaloniaObject a) 72 | { 73 | var t = GetTabType(a); 74 | if (t != null) 75 | EditorMode = t.Value; 76 | } 77 | } 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /TheKingsTable/Controls/StageEditorToolMenuStyles.axaml: -------------------------------------------------------------------------------- 1 |  4 | 5 | 6 | 7 | 8 | 9 | 10 | 49 | 50 | -------------------------------------------------------------------------------- /TheKingsTable/Controls/StageTableLocationEditor.axaml: -------------------------------------------------------------------------------- 1 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /TheKingsTable/Controls/StageTableLocationEditor.axaml.cs: -------------------------------------------------------------------------------- 1 | using Avalonia; 2 | using Avalonia.Controls; 3 | using Avalonia.Markup.Xaml; 4 | 5 | namespace TheKingsTable.Controls 6 | { 7 | public partial class StageTableLocationEditor : UserControl 8 | { 9 | public StageTableLocationEditor() 10 | { 11 | InitializeComponent(); 12 | } 13 | 14 | private void InitializeComponent() 15 | { 16 | AvaloniaXamlLoader.Load(this); 17 | } 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /TheKingsTable/Controls/TextEditorWrapper.axaml: -------------------------------------------------------------------------------- 1 | 5 | 6 | 7 | 8 | 9 | 16 | 17 | -------------------------------------------------------------------------------- /TheKingsTable/Controls/TextEditorWrapper.axaml.cs: -------------------------------------------------------------------------------- 1 | using Avalonia; 2 | using Avalonia.Controls; 3 | using Avalonia.Controls.Metadata; 4 | using Avalonia.Controls.Primitives; 5 | using AvaloniaEdit; 6 | 7 | namespace TheKingsTable.Controls 8 | { 9 | [TemplatePart(Name = PART_TextEditor, Type = typeof(TextEditor))] 10 | public class TextEditorWrapper : TemplatedControl 11 | { 12 | public const string PART_TextEditor = nameof(PART_TextEditor); 13 | 14 | #region Text Direct Property 15 | public static readonly DirectProperty TextProperty = 16 | AvaloniaProperty.RegisterDirect(nameof(Text), 17 | o => o.Text, (o,e) => o.Text = e, defaultBindingMode:Avalonia.Data.BindingMode.TwoWay); 18 | 19 | //This only exists because binding takes place before the TextEditor is set 20 | string backupTextQueue = ""; 21 | public string Text 22 | { 23 | get => TextEditor?.Text ?? backupTextQueue; 24 | private set 25 | { 26 | if(TextEditor == null) 27 | { 28 | backupTextQueue = value; 29 | } 30 | else if(value != TextEditor.Text) 31 | { 32 | var old = TextEditor.Text; 33 | TextEditor.Text = value; 34 | RaisePropertyChanged(TextProperty, old, Text); 35 | } 36 | } 37 | } 38 | #endregion 39 | 40 | TextEditor? TextEditor; 41 | protected override void OnApplyTemplate(TemplateAppliedEventArgs e) 42 | { 43 | base.OnApplyTemplate(e); 44 | TextEditor = e.NameScope.Find(PART_TextEditor); 45 | TextEditor.TextChanged += (o, e) => this.RaisePropertyChanged(TextProperty, Text, Text); 46 | Text = backupTextQueue; 47 | backupTextQueue = ""; 48 | } 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /TheKingsTable/Controls/TileMouseHelper.cs: -------------------------------------------------------------------------------- 1 | using Avalonia; 2 | using Avalonia.Controls; 3 | using Avalonia.Input; 4 | using System; 5 | using System.Windows.Input; 6 | 7 | namespace TheKingsTable.Controls 8 | { 9 | public class TileEventArgs : EventArgs 10 | { 11 | public int X { get; } 12 | public int Y { get; } 13 | public PointerUpdateKind? Pressed { get; } 14 | 15 | public TileEventArgs(int x, int y, PointerUpdateKind? pressed = null) 16 | { 17 | X = x; 18 | Y = y; 19 | Pressed = pressed; 20 | } 21 | } 22 | public abstract class TileMouseHelper : Control 23 | { 24 | static protected readonly AvaloniaProperty[] Properties; 25 | static TileMouseHelper() 26 | { 27 | //needed to use the static constructor 28 | //to avoid complaints about null 29 | Properties = new AvaloniaProperty[] 30 | { 31 | TileSizeProperty, 32 | SelectionStartXProperty, 33 | SelectionStartYProperty, 34 | SelectionEndXProperty, 35 | SelectionEndYProperty, 36 | ScaleProperty 37 | }; 38 | } 39 | 40 | #region TileSize Styled Property 41 | public static readonly StyledProperty TileSizeProperty = 42 | AvaloniaProperty.Register(nameof(TileSize), 16); 43 | 44 | public int TileSize 45 | { 46 | get => GetValue(TileSizeProperty); 47 | set => SetValue(TileSizeProperty, value); 48 | } 49 | #endregion 50 | 51 | #region SelectionStartX Styled Property 52 | public static readonly StyledProperty SelectionStartXProperty = 53 | AvaloniaProperty.Register(nameof(SelectionStartX), -1); 54 | 55 | public int SelectionStartX 56 | { 57 | get => GetValue(SelectionStartXProperty); 58 | set => SetValue(SelectionStartXProperty, value); 59 | } 60 | #endregion 61 | 62 | #region SelectionStartY Styled Property 63 | public static readonly StyledProperty SelectionStartYProperty = 64 | AvaloniaProperty.Register(nameof(SelectionStartY), -1); 65 | 66 | public int SelectionStartY 67 | { 68 | get => GetValue(SelectionStartYProperty); 69 | set => SetValue(SelectionStartYProperty, value); 70 | } 71 | #endregion 72 | 73 | #region SelectionEndX Styled Property 74 | public static readonly StyledProperty SelectionEndXProperty = 75 | AvaloniaProperty.Register(nameof(SelectionEndX), -1); 76 | 77 | public int SelectionEndX 78 | { 79 | get => GetValue(SelectionEndXProperty); 80 | set => SetValue(SelectionEndXProperty, value); 81 | } 82 | #endregion 83 | 84 | #region SelectionEndY Styled Property 85 | public static readonly StyledProperty SelectionEndYProperty = 86 | AvaloniaProperty.Register(nameof(SelectionEndY), -1); 87 | 88 | public int SelectionEndY 89 | { 90 | get => GetValue(SelectionEndYProperty); 91 | set => SetValue(SelectionEndYProperty, value); 92 | } 93 | #endregion 94 | 95 | #region Scale Styled Property 96 | public static readonly StyledProperty ScaleProperty = 97 | AvaloniaProperty.Register(nameof(Scale), 1); 98 | 99 | public double Scale 100 | { 101 | get => GetValue(ScaleProperty); 102 | set => SetValue(ScaleProperty, value); 103 | } 104 | #endregion 105 | 106 | protected double ZoomMin = 0.25, ZoomStep = 0.25, ZoomMax = 10; 107 | 108 | int TileX = -1, TileY = -1; 109 | TileEventArgs CurrentPos => new TileEventArgs(TileX, TileY); 110 | protected Rect GetTileRect(byte tile, int width) 111 | { 112 | return new Rect((tile % width) * TileSize, (tile / width) * TileSize, TileSize, TileSize); 113 | } 114 | protected (int, int) PointToTile(Point p) 115 | { 116 | return ((int)(p.X / (TileSize * Scale)), 117 | (int)(p.Y / (TileSize * Scale))); 118 | } 119 | 120 | public TileMouseHelper() 121 | { 122 | PointerEnter += OnPointerEnter; 123 | PointerMoved += OnPointerMoved; 124 | PointerWheelChanged += OnPointerWheelChanged; 125 | 126 | PointerPressed += OnPointerPressed; 127 | PointerReleased += OnPointerReleased; 128 | 129 | PointerLeave += OnPointerLeave; 130 | PointerCaptureLost += OnPointerCaptureLost; 131 | 132 | this.AddHandler(Control.KeyDownEvent, OnKeyDown); 133 | //KeyDown += OnKeyDown; 134 | Focusable = true; 135 | } 136 | ~TileMouseHelper() 137 | { 138 | PointerEnter -= OnPointerEnter; 139 | PointerMoved -= OnPointerMoved; 140 | PointerWheelChanged -= OnPointerWheelChanged; 141 | 142 | PointerPressed -= OnPointerPressed; 143 | PointerReleased -= OnPointerReleased; 144 | 145 | PointerLeave -= OnPointerLeave; 146 | PointerCaptureLost -= OnPointerCaptureLost; 147 | 148 | this.RemoveHandler(Control.KeyUpEvent, OnKeyDown); 149 | //KeyDown -= OnKeyDown; 150 | } 151 | public event EventHandler? PointerEnterTile, PointerWheelChangedTile, PointerMovedTile, PointerPressedTile, PointerReleasedTile; 152 | private void OnPointerEnter(object? sender, PointerEventArgs e) 153 | { 154 | (TileX, TileY) = PointToTile(e.GetPosition(this)); 155 | PointerEnterTile?.Invoke(sender, CurrentPos); 156 | pointerEnterCommand?.Execute(CurrentPos); 157 | } 158 | protected bool StepZoom(double step) 159 | { 160 | if (step != 0) 161 | { 162 | if (step > 0) 163 | Scale += ZoomStep; 164 | else if (step < 0) 165 | Scale -= ZoomStep; 166 | 167 | if (Scale < ZoomMin) 168 | Scale = ZoomMin; 169 | else if (Scale > ZoomMax) 170 | Scale = ZoomMax; 171 | 172 | return true; 173 | } 174 | return false; 175 | } 176 | private void OnPointerWheelChanged(object? sender, PointerWheelEventArgs e) 177 | { 178 | if (e.KeyModifiers == KeyModifiers.Control) 179 | { 180 | e.Handled = StepZoom(e.Delta.Y); 181 | } 182 | } 183 | private void OnPointerMoved(object? sender, PointerEventArgs e) 184 | { 185 | (var x, var y) = PointToTile(e.GetPosition(this)); 186 | if (x != TileX || y != TileY) 187 | { 188 | TileX = x; 189 | TileY = y; 190 | PointerMovedTile?.Invoke(sender, CurrentPos); 191 | pointerMovedCommand?.Execute(CurrentPos); 192 | } 193 | } 194 | private void OnPointerPressed(object? sender, PointerPressedEventArgs e) 195 | { 196 | //certain circumstances (such as opening the context menu, then clicking again) 197 | //cause the cursor position to desync at this point, so we run the move code again just in case 198 | OnPointerMoved(this, e); 199 | var args = new TileEventArgs(TileX, TileY, e.GetCurrentPoint(this).Properties.PointerUpdateKind); 200 | PointerPressedTile?.Invoke(sender, args); 201 | pointerPressedCommand?.Execute(args); 202 | } 203 | private void OnPointerReleased(object? sender, PointerReleasedEventArgs e) 204 | { 205 | var args = new TileEventArgs(TileX, TileY, e.GetCurrentPoint(this).Properties.PointerUpdateKind); 206 | PointerReleasedTile?.Invoke(sender, args); 207 | pointerReleasedCommand?.Execute(args); 208 | } 209 | private void OnPointerLeave(object? sender, PointerEventArgs e) 210 | { 211 | TileX = -1; 212 | TileY = -1; 213 | pointerLeaveCommand?.Execute(null); 214 | } 215 | private void OnPointerCaptureLost(object? sender, PointerCaptureLostEventArgs e) 216 | { 217 | TileX = -1; 218 | TileY = -1; 219 | pointerCaptureLostCommand?.Execute(null); 220 | } 221 | 222 | #region PointerEnterCommand Direct Property 223 | public static readonly DirectProperty PointerEnterCommandProperty = 224 | AvaloniaProperty.RegisterDirect(nameof(PointerEnterCommand), 225 | o => o.PointerEnterCommand, (o, v) => o.PointerEnterCommand = v); 226 | 227 | ICommand? pointerEnterCommand; 228 | public ICommand? PointerEnterCommand 229 | { 230 | get => pointerEnterCommand; 231 | private set => SetAndRaise(PointerEnterCommandProperty, ref pointerEnterCommand, value); 232 | } 233 | #endregion 234 | 235 | #region PointerMovedCommand Direct Property 236 | public static readonly DirectProperty PointerMovedCommandProperty = 237 | AvaloniaProperty.RegisterDirect(nameof(PointerMovedCommand), 238 | o => o.PointerMovedCommand, (o, v) => o.PointerMovedCommand = v); 239 | 240 | ICommand? pointerMovedCommand; 241 | public ICommand? PointerMovedCommand 242 | { 243 | get => pointerMovedCommand; 244 | private set => SetAndRaise(PointerMovedCommandProperty, ref pointerMovedCommand, value); 245 | } 246 | #endregion 247 | 248 | #region PointerWheelChangedCommand Direct Property 249 | public static readonly DirectProperty PointerWheelChangedCommandProperty = 250 | AvaloniaProperty.RegisterDirect(nameof(PointerWheelChangedCommand), 251 | o => o.PointerWheelChangedCommand, (o, v) => o.PointerWheelChangedCommand = v); 252 | 253 | ICommand? pointerWheelChangedCommand; 254 | public ICommand? PointerWheelChangedCommand 255 | { 256 | get => pointerWheelChangedCommand; 257 | private set => SetAndRaise(PointerWheelChangedCommandProperty, ref pointerWheelChangedCommand, value); 258 | } 259 | #endregion 260 | 261 | #region PointerPressedCommand Direct Property 262 | public static readonly DirectProperty PointerPressedCommandProperty = 263 | AvaloniaProperty.RegisterDirect(nameof(PointerPressedCommand), 264 | o => o.PointerPressedCommand, (o, v) => o.PointerPressedCommand = v); 265 | 266 | ICommand? pointerPressedCommand; 267 | public ICommand? PointerPressedCommand 268 | { 269 | get => pointerPressedCommand; 270 | private set => SetAndRaise(PointerPressedCommandProperty, ref pointerPressedCommand, value); 271 | } 272 | #endregion 273 | 274 | #region PointerReleasedCommand Direct Property 275 | public static readonly DirectProperty PointerReleasedCommandProperty = 276 | AvaloniaProperty.RegisterDirect(nameof(PointerReleasedCommand), 277 | o => o.PointerReleasedCommand, (o, v) => o.PointerReleasedCommand = v); 278 | 279 | ICommand? pointerReleasedCommand; 280 | public ICommand? PointerReleasedCommand 281 | { 282 | get => pointerReleasedCommand; 283 | private set => SetAndRaise(PointerReleasedCommandProperty, ref pointerReleasedCommand, value); 284 | } 285 | #endregion 286 | 287 | #region PointerLeaveCommand Direct Property 288 | public static readonly DirectProperty PointerLeaveCommandProperty = 289 | AvaloniaProperty.RegisterDirect(nameof(PointerLeaveCommand), 290 | o => o.PointerLeaveCommand, (o, v) => o.PointerLeaveCommand = v); 291 | 292 | ICommand? pointerLeaveCommand; 293 | public ICommand? PointerLeaveCommand 294 | { 295 | get => pointerLeaveCommand; 296 | private set => SetAndRaise(PointerLeaveCommandProperty, ref pointerLeaveCommand, value); 297 | } 298 | #endregion 299 | 300 | #region PointerCaptureLostCommand Direct Property 301 | public static readonly DirectProperty PointerCaptureLostCommandProperty = 302 | AvaloniaProperty.RegisterDirect(nameof(PointerCaptureLostCommand), 303 | o => o.PointerCaptureLostCommand, (o, v) => o.PointerCaptureLostCommand = v); 304 | 305 | ICommand? pointerCaptureLostCommand; 306 | public ICommand? PointerCaptureLostCommand 307 | { 308 | get => pointerCaptureLostCommand; 309 | private set => SetAndRaise(PointerCaptureLostCommandProperty, ref pointerCaptureLostCommand, value); 310 | } 311 | #endregion 312 | 313 | private void OnKeyDown(object? sender, KeyEventArgs e) 314 | { 315 | if(e.KeyModifiers == KeyModifiers.Control) 316 | { 317 | switch (e.Key) 318 | { 319 | case Key.OemPlus: 320 | case Key.Add: 321 | e.Handled = StepZoom(1); 322 | break; 323 | case Key.OemMinus: 324 | case Key.Subtract: 325 | e.Handled = StepZoom(-1); 326 | break; 327 | } 328 | } 329 | keyDownCommand?.Execute(e); 330 | } 331 | 332 | #region KeyDownCommand Direct Property 333 | public static readonly DirectProperty KeyDownCommandProperty = 334 | AvaloniaProperty.RegisterDirect(nameof(KeyDownCommand), 335 | o => o.KeyDownCommand, (o,v) => o.KeyDownCommand = v); 336 | 337 | ICommand? keyDownCommand; 338 | public ICommand? KeyDownCommand 339 | { 340 | get => keyDownCommand; 341 | private set => SetAndRaise(KeyDownCommandProperty, ref keyDownCommand, value); 342 | } 343 | #endregion 344 | } 345 | } 346 | -------------------------------------------------------------------------------- /TheKingsTable/Controls/TileSelectionEditor.axaml: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | 7 | 8 | 33 | 34 | -------------------------------------------------------------------------------- /TheKingsTable/Controls/TileSelectionEditor.axaml.cs: -------------------------------------------------------------------------------- 1 | using Avalonia; 2 | using Avalonia.Controls; 3 | using Avalonia.Controls.Metadata; 4 | using Avalonia.Controls.Primitives; 5 | using Avalonia.Media.Imaging; 6 | using CaveStoryModdingFramework.Editors; 7 | using CaveStoryModdingFramework.Maps; 8 | using static TheKingsTable.Utilities; 9 | 10 | namespace TheKingsTable.Controls 11 | { 12 | [TemplatePart(Name = PART_SelectionViewer, Type = typeof(StageRenderer))] 13 | [TemplatePart(Name = PART_TilesetViewer, Type = typeof(TilesetViewer))] 14 | public partial class TileSelectionEditor : TemplatedControl 15 | { 16 | public const string PART_SelectionViewer = nameof(PART_SelectionViewer); 17 | public const string PART_TilesetViewer = nameof(PART_TilesetViewer); 18 | 19 | #region ShowTileTypes Styled Property 20 | public static readonly StyledProperty ShowTileTypesProperty = 21 | AvaloniaProperty.Register(nameof(ShowTileTypes), false); 22 | 23 | public bool ShowTileTypes 24 | { 25 | get => GetValue(ShowTileTypesProperty); 26 | set => SetValue(ShowTileTypesProperty, value); 27 | } 28 | #endregion 29 | 30 | #region TheTileSelection Styled Property 31 | public static readonly StyledProperty TheTileSelectionProperty = 32 | AvaloniaProperty.Register(nameof(TheTileSelection), null); 33 | 34 | public TileSelection? TheTileSelection 35 | { 36 | get => GetValue(TheTileSelectionProperty); 37 | set => SetValue(TheTileSelectionProperty, value); 38 | } 39 | #endregion 40 | 41 | #region TileTypesImage Styled Property 42 | public static readonly StyledProperty TileTypesImageProperty = 43 | AvaloniaProperty.Register(nameof(TileTypesImage), null); 44 | 45 | public Bitmap? TileTypesImage 46 | { 47 | get => GetValue(TileTypesImageProperty); 48 | set => SetValue(TileTypesImageProperty, value); 49 | } 50 | #endregion 51 | 52 | #region TilesetAttributes Styled Property 53 | public static readonly StyledProperty TilesetAttributesProperty = 54 | AvaloniaProperty.Register(nameof(TilesetAttributes), null); 55 | 56 | public Attribute? TilesetAttributes 57 | { 58 | get => GetValue(TilesetAttributesProperty); 59 | set => SetValue(TilesetAttributesProperty, value); 60 | } 61 | #endregion 62 | 63 | #region TilesetImage Styled Property 64 | public static readonly StyledProperty TilesetImageProperty = 65 | AvaloniaProperty.Register(nameof(TilesetImage), null); 66 | 67 | public Bitmap? TilesetImage 68 | { 69 | get => GetValue(TilesetImageProperty); 70 | set => SetValue(TilesetImageProperty, value); 71 | } 72 | #endregion 73 | 74 | StageRenderer SelectionViewer; 75 | TilesetViewer TilesetView; 76 | protected override void OnApplyTemplate(TemplateAppliedEventArgs e) 77 | { 78 | base.OnApplyTemplate(e); 79 | SelectionViewer = e.NameScope.Find(PART_SelectionViewer); 80 | TilesetView = e.NameScope.Find(PART_TilesetViewer); 81 | 82 | SelectionViewer.PointerPressedTile += SelectionViewer_PointerPressedTile; 83 | 84 | TilesetView.PointerPressedTile += TilesetView_PointerPressedTile; 85 | TilesetView.PointerMovedTile += TilesetView_PointerMovedTile; 86 | TilesetView.PointerReleasedTile += TilesetView_PointerReleasedTile; 87 | } 88 | 89 | private void SelectionViewer_PointerPressedTile(object? sender, TileEventArgs e) 90 | { 91 | if (TheTileSelection != null && e.Pressed == Avalonia.Input.PointerUpdateKind.LeftButtonPressed) 92 | { 93 | TheTileSelection.CursorX = e.X; 94 | TheTileSelection.CursorY = e.Y; 95 | } 96 | } 97 | 98 | bool selectionActive = false; 99 | private void TilesetView_PointerPressedTile(object? sender, TileEventArgs e) 100 | { 101 | if (e.Pressed == Avalonia.Input.PointerUpdateKind.LeftButtonPressed) 102 | { 103 | selectionActive = true; 104 | TilesetView.SelectionStartX = TilesetView.SelectionEndX = e.X; 105 | TilesetView.SelectionStartY = TilesetView.SelectionEndY = e.Y; 106 | } 107 | } 108 | 109 | private void TilesetView_PointerMovedTile(object? sender, TileEventArgs e) 110 | { 111 | if (selectionActive) 112 | { 113 | TilesetView.SelectionEndX = e.X; 114 | TilesetView.SelectionEndY = e.Y; 115 | } 116 | } 117 | 118 | private void TilesetView_PointerReleasedTile(object? sender, TileEventArgs e) 119 | { 120 | if (selectionActive) 121 | { 122 | var r = PointsToRect(TilesetView.SelectionStartX, TilesetView.SelectionStartY, TilesetView.SelectionEndX, TilesetView.SelectionEndY); 123 | var newSel = new Map((short)(r.Width+1), (short)(r.Height+1)); 124 | int i = 0; 125 | for (int y = (int)r.Top; y < r.Bottom + 1; y++) 126 | { 127 | for (int x = (int)r.Left; x < r.Right + 1; x++) 128 | { 129 | newSel.Tiles[i++] = (byte)((y * 16) + x); 130 | } 131 | } 132 | /* 133 | TilesetView.Selection = new TileSelection( 134 | TilesetView.SelectionEndX - (int)r.Left, 135 | TilesetView.SelectionEndY - (int)r.Top, 136 | newSel); 137 | /*/ 138 | TilesetView.Selection.Contents = newSel; 139 | TilesetView.Selection.CursorX = TilesetView.SelectionEndX - (int)r.Left; 140 | TilesetView.Selection.CursorY = TilesetView.SelectionEndY - (int)r.Top; 141 | //*/ 142 | selectionActive = false; 143 | } 144 | } 145 | } 146 | } -------------------------------------------------------------------------------- /TheKingsTable/Controls/TilesetViewer.cs: -------------------------------------------------------------------------------- 1 | using Avalonia; 2 | using Avalonia.Controls; 3 | using Avalonia.Media; 4 | using Avalonia.Media.Imaging; 5 | using Avalonia.Styling; 6 | using CaveStoryModdingFramework.Editors; 7 | using System; 8 | using System.Linq; 9 | 10 | namespace TheKingsTable.Controls 11 | { 12 | public class TilesetViewer : TileMouseHelper, IStyleable 13 | { 14 | Type IStyleable.StyleKey => typeof(Control); 15 | 16 | static TilesetViewer() 17 | { 18 | AffectsRender(new AvaloniaProperty[] { 19 | TilesetImageProperty, 20 | TileTypesImageProperty, 21 | TilesetAttributesProperty, 22 | ShowTileTypesProperty, 23 | SelectionProperty 24 | }.Concat(Properties).ToArray()); 25 | } 26 | 27 | #region TilesetImage Styled Property 28 | public static readonly StyledProperty TilesetImageProperty = 29 | AvaloniaProperty.Register(nameof(TilesetImage), null); 30 | 31 | public Bitmap? TilesetImage 32 | { 33 | get => GetValue(TilesetImageProperty); 34 | set => SetValue(TilesetImageProperty, value); 35 | } 36 | #endregion 37 | 38 | #region TileTypesImage Styled Property 39 | public static readonly StyledProperty TileTypesImageProperty = 40 | AvaloniaProperty.Register(nameof(TileTypesImage), null); 41 | 42 | public Bitmap? TileTypesImage 43 | { 44 | get => GetValue(TileTypesImageProperty); 45 | set => SetValue(TileTypesImageProperty, value); 46 | } 47 | #endregion 48 | 49 | #region TilesetAttributes Styled Property 50 | public static readonly StyledProperty TilesetAttributesProperty = 51 | AvaloniaProperty.Register(nameof(TilesetAttributes), null); 52 | 53 | public CaveStoryModdingFramework.Maps.Attribute? TilesetAttributes 54 | { 55 | get => GetValue(TilesetAttributesProperty); 56 | set => SetValue(TilesetAttributesProperty, value); 57 | } 58 | #endregion 59 | 60 | #region ShowTileTypes Styled Property 61 | public static readonly StyledProperty ShowTileTypesProperty = 62 | AvaloniaProperty.Register(nameof(ShowTileTypes), false); 63 | 64 | public bool ShowTileTypes 65 | { 66 | get => GetValue(ShowTileTypesProperty); 67 | set => SetValue(ShowTileTypesProperty, value); 68 | } 69 | #endregion 70 | 71 | #region Selection Styled Property 72 | public static readonly StyledProperty SelectionProperty = 73 | AvaloniaProperty.Register(nameof(Selection), null); 74 | 75 | public TileSelection Selection 76 | { 77 | get => GetValue(SelectionProperty); 78 | set => SetValue(SelectionProperty, value); 79 | } 80 | #endregion 81 | 82 | PixelSize BufferSize => new PixelSize(TileSize * 16, TileSize * 16); 83 | Rect MainRect => new Rect(new Point(0, 0), BufferSize.ToSize(1)); 84 | 85 | public override void Render(DrawingContext context) 86 | { 87 | context.FillRectangle(Brushes.Black, MainRect); 88 | if (TilesetImage != null) 89 | { 90 | var src = new Rect(TilesetImage.Size); 91 | context.DrawImage(TilesetImage, src, MainRect.Intersect(src)); 92 | } 93 | if (TilesetAttributes != null && TileTypesImage != null && ShowTileTypes) 94 | { 95 | for(int i = 0; i < TilesetAttributes.Tiles.Count; i++) 96 | context.DrawImage(TileTypesImage, 97 | GetTileRect(TilesetAttributes.Tiles[i],16) 98 | ,GetTileRect((byte)i,16)); 99 | } 100 | if(Selection != null 101 | && Selection.Contents.Tiles.Count > 0 102 | && Selection.Contents.Tiles[0] != null) 103 | { 104 | //a valid selection to display in the tileset should be of the form 105 | //n, ..., n+x 106 | //n+(y*16), ..., n+(y*16)+x 107 | //so first we find n... 108 | var start = (byte)Selection.Contents.Tiles[0]!; 109 | int i = 0; 110 | for(int y = 0; y < Selection.Contents.Height; y++) 111 | { 112 | for(int x = 0; x < Selection.Contents.Width; x++) 113 | { 114 | //...then we check it meets the condition 115 | if (Selection.Contents.Tiles[i++] != start + (16 * y) + x) 116 | return; 117 | } 118 | } 119 | //if we made it to here, we can draw the selection box 120 | var sx = start % 16; 121 | var sy = start / 16; 122 | context.DrawRectangle(new Pen(Brushes.Gray), 123 | new Rect(sx * TileSize, sy * TileSize, 124 | (Selection.Contents.Width * TileSize) - 1, 125 | (Selection.Contents.Height * TileSize) - 1 126 | )); 127 | } 128 | } 129 | } 130 | } 131 | -------------------------------------------------------------------------------- /TheKingsTable/Controls/WizardControl.cs: -------------------------------------------------------------------------------- 1 | using Avalonia; 2 | using Avalonia.Controls; 3 | using Avalonia.Controls.Metadata; 4 | using Avalonia.Controls.Primitives; 5 | using Avalonia.Styling; 6 | using System; 7 | using System.Windows.Input; 8 | 9 | namespace TheKingsTable.Controls 10 | { 11 | [TemplatePart(Name = PART_Carousel, Type = typeof(Carousel))] 12 | [TemplatePart(Name = PART_BackButton, Type = typeof(Button))] 13 | [TemplatePart(Name = PART_NextButton, Type = typeof(Button))] 14 | public class WizardControl : Carousel, IStyleable 15 | { 16 | public const string PART_Carousel = nameof(PART_Carousel); 17 | public const string PART_BackButton = nameof(PART_BackButton); 18 | public const string PART_NextButton = nameof(PART_NextButton); 19 | 20 | Type IStyleable.StyleKey => typeof(WizardControl); 21 | 22 | //TODO I kinda wish these had default functions that just ++/-- 23 | #region BackCommand Styled Property 24 | public static readonly StyledProperty BackCommandProperty = 25 | AvaloniaProperty.Register(nameof(BackCommand)); 26 | public ICommand BackCommand 27 | { 28 | get => GetValue(BackCommandProperty); 29 | set => SetValue(BackCommandProperty, value); 30 | } 31 | #endregion 32 | 33 | #region NextCommand Styled Property 34 | public static readonly StyledProperty NextCommandProperty = 35 | AvaloniaProperty.Register(nameof(NextCommand)); 36 | public ICommand NextCommand 37 | { 38 | get => GetValue(NextCommandProperty); 39 | set => SetValue(NextCommandProperty, value); 40 | } 41 | #endregion 42 | 43 | Carousel Carousel; 44 | Button BackButton, NextButton; 45 | protected override void OnApplyTemplate(TemplateAppliedEventArgs e) 46 | { 47 | base.OnApplyTemplate(e); 48 | Carousel = e.NameScope.Find(PART_Carousel); 49 | BackButton = e.NameScope.Find 10 | 11 | 12 | 13 | 14 | 15 | 16 | 36 | 37 | -------------------------------------------------------------------------------- /TheKingsTable/Converters/EntityListConverter.cs: -------------------------------------------------------------------------------- 1 | using Avalonia.Data.Converters; 2 | using CaveStoryModdingFramework.Entities; 3 | using System; 4 | using System.Collections.Generic; 5 | using System.Globalization; 6 | using System.Linq; 7 | using System.Text; 8 | using System.Threading.Tasks; 9 | 10 | namespace TheKingsTable.Converters 11 | { 12 | internal class EntityListConverter : IMultiValueConverter 13 | { 14 | public object? Convert(IList values, Type targetType, object? parameter, CultureInfo culture) 15 | { 16 | if (targetType != typeof(string)) 17 | throw new NotSupportedException(); 18 | 19 | var val = ""; 20 | if (values[0] is KeyValuePair entity) 21 | { 22 | /* 23 | if (values[1] is Dictionary entities) 24 | { 25 | //TODO this can probably be cached 26 | var index = entities.ContainsValue(entity) ? entities.First(x => x.Value == entity).Key : -1; 27 | if (index != -1) 28 | val += $"{index} - "; 29 | } 30 | val += entity.Name; 31 | */ 32 | val = $"{entity.Key} - {entity.Value.Name}"; 33 | } 34 | return val; 35 | } 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /TheKingsTable/Converters/ShortConverter.cs: -------------------------------------------------------------------------------- 1 | using Avalonia.Data.Converters; 2 | using System; 3 | using System.Globalization; 4 | 5 | namespace TheKingsTable.Converters 6 | { 7 | internal class ShortConverter : IValueConverter 8 | { 9 | public object? Convert(object? value, Type targetType, object? parameter, CultureInfo culture) 10 | { 11 | if (targetType != typeof(string)) 12 | throw new NotSupportedException(); 13 | 14 | var val = ""; 15 | if (value is short s) 16 | val = s.ToString(); 17 | return val; 18 | } 19 | 20 | public object? ConvertBack(object? value, Type targetType, object? parameter, CultureInfo culture) 21 | { 22 | throw new NotSupportedException(); 23 | } 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /TheKingsTable/Converters/StageEntryListConverter.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using CaveStoryModdingFramework.Stages; 4 | using Avalonia.Data.Converters; 5 | using System.Globalization; 6 | using Avalonia.Controls; 7 | 8 | namespace TheKingsTable.Converters 9 | { 10 | class StageEntryListConverter : IMultiValueConverter 11 | { 12 | public object Convert(IList values, Type targetType, object? parameter, CultureInfo culture) 13 | { 14 | if (targetType != typeof(string)) 15 | throw new NotSupportedException(); 16 | 17 | var val = ""; 18 | if (values[0] is StageTableEntry entry) 19 | { 20 | if(values[1] is IList ic) 21 | { 22 | var index = ic.IndexOf(entry); 23 | if (index != -1) 24 | val += $"{index} - "; 25 | } 26 | val += $"{entry.MapName} ({entry.Filename})"; 27 | } 28 | return val; 29 | } 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /TheKingsTable/Models/AvaloniaClipboard.cs: -------------------------------------------------------------------------------- 1 | using Avalonia; 2 | using Avalonia.Input; 3 | using Avalonia.Input.Platform; 4 | using CaveStoryModdingFramework.Maps; 5 | using CaveStoryModdingFramework.Editors; 6 | using System; 7 | using System.Linq; 8 | using System.Collections.Generic; 9 | using CaveStoryModdingFramework.Entities; 10 | using System.Text; 11 | using System.IO; 12 | using System.Threading.Tasks; 13 | 14 | namespace TheKingsTable.Models 15 | { 16 | class StageEditorClipboard 17 | { 18 | IClipboard clipboard; 19 | const string TILE_BINARY = nameof(TILE_BINARY); 20 | const string TILE_CURSOR_POS = nameof(TILE_CURSOR_POS); 21 | const string ENTITY_BINARY = nameof(ENTITY_BINARY); 22 | public StageEditorClipboard() 23 | { 24 | if (Application.Current!.Clipboard == null) 25 | throw new ArgumentNullException(); 26 | clipboard = Application.Current.Clipboard; 27 | } 28 | static string MapToString(Map m) 29 | { 30 | var sb = new StringBuilder(((4 + m.Tiles.Count) * 3) - 1); 31 | sb.AppendJoin(' ', (m.Width & 0xFF).ToString("X2"), (m.Width >> 8).ToString("X2"), 32 | (m.Height & 0xFF).ToString("X2"), (m.Height >> 8).ToString("X2") 33 | ); 34 | for (int i = 0; i < m.Tiles.Count; i++) 35 | sb.Append(' ').Append(m.Tiles[i]?.ToString("X2") ?? "__"); 36 | return sb.ToString(); 37 | } 38 | static Map? StringToMap(string s) 39 | { 40 | try 41 | { 42 | var bs = s.Split(' '); 43 | var width = (short)(Convert.ToByte(bs[0], 16) | (Convert.ToByte(bs[1], 16) << 8)); 44 | var height = (short)(Convert.ToByte(bs[2], 16) | (Convert.ToByte(bs[3], 16) << 8)); 45 | var m = new Map(width, height); 46 | 47 | //new apple product 48 | const int iStart = 4; 49 | //no cheating those dimensions... 50 | if (m.Tiles.Count != bs.Length - iStart) 51 | return null; 52 | 53 | for(int i = iStart; i < bs.Length; i++) 54 | { 55 | m.Tiles[i - iStart] = bs[i] == "__" ? null : Convert.ToByte(bs[i], 16); 56 | } 57 | return m; 58 | } 59 | catch (OverflowException) 60 | { 61 | return null; 62 | } 63 | catch (FormatException) 64 | { 65 | return null; 66 | } 67 | } 68 | public async Task SetTiles(TileSelection value) 69 | { 70 | var d = new DataObject(); 71 | //d.Set(TILE_BINARY, value); 72 | d.Set(TILE_CURSOR_POS, Encoding.ASCII.GetBytes($"{value.CursorX},{value.CursorY}")); 73 | d.Set(DataFormats.Text, MapToString(value.Contents)); 74 | await clipboard.SetDataObjectAsync(d); 75 | } 76 | public async Task GetTiles() 77 | { 78 | var formats = new HashSet(await clipboard.GetFormatsAsync()); 79 | if (formats.Contains(TILE_BINARY)) 80 | { 81 | var sel = await clipboard.GetDataAsync(TILE_BINARY); 82 | return sel as TileSelection; 83 | } 84 | else if (formats.Contains(DataFormats.Text)) 85 | { 86 | var m = StringToMap((string)await clipboard.GetDataAsync(DataFormats.Text)); 87 | if (m != null) 88 | { 89 | var sel = new TileSelection(0, 0, m); 90 | if (formats.Contains(TILE_CURSOR_POS)) 91 | { 92 | //despite the fact that I'm storing a string, trying to cast directly back to string causes an exception 93 | var coords = Encoding.ASCII.GetString((byte[])await clipboard.GetDataAsync(TILE_CURSOR_POS)); 94 | var cs = coords.Split(','); 95 | if (cs.Length == 2 && int.TryParse(cs[0], out var p1) && int.TryParse(cs[1], out var p2)) 96 | { 97 | sel.CursorX = p1; 98 | sel.CursorY = p2; 99 | } 100 | } 101 | return sel; 102 | } 103 | } 104 | return null; 105 | } 106 | 107 | public async Task SetEntities(HashSet entities) 108 | { 109 | var left = short.MaxValue; 110 | var top = short.MaxValue; 111 | foreach(var e in entities) 112 | { 113 | if (e.X < left) 114 | left = e.X; 115 | if (e.Y < top) 116 | top = e.Y; 117 | } 118 | var l = new List(entities.Count); 119 | foreach(var e in entities) 120 | { 121 | var n = new Entity(e); 122 | n.X -= left; 123 | n.Y -= top; 124 | l.Add(n); 125 | } 126 | var d = new DataObject(); 127 | d.Set(ENTITY_BINARY, l); 128 | await clipboard.SetDataObjectAsync(d); 129 | } 130 | public async Task?> GetEntities() 131 | { 132 | var formats = new HashSet(await clipboard.GetFormatsAsync()); 133 | if (formats.Contains(ENTITY_BINARY)) 134 | { 135 | var ents = await clipboard.GetDataAsync(ENTITY_BINARY); 136 | return ents as List; 137 | } 138 | else 139 | { 140 | return null; 141 | } 142 | } 143 | } 144 | } 145 | -------------------------------------------------------------------------------- /TheKingsTable/Models/EditorManager.cs: -------------------------------------------------------------------------------- 1 | using Avalonia.Media.Imaging; 2 | using CaveStoryModdingFramework; 3 | using CaveStoryModdingFramework.Entities; 4 | using CaveStoryModdingFramework.Maps; 5 | using CaveStoryModdingFramework.Stages; 6 | using CaveStoryModdingFramework.Utilities; 7 | using ReactiveUI; 8 | using System; 9 | using System.Collections.Generic; 10 | using System.IO; 11 | using System.Reactive; 12 | using System.Reactive.Linq; 13 | using System.Threading.Tasks; 14 | using TheKingsTable.Controls; 15 | using TheKingsTable.ViewModels.Editors; 16 | 17 | namespace TheKingsTable.Models 18 | { 19 | public class EditorManager 20 | { 21 | public ProjectFile Project { get; private set; } 22 | 23 | SpriteCache Cache; 24 | Dictionary BackgroundCache = new Dictionary(); 25 | Dictionary TilesetCache = new Dictionary(); 26 | Dictionary AttributeCache = new Dictionary(); 27 | 28 | //TODO MAKE EDITOR SETTING 29 | bool CaseSensitive { get; set; } = false; 30 | 31 | public static readonly Interaction StageTableEditorOpened 32 | = new Interaction(); 33 | public static readonly Interaction StageEditorOpened 34 | = new Interaction(); 35 | public static readonly Interaction StageEditorSelected 36 | = new Interaction(); 37 | public static readonly Interaction ScriptEditorOpened 38 | = new Interaction(); 39 | public static readonly Interaction ProjectSettingsOpened 40 | = new Interaction(); 41 | 42 | public static readonly Interaction ProjectClosing 43 | = new Interaction(); 44 | 45 | ProjectEditorViewModel? ProjectEditor = null; 46 | //TODO make a thing to toggle showing the project editor 47 | 48 | StageEditorToolMenu ToolMenu; 49 | public void UpdateGlobalSelection(StageEditorViewModel editor) 50 | { 51 | //GlobalSelector 52 | } 53 | 54 | public EditorManager(ProjectFile project) 55 | { 56 | Project = project; 57 | 58 | //StageEditorSelected.RegisterHandler() 59 | } 60 | 61 | private List AttributeEditors { get; set; } 62 | private Dictionary StageEditors { get; } = new Dictionary(); 63 | public async Task OpenStageEditor(StageTableEntry entry) 64 | { 65 | if (StageEditors.ContainsKey(entry)) 66 | return StageEditors[entry]; 67 | 68 | var background = await TryGetBackground(entry.BackgroundName); 69 | if (background == null) 70 | return null; 71 | 72 | var tileset = await TryGetTileset(entry.TilesetName); 73 | if (tileset == null) 74 | return null; 75 | 76 | var attributes = await TryGetAttributes(entry.TilesetName); 77 | if (attributes == null) 78 | return null; 79 | 80 | var tiles = await TryGetTiles(entry.Filename); 81 | if (tiles == null) 82 | return null; 83 | 84 | var entities = await TryGetEntities(entry.Filename); 85 | if (entities == null) 86 | return null; 87 | 88 | var e = new StageEditorViewModel(Project, entry, background, tileset, attributes, tiles, entities); 89 | StageEditors.Add(entry, e); 90 | await StageEditorOpened.Handle(e); 91 | return e; 92 | } 93 | 94 | private List StageTableEditors { get; } = new List(); 95 | public async Task OpenStageTableEditor(StageTableLocation location) 96 | { 97 | foreach(var item in StageTableEditors) 98 | { 99 | if(item.Location == location) 100 | { 101 | return item; 102 | } 103 | } 104 | 105 | if(!File.Exists(location.Filename)) 106 | { 107 | if(!await CommonInteractions.IsOk.Handle(new Words("Warning", 108 | $"{location.Filename} doesn't exist! Would you like to create it and continue?"))) 109 | { 110 | File.Create(location.Filename); 111 | } 112 | else 113 | { 114 | return null; 115 | } 116 | } 117 | 118 | var e = new StageTableListViewModel(this, location); 119 | StageTableEditors.Add(e); 120 | await StageTableEditorOpened.Handle(e); 121 | return e; 122 | } 123 | 124 | private List ScriptEditors { get; } = new List(); 125 | public async Task OpenScriptEditor(StageTableEntry entry) 126 | { 127 | foreach(var item in ScriptEditors) 128 | { 129 | //TODO check for the things 130 | } 131 | 132 | var path = await TryGetScript(entry.Filename); 133 | if (path == null) 134 | return null; 135 | 136 | if (!File.Exists(path)) 137 | { 138 | if (!await CommonInteractions.IsOk.Handle(new Words("Warning", 139 | $"{path} doesn't exist! Would you like to create it and continue?"))) 140 | { 141 | //don't need to do this since the script editor handles it already 142 | //File.Create(path); 143 | } 144 | else 145 | { 146 | return null; 147 | } 148 | } 149 | 150 | var e = new TextScriptEditorViewModel(Project, path); 151 | ScriptEditors.Add(e); 152 | await ScriptEditorOpened.Handle(e); 153 | return e; 154 | } 155 | 156 | private List NpcTableEditors { get; } = new List(); 157 | private List BulletTableEditors { get; } = new List(); 158 | private List ArmsLevelTableEditors { get; } = new List(); 159 | 160 | 161 | 162 | #region Asset loading 163 | public async Task TryLoadAsset(bool caseOK, List foundFiles) 164 | { 165 | //if we found something 166 | if (foundFiles.Count > 0 167 | //and we don't care about the case 168 | && !CaseSensitive 169 | //or we do care about the casing, but it was ok 170 | || caseOK 171 | //or we do care about the casing, but it doesn't matter 'cause the user said it was ok 172 | || await CommonInteractions.IsOk.Handle(new Words("Warning", 173 | $"Case mismatch on {foundFiles[0]}! Continue using this file?" 174 | ))) 175 | { 176 | return foundFiles[0]; 177 | } 178 | return null; 179 | } 180 | public async Task TryGetTileset(string name) 181 | { 182 | var tilesetPath = await TryLoadAsset(Project.GetTileset(name, out var t), t); 183 | if (tilesetPath == null) 184 | return null; 185 | if (!TilesetCache.ContainsKey(tilesetPath)) 186 | TilesetCache.Add(tilesetPath, new Bitmap(tilesetPath)); 187 | return TilesetCache[tilesetPath]; 188 | } 189 | public async Task TryGetAttributes(string name) 190 | { 191 | var attributePath = await TryLoadAsset(Project.GetAttributes(name, out var a), a); 192 | if (attributePath == null) 193 | return null; 194 | if (!AttributeCache.ContainsKey(attributePath)) 195 | AttributeCache.Add(attributePath, new CaveStoryModdingFramework.Maps.Attribute(attributePath)); 196 | return AttributeCache[attributePath]; 197 | } 198 | public async Task TryGetBackground(string name) 199 | { 200 | var backgroundPath = await TryLoadAsset(Project.GetBackground(name, out var b), b); 201 | if (backgroundPath == null) 202 | return null; 203 | if (!BackgroundCache.ContainsKey(backgroundPath)) 204 | BackgroundCache.Add(backgroundPath, new Bitmap(backgroundPath)); 205 | return BackgroundCache[backgroundPath]; 206 | } 207 | public async Task TryGetTiles(string name) 208 | { 209 | return await TryLoadAsset(Project.GetTiles(name, out var t), t); 210 | } 211 | public async Task TryGetEntities(string name) 212 | { 213 | return await TryLoadAsset(Project.GetEntities(name, out var e), e); 214 | } 215 | public async Task TryGetScript(string name) 216 | { 217 | return await TryLoadAsset(Project.GetScript(name, out var e), e); 218 | } 219 | #endregion 220 | 221 | } 222 | } 223 | -------------------------------------------------------------------------------- /TheKingsTable/Program.cs: -------------------------------------------------------------------------------- 1 | using Avalonia; 2 | using Avalonia.Controls.ApplicationLifetimes; 3 | using Avalonia.ReactiveUI; 4 | using System; 5 | 6 | namespace TheKingsTable 7 | { 8 | class Program 9 | { 10 | // Initialization code. Don't use any Avalonia, third-party APIs or any 11 | // SynchronizationContext-reliant code before AppMain is called: things aren't initialized 12 | // yet and stuff might break. 13 | [STAThread] 14 | public static void Main(string[] args) => BuildAvaloniaApp() 15 | .StartWithClassicDesktopLifetime(args); 16 | 17 | // Avalonia configuration, don't remove; also used by visual designer. 18 | public static AppBuilder BuildAvaloniaApp() 19 | => AppBuilder.Configure() 20 | .UsePlatformDetect() 21 | .LogToTrace() 22 | .UseReactiveUI(); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /TheKingsTable/TheKingsTable.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | WinExe 4 | net5.0;net6.0 5 | enable 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | FileSystemSelector.axaml 33 | 34 | 35 | WizardView.axaml 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | PreserveNewest 47 | 48 | 49 | 50 | -------------------------------------------------------------------------------- /TheKingsTable/Utilities.cs: -------------------------------------------------------------------------------- 1 | using Avalonia; 2 | using System; 3 | using System.Collections.Generic; 4 | using System.Linq; 5 | using System.Text; 6 | using System.Threading.Tasks; 7 | 8 | namespace TheKingsTable 9 | { 10 | public static class Utilities 11 | { 12 | public static Rect PointsToRect(int x1, int y1, int x2, int y2, int addToBottomRight = 0) 13 | { 14 | var left = Math.Min(x1, x2); 15 | var top = Math.Min(y1, y2); 16 | var right = Math.Max(x2, x1) + addToBottomRight; 17 | var bottom = Math.Max(y2, y1) + addToBottomRight; 18 | return new Rect(new Point(left, top), new Point(right, bottom)); 19 | } 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /TheKingsTable/ViewLocator.cs: -------------------------------------------------------------------------------- 1 | using Avalonia.Controls; 2 | using Avalonia.Controls.Templates; 3 | using System; 4 | using TheKingsTable.ViewModels; 5 | 6 | namespace TheKingsTable 7 | { 8 | public class ViewLocator : IDataTemplate 9 | { 10 | public bool SupportsRecycling => false; 11 | 12 | public IControl Build(object data) 13 | { 14 | var name = data.GetType().FullName!.Replace("ViewModel", "View"); 15 | var type = Type.GetType(name); 16 | 17 | if (type != null) 18 | { 19 | return (Control)Activator.CreateInstance(type)!; 20 | } 21 | else 22 | { 23 | return new TextBlock { Text = "Not Found: " + name }; 24 | } 25 | } 26 | 27 | public bool Match(object data) 28 | { 29 | return data is ViewModelBase; 30 | } 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /TheKingsTable/ViewModels/Editors/ProjectEditorViewModel.cs: -------------------------------------------------------------------------------- 1 | using CaveStoryModdingFramework; 2 | using NP.Avalonia.UniDockService; 3 | using System; 4 | using System.Collections.Generic; 5 | using System.Linq; 6 | using System.Text; 7 | using System.Threading.Tasks; 8 | 9 | namespace TheKingsTable.ViewModels.Editors 10 | { 11 | internal class ProjectEditorDockItemViewModel : DockItemViewModel { } 12 | public class ProjectEditorViewModel : ViewModelBase 13 | { 14 | ProjectFile Project { get; set; } 15 | 16 | public ProjectEditorViewModel(ProjectFile project) 17 | { 18 | Project = project; 19 | } 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /TheKingsTable/ViewModels/Editors/StageEditorViewModel.cs: -------------------------------------------------------------------------------- 1 | using Avalonia.Input; 2 | using Avalonia.Interactivity; 3 | using Avalonia.Media.Imaging; 4 | using CaveStoryModdingFramework; 5 | using CaveStoryModdingFramework.Editors; 6 | using CaveStoryModdingFramework.Entities; 7 | using CaveStoryModdingFramework.Stages; 8 | using CaveStoryModdingFramework.Utilities; 9 | using NP.Avalonia.UniDockService; 10 | using ReactiveUI; 11 | using System; 12 | using System.Collections.Generic; 13 | using System.Reactive; 14 | using TheKingsTable.Controls; 15 | using TheKingsTable.Models; 16 | 17 | namespace TheKingsTable.ViewModels.Editors 18 | { 19 | internal class StageEditorDockItemViewModel : DockItemViewModel { } 20 | public class StageEditorViewModel : ViewModelBase 21 | { 22 | public string StageName 23 | { 24 | get 25 | { 26 | var name = Entry.MapName; 27 | if (!string.IsNullOrWhiteSpace(Entry.JapaneseName)) 28 | { 29 | name += " | " + Entry.JapaneseName; 30 | } 31 | name += " (" + Entry.Filename + ")"; 32 | return name; 33 | } 34 | } 35 | public ProjectFile Project { get; } 36 | public StageTableEntry Entry { get; private set; } 37 | 38 | public Bitmap Background { get; private set; } 39 | public Bitmap TilesetImage { get; private set; } 40 | public Bitmap TileTypesImage { get; private set; } 41 | 42 | CaveStoryModdingFramework.Maps.Attribute attributes; 43 | public CaveStoryModdingFramework.Maps.Attribute Attributes { get => attributes; private set => this.RaiseAndSetIfChanged(ref attributes, value); } 44 | 45 | 46 | public ChangeTracker ChangeTracker { get; } 47 | 48 | StageEditorClipboard Clipboard { get; } = new StageEditorClipboard(); 49 | 50 | #region Active editor 51 | public enum Editors 52 | { 53 | Tile, 54 | Entity, 55 | MapState 56 | } 57 | Editors activeEditor = Editors.Tile; 58 | public Editors ActiveEditor 59 | { 60 | get => activeEditor; 61 | set => this.RaiseAndSetIfChanged(ref activeEditor, value); 62 | } 63 | #endregion 64 | 65 | #region Tile mode radio buttons 66 | string lastSetTile = nameof(TileDraw); 67 | void SetRadioEnumFromBool(ref T field, bool value, T radio, 68 | ref string lastSet, 69 | [System.Runtime.CompilerServices.CallerMemberName] 70 | string propertyName = "") where T : Enum 71 | { 72 | if (value && !field.Equals(radio)) 73 | { 74 | this.RaisePropertyChanging(propertyName); 75 | this.RaisePropertyChanging(lastSet); 76 | field = radio; 77 | this.RaisePropertyChanged(propertyName); 78 | this.RaisePropertyChanged(lastSet); 79 | lastSet = propertyName; 80 | } 81 | } 82 | bool TileDraw 83 | { 84 | get => TileAction == TileEditorActions.Draw; 85 | set => SetRadioEnumFromBool(ref TileAction, value, TileEditorActions.Draw, ref lastSetTile); 86 | } 87 | bool TileRectangle 88 | { 89 | get => TileAction == TileEditorActions.Rectangle; 90 | set => SetRadioEnumFromBool(ref TileAction, value, TileEditorActions.Rectangle, ref lastSetTile); 91 | } 92 | bool TileFill 93 | { 94 | get => TileAction == TileEditorActions.Fill; 95 | set => SetRadioEnumFromBool(ref TileAction, value, TileEditorActions.Fill, ref lastSetTile); 96 | } 97 | bool TileReplace 98 | { 99 | get => TileAction == TileEditorActions.Replace; 100 | set => SetRadioEnumFromBool(ref TileAction, value, TileEditorActions.Replace, ref lastSetTile); 101 | } 102 | bool TileSelect 103 | { 104 | get => TileAction == TileEditorActions.Select; 105 | set => SetRadioEnumFromBool(ref TileAction, value, TileEditorActions.Select, ref lastSetTile); 106 | } 107 | #endregion 108 | 109 | public TileEditor TileEditor { get; } 110 | TileEditorActions TileAction = TileEditorActions.Draw; 111 | 112 | public CaveStoryModdingFramework.Editors.EntityEditor EntityEditor { get; } 113 | bool selectInProgress = false, moveInProgress = false; 114 | short entityMoveOffsetX = 0, entityMoveOffsetY = 0; 115 | public short EntityMoveOffsetX { get => entityMoveOffsetX; set => this.RaiseAndSetIfChanged(ref entityMoveOffsetX, value); } 116 | public short EntityMoveOffsetY { get => entityMoveOffsetY; set => this.RaiseAndSetIfChanged(ref entityMoveOffsetY, value); } 117 | short selectedEntityType = 0; 118 | public short SelectedEntityType { get => selectedEntityType; set => this.RaiseAndSetIfChanged(ref selectedEntityType, value); } 119 | 120 | 121 | bool showCursor = true; 122 | public bool ShowCursor { get => showCursor; private set => this.RaiseAndSetIfChanged(ref showCursor, value); } 123 | 124 | int selectionStartX = -1, selectionStartY = -1, selectionEndX = -1, selectionEndY = -1; 125 | public int SelectionStartX { get => selectionStartX; private set => this.RaiseAndSetIfChanged(ref selectionStartX, value); } 126 | public int SelectionStartY { get => selectionStartY; private set => this.RaiseAndSetIfChanged(ref selectionStartY, value); } 127 | public int SelectionEndX { get => selectionEndX; private set => this.RaiseAndSetIfChanged(ref selectionEndX, value); } 128 | public int SelectionEndY { get => selectionEndY; private set => this.RaiseAndSetIfChanged(ref selectionEndY, value); } 129 | 130 | string PXMPath, PXEPath; 131 | public StageEditorViewModel( 132 | ProjectFile project, StageTableEntry entry, 133 | Bitmap background, 134 | Bitmap tileset, CaveStoryModdingFramework.Maps.Attribute attributes, 135 | string pxmPath, string pxePath) 136 | { 137 | Project = project; 138 | Entry = entry; 139 | 140 | //TODO LOAD FROM PROJECT URL AND MAYBE CACHE??? 141 | TileTypesImage = new Bitmap(System.IO.Path.Combine(System.IO.Path.GetDirectoryName(System.Reflection.Assembly.GetExecutingAssembly().Location), @"tiletypes.png")); 142 | 143 | Background = background; 144 | 145 | TilesetImage = tileset; 146 | Attributes = attributes; 147 | 148 | PXMPath = pxmPath; 149 | PXEPath = pxePath; 150 | 151 | ChangeTracker = new ChangeTracker(); 152 | TileEditor = new TileEditor(PXMPath, ChangeTracker); 153 | EntityEditor = new CaveStoryModdingFramework.Editors.EntityEditor(PXEPath, ChangeTracker); 154 | 155 | CopyTRACommand = ReactiveCommand.Create(async e => { 156 | await Avalonia.Application.Current.Clipboard.SetTextAsync($"(async e => { 159 | await Avalonia.Application.Current.Clipboard.SetTextAsync($"(async e => { 162 | await Avalonia.Application.Current.Clipboard.SetTextAsync($"(e => DoAndTriggerRedraw(ChangeTracker.Undo)); 168 | RedoCommand = ReactiveCommand.Create(e => DoAndTriggerRedraw(ChangeTracker.Redo)); 169 | 170 | PointerEnterCommand = ReactiveCommand.Create(PointerEnter); 171 | PointerMovedCommand = ReactiveCommand.Create(PointerMoved); 172 | PointerPressedCommand = ReactiveCommand.Create(PointerPressed); 173 | PointerReleasedCommand = ReactiveCommand.Create(PointerReleased); 174 | PointerLeaveCommand = ReactiveCommand.Create(PointerLeave); 175 | PointerCaptureLostCommand = ReactiveCommand.Create(PointerCaptureLost); 176 | 177 | KeyDownCommand = ReactiveCommand.Create(KeyDown); 178 | 179 | InsertEntityCommand = ReactiveCommand.Create>(InsertEntity); 180 | SelectEntityCommand = ReactiveCommand.Create(SelectEntity); 181 | 182 | SaveCommand = ReactiveCommand.Create(Save); 183 | } 184 | public ReactiveCommand CopyTRACommand { get; } 185 | public ReactiveCommand CopyCMPCommand { get; } 186 | public ReactiveCommand CopySMPCommand { get; } 187 | 188 | public ReactiveCommand SaveCommand { get; } 189 | 190 | public ReactiveCommand UndoCommand { get; } 191 | public ReactiveCommand RedoCommand { get; } 192 | 193 | public ReactiveCommand PointerEnterCommand { get; } 194 | public ReactiveCommand PointerMovedCommand { get; } 195 | public ReactiveCommand PointerPressedCommand { get; } 196 | public ReactiveCommand PointerReleasedCommand { get; } 197 | public ReactiveCommand PointerLeaveCommand { get; } 198 | public ReactiveCommand PointerCaptureLostCommand { get; } 199 | 200 | public ReactiveCommand SelectEntityCommand { get; } 201 | public ReactiveCommand, Unit> InsertEntityCommand { get; } 202 | public ReactiveCommand KeyDownCommand { get; } 203 | 204 | void SelectEntity(Entity e) 205 | { 206 | EntityEditor.Selection.Clear(); 207 | EntityEditor.Selection.Add(e); 208 | } 209 | //HACK using a Tuple is kinda hacky, but it's the only way to have a single parameter for the command, so... 210 | void InsertEntity(Tuple coords) 211 | { 212 | if(coords.Item1 > -1 && coords.Item2 > -1) 213 | EntityEditor.CreateEntity(coords.Item1, coords.Item2, SelectedEntityType); 214 | } 215 | 216 | async void KeyDown(KeyEventArgs e) 217 | { 218 | switch (ActiveEditor) 219 | { 220 | case Editors.Tile: 221 | switch (e.Key) 222 | { 223 | case Key.C when e.KeyModifiers == KeyModifiers.Control: 224 | await Clipboard.SetTiles(TileEditor.Selection); 225 | e.Handled = true; 226 | break; 227 | case Key.V when e.KeyModifiers == KeyModifiers.Control: 228 | case Key.Insert when e.KeyModifiers == KeyModifiers.Shift: 229 | var ts = await Clipboard.GetTiles(); 230 | if (ts != null) 231 | { 232 | TileEditor.Selection.Contents = ts.Contents; 233 | TileEditor.Selection.CursorX = ts.CursorX; 234 | TileEditor.Selection.CursorY = ts.CursorY; 235 | } 236 | e.Handled = true; 237 | break; 238 | } 239 | break; 240 | case Editors.Entity: 241 | switch (e.Key) 242 | { 243 | case Key.Insert when e.KeyModifiers == KeyModifiers.None: 244 | case Key.I when e.KeyModifiers == KeyModifiers.None: 245 | InsertEntity(Tuple.Create((short)SelectionEndX, (short)SelectionEndY)); 246 | e.Handled = true; 247 | break; 248 | case Key.Delete when e.KeyModifiers == KeyModifiers.None && EntityEditor.Selection.Count > 0: 249 | EntityEditor.DeleteSelection(); 250 | e.Handled = true; 251 | break; 252 | 253 | case Key.C when e.KeyModifiers == KeyModifiers.Control: 254 | await Clipboard.SetEntities(EntityEditor.Selection); 255 | e.Handled = true; 256 | break; 257 | case Key.V when e.KeyModifiers == KeyModifiers.Control: 258 | case Key.Insert when e.KeyModifiers == KeyModifiers.Shift: 259 | var es = await Clipboard.GetEntities(); 260 | if(es != null) 261 | { 262 | EntityEditor.PasteEntities((short)SelectionEndX, (short)SelectionEndY, es); 263 | } 264 | e.Handled = true; 265 | break; 266 | } 267 | break; 268 | 269 | } 270 | } 271 | 272 | //HACK this is 100% not the correct way to create a context menu in this situation 273 | public Avalonia.Controls.ContextMenu ContextMenu 274 | { 275 | get 276 | { 277 | var cm = new Avalonia.Controls.ContextMenu(); 278 | var items = new List(); 279 | switch (ActiveEditor) 280 | { 281 | case Editors.Tile: 282 | items.Add(new Avalonia.Controls.MenuItem() 283 | { 284 | Header = "Copy 0) 307 | { 308 | items.Add(new Avalonia.Controls.Separator()); 309 | foreach (var e in ents) 310 | items.Add(new Avalonia.Controls.MenuItem() 311 | { 312 | Header = $"{EntityEditor.Entities.IndexOf(e)} - {e.Type}", 313 | Command = SelectEntityCommand, 314 | CommandParameter = e 315 | }); 316 | } 317 | break; 318 | case Editors.MapState: 319 | break; 320 | default: 321 | throw new ArgumentException("Invalid Active Editor: " + ActiveEditor, nameof(ActiveEditor)); 322 | } 323 | cm.Items = items; 324 | return cm; 325 | } 326 | } 327 | 328 | 329 | 330 | bool redrawTilesNeeded = true; 331 | public bool RedrawTilesNeeded { get => redrawTilesNeeded; set => this.RaiseAndSetIfChanged(ref redrawTilesNeeded, value); } 332 | bool redrawTileTypesNeeded = true; 333 | public bool RedrawTileTypesNeeded { get => redrawTileTypesNeeded; set => this.RaiseAndSetIfChanged(ref redrawTileTypesNeeded, value); } 334 | 335 | void DoAndTriggerRedraw(Action a) 336 | { 337 | a(); 338 | RedrawTilesNeeded = RedrawTileTypesNeeded = true; 339 | } 340 | bool showTileTypes = false; 341 | public bool ShowTileTypes { get => showTileTypes; set => this.RaiseAndSetIfChanged(ref showTileTypes, value); } 342 | 343 | #region Pointer stuff 344 | void HidePointer() 345 | { 346 | SelectionStartX = SelectionEndX = -1; 347 | SelectionStartY = SelectionEndY = -1; 348 | } 349 | void ResetPointer(TileEventArgs e) 350 | { 351 | SelectionStartX = SelectionEndX = e.X; 352 | SelectionStartY = SelectionEndY = e.Y; 353 | } 354 | void PointerPressed(TileEventArgs e) 355 | { 356 | if (e.Pressed == PointerUpdateKind.LeftButtonPressed) 357 | { 358 | switch (ActiveEditor) 359 | { 360 | case Editors.Tile: 361 | TileEditor.BeginSelection(e.X, e.Y, TileAction); 362 | break; 363 | case Editors.Entity: 364 | if (EntityEditor.AnySelectedEntitiesAt((short)e.X, (short)e.Y)) 365 | { 366 | moveInProgress = true; 367 | ShowCursor = false; 368 | } 369 | else 370 | { 371 | selectInProgress = true; 372 | } 373 | break; 374 | } 375 | } 376 | //HACK the context menu does indeed change every time you right click... potentionally 377 | else if(e.Pressed == PointerUpdateKind.RightButtonPressed) 378 | { 379 | ResetPointer(e); 380 | this.RaisePropertyChanged(nameof(ContextMenu)); 381 | } 382 | } 383 | void PointerEnter(TileEventArgs e) 384 | { 385 | SharedMove(e); 386 | } 387 | void PointerMoved(TileEventArgs e) 388 | { 389 | SharedMove(e); 390 | } 391 | void SharedMove(TileEventArgs e) 392 | { 393 | switch (ActiveEditor) 394 | { 395 | case Editors.Tile: 396 | switch (TileEditor.CurrentAction) 397 | { 398 | //these use a rectangular cursor 399 | case TileEditorActions.Rectangle: 400 | case TileEditorActions.Select: 401 | TileEditor.MoveSelection(SelectionEndX = e.X, SelectionEndY = e.Y); 402 | break; 403 | 404 | //these all use a size 1 cursor... 405 | case TileEditorActions.Fill: 406 | case TileEditorActions.Replace: 407 | //...except for draw, which has a special case when you've selected it too 408 | case TileEditorActions.Draw: 409 | TileEditor.MoveSelection(e.X, e.Y); 410 | goto default; 411 | default: 412 | //draw mode ALWAYS has a cursor as big as the selection 413 | if (TileAction == TileEditorActions.Draw) 414 | { 415 | SelectionStartX = e.X - TileEditor.Selection.CursorX; 416 | SelectionStartY = e.Y - TileEditor.Selection.CursorY; 417 | SelectionEndX = e.X + (TileEditor.Selection.Contents.Width - TileEditor.Selection.CursorX - 1); 418 | SelectionEndY = e.Y + (TileEditor.Selection.Contents.Height - TileEditor.Selection.CursorY - 1); 419 | } 420 | else 421 | { 422 | ResetPointer(e); 423 | } 424 | break; 425 | } 426 | break; 427 | case Editors.Entity: 428 | if (selectInProgress) 429 | { 430 | SelectionEndX = e.X; 431 | SelectionEndY = e.Y; 432 | } 433 | else if (moveInProgress) 434 | { 435 | EntityMoveOffsetX = (short)((SelectionEndX = e.X) - SelectionStartX); 436 | EntityMoveOffsetY = (short)((SelectionEndY = e.Y) - SelectionStartY); 437 | } 438 | else 439 | { 440 | ResetPointer(e); 441 | } 442 | break; 443 | } 444 | } 445 | void PointerReleased(TileEventArgs e) 446 | { 447 | if(e.Pressed == PointerUpdateKind.LeftButtonReleased) 448 | { 449 | switch (ActiveEditor) 450 | { 451 | case Editors.Tile: 452 | TileEditor.CommitSelection(); 453 | break; 454 | case Editors.Entity: 455 | if (moveInProgress) 456 | { 457 | EntityEditor.MoveSelection(EntityMoveOffsetX, EntityMoveOffsetY); 458 | ShowCursor = true; 459 | EntityMoveOffsetX = 0; 460 | EntityMoveOffsetY = 0; 461 | moveInProgress = false; 462 | } 463 | else 464 | { 465 | EntityEditor.SelectEntitiesInRange((short)SelectionStartX, (short)SelectionStartY, (short)SelectionEndX, (short)SelectionEndY); 466 | selectInProgress = false; 467 | } 468 | break; 469 | } 470 | ResetPointer(e); 471 | } 472 | } 473 | private void PointerLeave() 474 | { 475 | switch (ActiveEditor) 476 | { 477 | case Editors.Tile when TileEditor.CurrentAction == TileEditorActions.None: 478 | case Editors.Entity: 479 | HidePointer(); 480 | break; 481 | } 482 | } 483 | private void PointerCaptureLost() 484 | { 485 | switch (ActiveEditor) 486 | { 487 | case Editors.Tile: 488 | TileEditor.BeginSelection(-1, -1, TileEditorActions.None); 489 | break; 490 | case Editors.Entity: 491 | selectInProgress = false; 492 | moveInProgress = false; 493 | EntityMoveOffsetX = 0; 494 | EntityMoveOffsetY = 0; 495 | ShowCursor = true; 496 | break; 497 | } 498 | HidePointer(); 499 | } 500 | #endregion 501 | 502 | void Save(RoutedEventArgs e) 503 | { 504 | EntityEditor.Save(PXEPath); 505 | TileEditor.Save(PXMPath); 506 | } 507 | /* 508 | private void LinkedEntry_PropertyChanged(object? sender, PropertyChangedEventArgs e) 509 | { 510 | if (sender is StageTableEntry ste) 511 | { 512 | switch (e.PropertyName) 513 | { 514 | case nameof(StageTableEntry.Filename): 515 | //newFilename = ste.Filename; 516 | break; 517 | case nameof(StageTableEntry.TilesetName): 518 | //newTileset = ste.TilesetName; 519 | break; 520 | case nameof(StageTableEntry.BackgroundName): 521 | //newBackground = ste.BackgroundName; 522 | break; 523 | case nameof(StageTableEntry.Spritesheet1): 524 | //newSpritesheet1 = ste.Spritesheet1; 525 | break; 526 | case nameof(StageTableEntry.Spritesheet2): 527 | //newSpritesheet2 = ste.Spritesheet2; 528 | break; 529 | 530 | case nameof(StageTableEntry.MapName): 531 | case nameof(StageTableEntry.JapaneseName): 532 | case nameof(StageTableEntry.BossNumber): 533 | case nameof(StageTableEntry.BackgroundType): 534 | //auto update 535 | break; 536 | } 537 | } 538 | } 539 | */ 540 | } 541 | } 542 | -------------------------------------------------------------------------------- /TheKingsTable/ViewModels/Editors/StageTableListViewModel.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Text; 5 | using System.Threading.Tasks; 6 | using CaveStoryModdingFramework.Stages; 7 | using System.Collections.ObjectModel; 8 | using Avalonia.Controls.Selection; 9 | using CaveStoryModdingFramework; 10 | using NP.Avalonia.UniDockService; 11 | using TheKingsTable.Models; 12 | using ReactiveUI; 13 | using Avalonia.Interactivity; 14 | using System.Reactive; 15 | 16 | namespace TheKingsTable.ViewModels.Editors 17 | { 18 | internal class StageTableListDockItemViewModel : DockItemViewModel { } 19 | public class StageTableListViewModel : ViewModelBase 20 | { 21 | EditorManager Parent; 22 | public StageTableLocation Location { get; } 23 | public List Stages { get; } 24 | public SelectionModel Selection { get; } 25 | 26 | public ReactiveCommand OpenStageCommand { get; } 27 | public ReactiveCommand OpenScriptCommand { get; } 28 | public StageTableListViewModel(EditorManager parent, StageTableLocation location) 29 | { 30 | Parent = parent; 31 | Location = location; 32 | Selection = new SelectionModel(); 33 | 34 | Stages = location.Read(); 35 | 36 | OpenStageCommand = ReactiveCommand.Create(OpenStage); 37 | OpenScriptCommand = ReactiveCommand.Create(OpenScript); 38 | } 39 | public async void OpenStage(RoutedEventArgs e) 40 | { 41 | await Parent.OpenStageEditor(Selection.SelectedItems.First()); 42 | } 43 | public async void OpenScript(RoutedEventArgs e) 44 | { 45 | await Parent.OpenScriptEditor(Selection.SelectedItems.First()); 46 | } 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /TheKingsTable/ViewModels/Editors/TextScriptEditorViewModel.cs: -------------------------------------------------------------------------------- 1 | using CaveStoryModdingFramework; 2 | using CaveStoryModdingFramework.TSC; 3 | using ReactiveUI; 4 | using System; 5 | using CaveStoryModdingFramework.Compatability; 6 | using System.IO; 7 | using Avalonia.Interactivity; 8 | using System.Reactive; 9 | using NP.Avalonia.UniDockService; 10 | using System.Threading.Tasks; 11 | using System.Collections.Generic; 12 | using System.Reactive.Linq; 13 | 14 | namespace TheKingsTable.ViewModels.Editors 15 | { 16 | internal class ScriptEditorDockItemViewModel : DockItemViewModel { } 17 | public class TextScriptEditorViewModel : ViewModelBase 18 | { 19 | public ProjectFile Project { get; } 20 | public string TSCPath { get; private set; } 21 | string text = ""; 22 | public string Text { get => text; set => this.RaiseAndSetIfChanged(ref text, value); } 23 | void Load(string tscPath) 24 | { 25 | byte[] bytes = Array.Empty(); 26 | if (Project.UseScriptSource) 27 | { 28 | var sspath = ScriptSource.GetScriptSourcePath(tscPath); 29 | if(File.Exists(sspath)) 30 | bytes = File.ReadAllBytes(sspath); 31 | } 32 | if (bytes.Length <= 0 && File.Exists(tscPath)) 33 | { 34 | bytes = File.ReadAllBytes(tscPath); 35 | if (Project.ScriptsEncrypted) 36 | Encryptor.DecryptInPlace(bytes, Project.DefaultEncryptionKey); 37 | } 38 | if (bytes.Length > 0) 39 | Text = Project.ScriptEncoding.GetString(bytes); 40 | else 41 | Text = ""; 42 | } 43 | void Save(string tscPath) 44 | { 45 | var bytes = Project.ScriptEncoding.GetBytes(Text); 46 | if(Project.UseScriptSource) 47 | { 48 | string ssdir = ScriptSource.GetScriptSourceDirectory(tscPath); 49 | if (!Directory.Exists(ssdir)) 50 | Directory.CreateDirectory(ssdir); 51 | File.WriteAllBytes(ScriptSource.GetScriptSourcePath(tscPath), bytes); 52 | } 53 | if (Project.ScriptsEncrypted) 54 | Encryptor.EncryptInPlace(bytes, Project.DefaultEncryptionKey); 55 | 56 | File.WriteAllBytes(tscPath, bytes); 57 | } 58 | async void SaveAs(RoutedEventArgs e) 59 | { 60 | //HACK send help all this code is garbage 61 | var path = await CommonInteractions.BrowseToSaveFile.Handle(new FileSelection( 62 | "Select save location", 63 | new List>() { new Tuple("TSC Files", Project.ScriptExtension.Replace(".","")) 64 | , new Tuple("Text Files", "txt")}, 65 | "" 66 | )); 67 | if(path != null) 68 | { 69 | var bytes = Project.ScriptEncoding.GetBytes(Text); 70 | if (path.EndsWith(Project.ScriptExtension) && Project.ScriptsEncrypted) 71 | Encryptor.EncryptInPlace(bytes, Project.DefaultEncryptionKey); 72 | File.WriteAllBytes(path, bytes); 73 | } 74 | } 75 | public TextScriptEditorViewModel(ProjectFile project, string tscPath) 76 | { 77 | Project = project; 78 | TSCPath = tscPath; 79 | 80 | SaveCommand = ReactiveCommand.Create(e => Save(TSCPath)); 81 | SaveAsCommand = ReactiveCommand.Create(SaveAs); 82 | 83 | Load(TSCPath); 84 | } 85 | 86 | public ReactiveCommand SaveCommand { get; } 87 | public ReactiveCommand SaveAsCommand { get; } 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /TheKingsTable/ViewModels/MainWindowViewModel.cs: -------------------------------------------------------------------------------- 1 | using ReactiveUI; 2 | using System; 3 | using System.Collections.Generic; 4 | using CaveStoryModdingFramework; 5 | using TheKingsTable.Models; 6 | using Avalonia.Interactivity; 7 | using System.Reactive; 8 | using System.Threading.Tasks; 9 | using System.Reactive.Linq; 10 | 11 | namespace TheKingsTable.ViewModels 12 | { 13 | public class MainWindowViewModel : ViewModelBase 14 | { 15 | public ProjectFile? Project { get; private set; } = null; 16 | public EditorManager? EditorManager { get; private set; } = null; 17 | public MainWindowViewModel() 18 | { 19 | NewProjectCommand = ReactiveCommand.Create(NewProject); 20 | LoadProjectCommand = ReactiveCommand.Create(LoadProject); 21 | SaveProjectCommand = ReactiveCommand.Create(SaveProject); 22 | 23 | ShowNewProjectWizard = new Interaction(); 24 | } 25 | public Interaction ShowNewProjectWizard { get; } 26 | public ReactiveCommand NewProjectCommand { get; } 27 | public ReactiveCommand LoadProjectCommand { get; } 28 | public ReactiveCommand SaveProjectCommand { get; } 29 | 30 | Editors.ProjectEditorViewModel? ProjectEditor = null; 31 | 32 | bool projectSettingsShown = false; 33 | bool ProjectSettingsShown 34 | { 35 | get => Project != null && ProjectEditor != null && projectSettingsShown; 36 | set 37 | { 38 | if (Project == null) 39 | projectSettingsShown = false; 40 | else 41 | { 42 | if(ProjectEditor == null) 43 | { 44 | ProjectEditor = new Editors.ProjectEditorViewModel(Project); 45 | } 46 | } 47 | } 48 | } 49 | 50 | private async Task ProjectOverwriteOK() 51 | { 52 | if (Project != null) 53 | { 54 | var okToOverwrite = 55 | await CommonInteractions.IsOk.Handle(new Words( 56 | "Warning!", 57 | "You already have a project loaded! Are you sure you want to continue?")); 58 | return okToOverwrite; 59 | } 60 | return true; 61 | } 62 | public async void NewProject(RoutedEventArgs e) 63 | { 64 | if (await ProjectOverwriteOK()) 65 | { 66 | if (Project != null) 67 | { 68 | var closeOk = await EditorManager.ProjectClosing.Handle(new Unit()); 69 | if (!closeOk) 70 | return; 71 | } 72 | 73 | var wizard = await ShowNewProjectWizard.Handle(new Unit()); 74 | if (wizard != null) 75 | { 76 | Project = wizard.Project; 77 | if (wizard.SaveProject) 78 | Project.Save(wizard.SavePath); 79 | EditorManager = new EditorManager(Project); 80 | await EditorManager.OpenStageTableEditor(Project.StageTables[Project.SelectedLayout.StageTables[0].Key]); 81 | } 82 | } 83 | } 84 | public async void LoadProject(RoutedEventArgs e) 85 | { 86 | if (await ProjectOverwriteOK()) 87 | { 88 | if (Project != null) 89 | { 90 | var closeOk = await EditorManager.ProjectClosing.Handle(new Unit()); 91 | if (!closeOk) 92 | return; 93 | } 94 | 95 | var result = await CommonInteractions.BrowseToOpenFile.Handle(new FileSelection( 96 | "Select Project", 97 | new List>() { new Tuple("Project Files", ProjectFile.Extension) }, 98 | "" 99 | )); 100 | if (result != null) 101 | { 102 | Project = ProjectFile.Load(result); 103 | EditorManager = new EditorManager(Project); 104 | await EditorManager.OpenStageTableEditor(Project.StageTables[Project.SelectedLayout.StageTables[0].Key]); 105 | } 106 | } 107 | } 108 | 109 | public async void SaveProject(RoutedEventArgs e) 110 | { 111 | if(Project != null) 112 | { 113 | var result = await CommonInteractions.BrowseToSaveFile.Handle(new FileSelection( 114 | "Select save location", 115 | new List>() { new Tuple("Project Files", ProjectFile.Extension) }, 116 | "" 117 | )); 118 | if(result != null) 119 | { 120 | Project.Save(result); 121 | } 122 | } 123 | } 124 | } 125 | } 126 | -------------------------------------------------------------------------------- /TheKingsTable/ViewModels/ViewModelBase.cs: -------------------------------------------------------------------------------- 1 | using ReactiveUI; 2 | using System; 3 | using System.Collections.Generic; 4 | using System.Text; 5 | 6 | namespace TheKingsTable.ViewModels 7 | { 8 | public class ViewModelBase : ReactiveObject 9 | { 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /TheKingsTable/ViewModels/WizardViewModel.cs: -------------------------------------------------------------------------------- 1 | using Avalonia.Threading; 2 | using Avalonia.Interactivity; 3 | using CaveStoryModdingFramework; 4 | using ReactiveUI; 5 | using System; 6 | using System.Collections.Generic; 7 | using System.IO; 8 | using System.Linq; 9 | using System.Reactive; 10 | using System.Reactive.Linq; 11 | using CaveStoryModdingFramework.AutoDetection; 12 | using CaveStoryModdingFramework.Stages; 13 | using System.Collections.ObjectModel; 14 | 15 | namespace TheKingsTable.ViewModels 16 | { 17 | public class WizardViewModel : ViewModelBase 18 | { 19 | public Interaction Close { get; } = new Interaction(); 20 | 21 | public ProjectFile Project { get; } = new ProjectFile(); 22 | public AssetLayout BaseLayout { get; } = new AssetLayout() 23 | { 24 | DataPaths = new List() { string.Empty }, 25 | NpcPaths = new List() { string.Empty }, 26 | StagePaths = new List() { string.Empty } 27 | }; 28 | 29 | bool saveProject = true; 30 | public bool SaveProject { get => saveProject; set => this.RaiseAndSetIfChanged(ref saveProject, value); } 31 | string savePath = ""; 32 | public string SavePath { get => savePath; set => this.RaiseAndSetIfChanged(ref savePath, value); } 33 | 34 | const int ThrottleTimeMS = 250; 35 | 36 | public WizardViewModel() 37 | { 38 | Project.Layouts.Add(BaseLayout); 39 | 40 | DetectEXECommand = ReactiveCommand.Create(DetectEXE); 41 | DetectDataCommand = ReactiveCommand.Create(DetectData); 42 | 43 | EXEOK = this.WhenAnyValue(x => x.EXEPath, x => File.Exists(x)) 44 | .Throttle(TimeSpan.FromMilliseconds(ThrottleTimeMS), AvaloniaScheduler.Instance); 45 | EXEOK.Subscribe(x => DataStart = Path.GetDirectoryName(EXEPath) ?? ""); 46 | 47 | DataOK = this.WhenAnyValue(x => x.BaseDataPath, x => Directory.Exists(x)) 48 | .Throttle(TimeSpan.FromMilliseconds(ThrottleTimeMS), AvaloniaScheduler.Instance); 49 | DataOK.Subscribe(x => EXEStart = Path.GetDirectoryName(BaseDataPath) ?? ""); 50 | 51 | BackCommand = ReactiveCommand.Create(Back); 52 | NextCommand = ReactiveCommand.Create(Next, 53 | MakePageConditions(new Dictionary[]>() 54 | { 55 | { 56 | 0, 57 | new IObservable[] 58 | { 59 | DataOK 60 | } 61 | }, 62 | { 63 | 1, 64 | new IObservable[] 65 | { 66 | this.WhenAnyValue(x => x.StageTableCount, x => x > 0) 67 | } 68 | }, 69 | { 70 | 2, 71 | new IObservable[] 72 | { 73 | this.WhenAnyValue(x => x.LayoutDataPath, x => Directory.Exists(x)) 74 | .Throttle(TimeSpan.FromMilliseconds(ThrottleTimeMS), AvaloniaScheduler.Instance), 75 | this.WhenAnyValue(x => x.ImageExtension, x => !string.IsNullOrWhiteSpace(x)) 76 | } 77 | }, 78 | { 79 | 3, 80 | new IObservable[] 81 | { 82 | this.WhenAnyValue(x => x.LayoutNpcPath, x => Directory.Exists(x)) 83 | .Throttle(TimeSpan.FromMilliseconds(ThrottleTimeMS), AvaloniaScheduler.Instance), 84 | this.WhenAnyValue(x => x.SpritesheetPrefix, x => !string.IsNullOrWhiteSpace(x)) 85 | } 86 | }, 87 | { 88 | 4, 89 | new IObservable[] 90 | { 91 | this.WhenAnyValue(x => x.LayoutStagePath, x => Directory.Exists(x)) 92 | .Throttle(TimeSpan.FromMilliseconds(ThrottleTimeMS), AvaloniaScheduler.Instance), 93 | this.WhenAnyValue(x => x.EntityExtension, x => !string.IsNullOrWhiteSpace(x)), 94 | this.WhenAnyValue(x => x.MapExtension, x => !string.IsNullOrWhiteSpace(x)), 95 | this.WhenAnyValue(x => x.ScriptExtension, x => !string.IsNullOrWhiteSpace(x)), 96 | this.WhenAnyValue(x => x.AttributeExtension, x => !string.IsNullOrWhiteSpace(x)) 97 | } 98 | } 99 | })); 100 | } 101 | 102 | int StageTableCount => Project.StageTables.Count; 103 | 104 | 105 | 106 | 107 | int selectedIndex = 0; 108 | public int SelectedIndex { get => selectedIndex; set => this.RaiseAndSetIfChanged(ref selectedIndex, value); } 109 | 110 | 111 | #region Page 1 - Base Data/EXE 112 | 113 | string dataStart = ""; 114 | public string DataStart { get => dataStart; set => this.RaiseAndSetIfChanged(ref dataStart, value); } 115 | string exeStart = ""; 116 | public string EXEStart { get => exeStart; set => this.RaiseAndSetIfChanged(ref exeStart, value); } 117 | public string BaseDataPath 118 | { 119 | get => Project.BaseDataPath; 120 | set 121 | { 122 | if (Project.BaseDataPath != value) 123 | { 124 | this.RaisePropertyChanging(); 125 | Project.BaseDataPath = value; 126 | this.RaisePropertyChanged(); 127 | } 128 | } 129 | } 130 | public string EXEPath 131 | { 132 | get => Project.EXEPath; 133 | set 134 | { 135 | if (Project.EXEPath != value) 136 | { 137 | this.RaisePropertyChanging(); 138 | Project.EXEPath = value; 139 | this.RaisePropertyChanged(); 140 | } 141 | } 142 | } 143 | 144 | public IObservable EXEOK { get; } 145 | public IObservable DataOK { get; } 146 | 147 | public ReactiveCommand DetectEXECommand { get; } 148 | public ReactiveCommand DetectDataCommand { get; } 149 | 150 | private void DetectEXE(RoutedEventArgs e) 151 | { 152 | var d = Path.GetDirectoryName(BaseDataPath); 153 | if (d == null) 154 | return; 155 | 156 | //look for an exe 157 | EXEPath = AutoDetector.FindLargestFile(d, "*.exe"); 158 | //if we didn't find any, settle for the largest file 159 | if (EXEPath == null) 160 | EXEPath = AutoDetector.FindLargestFile(d, "*.*"); 161 | //if it's still null... idk, set it to the directory? 162 | if (EXEPath == null) 163 | EXEPath = d; 164 | } 165 | private void DetectData(RoutedEventArgs e) 166 | { 167 | var d = Path.GetDirectoryName(EXEPath); 168 | if (d == null) 169 | return; 170 | 171 | var p = Path.Combine(d, "data"); 172 | if (Directory.Exists(p)) 173 | BaseDataPath = p; 174 | else 175 | BaseDataPath = d; 176 | } 177 | 178 | #endregion 179 | 180 | #region Page 2 - Data folder/image extension 181 | 182 | public string LayoutDataPath 183 | { 184 | get => BaseLayout.DataPaths[0]; 185 | set 186 | { 187 | if (BaseLayout.DataPaths[0] != value) 188 | { 189 | this.RaisePropertyChanging(); 190 | BaseLayout.DataPaths[0] = value; 191 | this.RaisePropertyChanged(); 192 | } 193 | } 194 | } 195 | public string ImageExtension 196 | { 197 | get => Project.ImageExtension; 198 | set 199 | { 200 | if (Project.ImageExtension != value) 201 | { 202 | this.RaisePropertyChanging(); 203 | Project.ImageExtension = value; 204 | this.RaisePropertyChanged(); 205 | } 206 | } 207 | } 208 | public string BackgroundPrefix 209 | { 210 | get => Project.BackgroundPrefix; 211 | set 212 | { 213 | if (Project.BackgroundPrefix != value) 214 | { 215 | this.RaisePropertyChanging(); 216 | Project.BackgroundPrefix = value; 217 | this.RaisePropertyChanged(); 218 | } 219 | } 220 | } 221 | 222 | #endregion 223 | 224 | #region Page 3 - NPC Folder 225 | 226 | public string LayoutNpcPath 227 | { 228 | get => BaseLayout.NpcPaths[0]; 229 | set 230 | { 231 | if (BaseLayout.NpcPaths[0] != value) 232 | { 233 | this.RaisePropertyChanging(); 234 | BaseLayout.NpcPaths[0] = value; 235 | this.RaisePropertyChanged(); 236 | } 237 | } 238 | } 239 | 240 | public string SpritesheetPrefix 241 | { 242 | get => Project.SpritesheetPrefix; 243 | set 244 | { 245 | if (Project.SpritesheetPrefix != value) 246 | { 247 | this.RaisePropertyChanging(); 248 | Project.SpritesheetPrefix = value; 249 | this.RaisePropertyChanged(); 250 | } 251 | } 252 | } 253 | 254 | #endregion 255 | 256 | #region Page 4 - Stage Folder 257 | 258 | public string LayoutStagePath 259 | { 260 | get => BaseLayout.StagePaths[0]; 261 | set 262 | { 263 | if (BaseLayout.StagePaths[0] != value) 264 | { 265 | this.RaisePropertyChanging(); 266 | BaseLayout.StagePaths[0] = value; 267 | this.RaisePropertyChanged(); 268 | } 269 | } 270 | } 271 | 272 | public string EntityExtension 273 | { 274 | get => Project.EntityExtension; 275 | set 276 | { 277 | if (Project.EntityExtension != value) 278 | { 279 | this.RaisePropertyChanging(); 280 | Project.EntityExtension = value; 281 | this.RaisePropertyChanged(); 282 | } 283 | } 284 | } 285 | 286 | public string MapExtension 287 | { 288 | get => Project.MapExtension; 289 | set 290 | { 291 | if (Project.MapExtension != value) 292 | { 293 | this.RaisePropertyChanging(); 294 | Project.MapExtension = value; 295 | this.RaisePropertyChanged(); 296 | } 297 | } 298 | } 299 | 300 | public string ScriptExtension 301 | { 302 | get => Project.ScriptExtension; 303 | set 304 | { 305 | if (Project.ScriptExtension != value) 306 | { 307 | this.RaisePropertyChanging(); 308 | Project.ScriptExtension = value; 309 | this.RaisePropertyChanged(); 310 | } 311 | } 312 | } 313 | 314 | public bool ScriptsEncrypted 315 | { 316 | get => Project.ScriptsEncrypted; 317 | set 318 | { 319 | if(Project.ScriptsEncrypted != value) 320 | { 321 | this.RaisePropertyChanging(); 322 | Project.ScriptsEncrypted = value; 323 | this.RaisePropertyChanged(); 324 | } 325 | } 326 | } 327 | 328 | public string AttributeExtension 329 | { 330 | get => Project.AttributeExtension; 331 | set 332 | { 333 | if (Project.AttributeExtension != value) 334 | { 335 | this.RaisePropertyChanging(); 336 | Project.AttributeExtension = value; 337 | this.RaisePropertyChanged(); 338 | } 339 | } 340 | } 341 | 342 | public string TilesetPrefix 343 | { 344 | get => Project.TilesetPrefix; 345 | set 346 | { 347 | if (Project.TilesetPrefix != value) 348 | { 349 | this.RaisePropertyChanging(); 350 | Project.TilesetPrefix = value; 351 | this.RaisePropertyChanged(); 352 | } 353 | } 354 | } 355 | 356 | #endregion 357 | 358 | #region All of the logic for autodetecting and next/back buttons 359 | public ReactiveCommand BackCommand { get; } 360 | public ReactiveCommand NextCommand { get; } 361 | 362 | void Back(RoutedEventArgs e) 363 | { 364 | SelectedIndex--; 365 | } 366 | int nextPageToAutodetect = 1; 367 | 368 | async void Next(RoutedEventArgs e) 369 | { 370 | int nextPage = SelectedIndex+1; 371 | switch (nextPage) 372 | { 373 | case 1: 374 | if(nextPage == nextPageToAutodetect) 375 | { 376 | nextPageToAutodetect++; 377 | if (AutoDetectStageTables()) 378 | { 379 | nextPage++; 380 | goto case 2; 381 | } 382 | } 383 | break; 384 | case 2: 385 | ReloadTables(); 386 | if (nextPage == nextPageToAutodetect) 387 | { 388 | nextPageToAutodetect++; 389 | if (AutoDetectDataFolder()) 390 | { 391 | nextPage++; 392 | goto case 3; 393 | } 394 | } 395 | break; 396 | case 3: 397 | if(nextPage == nextPageToAutodetect) 398 | { 399 | nextPageToAutodetect = 5; 400 | nextPage += AutoDetectStageNpcFolders(); 401 | if (nextPage == 5) 402 | goto case 5; 403 | } 404 | break; 405 | case 5: 406 | if(nextPage == nextPageToAutodetect) 407 | { 408 | nextPageToAutodetect = 6; 409 | if(!AutoDetectMods()) 410 | nextPage++; 411 | if (nextPage == 6) 412 | goto case 6; 413 | } 414 | break; 415 | case 6: 416 | if(nextPage == nextPageToAutodetect) 417 | { 418 | if (!string.IsNullOrWhiteSpace(EXEPath)) 419 | SavePath = Path.ChangeExtension(EXEPath, ProjectFile.Extension); 420 | else 421 | SavePath = Path.Combine(Path.GetDirectoryName(BaseDataPath), Path.ChangeExtension("CS2", ProjectFile.Extension)); 422 | nextPageToAutodetect = -1; 423 | } 424 | break; 425 | case 7: 426 | await Close.Handle(this); 427 | return; 428 | } 429 | SelectedIndex = nextPage; 430 | } 431 | 432 | void ReloadTables() 433 | { 434 | LoadedTables?.Clear(); //this might be unecessary 435 | LoadedTables = new List>(Project.StageTables.Count); 436 | foreach (var entry in Project.StageTables.Values) 437 | LoadedTables.Add(entry.Read()); 438 | } 439 | 440 | bool AutoDetectStageTables() 441 | { 442 | var foundTables = 0; 443 | if (File.Exists(EXEPath)) 444 | { 445 | int i = 0; 446 | foreach (var result in AutoDetector.FindInternalStageTables(EXEPath)) 447 | { 448 | var key = "Internal " + (++i); 449 | Project.StageTables.Add(key, result); 450 | BaseLayout.StageTables.Add(new TableLoadInfo(key, true)); 451 | foundTables++; 452 | } 453 | 454 | //TODO based on what internal data was found we probably need to add a bunch of other locations 455 | //these need to be hardcoded 456 | //stuff like if you load freeware it knows where the other tables are 457 | //or dsiware, etc. 458 | } 459 | 460 | //external tables in general 461 | var extTables = AutoDetector.FindExternalTables(BaseDataPath); 462 | if (extTables != null) 463 | { 464 | Project.AddTables(extTables, BaseLayout); 465 | if (extTables.StageTables?.Count > 0) 466 | foundTables += extTables.StageTables.Count; 467 | } 468 | 469 | this.RaisePropertyChanged(nameof(StageTableCount)); 470 | 471 | return foundTables > 0; 472 | } 473 | List>? LoadedTables = null; 474 | bool AutoDetectDataFolder() 475 | { 476 | //find the data folder 477 | var firstDataPath = AutoDetector.FindDataFolderAndImageExtension(BaseDataPath, LoadedTables, out var foundExts); 478 | 479 | if (firstDataPath == null) 480 | return false; 481 | 482 | LayoutDataPath = firstDataPath; 483 | 484 | //find the image extension 485 | var possibleExtensions = AutoDetector.GetMaxes(foundExts); 486 | if (possibleExtensions.Count != 1) 487 | return false; 488 | ImageExtension = possibleExtensions[0]; 489 | 490 | 491 | return true; 492 | } 493 | //haha 494 | int AutoDetectStageNpcFolders() 495 | { 496 | //Find the npc/stage folders 497 | var spritesheets = AutoDetector.GetSpritesheets(LoadedTables); 498 | var filenames = AutoDetector.GetFilenames(LoadedTables); 499 | var tilesets = AutoDetector.GetTilesets(LoadedTables); 500 | 501 | var foundPaths = AutoDetector.FindNpcAndStageFolders(LayoutDataPath, 502 | //note that the project file gets modified by these functions 503 | x => AutoDetector.TryInitFromNpcFolder(x, spritesheets, Project.ImageExtension, Project), 504 | x => AutoDetector.TryInitFromStageFolder(x, filenames, tilesets, Project.ImageExtension, Project)); 505 | //this is changed by the first one 506 | this.RaisePropertyChanged(nameof(SpritesheetPrefix)); 507 | //all these are changed by the second 508 | this.RaisePropertyChanged(nameof(EntityExtension)); 509 | this.RaisePropertyChanged(nameof(MapExtension)); 510 | this.RaisePropertyChanged(nameof(ScriptExtension)); 511 | this.RaisePropertyChanged(nameof(ScriptsEncrypted)); 512 | this.RaisePropertyChanged(nameof(AttributeExtension)); 513 | this.RaisePropertyChanged(nameof(TilesetPrefix)); 514 | 515 | if (foundPaths.Item1 != null) 516 | LayoutNpcPath = foundPaths.Item1; 517 | if(foundPaths.Item2 != null) 518 | LayoutStagePath = foundPaths.Item2; 519 | 520 | return foundPaths.Item1 != null ? (foundPaths.Item2 != null ? 2 : 1) : 0; 521 | } 522 | 523 | bool AutoDetectMods() 524 | { 525 | int layoutCount = Project.Layouts.Count; 526 | if (BaseDataPath != LayoutDataPath) 527 | { 528 | bool SubAdd(string curr) 529 | { 530 | //the tables this layout will use 531 | var localTables = new List>(); 532 | 533 | var extTablesFound = false; 534 | //if there were external tables, we're already done 535 | if (extTablesFound = AutoDetector.TryFindExternalTables(curr, out var externalTables) 536 | || AutoDetector.CountHardcodedDataFiles(curr, Project.ImageExtension) >= 2 537 | //otherwise, we should check if backgrounds exist 538 | || AutoDetector.ContainsBackgrounds(curr, localTables) > 0.5) 539 | { 540 | //we are now committed to making a layout, so make it 541 | var localLayout = new AssetLayout(BaseLayout, true); 542 | localLayout.DataPaths.Add(curr); 543 | 544 | //also time to add the previously found tables... 545 | foreach (var table in LoadedTables) 546 | localTables.Add(table); 547 | 548 | //...the external tables if any were found... 549 | if (extTablesFound) 550 | { 551 | foreach (var tab in externalTables.StageTables) 552 | localTables.Add(tab.Read()); 553 | Project.AddTables(externalTables, localLayout); 554 | } 555 | 556 | //...and merge them! 557 | var merged = AutoDetector.MergeStageTables(localTables); 558 | 559 | var localSpritesheets = AutoDetector.GetSpritesheets(merged); 560 | var localFilenames = AutoDetector.GetFilenames(merged); 561 | var localTilesets = AutoDetector.GetTilesets(merged); 562 | 563 | var loc = AutoDetector.FindNpcAndStageFolders(curr, 564 | x => AutoDetector.ContainsNpcFiles(localSpritesheets, x, Project.ImageExtension, Project.SpritesheetPrefix) >= 0.5, 565 | x => AutoDetector.ContainsStageFiles(merged, x, Project.TilesetPrefix, Project.AttributeExtension, 566 | Project.ImageExtension, Project.MapExtension, Project.EntityExtension, Project.ScriptExtension) >= 0.5); 567 | 568 | if (loc.Item1 != null) 569 | localLayout.NpcPaths.Add(loc.Item1); 570 | if (loc.Item2 != null) 571 | localLayout.StagePaths.Add(loc.Item2); 572 | 573 | //we should have a valid layout by this point???? 574 | Project.Layouts.Add(localLayout); 575 | return true; 576 | } 577 | return false; 578 | } 579 | 580 | AutoDetector.BreadthFirstSearch( 581 | Directory.EnumerateDirectories(BaseDataPath).Where(x => x != LayoutDataPath), 582 | (x) => SubAdd(x)); 583 | } 584 | return Project.Layouts.Count > layoutCount; 585 | } 586 | 587 | const int PageCount = 7; 588 | static IObservable ObservableAND(IEnumerable> observables) 589 | { 590 | return Observable.CombineLatest(observables, x => x.All(y => y)); 591 | } 592 | static IObservable ObservableOR(IEnumerable> observables) 593 | { 594 | return Observable.CombineLatest(observables, x => x.Any(y => y)); 595 | } 596 | IObservable MakePageConditions(Dictionary[]> pageParams) 597 | { 598 | var allConditions = new List>(PageCount); 599 | for (int i = 0; i < PageCount; i++) 600 | { 601 | var pageConditions = new List>(); 602 | 603 | //HACK need to generate functions that do the comparison 604 | //otherwise i will stay at PageCount for every comparison 605 | Func> f = 606 | (int j) => 607 | (int x) => 608 | x == j; 609 | 610 | //Basic page check 611 | pageConditions.Add(this.WhenAnyValue(x => x.SelectedIndex, f(i))); 612 | 613 | //any other requirements on this page 614 | if (pageParams.ContainsKey(i)) 615 | pageConditions.AddRange(pageParams[i]); 616 | 617 | //add it to the list 618 | if (pageConditions.Count > 1) 619 | allConditions.Add(ObservableAND(pageConditions)); 620 | else 621 | allConditions.Add(pageConditions[0]); 622 | } 623 | return ObservableOR(allConditions); 624 | } 625 | #endregion 626 | 627 | } 628 | } 629 | -------------------------------------------------------------------------------- /TheKingsTable/ViewModels/WizardWindowViewModel.cs: -------------------------------------------------------------------------------- 1 | using ReactiveUI; 2 | using System; 3 | using System.Collections.Generic; 4 | using System.Linq; 5 | using System.Text; 6 | using System.Threading.Tasks; 7 | 8 | namespace TheKingsTable.ViewModels 9 | { 10 | public class WizardWindowViewModel : ViewModelBase 11 | { 12 | public WizardViewModel WizardView { get; } 13 | public WizardWindowViewModel() 14 | { 15 | WizardView = new WizardViewModel(); 16 | } 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /TheKingsTable/Views/Editors/ProjectEditor.axaml: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | Images Copyrighted? 16 | 17 | 18 | 19 | Transparent Color 20 | TBA 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | Scripts Encrypted 29 | 30 | 31 | 32 | Encryption Key Location 33 | 34 | 35 | 36 | Default Encryption Key 37 | 38 | 39 | 40 | 41 | TSC Command editing will go here 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | Screen Width (Pixels) 51 | 52 | 53 | 54 | Screen Height (Pixels) 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | -------------------------------------------------------------------------------- /TheKingsTable/Views/Editors/StageEditor.axaml: -------------------------------------------------------------------------------- 1 | 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 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 80 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | -------------------------------------------------------------------------------- /TheKingsTable/Views/Editors/StageTableEditors.axaml: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | 7 | 8 | 9 | 13 | 14 | 15 | 16 | 17 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 34 | 36 | 37 | 38 | 39 | -------------------------------------------------------------------------------- /TheKingsTable/Views/Editors/TextScriptEditor.axaml: -------------------------------------------------------------------------------- 1 |  4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /TheKingsTable/Views/MainWindow.axaml: -------------------------------------------------------------------------------- 1 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | -------------------------------------------------------------------------------- /TheKingsTable/Views/MainWindow.axaml.cs: -------------------------------------------------------------------------------- 1 | using Avalonia; 2 | using Avalonia.Controls; 3 | using Avalonia.Markup.Xaml; 4 | using Avalonia.ReactiveUI; 5 | using CaveStoryModdingFramework; 6 | using NP.Avalonia.UniDockService; 7 | using ReactiveUI; 8 | using System; 9 | using System.Collections.Generic; 10 | using System.Collections.ObjectModel; 11 | using System.Reactive; 12 | using System.Threading.Tasks; 13 | using TheKingsTable.Models; 14 | using TheKingsTable.ViewModels; 15 | using TheKingsTable.ViewModels.Editors; 16 | 17 | namespace TheKingsTable.Views 18 | { 19 | public partial class MainWindow : ReactiveWindow 20 | { 21 | IUniDockService UniDockService; 22 | const string ToolGroup = "ToolGroup"; 23 | const string EditorGroup = "EditorGroup"; 24 | const string DockManager = "TheDockManager"; 25 | 26 | public MainWindow() 27 | { 28 | InitializeComponent(); 29 | #if DEBUG 30 | this.AttachDevTools(); 31 | #endif 32 | UniDockService = (IUniDockService)this.FindResource(DockManager)!; 33 | UniDockService.DockItemsViewModels = new ObservableCollection(); 34 | 35 | this.WhenActivated(d => d(ViewModel!.ShowNewProjectWizard 36 | .RegisterHandler(DoShowWizard))); 37 | 38 | this.WhenActivated(d => d(EditorManager.StageTableEditorOpened 39 | .RegisterHandler(AddStageTableEditor))); 40 | this.WhenActivated(d => d(EditorManager.StageEditorOpened 41 | .RegisterHandler(AddStageEditor))); 42 | this.WhenActivated(d => d(EditorManager.ScriptEditorOpened 43 | .RegisterHandler(AddScriptEditor))); 44 | this.WhenActivated(d => d(EditorManager.ProjectClosing 45 | .RegisterHandler(CloseProject))); 46 | 47 | 48 | this.WhenActivated(d => d(CommonInteractions.BrowseToOpenFile. 49 | RegisterHandler(x => CommonAvaloniaHandlers.ShowOpenFileBrowser(x, this)))); 50 | this.WhenActivated(d => d(CommonInteractions.BrowseForFolder. 51 | RegisterHandler(x => CommonAvaloniaHandlers.ShowFolderBrowser(x, this)))); 52 | this.WhenActivated(d => d(CommonInteractions.BrowseToSaveFile. 53 | RegisterHandler(x => CommonAvaloniaHandlers.ShowSaveFileBrowser(x, this)))); 54 | this.WhenActivated(d => d(CommonInteractions.IsOk 55 | .RegisterHandler(x => CommonAvaloniaHandlers.ShowYesNoMessage(x, this)))); 56 | 57 | Button.ClickEvent.AddClassHandler((x, e) => 58 | { 59 | if ((e.Source as Button)?.Name == "CloseButton") 60 | { 61 | var t = e.RoutedEvent.EventArgsType; 62 | 63 | //TODO hook into FloatingWindow.Closing????? 64 | } 65 | }); 66 | } 67 | 68 | private void CloseProject(InteractionContext obj) 69 | { 70 | 71 | } 72 | 73 | private void InitializeComponent() 74 | { 75 | AvaloniaXamlLoader.Load(this); 76 | } 77 | 78 | private async Task AddStageTableEditor(InteractionContext context) 79 | { 80 | UniDockService.DockItemsViewModels.Add(new StageTableListDockItemViewModel() 81 | { 82 | //TODO this is an awful ID 83 | DockId = context.Input.Location.Filename, 84 | TheVM = context.Input, 85 | DefaultDockGroupId = EditorGroup, 86 | DefaultDockOrderInGroup = 0, 87 | HeaderContentTemplateResourceKey = "StageTableHeader", 88 | ContentTemplateResourceKey = "StageTableListView", 89 | IsSelected = true, 90 | IsActive = true, 91 | IsPredefined = false, 92 | }); 93 | 94 | context.SetOutput(new Unit()); 95 | } 96 | private async Task AddStageEditor(InteractionContext context) 97 | { 98 | UniDockService.DockItemsViewModels.Add(new StageEditorDockItemViewModel() 99 | { 100 | //TODO this is an awful ID 101 | DockId = context.Input.Entry.GetHashCode().ToString(), 102 | TheVM = context.Input, 103 | DefaultDockGroupId = ToolGroup, 104 | DefaultDockOrderInGroup = 0, 105 | HeaderContentTemplateResourceKey = "StageEditorHeaderDataTemplate", 106 | ContentTemplateResourceKey = "StageEditorDataTemplate", 107 | IsSelected = true, 108 | IsActive = true, 109 | IsPredefined = false, 110 | }); 111 | context.SetOutput(new Unit()); 112 | } 113 | private async Task AddScriptEditor(InteractionContext context) 114 | { 115 | UniDockService.DockItemsViewModels.Add(new ScriptEditorDockItemViewModel() 116 | { 117 | //TODO this is an awful ID 118 | DockId = context.Input.TSCPath.GetHashCode().ToString(), 119 | TheVM = context.Input, 120 | DefaultDockGroupId = ToolGroup, 121 | DefaultDockOrderInGroup = 0, 122 | HeaderContentTemplateResourceKey = "ScriptEditorHeaderDataTemplate", 123 | ContentTemplateResourceKey = "ScriptEditorDataTemplate", 124 | IsSelected = true, 125 | IsActive = true, 126 | IsPredefined = false, 127 | }); 128 | context.SetOutput(new Unit()); 129 | } 130 | 131 | private async Task DoShowWizard(InteractionContext context) 132 | { 133 | var dialog = new WizardWindow(); 134 | var vm = new WizardWindowViewModel(); 135 | dialog.DataContext = vm; 136 | dialog.ViewModel = vm; 137 | var result = await dialog.ShowDialog(this); 138 | context.SetOutput(result); 139 | } 140 | 141 | private async Task DoShowProjectEditor(InteractionContext context) 142 | { 143 | 144 | context.SetOutput(new Unit()); 145 | } 146 | 147 | protected override void OnClosed(EventArgs e) 148 | { 149 | base.OnClosed(e); 150 | //see https://github.com/npolyak/NP.Avalonia.UniDock/issues/8 151 | //tl;dr avalonia moment 152 | Environment.Exit(0); 153 | } 154 | } 155 | } 156 | -------------------------------------------------------------------------------- /TheKingsTable/Views/WizardView.axaml: -------------------------------------------------------------------------------- 1 | 8 | 13 | 14 | 15 | 18 | 20 | 23 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | Use Scriptsource 61 | 62 | 63 | Save a project file 64 | 65 | 69 | 70 | 71 | 72 | 73 | -------------------------------------------------------------------------------- /TheKingsTable/Views/WizardView.axaml.cs: -------------------------------------------------------------------------------- 1 | using Avalonia; 2 | using Avalonia.Controls; 3 | using Avalonia.Markup.Xaml; 4 | 5 | namespace TheKingsTable.Views 6 | { 7 | public partial class WizardView : UserControl 8 | { 9 | public WizardView() 10 | { 11 | InitializeComponent(); 12 | } 13 | 14 | private void InitializeComponent() 15 | { 16 | AvaloniaXamlLoader.Load(this); 17 | } 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /TheKingsTable/Views/WizardWindow.axaml: -------------------------------------------------------------------------------- 1 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /TheKingsTable/Views/WizardWindow.axaml.cs: -------------------------------------------------------------------------------- 1 | using Avalonia; 2 | using Avalonia.Markup.Xaml; 3 | using Avalonia.ReactiveUI; 4 | using ReactiveUI; 5 | using System.Reactive; 6 | using System.Threading.Tasks; 7 | using TheKingsTable.ViewModels; 8 | 9 | namespace TheKingsTable.Views 10 | { 11 | public partial class WizardWindow : ReactiveWindow 12 | { 13 | public WizardWindow() 14 | { 15 | InitializeComponent(); 16 | #if DEBUG 17 | this.AttachDevTools(); 18 | #endif 19 | this.WhenActivated(d => d(CommonInteractions.BrowseToOpenFile 20 | .RegisterHandler(x => CommonAvaloniaHandlers.ShowOpenFileBrowser(x,this)))); 21 | this.WhenActivated(d => d(CommonInteractions.BrowseForFolder 22 | .RegisterHandler(x => CommonAvaloniaHandlers.ShowFolderBrowser(x,this)))); 23 | this.WhenActivated(d => d(ViewModel!.WizardView.Close 24 | .RegisterHandler(HandleClose))); 25 | } 26 | 27 | async Task HandleClose(InteractionContext context) 28 | { 29 | Close(context.Input); 30 | context.SetOutput(new Unit()); 31 | } 32 | 33 | private void InitializeComponent() 34 | { 35 | AvaloniaXamlLoader.Load(this); 36 | } 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /TheKingsTable/nuget.config: -------------------------------------------------------------------------------- 1 | 2 | 3 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /TheKingsTable/tiletypes.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Brayconn/TheKingsTable/a6e20631ff60a28ff1fbd0d6cba702e147b81973/TheKingsTable/tiletypes.png -------------------------------------------------------------------------------- /appveyor.yml: -------------------------------------------------------------------------------- 1 | image: Visual Studio 2022 2 | version: 1.0.{build} 3 | skip_tags: true 4 | configuration: Release 5 | clone_folder: c:\projects\CaveStoryEditor 6 | clone_script: 7 | - cmd: >- 8 | git clone https://github.com/%APPVEYOR_REPO_NAME%.git %APPVEYOR_BUILD_FOLDER% 9 | 10 | cd %APPVEYOR_BUILD_FOLDER%/../ 11 | 12 | git clone https://github.com/Brayconn/CaveStoryModdingFramework.git 13 | 14 | git clone https://github.com/Brayconn/PixelModdingFramework.git 15 | 16 | git clone https://github.com/Brayconn/PETools.git 17 | before_build: 18 | - cmd: >- 19 | nuget restore %APPVEYOR_BUILD_FOLDER%/TheKingsTable.sln 20 | 21 | dotnet restore %APPVEYOR_BUILD_FOLDER% 22 | build: 23 | verbosity: minimal 24 | after_build: 25 | - cmd: >- 26 | cd %APPVEYOR_BUILD_FOLDER%/TheKingsTable/bin/%CONFIGURATION%/net6.0/ 27 | 28 | 7z a %CONFIGURATION%.zip *.exe *.dll *.png *.json runtimes 29 | 30 | appveyor PushArtifact %CONFIGURATION%.zip --------------------------------------------------------------------------------