diff --git a/CHANGELOG.md b/CHANGELOG.md index 79755bf..626f7db 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,10 +7,38 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/). ## Unreleased ### Added +- `OpenWrite` and `OpenRead` static functions in `MetaStream`. +- Experimental `JsonMetaStreamReader` and `JsonMetaStreamWriter` classes. You can export serialize objects to JSON files in `MetaStream`. +- Support for encrypted meta stream files with `LegacyEncryptedStream` which matches with Telltale Tool in `MetaStream`. This requires a workspace. The user also has the option to encrypt/decrypt using the `LegacyEncryption` class provided with a Blowfish key and an archive version. +- An abstract `Close` method. +- Experimental support for compressed meta stream resources. +- `ContainerStreamParams` class. +- `StreamVersion` to `GameProfile`. +- A constructor with a boolean argument in `Blowfish`. +- `Workspace` parameter in deserialize methods in `Toolkit`. +- Helper methods for `VersionInfo`. ### Changed +- Rename the following MetaStream implementations: `MetaStreamReader`/`MetaStreamWriter` to `BinaryMetaStreamReader`/`BinaryMetaStreamWriter`. +- Rename `MetaStreamConfig` to `MetaStreamParams`. +- Update database files with the new changes. +- Exposed `GetClass(ulong, uint)` method from `MetaClassRegistry`. +- Refactor `MetaStream` internally. +- Refactor `MetaStreamParams` with new fields and methods to handle compression and encryption. +- Reorganize some namespaces. +- Make the default serializer not throw errors and use logger instead. +- `Serialize` from `MetaStream` now automatically preserializes. + +### Fixed +- A regression that did not allow files being copied to the output folder. +- Blowfish encryption not working correctly. ### Removed +- A csproj folder. +- Redundant `MetaStreamVersion` from `GameProfile`. +- `AreSymbolsHashed` from `GameProfile` as symbols are always hashed. +- `PreSerialize` from `MetaStream`. +- Redundant `Version` from `MetaStreamParams`. ## [0.2.0] - 2026-05-28 diff --git a/data/CHANGELOG.md b/data/CHANGELOG.md index a8b8494..c709344 100644 --- a/data/CHANGELOG.md +++ b/data/CHANGELOG.md @@ -1,12 +1,19 @@ ## Database Changelog +### 1st of June, 2026 + +#### Game Profiles +- Added `StreamVersion` field that matches Telltale Tool. Please refer to the [docs](README.md#game-profile-fields). +- Removed redundant `MetaStreamVersion` and `AreSymbolsHashed` fields. +- Updated database files with the new changes. + ### 28th of May, 2026 #### Game Profiles -- Added initial game profile for The Wolf Among Us (2013) (Thank you Pumba) +- Added initial game profile for The Wolf Among Us (2013)(Thank you Pumba!). #### Version Database -- Added initial version database for The Wolf Among Us (2013) +- Added initial version database for The Wolf Among Us (2013). ### 28th of April, 2026 @@ -23,15 +30,15 @@ ### 24th of April, 2026 #### Game Profiles -- Added the year to TWDS2 game profile json file name to match other entries (Thank you @gamma-02) +- Added the year to TWDS2 game profile json file name to match other entries (Thank you @gamma-02!). ### 17th of March, 2026 #### Game Profiles -- Added initial game profile for Poker Night at the Inventory Remastered (2026) (Thank you @gamma-02) +- Added initial game profile for Poker Night at the Inventory Remastered (2026) (Thank you @gamma-02!). #### Version Database -- Added initial version database for aforementoned Poker Night at the Inventory Remastered (2026) +- Added initial version database for aforementoned Poker Night at the Inventory Remastered (2026). ### 18th of February, 2026 diff --git a/data/README.md b/data/README.md index 2defa22..f1a746e 100644 --- a/data/README.md +++ b/data/README.md @@ -58,17 +58,16 @@ For consistency, all database file names are in slug form with the appropriate e ## Game Profile Fields -| Field | Type | Description | Example | -| ------------------------ | -------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------- | -| `Name` | `string` | Display name of the game. | `"The Walking Dead: Season 2"` | -| `Description` | `string` | A brief description of the game. | `"The Walking Dead: Season 2 (2013) for PC."` | -| `BlowfishKey` | `string` (see below) | Blowfish encryption key for encrypting/decrypting files. Can be a custom key in a string form or an enum name from the [`T3BlowfishKey`](/src/TelltaleToolKit/Utility/T3BlowfishKey.cs) class. | `"Twds2"` | -| `IsTtarch2` | `bool` | Whether the game uses the TTArch2 archive format (`true` for TTArch2, `false` for TTArch). | `true` | -| `TtarchVersion` | `int` | The version of the archive format used by the game. If `IsTtarch2` is true, use versions 2-4, otherwise - versions 0-9. | `3` | -| `LuaVersion` | `string` | Lua scripting engine version used by the game. It can be either `5.0.2`, `5.1.2` or `5.2.3`. | `"5.1.2"` or `"5.1"` | -| `MetaStreamVersion` | `string` | Version of the metastream format used in the game. Can be `MBIN`, `MTRE`, `MSV5` or `MSV6`. | `"MSV5"` | -| `AreSymbolsHashed` | `bool` | Whether the game's symbols are hashed (`true` or `false`). This should almost always be `true`. Only `MBIN` are allowed to not be hashed. | `true` | -| `EnableOodleCompression` | `bool` | Whether Oodle compression is enabled for game archives. | `false` | +| Field | Type | Description | Example | +| ------------------------ | -------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------- | +| `Name` | `string` | Display name of the game. | `"The Walking Dead: Season 2"` | +| `Description` | `string` | A brief description of the game. | `"The Walking Dead: Season 2 (2013) for PC."` | +| `BlowfishKey` | `string` (see below) | Blowfish encryption key for encrypting/decrypting files. Can be a custom key in a string form or an enum name from the [`T3BlowfishKey`](../src/TelltaleToolKit/Utility/Blowfish/T3BlowfishKey.cs) class. | `"Twds2"` | +| `IsTtarch2` | `bool` | Whether the game uses the TTArch2 archive format (`true` for TTArch2, `false` for TTArch). | `true` | +| `TtarchVersion` | `int` | The version of the archive format used by the game. If `IsTtarch2` is true, use versions 2-4, otherwise - versions 0-9. | `3` | +| `LuaVersion` | `string` | Lua scripting engine version used by the game. It can be either `5.0.2`, `5.1.2` or `5.2.3`. | `"5.1.2"` or `"5.1"` | +| `streamVersion` | `uint` | Version of the metastream format used in the game. This value ranges from 1 to 6. You can determine the version by inspecting the magic (FourCC) of a metastream file and using [this](../src/TelltaleToolKit/Serialization/MetaStreamMagic.cs) as a reference. | `5` | +| `EnableOodleCompression` | `bool` | Whether Oodle compression is enabled for game archives. | `false` | ### Game Profile Full Example @@ -80,8 +79,7 @@ For consistency, all database file names are in slug form with the appropriate e "IsTtarch2": true, "TtarchVersion": 3, "LuaVersion": "5.1.2", - "MetaStreamVersion": "MSV5", - "AreSymbolsHashed": true, + "streamVersion": 5, "EnableOodleCompression": false, } ``` diff --git a/data/game_profiles/batman-the-enemy-within-2017.json b/data/game_profiles/batman-the-enemy-within-2017.json index 7297049..5ce1391 100644 --- a/data/game_profiles/batman-the-enemy-within-2017.json +++ b/data/game_profiles/batman-the-enemy-within-2017.json @@ -5,6 +5,6 @@ "isTtarch2": true, "ttarchVersion": 4, "luaVersion": "5.2.3", - "metaStreamVersion": "MSV6", + "streamVersion": 6, "enableOodleCompression": true } \ No newline at end of file diff --git a/data/game_profiles/batman-the-telltale-series-2016-12.json b/data/game_profiles/batman-the-telltale-series-2016-12.json index 0ffb5b5..b84da84 100644 --- a/data/game_profiles/batman-the-telltale-series-2016-12.json +++ b/data/game_profiles/batman-the-telltale-series-2016-12.json @@ -5,6 +5,6 @@ "isTtarch2": true, "ttarchVersion": 4, "luaVersion": "5.2.3", - "metaStreamVersion": "MSV6", + "streamVersion": 6, "enableOodleCompression": true } \ No newline at end of file diff --git a/data/game_profiles/bone-act-1-2-combo-pack-2007.json b/data/game_profiles/bone-act-1-2-combo-pack-2007.json index e942b7e..e60b63d 100644 --- a/data/game_profiles/bone-act-1-2-combo-pack-2007.json +++ b/data/game_profiles/bone-act-1-2-combo-pack-2007.json @@ -5,6 +5,6 @@ "isTtarch2": false, "ttarchVersion": 4, "luaVersion": "5.0", - "metaStreamVersion": "MBIN", + "streamVersion": 1, "enableOodleCompression": false } \ No newline at end of file diff --git a/data/game_profiles/bone-out-from-boneville-2005.json b/data/game_profiles/bone-out-from-boneville-2005.json index a6165cb..6cd0fd0 100644 --- a/data/game_profiles/bone-out-from-boneville-2005.json +++ b/data/game_profiles/bone-out-from-boneville-2005.json @@ -5,6 +5,6 @@ "isTtarch2": false, "ttarchVersion": 4, "luaVersion": "5.0", - "metaStreamVersion": "MBIN", + "streamVersion": 1, "enableOodleCompression": false } \ No newline at end of file diff --git a/data/game_profiles/bone-out-from-boneville-2007.json b/data/game_profiles/bone-out-from-boneville-2007.json index dd27c23..f911f2b 100644 --- a/data/game_profiles/bone-out-from-boneville-2007.json +++ b/data/game_profiles/bone-out-from-boneville-2007.json @@ -5,6 +5,6 @@ "isTtarch2": false, "ttarchVersion": 4, "luaVersion": "5.0", - "metaStreamVersion": "MBIN", + "streamVersion": 1, "enableOodleCompression": false } \ No newline at end of file diff --git a/data/game_profiles/bone-the-great-cowrace-2006.json b/data/game_profiles/bone-the-great-cowrace-2006.json index 92773ef..25c05e6 100644 --- a/data/game_profiles/bone-the-great-cowrace-2006.json +++ b/data/game_profiles/bone-the-great-cowrace-2006.json @@ -5,6 +5,6 @@ "isTtarch2": false, "ttarchVersion": 4, "luaVersion": "5.0", - "metaStreamVersion": "MBIN", + "streamVersion": 1, "enableOodleCompression": false } \ No newline at end of file diff --git a/data/game_profiles/borderlands-2015.json b/data/game_profiles/borderlands-2015.json index d490ad4..28b8fd4 100644 --- a/data/game_profiles/borderlands-2015.json +++ b/data/game_profiles/borderlands-2015.json @@ -5,6 +5,6 @@ "isTtarch2": true, "ttarchVersion": 4, "luaVersion": "5.1", - "metaStreamVersion": "MSV5", + "streamVersion": 5, "enableOodleCompression": false } \ No newline at end of file diff --git a/data/game_profiles/csi-3-dimensions-of-murder-2006-demo.json b/data/game_profiles/csi-3-dimensions-of-murder-2006-demo.json index 59b5de6..6fa16ed 100644 --- a/data/game_profiles/csi-3-dimensions-of-murder-2006-demo.json +++ b/data/game_profiles/csi-3-dimensions-of-murder-2006-demo.json @@ -5,6 +5,6 @@ "isTtarch2": false, "ttarchVersion": 4, "luaVersion": "5.0", - "metaStreamVersion": "MBIN", + "streamVersion": 1, "enableOodleCompression": false } \ No newline at end of file diff --git a/data/game_profiles/csi-hard-evidence-2007.json b/data/game_profiles/csi-hard-evidence-2007.json index b2c171d..c1d2a22 100644 --- a/data/game_profiles/csi-hard-evidence-2007.json +++ b/data/game_profiles/csi-hard-evidence-2007.json @@ -5,6 +5,6 @@ "isTtarch2": false, "ttarchVersion": 3, "luaVersion": "5.0", - "metaStreamVersion": "MBIN", + "streamVersion": 1, "enableOodleCompression": false } \ No newline at end of file diff --git a/data/game_profiles/game-of-thrones-2015.json b/data/game_profiles/game-of-thrones-2015.json index ff13697..bf111cf 100644 --- a/data/game_profiles/game-of-thrones-2015.json +++ b/data/game_profiles/game-of-thrones-2015.json @@ -5,6 +5,6 @@ "isTtarch2": true, "ttarchVersion": 3, "luaVersion": "5.1.2", - "metaStreamVersion": "MSV5", + "streamVersion": 5, "enableOodleCompression": false } \ No newline at end of file diff --git a/data/game_profiles/guardians-of-the-galaxy-2017.json b/data/game_profiles/guardians-of-the-galaxy-2017.json index 040151d..12cd486 100644 --- a/data/game_profiles/guardians-of-the-galaxy-2017.json +++ b/data/game_profiles/guardians-of-the-galaxy-2017.json @@ -5,6 +5,6 @@ "isTtarch2": true, "ttarchVersion": 4, "luaVersion": "5.2.3", - "metaStreamVersion": "MSV6", + "streamVersion": 6, "enableOodleCompression": true } \ No newline at end of file diff --git a/data/game_profiles/minecraft-story-mode-2015-old.json b/data/game_profiles/minecraft-story-mode-2015-old.json index 25ff103..1e456aa 100644 --- a/data/game_profiles/minecraft-story-mode-2015-old.json +++ b/data/game_profiles/minecraft-story-mode-2015-old.json @@ -5,6 +5,6 @@ "isTtarch2": true, "ttarchVersion": 3, "luaVersion": "5.1.2", - "metaStreamVersion": "MSV5", + "streamVersion": 5, "enableOodleCompression": false } \ No newline at end of file diff --git a/data/game_profiles/minecraft-story-mode-2015.json b/data/game_profiles/minecraft-story-mode-2015.json index 25ff103..1e456aa 100644 --- a/data/game_profiles/minecraft-story-mode-2015.json +++ b/data/game_profiles/minecraft-story-mode-2015.json @@ -5,6 +5,6 @@ "isTtarch2": true, "ttarchVersion": 3, "luaVersion": "5.1.2", - "metaStreamVersion": "MSV5", + "streamVersion": 5, "enableOodleCompression": false } \ No newline at end of file diff --git a/data/game_profiles/minecraft-story-mode-season-2-2017.json b/data/game_profiles/minecraft-story-mode-season-2-2017.json index 9bd9489..6203f12 100644 --- a/data/game_profiles/minecraft-story-mode-season-2-2017.json +++ b/data/game_profiles/minecraft-story-mode-season-2-2017.json @@ -5,6 +5,6 @@ "isTtarch2": false, "ttarchVersion": 4, "luaVersion": "5.2.3", - "metaStreamVersion": "MSV6", + "streamVersion": 6, "enableOodleCompression": true } \ No newline at end of file diff --git a/data/game_profiles/poker-night-2-2013.json b/data/game_profiles/poker-night-2-2013.json index a67afa8..3d308b2 100644 --- a/data/game_profiles/poker-night-2-2013.json +++ b/data/game_profiles/poker-night-2-2013.json @@ -5,6 +5,6 @@ "isTtarch2": false, "ttarchVersion": 9, "luaVersion": "5.2.3", - "metaStreamVersion": "MSV6", + "streamVersion": 6, "enableOodleCompression": true } \ No newline at end of file diff --git a/data/game_profiles/poker-night-at-the-inventory-remastered-2026.json b/data/game_profiles/poker-night-at-the-inventory-remastered-2026.json index ce78c92..fa35e04 100644 --- a/data/game_profiles/poker-night-at-the-inventory-remastered-2026.json +++ b/data/game_profiles/poker-night-at-the-inventory-remastered-2026.json @@ -5,6 +5,6 @@ "isTtarch2": true, "ttarchVersion": 4, "luaVersion": "5.2.3", - "metaStreamVersion": "MSV6", + "streamVersion": 6, "enableOodleCompression": true } \ No newline at end of file diff --git a/data/game_profiles/sam-and-max-101-2006-demo.json b/data/game_profiles/sam-and-max-101-2006-demo.json index 8449d4c..2a216b7 100644 --- a/data/game_profiles/sam-and-max-101-2006-demo.json +++ b/data/game_profiles/sam-and-max-101-2006-demo.json @@ -5,6 +5,6 @@ "isTtarch2": false, "ttarchVersion": 3, "luaVersion": "5.0", - "metaStreamVersion": "MBIN", + "streamVersion": 1, "enableOodleCompression": false } \ No newline at end of file diff --git a/data/game_profiles/sam-and-max-102-2006-demo.json b/data/game_profiles/sam-and-max-102-2006-demo.json index 05dedd5..191befa 100644 --- a/data/game_profiles/sam-and-max-102-2006-demo.json +++ b/data/game_profiles/sam-and-max-102-2006-demo.json @@ -5,6 +5,6 @@ "isTtarch2": false, "ttarchVersion": 3, "luaVersion": "5.0", - "metaStreamVersion": "MBIN", + "streamVersion": 1, "enableOodleCompression": false } \ No newline at end of file diff --git a/data/game_profiles/sam-and-max-103-2006-demo.json b/data/game_profiles/sam-and-max-103-2006-demo.json index ffa7db1..e1d373e 100644 --- a/data/game_profiles/sam-and-max-103-2006-demo.json +++ b/data/game_profiles/sam-and-max-103-2006-demo.json @@ -5,6 +5,6 @@ "isTtarch2": false, "ttarchVersion": 3, "luaVersion": "5.0", - "metaStreamVersion": "MBIN", + "streamVersion": 1, "enableOodleCompression": false } \ No newline at end of file diff --git a/data/game_profiles/sam-and-max-104-2006-demo.json b/data/game_profiles/sam-and-max-104-2006-demo.json index 0f8ffed..5243ccc 100644 --- a/data/game_profiles/sam-and-max-104-2006-demo.json +++ b/data/game_profiles/sam-and-max-104-2006-demo.json @@ -5,6 +5,6 @@ "isTtarch2": true, "ttarchVersion": 3, "luaVersion": "5.0", - "metaStreamVersion": "MBIN", + "streamVersion": 1, "enableOodleCompression": false } \ No newline at end of file diff --git a/data/game_profiles/sam-and-max-105-2006-demo.json b/data/game_profiles/sam-and-max-105-2006-demo.json index 10c03c5..8256d63 100644 --- a/data/game_profiles/sam-and-max-105-2006-demo.json +++ b/data/game_profiles/sam-and-max-105-2006-demo.json @@ -5,6 +5,6 @@ "isTtarch2": true, "ttarchVersion": 3, "luaVersion": "5.0", - "metaStreamVersion": "MBIN", + "streamVersion": 1, "enableOodleCompression": false } \ No newline at end of file diff --git a/data/game_profiles/sam-and-max-106-2006-demo.json b/data/game_profiles/sam-and-max-106-2006-demo.json index 01c8cc4..b0aaa6c 100644 --- a/data/game_profiles/sam-and-max-106-2006-demo.json +++ b/data/game_profiles/sam-and-max-106-2006-demo.json @@ -5,6 +5,6 @@ "isTtarch2": true, "ttarchVersion": 3, "luaVersion": "5.0", - "metaStreamVersion": "MBIN", + "streamVersion": 1, "enableOodleCompression": false } \ No newline at end of file diff --git a/data/game_profiles/sam-and-max-201-2007.json b/data/game_profiles/sam-and-max-201-2007.json index a8183ea..0cea5c2 100644 --- a/data/game_profiles/sam-and-max-201-2007.json +++ b/data/game_profiles/sam-and-max-201-2007.json @@ -5,6 +5,6 @@ "isTtarch2": false, "ttarchVersion": 2, "luaVersion": "5.0", - "metaStreamVersion": "MBIN", + "streamVersion": 1, "enableOodleCompression": false } \ No newline at end of file diff --git a/data/game_profiles/sam-and-max-203-2008.json b/data/game_profiles/sam-and-max-203-2008.json index 5917173..ab203a1 100644 --- a/data/game_profiles/sam-and-max-203-2008.json +++ b/data/game_profiles/sam-and-max-203-2008.json @@ -5,6 +5,6 @@ "isTtarch2": false, "ttarchVersion": 0, "luaVersion": "5.0.2", - "metaStreamVersion": "MBIN", + "streamVersion": 1, "enableOodleCompression": false } \ No newline at end of file diff --git a/data/game_profiles/sam-and-max-season-1-remastered-2019.json b/data/game_profiles/sam-and-max-season-1-remastered-2019.json index 096409d..697fe0a 100644 --- a/data/game_profiles/sam-and-max-season-1-remastered-2019.json +++ b/data/game_profiles/sam-and-max-season-1-remastered-2019.json @@ -5,6 +5,6 @@ "isTtarch2": true, "ttarchVersion": 4, "luaVersion": "5.2.3", - "metaStreamVersion": "MSV6", + "streamVersion": 6, "enableOodleCompression": true } \ No newline at end of file diff --git a/data/game_profiles/tales-from-the-borderlands-2021-pc.json b/data/game_profiles/tales-from-the-borderlands-2021-pc.json index b593d41..4812231 100644 --- a/data/game_profiles/tales-from-the-borderlands-2021-pc.json +++ b/data/game_profiles/tales-from-the-borderlands-2021-pc.json @@ -5,6 +5,6 @@ "isTtarch2": true, "ttarchVersion": 4, "luaVersion": "5.2.3", - "metaStreamVersion": "MSV6", + "streamVersion": 6, "enableOodleCompression": true } \ No newline at end of file diff --git a/data/game_profiles/tales-from-the-borderlands-2021-switch.json b/data/game_profiles/tales-from-the-borderlands-2021-switch.json index d2b67be..2716fed 100644 --- a/data/game_profiles/tales-from-the-borderlands-2021-switch.json +++ b/data/game_profiles/tales-from-the-borderlands-2021-switch.json @@ -5,6 +5,6 @@ "isTtarch2": true, "ttarchVersion": 4, "luaVersion": "5.2.3", - "metaStreamVersion": "MSV6", + "streamVersion": 6, "enableOodleCompression": true } \ No newline at end of file diff --git a/data/game_profiles/telltale-texas-hold-em-2005.json b/data/game_profiles/telltale-texas-hold-em-2005.json index 664abee..04aa468 100644 --- a/data/game_profiles/telltale-texas-hold-em-2005.json +++ b/data/game_profiles/telltale-texas-hold-em-2005.json @@ -5,6 +5,6 @@ "isTtarch2": false, "ttarchVersion": 4, "luaVersion": "5.2.3", - "metaStreamVersion": "MSV6", + "streamVersion": 6, "enableOodleCompression": false } \ No newline at end of file diff --git a/data/game_profiles/the-walking-dead-collection-2017.json b/data/game_profiles/the-walking-dead-collection-2017.json index 84fe4c9..f890f10 100644 --- a/data/game_profiles/the-walking-dead-collection-2017.json +++ b/data/game_profiles/the-walking-dead-collection-2017.json @@ -5,6 +5,6 @@ "isTtarch2": true, "ttarchVersion": 4, "luaVersion": "5.2.3", - "metaStreamVersion": "MSV6", + "streamVersion": 6, "enableOodleCompression": true } \ No newline at end of file diff --git a/data/game_profiles/the-walking-dead-definitive-series-2019.json b/data/game_profiles/the-walking-dead-definitive-series-2019.json index 9ca8e43..1f06218 100644 --- a/data/game_profiles/the-walking-dead-definitive-series-2019.json +++ b/data/game_profiles/the-walking-dead-definitive-series-2019.json @@ -5,7 +5,7 @@ "isTtarch2": true, "ttarchVersion": 4, "luaVersion": "5.2.3", - "metaStreamVersion": "MSV6", + "streamVersion": 6, "enableOodleCompression": true, "areSymbolsHashed": true } \ No newline at end of file diff --git a/data/game_profiles/the-walking-dead-michonne-2016.json b/data/game_profiles/the-walking-dead-michonne-2016.json index 90237df..508ecee 100644 --- a/data/game_profiles/the-walking-dead-michonne-2016.json +++ b/data/game_profiles/the-walking-dead-michonne-2016.json @@ -5,6 +5,6 @@ "isTtarch2": true, "ttarchVersion": 4, "luaVersion": "5.2.3", - "metaStreamVersion": "MSV5", + "streamVersion": 5, "enableOodleCompression": true } \ No newline at end of file diff --git a/data/game_profiles/the-walking-dead-season-2-2013.json b/data/game_profiles/the-walking-dead-season-2-2013.json index 9b926d2..81a8f0e 100644 --- a/data/game_profiles/the-walking-dead-season-2-2013.json +++ b/data/game_profiles/the-walking-dead-season-2-2013.json @@ -5,6 +5,6 @@ "isTtarch2": true, "ttarchVersion": 3, "luaVersion": "5.1.2", - "metaStreamVersion": "MSV5", + "streamVersion": 5, "enableOodleCompression": false } \ No newline at end of file diff --git a/data/game_profiles/the-walking-dead-season-3-2016.json b/data/game_profiles/the-walking-dead-season-3-2016.json index 9530fc4..a1d95e7 100644 --- a/data/game_profiles/the-walking-dead-season-3-2016.json +++ b/data/game_profiles/the-walking-dead-season-3-2016.json @@ -5,6 +5,6 @@ "isTtarch2": true, "ttarchVersion": 4, "luaVersion": "5.2.3", - "metaStreamVersion": "MSV6", + "streamVersion": 6, "enableOodleCompression": true } \ No newline at end of file diff --git a/data/game_profiles/the-walking-dead-season-4-2018.json b/data/game_profiles/the-walking-dead-season-4-2018.json index 0b1f050..63cf355 100644 --- a/data/game_profiles/the-walking-dead-season-4-2018.json +++ b/data/game_profiles/the-walking-dead-season-4-2018.json @@ -5,6 +5,6 @@ "isTtarch2": true, "ttarchVersion": 4, "luaVersion": "5.2.3", - "metaStreamVersion": "MSV6", + "streamVersion": 6, "enableOodleCompression": true } \ No newline at end of file diff --git a/data/game_profiles/the-walking-dead.json b/data/game_profiles/the-walking-dead.json index d3d614c..c72c5c6 100644 --- a/data/game_profiles/the-walking-dead.json +++ b/data/game_profiles/the-walking-dead.json @@ -5,6 +5,6 @@ "isTtarch2": false, "ttarchVersion": 9, "luaVersion": "5.1.2", - "metaStreamVersion": "MTRE", + "streamVersion": 3, "enableOodleCompression": false } \ No newline at end of file diff --git a/data/game_profiles/the-wolf-among-us-2013.json b/data/game_profiles/the-wolf-among-us-2013.json index a79161c..abc387c 100644 --- a/data/game_profiles/the-wolf-among-us-2013.json +++ b/data/game_profiles/the-wolf-among-us-2013.json @@ -1 +1 @@ -{"name":"the-wolf-among-us-2013","description":"","blowfishKey":"Fables","isTtarch2":true,"ttarchVersion":3,"luaVersion":"5.1.2","metaStreamVersion":"MSV5","enableOodleCompression":false} \ No newline at end of file +{"name":"the-wolf-among-us-2013","description":"","blowfishKey":"Fables","isTtarch2":true,"ttarchVersion":3,"luaVersion":"5.1.2","streamVersion":5,"enableOodleCompression":false} \ No newline at end of file diff --git a/samples/VersionDatabaseCreator/Program.cs b/samples/VersionDatabaseCreator/Program.cs index 1392dab..22de66b 100644 --- a/samples/VersionDatabaseCreator/Program.cs +++ b/samples/VersionDatabaseCreator/Program.cs @@ -6,9 +6,11 @@ using TelltaleToolKit; using TelltaleToolKit.GamesDatabase; using TelltaleToolKit.Reflection; +using TelltaleToolKit.Serialization; using TelltaleToolKit.Serialization.Binary; using TelltaleToolKit.TelltaleArchives; using TelltaleToolKit.Utility.Blowfish; +using TelltaleToolKit.Utility.Lua; // !!!IMPORTANT!!! // This project creates ONLY the VERSION DATABASE for a specific game. @@ -52,8 +54,7 @@ // The reason being is that when extracting a single entry directly from a ttarch, that entry is directly loaded in memory. // And sometimes...there are huge assets. // It takes a couple of minutes to scan TWD:DE with a stock Ryzen 5 2600. -var areSymbolsHashed = false; -var msv = MetaStreamVersion.Mbin; +uint msv = 2; TTArchiveVersion ttarchVersion = 0; await Parallel.ForEachAsync(archivePaths, async (filePath, _) => { @@ -70,10 +71,9 @@ await Parallel.ForEachAsync(archivePaths, async (filePath, _) => if (Toolkit.IsMetaFile(archive.GetAllEntries().First().Name)) { - MetaStreamConfiguration config = new MetaStreamReader(firstFile).Configuration; + MetaStreamParams config = new BinaryMetaStreamReader(firstFile, null).Params; - areSymbolsHashed = config.AreSymbolsHashed; - msv = config.Version; + msv = config.StreamVersion; } } @@ -86,27 +86,30 @@ await Parallel.ForEachAsync(archivePaths, async (filePath, _) => if (!Toolkit.IsMetaFile(file)) continue; - MetaStreamConfiguration config = new MetaStreamReader(file).Configuration; + MetaStreamParams config = new BinaryMetaStreamReader(file).Params; - foreach (MetaClass desc in config.SerializedClasses) + foreach (MetaClass desc in config.GetRegisteredClasses()) serializedMetaClasses.TryAdd(desc, 0); - foreach ((ulong, uint) type in config.UnregisteredTypes) + var localUnregisteredTypes = config.GetUnregisteredTypes(); + var localUnregisteredClasses = config.GetUnregisteredClasses(); + + foreach ((ulong, uint) type in localUnregisteredTypes) unregisteredTypes.TryAdd(type, 0); - foreach ((MetaClassType, uint crc32) urDesc in config.UnregisteredClasses) + foreach ((MetaClassType, uint crc32) urDesc in localUnregisteredClasses) unrecognizedMetaClassDescriptions.TryAdd(urDesc, 0); - if (config.UnregisteredTypes.Count > 0) + if (localUnregisteredTypes.Count > 0) { Console.Error.WriteLine( - $"File {entry.Name} has {config.UnregisteredTypes.Count} unregistered types in {Path.GetFileName(filePath)}."); + $"File {entry.Name} has {localUnregisteredTypes.Count} unregistered types in {Path.GetFileName(filePath)}."); } - if (config.UnregisteredClasses.Count > 0) + if (localUnregisteredClasses.Count > 0) { Console.Error.WriteLine( - $"File {entry.Name} has {config.UnregisteredClasses.Count} unregistered classes in {Path.GetFileName(filePath)}."); + $"File {entry.Name} has {localUnregisteredClasses.Count} unregistered classes in {Path.GetFileName(filePath)}."); } } } @@ -139,7 +142,7 @@ await Parallel.ForEachAsync(archivePaths, async (filePath, _) => // continue; // } // -// MetaStreamConfiguration config = TTK.ExtractMetaStreamConfiguration(archive.ExtractFile(entry.Name)); +// MetaStreamParams config = TTK.ExtractMetaStreamConfiguration(archive.ExtractFile(entry.Name)); // // // Use this if you want to debug which file has an unknown type. // if (config.UnregisteredTypes.Count > 0) @@ -230,12 +233,11 @@ await Parallel.ForEachAsync(archivePaths, async (filePath, _) => { Id = sluggedName, Name = sluggedName, - AreSymbolsHashed = areSymbolsHashed, BlowfishKey = blowfishKey.ToString(), EnableOodleCompression = false, IsTtarch2 = filter == "*.ttarch2", LuaVersion = LuaVersion.Lua502, // Determining lua versions is too much for me, sorry. - MetaStreamVersion = msv, + StreamVersion = msv, TtarchVersion = ttarchVersion, }; diff --git a/src/TelltaleToolKit/GamesDatabase/GameProfile.cs b/src/TelltaleToolKit/GamesDatabase/GameProfile.cs index fbffdf5..c7d4201 100644 --- a/src/TelltaleToolKit/GamesDatabase/GameProfile.cs +++ b/src/TelltaleToolKit/GamesDatabase/GameProfile.cs @@ -1,83 +1,80 @@ using TelltaleToolKit.Reflection; -using TelltaleToolKit.Serialization.Binary; +using TelltaleToolKit.Serialization; using TelltaleToolKit.TelltaleArchives; using TelltaleToolKit.Utility.Blowfish; +using TelltaleToolKit.Utility.Lua; namespace TelltaleToolKit.GamesDatabase; /// -/// Represents a registry entry for a Telltale game configuration. -/// Each entry is identified by its file name following the convention: -/// [slugified-title]-[year]-[month]-[platform]-[demo] -/// This registry contains general information about the game, versioning, encryption, and a mapping of metaclasses. +/// Represents a registry entry for a Telltale game configuration. +/// Each entry is identified by its file name following the convention: +/// [slugified-title]-[year]-[month]-[platform]-[demo] +/// This registry contains general information about the game, versioning, encryption, and a mapping of metaclasses. /// public sealed class GameProfile { /// - /// Gets or sets the unique identifier for the registry entry, derived from the file name. + /// Gets or sets the unique identifier for the registry entry, derived from the file name. /// public string Id { get; set; } = string.Empty; /// - /// Gets the display name of the game. + /// Gets the display name of the game. /// public string Name { get; set; } = string.Empty; /// - /// Gets a textual description of the game. + /// Gets a textual description of the game. /// public string Description { get; init; } = string.Empty; /// - /// Gets the Blowfish encryption key used for securing game archives. - /// In the config file, you can either use the name of or a custom string. + /// Gets the Blowfish encryption key used for securing game archives. + /// In the config file, you can either use the name of or a custom string. /// public string BlowfishKey { get; set; } = string.Empty; /// - /// Gets a value indicating whether the game uses the TTARCH2 archive format. + /// Gets a value indicating whether the game uses the TTARCH2 archive format. /// public bool IsTtarch2 { get; set; } /// - /// Gets the version of the TTARCH archive format used by the game. + /// Gets the version of the TTARCH archive format used by the game. /// - /// + /// public TTArchiveVersion TtarchVersion { get; set; } /// - /// Gets the Lua scripting engine version used by the game. + /// Gets the Lua scripting engine version used by the game. /// - /// + /// public LuaVersion LuaVersion { get; init; } /// - /// Gets the version of the stream format used in the game. + /// Gets the version of the stream format used in the game. /// - /// - public MetaStreamVersion MetaStreamVersion { get; set; } - - // TODO: Automatically assign false if MetaStreamVersion is MBIN. - public bool AreSymbolsHashed { get; set; } + public uint StreamVersion { get; set; } /// - /// Gets a value indicating whether Oodle compression is enabled for game archives. + /// Gets a value indicating whether Oodle compression is enabled for game archives. /// public bool EnableOodleCompression { get; init; } - + /// - /// Identifier (name) of the metaclass database used for resolving MetaClass definitions for this game. - /// The value is a slug title from the already existing game profiles. - /// By default, this value is the same as . Use this property to override the database name - /// when multiple games share a common metaclass database, or when the metaclass DB is stored under a different id. - /// If empty, resolution should default to . - /// If invalid (no metaclass database exists) + /// Identifier (name) of the metaclass database used for resolving MetaClass definitions for this game. + /// The value is a slug title from the already existing game profiles. + /// By default, this value is the same as . Use this property to override the database name + /// when multiple games share a common metaclass database, or when the metaclass DB is stored under a different id. + /// If empty, resolution should default to . + /// If invalid (no metaclass database exists) /// public string MetaClassDatabase { get; init; } = string.Empty; /// - /// Gets a dictionary mapping to their corresponding IDs. - /// This represents the metaclasses available in the game and their identifiers. + /// Gets a dictionary mapping to their corresponding IDs. + /// This represents the metaclasses available in the game and their identifiers. /// public Dictionary Classes { get; init; } = new(); -} \ No newline at end of file +} diff --git a/src/TelltaleToolKit/GamesDatabase/GameProfileJsonConverter.cs b/src/TelltaleToolKit/GamesDatabase/GameProfileJsonConverter.cs index 981b0cd..179eccd 100644 --- a/src/TelltaleToolKit/GamesDatabase/GameProfileJsonConverter.cs +++ b/src/TelltaleToolKit/GamesDatabase/GameProfileJsonConverter.cs @@ -1,24 +1,24 @@ using System.Text.Json; using System.Text.Json.Serialization; -using TelltaleToolKit.Serialization.Binary; using TelltaleToolKit.TelltaleArchives; using TelltaleToolKit.Utility.Blowfish; +using TelltaleToolKit.Utility.Lua; namespace TelltaleToolKit.GamesDatabase; /// -/// Custom JSON converter for objects. -/// Handles conversion between JSON and game registry entries. +/// Custom JSON converter for objects. +/// Handles conversion between JSON and game registry entries. /// public class GameRegistryJsonConverter : JsonConverter { /// - /// Reads and converts the JSON to a object. + /// Reads and converts the JSON to a object. /// /// The reader to read from. /// The type of object to convert. /// Serializer options. - /// The converted object. + /// The converted object. public override GameProfile Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { using JsonDocument doc = JsonDocument.ParseValue(ref reader); @@ -31,22 +31,9 @@ public override GameProfile Read(ref Utf8JsonReader reader, Type typeToConvert, bool isTtarch2 = obj.GetProperty("isTtarch2").GetBoolean(); int ttarchVersion = obj.GetProperty("ttarchVersion").GetInt32(); string luaVersion = obj.GetProperty("luaVersion").GetString() ?? string.Empty; - string metaStreamVersion = obj.GetProperty("metaStreamVersion").GetString() ?? string.Empty; + uint streamVersion = obj.GetProperty("streamVersion").GetUInt32(); bool enableOodleCompression = obj.GetProperty("enableOodleCompression").GetBoolean(); - var areSymbolsHashed = true; - if (obj.TryGetProperty("areSymbolsHashed", out JsonElement value)) - { - if (value.ValueKind is JsonValueKind.True or JsonValueKind.False) - { - areSymbolsHashed = value.GetBoolean(); - } - else if (value.ValueKind == JsonValueKind.String && - bool.TryParse(value.GetString(), out bool parsed)) - { - areSymbolsHashed = parsed; - } - } - + // Attempt to resolve the blowfish key from known enum values. If not, use the string as a key. if (Enum.TryParse(blowfishKey, out T3BlowfishKey enumBlowfishKey)) { @@ -61,17 +48,16 @@ public override GameProfile Read(ref Utf8JsonReader reader, Type typeToConvert, IsTtarch2 = isTtarch2, TtarchVersion = ttarchVersion.TtarchVersionFromNumber(isTtarch2), LuaVersion = luaVersion.ParseLuaVersion(), - MetaStreamVersion = metaStreamVersion.StreamVersionFromString(), - EnableOodleCompression = enableOodleCompression, - AreSymbolsHashed = areSymbolsHashed + StreamVersion = streamVersion, + EnableOodleCompression = enableOodleCompression }; } /// - /// Writes a object as JSON. + /// Writes a object as JSON. /// - /// The to write to. - /// The value to write. + /// The to write to. + /// The value to write. /// Serializer options. public override void Write(Utf8JsonWriter writer, GameProfile value, JsonSerializerOptions options) { @@ -82,8 +68,8 @@ public override void Write(Utf8JsonWriter writer, GameProfile value, JsonSeriali writer.WriteBoolean("isTtarch2", value.IsTtarch2); writer.WriteNumber("ttarchVersion", value.TtarchVersion.ToJsonNumber()); writer.WriteString("luaVersion", value.LuaVersion.ToVersionString()); - writer.WriteString("metaStreamVersion", value.MetaStreamVersion.ToJsonString()); + writer.WriteNumber("streamVersion", value.StreamVersion); writer.WriteBoolean("enableOodleCompression", value.EnableOodleCompression); writer.WriteEndObject(); } -} \ No newline at end of file +} diff --git a/src/TelltaleToolKit/GamesDatabase/GameProfileJsonExtensions.cs b/src/TelltaleToolKit/GamesDatabase/GameProfileJsonExtensions.cs index 9b496b6..64a1c5b 100644 --- a/src/TelltaleToolKit/GamesDatabase/GameProfileJsonExtensions.cs +++ b/src/TelltaleToolKit/GamesDatabase/GameProfileJsonExtensions.cs @@ -6,11 +6,11 @@ namespace TelltaleToolKit.GamesDatabase; public class GameProfileJsonExtensions { /// - /// Serializes GameProfile.Classes to JSON. + /// Serializes GameProfile.Classes to JSON. /// public static string ToClassesJson(GameProfile profile, JsonSerializerOptions options) { - var classes = new SortedDictionary( + SortedDictionary classes = new( profile.Classes.ToDictionary( kvp => kvp.Key.Symbol.DebugString, kvp => kvp.Value @@ -21,13 +21,15 @@ public static string ToClassesJson(GameProfile profile, JsonSerializerOptions op } /// - /// Reads JSON produced by and applies it back onto GameProfile.Classes. + /// Reads JSON produced by and applies it back onto GameProfile.Classes. /// public static void ReadClassesJsonInto(GameProfile profile, string json, JsonSerializerOptions options) { - var dict = JsonSerializer.Deserialize>(json, options); + Dictionary? dict = JsonSerializer.Deserialize>(json, options); if (dict is null) + { return; + } profile.Classes.Clear(); diff --git a/src/TelltaleToolKit/Reflection/MetaClassRegistry.cs b/src/TelltaleToolKit/Reflection/MetaClassRegistry.cs index e01ab33..03d7221 100644 --- a/src/TelltaleToolKit/Reflection/MetaClassRegistry.cs +++ b/src/TelltaleToolKit/Reflection/MetaClassRegistry.cs @@ -21,7 +21,7 @@ public class MetaClassRegistry /// /// Retrieves a class by type hash (CRC64) and CRC32. /// - private MetaClass? GetClass(ulong typeHash, uint crc32) + public MetaClass? GetClass(ulong typeHash, uint crc32) => GetClass((typeHash, crc32)); /// @@ -35,7 +35,7 @@ public class MetaClassRegistry /// private MetaClass? GetClass((ulong TypeHash, uint Crc32) key) { - return Classes.TryGetValue(key, out MetaClass? result) ? result : null; + return Classes.GetValueOrDefault(key); } /// diff --git a/src/TelltaleToolKit/Serialization/Binary/BinaryMetaStreamReader.cs b/src/TelltaleToolKit/Serialization/Binary/BinaryMetaStreamReader.cs new file mode 100644 index 0000000..7eb8ff0 --- /dev/null +++ b/src/TelltaleToolKit/Serialization/Binary/BinaryMetaStreamReader.cs @@ -0,0 +1,293 @@ +using System.Text; +using TelltaleToolKit.Reflection; +using TelltaleToolKit.T3Types; +using TelltaleToolKit.Utility.Hashing; + +namespace TelltaleToolKit.Serialization.Binary; + +public sealed class BinaryMetaStreamReader : MetaStream +{ + private BinaryReader Reader = null!; + + public BinaryMetaStreamReader(Stream inputStream, Workspace? workspace = null) + { + BaseStream = inputStream; + Params.Workspace = workspace; + + bool isValid = ReadHeader(); + + if (!isValid) + { + throw new InvalidDataException("Invalid MetaStream."); + } + + long offset = Sections[0].CompressedSize; + for (int i = 1; i <= 3; i++) + { + //for each section (default,async,debug) + SectionInfo currentSect = Sections[i]; + if (currentSect.CompressedSize > 0) + { + currentSect.Stream = new SubStream(Sections[0].Stream, offset, currentSect.CompressedSize); + if (currentSect.IsCompressed) + { + if (Params.Workspace is null) + { + throw new InvalidOperationException("[MetaStream] Workspace is not set for encrypted streams."); + } + + ContainerStream cs = new(currentSect.Stream, Params.Workspace.BlowfishKey); + currentSect.Stream = cs; + SetSection((SectionType)i); + + // No idea why this is skipped + this.ReadUInt64(); + } + + offset += currentSect.CompressedSize; + } + } + + SetSection(SectionType.Default); + } + + public override MetaStreamMode Mode => MetaStreamMode.Read; + + private bool ReadHeader() + { + if (BaseStream.Length < 4) + { + Toolkit.Instance.Logger.LogError("Invalid MetaHeader: Stream too short."); + return false; + } + + Sections[(int)SectionType.Header].Stream = BaseStream; + Reader = new BinaryReader(Sections[(int)SectionType.Header].Stream, Encoding.UTF8, true); + SetSection(SectionType.Header); + + MetaStreamMagic magic = (MetaStreamMagic)this.ReadUInt32(); + Params.StreamVersion = magic.GetMetaStreamVersion(); + uint streamVersion = Params.StreamVersion; + + if (Params.StreamVersion == 0) + { + Toolkit.Instance.Logger.LogError($"Not a valid meta stream version: {magic}"); + return false; + } + + long defaultSize = 0, debugSize = 0, asyncSize = 0; + + if (streamVersion >= 4) + { + if (streamVersion >= 5) + { + defaultSize = this.ReadUInt32(); + } + + debugSize = this.ReadUInt32(); + asyncSize = this.ReadUInt32(); + + if ((defaultSize & 0x80000000) != 0) + { + defaultSize &= 0x7FFFFFFF; + Sections[1].IsCompressed = true; + } + + if ((debugSize & 0x80000000) != 0) + { + debugSize &= 0x7FFFFFFF; + Sections[2].IsCompressed = true; + } + + if ((asyncSize & 0x80000000) != 0) + { + asyncSize &= 0x7FFFFFFF; + Sections[3].IsCompressed = true; + } + } + else + { + Params.Encrypt = true; + // For MCOM, there is an extra 4-byte field to skip + if (magic is MetaStreamMagic.Mcom or MetaStreamMagic.EncryptedMcom) + { + _ = this.ReadUInt32(); // Actually, this might be the fully decompressed data size. + + if (magic is MetaStreamMagic.Mcom) + { + Params.Encrypt = false; + /* TODO: This is not a priority, because Telltale haven't shipped any games with MCOM or encrypted MCOM. + But it would be nice to support them in the future. + In TWDS1, there's a function called "ReadUncompressAndStoreInMemory" which reads and decompresses the whole stream in memory. + It uses zlib compression. + This means MCOM is just a compressed version of the entire file. + While the encrypted version is still compressed, but the decompressed data is encrypted. + */ + } + + Toolkit.Instance.Logger.LogError("[MetaStream] Mcom or EncryptedMcom is not supported."); + return false; + } + + if (Params.Workspace is null) + { + Toolkit.Instance.Logger.LogError("[MetaStream] Workspace is not set for encrypted streams."); + return false; + } + + // Create a LegacyEncryptedStream that starts after the magic (and possible extra) + LegacyEncryptedStream encryptedStream = new(BaseStream, magic.GetMetaStreamVersion(), + GetPosition(), + Params.Workspace.Blowfish); + + // Replace the underlying stream and reinitialize the BinaryReader + Sections[0].Stream = encryptedStream; + Reader = new BinaryReader(Sections[0].Stream, Encoding.UTF8, true); + } + + // Read the number of serialized classes + uint numVers = this.ReadUInt32(); + + if (numVers >= 1024) + { + Toolkit.Instance.Logger.LogError("[MetaStream] Too many serialized classes."); + return false; + } + + Params.VersionInfo.Capacity = (int)numVers; + for (int i = 0; i < numVers; i++) + { + MetaVersionInfo verInfo = new(); + + if (magic.GetMetaStreamVersion() >= 3) // mStreamVersion >= 3 uses hashed symbols + { + verInfo.TypeSymbolCrc = this.ReadUInt64(); + } + else + { + string typeName = this.ReadString(); + verInfo.TypeSymbolCrc = Crc64.Compute(MetaClassType.GetStrippedTypeName(typeName)); + } + + verInfo.VersionCrc = this.ReadUInt32(); + Params.VersionInfo.Add(verInfo); + } + + long headerSize = Sections[(int)SectionType.Header].Stream!.Position; + + defaultSize = Params.StreamVersion >= 5 + ? defaultSize + : BaseStream.Length - headerSize - debugSize - asyncSize; + + // The encrypted header is lost in the process, so we need to add it back + if (Params.Encrypt) + { + defaultSize -= 4; + } + + Sections[0].CompressedSize = headerSize; + Sections[1].CompressedSize = defaultSize; + Sections[2].CompressedSize = debugSize; + Sections[3].CompressedSize = asyncSize; + + return true; + } + + protected override void SetSection(SectionType newSection) + { + if (_currentSection == newSection) + { + return; + } + + _currentSection = newSection; + Reader = new BinaryReader(Sections[(int)_currentSection].Stream, Encoding.UTF8, true); + } + + public override void BeginBlock() + { + int size = this.ReadInt32(); + long expectedPosition = Sections[(int)_currentSection].Stream!.Position + size - sizeof(int); + Sections[(int)_currentSection].Blocks.Push(expectedPosition); + } + + public override void EndBlock() + { + SectionInfo currentSectionInfo = Sections[(int)_currentSection]; + long expectedPosition = currentSectionInfo.Blocks.Pop(); + long currentPosition = currentSectionInfo.Stream!.Position; + if (expectedPosition == currentPosition) + { + return; + } + + // Telltale Tool skips the block if it's not read fully. + // I have no idea how they got there in the first place, but I saw one such file (ui_mainmenu_background.scene) which benefits from this. + // Previously, I used to throw exceptions since I thought it was impossible, but this seems better for stability purposes. + // This matches the implementation of the engine behaviour. + Toolkit.Instance.Logger.LogWarning( + $"[BinaryMetaStreamReader] Invalid data position! Current Position: {currentPosition}. Expected Position: {expectedPosition}."); + currentSectionInfo.Stream.Position = expectedPosition; + } + + public override void Serialize(ref bool value) => + value = Reader.ReadChar() switch + { + '1' => true, + '0' => false, + _ => throw new InvalidBooleanException($"Invalid boolean at position: {Reader.BaseStream.Position}!") + }; + + public override void Serialize(ref float value) => value = Reader.ReadSingle(); + + public override void Serialize(ref double value) => value = Reader.ReadDouble(); + + public override void Serialize(ref short value) => value = Reader.ReadInt16(); + + public override void Serialize(ref int value) => value = Reader.ReadInt32(); + + public override void Serialize(ref long value) => value = Reader.ReadInt64(); + + public override void Serialize(ref ushort value) => value = Reader.ReadUInt16(); + + public override void Serialize(ref uint value) => value = Reader.ReadUInt32(); + + public override void Serialize(ref ulong value) => value = Reader.ReadUInt64(); + + public override void Serialize(ref string value) + { + int strLength = Reader.ReadInt32(); + + if (strLength < 0) + { + throw new ArgumentOutOfRangeException(nameof(strLength)); + } + + byte[] buffer = new byte[strLength]; + int realLength = Reader.Read(buffer, 0, strLength); + + if (realLength != strLength) + { + throw new EndOfStreamException($"Expected {strLength} bytes but read {realLength}."); + } + + value = Encoding.UTF8.GetString(buffer, 0, realLength); + } + + public override void Serialize(ref char value) => value = Reader.ReadChar(); + + public override void Serialize(ref byte value) => value = Reader.ReadByte(); + + public override void Serialize(ref sbyte value) => value = Reader.ReadSByte(); + + public override void Serialize(ref Symbol value) + { + ulong crc64 = Reader.ReadUInt64(); + value = Symbol.FromCrc64(crc64); + Params.SerializedSymbols.Add(value); + } + + public override void Serialize(byte[] values, int offset, int count) => Reader.Read(values, offset, count); + + public override void Close() => Dispose(true); +} diff --git a/src/TelltaleToolKit/Serialization/Binary/BinaryMetaStreamWriter.cs b/src/TelltaleToolKit/Serialization/Binary/BinaryMetaStreamWriter.cs new file mode 100644 index 0000000..7a71535 --- /dev/null +++ b/src/TelltaleToolKit/Serialization/Binary/BinaryMetaStreamWriter.cs @@ -0,0 +1,219 @@ +using System.Text; +using TelltaleToolKit.T3Types; + +namespace TelltaleToolKit.Serialization.Binary; + +public sealed class BinaryMetaStreamWriter : MetaStream +{ + private readonly Stream _outputStream; + private bool _closed; + + public BinaryMetaStreamWriter(Stream outputStream, MetaStreamParams @params) + { + _outputStream = outputStream; + Params = @params; + Params.SerializedSymbols.Clear(); + + Sections[0].Stream = new MemoryStream(0x100); + SetSection(SectionType.Default); + } + + private BinaryWriter Writer { get; set; } = null!; + + public override MetaStreamMode Mode => MetaStreamMode.Write; + + private void WriteHeader() + { + SetSection(SectionType.Header); + this.Write((uint)Params.ResolveMagic()); + uint streamVersion = Params.StreamVersion; + + if (streamVersion >= 4) + { + uint defaultSize = (uint)(GetSection(SectionType.Default).Stream?.Length ?? 0); + uint debugSize = (uint)(GetSection(SectionType.Debug).Stream?.Length ?? 0); + uint asyncSize = (uint)(GetSection(SectionType.Async).Stream?.Length ?? 0); + + if (GetSection(SectionType.Default).IsCompressed) + { + defaultSize |= 0x80000000; + } + + if (GetSection(SectionType.Debug).IsCompressed) + { + debugSize |= 0x80000000; + } + + if (GetSection(SectionType.Async).IsCompressed) + { + asyncSize |= 0x80000000; + } + + if (streamVersion >= 5) + { + this.Write(defaultSize); + } + + this.Write(debugSize); + this.Write(asyncSize); + } + + Params.VersionInfo = Params.VersionInfo.Distinct().ToList(); + + this.Write(Params.VersionInfo.Count); + + foreach (MetaVersionInfo? versionInfo in Params.VersionInfo) + { + if (streamVersion >= 3) + { + this.Write(versionInfo.TypeSymbolCrc); + } + else + { + this.Write(versionInfo.GetMetaClassType()!.FullTypeName); + } + + this.Write(versionInfo.VersionCrc); + } + } + + protected override void SetSection(SectionType newSection) + { + if (_currentSection == newSection) + { + return; + } + + _currentSection = newSection; + Stream sectionStream = CurrentSection.Stream ?? new MemoryStream(0x4000); + Writer = new BinaryWriter(sectionStream, Encoding.UTF8, true); + CurrentSection.Stream ??= sectionStream; + } + + public override void BeginBlock() + { + long position = CurrentSection.Stream!.Position; + CurrentSection.Blocks.Push(position); + this.Write(0); + } + + public override void EndBlock() + { + long position = CurrentSection.Blocks.Pop(); + + long currentPosition = CurrentSection.Stream!.Position; + int blockSize = (int)(currentPosition - position); + + CurrentSection.Stream.Seek(position, SeekOrigin.Begin); + this.Write(blockSize); + CurrentSection.Stream.Seek(currentPosition, SeekOrigin.Begin); + } + + public override void Serialize(ref bool value) => Writer.Write(value ? '1' : '0'); + + public override void Serialize(ref float value) => Writer.Write(value); + + public override void Serialize(ref double value) => Writer.Write(value); + + public override void Serialize(ref short value) => Writer.Write(value); + + public override void Serialize(ref int value) => Writer.Write(value); + + public override void Serialize(ref long value) => Writer.Write(value); + + public override void Serialize(ref ushort value) => Writer.Write(value); + + public override void Serialize(ref uint value) => Writer.Write(value); + + public override void Serialize(ref ulong value) => Writer.Write(value); + + public override void Serialize(ref string value) + { + byte[] strBytes = Encoding.UTF8.GetBytes(value); + Writer.Write(strBytes.Length); + Writer.Write(strBytes); + } + + public override void Serialize(ref char value) => Writer.Write(value); + + public override void Serialize(ref byte value) => Writer.Write(value); + + public override void Serialize(ref sbyte value) => Writer.Write(value); + + public override void Serialize(byte[] values, int offset, int count) => Writer.Write(values, offset, count); + + private void FinalizeStream() + { + if (_closed) + { + throw new ObjectDisposedException(nameof(BinaryMetaStreamWriter)); + } + + uint streamVersion = Params.StreamVersion; + bool canCompress = streamVersion >= 4; + // Telltale does not compress the header section, so we can skip it. + for (int i = 1; i <= 3; i++) + { + SectionInfo section = Sections[i]; + if (section.Stream is { Length: > 0 } && Params.Compress && canCompress) + { + ContainerStreamParams options = new(); + MemoryStream outStream = new(0x10000); + ContainerStream.Create(outStream, section.Stream, options); + section.Stream.Dispose(); + section.Stream = outStream; + section.CompressedSize = outStream.Length; + section.IsCompressed = true; + } + else + { + section.CompressedSize = section.Stream?.Length ?? 0; + section.IsCompressed = false; + } + } + } + + public override void Close() + { + if (_closed) + { + return; + } + + FinalizeStream(); + WriteHeader(); + + // Iterate each section (header, default, debug, async) and copy its data to the output stream. + foreach (SectionInfo section in Sections) + { + if (section.Stream is null) + { + continue; + } + + section.Stream.Seek(0, SeekOrigin.Begin); + section.Stream.CopyTo(_outputStream); + } + + if (Params is { Encrypt: true, StreamVersion: <= 3 }) + { + MetaStreamMagic targetMagic = Params.ResolveMagic(); + + if (!_outputStream.CanRead || !_outputStream.CanSeek) + throw new InvalidOperationException( + "Legacy encryption requires a readable and seekable output stream (e.g. MemoryStream or FileStream opened with FileAccess.ReadWrite). " + + "The encryption is applied in-place after writing."); + + LegacyEncryption.Encrypt(_outputStream, targetMagic, Params.Workspace!.Blowfish); + } + + _closed = true; + Dispose(true); + } + + public override void Serialize(ref Symbol value) + { + Writer.Write(value.Crc64); + Params.SerializedSymbols.Add(value); + } +} diff --git a/src/TelltaleToolKit/Serialization/Binary/InvalidBooleanException.cs b/src/TelltaleToolKit/Serialization/Binary/InvalidBooleanException.cs index 805e39f..a31544b 100644 --- a/src/TelltaleToolKit/Serialization/Binary/InvalidBooleanException.cs +++ b/src/TelltaleToolKit/Serialization/Binary/InvalidBooleanException.cs @@ -15,4 +15,4 @@ public InvalidBooleanException(string message, Exception innerException) : base(message, innerException) { } -} \ No newline at end of file +} diff --git a/src/TelltaleToolKit/Serialization/Binary/LegacyEncryption.cs b/src/TelltaleToolKit/Serialization/Binary/LegacyEncryption.cs new file mode 100644 index 0000000..52d4697 --- /dev/null +++ b/src/TelltaleToolKit/Serialization/Binary/LegacyEncryption.cs @@ -0,0 +1,230 @@ +using System.Buffers; +using System.Buffers.Binary; +using TelltaleToolKit.Utility.Blowfish; + +namespace TelltaleToolKit.Serialization.Binary; + +/// +/// Provides static helpers to encrypt and decrypt legacy Telltale MetaStream files +/// directly on a seekable , one block at a time. +/// No full-file buffering is performed — memory usage is O(blockSize). +/// +public static class LegacyEncryption +{ + /// + public static void Decrypt(Stream stream, string blowfishKey, int archiveVersion = 7) + => Decrypt(stream, new Blowfish(blowfishKey, archiveVersion)); + + /// + /// Decrypts a legacy-encrypted MetaStream in-place by reading and rewriting + /// one block at a time. The magic is patched to its plaintext equivalent. + /// + /// + /// A readable, writable, seekable stream positioned anywhere. + /// The stream must be at least 4 bytes long. + /// + /// Pre-built Blowfish instance for this game. + /// + /// Thrown when the stream does not support reading, writing, or seeking. + /// + public static void Decrypt(Stream stream, Blowfish blowfish) + { + EnsureStream(stream); + if (stream.Length < 4) + { + return; + } + + MetaStreamMagic magic = ReadMagic(stream); + (int BlockSize, int CryptInterval, int CleanInterval) config = GetBlockConfig(magic); + if (config.BlockSize == 0) + { + return; // not an encrypted magic + } + + ProcessBlocks(stream, blowfish, config, false); + + // Patch magic to the readable plaintext equivalent + WriteMagic(stream, GetPlaintextMagic(magic)); + } + + // ------------------------------------------------------------------------- + // Public API — Encrypt + // ------------------------------------------------------------------------- + + /// + public static void Encrypt(Stream stream, MetaStreamMagic targetMagic, string blowfishKey, int archiveVersion = 7) + => Encrypt(stream, targetMagic, new Blowfish(blowfishKey, archiveVersion)); + + /// + /// Encrypts a plaintext MetaStream in-place by reading and rewriting one block + /// at a time. The magic is patched to first so + /// the block layout matches what the decryptor will expect. + /// + /// + /// A readable, writable, seekable stream containing a plaintext MetaStream + /// (Mbin or Mtre magic at offset 0). Must be at least 4 bytes. + /// + /// + /// The encrypted magic to embed, e.g. . + /// Must be a recognised encrypted magic — drives the block configuration. + /// + /// Pre-built Blowfish instance for this game. + /// + /// Thrown when has no encryption scheme. + /// + /// + /// Thrown when the stream does not support reading, writing, or seeking. + /// + public static void Encrypt(Stream stream, MetaStreamMagic targetMagic, Blowfish blowfish) + { + EnsureStream(stream); + if (stream.Length < 4) + { + return; + } + + (int BlockSize, int CryptInterval, int CleanInterval) config = GetBlockConfig(targetMagic); + if (config.BlockSize == 0) + { + throw new ArgumentException( + $"{targetMagic} is not a recognised encrypted magic.", nameof(targetMagic)); + } + + // Patch magic before processing so block offsets are consistent with the decryptor. + WriteMagic(stream, targetMagic); + ProcessBlocks(stream, blowfish, config, true); + } + + + private static void ProcessBlocks(Stream stream, Blowfish blowfish, + (int BlockSize, int CryptInterval, int CleanInterval) config, bool encrypt) + { + // Blocks begin immediately after the 4-byte magic. + const int dataStart = 4; + long dataLength = stream.Length - dataStart; + if (dataLength <= 0) + { + return; + } + + int blockSize = config.BlockSize; + long blockCount = dataLength / blockSize; + byte[] buffer = ArrayPool.Shared.Rent(blockSize); + // Trailing partial block: untouched, matches reference behaviour. + + // Rent exactly blockSize bytes — never more — so large files stay lean. + try + { + Span block = buffer.AsSpan(0, blockSize); + for (long i = 0; i < blockCount; i++) + { + long offset = dataStart + i * blockSize; + if (i % config.CryptInterval == 0) + { + ReadExactly(stream, offset, block); + if (encrypt) + blowfish.Encipher(block, block.Length); + else + blowfish.Decipher(block, block.Length); + WriteAt(stream, offset, block); + } + else if (i % config.CleanInterval == 0 && i > 0) + { + // clean block – do nothing + } + else + { + ReadExactly(stream, offset, block); + XorBlock(block, 0xFF); + WriteAt(stream, offset, block); + } + } + } + finally + { + ArrayPool.Shared.Return(buffer); + } + } + + private static void ReadExactly(Stream stream, long offset, Span buffer) + { + stream.Seek(offset, SeekOrigin.Begin); + int total = 0; + while (total < buffer.Length) + { + int read = stream.Read(buffer.Slice(total)); + if (read == 0) + throw new EndOfStreamException($"Expected {buffer.Length} bytes at offset {offset}, got {total}."); + total += read; + } + } + + private static void WriteAt(Stream stream, long offset, ReadOnlySpan data) + { + stream.Seek(offset, SeekOrigin.Begin); + stream.Write(data); + } + + private static MetaStreamMagic ReadMagic(Stream stream) + { + stream.Seek(0, SeekOrigin.Begin); + Span buf = stackalloc byte[4]; + ReadExactly(stream, 0, buf); + return (MetaStreamMagic)BitConverter.ToUInt32(buf); + } + + private static void WriteMagic(Stream stream, MetaStreamMagic magic) + { + stream.Seek(0, SeekOrigin.Begin); + Span buf = stackalloc byte[4]; + BitConverter.TryWriteBytes(buf, (uint)magic); + stream.Write(buf); + } + + private static void EnsureStream(Stream stream) + { + if (!stream.CanRead) + { + throw new NotSupportedException("Stream must be readable."); + } + + if (!stream.CanWrite) + { + throw new NotSupportedException("Stream must be writable."); + } + + if (!stream.CanSeek) + { + throw new NotSupportedException("Stream must be seekable."); + } + } + + private static void XorBlock(Span block, byte mask) + { + for (int i = 0; i < block.Length; i++) + block[i] ^= mask; + } + + private static (int BlockSize, int CryptInterval, int CleanInterval) GetBlockConfig(MetaStreamMagic magic) => + magic switch + { + MetaStreamMagic.EncryptedMbin1 + or MetaStreamMagic.EncryptedMbin2 + or MetaStreamMagic.EncryptedMbin3 => (128, 32, 80), + MetaStreamMagic.EncryptedMtre + or MetaStreamMagic.EncryptedMcom => (256, 8, 24), + MetaStreamMagic.Mbes => (64, 64, 100), + _ => default + }; + + private static MetaStreamMagic GetPlaintextMagic(MetaStreamMagic encrypted) => encrypted switch + { + MetaStreamMagic.EncryptedMbin1 + or MetaStreamMagic.EncryptedMbin2 + or MetaStreamMagic.EncryptedMbin3 or MetaStreamMagic.Mbes => MetaStreamMagic.Mbin, + MetaStreamMagic.EncryptedMtre => MetaStreamMagic.Mtre, + MetaStreamMagic.EncryptedMcom => MetaStreamMagic.Mcom, + _ => encrypted + }; +} diff --git a/src/TelltaleToolKit/Serialization/Binary/MetaStream.cs b/src/TelltaleToolKit/Serialization/Binary/MetaStream.cs deleted file mode 100644 index 9cd8b96..0000000 --- a/src/TelltaleToolKit/Serialization/Binary/MetaStream.cs +++ /dev/null @@ -1,246 +0,0 @@ -using TelltaleToolKit.HashDatabase; -using TelltaleToolKit.Reflection; -using TelltaleToolKit.T3Types; - -namespace TelltaleToolKit.Serialization.Binary; - -/// -/// Stream wrapper used for serializing telltale assets. -/// A single Telltale Tool file has 1-3 stream sections (Main, Debug, Async) depending on its metastream version. -/// -public abstract class MetaStream : IDisposable -{ - private SectionType _currentSection = SectionType.Main; - - protected enum SectionType - { - Main, - Debug, - Async - } - - protected MetaSection[] Sections = - [ - new(), // Main section - new(), // Debug section - new() // Async section - ]; - - public MetaStreamConfiguration Configuration { get; set; } = new(); - - protected Stream UnderlyingStream { get; set; } = null!; - - protected Stream CurrentSubstream => GetCurrentSection().Stream; - - public void Dispose() - { - foreach (MetaSection section in Sections) - { - section.Stream.Dispose(); - } - - UnderlyingStream.Dispose(); - GC.SuppressFinalize(this); - } - - protected MetaSection GetCurrentSection() => Sections[(int)_currentSection]; - - public void BeginAsyncSection() - { - if (_currentSection is SectionType.Async || - Configuration.Version is not (MetaStreamVersion.Msv5 or MetaStreamVersion.Msv6)) - return; - _currentSection = SectionType.Async; - InitSerializer(); - } - - public void EndAsyncSection() - { - if (_currentSection is not SectionType.Async || - Configuration.Version is not (MetaStreamVersion.Msv5 or MetaStreamVersion.Msv6)) - return; - _currentSection = SectionType.Main; // Switch back to header section - InitSerializer(); - } - - public void BeginDebugSection() - { - if (_currentSection is SectionType.Debug || - Configuration.Version is not (MetaStreamVersion.Msv4 or MetaStreamVersion.Msv5 or MetaStreamVersion.Msv6)) - return; - _currentSection = SectionType.Debug; - InitSerializer(); - } - - public void EndDebugSection() - { - if (_currentSection is not SectionType.Debug || - Configuration.Version is not (MetaStreamVersion.Msv4 or MetaStreamVersion.Msv5 or MetaStreamVersion.Msv6)) - return; - _currentSection = SectionType.Main; - InitSerializer(); - } - - public abstract void SerializeMetaHeader(); - - protected abstract void InitSerializer(); - - public abstract void BeginBlock(); - - public abstract void EndBlock(); - - public bool IsSectionEmpty() - => GetCurrentSection().Stream.Length == 0; - - public bool IsClassSerialized(string typeName) - => Configuration.SerializedClasses.Any(id => id.ClassType.Symbol.DebugString == typeName); - - public abstract MetaClass? GetMetaClass(Type type); - - public abstract MetaClass? GetMetaClass(Symbol symbol); - - /// - /// Serializes the specified boolean value. - /// - /// The value to serialize - public abstract void Serialize(ref bool value); - - /// - /// Serializes the specified float value. - /// - /// The value to serialize - public abstract void Serialize(ref float value); - - /// - /// Serializes the specified double value. - /// - /// The value to serialize - public abstract void Serialize(ref double value); - - /// - /// Serializes the specified short value. - /// - /// The value to serialize - public abstract void Serialize(ref short value); - - /// - /// Serializes the specified integer value. - /// - /// The value to serialize - public abstract void Serialize(ref int value); - - /// - /// Serializes the specified long value. - /// - /// The value to serialize - public abstract void Serialize(ref long value); - - /// - /// Serializes the specified ushort value. - /// - /// The value to serialize - public abstract void Serialize(ref ushort value); - - /// - /// Serializes the specified unsigned integer value. - /// - /// The value to serialize - public abstract void Serialize(ref uint value); - - /// - /// Serializes the specified unsigned long value. - /// - /// The value to serialize - public abstract void Serialize(ref ulong value); - - /// - /// Serializes the specified string value. - /// - /// The value to serialize - public abstract void Serialize(ref string value); - - /// - /// Serializes the specified char value. - /// - /// The value to serialize - public abstract void Serialize(ref char value); - - /// - /// Serializes the specified byte value. - /// - /// The value to serialize - public abstract void Serialize(ref byte value); - - /// - /// Serializes the specified signed byte value. - /// - /// The value to serialize - public abstract void Serialize(ref sbyte value); - - /// - /// Serializes the Symbol class. - /// - /// The value to serialize - public abstract void Serialize(ref Symbol value); - - /// - /// Serializes the MetaClassType class. A special reader/writer for types. - /// - /// The value to serialize - public abstract void Serialize(ref MetaClassType value); - - /// - /// Serializes the specified byte array. - /// - /// The buffer to serialize. - /// The starting offset in the buffer to begin serializing. - /// The size, in bytes, to serialize. - public abstract void Serialize(byte[] values, int offset, int count); - - public void ThrowIfNotEndOfFile() - { - foreach (MetaSection stream in Sections) - { - var reader = new StreamReader(stream.Stream); - if (reader.BaseStream.Position != stream.Stream.Length) - { - throw new InvalidDataException( - $"FILE END NOT REACHED! Current position: {reader.BaseStream.Position}. Total size: {reader.BaseStream.Length}. Left to read: {reader.BaseStream.Length - reader.BaseStream.Position}"); - } - } - } - - // If there are assets larger than 2GBs, I need to rethink my life. - public int GetRemainingSectionBytes() - => (int)(GetCurrentSection().Stream.Length - GetCurrentSection().Stream.Position); - - public long GetCurrentPosition() - => GetCurrentSection().Stream.Position; - - public void SetCurrentPosition(long position) - => GetCurrentSection().Stream.Position = position; - - public void ResolveSymbols(ISymbolResolver symbolResolver) - { - foreach (Symbol entry in Configuration.SerializedSymbols) - { - symbolResolver.ResolveSymbol(entry); - - if (entry.DebugString is null) - { - Toolkit.Instance.Logger.LogWarning($"[MetaStream] Symbol not resolved: {entry.Crc64}"); - } - } - } - - public class MetaSection - { - // For blocks. In read, stores the sizes, in write stores the block offset initial. - public Stack Blocks = new(); - - public bool IsCompressed = false; - - // Section data stream - public Stream Stream = null!; - } -} diff --git a/src/TelltaleToolKit/Serialization/Binary/MetaStreamConfiguration.cs b/src/TelltaleToolKit/Serialization/Binary/MetaStreamConfiguration.cs deleted file mode 100644 index 04452c2..0000000 --- a/src/TelltaleToolKit/Serialization/Binary/MetaStreamConfiguration.cs +++ /dev/null @@ -1,53 +0,0 @@ -using TelltaleToolKit.Reflection; -using TelltaleToolKit.T3Types; - -namespace TelltaleToolKit.Serialization.Binary; - -/// -/// Holds configuration parameters and metadata for a MetaStream during serialization or deserialization. -/// -public class MetaStreamConfiguration -{ - /// - /// Gets or sets the MetaStream version associated with this configuration. - /// - public MetaStreamVersion Version { get; set; } - - /// - /// Gets or sets a value indicating whether symbols are stored as hashes instead of names. - /// This exists only because MBIN is a hybrid version (Hashed in Sam and Max S2). - /// - public bool AreSymbolsHashed { get; set; } - - /// - /// Gets the list of objects for all classes that have been serialized in the current stream. - /// - public List SerializedClasses { get; set; } = []; - - /// - /// Gets the list of classes found in the stream that are not registered in the current game profile. - /// Each entry is a tuple containing the and its CRC32 checksum. - /// - public List<(MetaClassType, uint crc32)> UnregisteredClasses { get; set; } = []; - - /// - /// Gets the list of type hashes and checksums for types encountered in the stream that could not be resolved. - /// Each entry is a tuple of the type hash (ulong) and CRC32 checksum. - /// - public List<(ulong, uint)> UnregisteredTypes { get; set; } = []; - - /// - /// Gets the list of serialized objects found in the stream. - /// - public List SerializedSymbols { get; set; } = []; - - /// - /// Gets or sets a value indicating whether the list of serialized classes can be modified after initialization. - /// - public bool CanModifySerializedClassesList { get; set; } - - /// - /// Gets the current workspace. Required for serializing if custom types are added. - /// - public Workspace? Workspace { get; set; } -} \ No newline at end of file diff --git a/src/TelltaleToolKit/Serialization/Binary/MetaStreamReader.cs b/src/TelltaleToolKit/Serialization/Binary/MetaStreamReader.cs deleted file mode 100644 index 5e5e520..0000000 --- a/src/TelltaleToolKit/Serialization/Binary/MetaStreamReader.cs +++ /dev/null @@ -1,277 +0,0 @@ -using System.Text; -using TelltaleToolKit.Reflection; -using TelltaleToolKit.T3Types; - -namespace TelltaleToolKit.Serialization.Binary; - -public sealed class MetaStreamReader : MetaStream -{ - public MetaStreamReader(Stream inputStream) - { - Reader = new BinaryReader(inputStream, Encoding.UTF8, true); - UnderlyingStream = inputStream; - SerializeMetaHeader(); - } - - private BinaryReader Reader { get; set; } - - private void ValidateMetaHeader() - { - if (UnderlyingStream.Length < 4) - throw new InvalidDataException("Invalid MetaHeader: Stream too short."); - - UnderlyingStream.Position = 0; - Configuration.Version = (MetaStreamVersion)this.ReadUInt32(); - - if (!Enum.IsDefined(typeof(MetaStreamVersion), Configuration.Version)) - throw new InvalidDataException($"Not a valid meta stream version: {Configuration.Version}"); - } - - public override void SerializeMetaHeader() - { - ValidateMetaHeader(); - - uint mainSize = 0, debugSize = 0, asyncSize = 0; - - switch (Configuration.Version) - { - case MetaStreamVersion.Mbes: - throw new NotSupportedException("MBES is not supported yet."); - // TODO optional compressed sections (set top bit of size to 1) - case MetaStreamVersion.Msv4: - mainSize = this.ReadUInt32(); - asyncSize = this.ReadUInt32(); - break; - case MetaStreamVersion.Msv6 - or MetaStreamVersion.Msv5: - mainSize = this.ReadUInt32(); - debugSize = this.ReadUInt32(); - asyncSize = this.ReadUInt32(); - break; - } - - // Read the number of serialized classes - uint metaClassNum = this.ReadUInt32(); - - if (metaClassNum >= 1000) - throw new InvalidDataException("Too many classes in the meta stream."); - - for (var i = 0; i < metaClassNum; i++) - { - uint size = this.ReadUInt32(); - - // Read the type hash or name (256 is a good number to use for the size of the name) - Configuration.AreSymbolsHashed = size > 255; - - Reader.BaseStream.Seek(-4, SeekOrigin.Current); - - MetaClassType type = this.ReadMetaClassType(); - uint crc32 = this.ReadUInt32(); - - // If the type is not recognized at all - if (type.FullTypeName == string.Empty) - { - if (Configuration.UnregisteredTypes.Count > 0) - { - int lastIndex = Configuration.UnregisteredTypes.Count - 1; - (ulong, uint) lastTuple = Configuration.UnregisteredTypes[lastIndex]; - Configuration.UnregisteredTypes[lastIndex] = (lastTuple.Item1, crc32); - } - - continue; - } - - MetaClass? mcs = Toolkit.Instance.ClassRegistry.GetClass(type.Symbol, crc32); - - if (mcs is not null) - { - // If the class description exists, add it. Otherwise TODO - Configuration.SerializedClasses.Add(mcs); - } - else - { - Configuration.UnregisteredClasses.Add((type, crc32)); - } - } - - long currentPosition = Reader.BaseStream.Position; - if (Configuration.Version is MetaStreamVersion.Msv6 or MetaStreamVersion.Msv5) - { - Sections[(int)SectionType.Main].Stream = new SubStream(UnderlyingStream, currentPosition, mainSize); - Sections[(int)SectionType.Debug].Stream = - new SubStream(UnderlyingStream, currentPosition + mainSize, debugSize); - Sections[(int)SectionType.Async].Stream = new SubStream( - UnderlyingStream, - currentPosition + mainSize + debugSize, - asyncSize - ); - } - else - { - // For older versions, the entire stream is the main section - Sections[(int)SectionType.Main].Stream = new SubStream( - UnderlyingStream, - currentPosition, - UnderlyingStream.Length - currentPosition - ); - Sections[(int)SectionType.Debug].Stream = new SubStream(UnderlyingStream, 0, 0); - Sections[(int)SectionType.Async].Stream = new SubStream(UnderlyingStream, 0, 0); - } - - InitSerializer(); - } - - public static bool IsValidMetaStream(Stream stream) - { - if (stream.Length < 4) - return false; - - long originalPosition = stream.Position; - - try - { - using var reader = new BinaryReader(stream, Encoding.UTF8, leaveOpen: true); - uint version = reader.ReadUInt32(); - - // Check if it's a valid MetaStream version - return Enum.IsDefined(typeof(MetaStreamVersion), version); - } - finally - { - stream.Position = originalPosition; - } - } - - protected override void InitSerializer() => Reader = new BinaryReader(CurrentSubstream, Encoding.UTF8, true); - - public override void BeginBlock() - { - int size = this.ReadInt32(); - long expectedPosition = GetCurrentSection().Stream.Position + size - sizeof(int); - GetCurrentSection().Blocks.Push(expectedPosition); - } - - /// - public override void EndBlock() - { - MetaSection currentSection = GetCurrentSection(); - long expectedPosition = currentSection.Blocks.Pop(); - long currentPosition = currentSection.Stream.Position; - if (expectedPosition == currentPosition) - return; - // Telltale Tool skips the block if it's not read fully. - // I have no idea how they got there in the first place, but I saw one such file (ui_mainmenu_background.scene) which benefits from this. - // Previously, I used to throw exceptions since I thought it was impossible, but this seems better for stability purposes. - // This matches the implementation of the engine behaviour. - Toolkit.Instance.Logger.LogWarning($"[MetaStreamReader] Invalid data position! Current Position: {currentPosition}. Expected Position: {expectedPosition}."); - GetCurrentSection().Stream.Position = expectedPosition; - } - - /// - /// Skip a block. - /// This is internally used, even by Telltale themselves. - /// - public void SkipToEndOfCurrentBlock() - { - MetaSection currentSection = GetCurrentSection(); - long expectedPosition = currentSection.Blocks.Pop(); - currentSection.Stream.Seek(expectedPosition, SeekOrigin.Begin); - } - - public override MetaClass? GetMetaClass(Type type) => - Configuration.SerializedClasses.FirstOrDefault(tc => tc.ClassType.LinkingType == type); - - public override MetaClass? GetMetaClass(Symbol symbol) => - Configuration.SerializedClasses.FirstOrDefault(tc => tc.ClassType.Symbol.Crc64 == symbol.Crc64); - - /// - public override void Serialize(ref bool value) - { - value = Reader.ReadChar() switch - { - '1' => true, - '0' => false, - _ => throw new InvalidBooleanException($"Invalid boolean at position: {Reader.BaseStream.Position}!") - }; - } - - /// - public override void Serialize(ref float value) => value = Reader.ReadSingle(); - - /// - public override void Serialize(ref double value) => value = Reader.ReadDouble(); - - /// - public override void Serialize(ref short value) => value = Reader.ReadInt16(); - - /// - public override void Serialize(ref int value) => value = Reader.ReadInt32(); - - /// - public override void Serialize(ref long value) => value = Reader.ReadInt64(); - - /// - public override void Serialize(ref ushort value) => value = Reader.ReadUInt16(); - - /// - public override void Serialize(ref uint value) => value = Reader.ReadUInt32(); - - /// - public override void Serialize(ref ulong value) => value = Reader.ReadUInt64(); - - /// - public override void Serialize(ref string value) - { - int strLength = Reader.ReadInt32(); - - if (strLength < 0) - { - throw new ArgumentOutOfRangeException(nameof(strLength)); - } - - var buffer = new byte[strLength]; - int realLength = Reader.Read(buffer, 0, strLength); - - value = Encoding.UTF8.GetString(buffer, 0, realLength); - } - - public override void Serialize(ref char value) => value = Reader.ReadChar(); - - public override void Serialize(ref byte value) => value = Reader.ReadByte(); - - public override void Serialize(ref sbyte value) => value = Reader.ReadSByte(); - - public override void Serialize(ref Symbol value) - { - ulong crc64 = Reader.ReadUInt64(); - value = Symbol.FromCrc64(crc64); - Configuration.SerializedSymbols.Add(value); - } - - public override void Serialize(ref MetaClassType value) - { - if (Configuration.AreSymbolsHashed) - { - ulong hash = this.ReadUInt64(); - MetaClassType? type = MetaClassTypeRegistry.GetByHash(hash); - - if (type == null) - { - Toolkit.Instance.Logger.LogWarning($"[MetaStreamReader] Unknown MetaClassType: {hash:X16})"); - Configuration.UnregisteredTypes.Add((hash, 0)); - } - else - { - value = type; - } - } - else - { - string fullType = this.ReadString(); - value = MetaClassTypeRegistry.GetByName(MetaClassType.GetStrippedTypeName(fullType)); - // I don't check for unregistered types in old games. Since types are unhashed, they are probably all registered. :) - } - } - - public override void Serialize(byte[] values, int offset, int count) => Reader.Read(values, offset, count); -} diff --git a/src/TelltaleToolKit/Serialization/Binary/MetaStreamVersion.cs b/src/TelltaleToolKit/Serialization/Binary/MetaStreamVersion.cs deleted file mode 100644 index 03c89aa..0000000 --- a/src/TelltaleToolKit/Serialization/Binary/MetaStreamVersion.cs +++ /dev/null @@ -1,18 +0,0 @@ -namespace TelltaleToolKit.Serialization.Binary; - -public enum MetaStreamVersion : uint -{ - Mbin = 0x4D42494E, - Mbes = 0x4D424553, - Mtre = 0x4D545245, - Mcom = 0x4D434F4D, - Msv4 = 0x4D535356, - Msv5 = 0x4D535635, - Msv6 = 0x4D535636, - - // I have no idea why Telltale got these values - Unknown1 = 0xFB4A1764, - Unknown2 = 0xEB794091, - Unknown3 = 0x64AFDEFB, - Unknown4 = 0x64AFDEAA, -} \ No newline at end of file diff --git a/src/TelltaleToolKit/Serialization/Binary/MetaStreamVersionExtensions.cs b/src/TelltaleToolKit/Serialization/Binary/MetaStreamVersionExtensions.cs deleted file mode 100644 index 96c1695..0000000 --- a/src/TelltaleToolKit/Serialization/Binary/MetaStreamVersionExtensions.cs +++ /dev/null @@ -1,40 +0,0 @@ -namespace TelltaleToolKit.Serialization.Binary; - -public static class StreamVersionExtensions -{ - /// - /// Converts a string to StreamVersion. - /// - public static MetaStreamVersion StreamVersionFromString(this string value) - { - return value switch - { - "MBIN" => MetaStreamVersion.Mbin, - "MBES" => MetaStreamVersion.Mbes, - "MTRE" => MetaStreamVersion.Mtre, - "MCOM" => MetaStreamVersion.Mcom, - "MSV4" => MetaStreamVersion.Msv4, - "MSV5" => MetaStreamVersion.Msv5, - "MSV6" => MetaStreamVersion.Msv6, - _ => throw new ArgumentOutOfRangeException(nameof(value), value, null) - }; - } - - /// - /// Converts a StreamVersion to its string name. - /// - public static string ToJsonString(this MetaStreamVersion version) - { - return version switch - { - MetaStreamVersion.Mbin => "MBIN", - MetaStreamVersion.Mbes => "MBES", - MetaStreamVersion.Mcom => "MCOM", - MetaStreamVersion.Mtre => "MTRE", - MetaStreamVersion.Msv4 => "MSV4", - MetaStreamVersion.Msv5 => "MSV5", - MetaStreamVersion.Msv6 => "MSV6", - _ => string.Empty - }; - } -} \ No newline at end of file diff --git a/src/TelltaleToolKit/Serialization/Binary/MetaStreamWriter.cs b/src/TelltaleToolKit/Serialization/Binary/MetaStreamWriter.cs deleted file mode 100644 index 26bc12e..0000000 --- a/src/TelltaleToolKit/Serialization/Binary/MetaStreamWriter.cs +++ /dev/null @@ -1,183 +0,0 @@ -using System.Text; -using TelltaleToolKit.Reflection; -using TelltaleToolKit.T3Types; - -namespace TelltaleToolKit.Serialization.Binary; - -public sealed class MetaStreamWriter : MetaStream -{ - public MetaStreamWriter(Stream inputStream, MetaStreamConfiguration configuration) - { - UnderlyingStream = inputStream; - - Sections[(int)SectionType.Main].Stream = new MemoryStream(); - Sections[(int)SectionType.Debug].Stream = new MemoryStream(); - Sections[(int)SectionType.Async].Stream = new MemoryStream(); - Configuration = configuration; - - InitSerializer(); - } - - private BinaryWriter Writer { get; set; } = null!; - - public void Save() - { - SerializeMetaHeader(); - - foreach (MetaSection section in Sections) - { - section.Stream.Seek(0, SeekOrigin.Begin); - section.Stream.CopyTo(UnderlyingStream); - } - - // Truncate, otherwise there are some leftover bytes. - UnderlyingStream.SetLength(UnderlyingStream.Position); - } - - public override void SerializeMetaHeader() - { - Writer = new BinaryWriter(UnderlyingStream); - Writer.BaseStream.Seek(0, SeekOrigin.Begin); - - if (!Enum.IsDefined(typeof(MetaStreamVersion), Configuration.Version)) - throw new InvalidDataException("Version not defined"); - - this.Write((uint)Configuration.Version); - - switch (Configuration.Version) - { - case MetaStreamVersion.Mbes: - throw new NotSupportedException("MBES is not supported yet."); - // TODO: optional compressed sections (set top bit of size to 1) - case MetaStreamVersion.Msv6 - or MetaStreamVersion.Msv5: - this.Write((int)Sections[(int)SectionType.Main].Stream.Length); - this.Write((int)Sections[(int)SectionType.Debug].Stream.Length); - this.Write((int)Sections[(int)SectionType.Async].Stream.Length); - break; - } - - Configuration.SerializedClasses = Configuration.SerializedClasses.Distinct().ToList(); - - this.Write(Configuration.SerializedClasses.Count); - - foreach (MetaClass t in Configuration.SerializedClasses) - { - MetaClassType metaClassType = t.ClassType; - Serialize(ref metaClassType); - this.Write(t.Crc32); - } - } - - protected override void InitSerializer() => Writer = new BinaryWriter(CurrentSubstream, Encoding.UTF8, true); - - public override void BeginBlock() - { - GetCurrentSection().Blocks.Push(GetCurrentSection().Stream.Position); - this.Write(0); - } - - public override void EndBlock() - { - long position = GetCurrentSection().Blocks.Pop(); - - var blockSize = (int)(GetCurrentSection().Stream.Position - position); - long currentPosition = GetCurrentSection().Stream.Position; - - GetCurrentSection().Stream.Seek(position, SeekOrigin.Begin); - this.Write(blockSize); - GetCurrentSection().Stream.Seek(currentPosition, SeekOrigin.Begin); - } - - public override MetaClass? GetMetaClass(Type type) - { - // Do not modify the metaclass descriptions. This operation is not recommended for complex types, especially those which have PropertySet (properties). - if (!Configuration.CanModifySerializedClassesList) - { - return Configuration.SerializedClasses.FirstOrDefault(tc => tc.ClassType.LinkingType == type); - } - - return Configuration.Workspace?.GetMetaClassDescription(type); - } - - public override MetaClass? GetMetaClass(Symbol symbol) - { - // Do not modify the metaclass descriptions. This operation is not recommended for complex types, especially those which have PropertySet (properties). - if (!Configuration.CanModifySerializedClassesList) - { - return Configuration.SerializedClasses.FirstOrDefault(tc => tc.ClassType.Symbol.Crc64 == symbol.Crc64); - } - - return Configuration.Workspace?.GetMetaClassDescription(symbol); - } - - public override void Serialize(ref bool value) => Writer.Write(value ? '1' : '0'); - - public override void Serialize(ref float value) => Writer.Write(value); - - public override void Serialize(ref double value) => Writer.Write(value); - - public override void Serialize(ref short value) => Writer.Write(value); - - public override void Serialize(ref int value) => Writer.Write(value); - - public override void Serialize(ref long value) => Writer.Write(value); - - public override void Serialize(ref ushort value) => Writer.Write(value); - - public override void Serialize(ref uint value) => Writer.Write(value); - - public override void Serialize(ref ulong value) => Writer.Write(value); - - public override void Serialize(ref string value) - { - byte[] strBytes = Encoding.UTF8.GetBytes(value); - Writer.Write(strBytes.Length); - Writer.Write(strBytes); - } - - public override void Serialize(ref char value) => Writer.Write(value); - - public override void Serialize(ref byte value) => Writer.Write(value); - - public override void Serialize(ref sbyte value) => Writer.Write(value); - - public override void Serialize(ref MetaClassType value) - { - if (Configuration.AreSymbolsHashed) - { - this.Write(value.Symbol.Crc64); - } - else - { - this.Write(value.FullTypeName); - } - } - - public override void Serialize(byte[] values, int offset, int count) => Writer.Write(values, offset, count); - - public override void Serialize(ref Symbol value) - { - if (Configuration.AreSymbolsHashed) - { - MetaClass? description = GetMetaClass(typeof(Symbol)); - AddVersionInfo(description); - Writer.Write(value.Crc64); - } - else - { - string valueSymbolName = value.DebugString; - Serialize(ref valueSymbolName); - } - } - - public void AddVersionInfo(MetaClass? desc) - { - if (desc is null || Configuration.SerializedClasses.Contains(desc)) - { - return; - } - - Configuration.SerializedClasses.Add(desc); - } -} diff --git a/src/TelltaleToolKit/Serialization/Binary/StreamExtensions.cs b/src/TelltaleToolKit/Serialization/Binary/StreamExtensions.cs deleted file mode 100644 index 40c59e5..0000000 --- a/src/TelltaleToolKit/Serialization/Binary/StreamExtensions.cs +++ /dev/null @@ -1,17 +0,0 @@ -namespace TelltaleToolKit.Serialization.Binary; - -/// -/// Provides extension methods. -/// -public static class StreamExtensions -{ - /// - /// Returns a substream instance sourcing from this stream at the specified offset with the specified length. - /// - /// The sourcing stream. - /// The offset. - /// The length. - /// A substream. - public static SubStream Substream(this Stream stream, long offset, long length) => - new SubStream(stream, offset, length); -} \ No newline at end of file diff --git a/src/TelltaleToolKit/TelltaleArchives/ContainerMagic.cs b/src/TelltaleToolKit/Serialization/ContainerMagic.cs similarity index 95% rename from src/TelltaleToolKit/TelltaleArchives/ContainerMagic.cs rename to src/TelltaleToolKit/Serialization/ContainerMagic.cs index 651a467..4a984ab 100644 --- a/src/TelltaleToolKit/TelltaleArchives/ContainerMagic.cs +++ b/src/TelltaleToolKit/Serialization/ContainerMagic.cs @@ -1,4 +1,4 @@ -namespace TelltaleToolKit.TelltaleArchives; +namespace TelltaleToolKit.Serialization; /// /// Four‑byte magic numbers identifying the container format of a file. diff --git a/src/TelltaleToolKit/TelltaleArchives/IO/ContainerStream.cs b/src/TelltaleToolKit/Serialization/ContainerStream.cs similarity index 75% rename from src/TelltaleToolKit/TelltaleArchives/IO/ContainerStream.cs rename to src/TelltaleToolKit/Serialization/ContainerStream.cs index 2b0e9ee..c236c4c 100644 --- a/src/TelltaleToolKit/TelltaleArchives/IO/ContainerStream.cs +++ b/src/TelltaleToolKit/Serialization/ContainerStream.cs @@ -1,9 +1,11 @@ -using System.Text; -using TelltaleToolKit.TelltaleArchives.Caching; -using TelltaleToolKit.TelltaleArchives.Formats; +using System.IO.Compression; +using System.Text; +using ICSharpCode.SharpZipLib.Zip.Compression.Streams; using TelltaleToolKit.Utility.Blowfish; +using TelltaleToolKit.Utility.Caching; +using TelltaleToolKit.Utility.Compression; -namespace TelltaleToolKit.TelltaleArchives.IO; +namespace TelltaleToolKit.Serialization; /// /// A forward-readable, seekable that decodes the outer @@ -12,16 +14,11 @@ namespace TelltaleToolKit.TelltaleArchives.IO; /// /// /// Single responsibility: parse the container header and expose the -/// inner TTA2/3/4 payload as a readable stream so can walk +/// inner TTA2/3/4 payload as a readable stream so can walk /// the entry table and name-page stream. It is disposed immediately after that forward /// pass — extraction never goes through it. /// /// -/// No caching. Header parsing is a single sequential forward pass over -/// a tiny fraction of the archive. Decoding a chunk once and discarding it is always -/// enough. This avoids any heap accumulation for 2 GB+ archives. -/// -/// /// O(1) chunk offset lookup. A prefix-sum table () /// is built once from the chunk-size array during construction, so seeking to any chunk /// does not require walking the size array. @@ -33,7 +30,7 @@ public sealed class ContainerStream : Stream private readonly IChunkCache _cache; // Prefix-sum table: _chunkPrefixSums[i] = sum of compressed sizes of chunks 0..i-1. - // Length = ChunkCount + 1 so _chunkPrefixSums[ChunkCount] = total compressed payload size. + // Length = NumPages + 1 so _chunkPrefixSums[NumPages] = total compressed payload size. // Built once during construction for O(1) chunk-offset lookup. private readonly ulong[] _chunkPrefixSums; // ------------------------------------------------------------------------- @@ -65,60 +62,37 @@ public ContainerStream(Stream source, string blowfishKey, IChunkCache? cache = n // ---- Container magic ---- ContainerMagic = (ContainerMagic)reader.ReadUInt32(); - ArchiveFlags flags = ArchiveFlags.None; - - switch (ContainerMagic) + if (!Enum.IsDefined(typeof(ContainerMagic), ContainerMagic)) { - case ContainerMagic.TTCN: - // No encryption, no compression — nothing to add. - break; - - case ContainerMagic.TTCE: - flags |= ArchiveFlags.IsEncrypted | ArchiveFlags.IsRawDeflateCompressed; - break; - - case ContainerMagic.TTCZ: - flags |= ArchiveFlags.IsRawDeflateCompressed; - break; - - case ContainerMagic.TTCe: - case ContainerMagic.TTCz: - { - if (ContainerMagic is ContainerMagic.TTCe) - { - flags |= ArchiveFlags.IsEncrypted; - } - - uint compressionType = reader.ReadUInt32(); - flags |= compressionType switch - { - 0 => ArchiveFlags.IsRawDeflateCompressed, - 1 => ArchiveFlags.IsOodleCompressed, - _ => throw new NotSupportedException( - $"[ContainerStream] Unknown compression type {compressionType}.") - }; - break; - } - - default: - throw new NotSupportedException( - $"[ContainerStream] Unknown container version 0x{(uint)ContainerMagic:X8}."); + throw new NotSupportedException($"[ContainerStream] Unknown container magic {ContainerMagic}."); } - Flags = flags; + Compression algorithm = ContainerMagic switch + { + ContainerMagic.TTCN => Compression.None, + ContainerMagic.TTCe or ContainerMagic.TTCz => ReadCompressionType(reader), + _ => Compression.Deflate + }; + + Params = new ContainerStreamParams + { + Encrypt = ContainerMagic is ContainerMagic.TTCE or ContainerMagic.TTCe, + BlowfishKey = blowfishKey, + Algorithm = algorithm + }; - if (flags.IsCompressed()) + if (Params.Algorithm != Compression.None) { - ChunkSize = reader.ReadUInt32(); + WindowSize = reader.ReadUInt32(); - ChunkCount = reader.ReadUInt32(); - ChunkBlockSizes = new ulong[ChunkCount]; + NumPages = reader.ReadUInt32(); + ChunkBlockSizes = new ulong[NumPages]; - // The on-disk table stores ChunkCount + 1 cumulative absolute offsets. + // The on-disk table stores NumPages + 1 cumulative absolute offsets. // Element 0 is the start of the first chunk (always 0 relative to payload start). // We derive per-chunk sizes from successive differences. ulong prev = reader.ReadUInt64(); // always 0 - for (int i = 0; i < ChunkCount; i++) + for (int i = 0; i < NumPages; i++) { ulong next = reader.ReadUInt64(); ChunkBlockSizes[i] = next - prev; @@ -126,9 +100,9 @@ public ContainerStream(Stream source, string blowfishKey, IChunkCache? cache = n } // Build prefix-sum table for O(1) chunk-offset lookup. - _chunkPrefixSums = BuildPrefixSums(ChunkBlockSizes, ChunkCount); + _chunkPrefixSums = BuildPrefixSums(ChunkBlockSizes, NumPages); - Length = ChunkCount * ChunkSize; + Length = NumPages * WindowSize; } else { @@ -140,6 +114,18 @@ public ContainerStream(Stream source, string blowfishKey, IChunkCache? cache = n PayloadStart = source.Position; // first byte of TTA2/3/4 payload } + private static Compression ReadCompressionType(BinaryReader reader) + { + uint compressionType = reader.ReadUInt32(); + return compressionType switch + { + 0 => Compression.Deflate, + 1 => Compression.Oodle, + _ => throw new NotSupportedException( + $"[ContainerStream] Unknown compression type {compressionType}.") + }; + } + // ------------------------------------------------------------------------- // Properties exposed to TTArchive2 // ------------------------------------------------------------------------- @@ -148,13 +134,13 @@ public ContainerStream(Stream source, string blowfishKey, IChunkCache? cache = n public ContainerMagic ContainerMagic { get; } /// Encryption and compression flags derived from the container magic. - public ArchiveFlags Flags { get; } + public ContainerStreamParams Params { get; } /// Decompressed size of each chunk in bytes. Zero for uncompressed containers. - public uint ChunkSize { get; } + public uint WindowSize { get; } /// Number of compressed chunks. Zero for uncompressed containers. - public uint ChunkCount { get; } + public uint NumPages { get; } /// Compressed byte-length of each chunk. Empty for uncompressed containers. public ulong[] ChunkBlockSizes { get; } @@ -222,7 +208,7 @@ public override int Read(Span buffer) } // ---- Uncompressed — backed by MemoryStream ---- - if (!Flags.IsCompressed()) + if (Params.Algorithm is Compression.None) { _source.Position = PayloadStart + _position; int bytesToRead = (int)Math.Min(buffer.Length, Length - _position); @@ -238,7 +224,7 @@ public override int Read(Span buffer) return 0; } - if (Flags.IsEncrypted()) + if (Params.Encrypt) { Blowfish bf = new(_blowfishKey, 7); bf.Decipher(temp, read); @@ -256,8 +242,8 @@ public override int Read(Span buffer) while (totalRead < buffer.Length) { - int chunkIndex = (int)(_position / ChunkSize); - if ((uint)chunkIndex >= ChunkCount) + int chunkIndex = (int)(_position / WindowSize); + if ((uint)chunkIndex >= NumPages) { break; // past end of payload } @@ -265,7 +251,7 @@ public override int Read(Span buffer) // Use cache to get decoded chunk byte[] decoded = _cache.GetOrDecode(chunkIndex, DecodeChunk); - int inChunk = (int)(_position % ChunkSize); + int inChunk = (int)(_position % WindowSize); int avail = decoded.Length - inChunk; if (avail <= 0) { @@ -290,8 +276,44 @@ private byte[] DecodeChunk(int chunkIndex) byte[] compressed = new byte[ChunkBlockSizes[chunkIndex]]; ChunkDecoder.ReadExact(_source, compressed); - ArchiveInfo tempInfo = new() { Flags = Flags, BlowfishKey = _blowfishKey }; - return ChunkDecoder.DecryptAndDecompress(compressed, ChunkSize, tempInfo); + if (Params.Encrypt) + { + Blowfish bf = new(Params.BlowfishKey, 7); + bf.Decipher(compressed.AsSpan(), compressed.Length); + } + + return DecompressBlock(compressed, (int)WindowSize, Params.Algorithm); + } + + public static byte[] DecompressBlock(byte[] compressedData, int expectedSize, + Compression compression) + { + // This is a hack? In TWD:DE, 401_txmesh, there's a page which is the same size as the expected size (65535) + // C#'s raw deflate fails. + // ttarchext returns the same data: + // https://github.com/infernokun/TelltaleGamesExtractionGUI/blob/fec9fc1a70b545bcc67062fedc3fe3f7cd0d3e1b/bin/Debug/net6.0-windows/ttarchext/ttarchext.c#L1512 + if (compressedData.Length == expectedSize) + { + return compressedData; + } + + return compression switch + { + Compression.Deflate => Decompress(ms => new DeflateStream(ms, CompressionMode.Decompress)), + Compression.Zlib => Decompress(ms => new InflaterInputStream(ms)), + Compression.Oodle => throw new NotSupportedException("Oodle compression is not supported yet."), + // No compression, return the original data + _ => compressedData + }; + + // Local function to handle decompression + byte[] Decompress(Func streamFactory) + { + using MemoryStream outputStream = new(expectedSize); + using Stream decompressStream = streamFactory(new MemoryStream(compressedData)); + decompressStream.CopyTo(outputStream); + return outputStream.ToArray(); + } } // ------------------------------------------------------------------------- @@ -332,6 +354,8 @@ private static ulong[] BuildPrefixSums(ulong[] sizes, uint count) return sums; } + + /// /// Writes a Telltale Tool Container (TTC*) around the given payload. /// Automatically selects the magic number based on encryption and compression settings. @@ -339,7 +363,7 @@ private static ulong[] BuildPrefixSums(ulong[] sizes, uint count) /// Stream to write the container to. Will be left open. /// Uncompressed, unencrypted inner data (TTA header + files). /// Compression, encryption, chunk size, Blowfish key, etc. - public static void Create(Stream destination, ReadOnlySpan payload, ArchiveWriteOptions options) + public static void Create(Stream destination, ReadOnlySpan payload, ContainerStreamParams options) { using MemoryStream ms = new(payload.ToArray()); Create(destination, ms, options); @@ -353,9 +377,9 @@ public static void Create(Stream destination, ReadOnlySpan payload, Archiv /// Stream containing the uncompressed inner payload. Must support seeking and provide a correct Length. /// /// Archive options (compression, encryption, chunk size, key). - public static void Create(Stream destination, Stream payloadStream, ArchiveWriteOptions options) + public static void Create(Stream destination, Stream payloadStream, ContainerStreamParams options) { - if (options.Algorithm == CompressionAlgorithm.Oodle) + if (options.Algorithm == Compression.Oodle) { throw new NotSupportedException("Oodle compression not yet implemented."); } @@ -363,12 +387,12 @@ public static void Create(Stream destination, Stream payloadStream, ArchiveWrite // Choose magic automatically ContainerMagic magic = (options.Encrypt, options.Algorithm) switch { - (false, CompressionAlgorithm.None) => ContainerMagic.TTCN, - (true, CompressionAlgorithm.Deflate) => ContainerMagic.TTCE, - (false, CompressionAlgorithm.Deflate) => ContainerMagic.TTCZ, - (true, CompressionAlgorithm.Oodle) => ContainerMagic + (false, Compression.None) => ContainerMagic.TTCN, + (true, Compression.Deflate) => ContainerMagic.TTCE, + (false, Compression.Deflate) => ContainerMagic.TTCZ, + (true, Compression.Oodle) => ContainerMagic .TTCe, // encryption without compression? Rare, but fallback - (false, CompressionAlgorithm.Oodle) => ContainerMagic + (false, Compression.Oodle) => ContainerMagic .TTCz, // encryption without compression? Rare, but fallback _ => throw new NotSupportedException( $"Unsupported combination: Encrypt={options.Encrypt}, Algorithm={options.Algorithm}") @@ -379,11 +403,11 @@ public static void Create(Stream destination, Stream payloadStream, ArchiveWrite if (magic is ContainerMagic.TTCe or ContainerMagic.TTCz) { - uint comprType = options.Algorithm == CompressionAlgorithm.Deflate ? 0u : 1u; // 1 = Oodle + uint comprType = options.Algorithm == Compression.Deflate ? 0u : 1u; // 1 = Oodle writer.Write(comprType); } - bool compress = options.Algorithm != CompressionAlgorithm.None; + bool compress = options.Algorithm != Compression.None; Blowfish blowfish = new(options.BlowfishKey, 7); if (!compress) @@ -429,7 +453,7 @@ public static void Create(Stream destination, Stream payloadStream, ArchiveWrite throw new EndOfStreamException("Unexpected end of payload stream."); } - int compressedLen = TelltaleArchiveUtilities.CompressBlock(chunk, options.Algorithm, compressedBuffer); + int compressedLen = Utility.Compression.ChunkDecoder.CompressBlock(chunk, options.Algorithm, compressedBuffer); byte[] finalData = compressedBuffer.AsSpan(0, compressedLen).ToArray(); if (options.Encrypt) { diff --git a/src/TelltaleToolKit/Serialization/ContainerStreamParams.cs b/src/TelltaleToolKit/Serialization/ContainerStreamParams.cs new file mode 100644 index 0000000..4b8fa81 --- /dev/null +++ b/src/TelltaleToolKit/Serialization/ContainerStreamParams.cs @@ -0,0 +1,28 @@ +using TelltaleToolKit.Utility.Compression; + +namespace TelltaleToolKit.Serialization; + +/// +/// Parameters for creating a Telltale Tool Container stream. +/// Mirrors the engine's DataStreamContainerParams. +/// +public sealed class ContainerStreamParams +{ + /// Decompressed size of each chunk. Defaults to 0x10000 (64 KB). + public uint ChunkSize { get; init; } = 0x10000; + + /// Whether to encrypt compressed chunks with Blowfish. + public bool Encrypt { get; init; } + + /// + /// The compression algorithm used to compress the container. + /// Defaults to Deflate (ZLIB in Telltale's terms). + /// + public Compression Algorithm { get; init; } = Compression.Deflate; + + /// + /// Blowfish key used when is true. + /// Kept separate from pure format parameters — pass string.Empty when not encrypting. + /// + public string BlowfishKey { get; init; } = string.Empty; +} diff --git a/src/TelltaleToolKit/Serialization/Json/JsonMetaStreamReader.cs b/src/TelltaleToolKit/Serialization/Json/JsonMetaStreamReader.cs new file mode 100644 index 0000000..3502c84 --- /dev/null +++ b/src/TelltaleToolKit/Serialization/Json/JsonMetaStreamReader.cs @@ -0,0 +1,371 @@ +using System.Text.Json; +using TelltaleToolKit.T3Types; + +namespace TelltaleToolKit.Serialization.Json; + +/// +/// Reads a JSON file written by . +/// The file must be an array with two elements: header object and payload object. +/// +public sealed class JsonMetaStreamReader : MetaStream +{ + private readonly JsonDocument? _document; + private readonly JsonElement _current; + private readonly Stack _frames = new(); + private bool _closed; + + // Tracks whether the next value read should be preceded by a key. + // In JSON, keys are consumed by Key(), values by Serialize(). + private bool _expectKey; + + public JsonMetaStreamReader(Stream inputStream, MetaStreamParams @params) + { + Stream inputStream1 = inputStream ?? throw new ArgumentNullException(nameof(inputStream)); + Params = @params; + + // Load the entire JSON document (we need random access for header & payload) + _document = JsonDocument.Parse(inputStream1); + JsonElement root = _document.RootElement; + + // Expect an array with exactly two elements + if (root.ValueKind != JsonValueKind.Array || root.GetArrayLength() != 2) + throw new InvalidDataException("JSON root must be an array of [header, payload]."); + + JsonElement header = root[0]; + JsonElement payload = root[1]; + + // Parse header to populate MetaStreamParams.VersionInfo + ParseHeader(header); + + // Start reading the payload + _current = payload; + _frames.Clear(); + _expectKey = false; + } + + private void ParseHeader(JsonElement header) + { + if (!header.TryGetProperty("_metaVersionInfo", out JsonElement versionInfoElement)) + return; // no version info, ignore + + if (!versionInfoElement.TryGetProperty("mVersionInfo", out JsonElement arrayElement)) + return; + + if (arrayElement.ValueKind != JsonValueKind.Array) + return; + + var versionInfoList = new List(); + foreach (JsonElement item in arrayElement.EnumerateArray()) + { + if (!item.TryGetProperty("mTypeSymbolCrc", out JsonElement crcElem) || + !item.TryGetProperty("mVersionCrc", out JsonElement versionCrcElem)) + continue; + + ulong typeCrc = crcElem.GetUInt64(); + uint versionCrc = versionCrcElem.GetUInt32(); + + versionInfoList.Add(new MetaVersionInfo { TypeSymbolCrc = typeCrc, VersionCrc = versionCrc }); + } + + Params.VersionInfo.Clear(); + Params.VersionInfo.AddRange(versionInfoList); + } + + public override MetaStreamMode Mode => MetaStreamMode.Read; + + protected override void SetSection(SectionType section) + { + } + + public override void BeginAsyncSection() + { + } + + public override void EndAsyncSection() + { + } + + public override void BeginDebugSection() + { + } + + public override void EndDebugSection() + { + } + + public override void BeginObject(string name, bool isArray = false) + { + if (_frames.Count == 0) + { + // Root – we are already at the payload object, just push it + _frames.Push(new ReaderFrame(_current, isArray)); + _expectKey = false; + return; + } + + ReaderFrame currentFrame = _frames.Peek(); + + if (currentFrame.IsArray) + { + // Inside an array: read the next element (the array itself must be an object) + JsonElement child = currentFrame.GetNextArrayElement(); + if (isArray) + { + if (child.ValueKind != JsonValueKind.Array) + throw new InvalidDataException("Expected JSON array."); + } + else + { + if (child.ValueKind != JsonValueKind.Object) + throw new InvalidDataException("Expected JSON object."); + } + + _frames.Push(new ReaderFrame(child, isArray)); + _expectKey = false; + } + else + { + // Inside an object: consume a property with the given name + if (!currentFrame.TryGetProperty(name, out JsonElement child)) + throw new InvalidDataException($"Property '{name}' not found in JSON object."); + + if (isArray) + { + if (child.ValueKind != JsonValueKind.Array) + throw new InvalidDataException($"Property '{name}' is not a JSON array."); + } + else + { + if (child.ValueKind != JsonValueKind.Object) + throw new InvalidDataException($"Property '{name}' is not a JSON object."); + } + + _frames.Push(new ReaderFrame(child, isArray)); + _expectKey = false; + } + } + + public override void EndObject(string name) + { + if (_frames.Count == 0) + throw new InvalidOperationException("EndObject called without matching BeginObject."); + _frames.Pop(); + _expectKey = false; + } + + public override void Key(string name) + { + if (_frames.Count == 0) + throw new InvalidOperationException("Key called outside of an object."); + ReaderFrame frame = _frames.Peek(); + if (frame.IsArray) + return; // keys are ignored inside arrays (writer never calls Key there) + + // Verify that the next property name matches the expected name + if (!frame.TryGetNextProperty(out string actualName)) + throw new InvalidDataException("No more properties in current object."); + + if (actualName != name) + throw new InvalidDataException($"Expected property '{name}' but found '{actualName}'."); + + _expectKey = true; + } + + private void EnsureValue() + { + if (!_expectKey) + throw new InvalidOperationException("Key must be called before reading a value."); + _expectKey = false; + } + + private JsonElement GetCurrentValue() + { + ReaderFrame frame = _frames.Peek(); + if (frame.IsArray) + return frame.GetNextArrayElement(); + else + return frame.GetCurrentValue(); // after Key, we already advanced to the value + } + + public override void Serialize(ref bool value) + { + EnsureValue(); + value = GetCurrentValue().GetBoolean(); + } + + public override void Serialize(ref float value) + { + EnsureValue(); + value = (float)GetCurrentValue().GetDouble(); + } + + public override void Serialize(ref double value) + { + EnsureValue(); + value = GetCurrentValue().GetDouble(); + } + + public override void Serialize(ref short value) + { + EnsureValue(); + value = GetCurrentValue().GetInt16(); + } + + public override void Serialize(ref int value) + { + EnsureValue(); + value = GetCurrentValue().GetInt32(); + } + + public override void Serialize(ref long value) + { + EnsureValue(); + value = GetCurrentValue().GetInt64(); + } + + public override void Serialize(ref ushort value) + { + EnsureValue(); + value = GetCurrentValue().GetUInt16(); + } + + public override void Serialize(ref uint value) + { + EnsureValue(); + value = GetCurrentValue().GetUInt32(); + } + + public override void Serialize(ref ulong value) + { + EnsureValue(); + value = GetCurrentValue().GetUInt64(); + } + + public override void Serialize(ref string value) + { + EnsureValue(); + value = GetCurrentValue().GetString() ?? string.Empty; + } + + public override void Serialize(ref char value) + { + EnsureValue(); + string s = GetCurrentValue().GetString() ?? string.Empty; + value = s.Length > 0 ? s[0] : '\0'; + } + + public override void Serialize(ref byte value) + { + EnsureValue(); + value = GetCurrentValue().GetByte(); + } + + public override void Serialize(ref sbyte value) + { + EnsureValue(); + value = GetCurrentValue().GetSByte(); + } + + public override void Serialize(ref Symbol value) + { + EnsureValue(); + string str = GetCurrentValue().GetString() ?? ""; + // Try to parse as hex CRC, otherwise treat as debug string + if (str.StartsWith("0x", StringComparison.OrdinalIgnoreCase) && + ulong.TryParse(str.AsSpan(2), System.Globalization.NumberStyles.HexNumber, null, out ulong crc)) + { + value = Symbol.FromCrc64(crc); + } + else + { + value = Symbol.FromName(str); + } + + Params.SerializedSymbols.Add(value); + } + + public override void Serialize(byte[] values, int offset, int count) + { + EnsureValue(); + byte[] base64 = GetCurrentValue().GetBytesFromBase64(); + if (base64.Length < offset + count) + throw new InvalidDataException("Base64 data too short."); + Array.Copy(base64, offset, values, 0, count); + } + + + public override void BeginBlock() + { + } + + public override void EndBlock() + { + } + + public override void Close() + { + if (_closed) return; + _document?.Dispose(); + _closed = true; + } + + protected override void Dispose(bool disposing) + { + if (disposing && !_closed) + Close(); + base.Dispose(disposing); + } + + + private class ReaderFrame + { + private JsonElement Element { get; } + public bool IsArray { get; } + private readonly IEnumerator? _propertyEnumerator; + private readonly IEnumerator? _arrayEnumerator; + private JsonElement _currentValue; // after Key() we store the value + + public ReaderFrame(JsonElement element, bool isArray) + { + Element = element; + IsArray = isArray; + if (IsArray) + _arrayEnumerator = element.EnumerateArray().GetEnumerator(); + else + _propertyEnumerator = element.EnumerateObject().GetEnumerator(); + } + + public bool TryGetProperty(string name, out JsonElement child) + { + if (IsArray) + throw new InvalidOperationException("Cannot get property by name from an array."); + return Element.TryGetProperty(name, out child); + } + + public bool TryGetNextProperty(out string name) + { + if (IsArray || _propertyEnumerator == null) + throw new InvalidOperationException("Not in an object."); + if (_propertyEnumerator.MoveNext()) + { + name = _propertyEnumerator.Current.Name; + _currentValue = _propertyEnumerator.Current.Value; + return true; + } + + name = null!; + return false; + } + + public JsonElement GetCurrentValue() => _currentValue; + + public JsonElement GetNextArrayElement() + { + if (!IsArray || _arrayEnumerator == null) + throw new InvalidOperationException("Not in an array."); + if (!_arrayEnumerator.MoveNext()) + throw new InvalidDataException("Unexpected end of JSON array."); + return _arrayEnumerator.Current; + } + } +} diff --git a/src/TelltaleToolKit/Serialization/Json/JsonMetaStreamWriter.cs b/src/TelltaleToolKit/Serialization/Json/JsonMetaStreamWriter.cs new file mode 100644 index 0000000..8738739 --- /dev/null +++ b/src/TelltaleToolKit/Serialization/Json/JsonMetaStreamWriter.cs @@ -0,0 +1,337 @@ +using System.Text.Json; +using TelltaleToolKit.T3Types; + +namespace TelltaleToolKit.Serialization.Json; + +/// +/// Writes a MetaStream as human-readable JSON, following the same two-part layout +/// as Telltale Tool Lib's MetaStream_JSON with minor improvements to make visualization better.: +/// +/// A header object (watermark, game ID, version info). +/// A payload object containing the serialized asset data. +/// +/// +public sealed class JsonMetaStreamWriter : MetaStream +{ + private readonly Stream _outputStream; + private bool _closed; + + // The payload writer — accumulates the asset data between Open and Close. + private readonly MemoryStream _payloadBuffer = new(0x4000); + private readonly Utf8JsonWriter _payload; + private bool _keyPending; // true after Key() until a value is writte + + // Tracks whether the current JSON container is an array (true) or object (false). + // Mirrors C++ mbObjectIsArrayStack. + private readonly Stack _objectIsArrayStack = new(); + + public JsonMetaStreamWriter(Stream outputStream, MetaStreamParams @params) + { + _outputStream = outputStream ?? throw new ArgumentNullException(nameof(outputStream)); + Params = @params; + + _payload = new Utf8JsonWriter(_payloadBuffer, new JsonWriterOptions { Indented = true }); + _payload.WriteStartObject(); // root payload object — closed in Close() + _keyPending = false; + // JSON has no real sections; we still need a non-null stream so base-class + // helpers (GetRemainingSectionBytes, etc.) don't throw. + Sections[(int)SectionType.Default].Stream = _payloadBuffer; + _currentSection = SectionType.Default; + } + + public override MetaStreamMode Mode => MetaStreamMode.Write; + + protected override void SetSection(SectionType section) + { + // Intentional no-op: JSON folds all sections into the payload object via + // BeginAsyncSection / BeginDebugSection named sub-objects. + _currentSection = section; + } + + public override void BeginAsyncSection() + { + if (Params.StreamVersion < 4) return; + if (_currentSection is SectionType.Async) return; + + _payload.WritePropertyName("Async Data"); + _payload.WriteStartObject(); + _currentSection = SectionType.Async; + } + + public override void EndAsyncSection() + { + if (_currentSection is not SectionType.Async) return; + + _payload.WriteEndObject(); + _currentSection = SectionType.Default; + } + + public override void BeginDebugSection() + { + if (Params.StreamVersion < 4) return; + if (_currentSection is SectionType.Debug) return; + + _payload.WritePropertyName("Debug Data"); + _payload.WriteStartObject(); + _currentSection = SectionType.Debug; + } + + public override void EndDebugSection() + { + if (_currentSection is not SectionType.Debug) return; + + _payload.WriteEndObject(); + _currentSection = SectionType.Default; + } + + public override void BeginObject(string name, bool isArray = false) + { + EnsureKey(); + _payload.WritePropertyName(name); + if (isArray) + _payload.WriteStartArray(); + else + _payload.WriteStartObject(); + _objectIsArrayStack.Push(isArray); + _keyPending = false; // the key has been consumed + } + + public override void EndObject(string name) + { + if (_objectIsArrayStack.Count == 0) + throw new InvalidOperationException("EndObject called without a matching BeginObject."); + bool wasArray = _objectIsArrayStack.Pop(); + if (wasArray) + _payload.WriteEndArray(); + else + _payload.WriteEndObject(); + } + + /// + /// Emits a JSON property key. Only has effect when the current container is an object. + /// Mirrors MetaStream_JSON::Key. + /// + public override void Key(string name) + { + // Inside an array context a key makes no sense — silently ignore, + // matching the C++ NeedsKey() guard. + bool insideArray = _objectIsArrayStack.Count > 0 && _objectIsArrayStack.Peek(); + if (!insideArray) + { + _payload.WritePropertyName(name); + _keyPending = true; + } + } + + public override void BeginBlock() + { + } + + public override void EndBlock() + { + } + + private void EnsureKey() + { + // Only need a key if we're inside an object (not an array) + if (_objectIsArrayStack.Count > 0 && !_objectIsArrayStack.Peek()) + { + if (!_keyPending) + { + // No key provided – write a default property name + _payload.WritePropertyName("__default"); + } + + // Key is consumed (either the real one or the default) + _keyPending = false; + } + } + + public override void Serialize(ref bool value) + { + EnsureKey(); + _payload.WriteBooleanValue(value); + _keyPending = false; + } + + public override void Serialize(ref float value) + { + EnsureKey(); + _payload.WriteNumberValue(value); + _keyPending = false; + } + + public override void Serialize(ref double value) + { + EnsureKey(); + _payload.WriteNumberValue(value); + _keyPending = false; + } + + public override void Serialize(ref short value) + { + EnsureKey(); + _payload.WriteNumberValue(value); + _keyPending = false; + } + + public override void Serialize(ref int value) + { + EnsureKey(); + _payload.WriteNumberValue(value); + _keyPending = false; + } + + public override void Serialize(ref long value) + { + EnsureKey(); + _payload.WriteNumberValue(value); + _keyPending = false; + } + + public override void Serialize(ref ushort value) + { + EnsureKey(); + _payload.WriteNumberValue(value); + _keyPending = false; + } + + public override void Serialize(ref uint value) + { + EnsureKey(); + _payload.WriteNumberValue(value); + _keyPending = false; + } + + public override void Serialize(ref ulong value) + { + EnsureKey(); + _payload.WriteNumberValue(value); + _keyPending = false; + } + + public override void Serialize(ref string value) + { + EnsureKey(); + _payload.WriteStringValue(value); + _keyPending = false; + } + + public override void Serialize(ref char value) + { + EnsureKey(); + _payload.WriteStringValue(value.ToString()); + _keyPending = false; + } + + public override void Serialize(ref byte value) + { + EnsureKey(); + _payload.WriteNumberValue(value); + _keyPending = false; + } + + public override void Serialize(ref sbyte value) + { + EnsureKey(); + _payload.WriteNumberValue(value); + _keyPending = false; + } + + /// + /// Symbols are written as strings. If a debug string is available it is used; + /// otherwise the CRC is written as a hex string, matching C++ serialize_Symbol. + /// + public override void Serialize(ref Symbol value) + { + EnsureKey(); + string display = value.DebugString ?? $"0x{value.Crc64:X16}"; + _payload.WriteStringValue(display); + Params.SerializedSymbols.Add(value); + _keyPending = false; + } + + /// + /// Binary blobs are Base64-encoded, matching C++ WriteData. + /// + public override void Serialize(byte[] values, int offset, int count) + { + EnsureKey(); + _payload.WriteBase64StringValue(values.AsSpan(offset, count)); + _keyPending = false; + } + + public override void Close() + { + if (_closed) return; + + // Finish the root payload object opened in the constructor. + _payload.WriteEndObject(); + _payload.Flush(); + _payloadBuffer.Seek(0, SeekOrigin.Begin); + + using JsonDocument payloadDoc = JsonDocument.Parse(_payloadBuffer); + using MemoryStream finalBuffer = new(0x1000); + + using (Utf8JsonWriter final = new(finalBuffer, new JsonWriterOptions { Indented = true })) + { + final.WriteStartArray(); + + final.WriteStartObject(); + // Version info — type CRC, human-readable type name, version CRC. + final.WriteString("Watermark", + $"Converted to JSON by TelltaleToolKit v{typeof(MetaStream).Assembly.GetName().Version}"); + final.WritePropertyName("_metaVersionInfo"); + final.WriteStartObject(); + { + // Skip entries whose type cannot be resolved, matching C++ behaviour. + List resolvable = Params.VersionInfo + .Where(v => v.IsTypeRegistered()) + .ToList(); + + final.WriteNumber("mSize", resolvable.Count); + final.WritePropertyName("mVersionInfo"); + final.WriteStartArray(); + foreach (MetaVersionInfo v in resolvable) + { + final.WriteStartObject(); + final.WriteNumber("mTypeSymbolCrc", v.TypeSymbolCrc); // ulong → JSON number + final.WriteString("Type Name", v.GetMetaClassType()!.FullTypeName); + final.WriteNumber("mVersionCrc", v.VersionCrc); + final.WriteEndObject(); + } + + final.WriteEndArray(); + } + final.WriteEndObject(); + final.WriteEndObject(); + + payloadDoc.RootElement.WriteTo(final); + + final.WriteEndArray(); // end header root + } + + if (_outputStream.CanSeek) + { + _outputStream.SetLength(0); // remove any existing content + _outputStream.Seek(0, SeekOrigin.Begin); + } + + finalBuffer.Seek(0, SeekOrigin.Begin); + finalBuffer.CopyTo(_outputStream); + + _closed = true; + Dispose(true); + } + + protected override void Dispose(bool disposing) + { + if (disposing) + { + _payload.Dispose(); + _payloadBuffer.Dispose(); + } + + base.Dispose(disposing); + } +} diff --git a/src/TelltaleToolKit/Serialization/LegacyEncryptedStream.cs b/src/TelltaleToolKit/Serialization/LegacyEncryptedStream.cs new file mode 100644 index 0000000..e39ca88 --- /dev/null +++ b/src/TelltaleToolKit/Serialization/LegacyEncryptedStream.cs @@ -0,0 +1,220 @@ +using TelltaleToolKit.Utility.Blowfish; + +namespace TelltaleToolKit.Serialization.Binary; + +/// +/// Stream wrapper that decrypts TTArchive per‑file data on the fly. +/// For simplicity, it reads the entire file into memory – these files are small +/// (config files, scripts) so the overhead is acceptable. +/// +internal sealed class LegacyEncryptedStream : Stream +{ + private readonly Stream _baseStream; + private readonly long _headerSize; + private readonly long _dataLength; + private readonly int _blockSize; + private readonly int _cryptInterval; // Blowfish every N blocks + private readonly int _cleanInterval; // skip XOR every M blocks (if not Blowfish) + private readonly Blowfish _blowfish; + + private long _position; + private int _cachedBlockIndex = -1; + private byte[]? _cachedBlockData; + private int _cachedBlockValidLength; // for last partial block + + /// + /// Initializes a new legacy encrypted stream. + /// + /// Underlying stream (contains the asset header + encrypted data). + /// MetaStreamVersion that determines the block parameters. + /// Number of bytes at the beginning of that form the asset header (e.g. the magic). This header is not decrypted and is skipped. + /// The Blowfish algorithm used for decrypting. + /// Thrown when is null. + /// Thrown when is negative or larger than the stream length. + public LegacyEncryptedStream(Stream baseStream, uint version, long headerSize, Blowfish blowfish) + : this(baseStream, GetBlockConfiguration(version), headerSize, blowfish) + { + } + + private LegacyEncryptedStream(Stream baseStream, (int blockSize, int cryptInterval, int cleanInterval) config, + long headerSize, Blowfish blowfish) + { + _baseStream = baseStream ?? throw new ArgumentNullException(nameof(baseStream)); + if (!_baseStream.CanRead) throw new NotSupportedException("Base stream must be readable."); + if (!_baseStream.CanSeek) throw new NotSupportedException("Base stream must be seekable."); + + if (headerSize < 0 || headerSize > _baseStream.Length) + throw new ArgumentException("Invalid header size", nameof(headerSize)); + + _headerSize = headerSize; + _dataLength = _baseStream.Length - headerSize; + _blockSize = config.blockSize; + _cryptInterval = config.cryptInterval; + _cleanInterval = config.cleanInterval; + _blowfish = blowfish; + _position = 0; + } + + /// + public override bool CanRead => true; + + /// + public override bool CanSeek => true; + + /// + public override bool CanWrite => false; + + /// + public override long Length => _dataLength; + + /// + public override long Position + { + get => _position; + set + { + if (value < 0 || value > _dataLength) + throw new ArgumentOutOfRangeException(nameof(value)); + _position = value; + } + } + + /// + public override int Read(byte[] buffer, int offset, int count) + => Read(buffer.AsSpan(offset, count)); + + /// + public override int Read(Span buffer) + { + if (buffer.IsEmpty) return 0; + long remaining = _dataLength - _position; + if (remaining <= 0) return 0; + int toRead = (int)Math.Min(buffer.Length, remaining); + + int totalRead = 0; + while (totalRead < toRead) + { + // Determine current block index + int blockIdx = (int)(_position / _blockSize); + int blockOffset = (int)(_position % _blockSize); + int bytesInBlock = (int)Math.Min(_blockSize - blockOffset, _dataLength - _position); + + // Get decrypted block data + EnsureBlockDecrypted(blockIdx, out byte[] blockData, out int _); + + int copyLen = Math.Min(bytesInBlock, toRead - totalRead); + blockData.AsSpan(blockOffset, copyLen).CopyTo(buffer[totalRead..]); + + totalRead += copyLen; + _position += copyLen; + } + + return totalRead; + } + + private void EnsureBlockDecrypted(int blockIdx, out byte[] blockData, out int validLength) + { + if (_cachedBlockIndex == blockIdx && _cachedBlockData != null) + { + blockData = _cachedBlockData; + validLength = _cachedBlockValidLength; + return; + } + + // Read encrypted block from base stream + long blockStartInBase = _headerSize + (long)blockIdx * _blockSize; + int blockSizeThis = _blockSize; + bool isFullBlock = true; + if (blockStartInBase + _blockSize > _baseStream.Length) + { + blockSizeThis = (int)(_baseStream.Length - blockStartInBase); + isFullBlock = false; + } + + if (blockSizeThis <= 0) + { + blockData = []; + validLength = 0; + _cachedBlockIndex = blockIdx; + _cachedBlockData = blockData; + _cachedBlockValidLength = 0; + return; + } + + _baseStream.Seek(blockStartInBase, SeekOrigin.Begin); + byte[] encrypted = new byte[blockSizeThis]; + int read = _baseStream.Read(encrypted, 0, blockSizeThis); + if (read != blockSizeThis) + throw new EndOfStreamException("Unexpected end of base stream while reading block."); + + // Only apply decryption to FULL blocks (the trailing partial block is plaintext) + if (isFullBlock) + { + if (blockIdx % _cryptInterval == 0) + { + _blowfish.Decipher(encrypted.AsSpan(), encrypted.Length); + } + else if (blockIdx % _cleanInterval != 0) // not a clean block → XOR + { + for (int i = 0; i < encrypted.Length; i++) + encrypted[i] = (byte)(encrypted[i] ^ 0xFF); + } + // else: clean block – leave as is + } + + _cachedBlockData = encrypted; + _cachedBlockValidLength = encrypted.Length; + _cachedBlockIndex = blockIdx; + blockData = encrypted; + validLength = encrypted.Length; + } + + /// + public override long Seek(long offset, SeekOrigin origin) + { + long newPos = origin switch + { + SeekOrigin.Begin => offset, + SeekOrigin.Current => _position + offset, + SeekOrigin.End => _dataLength + offset, + _ => throw new ArgumentOutOfRangeException(nameof(origin)) + }; + if (newPos < 0 || newPos > _dataLength) + throw new ArgumentOutOfRangeException(nameof(offset)); + _position = newPos; + return _position; + } + + public override void Flush() + { + } + + public override void SetLength(long value) => throw new NotSupportedException(); + public override void Write(byte[] buffer, int offset, int count) => throw new NotSupportedException(); + + /// + protected override void Dispose(bool disposing) + { + if (disposing) + { + // We do not own the base stream; do not dispose it. + _cachedBlockData = null; + } + + base.Dispose(disposing); + } + + // Helper to get block configuration from MetaStreamVersion + private static (int blockSize, int cryptInterval, int cleanInterval) GetBlockConfiguration( + uint version) + { + return version switch + { + 1 => (64, 64, 100), + 2 => (128, 32, 80), + 3 => (256, 8, 24), + _ => throw new ArgumentException($"No legacy encryption scheme defined for version {version}", + nameof(version)) + }; + } +} diff --git a/src/TelltaleToolKit/Serialization/MetaStream.cs b/src/TelltaleToolKit/Serialization/MetaStream.cs new file mode 100644 index 0000000..6cb7110 --- /dev/null +++ b/src/TelltaleToolKit/Serialization/MetaStream.cs @@ -0,0 +1,449 @@ +using System.Text; +using TelltaleToolKit.Reflection; +using TelltaleToolKit.Serialization.Binary; +using TelltaleToolKit.Serialization.Json; +using TelltaleToolKit.T3Types; + +namespace TelltaleToolKit.Serialization; + +/// +/// Stream wrapper used for serializing telltale assets. +/// A single Telltale Tool file has 1-4 stream sections (Header, Default, Debug, Async) depending on its metastream +/// version. +/// +public abstract class MetaStream : IDisposable +{ + public enum SectionType + { + Header, + Default, + Debug, + Async + } + + protected readonly SectionInfo[] Sections = + [ + new(), // Header section + new(), // Default section + new(), // Debug section + new() // Async section + ]; + + /// Gets the currently active section (Header, Default, Debug, Async). + protected SectionType _currentSection = SectionType.Header; + + public MetaStreamParams Params { get; set; } = new(); + + protected Stream BaseStream { get; set; } = null!; + + /// + /// Gets the mode of this stream (read or write). + /// + public abstract MetaStreamMode Mode { get; } + + /// Gets the currently active section. + protected SectionInfo CurrentSection => Sections[(int)_currentSection]; + + public void Dispose() + { + Dispose(true); + GC.SuppressFinalize(this); + } + + /// + /// Opens a MetaStream for reading from the specified input stream. + /// + /// The stream containing the Telltale asset. + /// Optional workspace required for legacy decryption. + /// A MetaStream configured for reading. + public static MetaStream OpenRead(Stream inputStream, Workspace? workspace = null) + => new BinaryMetaStreamReader(inputStream, workspace); + + /// + /// Opens a MetaStream for writing to the specified output stream. + /// + /// The stream that will receive the serialized data. + /// Parameters that define the version, registered classes, etc. + /// A MetaStream configured for writing. + public static MetaStream OpenWrite(Stream outputStream, MetaStreamParams configuration) + => new BinaryMetaStreamWriter(outputStream, configuration); + + /// + /// Opens a MetaStream for reading from the specified input stream. + /// + /// The stream containing the Telltale asset. + /// Parameters that define the version, registered classes, etc. + /// A MetaStream configured for writing. + public static MetaStream OpenJsonRead(Stream inputStream, MetaStreamParams configuration) + => new JsonMetaStreamReader(inputStream, configuration); + + /// + /// Opens a MetaStream for writing to the specified output stream. + /// + /// The stream that will receive the serialized data. + /// Parameters that define the version, registered classes, etc. + /// A MetaStream configured for writing. + public static MetaStream OpenJsonWrite(Stream outputStream, MetaStreamParams configuration) + => new JsonMetaStreamWriter(outputStream, configuration); + + + public virtual void BeginAsyncSection() + { + if (_currentSection is SectionType.Async || Params.StreamVersion < 4) + { + return; + } + + SetSection(SectionType.Async); + } + + public virtual void EndAsyncSection() + { + if (_currentSection is not SectionType.Async || Params.StreamVersion < 4) + { + return; + } + + SetSection(SectionType.Default); + } + + public virtual void BeginDebugSection() + { + if (_currentSection is SectionType.Debug || Params.StreamVersion < 4) + { + return; + } + + SetSection(SectionType.Debug); + } + + public virtual void EndDebugSection() + { + if (_currentSection is not SectionType.Debug || Params.StreamVersion < 4) + { + return; + } + + SetSection(SectionType.Default); + } + + /// + /// Emits a property key for the next value. No-op in binary streams; + /// in JSON streams this labels the next value within an object container. + /// + /// The property/field name. + public virtual void Key(string name) { } + + /// + /// Begins a named object or array container. + /// No-op in binary streams. In JSON streams emits the opening brace or bracket. + /// + public virtual void BeginObject(string name, bool isArray = false) { } + + /// + /// Ends the most recently opened named container. + /// No-op in binary streams. In JSON streams emits the closing brace or bracket. + /// + public virtual void EndObject(string name) { } + + /// + /// Called when the current section changes. Implementations should set up their reader/writer + /// to point to the new section's stream. + /// + /// The section that is now active. + protected abstract void SetSection(SectionType section); + + public abstract void BeginBlock(); + + public abstract void EndBlock(); + + public bool IsSectionEmpty() + => Sections[(int)_currentSection].Stream?.Length == 0; + + public bool IsClassSerialized(string typeName) + => Params.GetRegisteredClasses().Any(id => id.ClassType.Symbol.DebugString == typeName); + + public MetaClass? GetMetaClass(Type type) + { + if (!Params.CanModifySerializedClassesList) + { + return Params.VersionInfo + .FirstOrDefault(versionInfo => versionInfo.GetMetaClassType()?.LinkingType == type)?.GetMetaClass(); + } + + return Params.Workspace?.GetMetaClassDescription(type); + } + + public MetaClass? GetMetaClass(Symbol symbol) + { + if (!Params.CanModifySerializedClassesList) + { + return Params.VersionInfo.FirstOrDefault(versionInfo => versionInfo.TypeSymbolCrc == symbol.Crc64) + ?.GetMetaClass(); + } + + return Params.Workspace?.GetMetaClassDescription(symbol); + } + + /// + /// Serializes the specified boolean value. + /// + /// The value to serialize + public abstract void Serialize(ref bool value); + + /// + /// Serializes the specified float value. + /// + /// The value to serialize + public abstract void Serialize(ref float value); + + /// + /// Serializes the specified double value. + /// + /// The value to serialize + public abstract void Serialize(ref double value); + + /// + /// Serializes the specified short value. + /// + /// The value to serialize + public abstract void Serialize(ref short value); + + /// + /// Serializes the specified integer value. + /// + /// The value to serialize + public abstract void Serialize(ref int value); + + /// + /// Serializes the specified long value. + /// + /// The value to serialize + public abstract void Serialize(ref long value); + + /// + /// Serializes the specified ushort value. + /// + /// The value to serialize + public abstract void Serialize(ref ushort value); + + /// + /// Serializes the specified unsigned integer value. + /// + /// The value to serialize + public abstract void Serialize(ref uint value); + + /// + /// Serializes the specified unsigned long value. + /// + /// The value to serialize + public abstract void Serialize(ref ulong value); + + /// + /// Serializes the specified string value. + /// + /// The value to serialize + public abstract void Serialize(ref string value); + + /// + /// Serializes the specified char value. + /// + /// The value to serialize + public abstract void Serialize(ref char value); + + /// + /// Serializes the specified byte value. + /// + /// The value to serialize + public abstract void Serialize(ref byte value); + + /// + /// Serializes the specified signed byte value. + /// + /// The value to serialize + public abstract void Serialize(ref sbyte value); + + /// + /// Serializes the Symbol class. + /// + /// The value to serialize + public abstract void Serialize(ref Symbol value); + + /// + /// Serializes the specified byte array. + /// + /// The buffer to serialize. + /// The starting offset in the buffer to begin serializing. + /// The size, in bytes, to serialize. + public abstract void Serialize(byte[] values, int offset, int count); + + public bool IsEndOfStream() + { + if (Mode is not MetaStreamMode.Read) + { + return false; + } + + for (int i = 1; i <= 3; i++) + { + //for each section (default,async,debug) + SectionInfo currentSect = Sections[i]; + if (currentSect.Stream == null) + { + continue; + } + + if (currentSect.Stream.Position != currentSect.Stream.Length) + { + return false; + } + } + + return true; + } + + /// + /// Skip a block. + /// This is internally used, even by Telltale themselves. + /// + public void SkipToEndOfCurrentBlock() + { + if (Mode is not MetaStreamMode.Read) + { + return; + } + + SectionInfo currentSectionInfo = Sections[(int)_currentSection]; + if (currentSectionInfo.Blocks.Count == 0) + { + return; + } + + long expectedPosition = currentSectionInfo.Blocks.Pop(); + currentSectionInfo.Stream?.Seek(expectedPosition, SeekOrigin.Begin); + } + + /// Gets the underlying stream for the specified section. + /// The section type (Header, Default, Debug, Async). + /// The stream if the section exists and is initialized; otherwise null. + public SectionInfo GetSection(SectionType type) + => Sections[(int)type]; + + // If there are assets larger than 2GBs, I need to rethink my life. + public long GetRemainingSectionBytes() + { + Stream? stream = CurrentSection.Stream; + if (stream == null) + { + return 0; + } + + return stream.Length - stream.Position; + } + + public long GetRemainingSectionBytes(SectionType section) + { + Stream? stream = Sections[(int)section].Stream; + if (stream == null) + { + return 0; + } + + return stream.Length - stream.Position; + } + + public long GetPosition() + { + Stream? stream = CurrentSection.Stream; + if (stream == null) + { + return 0; + } + + return stream.Position; + } + + public void SetPosition(long position) + { + Stream? stream = CurrentSection.Stream; + if (stream == null) + { + throw new InvalidOperationException("Cannot set position on a null section stream."); + } + + stream.Position = position; + } + + public void AddVersionInfo(MetaClass? desc) + { + if (desc is null) + { + return; + } + + foreach (MetaVersionInfo? versInfo in Params.VersionInfo) + { + if (versInfo.TypeSymbolCrc == desc.ClassType.Symbol.Crc64) + { + if (versInfo.VersionCrc != desc.Crc32) + { + throw new InvalidOperationException("Version CRC mismatch"); + } + + return; + } + } + + MetaVersionInfo versionInfo = new() { TypeSymbolCrc = desc.ClassType.Symbol.Crc64, VersionCrc = desc.Crc32 }; + Params.VersionInfo.Add(versionInfo); + } + + /// + /// Closes the stream and finalizes writing (if in write mode). + /// + public abstract void Close(); + + public static bool IsValidMetaStream(Stream stream) + { + if (stream.Length < 4) + { + return false; + } + + long originalPosition = stream.Position; + + try + { + using BinaryReader reader = new(stream, Encoding.UTF8, true); + uint version = reader.ReadUInt32(); + return Enum.IsDefined(typeof(MetaStreamMagic), version); + } + finally + { + stream.Position = originalPosition; + } + } + + protected virtual void Dispose(bool disposing) + { + if (disposing) + { + foreach (SectionInfo section in Sections) + { + section.Stream?.Dispose(); + } + } + } + + public class SectionInfo + { + // For blocks. In read, stores the sizes, in write stores the block offset initial. + public readonly Stack Blocks = []; + + public long CompressedSize = 0; + public bool IsCompressed = false; + + // Section data stream + public Stream? Stream = null; + } +} diff --git a/src/TelltaleToolKit/Serialization/Binary/MetaStreamExtensions.cs b/src/TelltaleToolKit/Serialization/MetaStreamExtensions.cs similarity index 78% rename from src/TelltaleToolKit/Serialization/Binary/MetaStreamExtensions.cs rename to src/TelltaleToolKit/Serialization/MetaStreamExtensions.cs index 3857ec6..0a6b588 100644 --- a/src/TelltaleToolKit/Serialization/Binary/MetaStreamExtensions.cs +++ b/src/TelltaleToolKit/Serialization/MetaStreamExtensions.cs @@ -2,7 +2,7 @@ using TelltaleToolKit.Reflection; using TelltaleToolKit.T3Types; -namespace TelltaleToolKit.Serialization.Binary; +namespace TelltaleToolKit.Serialization; public static class MetaStreamExtensions { @@ -12,7 +12,7 @@ public static class MetaStreamExtensions /// The stream. /// A boolean value read from the stream. [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static bool ReadBoolean(this MetaStreamReader stream) + public static bool ReadBoolean(this MetaStream stream) { bool value = false; stream.Serialize(ref value); @@ -25,7 +25,7 @@ public static bool ReadBoolean(this MetaStreamReader stream) /// The stream. /// A 4-byte floating point value read from the stream. [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static float ReadSingle(this MetaStreamReader stream) + public static float ReadSingle(this MetaStream stream) { float value = 0.0f; stream.Serialize(ref value); @@ -38,7 +38,7 @@ public static float ReadSingle(this MetaStreamReader stream) /// The stream. /// An 8-byte floating point value read from the stream. [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static double ReadDouble(this MetaStreamReader stream) + public static double ReadDouble(this MetaStream stream) { double value = 0.0; stream.Serialize(ref value); @@ -51,7 +51,7 @@ public static double ReadDouble(this MetaStreamReader stream) /// The stream. /// A 2-byte signed integer read from the stream. [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static short ReadInt16(this MetaStreamReader stream) + public static short ReadInt16(this MetaStream stream) { short value = 0; stream.Serialize(ref value); @@ -64,7 +64,7 @@ public static short ReadInt16(this MetaStreamReader stream) /// The stream. /// A 4-byte signed integer read from the stream. [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static int ReadInt32(this MetaStreamReader stream) + public static int ReadInt32(this MetaStream stream) { int value = 0; stream.Serialize(ref value); @@ -77,7 +77,7 @@ public static int ReadInt32(this MetaStreamReader stream) /// The stream. /// An 8-byte signed integer read from the stream. [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static long ReadInt64(this MetaStreamReader stream) + public static long ReadInt64(this MetaStream stream) { long value = 0; stream.Serialize(ref value); @@ -90,7 +90,7 @@ public static long ReadInt64(this MetaStreamReader stream) /// The stream. /// A 2-byte unsigned integer read from the stream. [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static ushort ReadUInt16(this MetaStreamReader stream) + public static ushort ReadUInt16(this MetaStream stream) { ushort value = 0; stream.Serialize(ref value); @@ -103,7 +103,7 @@ public static ushort ReadUInt16(this MetaStreamReader stream) /// The stream. /// A 4-byte unsigned integer read from the stream. [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static uint ReadUInt32(this MetaStreamReader stream) + public static uint ReadUInt32(this MetaStream stream) { uint value = 0; stream.Serialize(ref value); @@ -116,7 +116,7 @@ public static uint ReadUInt32(this MetaStreamReader stream) /// The stream. /// An 8-byte unsigned integer read from the stream. [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static ulong ReadUInt64(this MetaStreamReader stream) + public static ulong ReadUInt64(this MetaStream stream) { ulong value = 0; stream.Serialize(ref value); @@ -129,7 +129,7 @@ public static ulong ReadUInt64(this MetaStreamReader stream) /// The stream. /// A string read from the stream. [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static string ReadString(this MetaStreamReader stream) + public static string ReadString(this MetaStream stream) { string value = null!; stream.Serialize(ref value); @@ -142,7 +142,7 @@ public static string ReadString(this MetaStreamReader stream) /// The stream. /// A Unicode character read from the stream. [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static char ReadChar(this MetaStreamReader stream) + public static char ReadChar(this MetaStream stream) { char value = '\0'; stream.Serialize(ref value); @@ -155,7 +155,7 @@ public static char ReadChar(this MetaStreamReader stream) /// The stream. /// An unsigned byte read from the stream. [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static byte ReadByte(this MetaStreamReader stream) + public static byte ReadByte(this MetaStream stream) { byte value = 0; stream.Serialize(ref value); @@ -168,7 +168,7 @@ public static byte ReadByte(this MetaStreamReader stream) /// The stream. /// A signed byte read from the stream. [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static sbyte ReadSByte(this MetaStreamReader stream) + public static sbyte ReadSByte(this MetaStream stream) { sbyte value = 0; stream.Serialize(ref value); @@ -182,7 +182,7 @@ public static sbyte ReadSByte(this MetaStreamReader stream) /// /// A byte array containing the data read from the stream. [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static byte[] ReadBytes(this MetaStreamReader stream, int count) + public static byte[] ReadBytes(this MetaStream stream, int count) { byte[] value = new byte[count]; stream.Serialize(value, 0, count); @@ -195,7 +195,7 @@ public static byte[] ReadBytes(this MetaStreamReader stream, int count) /// The stream. /// A string read from the stream. [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static Symbol ReadSymbol(this MetaStreamReader stream) + public static Symbol ReadSymbol(this MetaStream stream) { var value = Symbol.Empty; stream.Serialize(ref value); @@ -209,10 +209,23 @@ public static Symbol ReadSymbol(this MetaStreamReader stream) /// The stream. /// A string read from the stream. [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static MetaClassType ReadMetaClassType(this MetaStreamReader stream) + public static MetaClassType? ReadMetaClassType(this MetaStream stream) { - MetaClassType value = MetaClassType.UninitializedClassType; - stream.Serialize(ref value); + MetaClassType? value; + if (stream.Params.StreamVersion >= 3) // mStreamVersion >= 3 uses hashed types + { + ulong typeSymbolCrc = stream.ReadUInt64(); + value = MetaClassTypeRegistry.GetByHash(typeSymbolCrc); + + if (value == null) + Toolkit.Instance.Logger.LogWarning($"Unknown type symbol CRC64: {typeSymbolCrc}"); + } + else + { + string typeName = stream.ReadString(); + value = MetaClassTypeRegistry.GetByName(typeName); + } + return value; } @@ -223,7 +236,7 @@ public static MetaClassType ReadMetaClassType(this MetaStreamReader stream) /// The boolean value to write. /// The stream. [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static MetaStreamWriter Write(this MetaStreamWriter stream, bool value) + public static MetaStream Write(this MetaStream stream, bool value) { stream.Serialize(ref value); return stream; @@ -236,7 +249,7 @@ public static MetaStreamWriter Write(this MetaStreamWriter stream, bool value) /// The 4-byte floating point value to write. /// The stream. [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static MetaStreamWriter Write(this MetaStreamWriter stream, float value) + public static MetaStream Write(this MetaStream stream, float value) { stream.Serialize(ref value); return stream; @@ -249,7 +262,7 @@ public static MetaStreamWriter Write(this MetaStreamWriter stream, float value) /// The 8-byte floating point value to write. /// The stream. [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static MetaStreamWriter Write(this MetaStreamWriter stream, double value) + public static MetaStream Write(this MetaStream stream, double value) { stream.Serialize(ref value); return stream; @@ -262,7 +275,7 @@ public static MetaStreamWriter Write(this MetaStreamWriter stream, double value) /// The 2-byte signed integer to write. /// The stream. [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static MetaStreamWriter Write(this MetaStreamWriter stream, short value) + public static MetaStream Write(this MetaStream stream, short value) { stream.Serialize(ref value); return stream; @@ -275,7 +288,7 @@ public static MetaStreamWriter Write(this MetaStreamWriter stream, short value) /// The 4-byte signed integer to write. /// The stream. [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static MetaStreamWriter Write(this MetaStreamWriter stream, int value) + public static MetaStream Write(this MetaStream stream, int value) { stream.Serialize(ref value); return stream; @@ -288,7 +301,7 @@ public static MetaStreamWriter Write(this MetaStreamWriter stream, int value) /// The 8-byte signed integer to write. /// The stream. [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static MetaStreamWriter Write(this MetaStreamWriter stream, long value) + public static MetaStream Write(this MetaStream stream, long value) { stream.Serialize(ref value); return stream; @@ -301,7 +314,7 @@ public static MetaStreamWriter Write(this MetaStreamWriter stream, long value) /// The 2-byte unsigned integer to write. /// The stream. [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static MetaStreamWriter Write(this MetaStreamWriter stream, ushort value) + public static MetaStream Write(this MetaStream stream, ushort value) { stream.Serialize(ref value); return stream; @@ -314,7 +327,7 @@ public static MetaStreamWriter Write(this MetaStreamWriter stream, ushort value) /// The 4-byte unsigned integer to write. /// The stream. [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static MetaStreamWriter Write(this MetaStreamWriter stream, uint value) + public static MetaStream Write(this MetaStream stream, uint value) { stream.Serialize(ref value); return stream; @@ -327,7 +340,7 @@ public static MetaStreamWriter Write(this MetaStreamWriter stream, uint value) /// The 8-byte unsigned integer to write. /// The stream. [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static MetaStreamWriter Write(this MetaStreamWriter stream, ulong value) + public static MetaStream Write(this MetaStream stream, ulong value) { stream.Serialize(ref value); return stream; @@ -340,7 +353,7 @@ public static MetaStreamWriter Write(this MetaStreamWriter stream, ulong value) /// The string to write. /// The stream. [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static MetaStreamWriter Write(this MetaStreamWriter stream, string value) + public static MetaStream Write(this MetaStream stream, string value) { stream.Serialize(ref value); return stream; @@ -353,7 +366,7 @@ public static MetaStreamWriter Write(this MetaStreamWriter stream, string value) /// The Unicode character to write. /// The stream. [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static MetaStreamWriter Write(this MetaStreamWriter stream, char value) + public static MetaStream Write(this MetaStream stream, char value) { stream.Serialize(ref value); return stream; @@ -366,7 +379,7 @@ public static MetaStreamWriter Write(this MetaStreamWriter stream, char value) /// The unsigned byte to write. /// The stream. [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static MetaStreamWriter Write(this MetaStreamWriter stream, byte value) + public static MetaStream Write(this MetaStream stream, byte value) { stream.Serialize(ref value); return stream; @@ -379,7 +392,7 @@ public static MetaStreamWriter Write(this MetaStreamWriter stream, byte value) /// The signed byte to write. /// The stream. [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static MetaStreamWriter Write(this MetaStreamWriter stream, sbyte value) + public static MetaStream Write(this MetaStream stream, sbyte value) { stream.Serialize(ref value); return stream; @@ -392,7 +405,7 @@ public static MetaStreamWriter Write(this MetaStreamWriter stream, sbyte value) /// The signed byte to write. /// The stream. [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static MetaStreamWriter Write(this MetaStreamWriter stream, Symbol value) + public static MetaStream Write(this MetaStream stream, Symbol value) { stream.Serialize(ref value); return stream; @@ -409,7 +422,7 @@ public static MetaStreamWriter Write(this MetaStreamWriter stream, Symbol value) /// The stream. /// [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static MetaStreamWriter Write(this MetaStreamWriter stream, byte[] values, int offset, int count) + public static MetaStream Write(this MetaStream stream, byte[] values, int offset, int count) { stream.Serialize(values, offset, count); return stream; @@ -424,7 +437,7 @@ public static MetaStreamWriter Write(this MetaStreamWriter stream, byte[] values /// The stream. /// [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static MetaStreamWriter Write(this MetaStreamWriter stream, byte[] values) + public static MetaStream Write(this MetaStream stream, byte[] values) { stream.Serialize(values, 0, values.Length); return stream; @@ -432,24 +445,18 @@ public static MetaStreamWriter Write(this MetaStreamWriter stream, byte[] values // TODO: Fix documentation [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static MetaStreamWriter Write(this MetaStreamWriter stream, MetaClassType value) + public static MetaStream Write(this MetaStream stream, MetaClassType value) { - stream.Serialize(ref value); - return stream; - } + if (stream.Params.StreamVersion >= 3) // mStreamVersion >= 3 uses hashed types + { + stream.Write(value.Symbol.Crc64); + } + else + { + stream.Write(value.FullTypeName); + } - /// - /// Performs the first step of serialization or deserialization using the Telltale ToolKit context. - /// - /// - /// Typically, it will instantiate the object if [null], and if it's a collection, clear it. - /// - /// The object to process. - /// The stream to serialize or deserialize to. - /// - public static void PreSerialize(this MetaStream stream, ref T obj, MetaClassType? type = null) where T : new() - { - Toolkit.Instance.GetSerializer().PreSerialize(ref obj, stream, type); + return stream; } /// @@ -457,8 +464,12 @@ public static MetaStreamWriter Write(this MetaStreamWriter stream, MetaClassType /// /// The object to serialize or deserialize. /// The stream to serialize or deserialize to. - public static void Serialize(this MetaStream stream, ref T obj) where T : new() + /// + public static void Serialize(this MetaStream stream, ref T obj, MetaClassType? type = null) where T : new() { - Toolkit.Instance.GetSerializer().Serialize(ref obj, stream); + var serializer = Toolkit.Instance.GetSerializer(); + + serializer.PreSerialize(ref obj, stream, type); + serializer.Serialize(ref obj, stream); } } diff --git a/src/TelltaleToolKit/Serialization/MetaStreamMagic.cs b/src/TelltaleToolKit/Serialization/MetaStreamMagic.cs new file mode 100644 index 0000000..1c0fea9 --- /dev/null +++ b/src/TelltaleToolKit/Serialization/MetaStreamMagic.cs @@ -0,0 +1,162 @@ +namespace TelltaleToolKit.Serialization; + +/// +/// Four‑character codes (magic numbers) that identify the format and encryption state +/// of a Telltale MetaStream file. +/// +/// +/// The magic appears as the first 4 bytes of the file. Plaintext magics are human‑readable +/// ASCII strings (e.g. "NIBM" in little-Endian). Encrypted magics are arbitrary 32‑bit values chosen by +/// Telltale to avoid easy pattern matching. +/// +public enum MetaStreamMagic : uint +{ + /// + /// MetaStream version 1.
+ /// Expansion: "Meta Binary".
+ /// ASCII value: "MBIN" in big-Endian, or "NIBM" in little-Endian.
+ /// Earliest Telltale asset version.
+ /// Version info is plain text.. + ///
+ Mbin = 0x4D42494E, + + /// + /// MetaStream version 1 (encrypted).
+ /// Expansion: "Meta Binary Encrypted Stream".
+ /// ASCII value: "MBES" in big-Endian, or "SEBM" in little-Endian.
+ /// Used in some early encrypted assets.
+ /// Version info is plain text after decryption. + ///
+ Mbes = 0x4D424553, + + /// + /// MetaStream version 2 (encrypted, variant 1).
+ /// Telltale used three different arbitrary values for version 2 assets.
+ /// These are determined by a number generator.
+ /// Version info is plain text after decryption. + ///
+ EncryptedMbin1 = 0xFB4A1764, + + /// + /// MetaStream version 2 (encrypted, variant 2).
+ /// Telltale used three different arbitrary values for version 2 assets.
+ /// These are determined by a number generator.
+ /// Version info is plain text after decryption. + ///
+ EncryptedMbin2 = 0xEB794091, + + /// + /// MetaStream version 2 (encrypted, variant 3).
+ /// Telltale used three different arbitrary values for version 2 assets.
+ /// These are determined by a number generator.
+ /// Version info is plain text after decryption. + ///
+ EncryptedMbin3 = 0x64AFDEFB, + + /// + /// MetaStream version 3.
+ /// Expansion: "Meta T(h)ree" or "Meta Type Reference."
+ /// ASCII value: "MTRE" in big-Endian, or "ERTM" in little-Endian.
+ /// Version info is hashed. + ///
+ Mtre = 0x4D545245, + + /// + /// MetaStream version 3 (encrypted).
+ /// Version info is hashed after decryption. + ///
+ EncryptedMtre = 0x64AFDEAA, + + /// + /// MetaStream version 3 (compressed).
+ /// Expansion: "Meta Compressed".
+ /// ASCII value: "MCOM" in big-Endian, or "MOCM" in little-Endian.
+ /// Version info is hashed after decompression. + ///
+ Mcom = 0x4D434F4D, + + /// + /// MetaStream version 3 (compressed, encrypted).
+ /// Version info is hashed after decompression and decryption (presumably). + ///
+ EncryptedMcom = 0x64AFDEBB, + + /// + /// MetaStream version 4 (optional compression).
+ /// Expansion: "Meta Stream Version 4".
+ /// ASCII value: "MSV4" in big-Endian, or "4VSM" in little-Endian.
+ /// Version info is hashed.
+ /// Unused in any games. + ///
+ Msv4 = 0x4D535356, + + /// + /// MetaStream version 5 (optional compression).
+ /// Expansion: "Meta Stream Version 5".
+ /// ASCII value: "MSV5" in big-Endian, or "5VSM" in little-Endian.
+ /// Version info is hashed. + ///
+ Msv5 = 0x4D535635, + + /// + /// MetaStream version 6 (optional compression).
+ /// Expansion: "Meta Stream Version 6".
+ /// ASCII value: "MSV6" in big-Endian, or "6VSM" in little-Endian.
+ /// Version info is hashed. + ///
+ Msv6 = 0x4D535636 +} + +/// +/// Extension methods for . +/// +public static class MetaStreamVersionExtensions +{ + /// + /// Returns the MetaStream format version associated with the given magic number. + /// + /// The magic value read from the file header. + /// + /// An integer representing the version of the MetaStream format: + /// + /// + /// 1Plaintext (Mbin, Mbes) or encrypted Mbes + /// + /// + /// 2Encrypted version 2 (EncryptedMbin1/2/3) + /// + /// + /// 3Version 3 (Mtre, Mcom, EncryptedMtre, EncryptedMcom) + /// + /// + /// 4Version 4 (Msv4) + /// + /// + /// 5Version 5 (Msv5) + /// + /// + /// 6Version 6 (Msv6) + /// + /// + /// 0Unknown or unsupported magic + /// + /// + /// + /// + /// The returned version corresponds to the + /// that should be used when reading or writing the stream. + /// + public static uint GetMetaStreamVersion(this MetaStreamMagic magic) => + magic switch + { + MetaStreamMagic.Mbin or MetaStreamMagic.Mbes => 1, + MetaStreamMagic.EncryptedMbin1 or MetaStreamMagic.EncryptedMbin2 + or MetaStreamMagic.EncryptedMbin3 => 2, // no basic version + MetaStreamMagic.Mtre or MetaStreamMagic.Mcom or MetaStreamMagic.EncryptedMcom + or MetaStreamMagic.EncryptedMtre => 3, + MetaStreamMagic.Msv4 => 4, + MetaStreamMagic.Msv5 => 5, + MetaStreamMagic.Msv6 => 6, + _ => 0 + }; +} diff --git a/src/TelltaleToolKit/Serialization/MetaStreamMagicExtensions.cs b/src/TelltaleToolKit/Serialization/MetaStreamMagicExtensions.cs new file mode 100644 index 0000000..9826c25 --- /dev/null +++ b/src/TelltaleToolKit/Serialization/MetaStreamMagicExtensions.cs @@ -0,0 +1,40 @@ +namespace TelltaleToolKit.Serialization; + +public static class MetaStreamMagicExtensions +{ + /// + /// Converts a string to StreamVersion. + /// + public static MetaStreamMagic StreamVersionFromString(this string value) + { + return value switch + { + "MBIN" => MetaStreamMagic.Mbin, + "MBES" => MetaStreamMagic.Mbes, + "MTRE" => MetaStreamMagic.Mtre, + "MCOM" => MetaStreamMagic.Mcom, + "MSV4" => MetaStreamMagic.Msv4, + "MSV5" => MetaStreamMagic.Msv5, + "MSV6" => MetaStreamMagic.Msv6, + _ => throw new ArgumentOutOfRangeException(nameof(value), value, null) + }; + } + + /// + /// Converts a StreamVersion to its string name. + /// + public static string ToJsonString(this MetaStreamMagic magic) + { + return magic switch + { + MetaStreamMagic.Mbin => "MBIN", + MetaStreamMagic.Mbes => "MBES", + MetaStreamMagic.Mcom => "MCOM", + MetaStreamMagic.Mtre => "MTRE", + MetaStreamMagic.Msv4 => "MSV4", + MetaStreamMagic.Msv5 => "MSV5", + MetaStreamMagic.Msv6 => "MSV6", + _ => string.Empty + }; + } +} diff --git a/src/TelltaleToolKit/Serialization/MetaStreamMode.cs b/src/TelltaleToolKit/Serialization/MetaStreamMode.cs new file mode 100644 index 0000000..b780672 --- /dev/null +++ b/src/TelltaleToolKit/Serialization/MetaStreamMode.cs @@ -0,0 +1,17 @@ +namespace TelltaleToolKit.Serialization; + +/// +/// Enumerates the different mode of serialization (either serialization or deserialization). +/// +public enum MetaStreamMode +{ + /// + /// The serializer is in serialize mode. + /// + Read, + + /// + /// The serializer is in deserialize mode. + /// + Write, +} diff --git a/src/TelltaleToolKit/Serialization/MetaStreamParams.cs b/src/TelltaleToolKit/Serialization/MetaStreamParams.cs new file mode 100644 index 0000000..2e8be5d --- /dev/null +++ b/src/TelltaleToolKit/Serialization/MetaStreamParams.cs @@ -0,0 +1,143 @@ +using TelltaleToolKit.Reflection; +using TelltaleToolKit.T3Types; + +namespace TelltaleToolKit.Serialization; + +/// +/// Holds parameters and metadata for a MetaStream during serialization or deserialization. +/// +public class MetaStreamParams +{ + /// + /// Gets or sets the MetaStream version associated with this stream. + /// + public uint StreamVersion { get; set; } + + /// + /// True if all version CRCs match the compiled ones. + /// + public bool IsCompiledVersion { get; set; } + + /// + /// Whether to apply legacy Blowfish block encryption (versions 2–3 only). + /// For version 4+, encryption is handled per-section via ContainerStream. + /// + public bool Encrypt { get; set; } + + /// + /// Whether to compress sections (versions 4+ only, via ContainerStream/Deflate). + /// + public bool Compress { get; set; } + + /// + /// Gets the list of objects for all classes that have been serialized in the current stream. + /// + public List VersionInfo { get; set; } = []; + + /// + /// Gets the list of serialized objects found in the stream. + /// + public List SerializedSymbols { get; set; } = []; + + /// + /// Gets or sets a value indicating whether the list of serialized classes can be modified after initialization. + /// + public bool CanModifySerializedClassesList { get; set; } + + /// + /// Gets the current workspace. Required for serializing if custom types are added. + /// + public Workspace? Workspace { get; set; } + + /// + /// Gets the list of objects for all classes that have been serialized in the current stream. + /// + public List GetRegisteredClasses() + { + List classes = []; + + foreach (MetaVersionInfo versionInfo in VersionInfo) + { + if (versionInfo.IsRegistered()) + classes.Add(versionInfo.GetMetaClass()!); + } + + return classes; + } + + /// + /// Gets the list of classes found in the stream that are not registered in the current game profile. + /// Each entry is a tuple containing the and its CRC32 checksum. + /// + public List<(MetaClassType Type, uint VersionCrc)> GetUnregisteredClasses() + { + List<(MetaClassType Type, uint VersionCrc)> unregistered = []; + + foreach (MetaVersionInfo versionInfo in VersionInfo) + { + if (!versionInfo.IsRegistered() && versionInfo.IsTypeRegistered()) + unregistered.Add((versionInfo.GetMetaClassType()!, versionInfo.VersionCrc)); + } + + return unregistered; + } + + /// + /// Gets the list of type hashes and checksums for types encountered in the stream that could not be resolved. + /// Each entry is a tuple of the type hash (ulong) and CRC32 checksum. + /// + public List<(ulong TypeSymbolCrc, uint VersionCrc)> GetUnregisteredTypes() + { + List<(ulong TypeSymbolCrc, uint VersionCrc)> unregisteredTypes = []; + + foreach (var versionInfo in VersionInfo) + { + if (!versionInfo.IsTypeRegistered()) + unregisteredTypes.Add((versionInfo.TypeSymbolCrc, versionInfo.VersionCrc)); + } + + return unregisteredTypes; + } + + /// + /// Derives the correct on-disk magic FourCC from Version + Encrypt. + /// Throws if the combination is invalid or not yet supported. + /// + internal MetaStreamMagic ResolveMagic() + { + // TODO: support MCOM and EncryptedMCOM, despite not being used in games + return (StreamVersion, Encrypt) switch + { + (1, false) => MetaStreamMagic.Mbin, + (1, true) => MetaStreamMagic.Mbes, + (2, false) => MetaStreamMagic.Mbin, // Telltale used multiple; + (2, true) => GetRandomEncryptedMbinMagic(), // Telltale used multiple; + (3, false) => MetaStreamMagic.Mtre, + (3, true) => MetaStreamMagic.EncryptedMtre, + (4, (false)) => MetaStreamMagic.Msv4, + (5, false) => MetaStreamMagic.Msv5, + (6, false) => MetaStreamMagic.Msv6, + _ => throw new NotSupportedException( + $"No valid magic for Version={StreamVersion}, Encrypt={Encrypt}.") + }; + } + + private static readonly Random s_random = new(); + private static readonly object s_lock = new(); + + private static MetaStreamMagic GetRandomEncryptedMbinMagic() + { + int choice; + lock (s_lock) + { + choice = s_random.Next(0, 3); // 0, 1, or 2 + } + return choice switch + { + 0 => MetaStreamMagic.EncryptedMbin1, + 1 => MetaStreamMagic.EncryptedMbin2, + 2 => MetaStreamMagic.EncryptedMbin3, + _ => MetaStreamMagic.EncryptedMbin1 // fallback + }; + } +} diff --git a/src/TelltaleToolKit/Serialization/Serializers/ByteArraySerializer.cs b/src/TelltaleToolKit/Serialization/Serializers/ByteArraySerializer.cs index 4a919ee..695ad89 100644 --- a/src/TelltaleToolKit/Serialization/Serializers/ByteArraySerializer.cs +++ b/src/TelltaleToolKit/Serialization/Serializers/ByteArraySerializer.cs @@ -5,15 +5,14 @@ namespace TelltaleToolKit.Serialization.Serializers; [MetaClassSerializerGlobal(typeof(ByteArraySerializer))] public class ByteArraySerializer : MetaClassSerializer { - /// public override void Serialize(ref byte[] obj, MetaStream stream) { - if (stream is MetaStreamWriter streamWriter) - streamWriter.Write(obj); - else if (stream is MetaStreamReader streamReader) + if (stream.Mode is MetaStreamMode.Write) + stream.Write(obj); + else if (stream.Mode is MetaStreamMode.Read) { - int size = streamReader.ReadInt32(); - obj = streamReader.ReadBytes(size); + int size = stream.ReadInt32(); + obj = stream.ReadBytes(size); } } -} \ No newline at end of file +} diff --git a/src/TelltaleToolKit/Serialization/Serializers/CollectionSerializers.cs b/src/TelltaleToolKit/Serialization/Serializers/CollectionSerializers.cs index 73697b5..6b44b93 100644 --- a/src/TelltaleToolKit/Serialization/Serializers/CollectionSerializers.cs +++ b/src/TelltaleToolKit/Serialization/Serializers/CollectionSerializers.cs @@ -17,7 +17,7 @@ public override void Initialize(MetaClassSerializerSelector selector) /// public override void PreSerialize(ref List obj, MetaStream stream, MetaClassType? type = null) { - if (stream is MetaStreamReader streamReader) + if (stream.Mode is MetaStreamMode.Read) { if (obj is null) obj = []; @@ -29,18 +29,18 @@ public override void PreSerialize(ref List obj, MetaStream stream, MetaClassT /// public override void Serialize(ref List obj, MetaStream stream) { - if (stream is MetaStreamWriter streamWriter) + if (stream.Mode is MetaStreamMode.Write) { - streamWriter.Write(obj.Count); + stream.Write(obj.Count); foreach (T entry in obj) { _itemDataClassSerializer.Serialize(entry, stream); } } - else if (stream is MetaStreamReader streamReader) + else if (stream.Mode is MetaStreamMode.Read) { - int count = streamReader.ReadInt32(); + int count = stream.ReadInt32(); obj.Capacity = count; for (var i = 0; i < count; i++) @@ -68,7 +68,7 @@ public override void Initialize(MetaClassSerializerSelector selector) /// public override void PreSerialize(ref HashSet obj, MetaStream stream, MetaClassType? type = null) { - if (stream is MetaStreamReader streamReader) + if (stream.Mode is MetaStreamMode.Read) { if (obj is null) obj = []; @@ -77,22 +77,21 @@ public override void PreSerialize(ref HashSet obj, MetaStream stream, MetaCla } } - /// public override void Serialize(ref HashSet obj, MetaStream stream) { - if (stream is MetaStreamWriter streamWriter) + if (stream.Mode is MetaStreamMode.Write) { - streamWriter.Write(obj.Count); + stream.Write(obj.Count); foreach (T entry in obj) { _itemDataClassSerializer.Serialize(entry, stream); } } - else if (stream is MetaStreamReader streamReader) + else if (stream.Mode is MetaStreamMode.Read) { - int count = streamReader.ReadInt32(); + int count = stream.ReadInt32(); for (var i = 0; i < count; i++) { @@ -116,7 +115,7 @@ public sealed class DictionarySerializer : MetaClassSerializer public override void PreSerialize(ref Dictionary obj, MetaStream stream, MetaClassType? type = null) { - if (stream is MetaStreamReader streamReader) + if (stream.Mode is MetaStreamMode.Read) { if (obj is null) obj = new Dictionary(); @@ -136,9 +135,9 @@ public override void Serialize(ref Dictionary obj, MetaStream stre { PreSerialize(ref obj, stream); - if (stream is MetaStreamWriter streamWriter) + if (stream.Mode is MetaStreamMode.Write) { - streamWriter.Write(obj.Count); + stream.Write(obj.Count); foreach (KeyValuePair item in obj) { @@ -146,9 +145,9 @@ public override void Serialize(ref Dictionary obj, MetaStream stre _valueClassSerializer.Serialize(item.Value, stream); } } - else if (stream is MetaStreamReader streamReader) + else if (stream.Mode is MetaStreamMode.Read) { - int count = streamReader.ReadInt32(); + int count = stream.ReadInt32(); for (var i = 0; i < count; i++) { @@ -173,7 +172,6 @@ public override void Initialize(MetaClassSerializerSelector selector) _itemDataClassSerializer = selector.GetSerializer(); } - /// public override void Serialize(ref T[] obj, MetaStream stream) { for (var i = 0; i < obj.Length; ++i) @@ -182,4 +180,4 @@ public override void Serialize(ref T[] obj, MetaStream stream) _itemDataClassSerializer.Serialize(ref obj[i], stream); } } -} \ No newline at end of file +} diff --git a/src/TelltaleToolKit/Serialization/Serializers/DefaultClassSerializer.cs b/src/TelltaleToolKit/Serialization/Serializers/DefaultClassSerializer.cs index 0bff35d..8257820 100644 --- a/src/TelltaleToolKit/Serialization/Serializers/DefaultClassSerializer.cs +++ b/src/TelltaleToolKit/Serialization/Serializers/DefaultClassSerializer.cs @@ -20,10 +20,6 @@ namespace TelltaleToolKit.Serialization.Serializers; /// The type of the class being serialized. public class DefaultClassSerializer : MetaClassSerializer where T : new() { - // TODO: For v0.2.0 or v0.3.0 extract this to a database. - // This is required for implementing JSON serializers and allow UI libraries to link to those fields in the future. - // Explanation: Telltale's JSON serializing is...interesting. - private static readonly Dictionary<(string, Type), CachedMember> MemberCache; // Initialize the mapping dictionary. @@ -49,50 +45,79 @@ public override void Serialize(ref T obj, MetaStream stream) { MetaClass? description = stream.GetMetaClass(typeof(T)); - // Console.WriteLine($"Reading {typeof(T).Name}."); + Toolkit.Instance.Logger.LogInfo($"Reading {typeof(T).Name}."); if (description is null || !description.ClassType.IsSerialized()) { if (description is null) - Toolkit.Instance.Logger.LogWarning($"[DefaultClassSerializer] No description available for {typeof(T).Name}."); + Toolkit.Instance.Logger.LogWarning( + $"[DefaultClassSerializer] No description available for {typeof(T).Name}."); return; } + stream.BeginObject(typeof(T).Name); // Add this class to the metaclass header. - if (stream is MetaStreamWriter { Configuration: { CanModifySerializedClassesList: true } } streamWriter) + if (stream is { Mode: MetaStreamMode.Write, Params.CanModifySerializedClassesList: true }) { - streamWriter.AddVersionInfo(description); + stream.AddVersionInfo(description); } // Loop through the metaclass's properties/members foreach (MetaMember propDesc in description.Members.Where(propDesc => propDesc.IsSerialized())) { - CachedMember cached = ResolveMember(propDesc); + CachedMember? cached = ResolveMember(propDesc); if (propDesc.Type.IsBlocked()) stream.BeginBlock(); - object? value = cached.Getter(ref obj); - MetaClassSerializer serializer = Toolkit.Instance.GetSerializer(cached.Property.PropertyType); + stream.Key(propDesc.MemberName); + + if (propDesc.Type.LinkingType.IsPrimitive || (propDesc.Type.LinkingType == typeof(string) || + propDesc.Type.LinkingType == typeof(byte[]) || + propDesc.Type.LinkingType == typeof(Symbol))) + { + + } + else + { + stream.BeginObject(propDesc.MemberName); + } + + object? value = cached?.Getter(ref obj); + + MetaClassSerializer serializer = + Toolkit.Instance.GetSerializer(cached?.Property.PropertyType ?? propDesc.Type.LinkingType); serializer.PreSerialize(ref value, stream, propDesc.Type); serializer.Serialize(ref value, stream); - // Console.WriteLine($"{propDesc.MemberName} - {value}"); + Toolkit.Instance.Logger.LogInfo($"{propDesc.MemberName} - {value}"); - if (stream is MetaStreamReader) - cached.Setter(ref obj, value); + if (stream.Mode is MetaStreamMode.Read) + cached?.Setter(ref obj, value); + + if (propDesc.Type.LinkingType.IsPrimitive || propDesc.Type.LinkingType == typeof(string) || + propDesc.Type.LinkingType == typeof(byte[]) || + propDesc.Type.LinkingType == typeof(Symbol)) + { + + } + else + { + stream.EndObject(propDesc.MemberName); + } if (propDesc.Type.IsBlocked()) stream.EndBlock(); } - // Console.WriteLine($"Ending {typeof(T).Name}."); + stream.EndObject(typeof(T).Name); + Toolkit.Instance.Logger.LogInfo($"Ending {typeof(T).Name}."); } public override void PreSerialize(ref T obj, MetaStream stream, MetaClassType? type = null) { - if (stream is MetaStreamReader && obj == null) + if (stream.Mode is MetaStreamMode.Read && obj == null) { obj = new T(); } @@ -104,7 +129,7 @@ public override void PreSerialize(ref T obj, MetaStream stream, MetaClassType? t /// /// /// - private static CachedMember ResolveMember(MetaMember propDesc) + private static CachedMember? ResolveMember(MetaMember propDesc) { // 1. Try direct match if (MemberCache.TryGetValue((propDesc.MemberName, propDesc.Type.LinkingType), out CachedMember? cached)) @@ -120,25 +145,6 @@ private static CachedMember ResolveMember(MetaMember propDesc) return cachedAlt; } - // 3. String<->Symbol fallback - if (propDesc.Type.LinkingType == typeof(string) || propDesc.Type.LinkingType == typeof(Symbol)) - { - throw new MetaMemberNotFoundException( - $"Property {propDesc.MemberName} with type {propDesc.Type.LinkingType} not found in class {typeof(T)}"); - if (MemberCache.TryGetValue((propDesc.MemberName, typeof(Symbol)), out CachedMember cachedAlt)) - return cachedAlt; - } - - //4. String<->Symbol fallback - if (propDesc.Type.LinkingType == typeof(Dictionary)) - { - throw new MetaMemberNotFoundException( - $"Property {propDesc.MemberName} with type {propDesc.Type.LinkingType} not found in class {typeof(T)}"); - if (MemberCache.TryGetValue((propDesc.MemberName, typeof(Dictionary)), - out CachedMember? cachedAlt)) - return cachedAlt; - } - // 5. int<->Flags if (propDesc.Type.LinkingType == typeof(int)) { @@ -146,29 +152,7 @@ private static CachedMember ResolveMember(MetaMember propDesc) return cachedAlt; } - // 6. string<->Symbol (dictionary) - if (propDesc.Type.LinkingType == typeof(Dictionary)) - { - throw new MetaMemberNotFoundException( - $"Property {propDesc.MemberName} with type {propDesc.Type.LinkingType} not found in class {typeof(T)}"); - if (MemberCache.TryGetValue( - (propDesc.MemberName, typeof(Dictionary)), - out CachedMember? cachedAlt)) - return cachedAlt; - } - - // 7. string<->Symbol (dictionary) - if (propDesc.Type.LinkingType == typeof(Dictionary)) - { - throw new MetaMemberNotFoundException( - $"Property {propDesc.MemberName} with type {propDesc.Type.LinkingType} not found in class {typeof(T)}"); - if (MemberCache.TryGetValue((propDesc.MemberName, typeof(Dictionary)), - out CachedMember? cachedAlt)) - return cachedAlt; - } - // Final: First available - if (propDesc.Type.LinkingType == typeof(int) || propDesc.Type.LinkingType == typeof(uint)) { CachedMember? cachedAltFinal = MemberCache @@ -178,8 +162,9 @@ private static CachedMember ResolveMember(MetaMember propDesc) return cachedAltFinal; } - throw new MetaMemberNotFoundException( + Toolkit.Instance.Logger.LogError( $"Property {propDesc.MemberName} with type {propDesc.Type.LinkingType} not found in class {typeof(T)}"); + return null; } // Delegate signatures for ref struct support diff --git a/src/TelltaleToolKit/Serialization/Serializers/MathTypeSerializer.cs b/src/TelltaleToolKit/Serialization/Serializers/MathTypeSerializer.cs index e81031a..41b5b68 100644 --- a/src/TelltaleToolKit/Serialization/Serializers/MathTypeSerializer.cs +++ b/src/TelltaleToolKit/Serialization/Serializers/MathTypeSerializer.cs @@ -9,21 +9,21 @@ public class Vector2Serializer : MetaClassSerializer { public override void Serialize(ref Vector2 obj, MetaStream stream) { - if (stream is MetaStreamWriter streamWriter) + if (stream.Mode is MetaStreamMode.Write) { - if (streamWriter.Configuration.CanModifySerializedClassesList) + if (stream.Params.CanModifySerializedClassesList) { MetaClass? description = stream.GetMetaClass(typeof(Vector2)); - streamWriter.AddVersionInfo(description); + stream.AddVersionInfo(description); } - streamWriter.Write(obj.X); - streamWriter.Write(obj.Y); + stream.Write(obj.X); + stream.Write(obj.Y); } - else if (stream is MetaStreamReader streamReader) + else if (stream.Mode is MetaStreamMode.Read) { - obj.X = streamReader.ReadSingle(); - obj.Y = streamReader.ReadSingle(); + obj.X = stream.ReadSingle(); + obj.Y = stream.ReadSingle(); } } } @@ -33,23 +33,23 @@ public class Vector3Serializer : MetaClassSerializer { public override void Serialize(ref Vector3 obj, MetaStream stream) { - if (stream is MetaStreamWriter streamWriter) + if (stream.Mode is MetaStreamMode.Write) { - if (streamWriter.Configuration.CanModifySerializedClassesList) + if (stream.Params.CanModifySerializedClassesList) { MetaClass? description = stream.GetMetaClass(typeof(Vector3)); - streamWriter.AddVersionInfo(description); + stream.AddVersionInfo(description); } - streamWriter.Write(obj.X); - streamWriter.Write(obj.Y); - streamWriter.Write(obj.Z); + stream.Write(obj.X); + stream.Write(obj.Y); + stream.Write(obj.Z); } - else if (stream is MetaStreamReader streamReader) + else if (stream.Mode is MetaStreamMode.Read) { - obj.X = streamReader.ReadSingle(); - obj.Y = streamReader.ReadSingle(); - obj.Z = streamReader.ReadSingle(); + obj.X = stream.ReadSingle(); + obj.Y = stream.ReadSingle(); + obj.Z = stream.ReadSingle(); } } } @@ -59,25 +59,25 @@ public class Vector4Serializer : MetaClassSerializer { public override void Serialize(ref Vector4 obj, MetaStream stream) { - if (stream is MetaStreamWriter streamWriter) + if (stream.Mode is MetaStreamMode.Write) { - if (streamWriter.Configuration.CanModifySerializedClassesList) + if (stream.Params.CanModifySerializedClassesList) { MetaClass? description = stream.GetMetaClass(typeof(Vector4)); - streamWriter.AddVersionInfo(description); + stream.AddVersionInfo(description); } - streamWriter.Write(obj.X); - streamWriter.Write(obj.Y); - streamWriter.Write(obj.Z); - streamWriter.Write(obj.W); + stream.Write(obj.X); + stream.Write(obj.Y); + stream.Write(obj.Z); + stream.Write(obj.W); } - else if (stream is MetaStreamReader streamReader) + else if (stream.Mode is MetaStreamMode.Read) { - obj.X = streamReader.ReadSingle(); - obj.Y = streamReader.ReadSingle(); - obj.Z = streamReader.ReadSingle(); - obj.W = streamReader.ReadSingle(); + obj.X = stream.ReadSingle(); + obj.Y = stream.ReadSingle(); + obj.Z = stream.ReadSingle(); + obj.W = stream.ReadSingle(); } } } @@ -87,25 +87,25 @@ public class QuaternionSerializer : MetaClassSerializer { public override void Serialize(ref Quaternion obj, MetaStream stream) { - if (stream is MetaStreamWriter streamWriter) + if (stream.Mode is MetaStreamMode.Write) { - if (streamWriter.Configuration.CanModifySerializedClassesList) + if (stream.Params.CanModifySerializedClassesList) { MetaClass? description = stream.GetMetaClass(typeof(Quaternion)); - streamWriter.AddVersionInfo(description); + stream.AddVersionInfo(description); } - streamWriter.Write(obj.X); - streamWriter.Write(obj.Y); - streamWriter.Write(obj.Z); - streamWriter.Write(obj.W); + stream.Write(obj.X); + stream.Write(obj.Y); + stream.Write(obj.Z); + stream.Write(obj.W); } - else if (stream is MetaStreamReader streamReader) + else if (stream.Mode is MetaStreamMode.Read) { - obj.X = streamReader.ReadSingle(); - obj.Y = streamReader.ReadSingle(); - obj.Z = streamReader.ReadSingle(); - obj.W = streamReader.ReadSingle(); + obj.X = stream.ReadSingle(); + obj.Y = stream.ReadSingle(); + obj.Z = stream.ReadSingle(); + obj.W = stream.ReadSingle(); } } -} \ No newline at end of file +} diff --git a/src/TelltaleToolKit/Serialization/Serializers/PrimitiveTypeSerializer.cs b/src/TelltaleToolKit/Serialization/Serializers/PrimitiveTypeSerializer.cs index eea6289..c11b08c 100644 --- a/src/TelltaleToolKit/Serialization/Serializers/PrimitiveTypeSerializer.cs +++ b/src/TelltaleToolKit/Serialization/Serializers/PrimitiveTypeSerializer.cs @@ -133,13 +133,13 @@ public sealed class EnumSerializer : MetaClassSerializer where TEn /// public override void Serialize(ref TEnum obj, MetaStream stream) { - if (stream is MetaStreamWriter streamWriter) + if (stream.Mode is MetaStreamMode.Write) { - streamWriter.Write(Convert.ToInt32(obj)); + stream.Write(Convert.ToInt32(obj)); } - else if (stream is MetaStreamReader streamReader) + else if (stream.Mode is MetaStreamMode.Read) { - obj = (TEnum)Enum.ToObject(typeof(TEnum), streamReader.ReadInt32()); + obj = (TEnum)Enum.ToObject(typeof(TEnum), stream.ReadInt32()); } } -} \ No newline at end of file +} diff --git a/src/TelltaleToolKit/Serialization/Serializers/SymbolSerializer.cs b/src/TelltaleToolKit/Serialization/Serializers/SymbolSerializer.cs index 953f903..1cc34e9 100644 --- a/src/TelltaleToolKit/Serialization/Serializers/SymbolSerializer.cs +++ b/src/TelltaleToolKit/Serialization/Serializers/SymbolSerializer.cs @@ -1,5 +1,4 @@ using TelltaleToolKit.Reflection; -using TelltaleToolKit.Serialization.Binary; using TelltaleToolKit.T3Types; namespace TelltaleToolKit.Serialization.Serializers; @@ -14,6 +13,18 @@ public override void PreSerialize(ref Symbol obj, MetaStream stream, MetaClassTy public override void Serialize(ref Symbol obj, MetaStream stream) { + MetaClass? description = stream.GetMetaClass(typeof(Symbol)); + + if (description is null) + { + throw new InvalidOperationException("Symbol description not found."); + } + + if (stream.Mode == MetaStreamMode.Write) + { + stream.AddVersionInfo(description); + } + stream.Serialize(ref obj); } } diff --git a/src/TelltaleToolKit/Serialization/Binary/Substream.cs b/src/TelltaleToolKit/Serialization/SubStream.cs similarity index 92% rename from src/TelltaleToolKit/Serialization/Binary/Substream.cs rename to src/TelltaleToolKit/Serialization/SubStream.cs index b96cbf3..eff15b2 100644 --- a/src/TelltaleToolKit/Serialization/Binary/Substream.cs +++ b/src/TelltaleToolKit/Serialization/SubStream.cs @@ -1,12 +1,21 @@ -namespace TelltaleToolKit.Serialization.Binary; +namespace TelltaleToolKit.Serialization; /// -/// Represents a substream of an underlying . +/// Represents a substream of an underlying . /// public class SubStream : Stream { + private readonly long _length; + + private readonly long _offset; + + private readonly Stream _stream; + + private long _position; + /// - /// Creates a new substream instance using the specified underlying stream at the specified offset with the specified length. + /// Creates a new substream instance using the specified underlying stream at the specified offset with the specified + /// length. /// /// The underlying stream. /// The offset. @@ -26,7 +35,7 @@ public SubStream(Stream stream, long offset, long length) throw new NotSupportedException("Stream does not support seeking."); } - this._stream = stream; + _stream = stream; if (offset < 0) { @@ -42,6 +51,55 @@ public SubStream(Stream stream, long offset, long length) _length = length; } + /// + public override long Length => _length; + + /// + public override bool CanRead => _stream.CanRead; + + /// + public override bool CanSeek => _stream.CanSeek; + + /// + public override bool CanWrite => _stream.CanWrite; + + /// + public override bool CanTimeout => _stream.CanTimeout; + + /// + public override int ReadTimeout + { + get => _stream.ReadTimeout; + set => throw new NotSupportedException("Cannot set the read timeout of a substream."); + } + + /// + public override int WriteTimeout + { + get => _stream.WriteTimeout; + set => throw new NotSupportedException("Cannot set the write timeout of a substream."); + } + + /// + public override long Position + { + get => _position; + set + { + if (value < 0) + { + throw new ArgumentOutOfRangeException("Position cannot be less than zero."); + } + + if (value > _length) + { + throw new ArgumentOutOfRangeException("Position cannot be greater than the length."); + } + + _stream.Position = _offset + (_position = value); + } + } + /// public override int Read(byte[] buffer, int offset, int count) { @@ -116,7 +174,7 @@ public override long Seek(long offset, SeekOrigin origin) "Offset cannot be less than the length of the substream."); } - _stream.Seek(_position = (_length + offset), SeekOrigin.End); + _stream.Seek(_position = _length + offset, SeekOrigin.End); break; case SeekOrigin.Current: @@ -139,70 +197,11 @@ public override long Seek(long offset, SeekOrigin origin) } /// - public override void SetLength(long value) - { + public override void SetLength(long value) => throw // While other Stream implementations allow the caller to set the length, this makes little sense in the context of a substream. // Perhaps, in the future, we can allow callers to reduce the length but not expand the length. - - throw new NotSupportedException("Cannot set the length of a fixed substream."); - } + new NotSupportedException("Cannot set the length of a fixed substream."); /// public override void Flush() => _stream.Flush(); - - /// - public override long Length => _length; - - /// - public override bool CanRead => _stream.CanRead; - - /// - public override bool CanSeek => _stream.CanSeek; - - /// - public override bool CanWrite => _stream.CanWrite; - - /// - public override bool CanTimeout => _stream.CanTimeout; - - /// - public override int ReadTimeout - { - get => _stream.ReadTimeout; - set => throw new NotSupportedException("Cannot set the read timeout of a substream."); - } - - /// - public override int WriteTimeout - { - get => _stream.WriteTimeout; - set => throw new NotSupportedException("Cannot set the write timeout of a substream."); - } - - /// - public override long Position - { - get => _position; - set - { - if (value < 0) - { - throw new ArgumentOutOfRangeException("Position cannot be less than zero."); - } - - if (value > _length) - { - throw new ArgumentOutOfRangeException("Position cannot be greater than the length."); - } - - _stream.Position = _offset + (_position = value); - } - } - - private readonly Stream _stream; - - private readonly long _offset; - private readonly long _length; - - private long _position; } diff --git a/src/TelltaleToolKit/T3Types/Animations/Animation.cs b/src/TelltaleToolKit/T3Types/Animations/Animation.cs index 3f87a75..2e73014 100644 --- a/src/TelltaleToolKit/T3Types/Animations/Animation.cs +++ b/src/TelltaleToolKit/T3Types/Animations/Animation.cs @@ -36,7 +36,7 @@ public class InterfaceInfo public int ValueCount { get; set; } public uint Version { get; set; } } - + public List Descriptors { get; set; } = []; public List Values { get; set; } = []; @@ -61,31 +61,33 @@ public override void Serialize(ref Animation obj, MetaStream stream) return; } - if (stream is MetaStreamWriter streamWriter) + if (stream.Mode is MetaStreamMode.Write) { throw new NotSupportedException(); } - if (stream is MetaStreamReader streamReader) + if (stream.Mode is MetaStreamMode.Read) { - int numTotalValues = streamReader.ReadInt32(); + int numTotalValues = stream.ReadInt32(); obj.Values = new List(numTotalValues); // Runtime buffer (not needed for now) - int dataBufferSize = streamReader.ReadInt32(); + int dataBufferSize = stream.ReadInt32(); obj.Buffer = new byte[dataBufferSize]; - - int numValueTypes = streamReader.ReadInt32(); + + int numValueTypes = stream.ReadInt32(); obj.Descriptors = new List(numValueTypes); for (var i = 0; i < numValueTypes; i++) { - MetaClassType typeSymbol = streamReader.ReadMetaClassType(); // The type of the class + MetaClassType? typeSymbol = stream.ReadMetaClassType(); // The type of the class + if (typeSymbol is null) + throw new InvalidOperationException("[Animation] Type symbol is not registered."); - int numOfType = streamReader.ReadInt16(); // The number of times that type has been serialized + int numOfType = stream.ReadInt16(); // The number of times that type has been serialized // TODO: Verify what these versions actually represent. // TelltaleToolLib casts to unsigned int and uses it as a CRC32. - var version = (uint)streamReader.ReadInt16(); + var version = (uint)stream.ReadInt16(); var interfaceInfo = new InterfaceInfo { @@ -103,7 +105,7 @@ public override void Serialize(ref Animation obj, MetaStream stream) for (var j = 0; j < desc.ValueCount; j++) { object? propertyValue = null; - + serializer.PreSerialize(ref propertyValue, stream); serializer.Serialize(ref propertyValue, stream); @@ -111,23 +113,23 @@ public override void Serialize(ref Animation obj, MetaStream stream) obj.Values.Add(value); } } - + // These are very weird hacks by Telltale. What an...interesting system // If for some reason the serializer for the baseclass of type `AnimationValueInterfaceBase` is not called, // we read its values - the Symbol (CRC64 in this case) and the flags. foreach (IAnimatedValueInterface value in obj.Values) { // TODO: Set other flags? - value.AnimationValueInterfaceBase.Flags = streamReader.ReadInt32(); + value.AnimationValueInterfaceBase.Flags = stream.ReadInt32(); } - ushort isNotInterface = streamReader.ReadUInt16(); + ushort isNotInterface = stream.ReadUInt16(); if (isNotInterface == 0) { foreach (IAnimatedValueInterface value in obj.Values) { - value.AnimationValueInterfaceBase.Name = streamReader.ReadSymbol(); + value.AnimationValueInterfaceBase.Name = stream.ReadSymbol(); } } } @@ -138,20 +140,23 @@ public override void Serialize(ref Animation obj, MetaStream stream) public void SerializeOldAnimation(ref Animation obj, MetaStream stream) { - if (stream is MetaStreamWriter streamWriter) + if (stream.Mode is MetaStreamMode.Write) { throw new NotSupportedException(); } - if (stream is MetaStreamReader streamReader) + if (stream.Mode is MetaStreamMode.Read) { - int numTotalValues = streamReader.ReadInt32(); - int numValueTypes = streamReader.ReadInt32(); + int numTotalValues = stream.ReadInt32(); + int numValueTypes = stream.ReadInt32(); for (var i = 0; i < numValueTypes; i++) { - MetaClassType typeSymbol = streamReader.ReadMetaClassType(); // The type of the class - int numOfType = streamReader.ReadInt32(); // The number of times that type has been serialized + MetaClassType? typeSymbol = stream.ReadMetaClassType(); // The type of the class + if (typeSymbol is null) + throw new InvalidOperationException("[Animation] Type symbol is not registered."); + + int numOfType = stream.ReadInt32(); // The number of times that type has been serialized MetaClassSerializer serializer = Toolkit.Instance.GetSerializer(typeSymbol.LinkingType); @@ -169,4 +174,4 @@ public void SerializeOldAnimation(ref Animation obj, MetaStream stream) stream.EndBlock(); } } -} \ No newline at end of file +} diff --git a/src/TelltaleToolKit/T3Types/Animations/CompressedQuaternionKeys.cs b/src/TelltaleToolKit/T3Types/Animations/CompressedQuaternionKeys.cs index d20c7c3..1ffe2b5 100644 --- a/src/TelltaleToolKit/T3Types/Animations/CompressedQuaternionKeys.cs +++ b/src/TelltaleToolKit/T3Types/Animations/CompressedQuaternionKeys.cs @@ -19,20 +19,20 @@ public override void Serialize(ref CompressedQuaternionKeys obj, MetaStream stre { // TODO: Test this type. - if (stream is MetaStreamWriter streamWriter) + if (stream.Mode is MetaStreamMode.Write) { } - else if (stream is MetaStreamReader streamReader) + else if (stream.Mode is MetaStreamMode.Read) { - obj.Name = streamReader.ReadString(); - obj.Flags = streamReader.ReadInt32(); - obj.MinTime = streamReader.ReadSingle(); - obj.MaxTime = streamReader.ReadSingle(); + obj.Name = stream.ReadString(); + obj.Flags = stream.ReadInt32(); + obj.MinTime = stream.ReadSingle(); + obj.MaxTime = stream.ReadSingle(); - short numSamples = streamReader.ReadInt16(); + short numSamples = stream.ReadInt16(); - obj.Buffer = streamReader.ReadBytes(numSamples * 6); + obj.Buffer = stream.ReadBytes(numSamples * 6); } } } -} \ No newline at end of file +} diff --git a/src/TelltaleToolKit/T3Types/Animations/CompressedSkeletonPoseKeys.cs b/src/TelltaleToolKit/T3Types/Animations/CompressedSkeletonPoseKeys.cs index 089d402..c970d6b 100644 --- a/src/TelltaleToolKit/T3Types/Animations/CompressedSkeletonPoseKeys.cs +++ b/src/TelltaleToolKit/T3Types/Animations/CompressedSkeletonPoseKeys.cs @@ -60,16 +60,16 @@ public override void PreSerialize(ref CompressedSkeletonPoseKeys2 obj, MetaStrea public override void Serialize(ref CompressedSkeletonPoseKeys2 obj, MetaStream stream) { // TODO: Test this type. - if (stream is MetaStreamWriter streamWriter) + if (stream.Mode is MetaStreamMode.Write) { } - else if (stream is MetaStreamReader streamReader) + else if (stream.Mode is MetaStreamMode.Read) { // TODO: Try experimenting with Marshall. - obj.DataSize = streamReader.ReadInt32(); // this is the size of struct + obj.DataSize = stream.ReadInt32(); // this is the size of struct - obj.Data = streamReader.ReadBytes(obj.DataSize); + obj.Data = stream.ReadBytes(obj.DataSize); } } } -} \ No newline at end of file +} diff --git a/src/TelltaleToolKit/T3Types/Animations/CompressedSkeletonPoseKeys2.cs b/src/TelltaleToolKit/T3Types/Animations/CompressedSkeletonPoseKeys2.cs index 6406166..4879681 100644 --- a/src/TelltaleToolKit/T3Types/Animations/CompressedSkeletonPoseKeys2.cs +++ b/src/TelltaleToolKit/T3Types/Animations/CompressedSkeletonPoseKeys2.cs @@ -59,16 +59,16 @@ public override void PreSerialize(ref CompressedSkeletonPoseKeys2 obj, MetaStrea public override void Serialize(ref CompressedSkeletonPoseKeys2 obj, MetaStream stream) { // TODO: Test this type. - if (stream is MetaStreamWriter streamWriter) + if (stream.Mode is MetaStreamMode.Write) { } - else if (stream is MetaStreamReader streamReader) + else if (stream.Mode is MetaStreamMode.Read) { // TODO: Try experimenting with Marshall. - obj.DataSize = streamReader.ReadInt32(); // this is the size of struct + obj.DataSize = stream.ReadInt32(); // this is the size of struct - obj.Data = streamReader.ReadBytes(obj.DataSize); + obj.Data = stream.ReadBytes(obj.DataSize); } } } -} \ No newline at end of file +} diff --git a/src/TelltaleToolKit/T3Types/Animations/CompressedVector3Keys.cs b/src/TelltaleToolKit/T3Types/Animations/CompressedVector3Keys.cs index 72a0cfd..34c6ed3 100644 --- a/src/TelltaleToolKit/T3Types/Animations/CompressedVector3Keys.cs +++ b/src/TelltaleToolKit/T3Types/Animations/CompressedVector3Keys.cs @@ -21,24 +21,24 @@ public override void Serialize(ref CompressedVector3Keys obj, MetaStream stream) { // TODO: Test this type. - if (stream is MetaStreamWriter streamWriter) + if (stream.Mode is MetaStreamMode.Write) { } - else if (stream is MetaStreamReader streamReader) + else if (stream.Mode is MetaStreamMode.Read) { - obj.Name = streamReader.ReadString(); - obj.Flags = streamReader.ReadInt32(); - - obj.Min = new Vector3 { X = streamReader.ReadSingle(), Y = streamReader.ReadSingle(), Z = streamReader.ReadSingle() }; - obj.Max = new Vector3 { X = streamReader.ReadSingle(), Y = streamReader.ReadSingle(), Z = streamReader.ReadSingle() }; + obj.Name = stream.ReadString(); + obj.Flags = stream.ReadInt32(); - obj.MinTime = streamReader.ReadSingle(); - obj.MaxTime = streamReader.ReadSingle(); + obj.Min = new Vector3 { X = stream.ReadSingle(), Y = stream.ReadSingle(), Z = stream.ReadSingle() }; + obj.Max = new Vector3 { X = stream.ReadSingle(), Y = stream.ReadSingle(), Z = stream.ReadSingle() }; - short numSamples = streamReader.ReadInt16(); + obj.MinTime = stream.ReadSingle(); + obj.MaxTime = stream.ReadSingle(); - obj.Buffer = streamReader.ReadBytes(numSamples * 6); + short numSamples = stream.ReadInt16(); + + obj.Buffer = stream.ReadBytes(numSamples * 6); } } } -} \ No newline at end of file +} diff --git a/src/TelltaleToolKit/T3Types/Animations/KeyframedValue.cs b/src/TelltaleToolKit/T3Types/Animations/KeyframedValue.cs index 1c6bed2..3811b69 100644 --- a/src/TelltaleToolKit/T3Types/Animations/KeyframedValue.cs +++ b/src/TelltaleToolKit/T3Types/Animations/KeyframedValue.cs @@ -62,10 +62,10 @@ public override void Serialize(ref KeyframedValue obj, MetaStream stream) DefaultSerializer.PreSerialize(ref obj, stream); DefaultSerializer.Serialize(ref obj, stream); - if (stream is MetaStreamWriter streamWriter) + if (stream.Mode is MetaStreamMode.Write) { } - else if (stream is MetaStreamReader streamReader) + else if (stream.Mode is MetaStreamMode.Read) { } } @@ -81,4 +81,4 @@ public enum TangentMode Knot = 2, Smooth = 3, Flat = 4 -} \ No newline at end of file +} diff --git a/src/TelltaleToolKit/T3Types/Animations/ProceduralLookAt.cs b/src/TelltaleToolKit/T3Types/Animations/ProceduralLookAt.cs index 991e771..d57415c 100644 --- a/src/TelltaleToolKit/T3Types/Animations/ProceduralLookAt.cs +++ b/src/TelltaleToolKit/T3Types/Animations/ProceduralLookAt.cs @@ -89,9 +89,9 @@ public class Serializer : MetaClassSerializer public override void Serialize(ref ProceduralLookAt obj, MetaStream stream) { // TODO: Check with meta stream version. - if (stream is MetaStreamWriter) + if (stream.Mode is MetaStreamMode.Write) { - if (stream.Configuration.Version is MetaStreamVersion.Msv5 or MetaStreamVersion.Msv6) + if (stream.Params.StreamVersion >= 5) { return; } @@ -99,9 +99,9 @@ public override void Serialize(ref ProceduralLookAt obj, MetaStream stream) Animation objAnimation = obj.Animation; Toolkit.Instance.GetSerializer().Serialize(ref objAnimation, stream); } - else if (stream is MetaStreamReader) + else if (stream.Mode is MetaStreamMode.Read) { - if (stream.Configuration.Version is MetaStreamVersion.Msv5 or MetaStreamVersion.Msv6) + if (stream.Params.StreamVersion >= 5) { return; } @@ -113,4 +113,4 @@ public override void Serialize(ref ProceduralLookAt obj, MetaStream stream) } } } -} \ No newline at end of file +} diff --git a/src/TelltaleToolKit/T3Types/Animations/ProceduralLookAtValue.cs b/src/TelltaleToolKit/T3Types/Animations/ProceduralLookAtValue.cs index e1c4bfc..6ff368a 100644 --- a/src/TelltaleToolKit/T3Types/Animations/ProceduralLookAtValue.cs +++ b/src/TelltaleToolKit/T3Types/Animations/ProceduralLookAtValue.cs @@ -15,12 +15,12 @@ public class Serializer : MetaClassSerializer { public override void Serialize(ref ProceduralLookAtValue obj, MetaStream stream) { - if (stream is MetaStreamWriter streamWriter) + if (stream.Mode is MetaStreamMode.Write) { } - else if (stream is MetaStreamReader streamReader) + else if (stream.Mode is MetaStreamMode.Read) { } } } -} \ No newline at end of file +} diff --git a/src/TelltaleToolKit/T3Types/Audio/AudioData.cs b/src/TelltaleToolKit/T3Types/Audio/AudioData.cs index c665961..28634fd 100644 --- a/src/TelltaleToolKit/T3Types/Audio/AudioData.cs +++ b/src/TelltaleToolKit/T3Types/Audio/AudioData.cs @@ -57,25 +57,25 @@ public override void Serialize(ref AudioData obj, MetaStream stream) { DefaultSerializer.Serialize(ref obj, stream); - if (stream is MetaStreamWriter streamWriter) + if (stream.Mode is MetaStreamMode.Write) { } - else if (stream is MetaStreamReader streamReader) + else if (stream.Mode is MetaStreamMode.Read) { - if (stream.Configuration.Version is MetaStreamVersion.Msv5 or MetaStreamVersion.Msv6) + if (stream.Params.StreamVersion >= 4) { - obj.NumChannels = streamReader.ReadInt16(); - short bitDepth = streamReader.ReadInt16(); - int sampleRate = streamReader.ReadInt32(); - int bytesPerSecond = streamReader.ReadInt32(); - obj.SampleSizeBytes = streamReader.ReadInt16(); - short always1 = streamReader.ReadInt16(); - int always328160 = streamReader.ReadInt32(); - int bytesPerSecCopy = streamReader.ReadInt32(); + obj.NumChannels = stream.ReadInt16(); + short bitDepth = stream.ReadInt16(); + int sampleRate = stream.ReadInt32(); + int bytesPerSecond = stream.ReadInt32(); + obj.SampleSizeBytes = stream.ReadInt16(); + short always1 = stream.ReadInt16(); + int always328160 = stream.ReadInt32(); + int bytesPerSecCopy = stream.ReadInt32(); - obj.OggBuffer = streamReader.ReadBytes(stream.GetRemainingSectionBytes()); + obj.OggBuffer = stream.ReadBytes((int)stream.GetRemainingSectionBytes()); } } } } -} \ No newline at end of file +} diff --git a/src/TelltaleToolKit/T3Types/BinaryBuffer.cs b/src/TelltaleToolKit/T3Types/BinaryBuffer.cs index 66bc392..30e47f7 100644 --- a/src/TelltaleToolKit/T3Types/BinaryBuffer.cs +++ b/src/TelltaleToolKit/T3Types/BinaryBuffer.cs @@ -13,21 +13,21 @@ public override void PreSerialize(ref BinaryBuffer obj, MetaStream stream, MetaC { obj ??= new BinaryBuffer(); } - + public override void Serialize(ref BinaryBuffer obj, MetaStream stream) { - if (stream is MetaStreamWriter streamWriter) + if (stream.Mode is MetaStreamMode.Write) { - streamWriter.Write(obj.Data.Length); - streamWriter.Write(obj.Data); + stream.Write(obj.Data.Length); + stream.Write(obj.Data); } - else if (stream is MetaStreamReader streamReader) + else if (stream.Mode is MetaStreamMode.Read) { - int bufferSize = streamReader.ReadInt32(); - obj.Data = streamReader.ReadBytes(bufferSize); + int bufferSize = stream.ReadInt32(); + obj.Data = stream.ReadBytes(bufferSize); } } } public byte[] Data { get; set; } -} \ No newline at end of file +} diff --git a/src/TelltaleToolKit/T3Types/Chores/Chore.cs b/src/TelltaleToolKit/T3Types/Chores/Chore.cs index 7bbb17b..126ebf8 100644 --- a/src/TelltaleToolKit/T3Types/Chores/Chore.cs +++ b/src/TelltaleToolKit/T3Types/Chores/Chore.cs @@ -14,10 +14,10 @@ public class Chore { // [MetaMember("mpChore")] // public Name { get; set; } = string.Empty; - + [MetaMember("mName")] public string Name { get; set; } = string.Empty; - + [MetaMember("mFlags")] public Flags Flags { get; set; } @@ -65,30 +65,30 @@ public class Chore [MetaMember("mSynchronizedToLocalization")] public LocalizeInfo SynchronizedToLocalization { get; set; } - + [MetaMember("mDependencies")] public DependencyLoader Dependencies { get; set; } [MetaMember("mToolProps")] public ToolProps ToolProps { get; set; } - + [MetaMember("mWalkPaths")] public Dictionary WalkPaths { get; set; } public class Serializer : MetaClassSerializer { private static readonly DefaultClassSerializer DefaultSerializer = new(); - + public override void Serialize(ref Chore obj, MetaStream stream) { DefaultSerializer.PreSerialize(ref obj, stream); DefaultSerializer.Serialize(ref obj, stream); MetaClass? choreMetaClassDescription = stream.GetMetaClass(typeof(Chore)); - if (stream is MetaStreamWriter streamWriter) + if (stream.Mode is MetaStreamMode.Write) { } - else if (stream is MetaStreamReader streamReader) + else if (stream.Mode is MetaStreamMode.Read) { if (obj.NumResources > 0 && !choreMetaClassDescription.ContainsMember("mResourcesChore")) { @@ -97,8 +97,7 @@ public override void Serialize(ref Chore obj, MetaStream stream) for (var i = 0; i < obj.NumResources; i++) { var choreResource = new ChoreResource(); - Toolkit.Instance.GetSerializer().PreSerialize(ref choreResource, stream); - Toolkit.Instance.GetSerializer().Serialize(ref choreResource, stream); + stream.Serialize(ref choreResource); obj.ResourcesChore.Add(choreResource); } } @@ -121,4 +120,4 @@ public override void Serialize(ref Chore obj, MetaStream stream) public class EnumExtentsMode { } -} \ No newline at end of file +} diff --git a/src/TelltaleToolKit/T3Types/Chores/ChoreResource.cs b/src/TelltaleToolKit/T3Types/Chores/ChoreResource.cs index e17e09c..66b53ea 100644 --- a/src/TelltaleToolKit/T3Types/Chores/ChoreResource.cs +++ b/src/TelltaleToolKit/T3Types/Chores/ChoreResource.cs @@ -12,13 +12,13 @@ public class ChoreResource { [MetaMember("mVersion")] public int Version { get; set; } - + [MetaMember("mResName")] public string ResName { get; set; } = string.Empty; [MetaMember("mResName")] public Symbol ResNameSymbol { get; set; } - + [MetaMember("mResLength")] public float ResLength { get; set; } @@ -54,7 +54,7 @@ public class ChoreResource [MetaMember("mbViewResourceGroups")] public bool ViewResourceGroups { get; set; } - + [MetaMember("mbViewEmptyGraphs")] public bool ViewEmptyGraphs { get; set; } @@ -63,7 +63,7 @@ public class ChoreResource [MetaMember("mResourceGroupInclude")] public Dictionary ResourceGroupInclude { get; set; } = new(); - + [MetaMember("mResourceGroupInclude")] public Dictionary ResourceGroupIncludeSymbol { get; set; } = new(); @@ -75,7 +75,7 @@ public class ChoreResource [MetaMember("mhObject")] public HandleBase ObjectHandle { get; set; } = new(); // Bone only - + public object? Embedded { get; set; } [MetaClassSerializerGlobal(typeof(DefaultClassSerializer))] @@ -110,15 +110,18 @@ public override void Serialize(ref ChoreResource obj, MetaStream stream) MetaClass? description = stream.GetMetaClass(typeof(ChoreResource)); - if (stream is MetaStreamWriter streamWriter) + if (stream.Mode is MetaStreamMode.Write) { } - else if (stream is MetaStreamReader streamReader) + else if (stream.Mode is MetaStreamMode.Read) { if (description != null && description.ContainsMember("mbEmbedded") && obj.HasEmbedded) { - MetaClassType embeddedClassType = streamReader.ReadMetaClassType(); - Symbol _ = streamReader.ReadSymbol(); + MetaClassType? embeddedClassType = stream.ReadMetaClassType(); + if (embeddedClassType is null) + throw new InvalidOperationException("[ChoreResource] Embedded type is not registered."); + + Symbol _ = stream.ReadSymbol(); object? embedded = Activator.CreateInstance(embeddedClassType.LinkingType); @@ -128,4 +131,4 @@ public override void Serialize(ref ChoreResource obj, MetaStream stream) } } } -} \ No newline at end of file +} diff --git a/src/TelltaleToolKit/T3Types/Chores/WalkPath.cs b/src/TelltaleToolKit/T3Types/Chores/WalkPath.cs index 7e14a07..81ba6d0 100644 --- a/src/TelltaleToolKit/T3Types/Chores/WalkPath.cs +++ b/src/TelltaleToolKit/T3Types/Chores/WalkPath.cs @@ -10,7 +10,7 @@ public class WalkPath { [MetaMember("mName")] public string Name { get; set; } - + public List Paths { get; set; } public class Serializer : MetaClassSerializer { @@ -21,18 +21,21 @@ public override void Serialize(ref WalkPath obj, MetaStream stream) DefaultSerializer.PreSerialize(ref obj, stream); DefaultSerializer.Serialize(ref obj, stream); - if (stream is MetaStreamWriter) + if (stream.Mode is MetaStreamMode.Write) { throw new NotImplementedException($"There is no serializer for {SerializationType}"); } - if (stream is MetaStreamReader streamReader) + if (stream.Mode is MetaStreamMode.Read) { - int count = streamReader.ReadInt32(); - MetaClassType type = streamReader.ReadMetaClassType(); + int count = stream.ReadInt32(); + MetaClassType? type = stream.ReadMetaClassType(); + if (type is null) + throw new InvalidOperationException("[WalkPath] Type is not registered."); + MetaClassSerializer classTypeSerializer = Toolkit.Instance.GetSerializer(type.LinkingType); - + for (var i = 0; i < count; i++) { object? value = null; @@ -48,4 +51,4 @@ public override void Serialize(ref WalkPath obj, MetaStream stream) } } } -} \ No newline at end of file +} diff --git a/src/TelltaleToolKit/T3Types/Dialogs/Dlg/ChildSets/DlgChildSet.cs b/src/TelltaleToolKit/T3Types/Dialogs/Dlg/ChildSets/DlgChildSet.cs index 71895ab..4876e46 100644 --- a/src/TelltaleToolKit/T3Types/Dialogs/Dlg/ChildSets/DlgChildSet.cs +++ b/src/TelltaleToolKit/T3Types/Dialogs/Dlg/ChildSets/DlgChildSet.cs @@ -25,18 +25,21 @@ public override void PreSerialize(ref DlgChildSet obj, MetaStream stream, MetaCl public override void Serialize(ref DlgChildSet obj, MetaStream stream) { - if (stream is MetaStreamWriter streamWriter) + if (stream.Mode is MetaStreamMode.Write) { throw new NotSupportedException(); } - if (stream is MetaStreamReader streamReader) + if (stream.Mode is MetaStreamMode.Read) { - int numChildren = streamReader.ReadInt32(); + int numChildren = stream.ReadInt32(); for (var i = 0; i < numChildren; i++) { - MetaClassType type = streamReader.ReadMetaClassType(); + MetaClassType? type = stream.ReadMetaClassType(); + if (type == null) + throw new InvalidOperationException("[DlgChildSet] Type is not registered."); + MetaClassSerializer childSerializer = Toolkit.Instance.GetSerializer(type.LinkingType); object? dlgChild = null; @@ -49,4 +52,4 @@ public override void Serialize(ref DlgChildSet obj, MetaStream stream) } } } -} \ No newline at end of file +} diff --git a/src/TelltaleToolKit/T3Types/Dialogs/Dlg/Conditions/DlgConditionSet.cs b/src/TelltaleToolKit/T3Types/Dialogs/Dlg/Conditions/DlgConditionSet.cs index 6bcfee3..567c819 100644 --- a/src/TelltaleToolKit/T3Types/Dialogs/Dlg/Conditions/DlgConditionSet.cs +++ b/src/TelltaleToolKit/T3Types/Dialogs/Dlg/Conditions/DlgConditionSet.cs @@ -8,29 +8,32 @@ namespace TelltaleToolKit.T3Types.Dialogs.Dlg; public class DlgConditionSet { public List Conditions { get; set; } = []; - + public class Serializer : MetaClassSerializer { - public override void PreSerialize(ref DlgConditionSet obj, MetaStream stream, MetaClassType? type = null ) + public override void PreSerialize(ref DlgConditionSet obj, MetaStream stream, MetaClassType? type = null) { obj ??= new DlgConditionSet(); } - - + + public override void Serialize(ref DlgConditionSet obj, MetaStream stream) { - if (stream is MetaStreamWriter streamWriter) + if (stream.Mode is MetaStreamMode.Write) { throw new NotSupportedException(); } - if (stream is MetaStreamReader streamReader) + if (stream.Mode is MetaStreamMode.Read) { - int numChildren = streamReader.ReadInt32(); + int numChildren = stream.ReadInt32(); for (var i = 0; i < numChildren; i++) { - MetaClassType type = streamReader.ReadMetaClassType(); + MetaClassType? type = stream.ReadMetaClassType(); + if (type == null) + throw new InvalidOperationException("[DlgConditionSet] Type is not registered."); + MetaClassSerializer conditionSerializer = Toolkit.Instance.GetSerializer(type.LinkingType); object? dlgConditionSet = null; @@ -43,4 +46,4 @@ public override void Serialize(ref DlgConditionSet obj, MetaStream stream) } } } -} \ No newline at end of file +} diff --git a/src/TelltaleToolKit/T3Types/Dialogs/Dlg/DepedencyLoader.cs b/src/TelltaleToolKit/T3Types/Dialogs/Dlg/DepedencyLoader.cs index d1eacf2..bf6d0b4 100644 --- a/src/TelltaleToolKit/T3Types/Dialogs/Dlg/DepedencyLoader.cs +++ b/src/TelltaleToolKit/T3Types/Dialogs/Dlg/DepedencyLoader.cs @@ -14,14 +14,14 @@ public class Serializer : MetaClassSerializer { public override void Serialize(ref DependencyLoader obj, MetaStream stream) { - if (stream is MetaStreamWriter) + if (stream.Mode is MetaStreamMode.Write) { throw new NotImplementedException(); } - if (stream is MetaStreamReader streamReader) + if (stream.Mode is MetaStreamMode.Read) { - bool hasResourceNames = streamReader.ReadBoolean(); + bool hasResourceNames = stream.ReadBoolean(); if (!hasResourceNames) return; @@ -29,7 +29,10 @@ public override void Serialize(ref DependencyLoader obj, MetaStream stream) // This is a little bit weird. // In the real serialization function, the object is base-casted to DCArray. (base-casting is casting to a parent class). // Which leads to the question - which type does inherit DCArray? It does not make any sense. - MetaClassType type = streamReader.ReadMetaClassType(); + MetaClassType? type = stream.ReadMetaClassType(); + + if (type == null) + throw new InvalidOperationException("[DependencyLoader] Type is not registered."); if (type.Symbol.DebugString != null && !type.Symbol.DebugString.Equals("DCArray")) { @@ -37,7 +40,6 @@ public override void Serialize(ref DependencyLoader obj, MetaStream stream) } List objResourceNames = obj.ResourceNames; - stream.PreSerialize(ref objResourceNames); stream.Serialize(ref objResourceNames); } } diff --git a/src/TelltaleToolKit/T3Types/Dialogs/Dlg/Dlg.cs b/src/TelltaleToolKit/T3Types/Dialogs/Dlg/Dlg.cs index be41bf8..0083d8b 100644 --- a/src/TelltaleToolKit/T3Types/Dialogs/Dlg/Dlg.cs +++ b/src/TelltaleToolKit/T3Types/Dialogs/Dlg/Dlg.cs @@ -68,29 +68,32 @@ public override void Serialize(ref Dlg obj, MetaStream stream) MetaClass metaClass = stream.GetMetaClass(typeof(Dlg))!; - if (stream is MetaStreamWriter) + if (stream.Mode is MetaStreamMode.Write) { throw new NotImplementedException(); } - if (stream is MetaStreamReader streamReader) + if (stream.Mode is MetaStreamMode.Read) { - int folderCount = streamReader.ReadInt32(); + int folderCount = stream.ReadInt32(); obj.Folders.Capacity = folderCount; for (var i = 0; i < folderCount; i++) { var folder = new DlgFolder(); - stream.PreSerialize(ref folder); stream.Serialize(ref folder); obj.Folders.Add(folder); } - int nodeCount = streamReader.ReadInt32(); + int nodeCount = stream.ReadInt32(); obj.Nodes.Capacity = nodeCount; for (var i = 0; i < nodeCount; i++) { - MetaClassType type = streamReader.ReadMetaClassType(); + MetaClassType? type = stream.ReadMetaClassType(); + + if (type == null) + throw new InvalidOperationException("[Dlg] Type is not registered."); + MetaClassSerializer metaClassSerializer = Toolkit.Instance.GetSerializer(type.LinkingType); object node = null!; @@ -106,9 +109,9 @@ public override void Serialize(ref Dlg obj, MetaStream stream) if (metaClass.ContainsMember("mbHasToolOnlyData")) { // Read runtime boolean - _ = streamReader.ReadBoolean(); + _ = stream.ReadBoolean(); } } } } -} \ No newline at end of file +} diff --git a/src/TelltaleToolKit/T3Types/Dialogs/Dlg/DlgObjectProps.cs b/src/TelltaleToolKit/T3Types/Dialogs/Dlg/DlgObjectProps.cs index 2d66c91..30d5ebf 100644 --- a/src/TelltaleToolKit/T3Types/Dialogs/Dlg/DlgObjectProps.cs +++ b/src/TelltaleToolKit/T3Types/Dialogs/Dlg/DlgObjectProps.cs @@ -14,10 +14,10 @@ public enum PropsType { ProductionProps = 2, ToolProps = 4 } - + [MetaMember("mFlags")] public Flags Flags { get; set; } - + public PropertySet UserProperties { get; set; } public PropertySet ProductionProperties { get; set; } public PropertySet ToolProperties { get; set; } @@ -30,37 +30,34 @@ public override void Serialize(ref DlgObjectProps obj, MetaStream stream) DefaultSerializer.PreSerialize(ref obj, stream); DefaultSerializer.Serialize(ref obj, stream); - if (stream is MetaStreamWriter streamWriter) + if (stream.Mode is MetaStreamMode.Write) { throw new NotImplementedException(); } - if (stream is MetaStreamReader streamReader) + if (stream.Mode is MetaStreamMode.Read) { if ((obj.Flags.Data & (int)PropsType.UserProps) != 0) { var userProps = new PropertySet(); - stream.PreSerialize(ref userProps); stream.Serialize(ref userProps); obj.UserProperties = userProps; } - + if ((obj.Flags.Data & (int)PropsType.ProductionProps) != 0) { var prodProps = new PropertySet(); - stream.PreSerialize(ref prodProps); stream.Serialize(ref prodProps); obj.ProductionProperties = prodProps; } - + if ((obj.Flags.Data & (int)PropsType.ToolProps) != 0) { var toolProps = new PropertySet(); - stream.PreSerialize(ref toolProps); stream.Serialize(ref toolProps); obj.ToolProperties = toolProps; } } } } -} \ No newline at end of file +} diff --git a/src/TelltaleToolKit/T3Types/Dialogs/Dlg/DlgVisibilityConditions.cs b/src/TelltaleToolKit/T3Types/Dialogs/Dlg/DlgVisibilityConditions.cs index 423fc9c..da70453 100644 --- a/src/TelltaleToolKit/T3Types/Dialogs/Dlg/DlgVisibilityConditions.cs +++ b/src/TelltaleToolKit/T3Types/Dialogs/Dlg/DlgVisibilityConditions.cs @@ -32,18 +32,17 @@ public override void Serialize(ref DlgVisibilityConditions obj, MetaStream strea DefaultSerializer.PreSerialize(ref obj, stream); DefaultSerializer.Serialize(ref obj, stream); - if (stream is MetaStreamWriter streamWriter) + if (stream.Mode is MetaStreamMode.Write) { } - else if (stream is MetaStreamReader streamReader) + else if (stream.Mode is MetaStreamMode.Read) { if ((obj.Flags.Data & 1) == 0) return; Rule objRule = obj.Rule; - stream.PreSerialize(ref objRule); stream.Serialize(ref objRule); } } } -} \ No newline at end of file +} diff --git a/src/TelltaleToolKit/T3Types/Dialogs/Dlg/JiraRecordManager.cs b/src/TelltaleToolKit/T3Types/Dialogs/Dlg/JiraRecordManager.cs index b16e5a6..2bbecc3 100644 --- a/src/TelltaleToolKit/T3Types/Dialogs/Dlg/JiraRecordManager.cs +++ b/src/TelltaleToolKit/T3Types/Dialogs/Dlg/JiraRecordManager.cs @@ -30,18 +30,18 @@ public override void Serialize(ref JiraRecordManager obj, MetaStream stream) return; } - if (stream is MetaStreamWriter) + if (stream.Mode is MetaStreamMode.Write) { throw new NotImplementedException(); } - if (stream is MetaStreamReader streamReader) + if (stream.Mode is MetaStreamMode.Read) { - int numRecords = streamReader.ReadInt32(); - + int numRecords = stream.ReadInt32(); + for (int i = 0; i < numRecords; i++) { - string key = streamReader.ReadString(); + string key = stream.ReadString(); var record = new JiraRecord(); obj.Records[key] = record; @@ -51,4 +51,4 @@ public override void Serialize(ref JiraRecordManager obj, MetaStream stream) stream.EndDebugSection(); } } -} \ No newline at end of file +} diff --git a/src/TelltaleToolKit/T3Types/Dialogs/Dlg/Nodes/DlgNodeExchange.cs b/src/TelltaleToolKit/T3Types/Dialogs/Dlg/Nodes/DlgNodeExchange.cs index cce0b47..1d27654 100644 --- a/src/TelltaleToolKit/T3Types/Dialogs/Dlg/Nodes/DlgNodeExchange.cs +++ b/src/TelltaleToolKit/T3Types/Dialogs/Dlg/Nodes/DlgNodeExchange.cs @@ -50,17 +50,16 @@ public override void Serialize(ref DlgNodeExchange obj, MetaStream stream) DefaultClassSerializer.PreSerialize(ref obj, stream); DefaultClassSerializer.Serialize(ref obj, stream); - if (stream is MetaStreamWriter streamWriter) + if (stream.Mode is MetaStreamMode.Write) { throw new NotSupportedException(); } - if (stream is MetaStreamReader) + if (stream.Mode is MetaStreamMode.Read) { if ((obj.DlgNode.Flags.Data & 1) != 0) { var notes = new NoteCollection(); - stream.PreSerialize(ref notes); stream.Serialize(ref notes); obj.Notes = notes; } @@ -68,11 +67,10 @@ public override void Serialize(ref DlgNodeExchange obj, MetaStream stream) if ((obj.DlgNode.Flags.Data & 2) != 0) { var lines = new DlgLineCollection(); - stream.PreSerialize(ref lines); stream.Serialize(ref lines); obj.Lines = lines; } } } } -} \ No newline at end of file +} diff --git a/src/TelltaleToolKit/T3Types/Dialogs/Dlg/Note.cs b/src/TelltaleToolKit/T3Types/Dialogs/Dlg/Note.cs index 6313efc..2332a49 100644 --- a/src/TelltaleToolKit/T3Types/Dialogs/Dlg/Note.cs +++ b/src/TelltaleToolKit/T3Types/Dialogs/Dlg/Note.cs @@ -14,7 +14,7 @@ public class Note : IGenerator, IOwner [MetaMember("Baseclass_UID::Owner")] public Owner Owner { get; set; } - + [MetaMember("mEntries")] public List Entries { get; set; } = []; @@ -51,23 +51,22 @@ public override void Serialize(ref Note obj, MetaStream stream) DefaultClassSerializer.PreSerialize(ref obj, stream); DefaultClassSerializer.Serialize(ref obj, stream); - if (stream is MetaStreamWriter streamWriter) + if (stream.Mode is MetaStreamMode.Write) { throw new NotSupportedException(); } - if (stream is MetaStreamReader streamReader) + if (stream.Mode is MetaStreamMode.Read) { // mEntries is not serialized. - int numEntries = streamReader.ReadInt32(); + int numEntries = stream.ReadInt32(); for (var i = 0; i < numEntries; i++) { Entry? entry = null; - stream.PreSerialize(ref entry); stream.Serialize(ref entry); obj.Entries.Add(entry); } } } } -} \ No newline at end of file +} diff --git a/src/TelltaleToolKit/T3Types/Dialogs/Dlg/NoteCollection.cs b/src/TelltaleToolKit/T3Types/Dialogs/Dlg/NoteCollection.cs index 321903a..5093548 100644 --- a/src/TelltaleToolKit/T3Types/Dialogs/Dlg/NoteCollection.cs +++ b/src/TelltaleToolKit/T3Types/Dialogs/Dlg/NoteCollection.cs @@ -18,19 +18,18 @@ public class Serializer : MetaClassSerializer { public override void Serialize(ref NoteCollection obj, MetaStream stream) { - if (stream is MetaStreamWriter streamWriter) + if (stream.Mode is MetaStreamMode.Write) { throw new NotSupportedException(); } - if (stream is MetaStreamReader streamReader) + if (stream.Mode is MetaStreamMode.Read) { // mEntries is not serialized. - int numNotes = streamReader.ReadInt32(); + int numNotes = stream.ReadInt32(); for (var i = 0; i < numNotes; i++) { Note? note = null; - stream.PreSerialize(ref note); stream.Serialize(ref note); // TODO: Set the correct IDs. Not a priority, I haven't seen this serialized anywhere. obj.Notes.Add(i, note); @@ -38,4 +37,4 @@ public override void Serialize(ref NoteCollection obj, MetaStream stream) } } } -} \ No newline at end of file +} diff --git a/src/TelltaleToolKit/T3Types/Dialogs/DlgSettings/DlgObjectPropsMap.cs b/src/TelltaleToolKit/T3Types/Dialogs/DlgSettings/DlgObjectPropsMap.cs index 42655b1..e437654 100644 --- a/src/TelltaleToolKit/T3Types/Dialogs/DlgSettings/DlgObjectPropsMap.cs +++ b/src/TelltaleToolKit/T3Types/Dialogs/DlgSettings/DlgObjectPropsMap.cs @@ -33,23 +33,22 @@ public class Serializer : MetaClassSerializer { public override void Serialize(ref DlgObjectPropsMap obj, MetaStream stream) { - if (stream is MetaStreamWriter streamWriter) + if (stream.Mode is MetaStreamMode.Write) { throw new NotSupportedException(); } - if (stream is MetaStreamReader streamReader) + if (stream.Mode is MetaStreamMode.Read) { - int groupDefinitionSize = streamReader.ReadInt32(); + int groupDefinitionSize = stream.ReadInt32(); for (var i = 0; i < groupDefinitionSize; i++) { var groupDefinition = new GroupDefinition(); - stream.PreSerialize(ref groupDefinition); stream.Serialize(ref groupDefinition); obj.GroupDefinitions.Add(groupDefinition); } } } } -} \ No newline at end of file +} diff --git a/src/TelltaleToolKit/T3Types/Dialogs/Dlog/DialogBase.cs b/src/TelltaleToolKit/T3Types/Dialogs/Dlog/DialogBase.cs index 172fe66..e5da38a 100644 --- a/src/TelltaleToolKit/T3Types/Dialogs/Dlog/DialogBase.cs +++ b/src/TelltaleToolKit/T3Types/Dialogs/Dlog/DialogBase.cs @@ -48,14 +48,14 @@ public override void Serialize(ref DialogBase obj, MetaStream stream) { new DefaultClassSerializer().Serialize(ref obj, stream); - if (stream is MetaStreamWriter streamWriter) + if (stream.Mode is MetaStreamMode.Write) { if (obj.HasStyleGuides) { Toolkit.Instance.GetSerializer>().Serialize(ref obj.StyleGuideRefs, stream); } } - else if (stream is MetaStreamReader streamReader) + else if (stream.Mode is MetaStreamMode.Read) { if (obj.HasStyleGuides) { @@ -64,4 +64,4 @@ public override void Serialize(ref DialogBase obj, MetaStream stream) } } } -} \ No newline at end of file +} diff --git a/src/TelltaleToolKit/T3Types/Dialogs/Dlog/DialogResource.cs b/src/TelltaleToolKit/T3Types/Dialogs/Dlog/DialogResource.cs index a86aacf..bf8776a 100644 --- a/src/TelltaleToolKit/T3Types/Dialogs/Dlog/DialogResource.cs +++ b/src/TelltaleToolKit/T3Types/Dialogs/Dlog/DialogResource.cs @@ -54,26 +54,26 @@ public class DialogResource public class Serializer : MetaClassSerializer { private static readonly DefaultClassSerializer DefaultSerializer = new(); - + public override void Serialize(ref DialogResource obj, MetaStream stream) { DefaultSerializer.PreSerialize(ref obj, stream); DefaultSerializer.Serialize(ref obj, stream); - if (stream is MetaStreamWriter streamWriter) + if (stream.Mode is MetaStreamMode.Write) { throw new NotImplementedException($"Serializer is not implement for {SerializationType}"); } - if (stream is MetaStreamReader streamReader) + if (stream.Mode is MetaStreamMode.Read) { - // Console.WriteLine("Current position: " + stream.GetCurrentPosition()); - int dialogsCount = streamReader.ReadInt32(); - int branchesCount = streamReader.ReadInt32(); - int itemsCount = streamReader.ReadInt32(); - int exchangesCount = streamReader.ReadInt32(); - int linesCount = streamReader.ReadInt32(); - int textCount = streamReader.ReadInt32(); + // Console.WriteLine("Current position: " + stream.GetPosition()); + int dialogsCount = stream.ReadInt32(); + int branchesCount = stream.ReadInt32(); + int itemsCount = stream.ReadInt32(); + int exchangesCount = stream.ReadInt32(); + int linesCount = stream.ReadInt32(); + int textCount = stream.ReadInt32(); SerializeDialogBaseArray(obj.Dialogs, dialogsCount, stream); SerializeDialogBaseArray(obj.Branches, branchesCount, stream); @@ -87,10 +87,10 @@ public override void Serialize(ref DialogResource obj, MetaStream stream) private static void SerializeDialogBaseArray(List list, int count, MetaStream stream) where T : IDialogBase, new() { - if (stream is MetaStreamWriter streamWriter) + if (stream.Mode is MetaStreamMode.Write) { } - else if (stream is MetaStreamReader streamReader) + else if (stream.Mode is MetaStreamMode.Read) { // Console.WriteLine(count); if (count <= 0) @@ -101,7 +101,7 @@ private static void SerializeDialogBaseArray(List list, int count, MetaStr var ids = new int[count]; for (int i = 0; i < count; i++) { - ids[i] = streamReader.ReadInt32(); + ids[i] = stream.ReadInt32(); } var dialog = default(T); @@ -115,4 +115,4 @@ private static void SerializeDialogBaseArray(List list, int count, MetaStr } } } -} \ No newline at end of file +} diff --git a/src/TelltaleToolKit/T3Types/Handle.cs b/src/TelltaleToolKit/T3Types/Handle.cs index bd725bf..6362fbb 100644 --- a/src/TelltaleToolKit/T3Types/Handle.cs +++ b/src/TelltaleToolKit/T3Types/Handle.cs @@ -35,8 +35,7 @@ public override void PreSerialize(ref HandleBase obj, MetaStream stream, MetaCla public override void Serialize(ref HandleBase obj, MetaStream stream) { - if (stream.Configuration.Version is MetaStreamVersion.Msv4 or MetaStreamVersion.Msv5 - or MetaStreamVersion.Msv6) + if (stream.Params.StreamVersion >= 4) { stream.Serialize(ref obj.ObjectInfo.ObjectName); } diff --git a/src/TelltaleToolKit/T3Types/Languages/Landb/LanguageDb.cs b/src/TelltaleToolKit/T3Types/Languages/Landb/LanguageDb.cs index 76bd7dc..6b34700 100644 --- a/src/TelltaleToolKit/T3Types/Languages/Landb/LanguageDb.cs +++ b/src/TelltaleToolKit/T3Types/Languages/Landb/LanguageDb.cs @@ -41,11 +41,11 @@ public override void Serialize(ref LanguageDb obj, MetaStream stream) DefaultSerializer.PreSerialize(ref obj, stream); DefaultSerializer.Serialize(ref obj, stream); - if (stream is MetaStreamReader streamReader) + if (stream.Mode is MetaStreamMode.Read) { // There should be things in the debug section that I would like to parse. // It's not required, but it may contain useful information. } } } -} \ No newline at end of file +} diff --git a/src/TelltaleToolKit/T3Types/Mathematics/Color.cs b/src/TelltaleToolKit/T3Types/Mathematics/Color.cs index 81c7190..8f09ab5 100644 --- a/src/TelltaleToolKit/T3Types/Mathematics/Color.cs +++ b/src/TelltaleToolKit/T3Types/Mathematics/Color.cs @@ -26,25 +26,25 @@ public class Serializer : MetaClassSerializer { public override void Serialize(ref Color obj, MetaStream stream) { - if (stream is MetaStreamWriter streamWriter) + if (stream.Mode is MetaStreamMode.Write) { - if (streamWriter.Configuration.CanModifySerializedClassesList) + if (stream.Params.CanModifySerializedClassesList) { MetaClass? description = stream.GetMetaClass(typeof(Color)); - streamWriter.AddVersionInfo(description); + stream.AddVersionInfo(description); } - streamWriter.Write(obj.R); - streamWriter.Write(obj.G); - streamWriter.Write(obj.B); - streamWriter.Write(obj.A); + stream.Write(obj.R); + stream.Write(obj.G); + stream.Write(obj.B); + stream.Write(obj.A); } - else if (stream is MetaStreamReader streamReader) + else if (stream.Mode is MetaStreamMode.Read) { - obj.R = streamReader.ReadSingle(); - obj.G = streamReader.ReadSingle(); - obj.B = streamReader.ReadSingle(); - obj.A = streamReader.ReadSingle(); + obj.R = stream.ReadSingle(); + obj.G = stream.ReadSingle(); + obj.B = stream.ReadSingle(); + obj.A = stream.ReadSingle(); } } } @@ -53,4 +53,4 @@ public override string ToString() { return $"Color: ({R}, {G}, {B}, {A})"; } -} \ No newline at end of file +} diff --git a/src/TelltaleToolKit/T3Types/Mathematics/Polar.cs b/src/TelltaleToolKit/T3Types/Mathematics/Polar.cs index 9fca8dc..6101e05 100644 --- a/src/TelltaleToolKit/T3Types/Mathematics/Polar.cs +++ b/src/TelltaleToolKit/T3Types/Mathematics/Polar.cs @@ -21,17 +21,17 @@ public static void Serialize(ref object obj, MetaStream stream, MetaClass desc) { Polar instance = (Polar)obj; - if (stream is MetaStreamWriter streamWriter) + if (stream.Mode is MetaStreamMode.Write) { - streamWriter.Write(instance.Radius); - streamWriter.Write(instance.Theta); - streamWriter.Write(instance.Phi); + stream.Write(instance.Radius); + stream.Write(instance.Theta); + stream.Write(instance.Phi); } - else if (stream is MetaStreamReader streamReader) + else if (stream.Mode is MetaStreamMode.Read) { - instance.Radius = streamReader.ReadSingle(); - instance.Theta = streamReader.ReadSingle(); - instance.Phi = streamReader.ReadSingle(); + instance.Radius = stream.ReadSingle(); + instance.Theta = stream.ReadSingle(); + instance.Phi = stream.ReadSingle(); } } -} \ No newline at end of file +} diff --git a/src/TelltaleToolKit/T3Types/Mathematics/TRange.cs b/src/TelltaleToolKit/T3Types/Mathematics/TRange.cs index cc68b80..1b0d9d3 100644 --- a/src/TelltaleToolKit/T3Types/Mathematics/TRange.cs +++ b/src/TelltaleToolKit/T3Types/Mathematics/TRange.cs @@ -22,7 +22,7 @@ public class RangeClassSerializer : MetaClassSerializer> /// public override void PreSerialize(ref Range obj, MetaStream stream, MetaClassType? type) { - if (stream is MetaStreamReader streamReader) + if (stream.Mode is MetaStreamMode.Read) { if (obj is null) obj = new Range(); @@ -34,4 +34,4 @@ public override void Serialize(ref Range obj, MetaStream stream) DefaultSerializer.PreSerialize(ref obj, stream); DefaultSerializer.Serialize(ref obj, stream); } -} \ No newline at end of file +} diff --git a/src/TelltaleToolKit/T3Types/Mathematics/TRect.cs b/src/TelltaleToolKit/T3Types/Mathematics/TRect.cs index 35eb0fe..1d5ec3e 100644 --- a/src/TelltaleToolKit/T3Types/Mathematics/TRect.cs +++ b/src/TelltaleToolKit/T3Types/Mathematics/TRect.cs @@ -23,7 +23,7 @@ public class RectClassSerializer : MetaClassSerializer> /// public override void PreSerialize(ref Rect obj, MetaStream stream, MetaClassType? type) { - if (stream is MetaStreamReader streamReader) + if (stream.Mode is MetaStreamMode.Read) { if (obj is null) obj = new Rect(); @@ -35,4 +35,4 @@ public override void Serialize(ref Rect obj, MetaStream stream) { DefaultSerializer.Serialize(ref obj, stream); } -} \ No newline at end of file +} diff --git a/src/TelltaleToolKit/T3Types/Meshes/D3DMesh.cs b/src/TelltaleToolKit/T3Types/Meshes/D3DMesh.cs index ca076ca..f7d8e70 100644 --- a/src/TelltaleToolKit/T3Types/Meshes/D3DMesh.cs +++ b/src/TelltaleToolKit/T3Types/Meshes/D3DMesh.cs @@ -302,17 +302,17 @@ public override void Serialize(ref TriangleSet obj, MetaStream stream) DefaultSerializer.PreSerialize(ref obj, stream); DefaultSerializer.Serialize(ref obj, stream); - if (stream is MetaStreamWriter streamWriter) + if (stream.Mode is MetaStreamMode.Write) { } - else if (stream is MetaStreamReader streamReader) + else if (stream.Mode is MetaStreamMode.Read) { if (stream.GetMetaClass(typeof(TriangleSet)).ContainsMember("mbHasPixelShader_RemoveMe")) { - string shaderName = streamReader.ReadString(); + string shaderName = stream.ReadString(); if (obj.HasPixelShaderRemoveMe) { - string pixelShader = streamReader.ReadString(); + string pixelShader = stream.ReadString(); } } } @@ -355,7 +355,7 @@ public override void Serialize(ref D3DMesh obj, MetaStream stream) DefaultSerializer.PreSerialize(ref obj, stream); DefaultSerializer.Serialize(ref obj, stream); - if (stream is MetaStreamWriter streamWriter) + if (stream.Mode is MetaStreamMode.Write) { if (obj.Version < 19) { @@ -369,13 +369,13 @@ public override void Serialize(ref D3DMesh obj, MetaStream stream) SerializeInternalResources(ref obj, stream); } - streamWriter.Write(0); + stream.Write(0); if (obj.Version >= 52) { bool hasOcclusionData = obj.OcclusionMeshData != null; - streamWriter.Write(hasOcclusionData); + stream.Write(hasOcclusionData); if (hasOcclusionData) { @@ -394,7 +394,7 @@ public override void Serialize(ref D3DMesh obj, MetaStream stream) Toolkit.Instance.GetSerializer().Serialize(ref objMeshData, stream); stream.EndBlock(); } - else if (stream is MetaStreamReader streamReader) + else if (stream.Mode is MetaStreamMode.Read) { // According to Telltale, the new meshes start from version 20. if (obj.Version == 0) @@ -414,19 +414,19 @@ public override void Serialize(ref D3DMesh obj, MetaStream stream) SerializeInternalResources(ref obj, stream); } - int toolData = streamReader.ReadInt32(); + int toolData = stream.ReadInt32(); // Telltale skip this data. if (toolData > 0) { stream.BeginBlock(); - streamReader.SkipToEndOfCurrentBlock(); + stream.SkipToEndOfCurrentBlock(); stream.EndBlock(); } // Probably new meshes? if (obj.Version >= 52) { - bool hasOcclusionData = streamReader.ReadBoolean(); + bool hasOcclusionData = stream.ReadBoolean(); if (hasOcclusionData) { // TODO: Assign this. @@ -448,12 +448,12 @@ public override void Serialize(ref D3DMesh obj, MetaStream stream) private static void SerializeOldMedD3DMesh(ref D3DMesh obj, MetaStream stream) { - if (stream is MetaStreamWriter streamWriter) + if (stream.Mode is MetaStreamMode.Write) { throw new NotSupportedException(); } - if (stream is MetaStreamReader streamReader) + if (stream.Mode is MetaStreamMode.Read) { obj.T3VertexBuffers = new T3VertexBuffer[15]; @@ -562,7 +562,7 @@ private static void SerializeOldMedD3DMesh(ref D3DMesh obj, MetaStream stream) obj.T3VertexBuffers[13] = buffer; } // Read Index Buffer - // bool hasIndexBuffer = streamReader.ReadBoolean(); + // bool hasIndexBuffer = stream.ReadBoolean(); // // if (hasIndexBuffer) // { @@ -580,7 +580,7 @@ private static void SerializeOldMedD3DMesh(ref D3DMesh obj, MetaStream stream) // // for (int i = 0; i < obj.T3VertexBuffers.Length; i++) // { - // bool hasVertexBuffer = streamReader.ReadBoolean(); + // bool hasVertexBuffer = stream.ReadBoolean(); // // if (!hasVertexBuffer) // continue; @@ -595,15 +595,15 @@ private static void SerializeOldMedD3DMesh(ref D3DMesh obj, MetaStream stream) private static void SerializeOldD3DMesh(ref D3DMesh obj, MetaStream stream) { - if (stream is MetaStreamWriter streamWriter) + if (stream.Mode is MetaStreamMode.Write) { throw new NotSupportedException(); } - if (stream is MetaStreamReader streamReader) + if (stream.Mode is MetaStreamMode.Read) { // Read Index Buffer - bool hasIndexBuffer = streamReader.ReadBoolean(); + bool hasIndexBuffer = stream.ReadBoolean(); if (hasIndexBuffer) { @@ -623,7 +623,7 @@ private static void SerializeOldD3DMesh(ref D3DMesh obj, MetaStream stream) for (int i = 0; i < obj.T3VertexBuffers.Length; i++) { - bool hasVertexBuffer = streamReader.ReadBoolean(); + bool hasVertexBuffer = stream.ReadBoolean(); if (!hasVertexBuffer) continue; @@ -638,11 +638,11 @@ private static void SerializeOldD3DMesh(ref D3DMesh obj, MetaStream stream) private static void SerializeInternalResources(ref D3DMesh obj, MetaStream stream) { - if (stream is MetaStreamWriter streamWriter) + if (stream.Mode is MetaStreamMode.Write) { obj.InternalResources ??= []; - streamWriter.Write(obj.InternalResources.Count); + stream.Write(obj.InternalResources.Count); foreach (HandleBase handle in obj.InternalResources) { @@ -657,8 +657,8 @@ private static void SerializeInternalResources(ref D3DMesh obj, MetaStream strea MetaClassType typeSymbol = metaClass.ClassType; handle.ObjectInfo.Type = typeSymbol; - streamWriter.Write(symbol); - streamWriter.Write(typeSymbol.Symbol.Crc64); + stream.Write(symbol); + stream.Write(typeSymbol.Symbol.Crc64); stream.BeginBlock(); @@ -676,25 +676,32 @@ private static void SerializeInternalResources(ref D3DMesh obj, MetaStream strea return; } - if (stream is MetaStreamReader streamReader) + if (stream.Mode is MetaStreamMode.Read) { obj.InternalResources = []; // Part of DC Array - int numRes = streamReader.ReadInt32(); + int numRes = stream.ReadInt32(); for (var i = 0; i < numRes; i++) { // Handle name? - Symbol symbol = streamReader.ReadSymbol(); - MetaClassType typeSymbol = streamReader.ReadMetaClassType(); + Symbol symbol = stream.ReadSymbol(); + MetaClassType? typeSymbol = stream.ReadMetaClassType(); stream.BeginBlock(); + if (typeSymbol is null) + { + Toolkit.Instance.Logger.LogError($"[D3DMesh] Internal resource '{symbol}' has no registered type."); + stream.EndBlock(); + continue; + } + object? propertyValue = null; Toolkit.Instance.GetSerializer(typeSymbol.LinkingType) - .PreSerialize(ref propertyValue, stream, typeSymbol); + .PreSerialize(ref propertyValue!, stream, typeSymbol); Toolkit.Instance.GetSerializer(typeSymbol.LinkingType) .Serialize(ref propertyValue, stream); diff --git a/src/TelltaleToolKit/T3Types/Meshes/T3Types/T3GFXBuffer.cs b/src/TelltaleToolKit/T3Types/Meshes/T3Types/T3GFXBuffer.cs index 9a53d05..9ae1d63 100644 --- a/src/TelltaleToolKit/T3Types/Meshes/T3Types/T3GFXBuffer.cs +++ b/src/TelltaleToolKit/T3Types/Meshes/T3Types/T3GFXBuffer.cs @@ -17,8 +17,8 @@ public class T3GFXBuffer public GFXPlatformFormat BufferFormat { get; set; } // [MetaMember("mBufferUsage")] This fails, because there's no flag set for this. - // public GFXPlatformBufferUsage BufferUsage { get; set; } - + // public GFXPlatformBufferUsage BufferUsage { get; set; } + [MetaMember("mBufferUsage")] public uint BufferUsage { get; set; } @@ -46,13 +46,13 @@ public override void Serialize(ref T3GFXBuffer obj, MetaStream stream) // I assume it's done like for better performance // This requires further testing stream.BeginAsyncSection(); - if (stream is MetaStreamWriter streamWriter) + if (stream.Mode is MetaStreamMode.Write) { - streamWriter.Write(obj.Buffer); + stream.Write(obj.Buffer); } - else if (stream is MetaStreamReader streamReader) + else if (stream.Mode is MetaStreamMode.Read) { - obj.Buffer = streamReader.ReadBytes((int)(obj.Count * obj.Stride)); + obj.Buffer = stream.ReadBytes((int)(obj.Count * obj.Stride)); } stream.EndAsyncSection(); @@ -90,4 +90,4 @@ public enum GFXPlatformBufferUsage ShaderReadWriteRaw = 0x38, DrawIndirectArgs = 0x40, SingleValue = 0x80, -} \ No newline at end of file +} diff --git a/src/TelltaleToolKit/T3Types/Meshes/T3Types/T3GFXVertexState.cs b/src/TelltaleToolKit/T3Types/Meshes/T3Types/T3GFXVertexState.cs index 418211b..27823b1 100644 --- a/src/TelltaleToolKit/T3Types/Meshes/T3Types/T3GFXVertexState.cs +++ b/src/TelltaleToolKit/T3Types/Meshes/T3Types/T3GFXVertexState.cs @@ -37,7 +37,7 @@ public override void Serialize(ref T3GFXVertexState obj, MetaStream stream) MetaClass? desc = stream.GetMetaClass(typeof(T3GFXVertexState)); - if (stream is MetaStreamWriter streamWriter) + if (stream.Mode is MetaStreamMode.Write) { if (obj.AttributeCount > 32) throw new InvalidDataException("AttributeCount is too large"); @@ -53,7 +53,6 @@ public override void Serialize(ref T3GFXVertexState obj, MetaStream stream) for (var i = 0; i < obj.Attributes.Count; i++) { GFXPlatformAttributeParams attribute = obj.Attributes[i]; - stream.PreSerialize(ref attribute); stream.Serialize(ref attribute); obj.Attributes[i] = attribute; } @@ -63,7 +62,6 @@ public override void Serialize(ref T3GFXVertexState obj, MetaStream stream) for (var i = 0; i < obj.IndexBuffer.Count; i++) { T3GFXBuffer indexBuffer = obj.IndexBuffer[i]; - stream.PreSerialize(ref indexBuffer); stream.Serialize(ref indexBuffer); obj.IndexBuffer[i] = indexBuffer; } @@ -71,12 +69,11 @@ public override void Serialize(ref T3GFXVertexState obj, MetaStream stream) else { bool hasIndexBuffer = obj.IndexBuffer.Count > 0; - streamWriter.Write(hasIndexBuffer); + stream.Write(hasIndexBuffer); if (hasIndexBuffer) { T3GFXBuffer indexBuffer = obj.IndexBuffer[0]; - stream.PreSerialize(ref indexBuffer); stream.Serialize(ref indexBuffer); obj.IndexBuffer[0] = indexBuffer; } @@ -85,13 +82,12 @@ public override void Serialize(ref T3GFXVertexState obj, MetaStream stream) for (var i = 0; i < obj.VertexBuffer.Count; i++) { T3GFXBuffer vertexBuffer = obj.VertexBuffer[i]; - stream.PreSerialize(ref vertexBuffer); stream.Serialize(ref vertexBuffer); obj.VertexBuffer[i] = vertexBuffer; } } - if (stream is MetaStreamReader streamReader) + if (stream.Mode is MetaStreamMode.Read) { if (obj.AttributeCount > 32) throw new InvalidDataException("AttributeCount is too large"); @@ -105,7 +101,6 @@ public override void Serialize(ref T3GFXVertexState obj, MetaStream stream) for (var i = 0; i < obj.AttributeCount; i++) { var attribute = new GFXPlatformAttributeParams(); - stream.PreSerialize(ref attribute); stream.Serialize(ref attribute); obj.Attributes.Add(attribute); } @@ -115,7 +110,6 @@ public override void Serialize(ref T3GFXVertexState obj, MetaStream stream) for (var i = 0; i < obj.IndexBufferCount; i++) { var indexBuffer = new T3GFXBuffer(); - stream.PreSerialize(ref indexBuffer); stream.Serialize(ref indexBuffer); obj.IndexBuffer.Add(indexBuffer); } @@ -124,11 +118,10 @@ public override void Serialize(ref T3GFXVertexState obj, MetaStream stream) { obj.IndexBufferCount = 1; - bool hasIndexBuffer = streamReader.ReadBoolean(); + bool hasIndexBuffer = stream.ReadBoolean(); if (hasIndexBuffer) { var indexBuffer = new T3GFXBuffer(); - stream.PreSerialize(ref indexBuffer); stream.Serialize(ref indexBuffer); obj.IndexBuffer.Add(indexBuffer); } @@ -137,11 +130,10 @@ public override void Serialize(ref T3GFXVertexState obj, MetaStream stream) for (var i = 0; i < obj.VertexBufferCount; i++) { var submesh = new T3GFXBuffer(); - stream.PreSerialize(ref submesh); stream.Serialize(ref submesh); obj.VertexBuffer.Add(submesh); } } } } -} \ No newline at end of file +} diff --git a/src/TelltaleToolKit/T3Types/Meshes/T3Types/T3IndexBuffer.cs b/src/TelltaleToolKit/T3Types/Meshes/T3Types/T3IndexBuffer.cs index cdb2701..97f948c 100644 --- a/src/TelltaleToolKit/T3Types/Meshes/T3Types/T3IndexBuffer.cs +++ b/src/TelltaleToolKit/T3Types/Meshes/T3Types/T3IndexBuffer.cs @@ -16,10 +16,10 @@ public class T3IndexBuffer [MetaMember("mFormat")] public int Format { get; set; } - + [MetaMember("mFlags")] public Flags Flags { get; set; } - + [MetaMember("mUsage")] public int Usage { get; set; } @@ -35,15 +35,15 @@ public class T3IndexBuffer public class T3IndexBufferSerializer : MetaClassSerializer { private static readonly DefaultClassSerializer DefaultSerializer = new(); - + public override void Serialize(ref T3IndexBuffer obj, MetaStream stream) { DefaultSerializer.Serialize(ref obj, stream); - if (stream is MetaStreamWriter streamWriter) + if (stream.Mode is MetaStreamMode.Write) { } - else if (stream is MetaStreamReader streamReader) + else if (stream.Mode is MetaStreamMode.Read) { int bufferBytes = obj.Format switch { @@ -57,7 +57,7 @@ public override void Serialize(ref T3IndexBuffer obj, MetaStream stream) obj.IndexByteSize = bufferBytes; } - obj.Buffer = streamReader.ReadBytes(bufferBytes * obj.NumIndices); + obj.Buffer = stream.ReadBytes(bufferBytes * obj.NumIndices); } } } @@ -71,10 +71,10 @@ public class T3VertexComponent [MetaMember("mCount")] public uint Count { get; set; } - + [MetaMember("mType")] public EnumType Type { get; set; } - + [MetaClassSerializerGlobal(typeof(EnumSerializer))] public enum EnumType { VTypeNone = 0, @@ -108,10 +108,10 @@ public enum EnumType { // { // new DefaultClassSerializer().Serialize(ref obj, stream, desc); // -// if (stream is MetaStreamWriter streamWriter) +// if (stream.Mode is MetaStreamMode.Write) // { // } -// else if (stream is MetaStreamReader streamReader) +// else if (stream.Mode is MetaStreamMode.Read) // { // var bufferBytes = 2; // if (obj.Format != 101) @@ -119,8 +119,8 @@ public enum EnumType { // bufferBytes = 4; // } // -// obj.IndexBufferData = streamReader.ReadBytes(obj.IndexByteSize * obj.NumIndices); +// obj.IndexBufferData = stream.ReadBytes(obj.IndexByteSize * obj.NumIndices); // } // } // } -// } \ No newline at end of file +// } diff --git a/src/TelltaleToolKit/T3Types/Meshes/T3Types/T3MaterialData.cs b/src/TelltaleToolKit/T3Types/Meshes/T3Types/T3MaterialData.cs index 50d1916..bd6ee1c 100644 --- a/src/TelltaleToolKit/T3Types/Meshes/T3Types/T3MaterialData.cs +++ b/src/TelltaleToolKit/T3Types/Meshes/T3Types/T3MaterialData.cs @@ -64,15 +64,15 @@ public override void Serialize(ref T3MaterialData obj, MetaStream stream) if (classDescription == null || classDescription.ContainsMember("mCompiledData2")) return; - if (stream is MetaStreamWriter streamWriter) + if (stream.Mode is MetaStreamMode.Write) { obj.CompiledData2 ??= []; - streamWriter.Write(obj.CompiledData2.Count); + stream.Write(obj.CompiledData2.Count); for (var i = 0; i < obj.CompiledData2.Count; i++) { - streamWriter.Write(i); + stream.Write(i); T3MaterialCompiledData compiledData = obj.CompiledData2[i]; @@ -86,9 +86,9 @@ public override void Serialize(ref T3MaterialData obj, MetaStream stream) return; } - if (stream is MetaStreamReader streamReader) + if (stream.Mode is MetaStreamMode.Read) { - int numCompiledData = streamReader.ReadInt32(); + int numCompiledData = stream.ReadInt32(); obj.CompiledData2 = new List(numCompiledData); @@ -97,7 +97,7 @@ public override void Serialize(ref T3MaterialData obj, MetaStream stream) for (var i = 0; i < numCompiledData; i++) { - int materialIndex = streamReader.ReadInt32(); + int materialIndex = stream.ReadInt32(); if ((uint)materialIndex >= (uint)numCompiledData) throw new InvalidDataException( @@ -114,4 +114,4 @@ public override void Serialize(ref T3MaterialData obj, MetaStream stream) } } } -} \ No newline at end of file +} diff --git a/src/TelltaleToolKit/T3Types/Meshes/T3Types/T3MeshData.cs b/src/TelltaleToolKit/T3Types/Meshes/T3Types/T3MeshData.cs index 9110c87..0cade51 100644 --- a/src/TelltaleToolKit/T3Types/Meshes/T3Types/T3MeshData.cs +++ b/src/TelltaleToolKit/T3Types/Meshes/T3Types/T3MeshData.cs @@ -102,7 +102,7 @@ public override void Serialize(ref T3MeshData obj, MetaStream stream) DefaultSerializer.PreSerialize(ref obj, stream); DefaultSerializer.Serialize(ref obj, stream); - if (stream is MetaStreamWriter streamWriter) + if (stream.Mode is MetaStreamMode.Write) { uint uvTransformCount = 0; // Referenced from Lucas Saragosa's serialization code @@ -115,7 +115,7 @@ public override void Serialize(ref T3MeshData obj, MetaStream stream) } } - streamWriter.Write(uvTransformCount); + stream.Write(uvTransformCount); for (var i = 0; i < Math.Min(obj.TexCoordTransform.Length, 4); i++) { @@ -126,8 +126,7 @@ public override void Serialize(ref T3MeshData obj, MetaStream stream) continue; } - streamWriter.Write((uint)i); - stream.PreSerialize(ref transform); + stream.Write((uint)i); stream.Serialize(ref transform); obj.TexCoordTransform[i] = transform; } @@ -136,32 +135,29 @@ public override void Serialize(ref T3MeshData obj, MetaStream stream) if (hasCpuSkinning) { T3MeshCPUSkinningData cpuSkinning = obj.CPUSkinningData; - stream.PreSerialize(ref cpuSkinning); stream.Serialize(ref cpuSkinning); obj.CPUSkinningData = cpuSkinning; } - streamWriter.Write(obj.VertexStates.Count); + stream.Write(obj.VertexStates.Count); for (var i = 0; i < obj.VertexStates.Count; i++) { T3GFXVertexState state = obj.VertexStates[i]; - stream.PreSerialize(ref state); stream.Serialize(ref state); obj.VertexStates[i] = state; } } - if (stream is MetaStreamReader streamReader) + if (stream.Mode is MetaStreamMode.Read) { - uint uvTransforms = streamReader.ReadUInt32(); + uint uvTransforms = stream.ReadUInt32(); for (var i = 0; i < uvTransforms; i++) { - int uvLayer = streamReader.ReadInt32(); + int uvLayer = stream.ReadInt32(); // Experimenting with a helper function. - stream.PreSerialize(ref obj.TexCoordTransform[uvLayer]); stream.Serialize(ref obj.TexCoordTransform[uvLayer]); // TTKContext.Instance().GetSerializer().Serialize(ref obj.TexCoordTransform[vector], stream); } @@ -169,12 +165,11 @@ public override void Serialize(ref T3MeshData obj, MetaStream stream) if ((obj.Flags.Data & (int)MeshFlags.eHasCPUSkinning) != 0) { T3MeshCPUSkinningData t3MeshCpuSkinningData = obj.CPUSkinningData; - stream.PreSerialize(ref t3MeshCpuSkinningData); stream.Serialize(ref t3MeshCpuSkinningData); obj.CPUSkinningData = t3MeshCpuSkinningData; } - int vertexStates = streamReader.ReadInt32(); + int vertexStates = stream.ReadInt32(); for (var i = 0; i < vertexStates; i++) { @@ -201,4 +196,4 @@ public class T3MeshBonePaletteEntry [MetaMember("mNumVerts")] public int NumVerts { get; set; } -} \ No newline at end of file +} diff --git a/src/TelltaleToolKit/T3Types/Meshes/T3Types/T3MeshTextureIndices.cs b/src/TelltaleToolKit/T3Types/Meshes/T3Types/T3MeshTextureIndices.cs index eae88bc..1d46624 100644 --- a/src/TelltaleToolKit/T3Types/Meshes/T3Types/T3MeshTextureIndices.cs +++ b/src/TelltaleToolKit/T3Types/Meshes/T3Types/T3MeshTextureIndices.cs @@ -12,7 +12,7 @@ public class Serializer : MetaClassSerializer { public override void Serialize(ref T3MeshTextureIndices obj, MetaStream stream) { - if (stream is MetaStreamWriter streamWriter) + if (stream.Mode is MetaStreamMode.Write) { if (obj?.Index != null) { @@ -22,28 +22,28 @@ public override void Serialize(ref T3MeshTextureIndices obj, MetaStream stream) if ((index & int.MinValue) == 0) // int.MinValue == 0x80000000 { int idx = i; - streamWriter.Write(idx); - streamWriter.Write(index); + stream.Write(idx); + stream.Write(index); } } } // Write the end - streamWriter.Write(-1); + stream.Write(-1); } - else if (stream is MetaStreamReader streamReader) + else if (stream.Mode is MetaStreamMode.Read) { - int i = streamReader.ReadInt32(); + int i = stream.ReadInt32(); while ((i & int.MinValue) == 0) { - int x = streamReader.ReadInt32(); + int x = stream.ReadInt32(); if (i < 2) obj.Index[i] = x; - i = streamReader.ReadInt32(); + i = stream.ReadInt32(); } } } } -} \ No newline at end of file +} diff --git a/src/TelltaleToolKit/T3Types/Meshes/T3Types/T3VertexBuffer.cs b/src/TelltaleToolKit/T3Types/Meshes/T3Types/T3VertexBuffer.cs index c967429..3f9733e 100644 --- a/src/TelltaleToolKit/T3Types/Meshes/T3Types/T3VertexBuffer.cs +++ b/src/TelltaleToolKit/T3Types/Meshes/T3Types/T3VertexBuffer.cs @@ -49,12 +49,12 @@ public override void Serialize(ref T3VertexBuffer obj, MetaStream stream) { DefaultSerializer.Serialize(ref obj, stream); - if (stream is MetaStreamWriter) + if (stream.Mode is MetaStreamMode.Write) { throw new NotImplementedException($"There is no serializer for {SerializationType}"); } - if (stream is MetaStreamReader streamReader) + if (stream.Mode is MetaStreamMode.Read) { int totalBytes = obj.StoreCompressed switch { @@ -62,7 +62,7 @@ public override void Serialize(ref T3VertexBuffer obj, MetaStream stream) _ => obj.NumVerts * obj.VertSize }; - obj.Buffer = streamReader.ReadBytes(totalBytes); + obj.Buffer = stream.ReadBytes(totalBytes); } } } @@ -88,12 +88,12 @@ public override void Serialize(ref T3VertexBuffer obj, MetaStream stream) // { // new DefaultClassSerializer().Serialize(ref obj, stream, desc); // -// if (stream is MetaStreamWriter streamWriter) +// if (stream.Mode is MetaStreamMode.Write) // { // } -// else if (stream is MetaStreamReader streamReader) +// else if (stream.Mode is MetaStreamMode.Read) // { // } // } // } -// } \ No newline at end of file +// } diff --git a/src/TelltaleToolKit/T3Types/MetaVersionInfo.cs b/src/TelltaleToolKit/T3Types/MetaVersionInfo.cs index 5946eca..a334a1c 100644 --- a/src/TelltaleToolKit/T3Types/MetaVersionInfo.cs +++ b/src/TelltaleToolKit/T3Types/MetaVersionInfo.cs @@ -4,12 +4,27 @@ namespace TelltaleToolKit.T3Types; +/// +/// Represents a single version info entry. +/// [MetaClassSerializerGlobal(typeof(DefaultClassSerializer))] public class MetaVersionInfo { [MetaMember("mTypeSymbolCrc")] - public ulong TypeSymbolCrc; + public ulong TypeSymbolCrc { get; set; } [MetaMember("mVersionCrc")] - public uint mVersionCrc; -} \ No newline at end of file + public uint VersionCrc { get; set; } + + public bool IsRegistered() + => Toolkit.Instance.ClassRegistry.GetClass(TypeSymbolCrc, VersionCrc) is not null; + + public bool IsTypeRegistered() + => GetMetaClassType() is not null; + + public MetaClassType? GetMetaClassType() + => MetaClassTypeRegistry.GetByHash(TypeSymbolCrc); + + public MetaClass? GetMetaClass() + => Toolkit.Instance.ClassRegistry.GetClass(TypeSymbolCrc, VersionCrc); +} diff --git a/src/TelltaleToolKit/T3Types/Properties/PropertySet.cs b/src/TelltaleToolKit/T3Types/Properties/PropertySet.cs index bdc56ba..9ef6f57 100644 --- a/src/TelltaleToolKit/T3Types/Properties/PropertySet.cs +++ b/src/TelltaleToolKit/T3Types/Properties/PropertySet.cs @@ -55,13 +55,13 @@ public override void Serialize(ref PropertySet obj, MetaStream stream) stream.BeginBlock(); - if (stream is MetaStreamWriter streamWriter) + if (stream.Mode is MetaStreamMode.Write) { // Write ParentList when PropVersion > 0 if (obj.PropVersion > 0) { int parentCount = obj.ParentList.Count; - streamWriter.Write(parentCount); + stream.Write(parentCount); MetaClassSerializer> parentSerializer = Toolkit.Instance.GetSerializer>(); @@ -107,7 +107,7 @@ public override void Serialize(ref PropertySet obj, MetaStream stream) } // Write number of distinct meta types - streamWriter.Write(groups.Count); + stream.Write(groups.Count); // Stable order: order by the MetaClassType's identity marker (e.g., its symbol or linking-type name) // Use MetaClassType.Symbol if present (prefer preserving the meta-symbol ordering), otherwise LinkingType.FullName @@ -124,15 +124,15 @@ public override void Serialize(ref PropertySet obj, MetaStream stream) foreach ((MetaClassType typeSymbol, List> entries) in ordered) { - streamWriter.Write(typeSymbol); - streamWriter.Write(entries.Count); + stream.Write(typeSymbol); + stream.Write(entries.Count); MetaClassSerializer classTypeSerializer = Toolkit.Instance.GetSerializer(typeSymbol.LinkingType); foreach ((Symbol key, PropertyEntry value) in entries) { - streamWriter.Write(key); + stream.Write(key); object? propertyValue = value.Value; @@ -144,11 +144,11 @@ public override void Serialize(ref PropertySet obj, MetaStream stream) } } } - else if (stream is MetaStreamReader streamReader) + else if (stream.Mode is MetaStreamMode.Read) { if (obj.PropVersion > 1) { - obj.ParentList.Capacity = streamReader.ReadInt32(); + obj.ParentList.Capacity = stream.ReadInt32(); for (var i = 0; i < obj.ParentList.Capacity; i++) { @@ -158,26 +158,29 @@ public override void Serialize(ref PropertySet obj, MetaStream stream) } } - if (obj.PropVersion == 1 && !stream.GetMetaClass(typeof(PropertySet)).ContainsMember("mParentList")) + if (obj.PropVersion == 1 && !stream.GetMetaClass(typeof(PropertySet))!.ContainsMember("mParentList")) { stream.EndBlock(); stream.BeginBlock(); } - int numTypes = streamReader.ReadInt32(); + int numTypes = stream.ReadInt32(); for (var i = 0; i < numTypes; i++) { // The type of the class - MetaClassType typeSymbol = streamReader.ReadMetaClassType(); + MetaClassType? typeSymbol = stream.ReadMetaClassType(); // The number of times that type has been serialized - int numOfType = streamReader.ReadInt32(); + int numOfType = stream.ReadInt32(); + + if (typeSymbol is null) + throw new InvalidOperationException("[PropertySet] Type symbol is null"); MetaClassSerializer classTypeSerializer = Toolkit.Instance.GetSerializer(typeSymbol.LinkingType); for (var j = 0; j < numOfType; j++) { // The property key - Symbol propertyKey = streamReader.ReadSymbol(); + Symbol propertyKey = stream.ReadSymbol(); object? propertyValue = null; @@ -192,7 +195,6 @@ public override void Serialize(ref PropertySet obj, MetaStream stream) if (embeddedParentProps) { - stream.PreSerialize(ref obj.ParentProperties); stream.Serialize(ref obj.ParentProperties); } diff --git a/src/TelltaleToolKit/T3Types/Properties/ToolProps.cs b/src/TelltaleToolKit/T3Types/Properties/ToolProps.cs index 139cd19..81caf3d 100644 --- a/src/TelltaleToolKit/T3Types/Properties/ToolProps.cs +++ b/src/TelltaleToolKit/T3Types/Properties/ToolProps.cs @@ -22,16 +22,16 @@ public override void PreSerialize(ref ToolProps obj, MetaStream stream, MetaClas public override void Serialize(ref ToolProps obj, MetaStream stream) { - if (stream is MetaStreamWriter streamWriter) + if (stream.Mode is MetaStreamMode.Write) { // Quoting Lucas: "we are not in the tool engine, runtime engine. would normally be a #ifdef COMPILING_AS_TOOL etc" obj.HasProps = false; - streamWriter.Write(obj.HasProps); + stream.Write(obj.HasProps); } - else if (stream is MetaStreamReader streamReader) + else if (stream.Mode is MetaStreamMode.Read) { - obj.HasProps = streamReader.ReadBoolean(); - + obj.HasProps = stream.ReadBoolean(); + if (!obj.HasProps) return; @@ -41,4 +41,4 @@ public override void Serialize(ref ToolProps obj, MetaStream stream) } } } -} \ No newline at end of file +} diff --git a/src/TelltaleToolKit/T3Types/Rules/Rule.cs b/src/TelltaleToolKit/T3Types/Rules/Rule.cs index ac7c4d7..3684a4b 100644 --- a/src/TelltaleToolKit/T3Types/Rules/Rule.cs +++ b/src/TelltaleToolKit/T3Types/Rules/Rule.cs @@ -77,12 +77,12 @@ public override void Serialize(ref Rule obj, MetaStream stream) } stream.BeginBlock(); - if (stream is MetaStreamWriter streamWriter) + if (stream.Mode is MetaStreamMode.Write) { } - else if (stream is MetaStreamReader streamReader) + else if (stream.Mode is MetaStreamMode.Read) { - // var numAgents = streamReader.ReadInt32(); + // var numAgents = stream.ReadInt32(); List agents = obj.AgentInformation; Toolkit.Instance.GetSerializer>().Serialize(ref agents, stream); @@ -91,8 +91,8 @@ public override void Serialize(ref Rule obj, MetaStream stream) // { // var agent = new Rule.AgentInfo(); // MetaClass? ruleDesc = stream.GetMetaClass(typeof(Rule)); - // - // + // + // // } // // @@ -103,4 +103,4 @@ public override void Serialize(ref Rule obj, MetaStream stream) stream.EndBlock(); } } -} \ No newline at end of file +} diff --git a/src/TelltaleToolKit/T3Types/Rules/Rules.cs b/src/TelltaleToolKit/T3Types/Rules/Rules.cs index 159922f..74aba5c 100644 --- a/src/TelltaleToolKit/T3Types/Rules/Rules.cs +++ b/src/TelltaleToolKit/T3Types/Rules/Rules.cs @@ -30,28 +30,25 @@ public override void Serialize(ref Rules obj, MetaStream stream) DefaultSerializer.Serialize(ref obj, stream); stream.BeginBlock(); - if (stream is MetaStreamWriter streamWriter) + if (stream.Mode is MetaStreamMode.Write) { throw new NotImplementedException(); } - if (stream is MetaStreamReader streamReader) + if (stream.Mode is MetaStreamMode.Read) { HashSet rulesSet = obj.RulesSet; - Toolkit.Instance.GetSerializer>() - .Serialize(ref rulesSet, stream); + stream.Serialize(ref rulesSet); //// - MetaClassSerializer ruleSerializer = Toolkit.Instance.GetSerializer(); - obj.RuleMap = new Dictionary(); foreach (string t in obj.RulesSet) { var rule = new Rule(); - ruleSerializer.Serialize(ref rule, stream); + stream.Serialize(ref rule); obj.RuleMap.Add(t, rule); } @@ -60,4 +57,4 @@ public override void Serialize(ref Rules obj, MetaStream stream) stream.EndBlock(); } } -} \ No newline at end of file +} diff --git a/src/TelltaleToolKit/T3Types/SaveGame.cs b/src/TelltaleToolKit/T3Types/SaveGame.cs index 462d36b..db9af29 100644 --- a/src/TelltaleToolKit/T3Types/SaveGame.cs +++ b/src/TelltaleToolKit/T3Types/SaveGame.cs @@ -43,8 +43,8 @@ public class AgentInfo [MetaMember("mAttachedToNode")] public string AttachedToNode { get; set; } = string.Empty; } - - + + public class Serializer : MetaClassSerializer { private static readonly DefaultClassSerializer DefaultSaveGameSerializer = new(); @@ -53,12 +53,12 @@ public override void Serialize(ref SaveGame obj, MetaStream stream) { DefaultSaveGameSerializer.Serialize(ref obj, stream); - if (stream is MetaStreamWriter) + if (stream.Mode is MetaStreamMode.Write) { throw new NotImplementedException(); } - if (stream is MetaStreamReader streamReader) + if (stream.Mode is MetaStreamMode.Read) { if (stream.GetMetaClass(typeof(PropertySet)) != null) { @@ -69,4 +69,4 @@ public override void Serialize(ref SaveGame obj, MetaStream stream) } } } -} \ No newline at end of file +} diff --git a/src/TelltaleToolKit/T3Types/Scenes/Scene.cs b/src/TelltaleToolKit/T3Types/Scenes/Scene.cs index 4811929..3af7b5c 100644 --- a/src/TelltaleToolKit/T3Types/Scenes/Scene.cs +++ b/src/TelltaleToolKit/T3Types/Scenes/Scene.cs @@ -32,14 +32,14 @@ public override void Serialize(ref Scene obj, MetaStream stream) stream.BeginBlock(); - if (stream is MetaStreamWriter streamWriter) + if (stream.Mode is MetaStreamMode.Write) { throw new NotImplementedException(); } - if (stream is MetaStreamReader streamReader) + if (stream.Mode is MetaStreamMode.Read) { - int numAgents = streamReader.ReadInt32(); + int numAgents = stream.ReadInt32(); obj.Agents.Capacity = numAgents; for (var i = 0; i < numAgents; i++) @@ -53,7 +53,7 @@ public override void Serialize(ref Scene obj, MetaStream stream) stream.EndBlock(); } } - + [MetaClassSerializerGlobal(typeof(DefaultClassSerializer))] public class AgentInfo { @@ -97,4 +97,4 @@ public class AgentQualitySettings [MetaMember("mFlags")] public Flags Flags { get; set; } } -} \ No newline at end of file +} diff --git a/src/TelltaleToolKit/T3Types/StyleGuides/ActingPaletteClass.cs b/src/TelltaleToolKit/T3Types/StyleGuides/ActingPaletteClass.cs index 3e3237f..7506fa4 100644 --- a/src/TelltaleToolKit/T3Types/StyleGuides/ActingPaletteClass.cs +++ b/src/TelltaleToolKit/T3Types/StyleGuides/ActingPaletteClass.cs @@ -54,16 +54,16 @@ public override void Serialize(ref ActingPaletteClass obj, MetaStream stream) if (classDescription is not null && classDescription.ContainsMember("mFlags")) { - if (stream is MetaStreamWriter streamWriter) + if (stream.Mode is MetaStreamMode.Write) { throw new NotImplementedException(); } - if (stream is MetaStreamReader streamReader) + if (stream.Mode is MetaStreamMode.Read) { throw new NotImplementedException(); } } } } -} \ No newline at end of file +} diff --git a/src/TelltaleToolKit/T3Types/StyleGuides/StyleGuide.cs b/src/TelltaleToolKit/T3Types/StyleGuides/StyleGuide.cs index 996d4f9..f367e0b 100644 --- a/src/TelltaleToolKit/T3Types/StyleGuides/StyleGuide.cs +++ b/src/TelltaleToolKit/T3Types/StyleGuides/StyleGuide.cs @@ -50,19 +50,18 @@ public override void Serialize(ref StyleGuide obj, MetaStream stream) if (classDescription is not null && classDescription.ContainsMember("mFlags")) { - if (stream is MetaStreamWriter streamWriter) + if (stream.Mode is MetaStreamMode.Write) { throw new NotImplementedException(); } - if (stream is MetaStreamReader streamReader) + if (stream.Mode is MetaStreamMode.Read) { - int values = streamReader.ReadInt32(); + int values = stream.ReadInt32(); for (var i = 0; i < values; i++) { var child = new ActingPaletteClass(); - stream.PreSerialize(ref child); stream.Serialize(ref child); obj.PaletteClassesPtrs.Add(child); } @@ -70,4 +69,4 @@ public override void Serialize(ref StyleGuide obj, MetaStream stream) } } } -} \ No newline at end of file +} diff --git a/src/TelltaleToolKit/T3Types/Textures/D3DTexture.cs b/src/TelltaleToolKit/T3Types/Textures/D3DTexture.cs index 584d9aa..7dcb370 100644 --- a/src/TelltaleToolKit/T3Types/Textures/D3DTexture.cs +++ b/src/TelltaleToolKit/T3Types/Textures/D3DTexture.cs @@ -47,12 +47,12 @@ namespace TelltaleToolKit.T3Types.Textures; // // Default Serializer // new DefaultClassSerializer().Serialize(ref obj, stream, desc); // -// if (stream is MetaStreamWriter streamWriter) +// if (stream.Mode is MetaStreamMode.Write) // { -// streamWriter.Write(obj.DdsTextureData.Length); -// streamWriter.Write(obj.DdsTextureData); +// stream.Write(obj.DdsTextureData.Length); +// stream.Write(obj.DdsTextureData); // } -// else if (stream is MetaStreamReader streamReader) +// else if (stream.Mode is MetaStreamMode.Read) // { // if (!obj.HasTextureData) // { @@ -60,9 +60,9 @@ namespace TelltaleToolKit.T3Types.Textures; // } // // // T3MetaStream.SerializeByteArray(ref texture.DdsTextureData, stream); -// var bufferSize = streamReader.ReadInt32(); -// obj.DdsTextureData = streamReader.ReadBytes(bufferSize); +// var bufferSize = stream.ReadInt32(); +// obj.DdsTextureData = stream.ReadBytes(bufferSize); // } // } // } -// } \ No newline at end of file +// } diff --git a/src/TelltaleToolKit/T3Types/Textures/T3Texture.cs b/src/TelltaleToolKit/T3Types/Textures/T3Texture.cs index 99d32d2..a2cc716 100644 --- a/src/TelltaleToolKit/T3Types/Textures/T3Texture.cs +++ b/src/TelltaleToolKit/T3Types/Textures/T3Texture.cs @@ -356,7 +356,7 @@ public override void Serialize(ref T3Texture obj, MetaStream stream) // The default serializer will throw if there is no serialized T3Texture. MetaClass classDescription = stream.GetMetaClass(typeof(T3Texture))!; - if (stream is MetaStreamWriter streamWriter) + if (stream.Mode is MetaStreamMode.Write) { if (classDescription.ContainsMember("mVersion")) { @@ -388,23 +388,27 @@ public override void Serialize(ref T3Texture obj, MetaStream stream) stream.BeginAsyncSection(); foreach (RegionStreamHeader region in obj.RegionHeaders) { - streamWriter.Write(region.RegionData); + stream.Write(region.RegionData); } stream.EndAsyncSection(); } else { - streamWriter.Write(obj.DdsTextureData.Length); - streamWriter.Write(obj.DdsTextureData); + stream.Key("DDSLength"); + stream.Write(obj.DdsTextureData.Length); + + stream.Key("DDSTextureData"); + stream.Write(obj.DdsTextureData); if (obj.TplTextureDataSize > 0) { - streamWriter.Write(obj.TplTextureData); + stream.Key("TplTextureData"); + stream.Write(obj.TplTextureData); } } } - else if (stream is MetaStreamReader streamReader) + else if (stream.Mode is MetaStreamMode.Read) { // This is easier than checking for region stream headers. That can also work. if (classDescription.ContainsMember("mVersion")) @@ -452,7 +456,7 @@ public override void Serialize(ref T3Texture obj, MetaStream stream) region.SlicePitch = region.DataSize; } - region.RegionData = streamReader.ReadBytes(region.DataSize); + region.RegionData = stream.ReadBytes(region.DataSize); } stream.EndAsyncSection(); @@ -464,14 +468,14 @@ public override void Serialize(ref T3Texture obj, MetaStream stream) return; } - int bufferSize = streamReader.ReadInt32(); - obj.DdsTextureData = streamReader.ReadBytes(bufferSize); + int bufferSize = stream.ReadInt32(); + obj.DdsTextureData = stream.ReadBytes(bufferSize); if (obj.TplTextureDataSize > 0) { obj.TplTextureData = new byte[obj.TplTextureDataSize]; - obj.TplTextureData = streamReader.ReadBytes(obj.TplTextureDataSize); + obj.TplTextureData = stream.ReadBytes(obj.TplTextureDataSize); } } } diff --git a/src/TelltaleToolKit/T3Types/UID.cs b/src/TelltaleToolKit/T3Types/UID.cs index bf60e20..919a5e9 100644 --- a/src/TelltaleToolKit/T3Types/UID.cs +++ b/src/TelltaleToolKit/T3Types/UID.cs @@ -15,6 +15,7 @@ public class Generator [MetaMember("miNextUniqueID")] public int NextUniqueId { get; set; } } + public interface IGenerator { public Generator Generator { get; set; } @@ -68,15 +69,15 @@ public override void PreSerialize(ref ActingOverridablePropOwner obj, MetaStream public override void Serialize(ref ActingOverridablePropOwner obj, MetaStream stream) { - if (stream is MetaStreamWriter streamWriter) + if (stream.Mode is MetaStreamMode.Write) { throw new NotImplementedException(); } - if (stream is MetaStreamReader streamReader) + if (stream.Mode is MetaStreamMode.Read) { - long currPos = streamReader.GetCurrentPosition(); - uint value = streamReader.ReadUInt32(); + long currPos = stream.GetPosition(); + uint value = stream.ReadUInt32(); if (value == ActingOverridablePropOwner.kHeader) { @@ -85,14 +86,13 @@ public override void Serialize(ref ActingOverridablePropOwner obj, MetaStream st return; } - streamReader.SetCurrentPosition(currPos); + stream.SetPosition(currPos); if ((obj.SerializationFlags.Data & 1) == 0) return; PropertySet propertySet = obj.OverridableValues; - Toolkit.Instance.GetSerializer().PreSerialize(ref propertySet, stream); - Toolkit.Instance.GetSerializer().Serialize(ref propertySet, stream); + stream.Serialize(ref propertySet); } } } -} \ No newline at end of file +} diff --git a/src/TelltaleToolKit/T3Types/Vers.cs b/src/TelltaleToolKit/T3Types/Vers.cs index bc8bb85..6036193 100644 --- a/src/TelltaleToolKit/T3Types/Vers.cs +++ b/src/TelltaleToolKit/T3Types/Vers.cs @@ -34,45 +34,45 @@ public override void Serialize(ref Vers obj, MetaStream stream) { PreSerialize(ref obj, stream); - if (stream is MetaStreamWriter streamWriter) + if (stream.Mode is MetaStreamMode.Write) { - streamWriter.Write(obj.FileName); - streamWriter.Write(obj.ClassName); - streamWriter.Write(obj.Crc32); - streamWriter.Write(obj.Size); - streamWriter.Write(obj.IsBlocked); - streamWriter.Write(obj.Crc32); - streamWriter.Write(obj.Registries.Count); + stream.Write(obj.FileName); + stream.Write(obj.ClassName); + stream.Write(obj.Crc32); + stream.Write(obj.Size); + stream.Write(obj.IsBlocked); + stream.Write(obj.Crc32); + stream.Write(obj.Registries.Count); for (int i = 0; i < obj.Registries.Capacity; i++) { - streamWriter.Write(obj.Registries[i].MemberName); - streamWriter.Write(obj.Registries[i].MemberType); - streamWriter.Write(obj.Registries[i].Size); - streamWriter.Write(obj.Registries[i].IsBlocked); - streamWriter.Write(obj.Registries[i].Crc32); + stream.Write(obj.Registries[i].MemberName); + stream.Write(obj.Registries[i].MemberType); + stream.Write(obj.Registries[i].Size); + stream.Write(obj.Registries[i].IsBlocked); + stream.Write(obj.Registries[i].Crc32); } } - else if (stream is MetaStreamReader streamReader) + else if (stream.Mode is MetaStreamMode.Read) { - obj.FileName = streamReader.ReadString(); - obj.ClassName = streamReader.ReadString(); - obj.Crc32 = streamReader.ReadUInt32(); - obj.Size = streamReader.ReadUInt32(); - obj.IsBlocked = streamReader.ReadBoolean(); + obj.FileName = stream.ReadString(); + obj.ClassName = stream.ReadString(); + obj.Crc32 = stream.ReadUInt32(); + obj.Size = stream.ReadUInt32(); + obj.IsBlocked = stream.ReadBoolean(); - obj.Registries.Capacity = streamReader.ReadInt32(); + obj.Registries.Capacity = stream.ReadInt32(); for (int i = 0; i < obj.Registries.Capacity; i++) { obj.Registries.Add(new MemberRegistry()); - obj.Registries[i].MemberName = streamReader.ReadString(); - obj.Registries[i].MemberType = streamReader.ReadString(); - obj.Registries[i].Size = streamReader.ReadUInt32(); - obj.Registries[i].IsBlocked = streamReader.ReadBoolean(); - obj.Registries[i].Crc32 = streamReader.ReadUInt32(); + obj.Registries[i].MemberName = stream.ReadString(); + obj.Registries[i].MemberType = stream.ReadString(); + obj.Registries[i].Size = stream.ReadUInt32(); + obj.Registries[i].IsBlocked = stream.ReadBoolean(); + obj.Registries[i].Crc32 = stream.ReadUInt32(); } } } } -} \ No newline at end of file +} diff --git a/src/TelltaleToolKit/T3Types/Voice/VoiceData.cs b/src/TelltaleToolKit/T3Types/Voice/VoiceData.cs index 133de0d..c773b33 100644 --- a/src/TelltaleToolKit/T3Types/Voice/VoiceData.cs +++ b/src/TelltaleToolKit/T3Types/Voice/VoiceData.cs @@ -41,14 +41,14 @@ public override void Serialize(ref VoiceData obj, MetaStream stream) DefaultSerializer.PreSerialize(ref obj, stream); DefaultSerializer.Serialize(ref obj, stream); - if (stream is MetaStreamWriter streamWriter) + if (stream.Mode is MetaStreamMode.Write) { - streamWriter.Write(obj.VoiceDataBuffer); + stream.Write(obj.VoiceDataBuffer); } - else if (stream is MetaStreamReader streamReader) + else if (stream.Mode is MetaStreamMode.Read) { - obj.VoiceDataBuffer = streamReader.ReadBytes(obj.AllPacketsSize); + obj.VoiceDataBuffer = stream.ReadBytes(obj.AllPacketsSize); } } } -} \ No newline at end of file +} diff --git a/src/TelltaleToolKit/TelltaleArchives/Archive.cs b/src/TelltaleToolKit/TelltaleArchives/Archive.cs index 466bb8d..798932c 100644 --- a/src/TelltaleToolKit/TelltaleArchives/Archive.cs +++ b/src/TelltaleToolKit/TelltaleArchives/Archive.cs @@ -40,19 +40,19 @@ public abstract class Archive : IDisposable /// /// Gets the original file path of the archive, if it was loaded from a file. - /// when the archive was opened from a . + /// when the archive was opened from a . /// public string? FilePath { get; private set; } /// /// Gets the length of the underlying stream in bytes, if the stream supports seeking. - /// when the stream is not seekable (e.g., a network stream). + /// when the stream is not seekable (e.g., a network stream). /// public long? Length => BaseStream?.CanSeek == true ? BaseStream.Length : null; /// /// Gets the file name (without directory) of the archive, if a file path is available. - /// when is . + /// when is . /// public string? FileName => FilePath is not null ? Path.GetFileName(FilePath) : null; diff --git a/src/TelltaleToolKit/TelltaleArchives/ArchiveWriteOptions.cs b/src/TelltaleToolKit/TelltaleArchives/ArchiveWriteOptions.cs index 01f9ff8..ddec78a 100644 --- a/src/TelltaleToolKit/TelltaleArchives/ArchiveWriteOptions.cs +++ b/src/TelltaleToolKit/TelltaleArchives/ArchiveWriteOptions.cs @@ -1,4 +1,7 @@ -namespace TelltaleToolKit.TelltaleArchives; +using TelltaleToolKit.Serialization; +using TelltaleToolKit.Utility.Compression; + +namespace TelltaleToolKit.TelltaleArchives; /// /// Controls how an archive is written or repacked. @@ -24,9 +27,9 @@ public sealed class ArchiveWriteOptions /// /// Gets or sets the compression algorithm to use. - /// Defaults to . + /// Defaults to . /// - public CompressionAlgorithm Algorithm { get; set; } = CompressionAlgorithm.Deflate; + public Compression Algorithm { get; set; } = Compression.Deflate; /// /// Gets or sets the decompressed size of each chunk in bytes. @@ -50,20 +53,4 @@ public sealed class ArchiveWriteOptions public string BlowfishKey { get; set; } = string.Empty; } -/// Compression algorithm to use when writing archive chunks. -public enum CompressionAlgorithm -{ - /// No compression. - None, - - /// Raw DEFLATE (no zlib header). Used by most Telltale games. - Deflate, - - /// Zlib-wrapped DEFLATE. Used by some CSI and earlier titles. - Zlib, - /// - /// Oodle compression. - /// - Oodle -} diff --git a/src/TelltaleToolKit/TelltaleArchives/Formats/TTArchive.cs b/src/TelltaleToolKit/TelltaleArchives/Formats/TTArchive.cs index 7c1e53e..cb45112 100644 --- a/src/TelltaleToolKit/TelltaleArchives/Formats/TTArchive.cs +++ b/src/TelltaleToolKit/TelltaleArchives/Formats/TTArchive.cs @@ -2,9 +2,10 @@ using System.IO.Hashing; using System.Text; using ICSharpCode.SharpZipLib.Zip.Compression.Streams; -using TelltaleToolKit.Serialization.Binary; +using TelltaleToolKit.Serialization; using TelltaleToolKit.TelltaleArchives.IO; using TelltaleToolKit.Utility.Blowfish; +using TelltaleToolKit.Utility.Compression; using Crc64 = TelltaleToolKit.Utility.Hashing.Crc64; namespace TelltaleToolKit.TelltaleArchives.Formats; @@ -24,7 +25,7 @@ public class TTArchive : Archive private Stream? _dataStream; // decompressed view of the entire file-data region - protected override void Activate() + protected override void Activate() { using BinaryReader reader = new(BaseStream!, Encoding.UTF8, true); @@ -143,7 +144,9 @@ protected override void Activate() } if (decryptionMode == 1) + { _blowfish.Decipher(header, header.Length); + } Info.FilesOffset = (ulong)reader.BaseStream.Position; ParseEntries(new MemoryStream(header)); @@ -262,7 +265,6 @@ private Stream BuildFileDataStream(int version, uint filesMode, uint decryptionM throw new InvalidOperationException("Archive not loaded."); } - // TODO: Implement decrypting in MetaStream return entry is null ? null : new SubStream(_dataStream, (long)entry.Offset, entry.Size); } @@ -292,7 +294,7 @@ public static void Create(Stream output, IEnumerable<(string name, Stream dataSt } bool encrypt = options.Encrypt; - bool compressFileData = options.Algorithm != CompressionAlgorithm.None && version >= 3; + bool compressFileData = options.Algorithm != Compression.None && version >= 3; bool compressHeader = compressFileData && version >= 6; // compressed entry table block (filesMode=2) Blowfish bf = new(options.BlowfishKey, version); @@ -350,7 +352,7 @@ public static void Create(Stream output, IEnumerable<(string name, Stream dataSt byte[] CompressBlock(byte[] data) { using MemoryStream ms = new(); - if (options.Algorithm == CompressionAlgorithm.Zlib) + if (options.Algorithm == Compression.Zlib) { using DeflaterOutputStream zlib = new(ms); zlib.Write(data, 0, data.Length); diff --git a/src/TelltaleToolKit/TelltaleArchives/Formats/TTArchive2.cs b/src/TelltaleToolKit/TelltaleArchives/Formats/TTArchive2.cs index e183c1d..d6900ae 100644 --- a/src/TelltaleToolKit/TelltaleArchives/Formats/TTArchive2.cs +++ b/src/TelltaleToolKit/TelltaleArchives/Formats/TTArchive2.cs @@ -1,6 +1,6 @@ using System.Text; -using TelltaleToolKit.Serialization.Binary; -using TelltaleToolKit.TelltaleArchives.IO; +using TelltaleToolKit.Serialization; +using TelltaleToolKit.Utility.Compression; using TelltaleToolKit.Utility.Hashing; namespace TelltaleToolKit.TelltaleArchives.Formats; @@ -33,9 +33,15 @@ protected override void Activate() _containerStream = new ContainerStream(BaseStream!, Info.BlowfishKey); // Copy container state back to Info so the rest of the code (and callers // that inspect Info) see the right values. - Info.Flags = _containerStream.Flags; - Info.ChunkSize = _containerStream.ChunkSize; - Info.ChunkCount = _containerStream.ChunkCount; + Info.Flags |= _containerStream.Params.Encrypt ? ArchiveFlags.IsEncrypted : 0; + Info.Flags |= _containerStream.Params.Algorithm == Compression.Deflate + ? ArchiveFlags.IsRawDeflateCompressed + : 0; + Info.Flags |= _containerStream.Params.Algorithm == Compression.Oodle + ? ArchiveFlags.IsOodleCompressed + : 0; + Info.ChunkSize = _containerStream.WindowSize; + Info.ChunkCount = _containerStream.NumPages; Info.ChunkBlockSizes = _containerStream.ChunkBlockSizes; ReadTtaHeader(_containerStream); @@ -311,7 +317,15 @@ public static void Create(Stream output, IEnumerable<(string name, Stream dataSt using (FileStream innerReader = new(tempFile, FileMode.Open, FileAccess.Read, FileShare.Read, 4096, FileOptions.DeleteOnClose | FileOptions.SequentialScan)) { - ContainerStream.Create(output, innerReader, options); + ContainerStreamParams containerOptions = new() + { + Encrypt = options.Encrypt, + ChunkSize = options.ChunkSize, + Algorithm = options.Algorithm, + BlowfishKey = options.BlowfishKey + }; + + ContainerStream.Create(output, innerReader, containerOptions); } } finally diff --git a/src/TelltaleToolKit/TelltaleArchives/IO/LegacyEncrypted.cs b/src/TelltaleToolKit/TelltaleArchives/IO/LegacyEncrypted.cs deleted file mode 100644 index 01d93ca..0000000 --- a/src/TelltaleToolKit/TelltaleArchives/IO/LegacyEncrypted.cs +++ /dev/null @@ -1,70 +0,0 @@ -using TelltaleToolKit.Utility.Blowfish; - -namespace TelltaleToolKit.TelltaleArchives.IO; - -/// -/// Stream wrapper that decrypts TTArchive per‑file data on the fly. -/// For simplicity, it reads the entire file into memory – these files are small -/// (config files, scripts) so the overhead is acceptable. -/// -internal sealed class LegacyEncrypted : Stream -{ - private readonly Blowfish _blowfish; - private readonly Stream _inner; - - public LegacyEncrypted(Stream inner, Blowfish blowfish) - { - _inner = inner; - _blowfish = blowfish; - // Read the whole file, decrypt it, and keep the result in memory. - using MemoryStream ms = new(); - _inner.CopyTo(ms); - // _decryptedData = ms.ToArray(); - // TelltaleArchiveUtilities.DecryptFile(_decryptedData, _blowfish); - } - - public override bool CanRead => true; - public override bool CanSeek => true; - public override bool CanWrite => false; - - public override long Length { get; } - // public override long Length => _decryptedData.Length; - - public override long Position { get; set; } - - public override int Read(byte[] buffer, int offset, int count) - => Read(buffer.AsSpan(offset, count)); - - public override int Read(Span buffer) - { - long remaining = Length - Position; - if (remaining <= 0) - { - return 0; - } - - int toRead = (int)Math.Min(buffer.Length, remaining); - // _decryptedData.AsSpan((int)_position, toRead).CopyTo(buffer); - Position += toRead; - return toRead; - } - - public override long Seek(long offset, SeekOrigin origin) - { - Position = origin switch - { - SeekOrigin.Begin => offset, - SeekOrigin.Current => Position + offset, - SeekOrigin.End => Length + offset, - _ => throw new ArgumentOutOfRangeException(nameof(origin)) - }; - return Position; - } - - public override void Flush() - { - } - - public override void SetLength(long value) => throw new NotSupportedException(); - public override void Write(byte[] buffer, int offset, int count) => throw new NotSupportedException(); -} diff --git a/src/TelltaleToolKit/TelltaleArchives/IO/TtarchiveChunkedDataStream.cs b/src/TelltaleToolKit/TelltaleArchives/IO/TtarchiveChunkedDataStream.cs index c464ba7..0b4a919 100644 --- a/src/TelltaleToolKit/TelltaleArchives/IO/TtarchiveChunkedDataStream.cs +++ b/src/TelltaleToolKit/TelltaleArchives/IO/TtarchiveChunkedDataStream.cs @@ -1,5 +1,6 @@ -using TelltaleToolKit.TelltaleArchives.Caching; -using TelltaleToolKit.Utility.Blowfish; +using TelltaleToolKit.Utility.Blowfish; +using TelltaleToolKit.Utility.Caching; +using TelltaleToolKit.Utility.Compression; namespace TelltaleToolKit.TelltaleArchives.IO; @@ -15,9 +16,9 @@ internal sealed class TtarchiveChunkedDataStream : Stream private readonly uint _chunkSize; private readonly ulong[] _chunkSizes; private readonly long _dataStart; - private ArchiveFlags _flags; private readonly bool _isEncrypted; private readonly Stream _source; + private ArchiveFlags _flags; private long _position; public TtarchiveChunkedDataStream( @@ -126,7 +127,7 @@ private byte[] DecodeChunk(int chunkIdx) // Decompress the chunk ArchiveFlags flags = _flags; - var res = ChunkDecoder.DecompressBlock(compressed, (int)_chunkSize, ref flags); + byte[] res = ChunkDecoder.DecompressBlock(compressed, (int)_chunkSize, ref flags); _flags = flags; return res; } diff --git a/src/TelltaleToolKit/TelltaleArchives/TelltaleArchiveUtilities.cs b/src/TelltaleToolKit/TelltaleArchives/TelltaleArchiveUtilities.cs deleted file mode 100644 index 6f0adc7..0000000 --- a/src/TelltaleToolKit/TelltaleArchives/TelltaleArchiveUtilities.cs +++ /dev/null @@ -1,120 +0,0 @@ -using System.IO.Compression; -using ICSharpCode.SharpZipLib.Zip.Compression.Streams; -using TelltaleToolKit.Serialization.Binary; -using TelltaleToolKit.Utility.Blowfish; -using BlockConfiguration = (int blockSize, int cryptInterval, int cleanInterval); - -namespace TelltaleToolKit.TelltaleArchives; - -public static class TelltaleArchiveUtilities -{ - public static void DecryptFile(byte[] file, string blowfishKey, int archiveVersion) - { - if (file.Length < 4) - { - return; - } - - DecryptFile(file, new Blowfish(blowfishKey, archiveVersion)); - } - - public static void DecryptFile(byte[] file, Blowfish blowfish) - { - if (file.Length < 4) - { - return; - } - - MetaStreamVersion magicFourCc = (MetaStreamVersion)BitConverter.ToUInt32(file, 0); - BlockConfiguration config = GetBlockConfiguration(magicFourCc); - - if (config.blockSize > 0) - { - DecryptFileHelper(file, blowfish, config); - } - } - - //(int blockSize, int cryptInterval, int cleanInterval); - private static BlockConfiguration GetBlockConfiguration(MetaStreamVersion type) - => type switch - { - MetaStreamVersion.Unknown1 or MetaStreamVersion.Unknown2 or MetaStreamVersion.Unknown3 => (128, 32, 80), - MetaStreamVersion.Unknown4 => (256, 8, 24), - MetaStreamVersion.Mbes => (64, 64, 100), - _ => (0, 0, 0) - }; - - private static void DecryptFileHelper(byte[] fileData, Blowfish blowFish, BlockConfiguration config) - { - // If the blocksize is 0 (Stream version is not existent), use regular blowfish - if (config.blockSize == 0) - { - blowFish.Decipher(fileData, fileData.Length); - return; - } - - // Replace the magic number with Mbin (Might be replaced with MBIN/MTRE) - Array.Copy(BitConverter.GetBytes((uint)MetaStreamVersion.Mbin), 0, fileData, 0, 4); - - long blocks = (fileData.Length - 4) / config.blockSize; - - for (int i = 0; i < blocks; i++) - { - Span block = new(fileData, 4 + i * config.blockSize, config.blockSize); - - if (i % config.cryptInterval == 0) - { - blowFish.Decipher(block, block.Length); - } - else if (i % config.cleanInterval == 0 && i > 0) - { - // Clean interval, skip this block - } - else - { - XorBlock(block, 0xFF); - } - } - } - - public static void XorBlock(Span block, byte xor) - { - for (int i = 0; i < block.Length; i++) - { - block[i] ^= xor; - } - } - - private static CompressionAlgorithm DetectCompressionAlgorithm(byte[] chunk) - { - // Zlib header: first byte 0x78, second byte in {0x9C, 0xDA, 0x01, 0x5E, 0x9E, ...} - if (chunk[0] == 0x78 && (chunk[1] & 0xF0) == 0xC0) // 0x78 0x9C, 0x78 0xDA, etc. - { - return CompressionAlgorithm.Zlib; - } - - return CompressionAlgorithm.Deflate; // raw Deflate has no header - } - - /// - /// Compresses a single chunk using the specified algorithm. - /// - /// Number of compressed bytes written into . - public static int CompressBlock(ReadOnlySpan input, CompressionAlgorithm algorithm, byte[] outputBuffer) - { - using MemoryStream ms = new(outputBuffer, true); - if (algorithm == CompressionAlgorithm.Zlib) - { - using DeflaterOutputStream zlib = new(ms); - zlib.Write(input); - zlib.Finish(); - } - else // Deflate (raw) - { - using DeflateStream deflate = new(ms, CompressionLevel.Optimal, true); - deflate.Write(input); - } - - return (int)ms.Position; - } -} diff --git a/src/TelltaleToolKit/TelltaleToolKit.csproj b/src/TelltaleToolKit/TelltaleToolKit.csproj index 0dd8c46..289231e 100644 --- a/src/TelltaleToolKit/TelltaleToolKit.csproj +++ b/src/TelltaleToolKit/TelltaleToolKit.csproj @@ -6,7 +6,7 @@ latest 13 TelltaleToolKit - 0.2.1 + 0.3.0 TelltaleToolKit Ivan Panayotov https://github.com/iMrShadow/TelltaleToolKit @@ -30,15 +30,18 @@ + PackagePath="contentFiles\any\any\ttk-data\game_profiles\%(RecursiveDir)%(Filename)%(Extension)" + CopyToOutputDirectory="PreserveNewest"/> + PackagePath="contentFiles\any\any\ttk-data\hashdb\%(RecursiveDir)%(Filename)%(Extension)" + CopyToOutputDirectory="PreserveNewest"/> + PackagePath="contentFiles\any\any\ttk-data\versiondb\%(RecursiveDir)%(Filename)%(Extension)" + CopyToOutputDirectory="PreserveNewest"/> @@ -66,8 +69,4 @@ - - - - diff --git a/src/TelltaleToolKit/Toolkit.cs b/src/TelltaleToolKit/Toolkit.cs index 5761f46..6671eda 100644 --- a/src/TelltaleToolKit/Toolkit.cs +++ b/src/TelltaleToolKit/Toolkit.cs @@ -4,7 +4,6 @@ using TelltaleToolKit.Logging; using TelltaleToolKit.Reflection; using TelltaleToolKit.Serialization; -using TelltaleToolKit.Serialization.Binary; using TelltaleToolKit.T3Types; using TelltaleToolKit.TelltaleArchives; using TelltaleToolKit.TelltaleArchives.Formats; @@ -65,7 +64,7 @@ public static Toolkit Instance "TelltaleToolkit must be initialized first. Call Toolkit.Initialize()."); /// - /// Gets the configuration supplied at time. + /// Gets the @params supplied at time. /// public Configuration Config { get; } @@ -110,7 +109,7 @@ public static Toolkit Instance /// Must be called exactly once before using any other API. /// /// - /// Optional configuration. When , defaults are used + /// Optional @params. When , defaults are used /// (DataFolder = "ttk-data"). /// /// @@ -220,21 +219,21 @@ public void ResolveSymbols(IEnumerable symbols) /// /// Deserializes a MetaStream object of type from a file, - /// also returning the that was embedded in the stream. + /// also returning the that was embedded in the stream. /// /// The target type. It must have a parameterless constructor. /// Path to the MetaStream file on disk. /// - /// A tuple containing the deserialized object and the configuration embedded in the stream, + /// A tuple containing the deserialized object and the @params embedded in the stream, /// or (null, null) if reading or parsing fails. /// - public (T? Asset, MetaStreamConfiguration? MetaConfig) DeserializeWithConfig(string fileName) + public (T? Asset, MetaStreamParams? MetaConfig) DeserializeWithConfig(string fileName, Workspace? workspace = null) where T : class, new() { try { using FileStream stream = File.OpenRead(fileName); - var result = DeserializeInternal(stream); + var result = DeserializeInternal(stream, workspace); if (result.Asset == null) { @@ -261,7 +260,7 @@ public void ResolveSymbols(IEnumerable symbols) /// /// Deserializes a MetaStream object of type from a stream, - /// also returning the that was embedded in the stream. + /// also returning the that was embedded in the stream. /// /// The target type. It must have a parameterless constructor. /// @@ -269,13 +268,13 @@ public void ResolveSymbols(IEnumerable symbols) /// Not disposed by this method. /// /// - /// A tuple containing the deserialized object and the configuration embedded in the stream, + /// A tuple containing the deserialized object and the @params embedded in the stream, /// or (null, null) if parsing fails. /// - public (T? Asset, MetaStreamConfiguration? MetaConfig) DeserializeWithConfig(Stream stream) + public (T? Asset, MetaStreamParams? MetaConfig) DeserializeWithConfig(Stream stream, Workspace? workspace = null) where T : class, new() { - var result = DeserializeInternal(stream); + var result = DeserializeInternal(stream, workspace); if (result.Asset == null) { @@ -287,19 +286,19 @@ public void ResolveSymbols(IEnumerable symbols) /// /// Deserializes a MetaStream object of type from a file, - /// discarding the embedded configuration. + /// discarding the embedded @params. /// /// The target type. It must have a parameterless constructor. /// Path to the MetaStream file on disk. /// /// The deserialized object, or if reading or parsing fails. /// - public T? Deserialize(string fileName) where T : class, new() + public T? Deserialize(string fileName, Workspace? workspace = null) where T : class, new() { try { using FileStream stream = File.OpenRead(fileName); - var result = DeserializeInternal(stream); + var result = DeserializeInternal(stream, workspace); if (result.Asset == null) { @@ -322,7 +321,7 @@ public void ResolveSymbols(IEnumerable symbols) /// /// Deserializes a MetaStream object of type from a stream, - /// discarding the embedded configuration. + /// discarding the embedded @params. /// /// The target type. It must have a parameterless constructor. /// @@ -332,9 +331,9 @@ public void ResolveSymbols(IEnumerable symbols) /// /// The deserialized object, or if parsing fails. /// - public T? Deserialize(Stream stream) where T : class, new() + public T? Deserialize(Stream stream, Workspace? workspace = null) where T : class, new() { - var result = DeserializeInternal(stream); + var result = DeserializeInternal(stream, workspace); if (result.Asset == null) { @@ -356,9 +355,9 @@ public void ResolveSymbols(IEnumerable symbols) /// The object to serialize. /// Destination file path. The file is overwritten if it exists. /// - /// The controlling format version, class list, and so on. + /// The controlling format version, class list, and so on. /// - public void Serialize(T obj, string fileName, MetaStreamConfiguration config) where T : class, new() + public void Serialize(T obj, string fileName, MetaStreamParams config) where T : class, new() { using FileStream stream = File.Create(fileName); SerializeInternal(obj, stream, config); @@ -372,8 +371,8 @@ public void ResolveSymbols(IEnumerable symbols) /// /// A writable stream. Not disposed by this method. /// - /// The MetaStream configuration to embed in the output. - public void Serialize(T obj, Stream stream, MetaStreamConfiguration config) where T : class, new() + /// The MetaStream @params to embed in the output. + public void Serialize(T obj, Stream stream, MetaStreamParams config) where T : class, new() => SerializeInternal(obj, stream, config); // ------------------------------------------------------------------------- @@ -420,14 +419,14 @@ public Archive LoadArchive(string archivePath, string blowfishKey) /// /// Path to the file to check. /// - /// if the header matches a known ; + /// if the header matches a known ; /// otherwise. /// public bool IsMetaStreamFile(string fileName) { // Use a using block so the file handle is released immediately after the check. using FileStream stream = File.OpenRead(fileName); - return MetaStreamReader.IsValidMetaStream(stream); + return MetaStream.IsValidMetaStream(stream); } /// @@ -438,11 +437,11 @@ public bool IsMetaStreamFile(string fileName) /// A readable, seekable stream. The stream position is restored after the check. /// /// - /// if the header matches a known ; + /// if the header matches a known ; /// otherwise. /// public bool IsMetaStreamFile(Stream stream) - => MetaStreamReader.IsValidMetaStream(stream); + => MetaStream.IsValidMetaStream(stream); /// /// Returns if has an extension @@ -468,7 +467,7 @@ public static bool IsMetaFile(string fileName) /// /// Returns if the stream's header matches a known - /// value. + /// value. /// /// /// A readable, seekable stream. The stream position is restored after the check. @@ -484,8 +483,8 @@ public static bool IsMetaFile(Stream stream) if (stream.Read(header, 0, 4) < 4) return false; - var version = (MetaStreamVersion)BitConverter.ToInt32(header); - return Enum.IsDefined(typeof(MetaStreamVersion), version); + var version = (MetaStreamMagic)BitConverter.ToInt32(header); + return Enum.IsDefined(typeof(MetaStreamMagic), version); } finally { @@ -605,49 +604,55 @@ private void LoadMetaClassDescriptions() ClassRegistry.Register(metaClasses); } - private (T? Asset, MetaStreamConfiguration? Config) DeserializeInternal(Stream stream) + private (T? Asset, MetaStreamParams? Config) DeserializeInternal(Stream stream, Workspace? workspace = null) where T : class, new() { try { var result = new T(); - var reader = new MetaStreamReader(stream); + var metaStream = MetaStream.OpenRead(stream, workspace); - reader.PreSerialize(ref result); - reader.Serialize(ref result); + metaStream.Serialize(ref result); - MetaStreamConfiguration config = reader.Configuration; + MetaStreamParams config = metaStream.Params; - Config.Logger.LogInfo( - $"[Toolkit] Successfully read {typeof(T).Name}, version: {config.Version}, symbols: {config.SerializedSymbols.Count}"); + if (metaStream.IsEndOfStream()) + { + Config.Logger.LogInfo( + $"[Toolkit] Successfully read {typeof(T).Name}, stream version: {config.StreamVersion}."); + } + else + { + Config.Logger.LogWarning($"[Toolkit] Unexpected end of stream while reading {typeof(T).Name}."); + } if (Config.AutoResolveSymbols) { ResolveSymbols(config.SerializedSymbols); } + metaStream.Close(); return (result, config); } catch (Exception ex) { // Internal method only logs debug details, doesn't warn/error - Config.Logger.LogError($"[Toolkit] Deserialization internal error: {ex.Message}"); + Config.Logger.LogError($"[Toolkit] Deserialization internal error: {ex.Message} {ex.StackTrace}"); return (null, null); } } - private void SerializeInternal(T obj, Stream stream, MetaStreamConfiguration config) + private void SerializeInternal(T obj, Stream stream, MetaStreamParams config) where T : class, new() { - var writer = new MetaStreamWriter(stream, config); + var metaStream = MetaStream.OpenWrite(stream, config); T refObj = obj; - writer.PreSerialize(ref refObj); - writer.Serialize(ref refObj); - writer.Save(); + metaStream.Serialize(ref refObj); + metaStream.Close(); } /// - /// Configuration supplied to . + /// Params supplied to . /// public class Configuration { @@ -660,7 +665,7 @@ public class Configuration /// /// Gets or sets a value indicating whether symbols found in the - /// table are automatically + /// table are automatically /// resolved against the global hash database after every successful /// call. /// diff --git a/src/TelltaleToolKit/Utility/Blowfish/Blowfish.cs b/src/TelltaleToolKit/Utility/Blowfish/Blowfish.cs index 679ed21..444a727 100644 --- a/src/TelltaleToolKit/Utility/Blowfish/Blowfish.cs +++ b/src/TelltaleToolKit/Utility/Blowfish/Blowfish.cs @@ -4,31 +4,32 @@ namespace TelltaleToolKit.Utility.Blowfish; /// -/// Class that provides 'Blowfish' encryption. -/// There are two versions: -/// - The regular Blowfish encryption which is used in older games until ttarch version 6.
-/// - The modified Blowfish encryption which is used in all new games. +/// Class that provides 'Blowfish' encryption. +/// There are two versions: +/// - The regular Blowfish encryption, which is used in older games until ttarch version 6.
+/// - The modified Blowfish encryption, which is used in all new games. ///
public class Blowfish { private const int N = 16; + private readonly bool _isModifiedBlowfishEncryption; private readonly uint[] _p; private readonly uint[,] _s; - private readonly bool _isModifiedBlowfishEncryption; /// - /// Constructs and initializes a blowfish instance with the supplied key. + /// Constructs and initializes a blowfish instance with the supplied key. /// /// The key to cipher with. - /// - public Blowfish(Span key, int version) + /// If true, uses the modified Blowfish encryption (version >= 7). + public Blowfish(Span key, bool isModified) { - _isModifiedBlowfishEncryption = version >= 7; + _isModifiedBlowfishEncryption = isModified; _p = BlowfishConstants.P.Clone() as uint[] ?? throw new InvalidOperationException(); _s = BlowfishConstants.S.Clone() as uint[,] ?? throw new InvalidOperationException(); + // Key expansion for (short i = 0, j = 0; i < N + 2; ++i) { uint data = 0x00000000; @@ -46,9 +47,7 @@ public Blowfish(Span key, int version) _s[0, 118] = BinaryPrimitives.ReverseEndianness(_s[0, 118]); } - uint dataLeft = 0x00000000; - uint dataRight = 0x00000000; - + uint dataLeft = 0, dataRight = 0; for (short i = 0; i < N + 2; i += 2) { Encipher(ref dataLeft, ref dataRight); @@ -57,41 +56,57 @@ public Blowfish(Span key, int version) } for (short i = 0; i < 4; ++i) + for (short j = 0; j < 256; j += 2) { - for (short j = 0; j < 256; j += 2) - { - Encipher(ref dataLeft, ref dataRight); - - _s[i, j] = dataLeft; - _s[i, j + 1] = dataRight; - } + Encipher(ref dataLeft, ref dataRight); + _s[i, j] = dataLeft; + _s[i, j + 1] = dataRight; } } /// - /// Constructs and initializes a blowfish instance with the supplied key. + /// Constructs and initializes a blowfish instance with the supplied key. + /// + /// The key to cipher with. + /// Version number; version >= 7 enables modified encryption. + public Blowfish(Span key, int version) + : this(key, version >= 7) + { + } + + /// + /// Constructs and initializes a blowfish instance with the supplied key. /// /// The key to cipher with. - /// + /// If true, uses the modified Blowfish encryption (version >= 7). + public Blowfish(string key, bool isModified) + : this(Encoding.GetEncoding("ISO-8859-1").GetBytes(key), isModified) + { + } + + /// + /// Constructs and initializes a blowfish instance with the supplied key. + /// + /// The key to cipher with. + /// Version number; version >= 7 enables modified encryption. public Blowfish(string key, int version) - : this(Encoding.GetEncoding("ISO-8859-1").GetBytes(key), version) + : this(key, version >= 7) { } /// - /// /// /// /// private uint F(uint x) { - var d = (ushort)(x & 0x00FF); + ushort d = (ushort)(x & 0x00FF); x >>= 8; - var c = (ushort)(x & 0x00FF); + ushort c = (ushort)(x & 0x00FF); x >>= 8; - var b = (ushort)(x & 0x00FF); + ushort b = (ushort)(x & 0x00FF); x >>= 8; - var a = (ushort)(x & 0x00FF); + ushort a = (ushort)(x & 0x00FF); // y = ((S[0][a] + S[1][b]) ^ S[2][c]) + S[3][d]; uint y = _s[0, a] + _s[1, b]; y ^= _s[2, c]; @@ -101,7 +116,7 @@ private uint F(uint x) } /// - /// Encrypts a byte array in place. + /// Encrypts a byte array in place. /// /// The array to encrypt. /// The amount to encrypt. @@ -109,10 +124,10 @@ public void Encipher(Span data, int length) { int fullBlocks = length - length % 8; // Ensure only complete blocks are processed - for (var i = 0; i < fullBlocks; i += 8) + for (int i = 0; i < fullBlocks; i += 8) { - var xl = (uint)((data[i + 3] << 24) | (data[i + 2] << 16) | (data[i + 1] << 8) | data[i]); - var xr = (uint)((data[i + 7] << 24) | (data[i + 6] << 16) | (data[i + 5] << 8) | data[i + 4]); + uint xl = (uint)((data[i + 3] << 24) | (data[i + 2] << 16) | (data[i + 1] << 8) | data[i]); + uint xr = (uint)((data[i + 7] << 24) | (data[i + 6] << 16) | (data[i + 5] << 8) | data[i + 4]); if (_isModifiedBlowfishEncryption) { @@ -123,19 +138,19 @@ public void Encipher(Span data, int length) Encipher(ref xl, ref xr); } - data[i] = (byte)(xl >> 24); - data[i + 1] = (byte)(xl >> 16); - data[i + 2] = (byte)(xl >> 8); - data[i + 3] = (byte)(xl); - data[i + 4] = (byte)(xr >> 24); - data[i + 5] = (byte)(xr >> 16); - data[i + 6] = (byte)(xr >> 8); - data[i + 7] = (byte)(xr); + data[i + 3] = (byte)(xl >> 24); + data[i + 2] = (byte)(xl >> 16); + data[i + 1] = (byte)(xl >> 8); + data[i + 0] = (byte)xl; + data[i + 7] = (byte)(xr >> 24); + data[i + 6] = (byte)(xr >> 16); + data[i + 5] = (byte)(xr >> 8); + data[i + 4] = (byte)xr; } } /// - /// Encrypts 8 bytes of data (1 block) + /// Encrypts 8 bytes of data (1 block) /// /// The left part of the 8 bytes. /// The right part of the 8 bytes. @@ -162,7 +177,7 @@ private void Encipher(ref uint xl, ref uint xr) } /// - /// Encrypts 8 bytes of data (1 block) + /// Encrypts 8 bytes of data (1 block) /// /// The left part of the 8 bytes. /// The right part of the 8 bytes. @@ -199,7 +214,7 @@ private void Encipher7(ref uint xl, ref uint xr) } /// - /// Encrypts a string. + /// Encrypts a string. /// /// The string to encrypt /// Encrypted string @@ -212,7 +227,7 @@ public string Encipher(string data) } /// - /// Decrypts a byte array in place. + /// Decrypts a byte array in place. /// /// The array to decrypt. /// The amount to decrypt. @@ -222,10 +237,10 @@ public void Decipher(Span data, int length) // Ensure only complete blocks are processed int fullBlocks = length - length % 8; - for (var i = 0; i < fullBlocks; i += 8) + for (int i = 0; i < fullBlocks; i += 8) { - var xl = (uint)((data[i + 3] << 24) | (data[i + 2] << 16) | (data[i + 1] << 8) | data[i]); - var xr = (uint)((data[i + 7] << 24) | (data[i + 6] << 16) | (data[i + 5] << 8) | data[i + 4]); + uint xl = (uint)((data[i + 3] << 24) | (data[i + 2] << 16) | (data[i + 1] << 8) | data[i]); + uint xr = (uint)((data[i + 7] << 24) | (data[i + 6] << 16) | (data[i + 5] << 8) | data[i + 4]); Decipher(ref xl, ref xr); data[i + 3] = (byte)(xl >> 24); @@ -240,7 +255,7 @@ public void Decipher(Span data, int length) } /// - /// Decrypts 8 bytes of data (1 block) + /// Decrypts 8 bytes of data (1 block) /// /// The left part of the 8 bytes. /// The right part of the 8 bytes. @@ -283,4 +298,4 @@ private void Decipher(ref uint xl, ref uint xr) (xl, xr) = (xlCopy, xrCopy); } -} \ No newline at end of file +} diff --git a/src/TelltaleToolKit/TelltaleArchives/Caching/ChunkCache.cs b/src/TelltaleToolKit/Utility/Caching/ChunkCache.cs similarity index 95% rename from src/TelltaleToolKit/TelltaleArchives/Caching/ChunkCache.cs rename to src/TelltaleToolKit/Utility/Caching/ChunkCache.cs index ffb1546..e1ed7e4 100644 --- a/src/TelltaleToolKit/TelltaleArchives/Caching/ChunkCache.cs +++ b/src/TelltaleToolKit/Utility/Caching/ChunkCache.cs @@ -1,4 +1,4 @@ -namespace TelltaleToolKit.TelltaleArchives.Caching; +namespace TelltaleToolKit.Utility.Caching; /// /// Defines the contract for a chunk-level cache. diff --git a/src/TelltaleToolKit/TelltaleArchives/Caching/LruChunkCache.cs b/src/TelltaleToolKit/Utility/Caching/LruChunkCache.cs similarity index 91% rename from src/TelltaleToolKit/TelltaleArchives/Caching/LruChunkCache.cs rename to src/TelltaleToolKit/Utility/Caching/LruChunkCache.cs index 5785a1d..b3c077e 100644 --- a/src/TelltaleToolKit/TelltaleArchives/Caching/LruChunkCache.cs +++ b/src/TelltaleToolKit/Utility/Caching/LruChunkCache.cs @@ -1,11 +1,11 @@ -namespace TelltaleToolKit.TelltaleArchives.Caching; +namespace TelltaleToolKit.Utility.Caching; /// /// Cache strategy that retains the N most-recently-used decoded chunks. /// /// /// -/// Memory overhead: up to N × ChunkSize bytes (e.g., N=8, ChunkSize=64 KiB → 512 KiB). +/// Memory overhead: up to N * WindowSize bytes (e.g., N=8, WindowSize=64 KiB -> 512 KiB). /// /// /// Not thread-safe. Each concurrent extraction path should use its own instance. @@ -25,7 +25,7 @@ public sealed class LruChunkCache : IChunkCache /// /// Maximum number of decoded chunks to retain simultaneously. /// Typical values: 4 (conservative), 8 (balanced), 16 (aggressive). - /// Must be ≥ 1. + /// Must be >= 1. /// public LruChunkCache(int capacity = 8) => // ArgumentOutOfRangeException.ThrowIfLessThan(capacity, 1); diff --git a/src/TelltaleToolKit/TelltaleArchives/Caching/SinglePageCache.cs b/src/TelltaleToolKit/Utility/Caching/SinglePageCache.cs similarity index 95% rename from src/TelltaleToolKit/TelltaleArchives/Caching/SinglePageCache.cs rename to src/TelltaleToolKit/Utility/Caching/SinglePageCache.cs index 9c6a0e8..9d9e72f 100644 --- a/src/TelltaleToolKit/TelltaleArchives/Caching/SinglePageCache.cs +++ b/src/TelltaleToolKit/Utility/Caching/SinglePageCache.cs @@ -1,4 +1,4 @@ -namespace TelltaleToolKit.TelltaleArchives.Caching; +namespace TelltaleToolKit.Utility.Caching; /// /// Cache strategy that keeps exactly one decoded chunk in memory at a time. diff --git a/src/TelltaleToolKit/TelltaleArchives/IO/ChunkDecoder.cs b/src/TelltaleToolKit/Utility/Compression/ChunkDecoder.cs similarity index 74% rename from src/TelltaleToolKit/TelltaleArchives/IO/ChunkDecoder.cs rename to src/TelltaleToolKit/Utility/Compression/ChunkDecoder.cs index c2fce78..10bd278 100644 --- a/src/TelltaleToolKit/TelltaleArchives/IO/ChunkDecoder.cs +++ b/src/TelltaleToolKit/Utility/Compression/ChunkDecoder.cs @@ -1,43 +1,22 @@ using System.IO.Compression; using ICSharpCode.SharpZipLib.Zip.Compression.Streams; -using TelltaleToolKit.TelltaleArchives.Caching; -using TelltaleToolKit.TelltaleArchives.Formats; -using TelltaleToolKit.Utility.Blowfish; +using TelltaleToolKit.Serialization; +using TelltaleToolKit.TelltaleArchives; -namespace TelltaleToolKit.TelltaleArchives.IO; +namespace TelltaleToolKit.Utility.Compression; /// -/// Provides chunk-level decrypt + decompress logic shared by both -/// and extraction paths. +/// Provides chunk-level decrypt + decompress logic. /// /// /// /// All methods are thread-safe with respect to the archive stream as long as each -/// concurrent caller holds its own instance and does not +/// concurrent caller holds its own instance and does not /// share a reference. /// /// internal static class ChunkDecoder { - /// - /// Decrypts (if encrypted) and decompresses a raw compressed chunk read from disk. - /// Returns the decoded byte array. - /// - public static byte[] DecryptAndDecompress(byte[] compressed, uint expectedSize, ArchiveInfo info) - { - // Decrypt in place when the archive uses chunk-level encryption. - if (info.Flags.IsEncrypted()) - { - Blowfish bf = new(info.BlowfishKey, 7); - bf.Decipher(compressed.AsSpan(), compressed.Length); - } - - ArchiveFlags flags = info.Flags; - byte[] arr = DecompressBlock(compressed, (int)expectedSize, ref flags); - info.Flags = flags; - return arr; - } - // ------------------------------------------------------------------------- // Build a block-offset table for the range [blockStart, blockEnd] // ------------------------------------------------------------------------- @@ -80,7 +59,7 @@ public static byte[] DecompressBlock(byte[] compressedData, int expectedSize, re // According to TTG Tools, version 8 archives use zlib (presumably for the files themselves) if (flags.HasFlag(ArchiveFlags.IsRawDeflateCompressed) && flags.HasFlag(ArchiveFlags.IsZlibCompressed)) { - // CompressionAlgorithm compressionAlgorithm = DetectCompressionAlgorithm(compressedData); + // Compression compressionAlgorithm = DetectCompressionAlgorithm(compressedData); try { @@ -135,4 +114,37 @@ byte[] Decompress(Func streamFactory) return outputStream.ToArray(); } } + + private static Compression DetectCompressionAlgorithm(byte[] chunk) + { + // Zlib header: first byte 0x78, second byte in {0x9C, 0xDA, 0x01, 0x5E, 0x9E, ...} + if (chunk[0] == 0x78 && (chunk[1] & 0xF0) == 0xC0) // 0x78 0x9C, 0x78 0xDA, etc. + { + return Compression.Zlib; + } + + return Compression.Deflate; // raw Deflate has no header + } + + /// + /// Compresses a single chunk using the specified algorithm. + /// + /// Number of compressed bytes written into . + public static int CompressBlock(ReadOnlySpan input, Compression algorithm, byte[] outputBuffer) + { + using MemoryStream ms = new(outputBuffer, true); + if (algorithm == Compression.Zlib) + { + using DeflaterOutputStream zlib = new(ms); + zlib.Write(input); + zlib.Finish(); + } + else // Deflate (raw) + { + using DeflateStream deflate = new(ms, CompressionLevel.Optimal, true); + deflate.Write(input); + } + + return (int)ms.Position; + } } diff --git a/src/TelltaleToolKit/Utility/Compression/Compression.cs b/src/TelltaleToolKit/Utility/Compression/Compression.cs new file mode 100644 index 0000000..4b1b516 --- /dev/null +++ b/src/TelltaleToolKit/Utility/Compression/Compression.cs @@ -0,0 +1,19 @@ +namespace TelltaleToolKit.Utility.Compression; + +/// Compression algorithm to use when writing archive chunks. +public enum Compression +{ + /// No compression. + None, + + /// Raw DEFLATE (no zlib header). Used by most Telltale games. + Deflate, + + /// Zlib-wrapped DEFLATE. Used by some CSI and earlier titles. + Zlib, + + /// + /// Oodle compression. + /// + Oodle +} diff --git a/src/TelltaleToolKit/Utility/Lua/LuaVersion.cs b/src/TelltaleToolKit/Utility/Lua/LuaVersion.cs index 3f5e03e..acc2931 100644 --- a/src/TelltaleToolKit/Utility/Lua/LuaVersion.cs +++ b/src/TelltaleToolKit/Utility/Lua/LuaVersion.cs @@ -1,4 +1,4 @@ -namespace TelltaleToolKit.GamesDatabase; +namespace TelltaleToolKit.Utility.Lua; /// /// Specifies the supported Lua scripting engine versions for Telltale games. @@ -19,4 +19,4 @@ public enum LuaVersion /// Lua version 5.2.3. /// Lua523, -} \ No newline at end of file +} diff --git a/src/TelltaleToolKit/Utility/Lua/LuaVersionExtensions.cs b/src/TelltaleToolKit/Utility/Lua/LuaVersionExtensions.cs index 2804b2c..0078c77 100644 --- a/src/TelltaleToolKit/Utility/Lua/LuaVersionExtensions.cs +++ b/src/TelltaleToolKit/Utility/Lua/LuaVersionExtensions.cs @@ -1,4 +1,4 @@ -namespace TelltaleToolKit.GamesDatabase; +namespace TelltaleToolKit.Utility.Lua; /// /// Extension methods for to handle conversions between string representations and enum values. @@ -38,4 +38,4 @@ public static string ToVersionString(this LuaVersion version) _ => throw new ArgumentOutOfRangeException(nameof(version), version, "Unsupported LuaVersion enum value.") }; } -} \ No newline at end of file +} diff --git a/src/TelltaleToolKit/Workspace.cs b/src/TelltaleToolKit/Workspace.cs index bf196e0..a398237 100644 --- a/src/TelltaleToolKit/Workspace.cs +++ b/src/TelltaleToolKit/Workspace.cs @@ -3,7 +3,7 @@ using TelltaleToolKit.GamesDatabase; using TelltaleToolKit.Reflection; using TelltaleToolKit.Resource; -using TelltaleToolKit.Serialization.Binary; +using TelltaleToolKit.Serialization; using TelltaleToolKit.T3Types; using TelltaleToolKit.TelltaleArchives; using TelltaleToolKit.Utility.Blowfish; @@ -62,10 +62,12 @@ internal Workspace(string name, Toolkit toolkit, GameProfile profile) Profile = profile ?? throw new ArgumentNullException(nameof(profile)); LocalHashDatabase = new HashDatabase.HashDatabase(); - DefaultMetaStreamConfig = new MetaStreamConfiguration + bool isModifiedBlowfish = Profile.TtarchVersion >= TTArchiveVersion.Seven; + Blowfish = new Blowfish(profile.BlowfishKey, isModifiedBlowfish); + + DefaultMetaStreamConfig = new MetaStreamParams { - AreSymbolsHashed = profile.AreSymbolsHashed, - Version = profile.MetaStreamVersion, + StreamVersion = profile.StreamVersion, Workspace = this, CanModifySerializedClassesList = true }; @@ -81,15 +83,18 @@ internal Workspace(string name, Toolkit toolkit, GameProfile profile) public string GameName => Profile.Name; /// - /// Gets the default for this workspace, + /// Gets the default for this workspace, /// pre-populated from the game profile. /// Passed to serialization helpers unless overridden by the caller. /// - public MetaStreamConfiguration DefaultMetaStreamConfig { get; } + public MetaStreamParams DefaultMetaStreamConfig { get; } /// Gets the blowfish key used to decrypt archives and resource descriptions for this game. public string BlowfishKey => Profile.BlowfishKey; + /// Gets the blowfish instance used to decrypt archives and resource descriptions for this game. + public Blowfish Blowfish { get; } + /// /// Gets the workspace-local hash database used for symbol resolution. /// Entries here complement (and are checked after) the global database in . @@ -534,31 +539,31 @@ public void ClearAllContexts() /// /// Loads and deserializes an asset of type by filename, also - /// returning the embedded in the stream. + /// returning the embedded in the stream. /// /// The target typ. It must have a parameterless constructor. /// The bare filename of the asset. /// - /// A tuple containing the deserialized asset and its configuration, + /// A tuple containing the deserialized asset and its @params, /// or (null, null) if not found. /// - public (T? Asset, MetaStreamConfiguration? MetaConfig) LoadAssetWithConfig(string name) where T : class, new() + public (T? Asset, MetaStreamParams? MetaConfig) LoadAssetWithConfig(string name) where T : class, new() => LoadAssetWithConfig(Crc64.Compute(name)); /// /// Loads and deserializes an asset of type by CRC-64, also - /// returning the embedded in the stream. + /// returning the embedded in the stream. /// /// The target type. It must have a parameterless constructor. /// Pre-computed CRC-64 of the asset filename. /// - /// A tuple containing the deserialized asset and its configuration, + /// A tuple containing the deserialized asset and its @params, /// or (null, null) if not found. /// - public (T? Asset, MetaStreamConfiguration? MetaConfig) LoadAssetWithConfig(ulong crc64) where T : class, new() + public (T? Asset, MetaStreamParams? MetaConfig) LoadAssetWithConfig(ulong crc64) where T : class, new() => LoadAssetInternal(crc64); - private (T? Value, MetaStreamConfiguration? MetaConfig) LoadAssetInternal(ulong crc64) where T : class, new() + private (T? Value, MetaStreamParams? MetaConfig) LoadAssetInternal(ulong crc64) where T : class, new() { for (int i = _contexts.Count - 1; i >= 0; i--) { @@ -573,11 +578,12 @@ public void ClearAllContexts() ? $"[Workspace:{Name}] Found asset '{assetName}' in context: {_contexts[i].Name}" : $"[Workspace:{Name}] Found asset 0x{crc64:X16} in context: {_contexts[i].Name}"); - var asset = _toolkit.DeserializeWithConfig(stream); + var asset = _toolkit.DeserializeWithConfig(stream, this); if (asset.Asset == null) { - _toolkit.Config.Logger.LogWarning($"[Workspace:{Name}] Asset {(assetName ?? $"0x{crc64:X16}")} found but failed to load in context: {_contexts[i].Name}"); + _toolkit.Config.Logger.LogWarning( + $"[Workspace:{Name}] Asset {(assetName ?? $"0x{crc64:X16}")} found but failed to load in context: {_contexts[i].Name}"); return (null, null); } @@ -614,7 +620,7 @@ public void ClearAllContexts() /// /// Serializes to a file on disk using a custom - /// . + /// . /// /// /// Use this overload when you need to override the MetaStream version or class-list @@ -627,8 +633,8 @@ public void ClearAllContexts() /// Destination file path on disk. /// The file is created or overwritten. /// - /// The MetaStream configuration to embed in the output. - public void ExportAsset(T obj, string fileName, MetaStreamConfiguration config) where T : class, new() + /// The MetaStream @params to embed in the output. + public void ExportAsset(T obj, string fileName, MetaStreamParams config) where T : class, new() => _toolkit.Serialize(obj, fileName, config); /// @@ -692,7 +698,7 @@ public bool ContainsFile(string name) => ContainsFile(Crc64.Compute(name)); /// - /// Finds and returns the for the given CRC-64, + /// Finds and returns the for the given CRC-64, /// searching enabled contexts from the highest priority to lowest. /// /// The entry, or if not found. @@ -741,7 +747,7 @@ public bool ContainsFile(string name) /// specific file. /// /// - /// A flat sequence of objects from all enabled contexts. + /// A flat sequence of objects from all enabled contexts. /// public IEnumerable GetAllEntries() => _contexts.Where(c => c.IsEnabled).SelectMany(c => c.GetAllEntries()); @@ -770,7 +776,8 @@ public IEnumerable GetAllEntries() var fileEntry = FindFileEntry(crc64); if (fileEntry?.Name != null) { - _toolkit.Config.Logger.LogInfo($"[Workspace:{Name}] Resolved 0x{crc64:X16} as '{fileEntry.Name}' from archive"); + _toolkit.Config.Logger.LogInfo( + $"[Workspace:{Name}] Resolved 0x{crc64:X16} as '{fileEntry.Name}' from archive"); return fileEntry.Name; } diff --git a/tools/TTKDatabaseGenerator/Program.cs b/tools/TTKDatabaseGenerator/Program.cs index a99ec05..3aa2d28 100644 --- a/tools/TTKDatabaseGenerator/Program.cs +++ b/tools/TTKDatabaseGenerator/Program.cs @@ -8,9 +8,11 @@ using TelltaleToolKit; using TelltaleToolKit.GamesDatabase; using TelltaleToolKit.Reflection; +using TelltaleToolKit.Serialization; using TelltaleToolKit.Serialization.Binary; using TelltaleToolKit.TelltaleArchives; using TelltaleToolKit.Utility.Blowfish; +using TelltaleToolKit.Utility.Lua; namespace DatabaseGenerator; @@ -135,7 +137,7 @@ static async Task Main(string[] args) var unregisteredTypes = new ConcurrentDictionary<(ulong, uint), byte>(); var areSymbolsHashed = true; - var msv = MetaStreamVersion.Mtre; + uint msv = 3; TTArchiveVersion ttarchVersion = 0; var processed = 0; @@ -162,9 +164,8 @@ await Parallel.ForEachAsync(archivePaths, parallelOptions, async (filePath, _) = if (Toolkit.IsMetaFile(archive.GetAllEntries().First().Name)) { - MetaStreamConfiguration config = new MetaStreamReader(firstFile).Configuration; - areSymbolsHashed = config.AreSymbolsHashed; - msv = config.Version; + MetaStreamParams config = new BinaryMetaStreamReader(firstFile, null).Params; + msv = config.StreamVersion; } } @@ -175,22 +176,25 @@ await Parallel.ForEachAsync(archivePaths, parallelOptions, async (filePath, _) = if (!Toolkit.IsMetaFile(file)) continue; - MetaStreamConfiguration config = new MetaStreamReader(file).Configuration; + MetaStreamParams config = new BinaryMetaStreamReader(file, null).Params; - foreach (MetaClass desc in config.SerializedClasses) + foreach (MetaClass desc in config.GetRegisteredClasses()) serializedMetaClasses.TryAdd(desc, 0); - foreach ((ulong, uint) t in config.UnregisteredTypes) + var localUnregisteredTypes = config.GetUnregisteredTypes(); + var localUnregisteredClasses = config.GetUnregisteredClasses(); + + foreach ((ulong, uint) t in localUnregisteredTypes) unregisteredTypes.TryAdd(t, 0); - foreach ((MetaClassType, uint crc32) c in config.UnregisteredClasses) + foreach ((MetaClassType, uint crc32) c in localUnregisteredClasses) unrecognizedMeta.TryAdd(c, 0); - if (config.UnregisteredTypes.Count > 0) - PrintWarning($"{entry.Name} -> {config.UnregisteredTypes.Count} unknown types", logWriter); + if (localUnregisteredTypes.Count > 0) + PrintWarning($"{entry.Name} -> {localUnregisteredTypes.Count} unknown types", logWriter); - if (config.UnregisteredClasses.Count > 0) - PrintWarning($"{entry.Name} -> {config.UnregisteredClasses.Count} unknown classes", + if (localUnregisteredClasses.Count > 0) + PrintWarning($"{entry.Name} -> {localUnregisteredClasses.Count} unknown classes", logWriter); } } @@ -282,12 +286,11 @@ await Parallel.ForEachAsync(archivePaths, parallelOptions, async (filePath, _) = { Id = slug, Name = slug, - AreSymbolsHashed = areSymbolsHashed, BlowfishKey = (key.ToString() ?? customKey) ?? "UNKNOWN", EnableOodleCompression = false, IsTtarch2 = selectedFilter == "*ttarch2", LuaVersion = LuaVersion.Lua512, - MetaStreamVersion = msv, + StreamVersion = msv, TtarchVersion = ttarchVersion, };