From 828f4d02c0a936670bdbac1d477a06ddde387a20 Mon Sep 17 00:00:00 2001 From: Lol124 <95558254+Romualdo666@users.noreply.github.com> Date: Tue, 17 Dec 2024 21:47:50 -0300 Subject: [PATCH] Squashed commit of the following: commit 73230b7f49a26063c6a19c6401c49f172863d6d7 Merge: 1c286c4f e1f5d22a Author: colinator27 <17358554+colinator27@users.noreply.github.com> Date: Mon Dec 2 13:32:38 2024 -0500 Merge branch 'master' into underanalyzer commit 1c286c4f7f7b8f22df1bc74d676f014cae690f7c Author: colinator27 <17358554+colinator27@users.noreply.github.com> Date: Mon Dec 2 13:32:23 2024 -0500 Update Underanalyzer commit e1f5d22aadd358b748c4ba785d0c647383064dbf Author: colinator27 <17358554+colinator27@users.noreply.github.com> Date: Mon Dec 2 12:12:01 2024 -0500 Revert debug constant defined for nightly builds commit 51ebf7f43f6fd7f09abd6b59f6640b1e78398970 Merge: cbb70cab 35dca46e Author: Miepee <38186597+Miepee@users.noreply.github.com> Date: Mon Dec 2 08:33:32 2024 +0100 Merge pull request #1908 from UnderminersTeam/replace-webclient Replace WebClient with HttpClient commit 35dca46ef61793d0ab88568fae902a81804d40a8 Author: colinator27 <17358554+colinator27@users.noreply.github.com> Date: Sun Dec 1 19:11:12 2024 -0500 Remove try/catch around progress status update commit cbb70cabbf6e31c0cd5fccc6be72c5a3e098d137 Merge: 68f06d77 d0a807d0 Author: Miepee <38186597+Miepee@users.noreply.github.com> Date: Mon Dec 2 00:53:37 2024 +0100 Merge pull request #1893 from UnderminersTeam/nightly-debug-fix Manually add DEBUG constant to nightly builds commit d0a807d04008d601bfa5a6c6ee8c39b1f8ceeefa Author: colinator27 <17358554+colinator27@users.noreply.github.com> Date: Sun Dec 1 18:47:37 2024 -0500 Add comment regarding debug macro commit 256c6b6e4ba5fcad7049f3da19411677f6d84b4b Author: colinator27 <17358554+colinator27@users.noreply.github.com> Date: Sun Dec 1 18:40:12 2024 -0500 Address some reviews commit 282ecca341d1867f73f81e97066a65cf94a199d2 Merge: 989a173f 68f06d77 Author: colinator27 <17358554+colinator27@users.noreply.github.com> Date: Sun Dec 1 18:21:16 2024 -0500 Merge branch 'master' into underanalyzer commit 989a173fd6a8e56080304a70cc618c9c042a9301 Author: colinator27 <17358554+colinator27@users.noreply.github.com> Date: Sun Dec 1 18:02:50 2024 -0500 Update UA to add local var declaration cleanup commit 68f06d77d7cd331c7ec4b39cf32ec061634d8271 Merge: b8587ce7 7c2cd83d Author: Miepee <38186597+Miepee@users.noreply.github.com> Date: Sun Dec 1 16:02:47 2024 +0100 Merge pull request #1985 from UnderminersTeam/copy-on-write-flag Add EnableCopyOnWrite flag commit b8587ce7feb4aa6b5943be0c7a516b10c2e8ecf7 Merge: 0ca898a5 54968e3e Author: Miepee <38186597+Miepee@users.noreply.github.com> Date: Sun Dec 1 16:01:53 2024 +0100 Merge pull request #1947 from zivmaor/find-references-crash-fix Find references crash fix commit 7c2cd83dde5f509015ed25d56c80cc96ed18624b Author: colinator27 <17358554+colinator27@users.noreply.github.com> Date: Sat Nov 30 11:48:31 2024 -0500 Add EnableCopyOnWrite flag commit 0ca898a5afdc87f98dbb081aeed1f6905583f865 Merge: f1e6da3d 54c52be7 Author: Miepee <38186597+Miepee@users.noreply.github.com> Date: Fri Nov 29 13:48:41 2024 +0100 Merge pull request #1983 from UnderminersTeam/dependabot/nuget/xunit-2.9.2 Bump xunit from 2.9.0 to 2.9.2 commit f1e6da3d2dd6e066d7b670f6ccb9e149cc7bcb70 Merge: 8626c6ec a1fadae6 Author: Miepee <38186597+Miepee@users.noreply.github.com> Date: Fri Nov 29 13:48:25 2024 +0100 Merge pull request #1963 from luizzeroxis/fix-2024-6-masks Fix 2024.6 masks not showing commit a1fadae6ab0e231a5766aa901f1f7030eea91e35 Author: luizzeroxis Date: Thu Nov 28 21:38:08 2024 -0300 Add XML documentation to Width and Height of MaskEntry commit 54c52be7a57308f828cfb6b2088c04d575175a8c Author: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu Nov 28 02:53:22 2024 +0000 Bump xunit from 2.9.0 to 2.9.2 Bumps [xunit](https://github.com/xunit/xunit) from 2.9.0 to 2.9.2. - [Commits](https://github.com/xunit/xunit/compare/v2-2.9.0...v2-2.9.2) --- updated-dependencies: - dependency-name: xunit dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] commit 8626c6ec75301f105a3d76246adf4cbc74cd9391 Merge: 5238dd16 ce0dfd58 Author: Miepee <38186597+Miepee@users.noreply.github.com> Date: Wed Nov 27 09:40:54 2024 +0100 Merge pull request #1982 from UnderminersTeam/dependabot/nuget/Microsoft.Windows.Compatibility-9.0.0 Bump Microsoft.Windows.Compatibility from 5.0.2 to 9.0.0 commit ce0dfd585932af39623ad1567a838a245d81ca96 Author: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed Nov 27 08:33:23 2024 +0000 Bump Microsoft.Windows.Compatibility from 5.0.2 to 9.0.0 Bumps [Microsoft.Windows.Compatibility](https://github.com/dotnet/windowsdesktop) from 5.0.2 to 9.0.0. - [Release notes](https://github.com/dotnet/windowsdesktop/releases) - [Commits](https://github.com/dotnet/windowsdesktop/compare/v5.0.2...v9.0.0) --- updated-dependencies: - dependency-name: Microsoft.Windows.Compatibility dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] commit 5238dd1664f203021cfbcbed4b513116bd79434f Merge: 5bbc2576 0ee9d276 Author: Miepee <38186597+Miepee@users.noreply.github.com> Date: Wed Nov 27 09:31:19 2024 +0100 Merge pull request #1980 from UnderminersTeam/dependabot/nuget/System.ComponentModel.Composition-9.0.0 Bump System.ComponentModel.Composition from 8.0.0 to 9.0.0 commit 5bbc257635e079639b5b566269f8b572928e9bed Merge: b286cd39 08a20340 Author: Miepee <38186597+Miepee@users.noreply.github.com> Date: Wed Nov 27 09:29:20 2024 +0100 Merge pull request #1939 from Skirlez/master Add ParentEntry to code editor commit b286cd391904cf1a8cc491742b2bb2ddb1fcdd6f Merge: e9a7cf0d bef58f5d Author: Miepee <38186597+Miepee@users.noreply.github.com> Date: Wed Nov 27 09:24:32 2024 +0100 Merge pull request #1979 from UnderminersTeam/dependabot/nuget/Microsoft.CodeAnalysis.Analyzers-3.11.0 Bump Microsoft.CodeAnalysis.Analyzers from 3.3.4 to 3.11.0 commit e9a7cf0d447293037327117a8f83bdc3d1f34f6c Merge: d9df6348 30bd6450 Author: Miepee <38186597+Miepee@users.noreply.github.com> Date: Wed Nov 27 09:24:20 2024 +0100 Merge pull request #1981 from UnderminersTeam/dependabot/nuget/Microsoft.NET.Test.Sdk-17.12.0 Bump Microsoft.NET.Test.Sdk from 17.11.1 to 17.12.0 commit 30bd645094f9423e20cefe72f0e8ca552ef1948f Author: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue Nov 26 02:42:04 2024 +0000 Bump Microsoft.NET.Test.Sdk from 17.11.1 to 17.12.0 Bumps [Microsoft.NET.Test.Sdk](https://github.com/microsoft/vstest) from 17.11.1 to 17.12.0. - [Release notes](https://github.com/microsoft/vstest/releases) - [Changelog](https://github.com/microsoft/vstest/blob/main/docs/releases.md) - [Commits](https://github.com/microsoft/vstest/compare/v17.11.1...v17.12.0) --- updated-dependencies: - dependency-name: Microsoft.NET.Test.Sdk dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] commit 0ee9d2767d5057b4b313937152291710a00048a1 Author: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue Nov 26 02:42:03 2024 +0000 Bump System.ComponentModel.Composition from 8.0.0 to 9.0.0 Bumps [System.ComponentModel.Composition](https://github.com/dotnet/runtime) from 8.0.0 to 9.0.0. - [Release notes](https://github.com/dotnet/runtime/releases) - [Commits](https://github.com/dotnet/runtime/compare/v8.0.0...v9.0.0) --- updated-dependencies: - dependency-name: System.ComponentModel.Composition dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] commit bef58f5db026435c22fea0203231ba5bfb4d5f77 Author: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue Nov 26 02:41:48 2024 +0000 Bump Microsoft.CodeAnalysis.Analyzers from 3.3.4 to 3.11.0 Bumps [Microsoft.CodeAnalysis.Analyzers](https://github.com/dotnet/roslyn-analyzers) from 3.3.4 to 3.11.0. - [Release notes](https://github.com/dotnet/roslyn-analyzers/releases) - [Changelog](https://github.com/dotnet/roslyn-analyzers/blob/main/PostReleaseActivities.md) - [Commits](https://github.com/dotnet/roslyn-analyzers/commits) --- updated-dependencies: - dependency-name: Microsoft.CodeAnalysis.Analyzers dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] commit 08a20340e41d0f68d7e9174601b3fda13e148297 Author: Skirlez Date: Mon Nov 25 21:54:47 2024 +0200 Rename UndertaleObjectReference's CanDragInto to CanChange commit 9c178ee32cfce312cef4cb7e82d5ac0aaef9d129 Merge: 49b3adad d9df6348 Author: colinator27 <17358554+colinator27@users.noreply.github.com> Date: Mon Nov 25 10:21:25 2024 -0500 Merge branch 'master' into underanalyzer commit d9df63482e94581eb4b92ee3d618115b475c66f5 Merge: 8744a525 7c904724 Author: Miepee <38186597+Miepee@users.noreply.github.com> Date: Mon Nov 25 11:59:21 2024 +0100 Merge pull request #1890 from UnderminersTeam/texture-handling-rewrite Compatibility upgrade of scripts involving textures commit 8744a525892d1e7cfbfe9c2da7f5ff0bd9da3d09 Merge: ee5b28ed 804135c6 Author: Miepee <38186597+Miepee@users.noreply.github.com> Date: Mon Nov 25 11:55:46 2024 +0100 Merge pull request #1970 from UnderminersTeam/dependabot/nuget/MSTest.TestFramework-3.6.3 Bump MSTest.TestFramework from 3.5.2 to 3.6.3 commit ee5b28ed03abedc7cddb37ec49b583fae988fb2c Merge: abb4578a 28a3e297 Author: Miepee <38186597+Miepee@users.noreply.github.com> Date: Mon Nov 25 11:55:26 2024 +0100 Merge pull request #1968 from UnderminersTeam/dependabot/nuget/log4net-3.0.3 Bump log4net from 2.0.17 to 3.0.3 commit abb4578a990857dce9142e767a90385681ebbce4 Merge: 1cdae186 9fb34bf3 Author: Miepee <38186597+Miepee@users.noreply.github.com> Date: Mon Nov 25 11:54:24 2024 +0100 Merge pull request #1971 from UnderminersTeam/dependabot/nuget/MSTest.TestAdapter-3.6.3 Bump MSTest.TestAdapter from 3.5.2 to 3.6.3 commit 1cdae1860f6651f360f8769706acd95049b2e418 Merge: f2caf89a b1c4154d Author: Miepee <38186597+Miepee@users.noreply.github.com> Date: Mon Nov 25 11:54:11 2024 +0100 Merge pull request #1977 from UnderminersTeam/dependabot/nuget/Fody-6.9.1 Bump Fody from 6.8.1 to 6.9.1 commit f2caf89a4c4c919f580691bf7e2012360e147469 Merge: ad5f211a 159dfd5a Author: Miepee <38186597+Miepee@users.noreply.github.com> Date: Mon Nov 25 11:53:43 2024 +0100 Merge pull request #1902 from luizzeroxis/app-manifest Add manifest to enable better styled dialogs commit 7c904724847da3b56c054fc7057df17c0c58976e Author: Miepee <38186597+Miepee@users.noreply.github.com> Date: Mon Nov 25 11:51:39 2024 +0100 Update UndertaleModTool/Scripts/Community Scripts/FontEditor.csx commit ad5f211af44d59adc5fb1a14120b65b827ea9af4 Merge: caf04126 216d7b5c Author: colinator27 <17358554+colinator27@users.noreply.github.com> Date: Sat Nov 23 12:17:11 2024 -0500 Merge pull request #1941 from zivmaor/saving-child-error fix #1940 commit 29d238d60d8a5a9827e02827b5537f538b47d18c Author: colinator27 <17358554+colinator27@users.noreply.github.com> Date: Sat Nov 23 12:15:33 2024 -0500 Fix missing parenthesis commit 49b3adad5988d90e325ea345013eff94541a8479 Merge: 8377c5a4 caf04126 Author: colinator27 <17358554+colinator27@users.noreply.github.com> Date: Sat Nov 23 12:08:02 2024 -0500 Merge branch 'master' into underanalyzer commit b1c4154d7e06f5d13a87176db95254fc4ecf0968 Author: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed Nov 20 03:06:22 2024 +0000 Bump Fody from 6.8.1 to 6.9.1 Bumps [Fody](https://github.com/Fody/Fody) from 6.8.1 to 6.9.1. - [Release notes](https://github.com/Fody/Fody/releases) - [Commits](https://github.com/Fody/Fody/compare/6.8.1...6.9.1) --- updated-dependencies: - dependency-name: Fody dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] commit 9fb34bf340b7d8257172984374ef9e6029749896 Author: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed Nov 13 02:36:08 2024 +0000 Bump MSTest.TestAdapter from 3.5.2 to 3.6.3 Bumps [MSTest.TestAdapter](https://github.com/microsoft/testfx) from 3.5.2 to 3.6.3. - [Release notes](https://github.com/microsoft/testfx/releases) - [Changelog](https://github.com/microsoft/testfx/blob/main/docs/Changelog.md) - [Commits](https://github.com/microsoft/testfx/commits) --- updated-dependencies: - dependency-name: MSTest.TestAdapter dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] commit 804135c69e48eae2ff14f12fa74df293ca247a46 Author: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed Nov 13 02:35:12 2024 +0000 Bump MSTest.TestFramework from 3.5.2 to 3.6.3 Bumps [MSTest.TestFramework](https://github.com/microsoft/testfx) from 3.5.2 to 3.6.3. - [Release notes](https://github.com/microsoft/testfx/releases) - [Changelog](https://github.com/microsoft/testfx/blob/main/docs/Changelog.md) - [Commits](https://github.com/microsoft/testfx/commits) --- updated-dependencies: - dependency-name: MSTest.TestFramework dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] commit 28a3e297e8acead4e6b268e66ff7d858b27161f6 Author: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri Nov 8 02:13:56 2024 +0000 Bump log4net from 2.0.17 to 3.0.3 Bumps [log4net](https://github.com/apache/logging-log4net) from 2.0.17 to 3.0.3. - [Release notes](https://github.com/apache/logging-log4net/releases) - [Commits](https://github.com/apache/logging-log4net/compare/rel/2.0.17...rel/3.0.3) --- updated-dependencies: - dependency-name: log4net dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] commit 05b4961b73508d7746e2249936410e9f89ccdcc5 Author: luizzeroxis Date: Sat Nov 2 18:17:41 2024 -0300 Fix 2024.6 masks not showing sometimes commit 159dfd5a65b74f706e5f021c1f7e641e922fd8b0 Author: Vladislav Stepanov Date: Mon Oct 28 19:20:47 2024 +0300 Uncomment the Windows 10 `supportedOS` tag commit 4f8782813d5abee903819085e8824ed68a3f927c Author: Vladislav Stepanov Date: Mon Oct 28 19:16:30 2024 +0300 Remove unsupported Windows versions. commit fb746806261853849d82959b3370db9a0688329a Author: Skirlez Date: Thu Oct 24 11:53:15 2024 +0300 Added CanDragInto property to UndertaleObjectReference commit 54968e3ef8592b1a4cbf2894b31f01fd9e50fe49 Author: zivmaor Date: Wed Oct 23 17:29:49 2024 +0300 minor code improvement commit 08ad289f7eed863b2ca85b02500a5aa8cce9f7f2 Author: zivmaor Date: Sun Oct 13 15:40:45 2024 +0300 Apple changes from 60515b1 to UndertaleResourceReferenceMap commit 992524c14ae167412de4797f1bb8d39e96743cc5 Author: zivmaor Date: Wed Oct 9 00:38:47 2024 +0300 Add error handling to FindReferencesTypesDialog commit 60515b110735120e74bba7c05e7a853709f60258 Author: zivmaor Date: Wed Oct 9 00:08:43 2024 +0300 Fix GetReferencesOfObject for Bytecode version 15 Move check for language to only be done on bytecode version 16 and up. See https://github.com/UnderminersTeam/UndertaleModTool/wiki/Bytecode-version-differences. Fixes #1946. commit 216d7b5c3f29185ecebd348b30245fab92da8fb1 Author: zivmaor Date: Sun Oct 6 13:39:46 2024 +0300 fix #1940 Fix DecompiledEditor and DisassemblyEditor erroneously reporting that they have changed when loading a child code entry, resulting in an error when trying to save. Also minor grammar fix. commit caf04126f273b57bb2200b8739bb9d57e25d8f92 Merge: 9f4f13a5 631db508 Author: Miepee <38186597+Miepee@users.noreply.github.com> Date: Sun Oct 6 09:29:26 2024 +0200 Merge pull request #1923 from zivmaor/unreferencedAssetsFix fix #1747 commit 8377c5a4a2ebe6e499c2cb328b25b5b82b08b806 Merge: 8c5462c4 34c0dec9 Author: Miepee <38186597+Miepee@users.noreply.github.com> Date: Sun Oct 6 09:29:00 2024 +0200 Merge pull request #1919 from ChronoVortex/struct-fix Remove struct keyword commit f9b6d5a0d813013c14612d7ee82d629b038d9415 Author: Skirlez Date: Sat Oct 5 16:56:59 2024 +0300 Add parent entry to code editor commit 631db5084869812a3d2746ca15314b0c1440121c Author: zivmaor Date: Fri Oct 4 22:32:46 2024 +0300 whitespace and style fix commit e5cba7e7ee679b0e626d12e1410642d900ebf31d Author: luizzeroxis Date: Mon Sep 23 20:40:20 2024 -0300 Removed name and version from manifest commit 1a4759e84749304bcbcdd1273c18f9d33784e834 Author: zivmaor Date: Fri Sep 20 15:48:55 2024 +0300 fix #1747 commit 34c0dec9771c13e5d0d6f96df999a538e23c394e Author: ChronoVortex <33610911+ChronoVortex@users.noreply.github.com> Date: Tue Sep 17 20:27:53 2024 -0700 remove struct keyword commit 9aef94cc393b71ebf5e00ef4a8dc857f76afa442 Author: colinator27 <17358554+colinator27@users.noreply.github.com> Date: Sun Sep 8 16:33:19 2024 -0400 Address some reviews commit 73ab086c41186514da836fea3a048bdccedb12f3 Author: colinator27 <17358554+colinator27@users.noreply.github.com> Date: Sun Sep 8 15:58:31 2024 -0400 Replace WebClient with HttpClient commit 9f4f13a52495f82663dd125c0028519850faba83 Merge: 5bf01fa2 b2833d6a Author: Miepee <38186597+Miepee@users.noreply.github.com> Date: Sat Sep 7 22:45:25 2024 +0200 Merge pull request #1904 from UnderminersTeam/dependabot/nuget/Microsoft.NET.Test.Sdk-17.11.1 Bump Microsoft.NET.Test.Sdk from 17.11.0 to 17.11.1 commit 5bf01fa286c17a7355e09e89a45d8bb7a3c4fbd0 Merge: 69095cdf 0ff48290 Author: Miepee <38186597+Miepee@users.noreply.github.com> Date: Sat Sep 7 22:45:07 2024 +0200 Merge pull request #1906 from UnderminersTeam/readme-screenshot-update Update RIBBIT screenshot on README commit 0ff482901bf83f3bb935656a1071e38030987f7a Author: colinator27 <17358554+colinator27@users.noreply.github.com> Date: Sat Sep 7 14:24:34 2024 -0400 Update RIBBIT screenshot on README commit ff7845ff5b66f708bb21971bb6a84f8435d4408e Author: Luiz Pontes Date: Sat Sep 7 12:08:27 2024 -0300 Manifest name and version (it probably doesn't matter) commit b2833d6a4eff03b972cd3c9e66332db49fd081d8 Author: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri Sep 6 02:27:51 2024 +0000 Bump Microsoft.NET.Test.Sdk from 17.11.0 to 17.11.1 Bumps [Microsoft.NET.Test.Sdk](https://github.com/microsoft/vstest) from 17.11.0 to 17.11.1. - [Release notes](https://github.com/microsoft/vstest/releases) - [Changelog](https://github.com/microsoft/vstest/blob/main/docs/releases.md) - [Commits](https://github.com/microsoft/vstest/compare/v17.11.0...v17.11.1) --- updated-dependencies: - dependency-name: Microsoft.NET.Test.Sdk dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] commit 7778b7609e094f83deb425aac00afdb4346599a3 Author: luizzeroxis Date: Wed Sep 4 19:49:20 2024 -0300 Add manifest to enable better styled dialogs commit 69095cdf7773fb7b35bea6a78b2476f343c1a92f Merge: dc1e2097 64a6b988 Author: Miepee <38186597+Miepee@users.noreply.github.com> Date: Sat Aug 31 22:20:19 2024 +0200 Merge pull request #1899 from Garethp/#1898-Texture-NRE-Fix Issue #1898: Prevent NREs when adding new Texture Page Items or Embedded textures commit 64a6b9880d4b57c6e213736cbe9666c3b1911f91 Author: Gareth Date: Sat Aug 31 19:10:42 2024 +0100 Update docbloc comments to use `see langword` commit ae0f210aed5d385ea948aa6b7e3d055c8dbe082c Author: Gareth Date: Fri Aug 30 23:54:41 2024 +0100 Documenting new return values and clearing the Image source when image is null commit 0dbcf7837e07bed4d713327c9e11e78e64458427 Author: Gareth Date: Fri Aug 30 23:14:08 2024 +0100 Add some null checks to ensure that we don't hit any NRE's when adding new Texture Page Items or new Embedded Textures. Closes issue #1898 commit 8008bd084972ecd102c708df9901364f289d138d Author: colinator27 <17358554+colinator27@users.noreply.github.com> Date: Tue Aug 27 12:07:45 2024 -0400 Manually add DEBUG constant to nightly builds commit dc1e209759472845681f66b1dafffcd1a1d46867 Merge: cdde5733 605198bd Author: Miepee <38186597+Miepee@users.noreply.github.com> Date: Mon Aug 26 22:27:40 2024 +0200 Merge pull request #1892 from UnderminersTeam/reword_id_tooltip Slightly reword asset ID hover tooltip commit 605198bd2dffa8619950776fe077387df23799eb Author: colinator27 <17358554+colinator27@users.noreply.github.com> Date: Mon Aug 26 16:20:13 2024 -0400 Slightly reword asset ID hover tooltip Tooltip is now more encompassing than just "object," since it can also be any asset (e.g. a sprite) or item (e.g. a texture page item or string). commit cdde5733d852a9eeaf285da03630a3666587f2cc Merge: fe84e46d 695a8359 Author: Miepee <38186597+Miepee@users.noreply.github.com> Date: Mon Aug 26 22:16:39 2024 +0200 Merge pull request #1891 from UnderminersTeam/net8_workflows Update workflows to setup .NET 8 commit 695a83591be4ed2b3504110f0c9aec5d6193f004 Author: colinator27 <17358554+colinator27@users.noreply.github.com> Date: Mon Aug 26 16:10:39 2024 -0400 Update workflows to setup .NET 8 commit 59be42c4a2af385c838938af48d4c0dce83ed182 Author: colinator27 <17358554+colinator27@users.noreply.github.com> Date: Mon Aug 26 15:53:39 2024 -0400 A few additional compatibility script upgrades commit c4b8b805d789c520f8a7ef308c629ab0f39e3e93 Author: colinator27 <17358554+colinator27@users.noreply.github.com> Date: Mon Aug 26 15:28:10 2024 -0400 Compatibility upgrade of texture scripts commit fe84e46d2896fcb5f16cec8a719d32cea9953fbb Merge: 68a14b91 b70fe3e8 Author: Miepee <38186597+Miepee@users.noreply.github.com> Date: Mon Aug 26 06:28:52 2024 +0200 Merge pull request #1889 from UnderminersTeam/net8 Upgrade to .net8 commit b70fe3e8dadeba2df9463c17f5f720b0d1eb6ec5 Author: Miepee <38186597+Miepee@users.noreply.github.com> Date: Mon Aug 26 00:12:17 2024 +0200 forgot a project the sequel commit e383cc19c486f68aadcd06eeabaec27b1e2f0fbb Author: Miepee <38186597+Miepee@users.noreply.github.com> Date: Mon Aug 26 00:04:31 2024 +0200 forgot a project commit 547926d646a2cd0f6b1156ee0b7c5820da1116bd Author: Miepee <38186597+Miepee@users.noreply.github.com> Date: Sun Aug 25 23:25:32 2024 +0200 Upgrade to .net8 commit 68a14b918cb58d0902ef5031dd408dd52b5e1f6b Merge: fe1b45b6 a2fa3e06 Author: Miepee <38186597+Miepee@users.noreply.github.com> Date: Sun Aug 25 14:34:28 2024 +0200 Merge pull request #1859 from NC-devC/droidfixnew More fixes for TouchControlsEnabler. commit fe1b45b6c5c468774254abdf5cd7b2c1f56f8824 Merge: 6f470f85 4d2ef0b6 Author: Miepee <38186597+Miepee@users.noreply.github.com> Date: Sat Aug 24 22:20:28 2024 +0200 Merge pull request #1887 from UnderminersTeam/fix-string-bounds-checks Fix string reading bounds checks commit 4d2ef0b62e2adbf56adc6460c48fe876a328190f Author: colinator27 <17358554+colinator27@users.noreply.github.com> Date: Sat Aug 24 15:18:55 2024 -0400 Fix string reading bounds checks commit a2fa3e063efdfc2fb7a15f186663e235a5a86227 Author: NC Date: Sat Aug 24 21:17:49 2024 +0300 Config ratio fix, added horizontal one Now it works better commit a1444313c60067867a3ac3128fa03a160f9e6f32 Author: NC Date: Sat Aug 24 20:43:51 2024 +0300 renamed variables, and removed useless one commit 6f470f8581f2d784b373d6422306d6ac89aed371 Merge: 3dd882b2 038e17f1 Author: Miepee <38186597+Miepee@users.noreply.github.com> Date: Fri Aug 23 22:26:17 2024 +0200 Merge pull request #1886 from UnderminersTeam/code-serialization-cleanup Code (de)serialization cleanup and optimization commit 038e17f123618d60ca9ae453348c982b04b64b9d Author: colinator27 <17358554+colinator27@users.noreply.github.com> Date: Fri Aug 23 15:58:01 2024 -0400 Adjust newlines commit cdd112f44f263d607aa88f172d6e1cea0bb7a5dc Merge: 4b6870b5 3dd882b2 Author: colinator27 <17358554+colinator27@users.noreply.github.com> Date: Fri Aug 23 15:03:00 2024 -0400 Merge branch 'master' into code-serialization-cleanup commit 3dd882b2ddd4cbd2909935235dc30fd12b0b6e63 Merge: 4e3f420a 28d7c874 Author: Miepee <38186597+Miepee@users.noreply.github.com> Date: Fri Aug 23 21:02:15 2024 +0200 Merge pull request #1885 from UnderminersTeam/add-negative-length-checks Add negative length checks during deserialization commit 28d7c874e94c7e4198b83f66b6476777e0cbb3b7 Author: colinator27 <17358554+colinator27@users.noreply.github.com> Date: Fri Aug 23 14:57:05 2024 -0400 Fix missing parenthesis commit 80627b6fd8c22c507f7a801f332c6537c9019803 Merge: 6d6b0f47 4e3f420a Author: colinator27 <17358554+colinator27@users.noreply.github.com> Date: Fri Aug 23 14:56:30 2024 -0400 Merge branch 'master' into add-negative-length-checks commit 4e3f420abb9b9eb723f14f61f465d628997aff8f Merge: 320d7892 d889e5ef Author: Miepee <38186597+Miepee@users.noreply.github.com> Date: Fri Aug 23 20:53:18 2024 +0200 Merge pull request #1884 from UnderminersTeam/remove-extra-debug-checks Remove extra debug checks commit 4b6870b55f11643bda7d768c129d53802fb2c36f Author: colinator27 <17358554+colinator27@users.noreply.github.com> Date: Fri Aug 23 14:49:07 2024 -0400 Code (de)serialization cleanup and optimization commit 6d6b0f475e72eb5f4235cd0cc7c77f25545af80b Author: colinator27 <17358554+colinator27@users.noreply.github.com> Date: Fri Aug 23 14:43:36 2024 -0400 Add negative length checks during deserialization commit d889e5ef91a72f7b3999124a8f0dddb4585db805 Author: colinator27 <17358554+colinator27@users.noreply.github.com> Date: Fri Aug 23 14:33:27 2024 -0400 Remove extra debug checks commit 320d7892729742eb792f29304f2543f3763fef47 Author: colinator27 <17358554+colinator27@users.noreply.github.com> Date: Fri Aug 23 12:47:21 2024 -0400 Merge in changes for 0.6.1.0 commit b93dcc5bcef59be31c690789b194d58a31431d34 Author: colinator27 <17358554+colinator27@users.noreply.github.com> Date: Fri Aug 23 12:08:58 2024 -0400 Bump version to 0.6.1.0 commit 0cea983f0026f35fb4453a83e1afc3b14b50c1c5 Author: Liu Wenyuan <15816141883@163.com> Date: Fri Aug 23 07:47:10 2024 +0800 Address suggestions, improve comments commit fb82caffe91826ce9e91944f14561b368a5b070e Author: Liu Wenyuan <15816141883@163.com> Date: Sun Aug 18 19:26:14 2024 +0800 Remove unnecessary brackets commit 51411072876169e99ade9863a0b3cb81484e542f Author: Liu Wenyuan <15816141883@163.com> Date: Sun Aug 18 19:03:34 2024 +0800 Address review suggestions commit 14cd8e434eff45eabfb7328c607663ae5441defa Author: Liu Wenyuan <15816141883@163.com> Date: Sat Aug 17 20:41:01 2024 +0800 Ok a compromise is possible commit 71a162d5a5493574b4d497a9f109bd575a939727 Author: Liu Wenyuan <15816141883@163.com> Date: Sat Aug 17 20:39:08 2024 +0800 Get the most use of the three quotation marks! Allt hay newlines commit c28b06e93e800991af73921c5e5428af192292e0 Author: Liu Wenyuan <15816141883@163.com> Date: Sat Aug 17 18:26:39 2024 +0800 Fix typo in intro error message commit 6605dfcacc12c506c48d4f2a8ede9b9f2c1eb1fd Author: Liu Wenyuan <15816141883@163.com> Date: Sat Aug 17 18:02:00 2024 +0800 Address review comment suggestions commit 7b37bc7ecc0bd00592cad8e56e80e64f6a19daff Author: Liu Wenyuan <15816141883@163.com> Date: Sat Aug 17 07:11:58 2024 +0800 More cleanup commit e5a22b32c96ba008d3f31e434deee88f3fce2f9c Author: Liu Wenyuan <15816141883@163.com> Date: Sat Aug 17 07:00:54 2024 +0800 Remove unneeded border drawing setup, cleanup commit f067fbd755852f659e1a9686f564fc32c598b7ab Author: Liu Wenyuan <15816141883@163.com> Date: Sat Aug 17 06:40:00 2024 +0800 Fix Joystick Menu and default buttons commit ba593ac95bc05acbd7a997009425005e1f88c843 Author: Liu Wenyuan <15816141883@163.com> Date: Sat Aug 17 00:53:36 2024 +0800 Fix door destination commit 6d69878675701ab05b586376a0ddbec30f021181 Author: Liu Wenyuan <15816141883@163.com> Date: Sat Aug 17 00:51:29 2024 +0800 Fix braces, ask to enable Dog Shrine commit 3326459d3e67da130f9c2a013f880f8867a2cbf5 Author: Liu Wenyuan <15816141883@163.com> Date: Sat Aug 17 00:42:39 2024 +0800 Add NXTALE fixes for Xbox version and gamepad commit 0f559dbd61613b5b46d9e19e8e28bd164e065d4f Author: Liu Wenyuan <15816141883@163.com> Date: Sat Aug 17 00:41:48 2024 +0800 Rename first so diffs look normal commit b238ba67d78347b53c708308093d52e6c5446495 Author: Jacky720 <32578221+Jacky720@users.noreply.github.com> Date: Thu Aug 22 16:28:04 2024 -0400 Remove old code commit 994f29b83974de0f61d8e2191a1c22fa40319431 Author: Jacky720 <32578221+Jacky720@users.noreply.github.com> Date: Sat Aug 17 11:03:52 2024 -0400 we don't need this here commit 3c958fbff9d8a8fa55b1c32c40c2eeeac4443811 Author: Jacky720 <32578221+Jacky720@users.noreply.github.com> Date: Sat Aug 17 10:57:23 2024 -0400 Fix Branch being overwritten less precisely commit 964ebd082f14f0d65dde231584ddc99f02b0493d Author: colinator27 <17358554+colinator27@users.noreply.github.com> Date: Sat Aug 17 12:49:23 2024 -0400 Fix EmSize being always parsed as 0, if a float commit 147a289d4729644dcbb367ba97d6429d1b7f094c Merge: 405f10f6 ad4cca11 Author: Miepee <38186597+Miepee@users.noreply.github.com> Date: Fri Aug 23 02:08:46 2024 +0200 Merge pull request #1872 from Dobby233Liu/nxtale-script-xbox-support-1 Add NXTALE fixes for Xbox version and gamepad commit ad4cca1110543a36737e29ca27154bcc685fd193 Author: Miepee <38186597+Miepee@users.noreply.github.com> Date: Fri Aug 23 02:08:26 2024 +0200 Update UndertaleModTool/Scripts/Builtin Scripts/RunSwitchAndXboxOnPC.csx commit 7c5822a933eb3247fa0e04783a0621a65883ca6b Author: Liu Wenyuan <15816141883@163.com> Date: Fri Aug 23 07:47:10 2024 +0800 Address suggestions, improve comments commit 405f10f665b1f337daa4d10d51a9d3d786887bec Merge: 468a6edb bcdaf415 Author: Miepee <38186597+Miepee@users.noreply.github.com> Date: Thu Aug 22 23:37:53 2024 +0200 Merge pull request #1870 from UnderminersTeam/texture-handling-rewrite Rewrite texture handling commit 468a6edbb873cdd5c6ed24cf0c157d16cd3b51a0 Merge: a1b0fdd2 520482e1 Author: Miepee <38186597+Miepee@users.noreply.github.com> Date: Thu Aug 22 23:21:28 2024 +0200 Merge pull request #1877 from luizzeroxis/master Changed quitting to show a yes/no/cancel dialog instead of 2 yes/no dialogs commit a1b0fdd221858072c463248207fea29fa9aa1b86 Merge: 77159770 eba41ca3 Author: Miepee <38186597+Miepee@users.noreply.github.com> Date: Thu Aug 22 23:08:57 2024 +0200 Merge pull request #1856 from Dobby233Liu/Dobby233Liu-UTWithJSON-loader-speedup-1 Use buffer for reading JSON in UndertaleWithJSONs commit 520482e12a7b071942c2f8e941aebdd4aea6170e Author: luizzeroxis Date: Thu Aug 22 17:56:44 2024 -0300 Improved code readability commit 7715977050612b7c814dd9adf47809b54309cee2 Merge: 16679bd4 077ac61b Author: Miepee <38186597+Miepee@users.noreply.github.com> Date: Thu Aug 22 22:50:25 2024 +0200 Merge pull request #1874 from Jacky720/lts-differentiation Fix Branch being overwritten less precisely commit 077ac61b58efefdfc1443ae001f2ed20608f981f Author: Jacky720 <32578221+Jacky720@users.noreply.github.com> Date: Thu Aug 22 16:28:04 2024 -0400 Remove old code commit bcdaf415e138432fb05ff92548f36b39f5436d6d Merge: e8e036f7 16679bd4 Author: colinator27 <17358554+colinator27@users.noreply.github.com> Date: Thu Aug 22 15:51:59 2024 -0400 Merge branch 'master' into texture-handling-rewrite commit 16679bd47d09435e04c449e9a70af10a2c16cc4f Merge: c82e5f74 931fda66 Author: Miepee <38186597+Miepee@users.noreply.github.com> Date: Thu Aug 22 21:13:14 2024 +0200 Merge pull request #1876 from UnderminersTeam/nightly-prerelease-flag Mark nightly releases as prereleases commit c82e5f741d35dbff6680137d3fbf683b6d7f817f Merge: 88eeb308 db7e1b1e Author: Miepee <38186597+Miepee@users.noreply.github.com> Date: Thu Aug 22 21:01:39 2024 +0200 Merge pull request #1881 from UnderminersTeam/dependabot/nuget/Microsoft.NET.Test.Sdk-17.11.0 Bump Microsoft.NET.Test.Sdk from 17.10.0 to 17.11.0 commit db7e1b1eddd424386cdfc9c4686a63e02ab35dd9 Author: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed Aug 21 02:38:09 2024 +0000 Bump Microsoft.NET.Test.Sdk from 17.10.0 to 17.11.0 Bumps [Microsoft.NET.Test.Sdk](https://github.com/microsoft/vstest) from 17.10.0 to 17.11.0. - [Release notes](https://github.com/microsoft/vstest/releases) - [Changelog](https://github.com/microsoft/vstest/blob/main/docs/releases.md) - [Commits](https://github.com/microsoft/vstest/compare/v17.10.0...v17.11.0) --- updated-dependencies: - dependency-name: Microsoft.NET.Test.Sdk dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] commit 22e6de2eeb52305d05cb4c28f7e6e7cdd3e72f26 Author: luizzeroxis Date: Sun Aug 18 15:42:21 2024 -0300 Removed quit var and fixed style commit 8b2e32e563d3b3812f2ec50bde2f629580264de9 Author: luizzeroxis Date: Sun Aug 18 14:31:47 2024 -0300 Changed quitting to show a yes/no/cancel dialog instead of 2 yes/no dialogs commit 931fda66b74f643bdc326d0401bfdc96891df255 Author: colinator27 <17358554+colinator27@users.noreply.github.com> Date: Sun Aug 18 10:59:01 2024 -0400 Mark nightly releases as prereleases commit e8e036f7956597679e5460d71511ff5fb8fea0a4 Author: colinator27 <17358554+colinator27@users.noreply.github.com> Date: Sun Aug 18 10:27:13 2024 -0400 Address reviews commit 1a1fcdf469eb255f777e6550f9a942d3f2580bb5 Author: Liu Wenyuan <15816141883@163.com> Date: Sun Aug 18 19:26:14 2024 +0800 Remove unnecessary brackets commit 825c1378285f0f1943656feb7cb0a807f884a704 Author: Liu Wenyuan <15816141883@163.com> Date: Sun Aug 18 19:03:34 2024 +0800 Address review suggestions commit 88eeb30829662721b4817355100a3f0c6bfaecb5 Merge: 308e7f74 1f6fa627 Author: Miepee <38186597+Miepee@users.noreply.github.com> Date: Sun Aug 18 09:32:45 2024 +0200 Merge pull request #1875 from UnderminersTeam/fix-emsize Fix EmSize being always parsed as 0, if a float commit 90834d81b095b1ce8d59a260ea17ec37bd60baba Author: colinator27 <17358554+colinator27@users.noreply.github.com> Date: Sat Aug 17 19:03:16 2024 -0400 Address reviews, and some small changes commit 1f6fa627841a146f182b53c39187ead4f39a6372 Author: colinator27 <17358554+colinator27@users.noreply.github.com> Date: Sat Aug 17 12:49:23 2024 -0400 Fix EmSize being always parsed as 0, if a float commit 7527b38b7462f7f91014d089ef397ce48aa2f368 Author: Jacky720 <32578221+Jacky720@users.noreply.github.com> Date: Sat Aug 17 11:03:52 2024 -0400 we don't need this here commit 7609366bb8cbf7aca808a9ae1c78711b88746b1e Author: Jacky720 <32578221+Jacky720@users.noreply.github.com> Date: Sat Aug 17 10:57:23 2024 -0400 Fix Branch being overwritten less precisely commit 65c4c0240c7b13c8a77d3db96c13467fe681b704 Merge: 56deb0ce 308e7f74 Author: Jacky720 <32578221+Jacky720@users.noreply.github.com> Date: Sat Aug 17 10:56:00 2024 -0400 Merge remote-tracking branch 'upstream/master' into lts-differentiation commit 56deb0ce1fcc171ebd1e51f916e51398e5f3c551 Merge: 4a4d55ea 1df139dc Author: Jacky720 <32578221+Jacky720@users.noreply.github.com> Date: Sat Aug 17 10:55:54 2024 -0400 Merge branch 'lts-differentiation' of https://github.com/Jacky720/UndertaleModTool into lts-differentiation commit 26aa32a96247cf71338720ba17e9680e3f769a9d Author: Liu Wenyuan <15816141883@163.com> Date: Sat Aug 17 20:41:01 2024 +0800 Ok a compromise is possible commit fc71a0bffc1167a36f4f13d4a8fb54b23f0ac2e8 Author: Liu Wenyuan <15816141883@163.com> Date: Sat Aug 17 20:39:08 2024 +0800 Get the most use of the three quotation marks! Allt hay newlines commit 3751d227da32cb986e636eeb237374d8152fe6a0 Author: Liu Wenyuan <15816141883@163.com> Date: Sat Aug 17 18:26:39 2024 +0800 Fix typo in intro error message commit b4b3ed51d1a9eef523d943b5e519ec1cbd8d6bf9 Author: Liu Wenyuan <15816141883@163.com> Date: Sat Aug 17 18:02:00 2024 +0800 Address review comment suggestions commit 308e7f7446c660be113575e11ecac663b4bcd060 Merge: f5774361 67ef4c4c Author: Miepee <38186597+Miepee@users.noreply.github.com> Date: Sat Aug 17 11:51:14 2024 +0200 Merge pull request #1865 from UnderminersTeam/dependabot/nuget/MSTest.TestFramework-3.5.2 Bump MSTest.TestFramework from 3.5.0 to 3.5.2 commit f57743610c685e44435cfe99d3325fd6de4103e8 Merge: b53ce21d 9acbe1bb Author: Miepee <38186597+Miepee@users.noreply.github.com> Date: Sat Aug 17 11:50:59 2024 +0200 Merge pull request #1866 from UnderminersTeam/dependabot/nuget/MSTest.TestAdapter-3.5.2 Bump MSTest.TestAdapter from 3.5.0 to 3.5.2 commit b53ce21dfd6ba9a5ba7016e4b238bfad0a6d89e4 Merge: 5f2b1afa 50e96640 Author: Miepee <38186597+Miepee@users.noreply.github.com> Date: Sat Aug 17 11:50:46 2024 +0200 Merge pull request #1871 from UnderminersTeam/dependabot/nuget/Microsoft.CodeAnalysis.CSharp.Scripting-4.11.0 Bump Microsoft.CodeAnalysis.CSharp.Scripting from 4.10.0 to 4.11.0 commit f16a254163c3c60aaac558bca90fc63c71a16dce Author: Liu Wenyuan <15816141883@163.com> Date: Sat Aug 17 07:11:58 2024 +0800 More cleanup commit 438ec5aa6022fc1eddc65519753ebd854e83b160 Author: Liu Wenyuan <15816141883@163.com> Date: Sat Aug 17 07:00:54 2024 +0800 Remove unneeded border drawing setup, cleanup commit dfc4c042fc4d78a57432e1b1c33b92e07f4b21d9 Author: Liu Wenyuan <15816141883@163.com> Date: Sat Aug 17 06:40:00 2024 +0800 Fix Joystick Menu and default buttons commit 064410860847abaa364aebf2bb3665c8681bdedc Author: colinator27 <17358554+colinator27@users.noreply.github.com> Date: Fri Aug 16 16:37:04 2024 -0400 Switch a remaining RGBA to BGRA commit 8c69e79f64d103a9a43dd8e9b49fea55e3222acb Author: Liu Wenyuan <15816141883@163.com> Date: Sat Aug 17 00:53:36 2024 +0800 Fix door destination commit 36828edfe279bddd0dcf23cf4e5d870a63734900 Author: Liu Wenyuan <15816141883@163.com> Date: Sat Aug 17 00:51:29 2024 +0800 Fix braces, ask to enable Dog Shrine commit bb707686d04f74c884af5b76004ad140f85c8270 Author: Liu Wenyuan <15816141883@163.com> Date: Sat Aug 17 00:42:39 2024 +0800 Add NXTALE fixes for Xbox version and gamepad commit 704b6ea9221f21db715fc8a82314bbff53c71ddc Author: Liu Wenyuan <15816141883@163.com> Date: Sat Aug 17 00:41:48 2024 +0800 Rename first so diffs look normal commit 50e966402f404661100657c8e584fec6c678e9f2 Author: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri Aug 16 02:59:29 2024 +0000 Bump Microsoft.CodeAnalysis.CSharp.Scripting from 4.10.0 to 4.11.0 Bumps [Microsoft.CodeAnalysis.CSharp.Scripting](https://github.com/dotnet/roslyn) from 4.10.0 to 4.11.0. - [Release notes](https://github.com/dotnet/roslyn/releases) - [Changelog](https://github.com/dotnet/roslyn/blob/main/docs/Breaking%20API%20Changes.md) - [Commits](https://github.com/dotnet/roslyn/commits) --- updated-dependencies: - dependency-name: Microsoft.CodeAnalysis.CSharp.Scripting dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] commit c020a948964068dc504758b0dcf0a733a73c8299 Author: colinator27 <17358554+colinator27@users.noreply.github.com> Date: Thu Aug 15 18:26:33 2024 -0400 Fix typo commit 03dfdd4f28b032966565fbfd05ad9965cbf16539 Author: colinator27 <17358554+colinator27@users.noreply.github.com> Date: Thu Aug 15 16:34:48 2024 -0400 Fix minor WPF error message commit 6838e692b0fef962b99c7a0c0c10cc4371911be1 Author: colinator27 <17358554+colinator27@users.noreply.github.com> Date: Thu Aug 15 16:34:36 2024 -0400 Fix mask import/export on 2024.6+, and cleanup commit 221f201b473adf4acf00af5be2c1a95cf52c56ce Author: colinator27 <17358554+colinator27@users.noreply.github.com> Date: Thu Aug 15 15:45:34 2024 -0400 Cleanup and memory leak fix commit ab6c198bb2ddbfad8661f3f6f30c97c96e007365 Author: colinator27 <17358554+colinator27@users.noreply.github.com> Date: Thu Aug 15 11:29:26 2024 -0400 Undo Microsoft.Windows.Compatibility bump for now commit ce94847ee112b9f4ff62fd4e71a795ee4c7b9529 Author: colinator27 <17358554+colinator27@users.noreply.github.com> Date: Thu Aug 15 11:16:05 2024 -0400 Fix spacing commit 70b6ab21eefc12a26d595ddb429ebb02fc2f557a Author: colinator27 <17358554+colinator27@users.noreply.github.com> Date: Thu Aug 15 11:11:19 2024 -0400 Address reviews commit 8c5462c41eab770fa9bcb0da1cf5ed014bc20362 Author: colinator27 <17358554+colinator27@users.noreply.github.com> Date: Wed Aug 14 22:55:51 2024 -0400 Fix ExportAllCode commit c74c24dfed062e87d7858cb493321f38b97079bc Author: colinator27 <17358554+colinator27@users.noreply.github.com> Date: Wed Aug 14 16:26:42 2024 -0400 Some cleanup commit ddea1576b9e6fe69ffb42f612afe8f9a26e8ba10 Author: colinator27 <17358554+colinator27@users.noreply.github.com> Date: Wed Aug 14 16:23:51 2024 -0400 Add two missing comments commit b269fa229ed28005097916a871635f66f42eac5d Author: colinator27 <17358554+colinator27@users.noreply.github.com> Date: Wed Aug 14 16:01:58 2024 -0400 Further optimize BZip2 textures commit 0b4c1c99fecebd4ae46d1a4c60366e2d4c7c6e2a Author: colinator27 <17358554+colinator27@users.noreply.github.com> Date: Wed Aug 14 11:45:13 2024 -0400 Go back to .NET 6 for now commit 74d3ccf9a096e8db43a188a39a412043e4c308d2 Author: colinator27 <17358554+colinator27@users.noreply.github.com> Date: Tue Aug 13 23:04:26 2024 -0400 Rewrite texture handling, update to .NET 8 Initial work, most scripts involving textures will no longer work commit 9acbe1bbb98ff66d896bfc5df1fa2015c928bf3f Author: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed Aug 14 02:29:33 2024 +0000 Bump MSTest.TestAdapter from 3.5.0 to 3.5.2 Bumps [MSTest.TestAdapter](https://github.com/microsoft/testfx) from 3.5.0 to 3.5.2. - [Release notes](https://github.com/microsoft/testfx/releases) - [Changelog](https://github.com/microsoft/testfx/blob/main/docs/Changelog.md) - [Commits](https://github.com/microsoft/testfx/compare/v3.5.0...v3.5.2) --- updated-dependencies: - dependency-name: MSTest.TestAdapter dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] commit 67ef4c4c0023f11a9bd99e4a2d1b33925ea36556 Author: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed Aug 14 02:29:15 2024 +0000 Bump MSTest.TestFramework from 3.5.0 to 3.5.2 Bumps [MSTest.TestFramework](https://github.com/microsoft/testfx) from 3.5.0 to 3.5.2. - [Release notes](https://github.com/microsoft/testfx/releases) - [Changelog](https://github.com/microsoft/testfx/blob/main/docs/Changelog.md) - [Commits](https://github.com/microsoft/testfx/compare/v3.5.0...v3.5.2) --- updated-dependencies: - dependency-name: MSTest.TestFramework dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] commit 390de700dd7014b68e9695fc87340c0d20f28246 Author: colinator27 <17358554+colinator27@users.noreply.github.com> Date: Mon Aug 12 13:53:33 2024 -0400 Fix #1863 Including this as part of the Underanalyzer branch, as it includes many other compiler changes/fixes commit 939e4d8b19af99b721071e5e18fd157e2878ef21 Author: colinator27 <17358554+colinator27@users.noreply.github.com> Date: Mon Aug 12 13:27:06 2024 -0400 Attempt workflow fix for GameSpecificData commit caa6cc8d044dba5fbad807145f0d7254a5393ab7 Author: colinator27 <17358554+colinator27@users.noreply.github.com> Date: Mon Aug 12 13:06:27 2024 -0400 Update Underanalyzer commit 41fbb43e50f6382b829f62ede7d89cb9479cff09 Merge: 834b4ead 5f2b1afa Author: colinator27 <17358554+colinator27@users.noreply.github.com> Date: Sun Aug 11 21:50:34 2024 -0400 Merge remote-tracking branch 'origin/master' into underanalyzer commit 834b4ead2994c35fdba141d402a3b80e1190115a Author: colinator27 <17358554+colinator27@users.noreply.github.com> Date: Sun Aug 11 20:36:19 2024 -0400 Add some comments to AssetReferenceTypes commit 5f2b1afa978e761c09258d81bcc7dd50777970ca Merge: 7944d8e5 57af2dba Author: colinator27 <17358554+colinator27@users.noreply.github.com> Date: Sun Aug 11 14:46:25 2024 -0400 Merge remote-tracking branch 'origin/master' into version-bump-0.6 commit 57af2dba0e8169a5308a4b0ca1cde8aae5cedafd Author: colinator27 <17358554+colinator27@users.noreply.github.com> Date: Sun Aug 11 14:45:20 2024 -0400 Attempt at a new workflow for stable builds commit 7944d8e5848621e214dc01351ebfc991c43c8708 Author: colinator27 <17358554+colinator27@users.noreply.github.com> Date: Sun Aug 11 14:43:39 2024 -0400 Hide updater button in non-debug builds commit f3afdf357a29ecfefc22dd5bd4692bbb20c5d4a8 Author: colinator27 <17358554+colinator27@users.noreply.github.com> Date: Sun Aug 11 14:22:24 2024 -0400 Version bump to 0.6.0.0 commit f86ac2a2ff7db26d68adc91b4f575d303545106a Merge: 63375c2d 1451fc8f Author: Miepee <38186597+Miepee@users.noreply.github.com> Date: Sat Aug 10 15:30:38 2024 +0200 Merge pull request #1861 from VladiStep/itemClipboardFix The item name clipboard crash fix. commit 1451fc8fb9ef6c089c2c73e2bfb3698ef5277225 Author: Vladislav Stepanov Date: Sat Aug 10 15:52:42 2024 +0300 Fix #1860. commit 154b18934eb6c6f8f800e38cb149f492fd5ee145 Author: NC Date: Sat Aug 10 15:06:16 2024 +0300 Disabled controls on destkop devices This can be very helpful. commit 076d5590d1dc7877c1d73b565d8d5b0b9f3d2b7a Author: NC Date: Sat Aug 10 15:02:52 2024 +0300 Update TouchControlsEnabler.csx yes commit 4c17ed5b8c8f663410b236010d37c6827f8aaada Author: NC Date: Sat Aug 10 14:59:34 2024 +0300 Display scaler(Tested on UT Yellow) commit 5c711253922741a2f1aba52755f75d445658c2cf Author: NC Date: Sat Aug 10 10:39:38 2024 +0300 Small fix I made an issue before, that made code name not correctly. commit 6d5ec52a99d1dd4fa7c1f9600a0da37bab20262c Merge: 557d4aef 63375c2d Author: colinator27 <17358554+colinator27@users.noreply.github.com> Date: Thu Aug 8 21:10:12 2024 -0400 Merge remote-tracking branch 'origin/master' into underanalyzer commit 63375c2d193e75414e7d853c9e36ea7308cd9274 Merge: 6dba40ab 9720c11a Author: Miepee <38186597+Miepee@users.noreply.github.com> Date: Fri Aug 9 02:51:20 2024 +0200 Merge pull request #1857 from UnderminersTeam/fix-new-mask-saving Fix new mask data dimensions when saving commit 9720c11a6a1e8c0fa673903c2d05b1ffe448968e Author: colinator27 <17358554+colinator27@users.noreply.github.com> Date: Thu Aug 8 17:28:41 2024 -0400 Fix new mask data dimensions when saving commit eba41ca394a742a70a7a22cc1f3c92fab860af95 Author: Liu Wenyuan <15816141883@163.com> Date: Thu Aug 8 19:10:25 2024 +0800 LFfix commit 613eb9a92414312c07aee405ca51c45aee52577f Author: Liu Wenyuan <15816141883@163.com> Date: Thu Aug 8 19:09:24 2024 +0800 Use buffer for reading JSON in UndertaleWithJSONs commit 557d4aefa79b8c7e5944259943a42f8a260bccef Author: colinator27 <17358554+colinator27@users.noreply.github.com> Date: Sun Aug 4 15:17:32 2024 -0400 Better handle base directory in game-specific data commit 298514ae36074233a9d45e4f8421724eb20db6db Author: colinator27 <17358554+colinator27@users.noreply.github.com> Date: Sat Aug 3 20:10:34 2024 -0400 Externalize game-specific data commit 3d305dc5cd58e84d741f4a1e21a5d23e71ed7769 Merge: 3a63da06 6dba40ab Author: colinator27 <17358554+colinator27@users.noreply.github.com> Date: Mon Jul 29 12:08:31 2024 -0400 Merge remote-tracking branch 'origin/master' into underanalyzer commit 6dba40ab2b2fec1154f3ee28e280d88442336b4d Author: Miepee <38186597+Miepee@users.noreply.github.com> Date: Mon Jul 29 18:01:40 2024 +0200 Dont publish release bulids of bleeding edge (#1851) commit b1ad4191fd552e7549c557e7da195e850df9b40e Merge: da9ec09f 2e46dff0 Author: Miepee <38186597+Miepee@users.noreply.github.com> Date: Mon Jul 29 15:46:22 2024 +0200 Merge pull request #1850 from UnderminersTeam/script-paths-and-exceptions Supply script file paths, better script exceptions commit 2e46dff02355a29bb90f7b43ec7802c4a79b16f0 Author: colinator27 <17358554+colinator27@users.noreply.github.com> Date: Sun Jul 28 00:03:52 2024 -0400 Supply script file paths, better script exceptions Based on code from https://github.com/UnderminersTeam/UndertaleModTool/pull/1504. Modified to fix the root cause of the problem (file paths/encodings not being specified in ScriptOptions), rather than using regular expressions to work around it. Also includes the improved script stack traces/exception handling, but further cleaned up and commented. Tested to work with built-in scripts and UMP. commit da9ec09f9f977bb7649a311177777e6636dba49a Author: Miepee <38186597+Miepee@users.noreply.github.com> Date: Sat Jul 27 16:47:34 2024 +0200 Revise for audio, texture, globalinit (#1520) * Redo documentation for audio, texture, globalinit * revert the one change * SPAAAAAAAAAAAAAAAAAAAACE commit b47792f84a6aa98ada16719065707020980c6629 Merge: c47aca8f e9d08b14 Author: colinator27 <17358554+colinator27@users.noreply.github.com> Date: Sat Jul 27 10:44:05 2024 -0400 Merge pull request #1849 from UnderminersTeam/Miepee-patch-3 Attempt 2 at show commit hash commit e9d08b148f4bd566ca66faa610b9f84401277004 Author: Miepee <38186597+Miepee@users.noreply.github.com> Date: Sat Jul 27 10:08:18 2024 +0200 Attempt 2 at show commit hash commit 4a4d55ea53d02a2d842af5a252690c3ecee93553 Merge: 215a2d25 21ecfe5e Author: Jacky720 <32578221+Jacky720@users.noreply.github.com> Date: Tue May 7 15:30:43 2024 -0400 Merge remote-tracking branch 'upstream/master' into lts-differentiation --- .github/workflows/publish_cli.yml | 3 +- .github/workflows/publish_gui.yml | 3 +- .github/workflows/publish_gui_nightly.yml | 16 +- .github/workflows/publish_gui_release.yml | 53 ++ .github/workflows/publish_pr.yml | 6 +- README.md | 2 +- SCRIPTS.md | 2 +- Underanalyzer | 2 +- UndertaleModCli/Program.UMTLibInherited.cs | 3 +- UndertaleModCli/Program.cs | 12 +- UndertaleModCli/UndertaleModCli.csproj | 4 +- UndertaleModLib/Compiler/AssemblyWriter.cs | 33 +- UndertaleModLib/Compiler/Lexer.cs | 2 - UndertaleModLib/Compiler/Parser.cs | 1 - .../Decompiler/GameSpecificResolver.cs | 185 +++- .../Definitions/deltarune.json | 14 + .../Definitions/gamemaker.json | 9 + .../Definitions/undertale.json | 10 + UndertaleModLib/GameSpecificData/README.txt | 5 + .../Underanalyzer}/deltaruined.json | 0 .../Underanalyzer}/deltarune.json | 0 .../Underanalyzer}/gamemaker.json | 0 .../Underanalyzer/template.json} | 0 .../Underanalyzer}/undertale.json | 0 UndertaleModLib/Models/UndertaleCode.cs | 797 +++++++++------- .../Models/UndertaleEmbeddedAudio.cs | 6 +- .../Models/UndertaleEmbeddedTexture.cs | 239 ++--- UndertaleModLib/Models/UndertaleFont.cs | 2 +- .../Models/UndertaleGeneralInfo.cs | 28 +- UndertaleModLib/Models/UndertaleGlobalInit.cs | 8 +- UndertaleModLib/Models/UndertaleSprite.cs | 30 +- .../Models/UndertaleTexturePageItem.cs | 31 +- UndertaleModLib/UndertaleChunks.cs | 44 +- UndertaleModLib/UndertaleModLib.csproj | 52 +- UndertaleModLib/Util/AdaptiveBinaryReader.cs | 6 +- UndertaleModLib/Util/AssetReferenceTypes.cs | 9 + UndertaleModLib/Util/BufferBinaryReader.cs | 23 +- UndertaleModLib/Util/FileBinaryReader.cs | 100 +- UndertaleModLib/Util/GMImage.cs | 863 ++++++++++++++++++ UndertaleModLib/Util/QoiConverter.cs | 422 ++++----- UndertaleModLib/Util/TextureWorker.cs | 348 ++++--- .../UndertaleModLibTests.csproj | 6 +- UndertaleModTests/UndertaleModTests.csproj | 12 +- .../Controls/UndertaleObjectReference.xaml | 6 +- .../Controls/UndertaleObjectReference.xaml.cs | 29 +- .../Converters/UndertaleCachedImageLoader.cs | 36 +- .../Editors/UndertaleCodeEditor.xaml | 11 +- .../Editors/UndertaleCodeEditor.xaml.cs | 6 +- .../UndertaleEmbeddedTextureEditor.xaml | 2 +- .../UndertaleEmbeddedTextureEditor.xaml.cs | 119 ++- .../Editors/UndertaleSpriteEditor.xaml | 4 +- .../Editors/UndertaleSpriteEditor.xaml.cs | 27 +- .../UndertaleTexturePageItemEditor.xaml | 4 +- .../UndertaleTexturePageItemEditor.xaml.cs | 134 ++- UndertaleModTool/MainWindow.xaml | 5 +- UndertaleModTool/MainWindow.xaml.cs | 357 +++++--- UndertaleModTool/MessageBoxExtensions.cs | 13 + UndertaleModTool/Properties/AssemblyInfo.cs | 6 +- .../Scripts/Builtin Scripts/BorderEnabler.csx | 13 +- .../Builtin Scripts/RunSwitchAndXboxOnPC.csx | 172 ++++ .../Scripts/Builtin Scripts/RunSwitchOnPC.csx | 43 - .../UndertaleDialogSimulator.csx | 9 +- .../Scripts/Community Scripts/FontEditor.csx | 32 +- .../Community Scripts/ImportGMS2FontData.csx | 45 +- .../Community Scripts/ScaleAllTextures.csx | 228 +++-- .../TouchControlsEnabler.csx | 27 +- ...gml_Object_obj_mobilecontrols_Create_0.gml | 2 + .../gml_Object_obj_mobilecontrols_Draw_64.gml | 44 +- .../gml_Object_obj_mobilecontrols_Other_4.gml | 33 +- .../Community Scripts/UndertaleWithJSONs.csx | 11 +- .../ApplyBasicGraphicsMod.csx | 183 ++-- .../Resource Repackers/CopySpriteBgFont.csx | 41 +- .../CopySpriteBgFontInternal.csx | 39 +- .../ImportAllEmbeddedTextures.csx | 14 +- .../Resource Repackers/ImportAllTilesets.csx | 23 +- .../Resource Repackers/ImportFontData.csx | 13 +- .../Resource Repackers/ImportGraphics.csx | 135 +-- .../Resource Repackers/ImportMasks.csx | 12 +- .../Resource Repackers/NewTextureRepacker.csx | 56 +- .../ReduceEmbeddedTexturePages.csx | 120 +-- .../Resource Unpackers/ExportAllCode.csx | 1 + .../ExportAllEmbeddedTextures.csx | 32 +- .../ExportAllRoomsToPng.csx | 2 - .../Resource Unpackers/ExportAllSprites.csx | 14 +- .../Resource Unpackers/ExportAllTextures.csx | 30 +- .../ExportAllTexturesGrouped.csx | 28 +- .../Resource Unpackers/ExportAllTilesets.csx | 23 +- .../Resource Unpackers/ExportFontData.csx | 27 +- .../Resource Unpackers/ExportMasks.csx | 27 +- .../ExportSpecificSprites.csx | 25 +- .../ExportTextureGroups.csx | 142 +-- .../ExtractEmbeddedDataFile.csx | 26 +- .../Resource Unpackers/MergeImages.csx | 64 +- .../ImportGraphics_Full_Repack.csx | 164 ++-- UndertaleModTool/Settings.cs | 1 + UndertaleModTool/UndertaleModTool.csproj | 11 +- .../FindReferencesTypesDialog.xaml.cs | 36 +- .../UndertaleResourceReferenceMap.cs | 10 +- .../UndertaleResourceReferenceMethodsMap.cs | 92 +- .../Windows/GMLSettingsWindow.xaml | 4 + .../Windows/SettingsWindow.xaml.cs | 6 + UndertaleModTool/app.manifest | 64 ++ images/ribbit-dr.png | Bin 54714 -> 41825 bytes 103 files changed, 4095 insertions(+), 2179 deletions(-) create mode 100644 .github/workflows/publish_gui_release.yml create mode 100644 UndertaleModLib/GameSpecificData/Definitions/deltarune.json create mode 100644 UndertaleModLib/GameSpecificData/Definitions/gamemaker.json create mode 100644 UndertaleModLib/GameSpecificData/Definitions/undertale.json create mode 100644 UndertaleModLib/GameSpecificData/README.txt rename UndertaleModLib/{BuiltinGameSpecificData => GameSpecificData/Underanalyzer}/deltaruined.json (100%) rename UndertaleModLib/{BuiltinGameSpecificData => GameSpecificData/Underanalyzer}/deltarune.json (100%) rename UndertaleModLib/{BuiltinGameSpecificData => GameSpecificData/Underanalyzer}/gamemaker.json (100%) rename UndertaleModLib/{BuiltinGameSpecificData/empty.json => GameSpecificData/Underanalyzer/template.json} (100%) rename UndertaleModLib/{BuiltinGameSpecificData => GameSpecificData/Underanalyzer}/undertale.json (100%) create mode 100644 UndertaleModLib/Util/GMImage.cs create mode 100644 UndertaleModTool/Scripts/Builtin Scripts/RunSwitchAndXboxOnPC.csx delete mode 100644 UndertaleModTool/Scripts/Builtin Scripts/RunSwitchOnPC.csx create mode 100644 UndertaleModTool/app.manifest diff --git a/.github/workflows/publish_cli.yml b/.github/workflows/publish_cli.yml index 9e11f33ab..49c173b40 100644 --- a/.github/workflows/publish_cli.yml +++ b/.github/workflows/publish_cli.yml @@ -31,7 +31,7 @@ jobs: - name: Setup .NET uses: actions/setup-dotnet@v4 with: - dotnet-version: 6.0.x + dotnet-version: 8.0.x - name: Restore dependencies run: dotnet restore UndertaleModCli - name: Build @@ -42,6 +42,7 @@ jobs: run: | cp ./README.md ./CLI-${{ matrix.os }}/ cp ./LICENSE.txt ./CLI-${{ matrix.os }}/ + cp -r ./UndertaleModLib/GameSpecificData/ ./CLI-${{ matrix.os }}/GameSpecificData/ - name: Upload ${{ matrix.os }} CLI uses: actions/upload-artifact@v4 with: diff --git a/.github/workflows/publish_gui.yml b/.github/workflows/publish_gui.yml index 908d5e538..36eed51d2 100644 --- a/.github/workflows/publish_gui.yml +++ b/.github/workflows/publish_gui.yml @@ -25,7 +25,7 @@ jobs: - name: Setup .NET uses: actions/setup-dotnet@v4 with: - dotnet-version: 6.0.x + dotnet-version: 8.0.x - name: Restore dependencies run: dotnet restore - name: Build @@ -41,6 +41,7 @@ jobs: cp ./README.md ./${{ matrix.os }} cp ./SCRIPTS.md ./${{ matrix.os }} cp ./LICENSE.txt ./${{ matrix.os }} + cp -r ./UndertaleModLib/GameSpecificData/ ./${{ matrix.os }}/GameSpecificData/ - name: Upload ${{ matrix.os }} GUI uses: actions/upload-artifact@v4 with: diff --git a/.github/workflows/publish_gui_nightly.yml b/.github/workflows/publish_gui_nightly.yml index e590c4838..c61421c8f 100644 --- a/.github/workflows/publish_gui_nightly.yml +++ b/.github/workflows/publish_gui_nightly.yml @@ -16,7 +16,7 @@ jobs: fail-fast: false matrix: os: [windows-latest] - configuration: [Debug, Release] + configuration: [Debug] bundled: [true] singlefile: [true, false] @@ -29,7 +29,7 @@ jobs: - name: Setup .NET uses: actions/setup-dotnet@v4 with: - dotnet-version: 6.0.x + dotnet-version: 8.0.x - name: Restore dependencies run: dotnet restore - name: Build @@ -37,14 +37,15 @@ jobs: dotnet build UndertaleModTool --no-restore dotnet build UndertaleModToolUpdater --no-restore - name: Publish ${{ matrix.os }} GUI - run: | - dotnet publish UndertaleModTool -c ${{ matrix.configuration }} -r win-x64 -p:DefineConstants=\"SHOW_COMMIT_HASH\" --self-contained ${{ matrix.bundled }} -p:PublishSingleFile=${{ matrix.singlefile }} --output ${{ matrix.os }} + run: | # FIXME: debug constant isn't being applied here, which disables updater, etc., so need to fix that or possibly add a new constant + dotnet publish UndertaleModTool -c ${{ matrix.configuration }} -r win-x64 -p:DefineConstants="SHOW_COMMIT_HASH" --self-contained ${{ matrix.bundled }} -p:PublishSingleFile=${{ matrix.singlefile }} --output ${{ matrix.os }} dotnet publish UndertaleModToolUpdater -c ${{ matrix.configuration }} -r win-x64 --self-contained ${{ matrix.bundled }} -p:PublishSingleFile=false --output ${{ matrix.os }}/Updater - name: Copy external files run: | cp ./README.md ./${{ matrix.os }} cp ./SCRIPTS.md ./${{ matrix.os }} cp ./LICENSE.txt ./${{ matrix.os }} + cp -r ./UndertaleModLib/GameSpecificData/ ./${{ matrix.os }}/GameSpecificData/ - name: Create zip for nightly release Windows GUI run: | 7z a -tzip GUI-${{ matrix.os }}-${{ matrix.configuration }}-isBundled-${{ matrix.bundled }}-isSingleFile-${{ matrix.singlefile }}.zip ./${{ matrix.os }}/* -mx0 @@ -60,7 +61,7 @@ jobs: fail-fast: false matrix: os: [ubuntu-latest, macOS-latest, windows-latest] - configuration: [Debug, Release] + configuration: [Debug] bundled: [true] include: - os: ubuntu-latest @@ -79,7 +80,7 @@ jobs: - name: Setup .NET uses: actions/setup-dotnet@v4 with: - dotnet-version: 6.0.x + dotnet-version: 8.0.x - name: Restore dependencies run: dotnet restore UndertaleModCli - name: Build @@ -90,6 +91,7 @@ jobs: run: | cp ./README.md ./CLI-${{ matrix.os }}/ cp ./LICENSE.txt ./CLI-${{ matrix.os }}/ + cp -r ./UndertaleModLib/GameSpecificData/ ./CLI-${{ matrix.os }}/GameSpecificData/ - name: Create zip for nightly release CLI run: | 7z a -tzip CLI-${{ matrix.os }}-${{ matrix.configuration }}-isBundled-${{ matrix.bundled }}.zip ./CLI-${{ matrix.os }}/* -mx0 @@ -118,7 +120,7 @@ jobs: with: tag_name: bleeding-edge name: Bleeding Edge - prerelease: false + prerelease: true fail_on_unmatched_files: true files: | */* diff --git a/.github/workflows/publish_gui_release.yml b/.github/workflows/publish_gui_release.yml new file mode 100644 index 000000000..598b3d6ee --- /dev/null +++ b/.github/workflows/publish_gui_release.yml @@ -0,0 +1,53 @@ +name: Publish stable release of UndertaleModTool GUI + +on: + workflow_dispatch: + +concurrency: + group: ${{ github.workflow }} + cancel-in-progress: true + +jobs: + build_gui: + + strategy: + fail-fast: false + matrix: + os: [windows-latest] + configuration: [Release] + bundled: [true] + singlefile: [true, false] + + runs-on: ${{ matrix.os }} + + steps: + - uses: actions/checkout@v4 + with: + submodules: true + - name: Setup .NET + uses: actions/setup-dotnet@v4 + with: + dotnet-version: 8.0.x + - name: Restore dependencies + run: dotnet restore + - name: Build + run: | + dotnet build UndertaleModTool --no-restore + - name: Publish ${{ matrix.os }} GUI + run: | + dotnet publish UndertaleModTool -c ${{ matrix.configuration }} -r win-x64 --self-contained ${{ matrix.bundled }} -p:PublishSingleFile=${{ matrix.singlefile }} --output ${{ matrix.os }} + - name: Copy external files + run: | + cp ./README.md ./${{ matrix.os }} + cp ./SCRIPTS.md ./${{ matrix.os }} + cp ./LICENSE.txt ./${{ matrix.os }} + cp -r ./UndertaleModLib/GameSpecificData/ ./${{ matrix.os }}/GameSpecificData/ + - name: Create zip for stable release Windows GUI + run: | + 7z a -tzip GUI-${{ matrix.os }}-${{ matrix.configuration }}-isBundled-${{ matrix.bundled }}-isSingleFile-${{ matrix.singlefile }}.zip ./${{ matrix.os }}/* -mx0 + - name: Upload artifact + uses: actions/upload-artifact@v4 + with: + name: GUI-${{ matrix.os }}-${{ matrix.configuration }}-isBundled-${{ matrix.bundled }}-isSingleFile-${{ matrix.singlefile }} + path: GUI-${{ matrix.os }}-${{ matrix.configuration }}-isBundled-${{ matrix.bundled }}-isSingleFile-${{ matrix.singlefile }}.zip + diff --git a/.github/workflows/publish_pr.yml b/.github/workflows/publish_pr.yml index 053725f14..f34433e33 100644 --- a/.github/workflows/publish_pr.yml +++ b/.github/workflows/publish_pr.yml @@ -27,7 +27,7 @@ jobs: - name: Setup .NET uses: actions/setup-dotnet@v4 with: - dotnet-version: 6.0.x + dotnet-version: 8.0.x - name: Restore dependencies run: dotnet restore - name: Build @@ -43,6 +43,7 @@ jobs: cp ./README.md ./${{ matrix.os }} cp ./SCRIPTS.md ./${{ matrix.os }} cp ./LICENSE.txt ./${{ matrix.os }} + cp -r ./UndertaleModLib/GameSpecificData/ ./${{ matrix.os }}/GameSpecificData/ - name: Upload ${{ matrix.os }} GUI uses: actions/upload-artifact@v4 with: @@ -74,7 +75,7 @@ jobs: - name: Setup .NET uses: actions/setup-dotnet@v4 with: - dotnet-version: 6.0.x + dotnet-version: 8.0.x - name: Restore dependencies run: dotnet restore UndertaleModCli - name: Build @@ -85,6 +86,7 @@ jobs: run: | cp ./README.md ./CLI-${{ matrix.os }}/ cp ./LICENSE.txt ./CLI-${{ matrix.os }}/ + cp -r ./UndertaleModLib/GameSpecificData/ ./CLI-${{ matrix.os }}/GameSpecificData/ - name: Upload ${{ matrix.os }} CLI uses: actions/upload-artifact@v4 with: diff --git a/README.md b/README.md index d9f46f89b..88017f768 100644 --- a/README.md +++ b/README.md @@ -16,7 +16,7 @@ Note, that you can update to the bleeding edge releases at any time from within | Releases | Status | |:---: |---------- | -| Stable | [![Latest Stable Release](https://img.shields.io/github/downloads/krzys-h/UndertaleModTool/0.5.1.0/total)](https://github.com/krzys-h/UndertaleModTool/releases/tag/0.5.1.0) | +| Stable | [![Latest Stable Release](https://img.shields.io/github/downloads/krzys-h/UndertaleModTool/0.6.1.0/total)](https://github.com/krzys-h/UndertaleModTool/releases/tag/0.6.1.0) | | Bleeding edge | [![Latest Bleeding Edge](https://img.shields.io/github/downloads/krzys-h/UndertaleModTool/bleeding-edge/total)](https://github.com/krzys-h/UndertaleModTool/releases/tag/bleeding-edge) | It's worth noting that UndertaleModTool has different builds per release. The differences are as follows: diff --git a/SCRIPTS.md b/SCRIPTS.md index 6a55fa8a8..4874879f1 100644 --- a/SCRIPTS.md +++ b/SCRIPTS.md @@ -12,7 +12,7 @@ They are relatively self-explanatory, but there are also some helpful general-pu - `EnableDebug.csx`: Enables debug mode in Undertale/Deltarune. - `FindAndReplace.csx`: Tool to find and replace GML code across an entire game. - `GoToRoom.csx`: Enables a hotkey to warp to a supplied room ID in a game. -- `RunSwitchOnPC.csx`: Converts the Switch version of Undertale to run on PC (certain versions). +- `RunSwitchAndXboxOnPC.csx`: Converts the Switch and Xbox versions of Undertale to run on PC (certain versions). - `Search.csx`: Tool to search the GML code across an entire game. - `ShowRoomName.csx`: Enables an overlay to display the current room name and ID. - `TTFFonts.csx`: Marks all fonts in Undertale to be externally loaded. Does not handle Japanese text. diff --git a/Underanalyzer b/Underanalyzer index 426bff30f..31064224d 160000 --- a/Underanalyzer +++ b/Underanalyzer @@ -1 +1 @@ -Subproject commit 426bff30fe179e517b057625cbb284569e72fb95 +Subproject commit 31064224d0d2ab736a0cbd8e56eb13616e2b7bb8 diff --git a/UndertaleModCli/Program.UMTLibInherited.cs b/UndertaleModCli/Program.UMTLibInherited.cs index 64fe7b4c8..b2b1b1342 100644 --- a/UndertaleModCli/Program.UMTLibInherited.cs +++ b/UndertaleModCli/Program.UMTLibInherited.cs @@ -4,6 +4,7 @@ using System.IO; using System.Linq; using System.Runtime.InteropServices; +using System.Text; using System.Text.RegularExpressions; using System.Threading; using System.Threading.Tasks; @@ -589,7 +590,7 @@ public bool LintUMTScript(string path) { CancellationTokenSource source = new CancellationTokenSource(100); CancellationToken token = source.Token; - CSharpScript.EvaluateAsync(File.ReadAllText(path), CliScriptOptions, this, typeof(IScriptInterface), token); + CSharpScript.EvaluateAsync(File.ReadAllText(path, Encoding.UTF8), CliScriptOptions.WithFilePath(path).WithFileEncoding(Encoding.UTF8), this, typeof(IScriptInterface), token); } catch (CompilationErrorException exc) { diff --git a/UndertaleModCli/Program.cs b/UndertaleModCli/Program.cs index 105ba89c9..f82724507 100644 --- a/UndertaleModCli/Program.cs +++ b/UndertaleModCli/Program.cs @@ -195,6 +195,7 @@ public Program(FileInfo datafile, FileInfo[] scripts, FileInfo output, bool verb typeof(JsonConvert).GetTypeInfo().Assembly, typeof(System.Text.RegularExpressions.Regex).GetTypeInfo().Assembly, typeof(TextureWorker).GetTypeInfo().Assembly, + typeof(ImageMagick.MagickImage).GetTypeInfo().Assembly, typeof(Underanalyzer.Decompiler.DecompileContext).Assembly) // "WithEmitDebugInformation(true)" not only lets us to see a script line number which threw an exception, // but also provides other useful debug info when we run UMT in "Debug". @@ -658,7 +659,8 @@ private void DumpAllTextures() { if (Verbose) Console.WriteLine($"Dumping {texture.Name}"); - File.WriteAllBytes($"{directory}/{texture.Name.Content}.png", texture.TextureData.TextureBlob); + using FileStream fs = new($"{directory}/{texture.Name.Content}.png", FileMode.Create); + texture.TextureData.Image.SavePng(fs); } } @@ -701,7 +703,7 @@ private void ReplaceTextureWithFile(string textureEntry, FileInfo fileToReplace) if (Verbose) Console.WriteLine("Replacing " + textureEntry); - texture.TextureData.TextureBlob = File.ReadAllBytes(fileToReplace.FullName); + texture.TextureData.Image = GMImage.FromPng(File.ReadAllBytes(fileToReplace.FullName)); } /// @@ -713,7 +715,7 @@ private void RunCSharpFile(string path) string lines; try { - lines = File.ReadAllText(path); + lines = File.ReadAllText(path, Encoding.UTF8); } catch (Exception exc) { @@ -740,7 +742,7 @@ private void RunCSharpCode(string code, string scriptFile = null) try { - CSharpScript.EvaluateAsync(code, CliScriptOptions, this, typeof(IScriptInterface)).GetAwaiter().GetResult(); + CSharpScript.EvaluateAsync(code, CliScriptOptions.WithFilePath(scriptFile ?? "").WithFileEncoding(Encoding.UTF8), this, typeof(IScriptInterface)).GetAwaiter().GetResult(); ScriptExecutionSuccess = true; ScriptErrorMessage = ""; } @@ -870,4 +872,4 @@ private void ProgressUpdater() Thread.Sleep(100); //10 times per second } } -} \ No newline at end of file +} diff --git a/UndertaleModCli/UndertaleModCli.csproj b/UndertaleModCli/UndertaleModCli.csproj index 86c98c05f..0ef164e2f 100644 --- a/UndertaleModCli/UndertaleModCli.csproj +++ b/UndertaleModCli/UndertaleModCli.csproj @@ -2,7 +2,7 @@ Exe - net6.0 + net8.0 AnyCPU;x64 disable LatestMajor @@ -15,7 +15,7 @@ - + diff --git a/UndertaleModLib/Compiler/AssemblyWriter.cs b/UndertaleModLib/Compiler/AssemblyWriter.cs index 0cd30d5ca..85263718b 100644 --- a/UndertaleModLib/Compiler/AssemblyWriter.cs +++ b/UndertaleModLib/Compiler/AssemblyWriter.cs @@ -2169,7 +2169,15 @@ private static void AssembleVariablePush(CodeWriter cw, Parser.Statement e, out cw.typeStack.Push(DataType.Variable); if (notLast) { - cw.Emit(Opcode.Conv, cw.typeStack.Pop(), DataType.Int32); + if (CompileContext.GMS2_3) + { + cw.typeStack.Pop(); + cw.Emit(Opcode.PushI, DataType.Int16).Value = (short)-9; // stacktop conversion + } + else + { + cw.Emit(Opcode.Conv, cw.typeStack.Pop(), DataType.Int32); + } } else isArray = true; @@ -2190,7 +2198,15 @@ private static void AssembleVariablePush(CodeWriter cw, Parser.Statement e, out cw.typeStack.Push(DataType.Variable); if (next + 1 < e.Children.Count) { - cw.Emit(Opcode.Conv, cw.typeStack.Pop(), DataType.Int32); + if (CompileContext.GMS2_3) + { + cw.typeStack.Pop(); + cw.Emit(Opcode.PushI, DataType.Int16).Value = (short)-9; // stacktop conversion + } + else + { + cw.Emit(Opcode.Conv, cw.typeStack.Pop(), DataType.Int32); + } } } } @@ -2464,7 +2480,18 @@ private static void AssembleStoreVariable(CodeWriter cw, Parser.Statement s, Dat VarType = s.Children[next].Children.Count != 0 ? VariableType.Array : VariableType.StackTop }); if (next + 1 < s.Children.Count) - cw.Emit(Opcode.Conv, DataType.Variable, DataType.Int32); + { + if (CompileContext.GMS2_3) + { + cw.typeStack.Pop(); + cw.Emit(Opcode.PushI, DataType.Int16).Value = (short)-9; // stacktop conversion + cw.typeStack.Push(DataType.Int32); + } + else + { + cw.Emit(Opcode.Conv, DataType.Variable, DataType.Int32); + } + } } if (!skip) cw.typeStack.Pop(); diff --git a/UndertaleModLib/Compiler/Lexer.cs b/UndertaleModLib/Compiler/Lexer.cs index ba87c350a..74b63bc9e 100644 --- a/UndertaleModLib/Compiler/Lexer.cs +++ b/UndertaleModLib/Compiler/Lexer.cs @@ -521,7 +521,6 @@ private static Token ReadIdentifier(CodeReader cr) "globalvar" => new Token(Token.TokenKind.KeywordGlobalVar, cr.GetPositionInfo(index)), "return" => new Token(Token.TokenKind.KeywordReturn, cr.GetPositionInfo(index)), "default" => new Token(Token.TokenKind.KeywordDefault, cr.GetPositionInfo(index)), - "struct" => new Token(Token.TokenKind.KeywordStruct, cr.GetPositionInfo(index)), "function" when CompileContext.GMS2_3 => new Token(Token.TokenKind.KeywordFunction, cr.GetPositionInfo(index)), "throw" when CompileContext.GMS2_3 => new Token(Token.TokenKind.KeywordThrow, cr.GetPositionInfo(index)), "constructor" when CompileContext.GMS2_3 => new Token(Token.TokenKind.KeywordConstructor, cr.GetPositionInfo(index)), @@ -844,7 +843,6 @@ public enum TokenKind KeywordExit, KeywordBreak, KeywordContinue, - KeywordStruct, // Apparently this exists KeywordFunction, KeywordThrow, KeywordConstructor, diff --git a/UndertaleModLib/Compiler/Parser.cs b/UndertaleModLib/Compiler/Parser.cs index 0d75c87b3..beedb06a9 100644 --- a/UndertaleModLib/Compiler/Parser.cs +++ b/UndertaleModLib/Compiler/Parser.cs @@ -3134,7 +3134,6 @@ private static bool IsKeyword(TokenKind t) TokenKind.KeywordIf, TokenKind.KeywordRepeat, TokenKind.KeywordReturn, - TokenKind.KeywordStruct, TokenKind.KeywordSwitch, TokenKind.KeywordThen, TokenKind.KeywordUntil, diff --git a/UndertaleModLib/Decompiler/GameSpecificResolver.cs b/UndertaleModLib/Decompiler/GameSpecificResolver.cs index e5719c3cf..b5becbb7d 100644 --- a/UndertaleModLib/Decompiler/GameSpecificResolver.cs +++ b/UndertaleModLib/Decompiler/GameSpecificResolver.cs @@ -1,50 +1,181 @@ using System; -using System.ComponentModel; -using System.Diagnostics; -using System.Globalization; +using System.Collections.Generic; using System.IO; -using System.Reflection; -using System.Text; -using UndertaleModLib.Scripting; +using System.Text.Json; +using System.Text.RegularExpressions; namespace UndertaleModLib.Decompiler; +/// +/// Class that helps load game-specific data, which tailor some features (such as the decompiler) to specific games. +/// public class GameSpecificResolver { - // Reads a built-in game-specific data file from the assembly - private static ReadOnlySpan ReadGameSpecificDataFile(string filename) + /// + /// Base app directory used for locating the "GameSpecificData" directory, which should be immediately inside. + /// + public static string BaseDirectory { get; set; } = AppContext.BaseDirectory; + + private enum ConditionResult + { + Ignore, + Accept, + Reject + } + + private static readonly Dictionary> _conditionEvaluators = new() { - Assembly assembly = Assembly.GetExecutingAssembly(); - string resourceName = $"UndertaleModLib.BuiltinGameSpecificData.{filename}"; + ["Always"] = (UndertaleData data, string value) => + { + return ConditionResult.Accept; + }, + + ["DisplayName.Regex"] = (UndertaleData data, string value) => + { + string displayName = data?.GeneralInfo?.DisplayName?.Content; + if (displayName is null) + { + return ConditionResult.Ignore; + } + + Match m = Regex.Match(displayName, value, RegexOptions.CultureInvariant); + if (m.Success) + { + return ConditionResult.Accept; + } + + return ConditionResult.Ignore; + }, + }; + private static readonly List _definitions = new(); + private static bool _loadedDefinitions = false; + + public class GameSpecificCondition + { + /// + /// Represents the kind of condition to be evaluated. + /// + public string ConditionKind { get; set; } + + /// + /// Value to be used during evaluation of condition, if applicable. + /// + public string Value { get; set; } + } + + public class GameSpecificDefinition + { + /// + /// Integer representing the order this definition should be evaluated/loaded in. + /// The lower this number is, the earlier this definition will be evaluated and loaded. + /// + /// + /// Built-in definitions currently use values 0 (for GameMaker builtins) and 1 (for games). + /// + public int LoadOrder { get; set; } = 100; + + /// + /// List of conditions that will be evaluated sequentially, to match this game-specific definition. + /// + public List Conditions { get; set; } + + /// + /// Filename to be loaded as an Underanalyzer game-specific config, when this definition successfully matches. + /// If empty, null, or the file is otherwise nonexistent, this will be ignored. + /// + public string UnderanalyzerFilename { get; set; } + + /// + /// Evaluates this game-specific definition against the given game data, returning whether this definition should be loaded. + /// + /// True if this definition should be loaded; false otherwise. + public bool Evaluate(UndertaleData data) + { + foreach (var condition in Conditions) + { + switch (_conditionEvaluators[condition.ConditionKind](data, condition.Value)) + { + case ConditionResult.Accept: + return true; + case ConditionResult.Reject: + return false; + case ConditionResult.Ignore: + // Pass-through to next condition + break; + } + } + + // Default to reject + return false; + } - using Stream stream = assembly.GetManifestResourceStream(resourceName); - using StreamReader reader = new(stream); - return reader.ReadToEnd(); + /// + /// Loads this game-specific definition. + /// + public void Load(UndertaleData data) + { + if (!string.IsNullOrEmpty(UnderanalyzerFilename)) + { + string underanalyzerPath = Path.Combine(BaseDirectory, "GameSpecificData", "Underanalyzer", UnderanalyzerFilename); + if (File.Exists(underanalyzerPath)) + { + data.GameSpecificRegistry.DeserializeFromJson(File.ReadAllText(underanalyzerPath)); + } + } + } } /// - /// Initializes the registry of game-specific data for the given game. + /// Forces a full reload of all game-specific definition files. /// - public static void Initialize(UndertaleData data) + public static void ReloadDefinitions() { - data.GameSpecificRegistry = new(); + // Mark definitions as loaded, and reset existing definitions + _loadedDefinitions = true; - // TODO: make proper file/manifest for all games to use, not just UT/DR, and also not these specific names + // Scan directory for files, if it exists + string dir = Path.Combine(BaseDirectory, "GameSpecificData", "Definitions"); + if (Directory.Exists(dir)) + { + foreach (string file in Directory.EnumerateFiles(dir, "*.json", SearchOption.TopDirectoryOnly)) + { + _definitions.Add(JsonSerializer.Deserialize(File.ReadAllText(file))); + } + } - var foundGame = false; + // Sort all definitions by their load order + _definitions.Sort((a, b) => a.LoadOrder); + } - // Read registry data files - string lowerName = data?.GeneralInfo?.DisplayName?.Content.ToLower(CultureInfo.InvariantCulture) ?? ""; - data.GameSpecificRegistry.DeserializeFromJson(ReadGameSpecificDataFile("gamemaker.json")); - if (lowerName.StartsWith("undertale", StringComparison.InvariantCulture) || lowerName.StartsWith("under", StringComparison.InvariantCulture)) + /// + /// Loads game-specific definitions, if not loaded already. Call to force a full reload. + /// + public static void LoadDefinitions() + { + if (!_loadedDefinitions) { - data.GameSpecificRegistry.DeserializeFromJson(ReadGameSpecificDataFile("undertale.json")); - foundGame = true; + ReloadDefinitions(); } - if (lowerName == "survey_program" || lowerName.StartsWith("deltarune", StringComparison.InvariantCulture) || lowerName.StartsWith("delta", StringComparison.InvariantCulture)) + } + + /// + /// Initializes the registry of game-specific data for the given game. + /// + public static void Initialize(UndertaleData data) + { + // Ensure all definitions are loaded + LoadDefinitions(); + + // Initialize empty game-specific registry for decompiler + data.GameSpecificRegistry = new(); + + // Evaluate all definitions, and load all successful ones + foreach (var definition in _definitions) { - data.GameSpecificRegistry.DeserializeFromJson(ReadGameSpecificDataFile("deltarune.json")); - foundGame = true; + if (definition.Evaluate(data)) + { + definition.Load(data); + } } if (!foundGame && File.Exists(lowerName + ".json")) { diff --git a/UndertaleModLib/GameSpecificData/Definitions/deltarune.json b/UndertaleModLib/GameSpecificData/Definitions/deltarune.json new file mode 100644 index 000000000..ab08e0218 --- /dev/null +++ b/UndertaleModLib/GameSpecificData/Definitions/deltarune.json @@ -0,0 +1,14 @@ +{ + "LoadOrder": 1, + "Conditions": [ + { + "ConditionKind": "DisplayName.Regex", + "Value": "(?i)^SURVEY_PROGRAM$" + }, + { + "ConditionKind": "DisplayName.Regex", + "Value": "(?i)^deltarune" + } + ], + "UnderanalyzerFilename": "deltarune.json" +} \ No newline at end of file diff --git a/UndertaleModLib/GameSpecificData/Definitions/gamemaker.json b/UndertaleModLib/GameSpecificData/Definitions/gamemaker.json new file mode 100644 index 000000000..dcf9cae1e --- /dev/null +++ b/UndertaleModLib/GameSpecificData/Definitions/gamemaker.json @@ -0,0 +1,9 @@ +{ + "LoadOrder": 0, + "Conditions": [ + { + "ConditionKind": "Always" + } + ], + "UnderanalyzerFilename": "gamemaker.json" +} \ No newline at end of file diff --git a/UndertaleModLib/GameSpecificData/Definitions/undertale.json b/UndertaleModLib/GameSpecificData/Definitions/undertale.json new file mode 100644 index 000000000..3f63150ee --- /dev/null +++ b/UndertaleModLib/GameSpecificData/Definitions/undertale.json @@ -0,0 +1,10 @@ +{ + "LoadOrder": 1, + "Conditions": [ + { + "ConditionKind": "DisplayName.Regex", + "Value": "(?i)^undertale" + } + ], + "UnderanalyzerFilename": "undertale.json" +} \ No newline at end of file diff --git a/UndertaleModLib/GameSpecificData/README.txt b/UndertaleModLib/GameSpecificData/README.txt new file mode 100644 index 000000000..d29b42d13 --- /dev/null +++ b/UndertaleModLib/GameSpecificData/README.txt @@ -0,0 +1,5 @@ +This folder contains data for specific games, to improve the modding experience when using certain features. + +Games are defined in the "Definitions" sub-folder, where they are given conditions to match the game(s) they target. Any JSON files from this folder are loaded automatically, if available. + +GML decompiler configs are contained within the "Underanalyzer" sub-folder, referenced from the original definition files. (These are loaded on-demand.) diff --git a/UndertaleModLib/BuiltinGameSpecificData/deltaruined.json b/UndertaleModLib/GameSpecificData/Underanalyzer/deltaruined.json similarity index 100% rename from UndertaleModLib/BuiltinGameSpecificData/deltaruined.json rename to UndertaleModLib/GameSpecificData/Underanalyzer/deltaruined.json diff --git a/UndertaleModLib/BuiltinGameSpecificData/deltarune.json b/UndertaleModLib/GameSpecificData/Underanalyzer/deltarune.json similarity index 100% rename from UndertaleModLib/BuiltinGameSpecificData/deltarune.json rename to UndertaleModLib/GameSpecificData/Underanalyzer/deltarune.json diff --git a/UndertaleModLib/BuiltinGameSpecificData/gamemaker.json b/UndertaleModLib/GameSpecificData/Underanalyzer/gamemaker.json similarity index 100% rename from UndertaleModLib/BuiltinGameSpecificData/gamemaker.json rename to UndertaleModLib/GameSpecificData/Underanalyzer/gamemaker.json diff --git a/UndertaleModLib/BuiltinGameSpecificData/empty.json b/UndertaleModLib/GameSpecificData/Underanalyzer/template.json similarity index 100% rename from UndertaleModLib/BuiltinGameSpecificData/empty.json rename to UndertaleModLib/GameSpecificData/Underanalyzer/template.json diff --git a/UndertaleModLib/BuiltinGameSpecificData/undertale.json b/UndertaleModLib/GameSpecificData/Underanalyzer/undertale.json similarity index 100% rename from UndertaleModLib/BuiltinGameSpecificData/undertale.json rename to UndertaleModLib/GameSpecificData/Underanalyzer/undertale.json diff --git a/UndertaleModLib/Models/UndertaleCode.cs b/UndertaleModLib/Models/UndertaleCode.cs index 5d8f76d51..9568f7f34 100644 --- a/UndertaleModLib/Models/UndertaleCode.cs +++ b/UndertaleModLib/Models/UndertaleCode.cs @@ -111,6 +111,10 @@ Opcode.PushBltn or Opcode.PushI _ => throw new IOException("Unknown opcode " + op.ToString().ToUpper(CultureInfo.InvariantCulture)), }; } + + /// + /// Converts from bytecode 14 instruction opcodes to modern opcodes. + /// private static byte ConvertInstructionKind(byte kind) { kind = kind switch @@ -383,102 +387,131 @@ public Reference GetReference(bool allowResolve = false) where T : class, /// public void Serialize(UndertaleWriter writer) { + // Flag tracking whether we're writing bytecode 14 (old) instructions + bool bytecode14 = writer.Bytecode14OrLower; + + // Switch on the basic format of instruction to encode switch (GetInstructionType(Kind)) { case InstructionType.SingleTypeInstruction: case InstructionType.DoubleTypeInstruction: case InstructionType.ComparisonInstruction: + { + // Write "extra" byte, used on some instructions + writer.Write(Extra); + + // Write comparison kind, if present + if (bytecode14 && Kind == Opcode.Cmp) { - writer.Write(Extra); - if (writer.Bytecode14OrLower && Kind == Opcode.Cmp) - writer.Write((byte)0); - else - writer.Write((byte)ComparisonKind); - byte TypePair = (byte)((byte)Type2 << 4 | (byte)Type1); - writer.Write(TypePair); + // Bytecode 14 encodes its comparison in the opcode itself, not here + writer.Write((byte)0); + } + else + { + // Bytecode 15 and above encode a comparison kind outside of the opcode + writer.Write((byte)ComparisonKind); + } - if (writer.Bytecode14OrLower) + // Write types + byte typePair = (byte)((byte)Type2 << 4 | (byte)Type1); + writer.Write(typePair); + + // Write opcode + if (bytecode14) + { + // Translate relevant opcodes to their old bytecode 14 equivalents + byte k = Kind switch { - byte k = Kind switch - { - Opcode.Conv => 0x03, - Opcode.Mul => 0x04, - Opcode.Div => 0x05, - Opcode.Rem => 0x06, - Opcode.Mod => 0x07, - Opcode.Add => 0x08, - Opcode.Sub => 0x09, - Opcode.And => 0x0A, - Opcode.Or => 0x0B, - Opcode.Xor => 0x0C, - Opcode.Neg => 0x0D, - Opcode.Not => 0x0E, - Opcode.Shl => 0x0F, - Opcode.Shr => 0x10, - Opcode.Dup => 0x82, - Opcode.Cmp => (byte)(ComparisonKind + 0x10), - Opcode.Ret => 0x9D, - Opcode.Exit => 0x9E, - Opcode.Popz => 0x9F, - _ => (byte)Kind, - }; - writer.Write(k); - } - else - writer.Write((byte)Kind); + Opcode.Conv => 0x03, + Opcode.Mul => 0x04, + Opcode.Div => 0x05, + Opcode.Rem => 0x06, + Opcode.Mod => 0x07, + Opcode.Add => 0x08, + Opcode.Sub => 0x09, + Opcode.And => 0x0A, + Opcode.Or => 0x0B, + Opcode.Xor => 0x0C, + Opcode.Neg => 0x0D, + Opcode.Not => 0x0E, + Opcode.Shl => 0x0F, + Opcode.Shr => 0x10, + Opcode.Dup => 0x82, + Opcode.Cmp => (byte)(ComparisonKind + 0x10), // Comparison kind is encoded into opcode + Opcode.Ret => 0x9D, + Opcode.Exit => 0x9E, + Opcode.Popz => 0x9F, + _ => (byte)Kind, + }; + writer.Write(k); } + else + { + // Write opcode verbatim on modern bytecode versions + writer.Write((byte)Kind); + } + break; + } case InstructionType.GotoInstruction: + { + // Write jump offset + if (bytecode14) { - // See unserialize - if (writer.Bytecode14OrLower) - writer.WriteInt24(JumpOffset); - else if (JumpOffsetPopenvExitMagic) - { - writer.WriteInt24(0xF00000); - } - else - { - uint JumpOffsetFixed = (uint)JumpOffset; - JumpOffsetFixed &= ~0xFF800000; - writer.WriteInt24((int)JumpOffsetFixed); - } + // Bytecode 14 writes the offset verbatim + writer.WriteInt24(JumpOffset); + } + else if (JumpOffsetPopenvExitMagic) + { + // If popenv exit magic is used, write that specifically + writer.WriteInt24(0xF00000); + } + else + { + // If not using popenv exit magic, encode jump offset as 23-bit signed integer + uint jumpOffsetFixed = (uint)JumpOffset; + jumpOffsetFixed &= ~0xFF800000; + writer.WriteInt24((int)jumpOffsetFixed); + } - if (writer.Bytecode14OrLower) + // Write opcode + if (bytecode14) + { + // Translate relevant opcodes to their old bytecode 14 equivalents + byte k = Kind switch { - if (Kind == Opcode.B) - writer.Write((byte)0xB7); - else if (Kind == Opcode.Bt) - writer.Write((byte)0xB8); - else if (Kind == Opcode.Bf) - writer.Write((byte)0xB9); - else if (Kind == Opcode.PushEnv) - writer.Write((byte)0xBB); - else if (Kind == Opcode.PopEnv) - writer.Write((byte)0xBC); - else - writer.Write((byte)Kind); - } - else - writer.Write((byte)Kind); + Opcode.B => 0xB7, + Opcode.Bt => 0xB8, + Opcode.Bf => 0xB9, + Opcode.PushEnv => 0xBB, + Opcode.PopEnv => 0xBC, + _ => (byte)Kind + }; + writer.Write(k); + } + else + { + // Write opcode verbatim on modern bytecode versions + writer.Write((byte)Kind); } + break; + } case InstructionType.PopInstruction: { - if (Type1 == DataType.Int16) - { - // Special scenario - the swap instruction - // TODO: Figure out the proper syntax, see #129 - writer.Write(SwapExtra); - byte TypePair = (byte)((byte)Type2 << 4 | (byte)Type1); - writer.Write(TypePair); - if (writer.Bytecode14OrLower && Kind == Opcode.Pop) - writer.Write((byte)0x41); - else - writer.Write((byte)Kind); - } + // Special scenario - the swap instruction (see #129) + // Write swap value + writer.Write(SwapExtra); + + // Write types + byte typePair = (byte)((byte)Type2 << 4 | (byte)Type1); + writer.Write(typePair); + + // Write opcode (if writing bytecode 14, translate to the old opcode) + if (bytecode14 && Kind == Opcode.Pop) + writer.Write((byte)0x41); else { writer.Write((short)TypeInst); @@ -491,87 +524,124 @@ public void Serialize(UndertaleWriter writer) writer.WriteUndertaleObject(Destination); } } + else + { + // Write instance type + writer.Write((short)TypeInst); + + // Write types + byte typePair = (byte)((byte)Type2 << 4 | (byte)Type1); + writer.Write(typePair); + + // Write opcode (if writing bytecode 14, translate to the old opcode) + if (bytecode14 && Kind == Opcode.Pop) + writer.Write((byte)0x41); + else + writer.Write((byte)Kind); + + // Write actual variable being stored to + writer.WriteUndertaleObject(Destination); + } + break; + } case InstructionType.PushInstruction: + { + // Write 16-bit integer, instance type, or empty data + writer.Write(Type1 switch { - if (Type1 == DataType.Int16) - { - //Debug.Assert(Value.GetType() == typeof(short)); - writer.Write((short)Value); - } - else if (Type1 == DataType.Variable) - { - writer.Write((short)TypeInst); - } - else - { - writer.Write((short)0); - } - writer.Write((byte)Type1); - if (writer.Bytecode14OrLower) - writer.Write((byte)0xC0); - else - writer.Write((byte)Kind); - switch (Type1) - { - case DataType.Double: - //Debug.Assert(Value.GetType() == typeof(double)); - writer.Write((double)Value); - break; - case DataType.Float: - //Debug.Assert(Value.GetType() == typeof(float)); - writer.Write((float)Value); - break; - case DataType.Int32: - if (Value.GetType() == typeof(Reference)) - { - writer.WriteUndertaleObject((Reference)Value); - break; - } - if (Value.GetType() == typeof(Reference)) - { - writer.WriteUndertaleObject((Reference)Value); - break; - } - //Debug.Assert(Value.GetType() == typeof(int)); - writer.Write((int)Value); - break; - case DataType.Int64: - //Debug.Assert(Value.GetType() == typeof(long)); - writer.Write((long)Value); - break; - case DataType.Boolean: - //Debug.Assert(Value.GetType() == typeof(bool)); - writer.Write((bool)Value ? 1 : 0); + DataType.Int16 => (short)Value, + DataType.Variable => (short)TypeInst, + _ => (short)0 + }); + + // Write type (no second type is used) + writer.Write((byte)Type1); + + // Write opcode (if writing bytecode 14, translate to the old opcode) + if (bytecode14) + writer.Write((byte)0xC0); + else + writer.Write((byte)Kind); + + // Write value being pushed + switch (Type1) + { + case DataType.Double: + writer.Write((double)Value); + break; + case DataType.Float: + writer.Write((float)Value); + break; + case DataType.Int32: + if (Value.GetType() == typeof(Reference)) + { + // Write function reference, rather than integer + writer.WriteUndertaleObject((Reference)Value); break; - case DataType.Variable: - //Debug.Assert(Value.GetType() == typeof(Reference)); + } + if (Value.GetType() == typeof(Reference)) + { + // Write variable reference, rather than integer writer.WriteUndertaleObject((Reference)Value); break; - case DataType.String: - //Debug.Assert(Value.GetType() == typeof(UndertaleResourceById)); - writer.WriteUndertaleObject((UndertaleResourceById)Value); - break; - case DataType.Int16: - break; - } + } + writer.Write((int)Value); + break; + case DataType.Int64: + writer.Write((long)Value); + break; + case DataType.Boolean: + writer.Write((bool)Value ? 1 : 0); + break; + case DataType.Variable: + writer.WriteUndertaleObject((Reference)Value); + break; + case DataType.String: + writer.WriteUndertaleObject((UndertaleResourceById)Value); + break; + case DataType.Int16: + // Data is encoded in the first two bytes of the instruction (was already written above) + break; } + break; + } case InstructionType.CallInstruction: - { - writer.Write(ArgumentsCount); - writer.Write((byte)Type1); - if (writer.Bytecode14OrLower && Kind == Opcode.Call) - writer.Write((byte)0xDA); - else - writer.Write((byte)Kind); - writer.WriteUndertaleObject(Function); - } + { + // Write number of arguments being used in call + writer.Write(ArgumentsCount); + + // Write type (no second type is used) + writer.Write((byte)Type1); + + // Write opcode (if writing bytecode 14, translate to the old opcode) + if (bytecode14 && Kind == Opcode.Call) + writer.Write((byte)0xDA); + else + writer.Write((byte)Kind); + + // Write reference to the function being called + writer.WriteUndertaleObject(Function); + break; + } case InstructionType.BreakInstruction: + { + // Write type of break instruction (encoded in Value) + writer.Write((short)Value); + + // Write type (no second type is used) + writer.Write((byte)Type1); + + // Write opcode + writer.Write((byte)Kind); + + // Write integer argument, or function, if either is present + if (Type1 == DataType.Int32) { //Debug.Assert(Value.GetType() == typeof(short)); writer.Write((short)Value); @@ -586,190 +656,240 @@ public void Serialize(UndertaleWriter writer) } } break; + } default: - throw new IOException("Unknown opcode " + Kind.ToString().ToUpper(CultureInfo.InvariantCulture)); + throw new IOException($"Unknown opcode {Kind.ToString().ToUpper(CultureInfo.InvariantCulture)}"); } } /// public void Unserialize(UndertaleReader reader) { - long instructionStartAddress = reader.Position; - reader.Position += 3; // skip for now, we'll read them later - byte kind = reader.ReadByte(); - if (reader.Bytecode14OrLower) + // Flag tracking whether we're parsing bytecode 14 (old) instructions + bool bytecode14 = reader.Bytecode14OrLower; + + // Read first word from instruction + uint firstWord = reader.ReadUInt32(); + + // Read opcode from most significant byte + byte kindByte = (byte)((firstWord & 0xFF000000) >> 24); + Opcode kind = (Opcode)kindByte; + if (bytecode14) { - // Convert opcode to our enum - kind = ConvertInstructionKind(kind); + // Convert opcode from old format to new format + kind = (Opcode)ConvertInstructionKind(kindByte); } - Kind = (Opcode)kind; - reader.Position = instructionStartAddress; - switch (GetInstructionType(Kind)) + + // Extract first three bytes from first word + byte b0 = (byte)(firstWord & 0x000000FF); + byte b1 = (byte)((firstWord & 0x0000FF00) >> 8); + byte b2 = (byte)((firstWord & 0x00FF0000) >> 16); + + // Parse instruction contents + InstructionType instructionType = GetInstructionType(kind); + switch (instructionType) { case InstructionType.SingleTypeInstruction: case InstructionType.DoubleTypeInstruction: case InstructionType.ComparisonInstruction: + { + // Parse instruction components from bytes + byte extra = b0; + ComparisonType comparisonKind = (ComparisonType)b1; + DataType type1 = (DataType)(b2 & 0xf); + DataType type2 = (DataType)(b2 >> 4); + + // Ensure basic conditions hold + if (extra != 0 && kind != Opcode.Dup && kind != Opcode.CallV) { - Extra = reader.ReadByte(); -#if DEBUG - if (Extra != 0 && Kind != Opcode.Dup && Kind != Opcode.CallV) - throw new IOException("Invalid padding in " + Kind.ToString().ToUpper(CultureInfo.InvariantCulture)); -#endif - ComparisonKind = (ComparisonType)reader.ReadByte(); - //if (!bytecode14 && (Kind == Opcode.Cmp) != ((byte)ComparisonKind != 0)) - // throw new IOException("Got unexpected comparison type in " + Kind.ToString().ToUpper(CultureInfo.InvariantCulture) + " (should be only in CMP)"); - byte TypePair = reader.ReadByte(); - Type1 = (DataType)(TypePair & 0xf); - Type2 = (DataType)(TypePair >> 4); -#if DEBUG - if (GetInstructionType(Kind) == InstructionType.SingleTypeInstruction && Type2 != (byte)0) - throw new IOException("Second type should be 0 in " + Kind.ToString().ToUpper(CultureInfo.InvariantCulture)); -#endif - //if(reader.ReadByte() != (byte)Kind) throw new Exception("really shouldn't happen"); - if (reader.Bytecode14OrLower && Kind == Opcode.Cmp) - ComparisonKind = (ComparisonType)(reader.ReadByte() - 0x10); - else - reader.Position++; + throw new IOException($"Invalid padding in {kind.ToString().ToUpper(CultureInfo.InvariantCulture)}"); + } - if (Kind == Opcode.And || Kind == Opcode.Or) - { - if (Type1 == DataType.Boolean && Type2 == DataType.Boolean) - reader.undertaleData.ShortCircuit = false; - } + if (instructionType == InstructionType.SingleTypeInstruction && type2 != 0) + { + throw new IOException($"Second type should be 0 in {kind.ToString().ToUpper(CultureInfo.InvariantCulture)}"); + } + + + // In bytecode 14, the comparison kind is encoded in the opcode itself + if (bytecode14 && kind == Opcode.Cmp) + { + comparisonKind = (ComparisonType)(kindByte - 0x10); } + + // Check for "and.b.b" or "or.b.b", which imply the code was compiled without short-circuiting + if ((kind is Opcode.And or Opcode.Or) && type1 == DataType.Boolean && type2 == DataType.Boolean) + { + reader.undertaleData.ShortCircuit = false; + } + + // Assign to instruction + Extra = extra; + ComparisonKind = comparisonKind; + Type1 = type1; + Type2 = type2; + break; + } case InstructionType.GotoInstruction: + { + if (bytecode14) { - if (reader.Bytecode14OrLower) - { - JumpOffset = reader.ReadInt24(); - if (JumpOffset == -1048576) // magic? encoded in little endian as 00 00 F0, which is like below - JumpOffsetPopenvExitMagic = true; - reader.Position++; - break; - } + // Bytecode 14 has slightly different parsing + int jumpOffset = b0 | (b1 << 8) | ((sbyte)b2 << 16); + JumpOffset = jumpOffset; + JumpOffsetPopenvExitMagic = (jumpOffset == -1048576); // encoded in little endian as 00 00 F0 (same as below) + break; + } - uint v = reader.ReadUInt24(); + // Parse normally + uint v = (uint)(b0 | (b1 << 8) | (b2 << 16)); + bool popenvExitMagic = (v & 0x800000) != 0; + if (popenvExitMagic && v != 0xF00000) + { + throw new Exception("Popenv magic doesn't work, call issue #90 again"); + } - JumpOffsetPopenvExitMagic = (v & 0x800000) != 0; + // The rest is int23 signed value, so make sure + uint r = v & 0x003FFFFF; + if ((v & 0x00C00000) != 0) + { + r |= 0xFFC00000; + } - // The rest is int23 signed value, so make sure - uint r = v & 0x003FFFFF; -#if DEBUG - if (JumpOffsetPopenvExitMagic && v != 0xF00000) - throw new Exception("Popenv magic doesn't work, call issue #90 again"); - else -#endif - { - if ((v & 0x00C00000) != 0) - r |= 0xFFC00000; - JumpOffset = (int)r; - } + // Assign to instruction + JumpOffset = (int)r; + JumpOffsetPopenvExitMagic = popenvExitMagic; - //if(reader.ReadByte() != (byte)Kind) throw new Exception("really shouldn't happen"); - reader.Position++; - } break; + } case InstructionType.PopInstruction: + { + // Parse instruction components from bytes + InstanceType typeInst = (InstanceType)(b0 | (b1 << 8)); + DataType type1 = (DataType)(b2 & 0xf); + DataType type2 = (DataType)(b2 >> 4); + + if (type1 == DataType.Int16) { - TypeInst = (InstanceType)reader.ReadInt16(); - byte TypePair = reader.ReadByte(); - Type1 = (DataType)(TypePair & 0xf); - Type2 = (DataType)(TypePair >> 4); - //if(reader.ReadByte() != (byte)Kind) throw new Exception("really shouldn't happen"); - reader.Position++; - if (Type1 == DataType.Int16) - { - // Special scenario - the swap instruction - // TODO: Figure out the proper syntax, see #129 - SwapExtra = (ushort)TypeInst; - TypeInst = 0; - } - else - { - Destination = reader.ReadUndertaleObject>(); - } + // Special scenario - the swap instruction (see #129) + SwapExtra = (ushort)typeInst; + typeInst = 0; } + else + { + // Destination is an actual variable + Destination = reader.ReadUndertaleObject>(); + } + + // Assign remaining values to instruction + TypeInst = typeInst; + Type1 = type1; + Type2 = type2; + break; + } case InstructionType.PushInstruction: + { + // Parse instruction components from bytes + short val = (short)(b0 | (b1 << 8)); + DataType type1 = (DataType)b2; + + // Modify opcode of instruction, if in bytecode 14 + if (bytecode14) { - short val = reader.ReadInt16(); - Type1 = (DataType)reader.ReadByte(); - if (reader.Bytecode14OrLower) + if (type1 == DataType.Variable) { if (Type1 == DataType.Variable) { - switch (val) - { - case -5: - Kind = Opcode.PushGlb; - break; - case -6: // builtin - Kind = Opcode.PushBltn; - break; - case -7: - Kind = Opcode.PushLoc; - break; - } - } - else if (Type1 == DataType.Int16) - { - Kind = Opcode.PushI; + case -5: + kind = Opcode.PushGlb; + break; + case -6: // builtin + kind = Opcode.PushBltn; + break; + case -7: + kind = Opcode.PushLoc; + break; } } - //if(reader.ReadByte() != (byte)Kind) throw new Exception("really shouldn't happen"); - reader.Position++; - switch (Type1) + else if (type1 == DataType.Int16) { - case DataType.Double: - Value = reader.ReadDouble(); - break; - case DataType.Float: - Value = reader.ReadSingle(); - break; - case DataType.Int32: - Value = reader.ReadInt32(); - break; - case DataType.Int64: - Value = reader.ReadInt64(); - break; - case DataType.Boolean: - Value = (reader.ReadUInt32() == 1); // TODO: double check - break; - case DataType.Variable: - TypeInst = (InstanceType)val; - Value = reader.ReadUndertaleObject>(); - break; - case DataType.String: - Value = reader.ReadUndertaleObject>(); - break; - case DataType.Int16: - Value = val; - break; + kind = Opcode.PushI; } } - break; - case InstructionType.CallInstruction: + // Parse data being pushed + switch (type1) { - ArgumentsCount = reader.ReadUInt16(); - Type1 = (DataType)reader.ReadByte(); - //if(reader.ReadByte() != (byte)Kind) throw new Exception("really shouldn't happen"); - reader.Position++; - Function = reader.ReadUndertaleObject>(); + case DataType.Double: + Value = reader.ReadDouble(); + break; + case DataType.Float: + Value = reader.ReadSingle(); + break; + case DataType.Int32: + Value = reader.ReadInt32(); + break; + case DataType.Int64: + Value = reader.ReadInt64(); + break; + case DataType.Boolean: + Value = reader.ReadBoolean(); + break; + case DataType.Variable: + TypeInst = (InstanceType)val; + Value = reader.ReadUndertaleObject>(); + break; + case DataType.String: + Value = reader.ReadUndertaleObject>(); + break; + case DataType.Int16: + Value = val; + break; } + + // Assign remaining values to instruction + Type1 = type1; + + break; + } + + case InstructionType.CallInstruction: + { + // Parse instruction components from bytes + ArgumentsCount = (ushort)(b0 | (b1 << 8)); + Type1 = (DataType)b2; + + // Parse function being called + Function = reader.ReadUndertaleObject>(); + break; + } case InstructionType.BreakInstruction: + { + // Parse instruction components from bytes + short value = (short)(b0 | (b1 << 8)); + DataType type1 = (DataType)b2; + + // Parse integer argument, if provided + if (type1 == DataType.Int32) { - Value = reader.ReadInt16(); - Type1 = (DataType)reader.ReadByte(); - if (reader.ReadByte() != (byte)Kind) throw new Exception("really shouldn't happen"); - if (Type1 == DataType.Int32) + IntArgument = reader.ReadInt32(); + + // Existence of this argument implies GameMaker 2023.8 or above + if (!reader.undertaleData.IsVersionAtLeast(2023, 8)) + { + reader.undertaleData.SetGMS2Version(2023, 8); + } + + // If this is an asset type found in GameMaker 2024.4 or above, track that as well + if (!reader.undertaleData.IsVersionAtLeast(2024, 4)) { IntArgument = reader.ReadInt32(); if (!reader.undertaleData.IsVersionAtLeast(2023, 8)) @@ -789,89 +909,116 @@ public void Unserialize(UndertaleReader reader) } } } + + // If this is a chknullish instruction (ID -10), then this implies GameMaker 2.3.7 or above + if (value == -10 && reader.undertaleData.IsVersionAtLeast(2, 3)) + { + if (!reader.undertaleData.IsVersionAtLeast(2, 3, 7)) + { + reader.undertaleData.SetGMS2Version(2, 3, 7); + } + } + + // Assign remaining values to instruction + Value = value; + Type1 = type1; + break; + } default: - throw new IOException("Unknown opcode " + Kind.ToString().ToUpper(CultureInfo.InvariantCulture)); + throw new IOException($"Unknown opcode {Kind.ToString().ToUpper(CultureInfo.InvariantCulture)}"); } + + // Assign final opcode to instruction + Kind = kind; } + /// public static uint UnserializeChildObjectCount(UndertaleReader reader) { - long instructionStartAddress = reader.Position; - reader.Position += 3; // skip for now, we'll read them later - byte kind = reader.ReadByte(); - if (reader.Bytecode14OrLower) + // Flag tracking whether we're parsing bytecode 14 (old) instructions + bool bytecode14 = reader.Bytecode14OrLower; + + // Read first word from instruction + uint firstWord = reader.ReadUInt32(); + + // Read opcode from most significant byte + Opcode kind = (Opcode)((firstWord & 0xFF000000) >> 24); + if (bytecode14) { - // Convert opcode to our enum - kind = ConvertInstructionKind(kind); + // Convert opcode from old format to new format + kind = (Opcode)ConvertInstructionKind((byte)kind); } - Opcode Kind = (Opcode)kind; - reader.Position = instructionStartAddress; - switch (GetInstructionType(Kind)) + + // Extract third byte from first word + byte b2 = (byte)((firstWord & 0x00FF0000) >> 16); + + // Parse instruction contents + InstructionType instructionType = GetInstructionType(kind); + switch (instructionType) { case InstructionType.SingleTypeInstruction: case InstructionType.DoubleTypeInstruction: case InstructionType.ComparisonInstruction: case InstructionType.GotoInstruction: - reader.Position += 4; + // No special handling required break; case InstructionType.PopInstruction: - reader.Position += 2; // "TypeInst" - int type1 = reader.ReadByte() & 0xf; - if (type1 != 0x0f) + { + // Skip destination of pop instruction, if present + DataType type1 = (DataType)(b2 & 0xf); + if (type1 != DataType.Int16) { - reader.Position += 1 + 4; + reader.Position += 4; return 1; // "Destination" } - else - reader.Position++; break; + } case InstructionType.PushInstruction: + { + // Skip value being pushed, if present + DataType type1 = (DataType)(b2 & 0xf); + switch (type1) { - reader.Position += 2; - DataType Type1 = (DataType)reader.ReadByte(); - reader.Position++; - switch (Type1) - { - case DataType.Double: - case DataType.Int64: - reader.Position += 8; - break; + case DataType.Double: + case DataType.Int64: + reader.Position += 8; + break; - case DataType.Float: - case DataType.Int32: - case DataType.Boolean: - reader.Position += 4; - break; + case DataType.Float: + case DataType.Int32: + case DataType.Boolean: + reader.Position += 4; + break; - case DataType.Variable: - case DataType.String: - reader.Position += 4; - return 1; - } + case DataType.Variable: + case DataType.String: + reader.Position += 4; + return 1; } break; + } case InstructionType.CallInstruction: - reader.Position += 8; + reader.Position += 4; return 1; // "Function" case InstructionType.BreakInstruction: + { + // Skip past integer argument, if present + DataType type1 = (DataType)(b2 & 0xf); + if (type1 == DataType.Int32) { - reader.Position += 2; - DataType Type1 = (DataType)reader.ReadByte(); - if (Type1 == DataType.Int32) - reader.Position += 5; - else - reader.Position += 1; - break; + reader.Position += 4; } + break; + } default: - throw new IOException("Unknown opcode " + Kind.ToString().ToUpper(CultureInfo.InvariantCulture)); + throw new IOException($"Unknown opcode {kind.ToString().ToUpper(CultureInfo.InvariantCulture)}"); } return 0; @@ -991,7 +1138,7 @@ public void ToString(StringBuilder stringBuilder, UndertaleCode code, List sbh.Append(stringBuilder, ' '); if (Type1 == DataType.Int16) { - // Special scenario - the swap instruction + // Special scenario - the swap instruction (see #129) sbh.Append(stringBuilder, SwapExtra); } else @@ -1646,4 +1793,4 @@ public void Dispose() int IGMCode.LocalCount => (int)LocalsCount; public IGMInstruction GetInstruction(int index) => Instructions[index]; public IGMCode GetChild(int index) => ChildEntries[index]; -} \ No newline at end of file +} diff --git a/UndertaleModLib/Models/UndertaleEmbeddedAudio.cs b/UndertaleModLib/Models/UndertaleEmbeddedAudio.cs index 94c30d47f..991c5ca59 100644 --- a/UndertaleModLib/Models/UndertaleEmbeddedAudio.cs +++ b/UndertaleModLib/Models/UndertaleEmbeddedAudio.cs @@ -12,7 +12,7 @@ public class UndertaleEmbeddedAudio : UndertaleNamedResource, PaddedObject, IDis /// /// The name of the embedded audio entry. /// - /// This is an UTMT only attribute. GameMaker does not store names for them. + /// This is a UTMT only attribute. GameMaker does not store names for them. public UndertaleString Name { get; set; } /// @@ -56,13 +56,13 @@ public override string ToString() try { // TODO: Does only the GUI set this? - return Name.Content + " (" + GetType().Name + ")"; + return $"{Name.Content} ({GetType().Name})"; } catch { Name = new UndertaleString("EmbeddedSound Unknown Index"); } - return Name.Content + " (" + GetType().Name + ")"; + return $"{Name.Content} ({GetType().Name})"; } /// diff --git a/UndertaleModLib/Models/UndertaleEmbeddedTexture.cs b/UndertaleModLib/Models/UndertaleEmbeddedTexture.cs index c5624e846..b95cb76bb 100644 --- a/UndertaleModLib/Models/UndertaleEmbeddedTexture.cs +++ b/UndertaleModLib/Models/UndertaleEmbeddedTexture.cs @@ -4,7 +4,6 @@ using System.Collections.Generic; using System.ComponentModel; using System.Drawing; -using System.Drawing.Imaging; using System.IO; using System.Linq; using System.Runtime.CompilerServices; @@ -22,6 +21,9 @@ public class UndertaleEmbeddedTexture : UndertaleNamedResource, IDisposable /// /// The name of the embedded texture entry. /// + /// + /// This is UTMT specific. The data file does not contain names for embedded textures. + /// public UndertaleString Name { get; set; } /// @@ -55,19 +57,18 @@ public TexData TextureData get => _textureData ??= LoadExternalTexture(); set => _textureData = value; } - private TexData _textureData = new TexData(); - + private TexData _textureData = new(); /// /// Helper variable for whether or not this texture is to be stored externally or not. /// - public bool TextureExternal { get; set; } = false; + public bool TextureExternal { get; set; } /// /// Helper variable for whether or not a texture was loaded yet. /// - public bool TextureLoaded { get; set; } = false; + public bool TextureLoaded { get; set; } /// /// Width of the texture. 2022.9+ only. @@ -229,20 +230,9 @@ public static void FindAllTextureInfo(UndertaleData data) } } - // 1x1 black pixel in PNG format - private static TexData _placeholderTexture = new() - { - TextureBlob = new byte[] - { - 0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A, 0x00, 0x00, 0x00, 0x0D, 0x49, 0x48, 0x44, 0x52, 0x00, 0x00, 0x00, 0x01, - 0x00, 0x00, 0x00, 0x01, 0x08, 0x02, 0x00, 0x00, 0x00, 0x90, 0x77, 0x53, 0xDE, 0x00, 0x00, 0x00, 0x01, 0x73, 0x52, 0x47, - 0x42, 0x00, 0xAE, 0xCE, 0x1C, 0xE9, 0x00, 0x00, 0x00, 0x04, 0x67, 0x41, 0x4D, 0x41, 0x00, 0x00, 0xB1, 0x8F, 0x0B, 0xFC, - 0x61, 0x05, 0x00, 0x00, 0x00, 0x09, 0x70, 0x48, 0x59, 0x73, 0x00, 0x00, 0x0E, 0xC3, 0x00, 0x00, 0x0E, 0xC3, 0x01, 0xC7, - 0x6F, 0xA8, 0x64, 0x00, 0x00, 0x00, 0x0C, 0x49, 0x44, 0x41, 0x54, 0x18, 0x57, 0x63, 0x60, 0x60, 0x60, 0x00, 0x00, 0x00, - 0x04, 0x00, 0x01, 0x5C, 0xCD, 0xFF, 0x69, 0x00, 0x00, 0x00, 0x00, 0x49, 0x45, 0x4E, 0x44, 0xAE, 0x42, 0x60, 0x82 - } - }; - private static object _textureLoadLock = new(); + // 1x1 blank image + private static readonly TexData _placeholderTexture = new() { Image = new GMImage(1, 1) }; + private static readonly object _textureLoadLock = new(); /// /// Attempts to load the corresponding external texture. Should only happen in 2022.9 and above. @@ -272,7 +262,7 @@ public TexData LoadExternalTexture() using FileStream fs = new(path, FileMode.Open); using FileBinaryReader fbr = new(fs); texData = new TexData(); - texData.Unserialize(fbr, true); + texData.Unserialize(fbr, fs.Length, true); TextureLoaded = true; } catch (IOException) @@ -310,62 +300,60 @@ public void Dispose() /// public class TexData : UndertaleObject, INotifyPropertyChanged, IDisposable { - private byte[] _textureBlob; - private static MemoryStream sharedStream; + private GMImage _image; /// - /// The image data of the texture. + /// The underlying image of the texture. /// - public byte[] TextureBlob - { - get => _textureBlob; + public GMImage Image + { + get => _image; set { - _textureBlob = value; + _image = value; OnPropertyChanged(); } } /// /// The width of the texture. - /// In case of an invalid texture data, this will be -1. /// - public int Width - { - get - { - if (_textureBlob is null || _textureBlob.Length < 24) - return -1; + /// + /// In the case of an invalid or missing image, this will always be -1. + /// + public int Width => _image?.Width ?? -1; - ReadOnlySpan span = _textureBlob.AsSpan(); - return BinaryPrimitives.ReadInt32BigEndian(span[16..20]); - } - } /// /// The height of the texture. - /// In case of an invalid texture data, this will be -1. /// - public int Height - { - get - { - if (_textureBlob is null || _textureBlob.Length < 24) - return -1; - - ReadOnlySpan span = _textureBlob.AsSpan(); - return BinaryPrimitives.ReadInt32BigEndian(span[20..24]); - } - } + /// + /// In the case of an invalid or missing image, this will always be -1. + /// + public int Height => _image?.Height ?? -1; /// /// Whether this texture uses the QOI format. /// - public bool FormatQOI { get; set; } = false; + /// + /// In the case of an invalid or missing image, this will always be . + /// + public bool FormatQOI => _image.Format is GMImage.ImageFormat.Qoi or GMImage.ImageFormat.Bz2Qoi; /// /// Whether this texture uses the BZ2 format. (Always used in combination with QOI.) /// - public bool FormatBZ2 { get; set; } = false; + /// + /// In the case of an invalid or missing image, this will always be . + /// + public bool FormatBZ2 => _image.Format is GMImage.ImageFormat.Bz2Qoi; + + /// + /// If located within a data file, this is the upper bound on the end position of the image data (or start of the next texture blob). + /// + /// + /// All data between the actual end position and this maximum end position should be 0x00 byte padding. + /// + private int _maxEndOfStreamPosition { get; set; } = -1; /// public event PropertyChangedEventHandler PropertyChanged; @@ -378,152 +366,56 @@ protected void OnPropertyChanged([CallerMemberName] string name = null) PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(name)); } - /// - /// Header used for PNG files. - /// - public static readonly byte[] PNGHeader = { 137, 80, 78, 71, 13, 10, 26, 10 }; - - /// - /// Header used for GameMaker QOI + BZ2 files. - /// - public static readonly byte[] QOIAndBZip2Header = { 50, 122, 111, 113 }; - - /// - /// Header used for GameMaker QOI files. - /// - public static readonly byte[] QOIHeader = { 102, 105, 111, 113 }; - - /// - /// Frees up from memory. - /// - public static void ClearSharedStream() - { - sharedStream?.Dispose(); - sharedStream = null; - } - - /// - /// Initializes with a specified initial size. - /// - /// Initial size of in bytes - public static void InitSharedStream(int size) => sharedStream = new(size); - /// public void Serialize(UndertaleWriter writer) { - Serialize(writer, writer.undertaleData.IsVersionAtLeast(2022, 3), writer.undertaleData.IsVersionAtLeast(2022, 5)); + Serialize(writer, writer.undertaleData.IsVersionAtLeast(2022, 5)); } /// /// Serializes the texture to any type of writer (can be any destination file). /// - public void Serialize(FileBinaryWriter writer, bool gm2022_3, bool gm2022_5) + public void Serialize(FileBinaryWriter writer, bool gm2022_5) { - if (FormatQOI) + if (Image.Format == GMImage.ImageFormat.RawBgra) { - if (FormatBZ2) - { - writer.Write(QOIAndBZip2Header); - - // Encode the PNG data back to QOI+BZip2 - using Bitmap bmp = TextureWorker.GetImageFromByteArray(TextureBlob); - writer.Write((short)bmp.Width); - writer.Write((short)bmp.Height); - byte[] qoiData = QoiConverter.GetArrayFromImage(bmp, gm2022_3 ? 0 : 4); - using MemoryStream input = new MemoryStream(qoiData); - if (sharedStream.Length != 0) - sharedStream.Seek(0, SeekOrigin.Begin); - BZip2.Compress(input, sharedStream, false, 9); - if (gm2022_5) - writer.Write((uint)qoiData.Length); - writer.Write(sharedStream.GetBuffer().AsSpan()[..(int)sharedStream.Position]); - } - else - { - // Encode the PNG data back to QOI - using Bitmap bmp = TextureWorker.GetImageFromByteArray(TextureBlob); - writer.Write(QoiConverter.GetSpanFromImage(bmp, gm2022_3 ? 0 : 4)); - } + throw new Exception("Unexpected raw RGBA image"); } - else - writer.Write(TextureBlob); + + Image.WriteToBinaryWriter(writer, gm2022_5); } /// public void Unserialize(UndertaleReader reader) { - Unserialize(reader, reader.undertaleData.IsVersionAtLeast(2022, 5)); + Unserialize(reader, _maxEndOfStreamPosition, reader.undertaleData.IsVersionAtLeast(2022, 5)); } /// /// Unserializes the texture from any type of reader (can be from any source). /// - public void Unserialize(IBinaryReader reader, bool gm2022_5) + /// to read the texture's image from. + /// Upper bound on the end of the texture's image data (e.g., for padding). + /// Whether to unserialize the image data using GameMaker 2022.5+ format. + public void Unserialize(IBinaryReader reader, long maxEndOfStreamPosition, bool gm2022_5) { - sharedStream ??= new(); - - long startAddress = reader.Position; - - byte[] header = reader.ReadBytes(8); - if (!header.SequenceEqual(PNGHeader)) + if (maxEndOfStreamPosition == -1) { - reader.Position = startAddress; - - if (header.Take(4).SequenceEqual(QOIAndBZip2Header)) - { - FormatQOI = true; - FormatBZ2 = true; - - // Don't really care about the width/height, so skip them, as well as header - reader.Position += (uint)(gm2022_5 ? 12 : 8); - - // Need to fully decompress and convert the QOI data to PNG for compatibility purposes (at least for now) - if (sharedStream.Length != 0) - sharedStream.Seek(0, SeekOrigin.Begin); - BZip2.Decompress(reader.Stream, sharedStream, false); - ReadOnlySpan decompressed = sharedStream.GetBuffer().AsSpan()[..(int)sharedStream.Position]; - using Bitmap bmp = QoiConverter.GetImageFromSpan(decompressed); - sharedStream.Seek(0, SeekOrigin.Begin); - bmp.Save(sharedStream, ImageFormat.Png); - TextureBlob = new byte[(int)sharedStream.Position]; - sharedStream.Seek(0, SeekOrigin.Begin); - sharedStream.Read(TextureBlob, 0, TextureBlob.Length); - return; - } - else if (header.Take(4).SequenceEqual(QOIHeader)) - { - FormatQOI = true; - FormatBZ2 = false; - - // Need to convert the QOI data to PNG for compatibility purposes (at least for now) - using Bitmap bmp = QoiConverter.GetImageFromStream(reader.Stream); - if (sharedStream.Length != 0) - sharedStream.Seek(0, SeekOrigin.Begin); - bmp.Save(sharedStream, ImageFormat.Png); - TextureBlob = new byte[(int)sharedStream.Position]; - sharedStream.Seek(0, SeekOrigin.Begin); - sharedStream.Read(TextureBlob, 0, TextureBlob.Length); - return; - } - else - throw new IOException("Didn't find PNG or QOI+BZip2 header"); + throw new Exception("Expected max end of stream position to be set before unserializing"); } - // There is no length for the PNG anywhere as far as I can see - // The only thing we can do is parse the image to find the end - while (true) - { - // PNG is big endian and BinaryRead can't handle that (damn) - uint len = (uint)reader.ReadByte() << 24 | (uint)reader.ReadByte() << 16 | (uint)reader.ReadByte() << 8 | (uint)reader.ReadByte(); - uint type = reader.ReadUInt32(); - reader.Position += len + 4; - if (type == 0x444e4549) // 0x444e4549 -> "IEND" - break; - } + Image = GMImage.FromBinaryReader(reader, maxEndOfStreamPosition, gm2022_5); + } - long length = reader.Position - startAddress; - reader.Position = startAddress; - TextureBlob = reader.ReadBytes((int)length); + /// + /// Sets the upper bound on the position of the end of the image stream, for use when loading a full data file. + /// + /// + /// All data between the actual end position and this maximum end position should be padding (zero bytes). + /// + public void SetMaxEndOfStreamPosition(int position) + { + _maxEndOfStreamPosition = position; } @@ -532,8 +424,7 @@ public void Dispose() { GC.SuppressFinalize(this); - _textureBlob = null; - ClearSharedStream(); + _image = null; } } } \ No newline at end of file diff --git a/UndertaleModLib/Models/UndertaleFont.cs b/UndertaleModLib/Models/UndertaleFont.cs index 34a495aff..e8d5a57d2 100644 --- a/UndertaleModLib/Models/UndertaleFont.cs +++ b/UndertaleModLib/Models/UndertaleFont.cs @@ -313,7 +313,7 @@ public void Unserialize(UndertaleReader reader) // since the float is always written negated, it has the first bit set. if ((readEmSize & (1 << 31)) != 0) { - float fsize = -BitConverter.ToSingle(BitConverter.GetBytes(EmSize), 0); + float fsize = -BitConverter.ToSingle(BitConverter.GetBytes(readEmSize), 0); EmSize = fsize; EmSizeIsFloat = true; } diff --git a/UndertaleModLib/Models/UndertaleGeneralInfo.cs b/UndertaleModLib/Models/UndertaleGeneralInfo.cs index abd16c877..edab0b354 100644 --- a/UndertaleModLib/Models/UndertaleGeneralInfo.cs +++ b/UndertaleModLib/Models/UndertaleGeneralInfo.cs @@ -479,24 +479,13 @@ public void Unserialize(UndertaleReader reader) if (reader.ReadOnlyGEN8) return; - var detectedVer = TestForCommonGMSVersions(reader, (Major, Minor, Release, Build, Branch)); - (Major, Minor, Release, Build, Branch) = detectedVer; - - if (reader.undertaleData.GeneralInfo is not null) - { - var prevGenInfo = reader.undertaleData.GeneralInfo; - // If previous version is greater than current - if (prevGenInfo.Major > Major - || prevGenInfo.Major == Major && prevGenInfo.Minor > Minor - || prevGenInfo.Major == Major && prevGenInfo.Minor == Minor && prevGenInfo.Release > Release - || prevGenInfo.Major == Major && prevGenInfo.Minor == Minor && prevGenInfo.Release == Release && prevGenInfo.Build > Build) - { - Major = prevGenInfo.Major; - Minor = prevGenInfo.Minor; - Release = prevGenInfo.Release; - Build = prevGenInfo.Build; - } - } + // TestForCommonGMSVersions is run during the object counting phase, so the previous general info is always accurate. + var prevGenInfo = reader.undertaleData.GeneralInfo; + Major = prevGenInfo.Major; + Minor = prevGenInfo.Minor; + Release = prevGenInfo.Release; + Build = prevGenInfo.Build; + Branch = prevGenInfo.Branch; DefaultWindowWidth = reader.ReadUInt32(); DefaultWindowHeight = reader.ReadUInt32(); @@ -712,7 +701,8 @@ public enum OptionsFlags : ulong UseRearTouch = 0x2000000, UseFastCollision = 0x4000000, FastCollisionCompatibility = 0x8000000, - DisableSandbox = 0x10000000 + DisableSandbox = 0x10000000, + EnableCopyOnWrite = 0x20000000 } /// diff --git a/UndertaleModLib/Models/UndertaleGlobalInit.cs b/UndertaleModLib/Models/UndertaleGlobalInit.cs index 8f36c3884..3c833ab88 100644 --- a/UndertaleModLib/Models/UndertaleGlobalInit.cs +++ b/UndertaleModLib/Models/UndertaleGlobalInit.cs @@ -6,7 +6,8 @@ namespace UndertaleModLib.Models; /// /// A global initialization entry in a data file. /// -/// Never seen in GMS1.4 so uncertain if the structure was the same. +/// +// TODO: Never seen in GMS1.4 so uncertain if the structure was the same. public class UndertaleGlobalInit : UndertaleObject, INotifyPropertyChanged, IDisposable { private UndertaleResourceById _code = new(); @@ -14,6 +15,7 @@ public class UndertaleGlobalInit : UndertaleObject, INotifyPropertyChanged, IDis /// /// The object which contains the code. /// + /// This code is executed at a global scope, before the first room of the game executes. public UndertaleCode Code { get => _code.Resource; set { _code.Resource = value; PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(Code))); } } /// @@ -29,7 +31,7 @@ public void Serialize(UndertaleWriter writer) public void Unserialize(UndertaleReader reader) { _code = new UndertaleResourceById(); - _code.Unserialize(reader); // TODO: reader.ReadUndertaleObject if one object starts with another one + _code.Unserialize(reader); // Cannot use ReadUndertaleObject, as that messes things up. } /// @@ -39,4 +41,4 @@ public void Dispose() _code.Dispose(); } -} \ No newline at end of file +} diff --git a/UndertaleModLib/Models/UndertaleSprite.cs b/UndertaleModLib/Models/UndertaleSprite.cs index 3eedfe981..e1f584e94 100644 --- a/UndertaleModLib/Models/UndertaleSprite.cs +++ b/UndertaleModLib/Models/UndertaleSprite.cs @@ -279,9 +279,8 @@ public void Dispose() public MaskEntry NewMaskEntry() { - MaskEntry newEntry = new MaskEntry(); uint len = (Width + 7) / 8 * Height; - newEntry.Data = new byte[len]; + MaskEntry newEntry = new MaskEntry(new byte[len], Width, Height); return newEntry; } @@ -348,13 +347,24 @@ public class MaskEntry : IDisposable { public byte[] Data { get; set; } + /// + /// Width of this sprite mask. UTMT only. + /// + public uint Width { get; set; } + /// + /// Height of this sprite mask. UTMT only. + /// + public uint Height { get; set; } + public MaskEntry() { } - public MaskEntry(byte[] data) + public MaskEntry(byte[] data, uint width, uint height) { this.Data = data; + this.Width = width; + this.Height = height; } /// @@ -514,7 +524,9 @@ private void WriteMaskData(UndertaleWriter writer) writer.Write((byte)0); total++; } - Util.DebugUtil.Assert(total == CalculateMaskDataSize(Width, Height, (uint)CollisionMasks.Count), "Invalid mask data for sprite"); + + (uint width, uint height) = CalculateMaskDimensions(writer.undertaleData); + Util.DebugUtil.Assert(total == CalculateMaskDataSize(width, height, (uint)CollisionMasks.Count), "Invalid mask data for sprite"); } private static byte[] DecodeSpineBlob(byte[] blob) @@ -744,7 +756,9 @@ public static uint UnserializeChildObjectCount(UndertaleReader reader) case SpriteType.Spine: { - reader.Align(4); + case 1: + reader.Position += 8 + (uint)jsonLength + (uint)atlasLength + (uint)textures; + break; if (reader.undertaleData.IsVersionAtLeast(2023, 1)) count += 1 + UndertaleSimpleList.UnserializeChildObjectCount(reader); @@ -761,9 +775,7 @@ public static uint UnserializeChildObjectCount(UndertaleReader reader) switch (spineVersion) { - case 1: - reader.Position += 8 + jsonLength + atlasLength + textures; - break; + reader.Position += (uint)jsonLength + (uint)atlasLength; case 2: case 3: @@ -851,7 +863,7 @@ private void ReadMaskData(UndertaleReader reader) uint total = 0; for (uint i = 0; i < maskCount; i++) { - newMasks.Add(new MaskEntry(reader.ReadBytes((int)len))); + newMasks.Add(new MaskEntry(reader.ReadBytes((int)len), width, height)); total += len; } diff --git a/UndertaleModLib/Models/UndertaleTexturePageItem.cs b/UndertaleModLib/Models/UndertaleTexturePageItem.cs index 612041e1e..7cfcc3e78 100644 --- a/UndertaleModLib/Models/UndertaleTexturePageItem.cs +++ b/UndertaleModLib/Models/UndertaleTexturePageItem.cs @@ -1,4 +1,5 @@ -using System; +using ImageMagick; +using System; using System.ComponentModel; using System.Drawing; using UndertaleModLib.Util; @@ -145,33 +146,25 @@ public void Dispose() /// Replaces the current image of this texture page item to hold a new image. /// /// The new image that shall be applied to this texture page item. - /// Whether to dispose afterwards. - public void ReplaceTexture(Image replaceImage, bool disposeImage = true) + public void ReplaceTexture(MagickImage replaceImage) { - Image finalImage = TextureWorker.ResizeImage(replaceImage, SourceWidth, SourceHeight); + // Resize image to bounds on texture page + using IMagickImage finalImage = TextureWorker.ResizeImage(replaceImage, SourceWidth, SourceHeight); - // Apply the image to the TexturePage. + // Apply the image to the texture page lock (TexturePage.TextureData) { - TextureWorker worker = new TextureWorker(); - Bitmap embImage = worker.GetEmbeddedTexture(TexturePage); // Use SetPixel if needed. + using TextureWorker worker = new(); + MagickImage embImage = worker.GetEmbeddedTexture(TexturePage); - Graphics g = Graphics.FromImage(embImage); - g.CompositingMode = System.Drawing.Drawing2D.CompositingMode.SourceCopy; - g.DrawImage(finalImage, SourceX, SourceY); - g.Dispose(); + embImage.Composite(finalImage, SourceX, SourceY, CompositeOperator.Copy); - TexturePage.TextureData.TextureBlob = TextureWorker.GetImageBytes(embImage); - - worker.Cleanup(); + // Replace original texture with the new version, in the original texture format + TexturePage.TextureData.Image = GMImage.FromMagickImage(embImage) + .ConvertToFormat(TexturePage.TextureData.Image.Format); } TargetWidth = (ushort)replaceImage.Width; TargetHeight = (ushort)replaceImage.Height; - - // Cleanup. - finalImage.Dispose(); - if (disposeImage) - replaceImage.Dispose(); } } \ No newline at end of file diff --git a/UndertaleModLib/UndertaleChunks.cs b/UndertaleModLib/UndertaleChunks.cs index 319067262..d58b80dd0 100644 --- a/UndertaleModLib/UndertaleChunks.cs +++ b/UndertaleModLib/UndertaleChunks.cs @@ -1168,7 +1168,7 @@ private void CheckForTileCompression(UndertaleReader reader) reader.Position += 32; int effectCount = reader.ReadInt32(); - reader.Position += effectCount * 12 + 4; + reader.Position += (uint)effectCount * 12 + 4; int tileMapWidth = reader.ReadInt32(); int tileMapHeight = reader.ReadInt32(); @@ -1503,8 +1503,8 @@ private void CheckFor2022_3And5(UndertaleReader reader) reader.Position = positionToReturn + 4 + (i * 4); reader.AbsPosition = reader.ReadUInt32() + 12; // Go to texture, at an offset reader.AbsPosition = reader.ReadUInt32(); // Go to texture data - byte[] header = reader.ReadBytes(4); - if (!header.SequenceEqual(UndertaleEmbeddedTexture.TexData.QOIAndBZip2Header)) + ReadOnlySpan header = reader.ReadBytes(4); + if (!header.SequenceEqual(GMImage.MagicBz2Qoi)) { // Nothing useful, check the next texture continue; @@ -1547,10 +1547,6 @@ internal override void SerializeChunk(UndertaleWriter writer) // texture blobs if (List.Count > 0) { - // Compressed size can't be bigger than maximum decompressed size - int maxSize = List.Select(x => x.TextureData.TextureBlob?.Length ?? 0).Max(); - UndertaleEmbeddedTexture.TexData.InitSharedStream(maxSize); - bool anythingUsesQoi = false; foreach (var tex in List) { @@ -1565,8 +1561,7 @@ internal override void SerializeChunk(UndertaleWriter writer) if (anythingUsesQoi) { // Calculate maximum size of QOI converter buffer - maxSize = List.Select(x => x.TextureData.Width * x.TextureData.Height).Max() - * QoiConverter.MaxChunkSize + QoiConverter.HeaderSize + (writer.undertaleData.IsVersionAtLeast(2022, 3) ? 0 : 4); + int maxSize = (List.Max(x => x.TextureData.Width * x.TextureData.Height) * QoiConverter.MaxChunkSize) + QoiConverter.HeaderSize; QoiConverter.InitSharedBuffer(maxSize); } } @@ -1631,6 +1626,8 @@ private void CheckForGMS2_0_6(UndertaleReader reader) internal override void UnserializeChunk(UndertaleReader reader) { + long startPosition = reader.AbsPosition; + if (!checkedFor2022_3) CheckFor2022_3And5(reader); @@ -1645,6 +1642,35 @@ internal override void UnserializeChunk(UndertaleReader reader) { UndertaleEmbeddedTexture obj = List[index]; + if (!obj.TextureExternal) + { + // Calculate maximum end stream position for this blob + int searchIndex = index + 1; + int maxEndOfStreamPosition = -1; + while (searchIndex < List.Count) + { + UndertaleEmbeddedTexture searchObj = List[searchIndex]; + + if (searchObj.TextureExternal) + { + // Skip this texture, as it's external + searchIndex++; + continue; + } + + // Use start address of this blob + maxEndOfStreamPosition = (int)reader.GetOffsetMapRev()[searchObj.TextureData]; + break; + } + + if (maxEndOfStreamPosition == -1) + { + // At end of list, so just use the end of the chunk + maxEndOfStreamPosition = (int)(startPosition + Length); + } + obj.TextureData.SetMaxEndOfStreamPosition(maxEndOfStreamPosition); + } + obj.UnserializeBlob(reader); obj.Name = new UndertaleString("Texture " + index.ToString()); } diff --git a/UndertaleModLib/UndertaleModLib.csproj b/UndertaleModLib/UndertaleModLib.csproj index acb87b3cf..f2cb79074 100644 --- a/UndertaleModLib/UndertaleModLib.csproj +++ b/UndertaleModLib/UndertaleModLib.csproj @@ -1,13 +1,14 @@  - net6.0 + net8.0 + 11 Library UndertaleModLib UndertaleModLib - Copyright © 2018-2023, licensed under GPLv3 - 0.5.1.0 - 0.5.1.0 + Copyright © 2018-2024, licensed under GPLv3 + 0.6.1.0 + 0.6.1.0 embedded AnyCPU;x64 win-x64;win-x86 @@ -24,10 +25,11 @@ True - + all runtime; build; native; contentfiles; analyzers; buildtransitive + @@ -35,19 +37,43 @@ - - - - + + + + + + + + - - - - + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + diff --git a/UndertaleModLib/Util/AdaptiveBinaryReader.cs b/UndertaleModLib/Util/AdaptiveBinaryReader.cs index 5522678f0..42392484d 100644 --- a/UndertaleModLib/Util/AdaptiveBinaryReader.cs +++ b/UndertaleModLib/Util/AdaptiveBinaryReader.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.IO; using System.Linq; @@ -86,10 +86,8 @@ public long AbsPosition { if (isUsingBufferReader) { -#if DEBUG - if (value > Length) + if (value < 0 || value > Length) throw new IOException("Reading out of bounds."); -#endif bufferBinaryReader.Position = value - bufferBinaryReader.ChunkStartPosition + 8; } else diff --git a/UndertaleModLib/Util/AssetReferenceTypes.cs b/UndertaleModLib/Util/AssetReferenceTypes.cs index f3686c07a..15f11c43b 100644 --- a/UndertaleModLib/Util/AssetReferenceTypes.cs +++ b/UndertaleModLib/Util/AssetReferenceTypes.cs @@ -4,6 +4,9 @@ namespace UndertaleModLib.Util; public static class AssetReferenceTypes { + /// + /// Asset types as used in GML code references. + /// public enum RefType { Object, @@ -22,6 +25,9 @@ public enum RefType RoomInstance } + /// + /// Converts an integer asset type to its equivalent, depending on GameMaker version. + /// public static RefType ConvertToRefType(UndertaleData data, int type) { if (data.IsVersionAtLeast(2024, 4)) @@ -66,6 +72,9 @@ public static RefType ConvertToRefType(UndertaleData data, int type) }; } + /// + /// Converts a to its integer equivalent, depending on GameMaker version. + /// public static int ConvertFromRefType(UndertaleData data, RefType type) { if (data.IsVersionAtLeast(2024, 4)) diff --git a/UndertaleModLib/Util/BufferBinaryReader.cs b/UndertaleModLib/Util/BufferBinaryReader.cs index 4fded571d..f133739e5 100644 --- a/UndertaleModLib/Util/BufferBinaryReader.cs +++ b/UndertaleModLib/Util/BufferBinaryReader.cs @@ -27,6 +27,11 @@ public ChunkBuffer(int capacity) public int Read(byte[] buffer, int count) { + if (count < 0) + { + throw new ArgumentOutOfRangeException(nameof(count)); + } + int n = _length - _position; if (n > count) n = count; @@ -75,6 +80,9 @@ public byte ReadByte() public void Write(byte[] buffer, int count) { + if (count < 0) + throw new ArgumentOutOfRangeException(nameof(count)); + int i = _position + count; if (i < 0) throw new IOException("Writing out of the chunk buffer bounds."); @@ -174,6 +182,10 @@ public virtual bool ReadBoolean() public string ReadChars(int count) { + if (count < 0) + { + throw new ArgumentOutOfRangeException(nameof(count)); + } if (chunkBuffer.Position + count > _length) { throw new IOException("Reading out of chunk bounds"); @@ -198,6 +210,10 @@ public string ReadChars(int count) public byte[] ReadBytes(int count) { + if (count < 0) + { + throw new ArgumentOutOfRangeException(nameof(count)); + } if (chunkBuffer.Position + count > _length) { throw new IOException("Reading out of chunk bounds"); @@ -268,7 +284,9 @@ public string ReadGMString() int length = BinaryPrimitives.ReadInt32LittleEndian(ReadToBuffer(4)); - if (chunkBuffer.Position + length + 1 >= _length) + if (length < 0) + throw new IOException("Invalid string length"); + if (chunkBuffer.Position + length + 1 > _length) throw new IOException("Reading out of chunk bounds"); string res; @@ -293,9 +311,12 @@ public string ReadGMString() return res; } + public void SkipGMString() { int length = BinaryPrimitives.ReadInt32LittleEndian(ReadToBuffer(4)); + if (length < 0) + throw new IOException("Invalid string length"); Position += (uint)length + 1; } diff --git a/UndertaleModLib/Util/FileBinaryReader.cs b/UndertaleModLib/Util/FileBinaryReader.cs index 87525d226..419ae9551 100644 --- a/UndertaleModLib/Util/FileBinaryReader.cs +++ b/UndertaleModLib/Util/FileBinaryReader.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Buffers.Binary; using System.IO; using System.Text; @@ -21,10 +21,8 @@ public long Position get => Stream.Position; set { -#if DEBUG if (value > Length) throw new IOException("Reading out of bounds."); -#endif Stream.Position = value; } } @@ -49,10 +47,11 @@ private ReadOnlySpan ReadToBuffer(int count) public byte ReadByte() { -#if DEBUG if (Stream.Position + 1 > _length) + { throw new IOException("Reading out of bounds"); -#endif + } + return (byte)Stream.ReadByte(); } @@ -63,10 +62,15 @@ public virtual bool ReadBoolean() public string ReadChars(int count) { -#if DEBUG + if (count < 0) + { + throw new ArgumentOutOfRangeException(nameof(count)); + } if (Stream.Position + count > _length) + { throw new IOException("Reading out of bounds"); -#endif + } + if (count > 1024) { byte[] buf = new byte[count]; @@ -85,10 +89,15 @@ public string ReadChars(int count) public byte[] ReadBytes(int count) { -#if DEBUG + if (count < 0) + { + throw new ArgumentOutOfRangeException(nameof(count)); + } if (Stream.Position + count > _length) + { throw new IOException("Reading out of bounds"); -#endif + } + byte[] val = new byte[count]; Stream.Read(val, 0, count); return val; @@ -96,107 +105,118 @@ public byte[] ReadBytes(int count) public short ReadInt16() { -#if DEBUG if (Stream.Position + 2 > _length) + { throw new IOException("Reading out of bounds"); -#endif + } + return BinaryPrimitives.ReadInt16LittleEndian(ReadToBuffer(2)); } public ushort ReadUInt16() { -#if DEBUG if (Stream.Position + 2 > _length) + { throw new IOException("Reading out of bounds"); -#endif + } + return BinaryPrimitives.ReadUInt16LittleEndian(ReadToBuffer(2)); } public int ReadInt24() { -#if DEBUG if (Stream.Position + 3 > _length) + { throw new IOException("Reading out of bounds"); -#endif + } + ReadToBuffer(3); return buffer[0] | buffer[1] << 8 | (sbyte)buffer[2] << 16; } public uint ReadUInt24() { -#if DEBUG if (Stream.Position + 3 > _length) + { throw new IOException("Reading out of bounds"); -#endif + } + ReadToBuffer(3); return (uint)(buffer[0] | buffer[1] << 8 | buffer[2] << 16); } public int ReadInt32() { -#if DEBUG if (Stream.Position + 4 > _length) + { throw new IOException("Reading out of bounds"); -#endif + } + return BinaryPrimitives.ReadInt32LittleEndian(ReadToBuffer(4)); } public uint ReadUInt32() { -#if DEBUG if (Stream.Position + 4 > _length) + { throw new IOException("Reading out of bounds"); -#endif + } + return BinaryPrimitives.ReadUInt32LittleEndian(ReadToBuffer(4)); } public float ReadSingle() { -#if DEBUG if (Stream.Position + 4 > _length) + { throw new IOException("Reading out of bounds"); -#endif + } + return BitConverter.Int32BitsToSingle(BinaryPrimitives.ReadInt32LittleEndian(ReadToBuffer(4))); } public double ReadDouble() { -#if DEBUG if (Stream.Position + 8 > _length) + { throw new IOException("Reading out of bounds"); -#endif + } + return BitConverter.Int64BitsToDouble(BinaryPrimitives.ReadInt64LittleEndian(ReadToBuffer(8))); } public long ReadInt64() { -#if DEBUG if (Stream.Position + 8 > _length) + { throw new IOException("Reading out of bounds"); -#endif + } + return BinaryPrimitives.ReadInt64LittleEndian(ReadToBuffer(8)); } public ulong ReadUInt64() { -#if DEBUG if (Stream.Position + 8 > _length) + { throw new IOException("Reading out of bounds"); -#endif + } + return BinaryPrimitives.ReadUInt64LittleEndian(ReadToBuffer(8)); } public string ReadGMString() { -#if DEBUG if (Stream.Position + 5 > _length) throw new IOException("Reading out of bounds"); -#endif + int length = BinaryPrimitives.ReadInt32LittleEndian(ReadToBuffer(4)); -#if DEBUG - if (Stream.Position + length + 1 >= _length) + + if (length < 0) + throw new IOException("Invalid string length"); + if (Stream.Position + length + 1 > _length) throw new IOException("Reading out of bounds"); -#endif + string res; if (length > 1024) { @@ -210,18 +230,20 @@ public string ReadGMString() Stream.Read(buf); res = encoding.GetString(buf); } - -#if DEBUG + if (Stream.ReadByte() != 0) + { throw new IOException("String not null terminated!"); -#else - Position++; -#endif + } + return res; } + public void SkipGMString() { int length = BinaryPrimitives.ReadInt32LittleEndian(ReadToBuffer(4)); + if (length < 0) + throw new IOException("Invalid string length"); Position += (uint)length + 1; } diff --git a/UndertaleModLib/Util/GMImage.cs b/UndertaleModLib/Util/GMImage.cs new file mode 100644 index 000000000..3ce06e930 --- /dev/null +++ b/UndertaleModLib/Util/GMImage.cs @@ -0,0 +1,863 @@ +using ICSharpCode.SharpZipLib.BZip2; +using ImageMagick; +using System; +using System.Buffers.Binary; +using System.IO; + +namespace UndertaleModLib.Util; + +/// +/// Immutable wrapper around GameMaker texture images. +/// +public class GMImage +{ + /// + /// Supported formats of GameMaker textures. + /// + public enum ImageFormat + { + /// + /// Raw BGRA color format, with 8 bits per channel (32 bits per pixel). + /// + RawBgra, + + /// + /// PNG file format. + /// + Png, + + /// + /// GameMaker's custom variant of the QOI image file format. + /// + Qoi, + + /// + /// BZip2 compression applied on top of GameMaker's custom variant of the QOI image file format. + /// + Bz2Qoi + } + + /// + /// Format of this image. + /// + public ImageFormat Format { get; init; } + + /// + /// Width of this image, in pixels. + /// + public int Width { get; init; } + + /// + /// Height of this image, in pixels. + /// + public int Height { get; init; } + + /// + /// Maximum supported image width or height. + /// + public const int MaxImageDimension = 16384; + + /// + /// PNG file format magic. + /// + public static ReadOnlySpan MagicPng => new byte[] { 137, 80, 78, 71, 13, 10, 26, 10 }; + + /// + /// QOI file format magic. + /// + public static ReadOnlySpan MagicQoi => "fioq"u8; + + /// + /// BZip2 + QOI file format magic. + /// + public static ReadOnlySpan MagicBz2Qoi => "2zoq"u8; + + /// + /// Magic value found near the end of a BZip2 stream (square root of pi). + /// + private static ReadOnlySpan MagicBz2Footer => new byte[] { 0x17, 0x72, 0x45, 0x38, 0x50, 0x90 }; + + /// + /// Backing data for the image, whether compressed or not. + /// + private readonly byte[] _data = null; + + /// + /// If this is a Bz2Qoi image in GameMaker 2022.5 and above, then this is + /// the size of the BZip2 data when entirely uncompressed. + /// + private int _bz2UncompressedSize { get; init; } = -1; + + /// + /// Initializes an image with raw format, of the desired width and height. + /// + /// + /// Creates a completely blank image (black, fully transparent). + /// + public GMImage(int width, int height) + { + if (width is < 0 or > MaxImageDimension) + { + throw new ArgumentOutOfRangeException(nameof(width)); + } + if (height is < 0 or > MaxImageDimension) + { + throw new ArgumentOutOfRangeException(nameof(height)); + } + + Format = ImageFormat.RawBgra; + Width = width; + Height = height; + _data = new byte[width * height * 4]; + } + + /// + /// Basic private constructor for use by other creation methods; just initializes the given fields. + /// + private GMImage(ImageFormat format, int width, int height, byte[] data) + { + Format = format; + Width = width; + Height = height; + _data = data; + } + + /// + /// Searches for the BZ2 footer magic, when around the end of a BZ2 stream, + /// and returns the exact end position of the stream. + /// + private static long FindEndOfBZ2Search(IBinaryReader reader, long endDataPosition) + { + // Read 16 bytes from the end of the BZ2 stream + Span data = stackalloc byte[16]; + reader.Position = endDataPosition - data.Length; + int numBytesRead = reader.Stream.Read(data); + + // Start searching for magic, bit by bit (it is not always byte-aligned) + ReadOnlySpan footerMagic = MagicBz2Footer; + int searchStartPosition = numBytesRead - 1; + int searchStartBitPosition = 0; + while (searchStartPosition >= 0) + { + // Perform search starting from the current search start position + bool foundMatch = false; + int bitPosition = searchStartBitPosition; + int searchPosition = searchStartPosition; + int magicBitPosition = 0; + int magicPosition = footerMagic.Length - 1; + while (searchPosition >= 0) + { + // Get bits at search position and corresponding magic position + bool currentBit = (data[searchPosition] & (1 << bitPosition)) != 0; + bool magicCurrentBit = (footerMagic[magicPosition] & (1 << magicBitPosition)) != 0; + + // If bits mismatch, terminate the current search + if (currentBit != magicCurrentBit) + { + break; + } + + // Found a matching bit! + // Progress magic position to next bit + magicBitPosition++; + if (magicBitPosition >= 8) + { + magicBitPosition = 0; + magicPosition--; + } + + // If we reached the end of the magic, then we successfully found a full match! + if (magicPosition < 0) + { + foundMatch = true; + break; + } + + // We didn't find a full match yet, so we also need to progress our search position to the next bit + bitPosition++; + if (bitPosition >= 8) + { + bitPosition = 0; + searchPosition--; + } + } + + if (foundMatch) + { + // We found a full match, so calculate end of stream position + const int footerByteLength = 10; + long endOfBZ2StreamPosition = searchPosition + footerByteLength; + if (bitPosition != 7) + { + // BZip2 footer started partway through a byte, and so it will end partway through the last byte. + // By the BZip2 specification, the unused bits of the last byte are essentially padding. + endOfBZ2StreamPosition++; + } + + // Return position relative to the start of the data we read + return (endDataPosition - data.Length) + endOfBZ2StreamPosition; + } + + // Current search failed to make a full match, so progress to next bit, to search starting from there + searchStartBitPosition++; + if (searchStartBitPosition >= 8) + { + searchStartBitPosition = 0; + searchStartPosition--; + } + } + + throw new IOException("Failed to find BZip2 footer magic"); + } + + /// + /// Finds the end position of a BZ2 stream exactly, given the start and end bounds of the data. + /// + private static long FindEndOfBZ2Stream(IBinaryReader reader, long startOfStreamPosition, long maxEndOfStreamPosition) + { + if (startOfStreamPosition >= maxEndOfStreamPosition) + { + throw new ArgumentOutOfRangeException(nameof(startOfStreamPosition)); + } + + // Read backwards from the max end of stream position, in up to 256-byte chunks. + // We want to find the end of nonzero data. + const int maxChunkSize = 256; + Span chunkData = stackalloc byte[maxChunkSize]; + long chunkStartPosition = Math.Max(startOfStreamPosition, maxEndOfStreamPosition - maxChunkSize); + int chunkSize = (int)(maxEndOfStreamPosition - chunkStartPosition); + do + { + // Read chunk from stream + reader.Position = chunkStartPosition; + reader.Stream.Read(chunkData[..chunkSize]); + + // Find first nonzero byte at end of stream + int position = chunkSize - 1; + while (position >= 0 && chunkData[position] == 0) + { + position--; + } + + // If we're at nonzero data, then invoke search for footer magic + if (position >= 0 && chunkData[position] != 0) + { + return FindEndOfBZ2Search(reader, chunkStartPosition + position + 1); + } + + // Move backwards to next chunk + chunkStartPosition = Math.Max(startOfStreamPosition, chunkStartPosition - maxChunkSize); + } + while (chunkStartPosition > startOfStreamPosition); + + throw new IOException("Failed to find nonzero data"); + } + + /// + /// Creates a from the image contents stored at the current position of the provided . + /// + /// Binary reader to read the image data from. + /// + /// Location where the image stream must end at or before, from within the . + /// There should only be 0x00 bytes (AKA padding), between the end of the image data and this position. + /// + /// Whether using GameMaker version 2022.5 or above. Relevant only for BZ2 + QOI format images. + /// If no supported texture format is found + /// Image data fails to parse + public static GMImage FromBinaryReader(IBinaryReader reader, long maxEndOfStreamPosition, bool gm2022_5) + { + ArgumentNullException.ThrowIfNull(reader); + + // Determine type of image by reading the first 8 bytes + long startAddress = reader.Position; + ReadOnlySpan header = reader.ReadBytes(8); + + // PNG + if (header.SequenceEqual(MagicPng)) + { + // There's no overall PNG image length, so we parse image chunks, + // which do have their own length, until we find the end + while (true) + { + // PNG is big endian, so swap endianness here manually + uint len = reader.ReadUInt32(); + len = (len >> 16) | (len << 16); + len = ((len & 0xFF00FF00) >> 8) | ((len & 0x00FF00FF) << 8); + + uint type = reader.ReadUInt32(); + reader.Position += len + 4; + if (type == 0x444e4549) // 0x444e4549 -> "IEND" + break; + } + + // Calculate length, read entire image to byte array + long length = reader.Position - startAddress; + reader.Position = startAddress; + return FromPng(reader.ReadBytes((int)length)); + } + + // QOI + BZip2 + if (header.StartsWith(MagicBz2Qoi)) + { + // Skip past (start of) header + reader.Position = startAddress + 8; + + // Read uncompressed data size, if it exists + int serializedUncompressedLength = -1; + int headerSize = 8; + if (gm2022_5) + { + serializedUncompressedLength = reader.ReadInt32(); + headerSize = 12; + } + + // Find compressed data length, by finding end of BZip2 stream + long endOfBZ2Stream = FindEndOfBZ2Stream(reader, reader.Position, maxEndOfStreamPosition); + int compressedLength = (int)(endOfBZ2Stream - (startAddress + headerSize)); + + // Get width/height of image from BZ2 header + int width = header[4] | (header[5] << 8); + int height = header[6] | (header[7] << 8); + + // Read entire image, *EXCLUDING BZ2 HEADER*, to byte array + reader.Position = startAddress + headerSize; + return FromBz2Qoi(reader.ReadBytes(compressedLength), width, height, serializedUncompressedLength); + } + + // QOI + if (header.StartsWith(MagicQoi)) + { + // Read length of data + uint compressedLength = reader.ReadUInt32(); + + // Read entire image to byte array + reader.Position = startAddress; + return FromQoi(reader.ReadBytes(12 + (int)compressedLength)); + } + + throw new IOException("Failed to recognize any known image header"); + } + + // Either retrieves the known uncompressed data size, or makes a lowball guess as to what it could be + private int GetInitialUncompressedBufferCapacity() + { + if (_bz2UncompressedSize != -1) + { + // We already know the uncompressed size, so use it + return _bz2UncompressedSize; + } + else + { + // Make a guess - it's probably at LEAST 2 times larger + return _data.Length * 2; + } + } + + /// + /// Creates a of PNG format, wrapping around the provided byte array containing PNG data. + /// + /// Byte array of PNG data. + /// Whether to check that the PNG magic exists or not. + /// Invalid PNG data, or image is too large + public static GMImage FromPng(byte[] data, bool verifyHeader = false) + { + ArgumentNullException.ThrowIfNull(data); + if (data.Length < 24) + { + throw new InvalidDataException("PNG data is too short"); + } + ReadOnlySpan span = data.AsSpan(); + + // Verify header, if requested + if (verifyHeader && !span[0..8].SequenceEqual(MagicPng)) + { + throw new InvalidDataException("PNG header mismatch (not a PNG file)"); + } + + // Calculate width/height from data + int width = BinaryPrimitives.ReadInt32BigEndian(span[16..20]); + int height = BinaryPrimitives.ReadInt32BigEndian(span[20..24]); + + // Ensure dimensions are valid + if (width is < 0 or > MaxImageDimension) + { + throw new InvalidDataException($"Width out of range ({width})"); + } + if (height is < 0 or > MaxImageDimension) + { + throw new InvalidDataException($"Height out of range ({height})"); + } + + // Create wrapper image + return new GMImage(ImageFormat.Png, width, height, data); + } + + /// + /// Creates a of BZ2 + QOI format, wrapping around the provided byte array containing BZ2-compressed data (no header). + /// + /// Compressed BZ2 data, excluding the header. + /// Width of the image, as provided in BZ2 + QOI header. + /// Height of the image, as provideed in BZ2 + QOI header. + /// Length of BZ2 data when fully uncompressed. + /// Invalid BZ2 + QOI data, or image is too large + public static GMImage FromBz2Qoi(byte[] compressedData, int width, int height, int uncompressedLength) + { + ArgumentNullException.ThrowIfNull(compressedData); + + // Ensure dimensions are valid + if (width is < 0 or > MaxImageDimension) + { + throw new InvalidDataException($"Width out of range ({width})"); + } + if (height is < 0 or > MaxImageDimension) + { + throw new InvalidDataException($"Height out of range ({height})"); + } + + // Create wrapper image + return new GMImage(ImageFormat.Bz2Qoi, width, height, compressedData) + { + _bz2UncompressedSize = uncompressedLength + }; + } + + /// + /// Creates a of QOI format, wrapping around the provided byte array containing QOI data (GameMaker's custom version). + /// + /// Invalid QOI data, or image is too large + public static GMImage FromQoi(byte[] data) + { + ArgumentNullException.ThrowIfNull(data); + if (data.Length < 12) + { + throw new InvalidDataException("QOI data is too short"); + } + + // Calculate width/height from data + ReadOnlySpan span = data.AsSpan(); + int width = BinaryPrimitives.ReadInt16LittleEndian(span[4..6]); + int height = BinaryPrimitives.ReadInt16LittleEndian(span[6..8]); + + // Ensure dimensions are valid + if (width is < 0 or > MaxImageDimension) + { + throw new InvalidDataException($"Width out of range ({width})"); + } + if (height is < 0 or > MaxImageDimension) + { + throw new InvalidDataException($"Height out of range ({height})"); + } + + // Create wrapper image + return new GMImage(ImageFormat.Qoi, width, height, data); + } + + // Settings to be used for raw data, and when encoding a PNG + private MagickReadSettings GetMagickRawToPngSettings() + { + var settings = new MagickReadSettings() + { + Width = Width, + Height = Height, + Format = MagickFormat.Bgra, + Compression = CompressionMethod.NoCompression + }; + settings.SetDefine(MagickFormat.Png32, "compression-level", 4); + settings.SetDefine(MagickFormat.Png32, "compression-filter", 5); + settings.SetDefine(MagickFormat.Png32, "compression-strategy", 2); + return settings; + } + + /// + /// Saves this image as a PNG file, writing the data to the provided . + /// + public void SavePng(Stream stream) + { + switch (Format) + { + case ImageFormat.RawBgra: + { + // Create image using ImageMagick, and save it as PNG format + using var image = new MagickImage(_data, GetMagickRawToPngSettings()); + image.Alpha(AlphaOption.Set); + image.Format = MagickFormat.Png32; + image.Write(stream); + break; + } + case ImageFormat.Png: + { + // Data is already encoded as PNG; just use that + stream.Write(_data); + break; + } + case ImageFormat.Qoi: + { + // Convert to raw image data, and then save that to a PNG + GMImage rawImage = QoiConverter.GetImageFromSpan(_data); + rawImage.SavePng(stream); + break; + } + case ImageFormat.Bz2Qoi: + { + GMImage rawImage; + + using (MemoryStream uncompressedData = new(GetInitialUncompressedBufferCapacity())) + { + // Decompress BZ2 data + using (MemoryStream compressedData = new(_data)) + { + BZip2.Decompress(compressedData, uncompressedData, false); + } + + // Convert to raw image data + uncompressedData.Seek(0, SeekOrigin.Begin); + rawImage = QoiConverter.GetImageFromStream(uncompressedData); + } + + // Save raw image to PNG + rawImage.SavePng(stream); + break; + } + default: + throw new InvalidOperationException($"Unknown format {Format}"); + } + } + + /// + /// Returns the same or a new ; the result of converting this image to the specified . + /// + /// Format to convert to + /// Reusable shared to be used when compressing with BZ2, as required. + public GMImage ConvertToFormat(ImageFormat format, MemoryStream sharedStream = null) + { + return format switch + { + ImageFormat.RawBgra => ConvertToRawBgra(), + ImageFormat.Png => ConvertToPng(), + ImageFormat.Qoi => ConvertToQoi(), + ImageFormat.Bz2Qoi => ConvertToBz2Qoi(sharedStream), + _ => throw new ArgumentOutOfRangeException(nameof(format)), + }; + } + + /// + /// Returns the same or a new ; the result of converting this image to format. + /// + public GMImage ConvertToRawBgra() + { + switch (Format) + { + case ImageFormat.RawBgra: + { + // Already in correct format; no conversion to be done + return this; + } + case ImageFormat.Png: + { + // Convert image to raw byte array + var image = new MagickImage(_data); + image.Alpha(AlphaOption.Set); + image.Format = MagickFormat.Bgra; + image.SetCompression(CompressionMethod.NoCompression); + return new GMImage(ImageFormat.RawBgra, Width, Height, image.ToByteArray()); + } + case ImageFormat.Qoi: + { + // Convert to raw image data + return QoiConverter.GetImageFromSpan(_data); + } + case ImageFormat.Bz2Qoi: + { + using (MemoryStream uncompressedData = new(GetInitialUncompressedBufferCapacity())) + { + // Decompress BZ2 data + using (MemoryStream compressedData = new(_data)) + { + BZip2.Decompress(compressedData, uncompressedData, false); + } + + // Convert to raw image data + uncompressedData.Seek(0, SeekOrigin.Begin); + return QoiConverter.GetImageFromStream(uncompressedData); + } + } + } + + throw new InvalidOperationException($"Unknown source format {Format}"); + } + + /// + /// Returns the same or a new ; the result of converting this image to format. + /// + public GMImage ConvertToPng() + { + switch (Format) + { + case ImageFormat.RawBgra: + { + // Create image using ImageMagick, and convert it to PNG format + using var image = new MagickImage(_data, GetMagickRawToPngSettings()); + image.Alpha(AlphaOption.Set); + image.Format = MagickFormat.Png32; + return new GMImage(ImageFormat.Png, Width, Height, image.ToByteArray()); + } + case ImageFormat.Png: + { + // Already in correct format; no conversion to be done + return this; + } + case ImageFormat.Qoi: + { + // Convert to raw image data, and then convert that to a PNG + GMImage rawImage = QoiConverter.GetImageFromSpan(_data); + return rawImage.ConvertToPng(); + } + case ImageFormat.Bz2Qoi: + { + GMImage rawImage; + + using (MemoryStream uncompressedData = new(GetInitialUncompressedBufferCapacity())) + { + // Decompress BZ2 data + using (MemoryStream compressedData = new(_data)) + { + BZip2.Decompress(compressedData, uncompressedData, false); + } + + // Convert to raw image data + uncompressedData.Seek(0, SeekOrigin.Begin); + rawImage = QoiConverter.GetImageFromStream(uncompressedData); + } + + // Convert raw image to PNG + return rawImage.ConvertToPng(); + } + } + + throw new InvalidOperationException($"Unknown source format {Format}"); + } + + /// + /// Returns the same or a new ; the result of converting this image to format. + /// + public GMImage ConvertToQoi() + { + switch (Format) + { + case ImageFormat.RawBgra: + case ImageFormat.Png: + case ImageFormat.Bz2Qoi: + { + // Encode image as QOI + return new GMImage(ImageFormat.Qoi, Width, Height, QoiConverter.GetArrayFromImage(this, false)); + } + case ImageFormat.Qoi: + { + // Already in correct format; no conversion to be done + return this; + } + } + + throw new InvalidOperationException($"Unknown source format {Format}"); + } + + /// + /// Compresses the provided QOI data using BZ2, and using the shared , if not null. + /// + /// A new BZ2 + QOI image with the compressed data. + private static GMImage CompressQoiData(int width, int height, byte[] qoiData, MemoryStream sharedStream) + { + // Compress into new byte array + byte[] compressed; + if (sharedStream is not null) + { + // Use existing shared stream to compress the data + using var input = new MemoryStream(qoiData); + if (sharedStream.Length != 0) + { + // Ensure shared stream is at the beginning + sharedStream.Seek(0, SeekOrigin.Begin); + } + BZip2.Compress(input, sharedStream, false, 9); + compressed = sharedStream.GetBuffer().AsSpan()[..(int)sharedStream.Position].ToArray(); + } + else + { + // Use a new memory stream to compress the data + using var input = new MemoryStream(qoiData); + using var output = new MemoryStream(); + BZip2.Compress(input, output, false, 9); + compressed = output.GetBuffer().AsSpan()[..(int)output.Position].ToArray(); + } + + return new GMImage(ImageFormat.Bz2Qoi, width, height, compressed) + { + _bz2UncompressedSize = qoiData.Length + }; + } + + /// + /// Returns the same or a new ; the result of converting this image to format. + /// + /// Shared to be reused for BZ2 compression, if required. + public GMImage ConvertToBz2Qoi(MemoryStream sharedStream = null) + { + switch (Format) + { + case ImageFormat.RawBgra: + case ImageFormat.Png: + { + // Encode image as QOI, first + byte[] data = QoiConverter.GetArrayFromImage(this, false); + return CompressQoiData(Width, Height, data, sharedStream); + } + case ImageFormat.Qoi: + { + // Already have QOI data, so just compress it + return CompressQoiData(Width, Height, _data, sharedStream); + } + case ImageFormat.Bz2Qoi: + { + // Already in correct format; no conversion to be done + return this; + } + } + + throw new InvalidOperationException($"Unknown source format {Format}"); + } + + /// + /// Returns the raw BGRA32 pixel data of this image, which can be modified. + /// + /// + /// Only works if the image format is ; otherwise, you must first convert to that format using . + /// + /// Image format is not . + public Span GetRawImageData() + { + if (Format != ImageFormat.RawBgra) + { + throw new InvalidOperationException("Image is not in raw format"); + } + + return _data.AsSpan(); + } + + /// + /// Writes this image, in its current format (as seen on disk), to the current position of the specified . + /// + /// The gm2022_5 parameter is only relevant for images of BZ2 + QOI format. + /// instance to write to. + /// True if using GameMaker 2022.5 format or above; false otherwise. + public void WriteToBinaryWriter(BinaryWriter writer, bool gm2022_5) + { + switch (Format) + { + case ImageFormat.RawBgra: + case ImageFormat.Png: + case ImageFormat.Qoi: + // Data is stored identically to file format, so write it verbatim + writer.Write(_data); + break; + case ImageFormat.Bz2Qoi: + // Header is missing in this case, so we need to generate it first + writer.Write(MagicBz2Qoi); + writer.Write((short)Width); + writer.Write((short)Height); + if (gm2022_5) + { + if (_bz2UncompressedSize == -1) + { + throw new InvalidOperationException("BZ2 uncompressed data size was not set"); + } + writer.Write(_bz2UncompressedSize); + } + writer.Write(_data); + break; + default: + throw new InvalidOperationException($"Unknown format {Format}"); + } + } + + /// + /// Converts the image to its byte array/span representation (as seen on disk). + /// + /// True if using GameMaker 2022.5 format or above; false otherwise. + /// The gm2022_5 parameter is only relevant for images of BZ2 + QOI format. + public ReadOnlySpan ToSpan(bool gm2022_5 = false) + { + if (Format != ImageFormat.Bz2Qoi) + { + // All formats except BZ2 + QOI are stored verbatim, so just return them + return _data.AsSpan(); + } + + // We need to perform a full write with a BinaryWriter + using (MemoryStream ms = new(_data.Length + 16)) + { + using (BinaryWriter bw = new(ms)) + { + WriteToBinaryWriter(bw, gm2022_5); + } + + return ms.GetBuffer()[..(int)ms.Position].AsSpan(); + } + } + + /// + /// Returns a new with the contents of this image. + /// + public MagickImage GetMagickImage() + { + switch (Format) + { + case ImageFormat.Png: + { + // Parse the PNG data + MagickReadSettings settings = new() + { + ColorSpace = ColorSpace.sRGB, + Format = MagickFormat.Png + }; + MagickImage image = new(_data, settings); + image.Alpha(AlphaOption.Set); + image.Format = MagickFormat.Bgra; + image.SetCompression(CompressionMethod.NoCompression); + return image; + } + case ImageFormat.RawBgra: + { + // Parse the raw data + MagickReadSettings settings = new() + { + Width = Width, + Height = Height, + Format = MagickFormat.Bgra, + Compression = CompressionMethod.NoCompression + }; + MagickImage image = new(_data, settings); + image.Alpha(AlphaOption.Set); + return image; + } + case ImageFormat.Qoi: + case ImageFormat.Bz2Qoi: + // Convert to raw data, then parse that + return ConvertToRawBgra().GetMagickImage(); + } + + throw new InvalidOperationException($"Unknown format {Format}"); + } + + /// + /// Creates a new raw format with the contents of the provided . + /// + /// + /// This modifies the image format of the provided to avoid unnecessary copies. + /// + public static GMImage FromMagickImage(IMagickImage image) + { + image.Format = MagickFormat.Bgra; + image.SetCompression(CompressionMethod.NoCompression); + return new GMImage(ImageFormat.RawBgra, image.Width, image.Height, image.ToByteArray()); + } +} diff --git a/UndertaleModLib/Util/QoiConverter.cs b/UndertaleModLib/Util/QoiConverter.cs index 19ceecc78..bbf48d375 100644 --- a/UndertaleModLib/Util/QoiConverter.cs +++ b/UndertaleModLib/Util/QoiConverter.cs @@ -1,7 +1,4 @@ using System; -using System.Collections.Generic; -using System.Drawing; -using System.Drawing.Imaging; using System.IO; namespace UndertaleModLib.Util @@ -46,12 +43,12 @@ public static void InitSharedBuffer(int size) } /// - /// Creates a from a . + /// Creates a raw format from a . /// /// The stream to create the PNG image from. - /// The QOI image as a PNG. + /// The QOI image as a raw format image. /// If there is an invalid QOIF magic header or there was an error with stride width. - public static Bitmap GetImageFromStream(Stream s) + public static GMImage GetImageFromStream(Stream s) { Span header = stackalloc byte[12]; s.Read(header); @@ -63,19 +60,19 @@ public static Bitmap GetImageFromStream(Stream s) } /// - /// Creates a from a of s. + /// Creates a raw format from a of s. /// - /// The of s to create the PNG image from. - /// The QOI image as a PNG. + /// The of s to create the raw image from. + /// The QOI image as a raw format image. /// If there is an invalid QOIF magic header or there was an error with stride width. - public static Bitmap GetImageFromSpan(ReadOnlySpan bytes) => GetImageFromSpan(bytes, out _); + public static GMImage GetImageFromSpan(ReadOnlySpan bytes) => GetImageFromSpan(bytes, out _); /// /// /// The total amount of data read from the . /// /// - public unsafe static Bitmap GetImageFromSpan(ReadOnlySpan bytes, out int length) + public unsafe static GMImage GetImageFromSpan(ReadOnlySpan bytes, out int length) { ReadOnlySpan header = bytes[..12]; if (header[0] != (byte)'f' || header[1] != (byte)'i' || header[2] != (byte)'o' || header[3] != (byte)'q') @@ -87,245 +84,262 @@ public unsafe static Bitmap GetImageFromSpan(ReadOnlySpan bytes, out int l ReadOnlySpan pixelData = bytes.Slice(12, length); - Bitmap bmp = new Bitmap(width, height, PixelFormat.Format32bppArgb); - bmp.SetResolution(96.0f, 96.0f); - - BitmapData data = bmp.LockBits(new Rectangle(0, 0, bmp.Width, bmp.Height), ImageLockMode.ReadWrite, PixelFormat.Format32bppArgb); - if (data.Stride != width * 4) - throw new Exception("Need to reimplement QOI conversions to account for stride, apparently"); - - byte* bmpPtr = (byte*)data.Scan0; - byte* bmpEnd = bmpPtr + (4 * width * height); - - int pos = 0; - int run = 0; - byte r = 0, g = 0, b = 0, a = 255; - Span index = stackalloc byte[64 * 4]; - while (bmpPtr < bmpEnd) + GMImage img = new(width, height); + Span rawData = img.GetRawImageData(); + fixed (byte* imgData = rawData) { - if (run > 0) - { - run--; - } - else if (pos < pixelData.Length) + byte* bmpPtr = imgData; + byte* bmpEnd = bmpPtr + rawData.Length; + + int pos = 0; + int run = 0; + byte r = 0, g = 0, b = 0, a = 255; + Span index = stackalloc byte[64 * 4]; + while (bmpPtr < bmpEnd) { - int b1 = pixelData[pos++]; - - if ((b1 & QOI_MASK_2) == QOI_INDEX) - { - int indexPos = (b1 ^ QOI_INDEX) << 2; - r = index[indexPos]; - g = index[indexPos + 1]; - b = index[indexPos + 2]; - a = index[indexPos + 3]; - } - else if ((b1 & QOI_MASK_3) == QOI_RUN_8) - { - run = b1 & 0x1f; - } - else if ((b1 & QOI_MASK_3) == QOI_RUN_16) + if (run > 0) { - int b2 = pixelData[pos++]; - run = (((b1 & 0x1f) << 8) | b2) + 32; + run--; } - else if ((b1 & QOI_MASK_2) == QOI_DIFF_8) + else if (pos < pixelData.Length) { - r += (byte)(((b1 & 48) << 26 >> 30) & 0xff); - g += (byte)(((b1 & 12) << 28 >> 22 >> 8) & 0xff); - b += (byte)(((b1 & 3) << 30 >> 14 >> 16) & 0xff); - } - else if ((b1 & QOI_MASK_3) == QOI_DIFF_16) - { - int b2 = pixelData[pos++]; - int merged = b1 << 8 | b2; - r += (byte)(((merged & 7936) << 19 >> 27) & 0xff); - g += (byte)(((merged & 240) << 24 >> 20 >> 8) & 0xff); - b += (byte)(((merged & 15) << 28 >> 12 >> 16) & 0xff); - } - else if ((b1 & QOI_MASK_4) == QOI_DIFF_24) - { - int b2 = pixelData[pos++]; - int b3 = pixelData[pos++]; - int merged = b1 << 16 | b2 << 8 | b3; - r += (byte)(((merged & 1015808) << 12 >> 27) & 0xff); - g += (byte)(((merged & 31744) << 17 >> 19 >> 8) & 0xff); - b += (byte)(((merged & 992) << 22 >> 11 >> 16) & 0xff); - a += (byte)(((merged & 31) << 27 >> 3 >> 24) & 0xff); - } - else if ((b1 & QOI_MASK_4) == QOI_COLOR) - { - if ((b1 & 8) != 0) - r = pixelData[pos++]; - if ((b1 & 4) != 0) - g = pixelData[pos++]; - if ((b1 & 2) != 0) - b = pixelData[pos++]; - if ((b1 & 1) != 0) - a = pixelData[pos++]; + int b1 = pixelData[pos++]; + + if ((b1 & QOI_MASK_2) == QOI_INDEX) + { + int indexPos = (b1 ^ QOI_INDEX) << 2; + r = index[indexPos]; + g = index[indexPos + 1]; + b = index[indexPos + 2]; + a = index[indexPos + 3]; + } + else if ((b1 & QOI_MASK_3) == QOI_RUN_8) + { + run = b1 & 0x1f; + } + else if ((b1 & QOI_MASK_3) == QOI_RUN_16) + { + int b2 = pixelData[pos++]; + run = (((b1 & 0x1f) << 8) | b2) + 32; + } + else if ((b1 & QOI_MASK_2) == QOI_DIFF_8) + { + r += (byte)(((b1 & 48) << 26 >> 30) & 0xff); + g += (byte)(((b1 & 12) << 28 >> 22 >> 8) & 0xff); + b += (byte)(((b1 & 3) << 30 >> 14 >> 16) & 0xff); + } + else if ((b1 & QOI_MASK_3) == QOI_DIFF_16) + { + int b2 = pixelData[pos++]; + int merged = b1 << 8 | b2; + r += (byte)(((merged & 7936) << 19 >> 27) & 0xff); + g += (byte)(((merged & 240) << 24 >> 20 >> 8) & 0xff); + b += (byte)(((merged & 15) << 28 >> 12 >> 16) & 0xff); + } + else if ((b1 & QOI_MASK_4) == QOI_DIFF_24) + { + int b2 = pixelData[pos++]; + int b3 = pixelData[pos++]; + int merged = b1 << 16 | b2 << 8 | b3; + r += (byte)(((merged & 1015808) << 12 >> 27) & 0xff); + g += (byte)(((merged & 31744) << 17 >> 19 >> 8) & 0xff); + b += (byte)(((merged & 992) << 22 >> 11 >> 16) & 0xff); + a += (byte)(((merged & 31) << 27 >> 3 >> 24) & 0xff); + } + else if ((b1 & QOI_MASK_4) == QOI_COLOR) + { + if ((b1 & 8) != 0) + r = pixelData[pos++]; + if ((b1 & 4) != 0) + g = pixelData[pos++]; + if ((b1 & 2) != 0) + b = pixelData[pos++]; + if ((b1 & 1) != 0) + a = pixelData[pos++]; + } + + int indexPos2 = ((r ^ g ^ b ^ a) & 63) << 2; + index[indexPos2] = r; + index[indexPos2 + 1] = g; + index[indexPos2 + 2] = b; + index[indexPos2 + 3] = a; } - int indexPos2 = ((r ^ g ^ b ^ a) & 63) << 2; - index[indexPos2] = r; - index[indexPos2 + 1] = g; - index[indexPos2 + 2] = b; - index[indexPos2 + 3] = a; + *bmpPtr++ = b; + *bmpPtr++ = g; + *bmpPtr++ = r; + *bmpPtr++ = a; } - - *bmpPtr++ = b; - *bmpPtr++ = g; - *bmpPtr++ = r; - *bmpPtr++ = a; } - bmp.UnlockBits(data); - length += header.Length; - return bmp; + return img; } /// - /// Creates a QOI image as a byte array from a . + /// Creates a QOI image as a byte array from a . /// - /// The to create the QOI image from. - /// The amount of bytes of padding that should be used. + /// The to create the QOI image from. + /// True if the QOI shared buffer should be used; false if a newly-allocated buffer should be used. /// A QOI Image as a byte array. /// If there was an error with stride width. - public static byte[] GetArrayFromImage(Bitmap bmp, int padding = 4) => GetSpanFromImage(bmp, padding).ToArray(); + public static byte[] GetArrayFromImage(GMImage img, bool useSharedBuffer = true) => GetSpanFromImage(img, useSharedBuffer).ToArray(); /// - /// Creates a QOI image as a from a . + /// Creates a QOI image as a from a . /// - /// The to create the QOI image from. - /// The amount of bytes of padding that should be used. + /// The to create the QOI image from. + /// True if the QOI shared buffer should be used; false if a newly-allocated buffer should be used. /// A QOI Image as a byte array. /// If there was an error with stride width. - public static unsafe Span GetSpanFromImage(Bitmap bmp, int padding = 4) + public static unsafe Span GetSpanFromImage(GMImage img, bool useSharedBuffer = true) { - if (!isBufferEmpty) - Array.Clear(sharedBuffer); - - // Little-endian QOIF image magic - sharedBuffer[0] = (byte)'f'; - sharedBuffer[1] = (byte)'i'; - sharedBuffer[2] = (byte)'o'; - sharedBuffer[3] = (byte)'q'; - sharedBuffer[4] = (byte)(bmp.Width & 0xff); - sharedBuffer[5] = (byte)((bmp.Width >> 8) & 0xff); - sharedBuffer[6] = (byte)(bmp.Height & 0xff); - sharedBuffer[7] = (byte)((bmp.Height >> 8) & 0xff); - - BitmapData data = bmp.LockBits(new Rectangle(0, 0, bmp.Width, bmp.Height), ImageLockMode.ReadWrite, PixelFormat.Format32bppArgb); - if (data.Stride != bmp.Width * 4) - throw new Exception("Need to reimplement QOI conversions to account for stride, apparently"); + ArgumentNullException.ThrowIfNull(img); - byte* bmpPtr = (byte*)data.Scan0; - byte* bmpEnd = bmpPtr + (4 * bmp.Width * bmp.Height); - - int resPos = HeaderSize; - byte r = 0, g = 0, b = 0, a = 255; - int run = 0; - int v = 0, vPrev = 0xff; - Span index = stackalloc int[64]; - while (bmpPtr < bmpEnd) + // Prepare buffer + byte[] buffer; + int requiredSize = (img.Width * img.Height * MaxChunkSize) + HeaderSize; + if (useSharedBuffer) { - b = *bmpPtr; - g = *(bmpPtr + 1); - r = *(bmpPtr + 2); - a = *(bmpPtr + 3); - - v = (r << 24) | (g << 16) | (b << 8) | a; - if (v == vPrev) - run++; - if (run > 0 && (run == 0x2020 || v != vPrev || bmpPtr == bmpEnd - 4)) + // Use shared buffer (ensure it has enough space) + if (sharedBuffer is null || sharedBuffer.Length < requiredSize) { - if (run < 33) - { - run -= 1; - sharedBuffer[resPos++] = (byte)(QOI_RUN_8 | run); - } - else - { - run -= 33; - sharedBuffer[resPos++] = (byte)(QOI_RUN_16 | (run >> 8)); - sharedBuffer[resPos++] = (byte)run; - } - run = 0; + InitSharedBuffer(requiredSize); + } + if (!isBufferEmpty) + { + Array.Clear(sharedBuffer); } - if (v != vPrev) + buffer = sharedBuffer; + } + else + { + // Allocate a new buffer + buffer = new byte[requiredSize]; + } + + // Little-endian QOIF image magic + buffer[0] = (byte)'f'; + buffer[1] = (byte)'i'; + buffer[2] = (byte)'o'; + buffer[3] = (byte)'q'; + buffer[4] = (byte)(img.Width & 0xff); + buffer[5] = (byte)((img.Width >> 8) & 0xff); + buffer[6] = (byte)(img.Height & 0xff); + buffer[7] = (byte)((img.Height >> 8) & 0xff); + + // Get raw image data, and encode the compressed data as per custom GameMaker format + GMImage rawImage = img.ConvertToRawBgra(); + Span rawData = rawImage.GetRawImageData(); + int resPos = HeaderSize; + fixed (byte* bmpStart = rawData) + { + byte* bmpPtr = bmpStart; + byte* bmpEnd = bmpPtr + (4 * img.Width * img.Height); + + byte r = 0, g = 0, b = 0, a = 255; + int run = 0; + int v = 0, vPrev = 0xff; + Span index = stackalloc int[64]; + while (bmpPtr < bmpEnd) { - int indexPos = (r ^ g ^ b ^ a) & 63; - if (index[indexPos] == v) + b = *bmpPtr; + g = *(bmpPtr + 1); + r = *(bmpPtr + 2); + a = *(bmpPtr + 3); + + v = (r << 24) | (g << 16) | (b << 8) | a; + if (v == vPrev) + run++; + if (run > 0 && (run == 0x2020 || v != vPrev || bmpPtr == bmpEnd - 4)) { - sharedBuffer[resPos++] = (byte)(QOI_INDEX | indexPos); + if (run < 33) + { + run -= 1; + buffer[resPos++] = (byte)(QOI_RUN_8 | run); + } + else + { + run -= 33; + buffer[resPos++] = (byte)(QOI_RUN_16 | (run >> 8)); + buffer[resPos++] = (byte)run; + } + run = 0; } - else + if (v != vPrev) { - index[indexPos] = v; - - int vr = r - ((vPrev >> 24) & 0xff); - int vg = g - ((vPrev >> 16) & 0xff); - int vb = b - ((vPrev >> 8) & 0xff); - int va = a - (vPrev & 0xff); - if (vr > -17 && vr < 16 && - vg > -17 && vg < 16 && - vb > -17 && vb < 16 && - va > -17 && va < 16) + int indexPos = (r ^ g ^ b ^ a) & 63; + if (index[indexPos] == v) { - if (va == 0 && - vr > -3 && vr < 2 && - vg > -3 && vg < 2 && - vb > -3 && vb < 2) - { - sharedBuffer[resPos++] = (byte)(QOI_DIFF_8 | (vr << 4 & 48) | (vg << 2 & 12) | (vb & 3)); - } - else if (va == 0 && - vg > -9 && vg < 8 && - vb > -9 && vb < 8) + buffer[resPos++] = (byte)(QOI_INDEX | indexPos); + } + else + { + index[indexPos] = v; + + int vr = r - ((vPrev >> 24) & 0xff); + int vg = g - ((vPrev >> 16) & 0xff); + int vb = b - ((vPrev >> 8) & 0xff); + int va = a - (vPrev & 0xff); + if (vr > -17 && vr < 16 && + vg > -17 && vg < 16 && + vb > -17 && vb < 16 && + va > -17 && va < 16) { - sharedBuffer[resPos++] = (byte)(QOI_DIFF_16 | (vr & 31)); - sharedBuffer[resPos++] = (byte)((vg << 4 & 240) | (vb & 15)); + if (va == 0 && + vr > -3 && vr < 2 && + vg > -3 && vg < 2 && + vb > -3 && vb < 2) + { + buffer[resPos++] = (byte)(QOI_DIFF_8 | (vr << 4 & 48) | (vg << 2 & 12) | (vb & 3)); + } + else if (va == 0 && + vg > -9 && vg < 8 && + vb > -9 && vb < 8) + { + buffer[resPos++] = (byte)(QOI_DIFF_16 | (vr & 31)); + buffer[resPos++] = (byte)((vg << 4 & 240) | (vb & 15)); + } + else + { + buffer[resPos++] = (byte)(QOI_DIFF_24 | (vr >> 1 & 15)); + buffer[resPos++] = (byte)((vr << 7 & 128) | (vg << 2 & 124) | (vb >> 3 & 3)); + buffer[resPos++] = (byte)((vb << 5 & 224) | (va & 31)); + } } else { - sharedBuffer[resPos++] = (byte)(QOI_DIFF_24 | (vr >> 1 & 15)); - sharedBuffer[resPos++] = (byte)((vr << 7 & 128) | (vg << 2 & 124) | (vb >> 3 & 3)); - sharedBuffer[resPos++] = (byte)((vb << 5 & 224) | (va & 31)); + buffer[resPos++] = (byte)(QOI_COLOR | (vr != 0 ? 8 : 0) | (vg != 0 ? 4 : 0) | (vb != 0 ? 2 : 0) | (va != 0 ? 1 : 0)); + if (vr != 0) + buffer[resPos++] = r; + if (vg != 0) + buffer[resPos++] = g; + if (vb != 0) + buffer[resPos++] = b; + if (va != 0) + buffer[resPos++] = a; } } - else - { - sharedBuffer[resPos++] = (byte)(QOI_COLOR | (vr != 0 ? 8 : 0) | (vg != 0 ? 4 : 0) | (vb != 0 ? 2 : 0) | (va != 0 ? 1 : 0)); - if (vr != 0) - sharedBuffer[resPos++] = r; - if (vg != 0) - sharedBuffer[resPos++] = g; - if (vb != 0) - sharedBuffer[resPos++] = b; - if (va != 0) - sharedBuffer[resPos++] = a; - } } - } - vPrev = v; - bmpPtr += 4; + vPrev = v; + bmpPtr += 4; + } } - bmp.UnlockBits(data); - - // Add padding - resPos += padding; - // Write final length int length = resPos - HeaderSize; - sharedBuffer[8] = (byte)(length & 0xff); - sharedBuffer[9] = (byte)((length >> 8) & 0xff); - sharedBuffer[10] = (byte)((length >> 16) & 0xff); - sharedBuffer[11] = (byte)((length >> 24) & 0xff); + buffer[8] = (byte)(length & 0xff); + buffer[9] = (byte)((length >> 8) & 0xff); + buffer[10] = (byte)((length >> 16) & 0xff); + buffer[11] = (byte)((length >> 24) & 0xff); - isBufferEmpty = false; + if (useSharedBuffer) + { + isBufferEmpty = false; + } - return sharedBuffer.AsSpan()[..resPos]; + return buffer.AsSpan()[..resPos]; } } } \ No newline at end of file diff --git a/UndertaleModLib/Util/TextureWorker.cs b/UndertaleModLib/Util/TextureWorker.cs index aef3189bc..30c712c4e 100644 --- a/UndertaleModLib/Util/TextureWorker.cs +++ b/UndertaleModLib/Util/TextureWorker.cs @@ -1,217 +1,291 @@ -using System; +using ImageMagick; +using System; using System.Collections.Generic; -using System.Drawing; -using System.Drawing.Drawing2D; -using System.Drawing.Imaging; using System.IO; using UndertaleModLib.Models; namespace UndertaleModLib.Util { - public class TextureWorker + /// + /// Helper class used to manage and cache textures. + /// + public class TextureWorker : IDisposable { - private Dictionary embeddedDictionary = new Dictionary(); - private static readonly ImageConverter _imageConverter = new ImageConverter(); - - // Cleans up all the images when usage of this worker is finished. - // Should be called when a TextureWorker will never be used again. - public void Cleanup() - { - foreach (Bitmap img in embeddedDictionary.Values) - img.Dispose(); - embeddedDictionary.Clear(); - } - - public Bitmap GetEmbeddedTexture(UndertaleEmbeddedTexture embeddedTexture) + private Dictionary embeddedDictionary = new(); + private readonly object embeddedDictionaryLock = new(); + + /// + /// Retrieves an image representing the supplied texture page. + /// + /// + /// The returned image will be cached for this instance. + /// + /// Texture to get an image from. + /// with the contents of the given texture's image. + public MagickImage GetEmbeddedTexture(UndertaleEmbeddedTexture embeddedTexture) { - lock (embeddedDictionary) + lock (embeddedDictionaryLock) { - if (!embeddedDictionary.ContainsKey(embeddedTexture)) - embeddedDictionary[embeddedTexture] = GetImageFromByteArray(embeddedTexture.TextureData.TextureBlob); - return embeddedDictionary[embeddedTexture]; + // Try to find cached image + if (embeddedDictionary.TryGetValue(embeddedTexture, out MagickImage image)) + { + return image; + } + + // Otherwise, create new image + MagickImage newImage = embeddedTexture.TextureData.Image.GetMagickImage(); + embeddedDictionary[embeddedTexture] = newImage; + + return newImage; } } - public void ExportAsPNG(UndertaleTexturePageItem texPageItem, string FullPath, string imageName = null, bool includePadding = false) + /// + /// Exports the given texture page item to disk, as a PNG, to the supplied path. (With or without padding.) + /// + /// Texture page item to export. + /// File path to export to. + /// Image name to be used when throwing exceptions, or null to use the filename from the path. + /// True if padding should be exported; false otherwise. + public void ExportAsPNG(UndertaleTexturePageItem texPageItem, string filePath, string imageName = null, bool includePadding = false) { - SaveImageToFile(FullPath, GetTextureFor(texPageItem, imageName != null ? imageName : Path.GetFileNameWithoutExtension(FullPath), includePadding)); + using var image = GetTextureFor(texPageItem, imageName ?? Path.GetFileNameWithoutExtension(filePath), includePadding); + SaveImageToFile(image, filePath); } - public Bitmap GetTextureFor(UndertaleTexturePageItem texPageItem, string imageName, bool includePadding = false) + /// + /// Creates an image representing the sole texture page item supplied, with or without padding. + /// + /// Texture page item to get the image of. + /// Image name to be used when throwing exceptions. + /// True if padding should be used in the returned image; false otherwise. + /// An image with the contents of the given texture page item's portion of its texture page. + public IMagickImage GetTextureFor(UndertaleTexturePageItem texPageItem, string imageName, bool includePadding = false) { + // Get texture page that the item lives on + MagickImage embeddedImage = GetEmbeddedTexture(texPageItem.TexturePage); + + // Ensure texture is no larger than its bounding box int exportWidth = texPageItem.BoundingWidth; // sprite.Width int exportHeight = texPageItem.BoundingHeight; // sprite.Height - Bitmap embeddedImage = GetEmbeddedTexture(texPageItem.TexturePage); - - // Sanity checks. - if (includePadding && ((texPageItem.TargetWidth > exportWidth) || (texPageItem.TargetHeight > exportHeight))) - throw new InvalidDataException(imageName + "'s texture is larger than its bounding box!"); + if (includePadding && (texPageItem.TargetWidth > exportWidth || texPageItem.TargetHeight > exportHeight)) + { + throw new InvalidDataException($"{imageName}'s texture is larger than its bounding box!"); + } - // Create a bitmap representing that part of the texture page. - Bitmap resultImage = null; + // Create an image cropped from the item's part of the texture page + IMagickImage croppedImage = null; lock (embeddedImage) { - try - { - resultImage = embeddedImage.Clone(new Rectangle(texPageItem.SourceX, texPageItem.SourceY, texPageItem.SourceWidth, texPageItem.SourceHeight), PixelFormat.DontCare); - } - catch (OutOfMemoryException) - { - throw new OutOfMemoryException(imageName + "'s texture is abnormal. 'Source Position/Size' boxes 3 & 4 on texture page may be bigger than the sprite itself or it's set to '0'."); - } + croppedImage = embeddedImage.Clone(texPageItem.SourceX, texPageItem.SourceY, texPageItem.SourceWidth, texPageItem.SourceHeight); } - // Resize the image, if necessary. - if ((texPageItem.SourceWidth != texPageItem.TargetWidth) || (texPageItem.SourceHeight != texPageItem.TargetHeight)) - resultImage = ResizeImage(resultImage, texPageItem.TargetWidth, texPageItem.TargetHeight); + // Resize the image, if necessary + if (texPageItem.SourceWidth != texPageItem.TargetWidth || texPageItem.SourceHeight != texPageItem.TargetHeight) + { + IMagickImage original = croppedImage; + croppedImage = ResizeImage(croppedImage, texPageItem.TargetWidth, texPageItem.TargetHeight); + original.Dispose(); + } - // Put it in the final holder image. - Bitmap returnImage = resultImage; + // Put it in the final holder image, if necessary + IMagickImage returnImage = croppedImage; if (includePadding) { - returnImage = new Bitmap(exportWidth, exportHeight); - Graphics g = Graphics.FromImage(returnImage); - g.DrawImage(resultImage, new Rectangle(texPageItem.TargetX, texPageItem.TargetY, resultImage.Width, resultImage.Height), new Rectangle(0, 0, resultImage.Width, resultImage.Height), GraphicsUnit.Pixel); - g.Dispose(); + returnImage = new MagickImage(MagickColors.Transparent, exportWidth, exportHeight); + returnImage.Composite(croppedImage, texPageItem.TargetX, texPageItem.TargetY, CompositeOperator.Copy); + croppedImage.Dispose(); } return returnImage; } - public static Bitmap ReadImageFromFile(string filePath) + /// + /// Reads an image from the given file path (of arbitrary format, as supported by ). + /// + /// + /// Image color format will always be converted to BGRA, with no compression. + /// + /// File path to read the image from. + /// An image, in uncompressed BGRA format, containing the contents of the image file at the given path. + public static MagickImage ReadBGRAImageFromFile(string filePath) { - return GetImageFromByteArray(File.ReadAllBytes(filePath)); + MagickReadSettings settings = new() + { + ColorSpace = ColorSpace.sRGB, + }; + MagickImage image = new(filePath, settings); + image.Alpha(AlphaOption.Set); + image.Format = MagickFormat.Bgra; + image.SetCompression(CompressionMethod.NoCompression); + return image; } - // Grabbed from https://stackoverflow.com/questions/3801275/how-to-convert-image-to-byte-array/16576471#16576471 - public static Bitmap GetImageFromByteArray(byte[] byteArray) + /// + /// Performs a resize of the given image, if required, using the specified interpolation (bilinear by default). Always returns a new image. + /// + /// Image to be resized (without being modified). + /// Desired width to resize to. + /// Desired height to resize to. + /// Pixel interpolation method to use, or specify none to use bilinear interpolation. + /// A copy of the provided image, which is resized to the given dimensions when required. + public static IMagickImage ResizeImage(IMagickImage image, int width, int height, PixelInterpolateMethod interpolateMethod = PixelInterpolateMethod.Bilinear) { - Bitmap bm = (Bitmap)_imageConverter.ConvertFrom(byteArray); + // Clone image + IMagickImage newImage = image.Clone(); - if (bm != null && (bm.HorizontalResolution != (int)bm.HorizontalResolution || - bm.VerticalResolution != (int)bm.VerticalResolution)) + // If the image already has the correct dimensions, skip resizing + if (image.Width == width && image.Height == height) { - // Correct a strange glitch that has been observed in the test program when converting - // from a PNG file image created by CopyImageToByteArray() - the dpi value "drifts" - // slightly away from the nominal integer value - bm.SetResolution((int)(bm.HorizontalResolution + 0.5f), - (int)(bm.VerticalResolution + 0.5f)); + return newImage; } - return bm; + // Resize using bilinear interpolation + newImage.InterpolativeResize(width, height, interpolateMethod); + return newImage; } - // This should perform a high quality resize. - // Grabbed from https://stackoverflow.com/questions/1922040/how-to-resize-an-image-c-sharp - public static Bitmap ResizeImage(Image image, int width, int height) + /// + /// Reads collision mask data from the given file path, and required width/height. + /// + /// Image file to read the mask data from (usually a black-and-white PNG). + /// The width that the collision mask must be (e.g., sprite width or bbox width, depending on version). + /// The height that the collision mask must be (e.g., sprite height or bbox height, depending on version). + /// A byte array, encoding the collision mask as a 1-bit-per-pixel image. + /// If the loaded image dimensions do not match the required width/height + public static byte[] ReadMaskData(string filePath, int requiredWidth, int requiredHeight) { - var destRect = new Rectangle(0, 0, width, height); - var destImage = new Bitmap(width, height); - - destImage.SetResolution(image.HorizontalResolution, image.VerticalResolution); - - using (var graphics = Graphics.FromImage(destImage)) + List bytes; + using (MagickImage image = ReadBGRAImageFromFile(filePath)) { - graphics.CompositingMode = CompositingMode.SourceCopy; - graphics.CompositingQuality = CompositingQuality.HighQuality; - graphics.InterpolationMode = InterpolationMode.HighQualityBicubic; - graphics.SmoothingMode = SmoothingMode.HighQuality; - graphics.PixelOffsetMode = PixelOffsetMode.HighQuality; - - using (var wrapMode = new ImageAttributes()) + // Verify width/height match required width/height + if (image.Width != requiredWidth || image.Height != requiredHeight) { - wrapMode.SetWrapMode(WrapMode.TileFlipXY); - graphics.DrawImage(image, destRect, 0, 0, image.Width, image.Height, GraphicsUnit.Pixel, wrapMode); + throw new Exception($"{filePath} is not the proper size to be imported! The proper dimensions are width: {requiredWidth} px, height: {requiredHeight} px."); } - } - return destImage; - } + // Get image pixels, and allocate enough capacity for mask + IPixelCollection pixels = image.GetPixels(); + bytes = new((requiredWidth + 7) / 8 * requiredHeight); - public static byte[] ReadMaskData(string filePath) - { - Bitmap image = ReadImageFromFile(filePath); - List bytes = new List(); + // Get white color, used to represent bits that are set + IMagickColor white = MagickColors.White; - int enableColor = Color.White.ToArgb(); - for (int y = 0; y < image.Height; y++) - { - for (int xByte = 0; xByte < (image.Width + 7) / 8; xByte++) + // Read all pixels of image, and set a bit on the mask if a given pixel matches the white color + for (int y = 0; y < image.Height; y++) { - byte fullByte = 0x00; - int pxStart = (xByte * 8); - int pxEnd = Math.Min(pxStart + 8, (int) image.Width); - - for (int x = pxStart; x < pxEnd; x++) - if (image.GetPixel(x, y).ToArgb() == enableColor) // Don't use Color == OtherColor, it doesn't seem to give us the type of equals comparison we want here. - fullByte |= (byte)(0b1 << (7 - (x - pxStart))); - - bytes.Add(fullByte); + for (int xByte = 0; xByte < (image.Width + 7) / 8; xByte++) + { + byte fullByte = 0x00; + int pxStart = (xByte * 8); + int pxEnd = Math.Min(pxStart + 8, image.Width); + + for (int x = pxStart; x < pxEnd; x++) + { + if (pixels.GetPixel(x, y).ToColor().Equals(white)) + { + fullByte |= (byte)(0b1 << (7 - (x - pxStart))); + } + } + + bytes.Add(fullByte); + } } } - image.Dispose(); return bytes.ToArray(); } - public static byte[] ReadTextureBlob(string filePath) + /// + /// Generates and returns a black-and-white image representing a given sprite's collision mask, + /// and with the given width/height. + /// + /// Mask entry to generate the image from. + /// Width of the image to generate (and to interpret the collision mask with). + /// Height of the image to generate (and to interpret the collision mask with). + /// A new black-and-white image representing the specified collision mask. + public static IMagickImage GetCollisionMaskImage(UndertaleSprite.MaskEntry mask, int maskWidth, int maskHeight) { - Image.FromFile(filePath).Dispose(); // Make sure the file is valid image. - return File.ReadAllBytes(filePath); - } + // Create image to draw on + MagickImage image = new(MagickColor.FromRgba(0, 0, 0, 255), maskWidth, maskHeight); + IPixelCollection pixels = image.GetPixels(); - public static void SaveEmptyPNG(string FullPath, int width, int height) - { - var blackImage = new Bitmap(width, height); - for (int x = 0; x < width; x++) - for (int y = 0; y < height; y++) - blackImage.SetPixel(x, y, Color.Black); - SaveImageToFile(FullPath, blackImage); - } + // Get black/white colors to use for drawing + ReadOnlySpan black = MagickColors.Black.ToByteArray().AsSpan(); + ReadOnlySpan white = MagickColors.White.ToByteArray().AsSpan(); - public static Bitmap GetCollisionMaskImage(UndertaleSprite sprite, UndertaleSprite.MaskEntry mask) - { + // Draw white pixels if a given bit is set; black pixels otherwise byte[] maskData = mask.Data; - Bitmap bitmap = new Bitmap((int)sprite.Width, (int)sprite.Height, PixelFormat.Format32bppArgb); // Ugh. I want to use 1bpp, but for some BS reason C# doesn't allow SetPixel in that mode. - - for (int y = 0; y < sprite.Height; y++) + for (int y = 0; y < maskHeight; y++) { - int rowStart = y * (int)((sprite.Width + 7) / 8); - for (int x = 0; x < sprite.Width; x++) + int rowStart = y * ((maskWidth + 7) / 8); + for (int x = 0; x < maskWidth; x++) { byte temp = maskData[rowStart + (x / 8)]; bool pixelBit = (temp & (0b1 << (7 - (x % 8)))) != 0b0; - bitmap.SetPixel(x, y, pixelBit ? Color.White : Color.Black); + pixels.SetPixel(x, y, pixelBit ? white : black); } } - return bitmap; + return image; } - public static void ExportCollisionMaskPNG(UndertaleSprite sprite, UndertaleSprite.MaskEntry mask, string fullPath) + /// + /// Exports a collision mask entry from a given sprite's collision mask, as a PNG file, at the specified path, and with the given width/height. + /// + /// Mask entry to export the image from. + /// File path to export to. + /// Width of the image to export (and to interpret the collision mask with). + /// Height of the image to export (and to interpret the collision mask with). + public static void ExportCollisionMaskPNG(UndertaleSprite.MaskEntry mask, string filePath, int maskWidth, int maskHeight) { - SaveImageToFile(fullPath, GetCollisionMaskImage(sprite, mask)); + using var image = GetCollisionMaskImage(mask, maskWidth, maskHeight); + SaveImageToFile(image, filePath); } - public static byte[] GetImageBytes(Image image, bool disposeImage = true) + /// + /// Saves the provided image as a PNG file, at the specified path. + /// + /// Image to save. + /// File path to save the image to. + public static void SaveImageToFile(IMagickImage image, string filePath) { - using (var ms = new MemoryStream()) + using var stream = new FileStream(filePath, FileMode.Create); + image.Write(stream, MagickFormat.Png32); + } + + /// + /// Returns the width and height of the image stored at the given path, or -1 width/height if the file fails to parse as an image. + /// + /// File path to get the image size from. + /// Width and height of the image stored at the file path, or -1 for both values if invalid. + public static (int width, int height) GetImageSizeFromFile(string filePath) + { + try + { + MagickImageInfo info = new(filePath); + return (info.Width, info.Height); + } + catch (Exception) { - image.Save(ms, image.RawFormat); - byte[] result = ms.ToArray(); - if (disposeImage) - image.Dispose(); - return result; + return (-1, -1); } } - public static void SaveImageToFile(string FullPath, Image image, Boolean disposeImage = true) + /// + public void Dispose() { - var stream = new FileStream(FullPath, FileMode.Create); - image.Save(stream, ImageFormat.Png); - stream.Close(); - if (disposeImage) - image.Dispose(); + if (embeddedDictionary is not null) + { + foreach (MagickImage img in embeddedDictionary.Values) + { + img.Dispose(); + } + embeddedDictionary.Clear(); + embeddedDictionary = null; + } + + GC.SuppressFinalize(this); } } } diff --git a/UndertaleModLibTests/UndertaleModLibTests.csproj b/UndertaleModLibTests/UndertaleModLibTests.csproj index 505955bd2..40faddd00 100644 --- a/UndertaleModLibTests/UndertaleModLibTests.csproj +++ b/UndertaleModLibTests/UndertaleModLibTests.csproj @@ -1,7 +1,7 @@ - net6.0 + net8.0 enable enable @@ -9,8 +9,8 @@ - - + + runtime; build; native; contentfiles; analyzers; buildtransitive all diff --git a/UndertaleModTests/UndertaleModTests.csproj b/UndertaleModTests/UndertaleModTests.csproj index 9ed0d547f..3c3f886ac 100644 --- a/UndertaleModTests/UndertaleModTests.csproj +++ b/UndertaleModTests/UndertaleModTests.csproj @@ -1,6 +1,6 @@  - net6.0 + net8.0 Library false AnyCPU;x64 @@ -11,12 +11,12 @@ - 4.10.0 + 4.11.0 - 3.5.0 + 3.6.3 - - + + - \ No newline at end of file + diff --git a/UndertaleModTool/Controls/UndertaleObjectReference.xaml b/UndertaleModTool/Controls/UndertaleObjectReference.xaml index 43db49af2..1b5a72305 100644 --- a/UndertaleModTool/Controls/UndertaleObjectReference.xaml +++ b/UndertaleModTool/Controls/UndertaleObjectReference.xaml @@ -22,13 +22,15 @@